sicua
Version:
A tool for analyzing project structure and dependencies
326 lines (325 loc) • 12.8 kB
JavaScript
;
/**
* Detector for hardcoded secrets, API keys, passwords, and tokens
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HardcodedSecretDetector = void 0;
const typescript_1 = __importDefault(require("typescript"));
const BaseDetector_1 = require("./BaseDetector");
const PatternMatcher_1 = require("../utils/PatternMatcher");
const ASTTraverser_1 = require("../utils/ASTTraverser");
const debugging_constants_1 = require("../constants/debugging.constants");
const sensitiveData_constants_1 = require("../constants/sensitiveData.constants");
class HardcodedSecretDetector extends BaseDetector_1.BaseDetector {
constructor() {
super("HardcodedSecretDetector", "hardcoded-secret", "critical", HardcodedSecretDetector.SECRET_PATTERNS);
}
async detect(scanResult) {
const vulnerabilities = [];
// Filter relevant files (exclude node_modules, dist, etc.)
// TODO: MOVE TO CONSTANTS
const relevantFiles = this.filterRelevantFiles(scanResult, [".ts", ".tsx", ".js", ".jsx", ".json"], ["node_modules", "dist", "build", ".git", "coverage"]);
for (const filePath of relevantFiles) {
const content = scanResult.fileContents.get(filePath);
if (!content)
continue;
// Apply pattern matching
const patternResults = this.applyPatternMatching(content, filePath);
const patternVulnerabilities = this.convertPatternMatchesToVulnerabilities(patternResults, (match) => this.validateSecretMatch(match));
// Apply AST-based analysis for more sophisticated detection
const sourceFile = scanResult.sourceFiles.get(filePath);
if (sourceFile) {
const astVulnerabilities = this.applyASTAnalysis(sourceFile, filePath, (sf, fp) => this.analyzeASTForSecrets(sf, fp));
vulnerabilities.push(...astVulnerabilities);
}
// Adjust confidence based on file context
const fileContext = this.getFileContext(filePath, content);
for (const vuln of patternVulnerabilities) {
vuln.confidence = this.adjustConfidenceBasedOnContext(vuln, fileContext);
if (this.validateVulnerability(vuln)) {
vulnerabilities.push(vuln);
}
}
}
return vulnerabilities;
}
/**
* Validate if a pattern match is actually a secret
*/
validateSecretMatch(matchResult) {
const match = matchResult.matches[0];
if (!match)
return false;
const matchedText = match.match;
// Check if it's in a comment
if (this.isInComment(match.context || "", match.match)) {
return false;
}
// Check if it's a placeholder or example
if (this.isPlaceholderValue(matchedText)) {
return false;
}
// Check entropy for potential secrets
const pattern = matchResult.pattern;
if (pattern.entropyThreshold) {
const entropy = PatternMatcher_1.PatternMatcher.calculateEntropy(matchedText);
if (entropy < pattern.entropyThreshold) {
return false;
}
}
// Length validation
if (pattern.minLength && matchedText.length < pattern.minLength) {
return false;
}
if (pattern.maxLength && matchedText.length > pattern.maxLength) {
return false;
}
return true;
}
/**
* AST-based secret detection for more sophisticated analysis
*/
analyzeASTForSecrets(sourceFile, filePath) {
const vulnerabilities = [];
// Find all string literals
const stringLiterals = ASTTraverser_1.ASTTraverser.findNodesByKind(sourceFile, typescript_1.default.SyntaxKind.StringLiteral);
for (const stringLiteral of stringLiterals) {
const text = stringLiteral.text;
// Skip short strings
if (text.length < 16)
continue;
// Check if it looks like a secret
if (this.looksLikeSecret(text, stringLiteral)) {
const location = ASTTraverser_1.ASTTraverser.getNodeLocation(stringLiteral, sourceFile);
const context = ASTTraverser_1.ASTTraverser.getNodeContext(stringLiteral, sourceFile);
const vulnerability = this.createVulnerability(filePath, {
line: location.line,
column: location.column,
endLine: location.line,
endColumn: location.column + text.length,
}, {
code: text,
surroundingContext: context,
functionName: this.extractFunctionFromAST(stringLiteral),
componentName: this.extractComponentName(filePath),
}, "Potential hardcoded secret detected in string literal", "critical", "medium", {
entropy: PatternMatcher_1.PatternMatcher.calculateEntropy(text),
length: text.length,
detectionMethod: "ast-analysis",
});
vulnerabilities.push(vulnerability);
}
}
return vulnerabilities;
}
/**
* Check if a string looks like a secret based on various heuristics
*/
looksLikeSecret(text, node) {
// Calculate entropy
const entropy = PatternMatcher_1.PatternMatcher.calculateEntropy(text);
if (entropy < 4.0)
return false;
// Check if it has mixed character types
if (!PatternMatcher_1.PatternMatcher.isPotentialSecret(text, 4.0, 16)) {
return false;
}
// Check the variable name or property name
const variableName = this.getVariableNameForStringLiteral(node);
if (variableName && this.isSensitiveVariableName(variableName)) {
return true;
}
// Check if it's assigned to a sensitive property
const parent = node.parent;
if (typescript_1.default.isPropertyAssignment(parent) && typescript_1.default.isIdentifier(parent.name)) {
const propertyName = parent.name.text;
if (this.isSensitiveVariableName(propertyName)) {
return true;
}
}
return false;
}
/**
* Check if a variable name suggests it contains sensitive data
*/
isSensitiveVariableName(name) {
const lowerName = name.toLowerCase();
return sensitiveData_constants_1.SENSITIVE_VARIABLE_NAMES.some((sensitive) => lowerName.includes(sensitive) ||
lowerName.replace(/[_-]/g, "").includes(sensitive.replace(/[_-]/g, "")));
}
/**
* Get variable name for a string literal
*/
getVariableNameForStringLiteral(node) {
let parent = node.parent;
// Check if it's a variable declaration
if (typescript_1.default.isVariableDeclaration(parent) && typescript_1.default.isIdentifier(parent.name)) {
return parent.name.text;
}
// Check if it's a property assignment
if (typescript_1.default.isPropertyAssignment(parent) && typescript_1.default.isIdentifier(parent.name)) {
return parent.name.text;
}
return undefined;
}
/**
* Fixed extractFunctionFromAST function
*/
extractFunctionFromAST(node) {
let current = node.parent;
while (current) {
if (typescript_1.default.isFunctionDeclaration(current) && current.name) {
return current.name.text;
}
if (typescript_1.default.isMethodDeclaration(current) && typescript_1.default.isIdentifier(current.name)) {
return current.name.text;
}
if (typescript_1.default.isVariableDeclaration(current) &&
typescript_1.default.isIdentifier(current.name) &&
current.initializer &&
(typescript_1.default.isFunctionExpression(current.initializer) ||
typescript_1.default.isArrowFunction(current.initializer))) {
return current.name.text;
}
current = current.parent;
}
return undefined;
}
/**
* Check if value is a placeholder or example
*/
isPlaceholderValue(value) {
return debugging_constants_1.PLACEHOLDER_PATTERNS.some((pattern) => pattern.test(value));
}
}
exports.HardcodedSecretDetector = HardcodedSecretDetector;
HardcodedSecretDetector.SECRET_PATTERNS = [
{
id: "aws-access-key",
name: "AWS Access Key",
description: "Hardcoded AWS access key detected",
pattern: {
type: "regex",
expression: /AKIA[0-9A-Z]{16}/g,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json"],
enabled: true,
entropyThreshold: 4.5,
minLength: 20,
knownPrefixes: ["AKIA"],
},
{
id: "aws-secret-key",
name: "AWS Secret Key",
description: "Hardcoded AWS secret access key detected",
pattern: {
type: "regex",
expression: /[A-Za-z0-9\/\+=]{40}/g,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "medium",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json"],
enabled: true,
entropyThreshold: 5.0,
minLength: 40,
maxLength: 40,
},
{
id: "github-token",
name: "GitHub Token",
description: "Hardcoded GitHub personal access token detected",
pattern: {
type: "regex",
expression: /ghp_[a-zA-Z0-9]{36}/g,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json"],
enabled: true,
knownPrefixes: ["ghp_"],
},
{
id: "api-key-generic",
name: "Generic API Key",
description: "Hardcoded API key detected",
pattern: {
type: "regex",
expression: /(?:api[_-]?key|apikey|key)\s*[:=]\s*['"]([a-zA-Z0-9\-_]{20,})['"]/gi,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "medium",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json"],
enabled: true,
entropyThreshold: 4.0,
minLength: 20,
},
{
id: "database-url",
name: "Database Connection String",
description: "Hardcoded database connection string detected",
pattern: {
type: "regex",
expression: /(?:mongodb|mysql|postgres|postgresql):\/\/[^\s'"]+/gi,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json"],
enabled: true,
},
{
id: "jwt-secret",
name: "JWT Secret",
description: "Hardcoded JWT secret detected",
pattern: {
type: "regex",
expression: /(?:jwt[_-]?secret|jwtsecret)\s*[:=]\s*['"]([a-zA-Z0-9\-_+=\/]{16,})['"]/gi,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json"],
enabled: true,
entropyThreshold: 4.5,
minLength: 16,
},
{
id: "private-key",
name: "Private Key",
description: "Hardcoded private key detected",
pattern: {
type: "regex",
expression: /-----BEGIN\s+(RSA\s+)?PRIVATE\s+KEY-----/gi,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "high",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json", ".pem", ".key"],
enabled: true,
},
{
id: "password-hardcoded",
name: "Hardcoded Password",
description: "Hardcoded password detected",
pattern: {
type: "regex",
expression: /(?:password|passwd|pwd)\s*[:=]\s*['"](?!.*\$\{)[a-zA-Z0-9\-_!@#$%^&*()+=]{8,}['"]/gi,
},
vulnerabilityType: "hardcoded-secret",
severity: "critical",
confidence: "medium",
fileTypes: [".ts", ".tsx", ".js", ".jsx", ".json"],
enabled: true,
entropyThreshold: 3.5,
minLength: 8,
},
];