mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
524 lines
16 KiB
TypeScript
524 lines
16 KiB
TypeScript
import { ValueObject } from "./ValueObject"
|
|
import { DETECTION_PATTERNS, HARDCODE_TYPES } from "../../shared/constants/rules"
|
|
import {
|
|
API_KEY_CONTEXT_KEYWORDS,
|
|
BASE64_CONTEXT_KEYWORDS,
|
|
COLOR_CONTEXT_KEYWORDS,
|
|
CONFIG_CONTEXT_KEYWORDS,
|
|
CONSTANT_NAMES,
|
|
DATE_CONTEXT_KEYWORDS,
|
|
EMAIL_CONTEXT_KEYWORDS,
|
|
FILE_PATH_CONTEXT_KEYWORDS,
|
|
IP_CONTEXT_KEYWORDS,
|
|
LOCATIONS,
|
|
SUGGESTION_KEYWORDS,
|
|
URL_CONTEXT_KEYWORDS,
|
|
UUID_CONTEXT_KEYWORDS,
|
|
VERSION_CONTEXT_KEYWORDS,
|
|
} from "../constants/Suggestions"
|
|
|
|
export type HardcodeType = (typeof HARDCODE_TYPES)[keyof typeof HARDCODE_TYPES]
|
|
|
|
export type ValueType =
|
|
| "email"
|
|
| "url"
|
|
| "ip_address"
|
|
| "file_path"
|
|
| "date"
|
|
| "api_key"
|
|
| "uuid"
|
|
| "version"
|
|
| "color"
|
|
| "mac_address"
|
|
| "base64"
|
|
| "config"
|
|
| "generic"
|
|
|
|
export type ValueImportance = "critical" | "high" | "medium" | "low"
|
|
|
|
export interface DuplicateLocation {
|
|
file: string
|
|
line: number
|
|
}
|
|
|
|
interface HardcodedValueProps {
|
|
readonly value: string | number | boolean
|
|
readonly type: HardcodeType
|
|
readonly valueType?: ValueType
|
|
readonly line: number
|
|
readonly column: number
|
|
readonly context: string
|
|
readonly duplicateLocations?: DuplicateLocation[]
|
|
readonly withinFileUsageCount?: number
|
|
}
|
|
|
|
/**
|
|
* Represents a hardcoded value found in source code
|
|
*/
|
|
export class HardcodedValue extends ValueObject<HardcodedValueProps> {
|
|
private constructor(props: HardcodedValueProps) {
|
|
super(props)
|
|
}
|
|
|
|
public static create(
|
|
value: string | number | boolean,
|
|
type: HardcodeType,
|
|
line: number,
|
|
column: number,
|
|
context: string,
|
|
valueType?: ValueType,
|
|
duplicateLocations?: DuplicateLocation[],
|
|
withinFileUsageCount?: number,
|
|
): HardcodedValue {
|
|
return new HardcodedValue({
|
|
value,
|
|
type,
|
|
valueType,
|
|
line,
|
|
column,
|
|
context,
|
|
duplicateLocations,
|
|
withinFileUsageCount,
|
|
})
|
|
}
|
|
|
|
public get value(): string | number | boolean {
|
|
return this.props.value
|
|
}
|
|
|
|
public get type(): HardcodeType {
|
|
return this.props.type
|
|
}
|
|
|
|
public get line(): number {
|
|
return this.props.line
|
|
}
|
|
|
|
public get column(): number {
|
|
return this.props.column
|
|
}
|
|
|
|
public get context(): string {
|
|
return this.props.context
|
|
}
|
|
|
|
public get valueType(): ValueType | undefined {
|
|
return this.props.valueType
|
|
}
|
|
|
|
public get duplicateLocations(): DuplicateLocation[] | undefined {
|
|
return this.props.duplicateLocations
|
|
}
|
|
|
|
public get withinFileUsageCount(): number | undefined {
|
|
return this.props.withinFileUsageCount
|
|
}
|
|
|
|
public hasDuplicates(): boolean {
|
|
return (
|
|
this.props.duplicateLocations !== undefined && this.props.duplicateLocations.length > 0
|
|
)
|
|
}
|
|
|
|
public isAlmostConstant(): boolean {
|
|
return this.props.withinFileUsageCount !== undefined && this.props.withinFileUsageCount >= 2
|
|
}
|
|
|
|
public isMagicNumber(): boolean {
|
|
return this.props.type === HARDCODE_TYPES.MAGIC_NUMBER
|
|
}
|
|
|
|
public isMagicString(): boolean {
|
|
return this.props.type === HARDCODE_TYPES.MAGIC_STRING
|
|
}
|
|
|
|
public suggestConstantName(): string {
|
|
if (this.isMagicNumber()) {
|
|
return this.suggestNumberConstantName()
|
|
}
|
|
if (this.isMagicString()) {
|
|
return this.suggestStringConstantName()
|
|
}
|
|
return CONSTANT_NAMES.UNKNOWN_CONSTANT
|
|
}
|
|
|
|
private suggestNumberConstantName(): string {
|
|
const value = this.props.value
|
|
const context = this.props.context.toLowerCase()
|
|
|
|
if (context.includes(SUGGESTION_KEYWORDS.TIMEOUT)) {
|
|
return CONSTANT_NAMES.TIMEOUT_MS
|
|
}
|
|
if (
|
|
context.includes(SUGGESTION_KEYWORDS.RETRY) ||
|
|
context.includes(SUGGESTION_KEYWORDS.ATTEMPT)
|
|
) {
|
|
return CONSTANT_NAMES.MAX_RETRIES
|
|
}
|
|
if (
|
|
context.includes(SUGGESTION_KEYWORDS.LIMIT) ||
|
|
context.includes(SUGGESTION_KEYWORDS.MAX)
|
|
) {
|
|
return CONSTANT_NAMES.MAX_LIMIT
|
|
}
|
|
if (context.includes(SUGGESTION_KEYWORDS.PORT)) {
|
|
return CONSTANT_NAMES.DEFAULT_PORT
|
|
}
|
|
if (context.includes(SUGGESTION_KEYWORDS.DELAY)) {
|
|
return CONSTANT_NAMES.DELAY_MS
|
|
}
|
|
|
|
return `${CONSTANT_NAMES.MAGIC_NUMBER}_${String(value)}`
|
|
}
|
|
|
|
// eslint-disable-next-line complexity, max-lines-per-function
|
|
private suggestStringConstantName(): string {
|
|
const value = String(this.props.value)
|
|
const context = this.props.context.toLowerCase()
|
|
const valueType = this.props.valueType
|
|
|
|
if (valueType === "email") {
|
|
if (context.includes(EMAIL_CONTEXT_KEYWORDS.ADMIN)) {
|
|
return CONSTANT_NAMES.ADMIN_EMAIL
|
|
}
|
|
if (context.includes(EMAIL_CONTEXT_KEYWORDS.SUPPORT)) {
|
|
return CONSTANT_NAMES.SUPPORT_EMAIL
|
|
}
|
|
if (
|
|
context.includes(EMAIL_CONTEXT_KEYWORDS.NOREPLY) ||
|
|
context.includes(EMAIL_CONTEXT_KEYWORDS.NO_REPLY)
|
|
) {
|
|
return CONSTANT_NAMES.NOREPLY_EMAIL
|
|
}
|
|
return CONSTANT_NAMES.DEFAULT_EMAIL
|
|
}
|
|
|
|
if (valueType === "api_key") {
|
|
if (context.includes(API_KEY_CONTEXT_KEYWORDS.SECRET)) {
|
|
return CONSTANT_NAMES.API_SECRET_KEY
|
|
}
|
|
if (context.includes(API_KEY_CONTEXT_KEYWORDS.PUBLIC)) {
|
|
return CONSTANT_NAMES.API_PUBLIC_KEY
|
|
}
|
|
return CONSTANT_NAMES.API_KEY
|
|
}
|
|
|
|
if (valueType === "url") {
|
|
if (context.includes(URL_CONTEXT_KEYWORDS.API)) {
|
|
return CONSTANT_NAMES.API_BASE_URL
|
|
}
|
|
if (
|
|
context.includes(URL_CONTEXT_KEYWORDS.DATABASE) ||
|
|
context.includes(URL_CONTEXT_KEYWORDS.DB)
|
|
) {
|
|
return CONSTANT_NAMES.DATABASE_URL
|
|
}
|
|
if (context.includes(URL_CONTEXT_KEYWORDS.MONGO)) {
|
|
return CONSTANT_NAMES.MONGODB_CONNECTION_STRING
|
|
}
|
|
if (
|
|
context.includes(URL_CONTEXT_KEYWORDS.POSTGRES) ||
|
|
context.includes(URL_CONTEXT_KEYWORDS.PG)
|
|
) {
|
|
return CONSTANT_NAMES.POSTGRES_URL
|
|
}
|
|
return CONSTANT_NAMES.BASE_URL
|
|
}
|
|
|
|
if (valueType === "ip_address") {
|
|
if (context.includes(IP_CONTEXT_KEYWORDS.SERVER)) {
|
|
return CONSTANT_NAMES.SERVER_IP
|
|
}
|
|
if (
|
|
context.includes(URL_CONTEXT_KEYWORDS.DATABASE) ||
|
|
context.includes(URL_CONTEXT_KEYWORDS.DB)
|
|
) {
|
|
return CONSTANT_NAMES.DATABASE_HOST
|
|
}
|
|
if (context.includes(IP_CONTEXT_KEYWORDS.REDIS)) {
|
|
return CONSTANT_NAMES.REDIS_HOST
|
|
}
|
|
return CONSTANT_NAMES.HOST_IP
|
|
}
|
|
|
|
if (valueType === "file_path") {
|
|
if (context.includes(FILE_PATH_CONTEXT_KEYWORDS.LOG)) {
|
|
return CONSTANT_NAMES.LOG_FILE_PATH
|
|
}
|
|
if (context.includes(SUGGESTION_KEYWORDS.CONFIG)) {
|
|
return CONSTANT_NAMES.CONFIG_FILE_PATH
|
|
}
|
|
if (context.includes(FILE_PATH_CONTEXT_KEYWORDS.DATA)) {
|
|
return CONSTANT_NAMES.DATA_DIR_PATH
|
|
}
|
|
if (context.includes(FILE_PATH_CONTEXT_KEYWORDS.TEMP)) {
|
|
return CONSTANT_NAMES.TEMP_DIR_PATH
|
|
}
|
|
return CONSTANT_NAMES.FILE_PATH
|
|
}
|
|
|
|
if (valueType === "date") {
|
|
if (context.includes(DATE_CONTEXT_KEYWORDS.DEADLINE)) {
|
|
return CONSTANT_NAMES.DEADLINE
|
|
}
|
|
if (context.includes(DATE_CONTEXT_KEYWORDS.START)) {
|
|
return CONSTANT_NAMES.START_DATE
|
|
}
|
|
if (context.includes(DATE_CONTEXT_KEYWORDS.END)) {
|
|
return CONSTANT_NAMES.END_DATE
|
|
}
|
|
if (context.includes(DATE_CONTEXT_KEYWORDS.EXPIR)) {
|
|
return CONSTANT_NAMES.EXPIRATION_DATE
|
|
}
|
|
return CONSTANT_NAMES.DEFAULT_DATE
|
|
}
|
|
|
|
if (valueType === "uuid") {
|
|
if (
|
|
context.includes(UUID_CONTEXT_KEYWORDS.ID) ||
|
|
context.includes(UUID_CONTEXT_KEYWORDS.IDENTIFIER)
|
|
) {
|
|
return CONSTANT_NAMES.DEFAULT_ID
|
|
}
|
|
if (context.includes(UUID_CONTEXT_KEYWORDS.REQUEST)) {
|
|
return CONSTANT_NAMES.REQUEST_ID
|
|
}
|
|
if (context.includes(UUID_CONTEXT_KEYWORDS.SESSION)) {
|
|
return CONSTANT_NAMES.SESSION_ID
|
|
}
|
|
return CONSTANT_NAMES.UUID_CONSTANT
|
|
}
|
|
|
|
if (valueType === "version") {
|
|
if (context.includes(URL_CONTEXT_KEYWORDS.API)) {
|
|
return CONSTANT_NAMES.API_VERSION
|
|
}
|
|
if (context.includes(VERSION_CONTEXT_KEYWORDS.APP)) {
|
|
return CONSTANT_NAMES.APP_VERSION
|
|
}
|
|
return CONSTANT_NAMES.VERSION
|
|
}
|
|
|
|
if (valueType === "color") {
|
|
if (context.includes(COLOR_CONTEXT_KEYWORDS.PRIMARY)) {
|
|
return CONSTANT_NAMES.PRIMARY_COLOR
|
|
}
|
|
if (context.includes(COLOR_CONTEXT_KEYWORDS.SECONDARY)) {
|
|
return CONSTANT_NAMES.SECONDARY_COLOR
|
|
}
|
|
if (context.includes(COLOR_CONTEXT_KEYWORDS.BACKGROUND)) {
|
|
return CONSTANT_NAMES.BACKGROUND_COLOR
|
|
}
|
|
return CONSTANT_NAMES.COLOR_CONSTANT
|
|
}
|
|
|
|
if (valueType === "mac_address") {
|
|
return CONSTANT_NAMES.MAC_ADDRESS
|
|
}
|
|
|
|
if (valueType === "base64") {
|
|
if (context.includes(BASE64_CONTEXT_KEYWORDS.TOKEN)) {
|
|
return CONSTANT_NAMES.ENCODED_TOKEN
|
|
}
|
|
if (context.includes(BASE64_CONTEXT_KEYWORDS.KEY)) {
|
|
return CONSTANT_NAMES.ENCODED_KEY
|
|
}
|
|
return CONSTANT_NAMES.BASE64_VALUE
|
|
}
|
|
|
|
if (valueType === "config") {
|
|
if (context.includes(CONFIG_CONTEXT_KEYWORDS.ENDPOINT)) {
|
|
return CONSTANT_NAMES.API_ENDPOINT
|
|
}
|
|
if (context.includes(CONFIG_CONTEXT_KEYWORDS.ROUTE)) {
|
|
return CONSTANT_NAMES.ROUTE_PATH
|
|
}
|
|
if (context.includes(CONFIG_CONTEXT_KEYWORDS.CONNECTION)) {
|
|
return CONSTANT_NAMES.CONNECTION_STRING
|
|
}
|
|
return CONSTANT_NAMES.CONFIG_VALUE
|
|
}
|
|
|
|
if (value.includes(SUGGESTION_KEYWORDS.HTTP)) {
|
|
return CONSTANT_NAMES.API_BASE_URL
|
|
}
|
|
if (value.includes(".") && !value.includes(" ")) {
|
|
if (value.includes("/")) {
|
|
return CONSTANT_NAMES.DEFAULT_PATH
|
|
}
|
|
return CONSTANT_NAMES.DEFAULT_DOMAIN
|
|
}
|
|
if (
|
|
context.includes(SUGGESTION_KEYWORDS.ERROR) ||
|
|
context.includes(SUGGESTION_KEYWORDS.MESSAGE)
|
|
) {
|
|
return CONSTANT_NAMES.ERROR_MESSAGE
|
|
}
|
|
if (context.includes(SUGGESTION_KEYWORDS.DEFAULT)) {
|
|
return CONSTANT_NAMES.DEFAULT_VALUE
|
|
}
|
|
|
|
return CONSTANT_NAMES.MAGIC_STRING
|
|
}
|
|
|
|
public suggestLocation(currentLayer?: string): string {
|
|
if (!currentLayer) {
|
|
return LOCATIONS.SHARED_CONSTANTS
|
|
}
|
|
|
|
const context = this.props.context.toLowerCase()
|
|
const valueType = this.props.valueType
|
|
|
|
if (valueType === "api_key" || valueType === "url" || valueType === "ip_address") {
|
|
return LOCATIONS.CONFIG_ENVIRONMENT
|
|
}
|
|
|
|
if (valueType === "email") {
|
|
return LOCATIONS.CONFIG_CONTACTS
|
|
}
|
|
|
|
if (valueType === "file_path") {
|
|
return LOCATIONS.CONFIG_PATHS
|
|
}
|
|
|
|
if (valueType === "date") {
|
|
return LOCATIONS.CONFIG_DATES
|
|
}
|
|
|
|
if (
|
|
context.includes(SUGGESTION_KEYWORDS.ENTITY) ||
|
|
context.includes(SUGGESTION_KEYWORDS.AGGREGATE) ||
|
|
context.includes(SUGGESTION_KEYWORDS.DOMAIN)
|
|
) {
|
|
return currentLayer ? `${currentLayer}/constants` : LOCATIONS.DOMAIN_CONSTANTS
|
|
}
|
|
|
|
if (
|
|
context.includes(SUGGESTION_KEYWORDS.CONFIG) ||
|
|
context.includes(SUGGESTION_KEYWORDS.ENV)
|
|
) {
|
|
return LOCATIONS.INFRASTRUCTURE_CONFIG
|
|
}
|
|
|
|
return LOCATIONS.SHARED_CONSTANTS
|
|
}
|
|
|
|
public getDetailedSuggestion(currentLayer?: string): string {
|
|
const constantName = this.suggestConstantName()
|
|
const location = this.suggestLocation(currentLayer)
|
|
const valueTypeLabel = this.valueType ? ` (${this.valueType})` : ""
|
|
|
|
let suggestion = `Extract${valueTypeLabel} to constant ${constantName} in ${location}`
|
|
|
|
if (this.isAlmostConstant() && this.withinFileUsageCount) {
|
|
suggestion += `. This value appears ${String(this.withinFileUsageCount)} times in this file`
|
|
}
|
|
|
|
if (this.hasDuplicates() && this.duplicateLocations) {
|
|
const count = this.duplicateLocations.length
|
|
const fileList = this.duplicateLocations
|
|
.slice(0, 3)
|
|
.map((loc) => `${loc.file}:${String(loc.line)}`)
|
|
.join(", ")
|
|
|
|
const more = count > 3 ? ` and ${String(count - 3)} more` : ""
|
|
suggestion += `. Also duplicated in ${String(count)} other file(s): ${fileList}${more}`
|
|
}
|
|
|
|
return suggestion
|
|
}
|
|
|
|
/**
|
|
* Analyzes variable name and context to determine importance
|
|
*/
|
|
public getImportance(): ValueImportance {
|
|
const context = this.props.context.toLowerCase()
|
|
const valueType = this.props.valueType
|
|
|
|
if (valueType === "api_key") {
|
|
return "critical"
|
|
}
|
|
|
|
const criticalKeywords = [
|
|
...DETECTION_PATTERNS.SENSITIVE_KEYWORDS,
|
|
...DETECTION_PATTERNS.BUSINESS_KEYWORDS,
|
|
"key",
|
|
"age",
|
|
]
|
|
|
|
if (criticalKeywords.some((keyword) => context.includes(keyword))) {
|
|
return "critical"
|
|
}
|
|
|
|
const highKeywords = [...DETECTION_PATTERNS.TECHNICAL_KEYWORDS, "db", "api"]
|
|
|
|
if (highKeywords.some((keyword) => context.includes(keyword))) {
|
|
return "high"
|
|
}
|
|
|
|
if (valueType === "url" || valueType === "ip_address" || valueType === "email") {
|
|
return "high"
|
|
}
|
|
|
|
const mediumKeywords = DETECTION_PATTERNS.MEDIUM_KEYWORDS
|
|
|
|
if (mediumKeywords.some((keyword) => context.includes(keyword))) {
|
|
return "medium"
|
|
}
|
|
|
|
const lowKeywords = DETECTION_PATTERNS.UI_KEYWORDS
|
|
|
|
if (lowKeywords.some((keyword) => context.includes(keyword))) {
|
|
return "low"
|
|
}
|
|
|
|
return "medium"
|
|
}
|
|
|
|
/**
|
|
* Checks if this violation should be skipped based on layer strictness
|
|
*
|
|
* Different layers have different tolerance levels:
|
|
* - domain: strictest (no hardcoded values allowed)
|
|
* - application: strict (only low importance allowed)
|
|
* - infrastructure: moderate (low and some medium allowed)
|
|
* - cli: lenient (UI constants allowed)
|
|
*/
|
|
public shouldSkip(layer?: string): boolean {
|
|
if (!layer) {
|
|
return false
|
|
}
|
|
|
|
const importance = this.getImportance()
|
|
|
|
if (layer === "domain") {
|
|
return false
|
|
}
|
|
|
|
if (layer === "application") {
|
|
return false
|
|
}
|
|
|
|
if (layer === "infrastructure") {
|
|
return importance === "low" && this.isUIConstant()
|
|
}
|
|
|
|
if (layer === "cli") {
|
|
return importance === "low" && this.isUIConstant()
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Checks if this value is a UI-related constant
|
|
*/
|
|
private isUIConstant(): boolean {
|
|
const context = this.props.context.toLowerCase()
|
|
|
|
const uiKeywords = DETECTION_PATTERNS.UI_KEYWORDS
|
|
|
|
return uiKeywords.some((keyword) => context.includes(keyword))
|
|
}
|
|
}
|