mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-28 07:16:53 +05:00
- Replace regex-based matchers with tree-sitter AST traversal - Add duplicate value tracking across files - Implement boolean literal detection - Add value type classification (email, url, ip, api_key, etc.) - Improve context awareness with AST node analysis - Reduce false positives with better constant detection Breaking changes removed: - BraceTracker.ts - ExportConstantAnalyzer.ts - MagicNumberMatcher.ts - MagicStringMatcher.ts New components added: - AstTreeTraverser for AST walking - DuplicateValueTracker for cross-file tracking - AstContextChecker for node context analysis - AstNumberAnalyzer, AstStringAnalyzer, AstBooleanAnalyzer - ValuePatternMatcher for type detection Test coverage: 87.97% statements, 96.75% functions
170 lines
6.0 KiB
TypeScript
170 lines
6.0 KiB
TypeScript
import { createEngine } from "@secretlint/node"
|
|
import type { SecretLintConfigDescriptor } from "@secretlint/types"
|
|
import { ISecretDetector } from "../../domain/services/ISecretDetector"
|
|
import { SecretViolation } from "../../domain/value-objects/SecretViolation"
|
|
import { SECRET_KEYWORDS, SECRET_TYPE_NAMES } from "../../domain/constants/SecretExamples"
|
|
import { EXTERNAL_PACKAGES } from "../../shared/constants/rules"
|
|
|
|
/**
|
|
* Detects hardcoded secrets in TypeScript/JavaScript code
|
|
*
|
|
* Uses industry-standard Secretlint library to detect 350+ types of secrets
|
|
* including AWS keys, GitHub tokens, NPM tokens, SSH keys, API keys, and more.
|
|
*
|
|
* All detected secrets are marked as CRITICAL severity because they represent
|
|
* serious security risks that could lead to unauthorized access or data breaches.
|
|
*
|
|
* @example
|
|
* ```typescript
|
|
* const detector = new SecretDetector()
|
|
* const code = `const AWS_KEY = "AKIA1234567890ABCDEF"`
|
|
* const violations = await detector.detectAll(code, 'config.ts')
|
|
* // Returns array of SecretViolation objects with CRITICAL severity
|
|
* ```
|
|
*/
|
|
export class SecretDetector implements ISecretDetector {
|
|
private readonly secretlintConfig: SecretLintConfigDescriptor = {
|
|
rules: [
|
|
{
|
|
id: EXTERNAL_PACKAGES.SECRETLINT_PRESET,
|
|
},
|
|
],
|
|
}
|
|
|
|
/**
|
|
* Detects all types of hardcoded secrets in the provided code
|
|
*
|
|
* @param code - Source code to analyze
|
|
* @param filePath - Path to the file being analyzed
|
|
* @returns Promise resolving to array of secret violations
|
|
*/
|
|
public async detectAll(code: string, filePath: string): Promise<SecretViolation[]> {
|
|
try {
|
|
const engine = await createEngine({
|
|
cwd: process.cwd(),
|
|
configFileJSON: this.secretlintConfig,
|
|
formatter: "stylish",
|
|
color: false,
|
|
})
|
|
|
|
const result = await engine.executeOnContent({
|
|
content: code,
|
|
filePath,
|
|
})
|
|
|
|
return this.parseOutputToViolations(result.output, filePath)
|
|
} catch (_error) {
|
|
return []
|
|
}
|
|
}
|
|
|
|
private parseOutputToViolations(output: string, filePath: string): SecretViolation[] {
|
|
const violations: SecretViolation[] = []
|
|
|
|
if (!output || output.trim() === "") {
|
|
return violations
|
|
}
|
|
|
|
const lines = output.split("\n")
|
|
|
|
for (const line of lines) {
|
|
const match = /^\s*(\d+):(\d+)\s+(error|warning)\s+(.+?)\s+(.+)$/.exec(line)
|
|
|
|
if (match) {
|
|
const [, lineNum, column, , message, ruleId] = match
|
|
const secretType = this.extractSecretType(message, ruleId)
|
|
|
|
const violation = SecretViolation.create(
|
|
filePath,
|
|
parseInt(lineNum, 10),
|
|
parseInt(column, 10),
|
|
secretType,
|
|
message,
|
|
)
|
|
|
|
violations.push(violation)
|
|
}
|
|
}
|
|
|
|
return violations
|
|
}
|
|
|
|
private extractSecretType(message: string, ruleId: string): string {
|
|
if (ruleId.includes(SECRET_KEYWORDS.AWS)) {
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.ACCESS_KEY)) {
|
|
return SECRET_TYPE_NAMES.AWS_ACCESS_KEY
|
|
}
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.SECRET)) {
|
|
return SECRET_TYPE_NAMES.AWS_SECRET_KEY
|
|
}
|
|
return SECRET_TYPE_NAMES.AWS_CREDENTIAL
|
|
}
|
|
|
|
if (ruleId.includes(SECRET_KEYWORDS.GITHUB)) {
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.PERSONAL_ACCESS_TOKEN)) {
|
|
return SECRET_TYPE_NAMES.GITHUB_PERSONAL_ACCESS_TOKEN
|
|
}
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.OAUTH)) {
|
|
return SECRET_TYPE_NAMES.GITHUB_OAUTH_TOKEN
|
|
}
|
|
return SECRET_TYPE_NAMES.GITHUB_TOKEN
|
|
}
|
|
|
|
if (ruleId.includes(SECRET_KEYWORDS.NPM)) {
|
|
return SECRET_TYPE_NAMES.NPM_TOKEN
|
|
}
|
|
|
|
if (ruleId.includes(SECRET_KEYWORDS.GCP) || ruleId.includes(SECRET_KEYWORDS.GOOGLE)) {
|
|
return SECRET_TYPE_NAMES.GCP_SERVICE_ACCOUNT_KEY
|
|
}
|
|
|
|
if (ruleId.includes(SECRET_KEYWORDS.PRIVATEKEY) || ruleId.includes(SECRET_KEYWORDS.SSH)) {
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.RSA)) {
|
|
return SECRET_TYPE_NAMES.SSH_RSA_PRIVATE_KEY
|
|
}
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.DSA)) {
|
|
return SECRET_TYPE_NAMES.SSH_DSA_PRIVATE_KEY
|
|
}
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.ECDSA)) {
|
|
return SECRET_TYPE_NAMES.SSH_ECDSA_PRIVATE_KEY
|
|
}
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.ED25519)) {
|
|
return SECRET_TYPE_NAMES.SSH_ED25519_PRIVATE_KEY
|
|
}
|
|
return SECRET_TYPE_NAMES.SSH_PRIVATE_KEY
|
|
}
|
|
|
|
if (ruleId.includes(SECRET_KEYWORDS.SLACK)) {
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.BOT)) {
|
|
return SECRET_TYPE_NAMES.SLACK_BOT_TOKEN
|
|
}
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.USER)) {
|
|
return SECRET_TYPE_NAMES.SLACK_USER_TOKEN
|
|
}
|
|
return SECRET_TYPE_NAMES.SLACK_TOKEN
|
|
}
|
|
|
|
if (ruleId.includes(SECRET_KEYWORDS.BASICAUTH)) {
|
|
return SECRET_TYPE_NAMES.BASIC_AUTH_CREDENTIALS
|
|
}
|
|
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.API_KEY)) {
|
|
return SECRET_TYPE_NAMES.API_KEY
|
|
}
|
|
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.TOKEN)) {
|
|
return SECRET_TYPE_NAMES.AUTHENTICATION_TOKEN
|
|
}
|
|
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.PASSWORD)) {
|
|
return SECRET_TYPE_NAMES.PASSWORD
|
|
}
|
|
|
|
if (message.toLowerCase().includes(SECRET_KEYWORDS.SECRET)) {
|
|
return SECRET_TYPE_NAMES.SECRET
|
|
}
|
|
|
|
return SECRET_TYPE_NAMES.SENSITIVE_DATA
|
|
}
|
|
}
|