feat: add secret detection with Secretlint (v0.8.0)

Add critical security feature to detect 350+ types of hardcoded secrets
using industry-standard Secretlint library.

Features:
- Detect AWS keys, GitHub tokens, NPM tokens, SSH keys, API keys, etc.
- All secrets marked as CRITICAL severity
- Context-aware remediation suggestions per secret type
- New SecretDetector using @secretlint/node
- New SecretViolation value object (100% test coverage)
- CLI output with "🔐 Secrets" section
- Async pipeline support for secret detection

Tests:
- Added 47 new tests (566 total, 100% pass rate)
- Coverage: 93.3% statements, 83.74% branches
- SecretViolation: 23 tests, 100% coverage
- SecretDetector: 24 tests

Dependencies:
- @secretlint/node: 11.2.5
- @secretlint/core: 11.2.5
- @secretlint/types: 11.2.5
- @secretlint/secretlint-rule-preset-recommend: 11.2.5
This commit is contained in:
imfozilbek
2025-11-25 18:24:22 +05:00
parent 8d400c9517
commit 0b1cc5a79a
18 changed files with 1186 additions and 11 deletions

View File

@@ -12,6 +12,7 @@ import { IEntityExposureDetector } from "./domain/services/IEntityExposureDetect
import { IDependencyDirectionDetector } from "./domain/services/IDependencyDirectionDetector"
import { IRepositoryPatternDetector } from "./domain/services/RepositoryPatternDetectorService"
import { IAggregateBoundaryDetector } from "./domain/services/IAggregateBoundaryDetector"
import { ISecretDetector } from "./domain/services/ISecretDetector"
import { FileScanner } from "./infrastructure/scanners/FileScanner"
import { CodeParser } from "./infrastructure/parsers/CodeParser"
import { HardcodeDetector } from "./infrastructure/analyzers/HardcodeDetector"
@@ -21,6 +22,7 @@ import { EntityExposureDetector } from "./infrastructure/analyzers/EntityExposur
import { DependencyDirectionDetector } from "./infrastructure/analyzers/DependencyDirectionDetector"
import { RepositoryPatternDetector } from "./infrastructure/analyzers/RepositoryPatternDetector"
import { AggregateBoundaryDetector } from "./infrastructure/analyzers/AggregateBoundaryDetector"
import { SecretDetector } from "./infrastructure/analyzers/SecretDetector"
import { ERROR_MESSAGES } from "./shared/constants"
/**
@@ -79,6 +81,7 @@ export async function analyzeProject(
new DependencyDirectionDetector()
const repositoryPatternDetector: IRepositoryPatternDetector = new RepositoryPatternDetector()
const aggregateBoundaryDetector: IAggregateBoundaryDetector = new AggregateBoundaryDetector()
const secretDetector: ISecretDetector = new SecretDetector()
const useCase = new AnalyzeProject(
fileScanner,
codeParser,
@@ -89,6 +92,7 @@ export async function analyzeProject(
dependencyDirectionDetector,
repositoryPatternDetector,
aggregateBoundaryDetector,
secretDetector,
)
const result = await useCase.execute(options)

View File

@@ -9,6 +9,7 @@ import { IEntityExposureDetector } from "../../domain/services/IEntityExposureDe
import { IDependencyDirectionDetector } from "../../domain/services/IDependencyDirectionDetector"
import { IRepositoryPatternDetector } from "../../domain/services/RepositoryPatternDetectorService"
import { IAggregateBoundaryDetector } from "../../domain/services/IAggregateBoundaryDetector"
import { ISecretDetector } from "../../domain/services/ISecretDetector"
import { SourceFile } from "../../domain/entities/SourceFile"
import { DependencyGraph } from "../../domain/entities/DependencyGraph"
import { FileCollectionStep } from "./pipeline/FileCollectionStep"
@@ -42,6 +43,7 @@ export interface AnalyzeProjectResponse {
dependencyDirectionViolations: DependencyDirectionViolation[]
repositoryPatternViolations: RepositoryPatternViolation[]
aggregateBoundaryViolations: AggregateBoundaryViolation[]
secretViolations: SecretViolation[]
metrics: ProjectMetrics
}
@@ -163,6 +165,17 @@ export interface AggregateBoundaryViolation {
severity: SeverityLevel
}
export interface SecretViolation {
rule: typeof RULES.SECRET_EXPOSURE
secretType: string
file: string
line: number
column: number
message: string
suggestion: string
severity: SeverityLevel
}
export interface ProjectMetrics {
totalFiles: number
totalFunctions: number
@@ -193,6 +206,7 @@ export class AnalyzeProject extends UseCase<
dependencyDirectionDetector: IDependencyDirectionDetector,
repositoryPatternDetector: IRepositoryPatternDetector,
aggregateBoundaryDetector: IAggregateBoundaryDetector,
secretDetector: ISecretDetector,
) {
super()
this.fileCollectionStep = new FileCollectionStep(fileScanner)
@@ -205,6 +219,7 @@ export class AnalyzeProject extends UseCase<
dependencyDirectionDetector,
repositoryPatternDetector,
aggregateBoundaryDetector,
secretDetector,
)
this.resultAggregator = new ResultAggregator()
}
@@ -224,7 +239,7 @@ export class AnalyzeProject extends UseCase<
rootDir: request.rootDir,
})
const detectionResult = this.detectionPipeline.execute({
const detectionResult = await this.detectionPipeline.execute({
sourceFiles,
dependencyGraph,
})

View File

@@ -5,6 +5,7 @@ import { IEntityExposureDetector } from "../../../domain/services/IEntityExposur
import { IDependencyDirectionDetector } from "../../../domain/services/IDependencyDirectionDetector"
import { IRepositoryPatternDetector } from "../../../domain/services/RepositoryPatternDetectorService"
import { IAggregateBoundaryDetector } from "../../../domain/services/IAggregateBoundaryDetector"
import { ISecretDetector } from "../../../domain/services/ISecretDetector"
import { SourceFile } from "../../../domain/entities/SourceFile"
import { DependencyGraph } from "../../../domain/entities/DependencyGraph"
import {
@@ -25,6 +26,7 @@ import type {
HardcodeViolation,
NamingConventionViolation,
RepositoryPatternViolation,
SecretViolation,
} from "../AnalyzeProject"
export interface DetectionRequest {
@@ -42,6 +44,7 @@ export interface DetectionResult {
dependencyDirectionViolations: DependencyDirectionViolation[]
repositoryPatternViolations: RepositoryPatternViolation[]
aggregateBoundaryViolations: AggregateBoundaryViolation[]
secretViolations: SecretViolation[]
}
/**
@@ -56,9 +59,12 @@ export class DetectionPipeline {
private readonly dependencyDirectionDetector: IDependencyDirectionDetector,
private readonly repositoryPatternDetector: IRepositoryPatternDetector,
private readonly aggregateBoundaryDetector: IAggregateBoundaryDetector,
private readonly secretDetector: ISecretDetector,
) {}
public execute(request: DetectionRequest): DetectionResult {
public async execute(request: DetectionRequest): Promise<DetectionResult> {
const secretViolations = await this.detectSecrets(request.sourceFiles)
return {
violations: this.sortBySeverity(this.detectViolations(request.sourceFiles)),
hardcodeViolations: this.sortBySeverity(this.detectHardcode(request.sourceFiles)),
@@ -83,6 +89,7 @@ export class DetectionPipeline {
aggregateBoundaryViolations: this.sortBySeverity(
this.detectAggregateBoundaryViolations(request.sourceFiles),
),
secretViolations: this.sortBySeverity(secretViolations),
}
}
@@ -365,6 +372,32 @@ export class DetectionPipeline {
return violations
}
private async detectSecrets(sourceFiles: SourceFile[]): Promise<SecretViolation[]> {
const violations: SecretViolation[] = []
for (const file of sourceFiles) {
const secretViolations = await this.secretDetector.detectAll(
file.content,
file.path.relative,
)
for (const secret of secretViolations) {
violations.push({
rule: RULES.SECRET_EXPOSURE,
secretType: secret.secretType,
file: file.path.relative,
line: secret.line,
column: secret.column,
message: secret.getMessage(),
suggestion: secret.getSuggestion(),
severity: "critical",
})
}
}
return violations
}
private sortBySeverity<T extends { severity: SeverityLevel }>(violations: T[]): T[] {
return violations.sort((a, b) => {
return SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity]

View File

@@ -12,6 +12,7 @@ import type {
NamingConventionViolation,
ProjectMetrics,
RepositoryPatternViolation,
SecretViolation,
} from "../AnalyzeProject"
export interface AggregationRequest {
@@ -27,6 +28,7 @@ export interface AggregationRequest {
dependencyDirectionViolations: DependencyDirectionViolation[]
repositoryPatternViolations: RepositoryPatternViolation[]
aggregateBoundaryViolations: AggregateBoundaryViolation[]
secretViolations: SecretViolation[]
}
/**
@@ -52,6 +54,7 @@ export class ResultAggregator {
dependencyDirectionViolations: request.dependencyDirectionViolations,
repositoryPatternViolations: request.repositoryPatternViolations,
aggregateBoundaryViolations: request.aggregateBoundaryViolations,
secretViolations: request.secretViolations,
metrics,
}
}

View File

@@ -9,6 +9,7 @@ import type {
HardcodeViolation,
NamingConventionViolation,
RepositoryPatternViolation,
SecretViolation,
} from "../../application/use-cases/AnalyzeProject"
import { SEVERITY_DISPLAY_LABELS, SEVERITY_SECTION_HEADERS } from "../constants"
import { ViolationGrouper } from "../groupers/ViolationGrouper"
@@ -177,6 +178,22 @@ export class OutputFormatter {
console.log("")
}
formatSecretViolation(sv: SecretViolation, index: number): void {
const location = `${sv.file}:${String(sv.line)}:${String(sv.column)}`
console.log(`${String(index + 1)}. ${location}`)
console.log(` Severity: ${SEVERITY_LABELS[sv.severity]} ⚠️`)
console.log(` Secret Type: ${sv.secretType}`)
console.log(` ${sv.message}`)
console.log(" 🔐 CRITICAL: Rotate this secret immediately!")
console.log(" 💡 Suggestion:")
sv.suggestion.split("\n").forEach((line) => {
if (line.trim()) {
console.log(` ${line}`)
}
})
console.log("")
}
formatHardcodeViolation(hc: HardcodeViolation, index: number): void {
console.log(`${String(index + 1)}. ${hc.file}:${String(hc.line)}:${String(hc.column)}`)
console.log(` Severity: ${SEVERITY_LABELS[hc.severity]}`)

View File

@@ -92,6 +92,7 @@ program
dependencyDirectionViolations,
repositoryPatternViolations,
aggregateBoundaryViolations,
secretViolations,
} = result
const minSeverity: SeverityLevel | undefined = options.onlyCritical
@@ -132,6 +133,7 @@ program
aggregateBoundaryViolations,
minSeverity,
)
secretViolations = grouper.filterBySeverity(secretViolations, minSeverity)
statsFormatter.displaySeverityFilterMessage(
options.onlyCritical,
@@ -245,6 +247,19 @@ program
)
}
if (secretViolations.length > 0) {
console.log(
`\n🔐 Found ${String(secretViolations.length)} hardcoded secret(s) - CRITICAL SECURITY RISK`,
)
outputFormatter.displayGroupedViolations(
secretViolations,
(sv, i) => {
outputFormatter.formatSecretViolation(sv, i)
},
limit,
)
}
if (options.hardcode && hardcodeViolations.length > 0) {
console.log(
`\n${CLI_MESSAGES.HARDCODE_VIOLATIONS_HEADER} ${String(hardcodeViolations.length)} ${CLI_LABELS.HARDCODE_VIOLATIONS}`,
@@ -267,7 +282,8 @@ program
entityExposureViolations.length +
dependencyDirectionViolations.length +
repositoryPatternViolations.length +
aggregateBoundaryViolations.length
aggregateBoundaryViolations.length +
secretViolations.length
statsFormatter.displaySummary(totalIssues, options.verbose)
} catch (error) {

View File

@@ -60,3 +60,12 @@ export const AGGREGATE_VIOLATION_MESSAGES = {
AVOID_DIRECT_REFERENCE: "3. Avoid direct entity references to maintain aggregate independence",
MAINTAIN_INDEPENDENCE: "4. Each aggregate should be independently modifiable and deployable",
}
export const SECRET_VIOLATION_MESSAGES = {
USE_ENV_VARIABLES: "1. Use environment variables for sensitive data (process.env.API_KEY)",
USE_SECRET_MANAGER:
"2. Use secret management services (AWS Secrets Manager, HashiCorp Vault, etc.)",
NEVER_COMMIT_SECRETS: "3. Never commit secrets to version control",
ROTATE_IF_EXPOSED: "4. If secret was committed, rotate it immediately",
USE_GITIGNORE: "5. Add secret files to .gitignore (.env, credentials.json, etc.)",
}

View File

@@ -0,0 +1,34 @@
import { SecretViolation } from "../value-objects/SecretViolation"
/**
* Interface for detecting hardcoded secrets in source code
*
* Detects sensitive data like API keys, tokens, passwords, and credentials
* that should never be hardcoded in source code. Uses industry-standard
* Secretlint library for pattern matching.
*
* All detected secrets are marked as CRITICAL severity violations.
*
* @example
* ```typescript
* const detector: ISecretDetector = new SecretDetector()
* const violations = await detector.detectAll(
* 'const AWS_KEY = "AKIA1234567890ABCDEF"',
* 'src/config/aws.ts'
* )
*
* violations.forEach(v => {
* console.log(v.getMessage()) // "Hardcoded AWS Access Key detected"
* })
* ```
*/
export interface ISecretDetector {
/**
* Detect all types of hardcoded secrets in the provided code
*
* @param code - Source code to analyze
* @param filePath - Path to the file being analyzed
* @returns Array of secret violations found
*/
detectAll(code: string, filePath: string): Promise<SecretViolation[]>
}

View File

@@ -0,0 +1,198 @@
import { ValueObject } from "./ValueObject"
import { SECRET_VIOLATION_MESSAGES } from "../constants/Messages"
interface SecretViolationProps {
readonly file: string
readonly line: number
readonly column: number
readonly secretType: string
readonly matchedPattern: string
}
/**
* Represents a secret exposure violation in the codebase
*
* Secret violations occur when sensitive data like API keys, tokens, passwords,
* or credentials are hardcoded in the source code instead of being stored
* in secure environment variables or secret management systems.
*
* All secret violations are marked as CRITICAL severity because they represent
* serious security risks that could lead to unauthorized access, data breaches,
* or service compromise.
*
* @example
* ```typescript
* const violation = SecretViolation.create(
* 'src/config/aws.ts',
* 10,
* 15,
* 'AWS Access Key',
* 'AKIA1234567890ABCDEF'
* )
*
* console.log(violation.getMessage())
* // "Hardcoded AWS Access Key detected"
*
* console.log(violation.getSeverity())
* // "critical"
* ```
*/
export class SecretViolation extends ValueObject<SecretViolationProps> {
private constructor(props: SecretViolationProps) {
super(props)
}
public static create(
file: string,
line: number,
column: number,
secretType: string,
matchedPattern: string,
): SecretViolation {
return new SecretViolation({
file,
line,
column,
secretType,
matchedPattern,
})
}
public get file(): string {
return this.props.file
}
public get line(): number {
return this.props.line
}
public get column(): number {
return this.props.column
}
public get secretType(): string {
return this.props.secretType
}
public get matchedPattern(): string {
return this.props.matchedPattern
}
public getMessage(): string {
return `Hardcoded ${this.props.secretType} detected`
}
public getSuggestion(): string {
const suggestions: string[] = [
SECRET_VIOLATION_MESSAGES.USE_ENV_VARIABLES,
SECRET_VIOLATION_MESSAGES.USE_SECRET_MANAGER,
SECRET_VIOLATION_MESSAGES.NEVER_COMMIT_SECRETS,
SECRET_VIOLATION_MESSAGES.ROTATE_IF_EXPOSED,
SECRET_VIOLATION_MESSAGES.USE_GITIGNORE,
]
return suggestions.join("\n")
}
public getExampleFix(): string {
return this.getExampleFixForSecretType(this.props.secretType)
}
public getSeverity(): "critical" {
return "critical"
}
private getExampleFixForSecretType(secretType: string): string {
const lowerType = secretType.toLowerCase()
if (lowerType.includes("aws")) {
return `
// ❌ Bad: Hardcoded AWS credentials
const AWS_ACCESS_KEY_ID = "AKIA1234567890ABCDEF"
const AWS_SECRET_ACCESS_KEY = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
// ✅ Good: Use environment variables
const AWS_ACCESS_KEY_ID = process.env.AWS_ACCESS_KEY_ID
const AWS_SECRET_ACCESS_KEY = process.env.AWS_SECRET_ACCESS_KEY
// ✅ Good: Use AWS SDK credentials provider
import { fromEnv } from "@aws-sdk/credential-providers"
const credentials = fromEnv()`
}
if (lowerType.includes("github")) {
return `
// ❌ Bad: Hardcoded GitHub token
const GITHUB_TOKEN = "ghp_1234567890abcdefghijklmnopqrstuv"
// ✅ Good: Use environment variables
const GITHUB_TOKEN = process.env.GITHUB_TOKEN
// ✅ Good: GitHub Apps with temporary tokens
// Use GitHub Apps for automated workflows instead of personal access tokens`
}
if (lowerType.includes("npm")) {
return `
// ❌ Bad: Hardcoded NPM token in code
const NPM_TOKEN = "npm_abc123xyz"
// ✅ Good: Use .npmrc file (add to .gitignore)
// .npmrc
//registry.npmjs.org/:_authToken=\${NPM_TOKEN}
// ✅ Good: Use environment variable
const NPM_TOKEN = process.env.NPM_TOKEN`
}
if (lowerType.includes("ssh") || lowerType.includes("private key")) {
return `
// ❌ Bad: Hardcoded SSH private key
const privateKey = \`-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA...\`
// ✅ Good: Load from secure file (not in repository)
import fs from "fs"
const privateKey = fs.readFileSync(process.env.SSH_KEY_PATH, "utf-8")
// ✅ Good: Use SSH agent
// Configure SSH agent to handle keys securely`
}
if (lowerType.includes("slack")) {
return `
// ❌ Bad: Hardcoded Slack token
const SLACK_TOKEN = "xoxb-XXXX-XXXX-XXXX-example-token-here"
// ✅ Good: Use environment variables
const SLACK_TOKEN = process.env.SLACK_BOT_TOKEN
// ✅ Good: Use OAuth flow for user tokens
// Implement OAuth 2.0 flow instead of hardcoding tokens`
}
if (lowerType.includes("api key") || lowerType.includes("apikey")) {
return `
// ❌ Bad: Hardcoded API key
const API_KEY = "sk_live_XXXXXXXXXXXXXXXXXXXX_example_key"
// ✅ Good: Use environment variables
const API_KEY = process.env.API_KEY
// ✅ Good: Use secret management service
import { SecretsManager } from "aws-sdk"
const secretsManager = new SecretsManager()
const secret = await secretsManager.getSecretValue({ SecretId: "api-key" }).promise()`
}
return `
// ❌ Bad: Hardcoded secret
const SECRET = "hardcoded-secret-value"
// ✅ Good: Use environment variables
const SECRET = process.env.SECRET_KEY
// ✅ Good: Use secret management
// AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, etc.`
}
}

View File

@@ -0,0 +1,167 @@
import { createEngine } from "@secretlint/node"
import type { SecretLintConfigDescriptor } from "@secretlint/types"
import { ISecretDetector } from "../../domain/services/ISecretDetector"
import { SecretViolation } from "../../domain/value-objects/SecretViolation"
/**
* 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: "@secretlint/secretlint-rule-preset-recommend",
},
],
}
/**
* 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("aws")) {
if (message.toLowerCase().includes("access key")) {
return "AWS Access Key"
}
if (message.toLowerCase().includes("secret")) {
return "AWS Secret Key"
}
return "AWS Credential"
}
if (ruleId.includes("github")) {
if (message.toLowerCase().includes("personal access token")) {
return "GitHub Personal Access Token"
}
if (message.toLowerCase().includes("oauth")) {
return "GitHub OAuth Token"
}
return "GitHub Token"
}
if (ruleId.includes("npm")) {
return "NPM Token"
}
if (ruleId.includes("gcp") || ruleId.includes("google")) {
return "GCP Service Account Key"
}
if (ruleId.includes("privatekey") || ruleId.includes("ssh")) {
if (message.toLowerCase().includes("rsa")) {
return "SSH RSA Private Key"
}
if (message.toLowerCase().includes("dsa")) {
return "SSH DSA Private Key"
}
if (message.toLowerCase().includes("ecdsa")) {
return "SSH ECDSA Private Key"
}
if (message.toLowerCase().includes("ed25519")) {
return "SSH Ed25519 Private Key"
}
return "SSH Private Key"
}
if (ruleId.includes("slack")) {
if (message.toLowerCase().includes("bot")) {
return "Slack Bot Token"
}
if (message.toLowerCase().includes("user")) {
return "Slack User Token"
}
return "Slack Token"
}
if (ruleId.includes("basicauth")) {
return "Basic Authentication Credentials"
}
if (message.toLowerCase().includes("api key")) {
return "API Key"
}
if (message.toLowerCase().includes("token")) {
return "Authentication Token"
}
if (message.toLowerCase().includes("password")) {
return "Password"
}
if (message.toLowerCase().includes("secret")) {
return "Secret"
}
return "Sensitive Data"
}
}

View File

@@ -86,6 +86,7 @@ export const SEVERITY_ORDER: Record<SeverityLevel, number> = {
* Violation type to severity mapping
*/
export const VIOLATION_SEVERITY_MAP = {
SECRET_EXPOSURE: SEVERITY_LEVELS.CRITICAL,
CIRCULAR_DEPENDENCY: SEVERITY_LEVELS.CRITICAL,
REPOSITORY_PATTERN: SEVERITY_LEVELS.CRITICAL,
AGGREGATE_BOUNDARY: SEVERITY_LEVELS.CRITICAL,

View File

@@ -11,6 +11,7 @@ export const RULES = {
DEPENDENCY_DIRECTION: "dependency-direction",
REPOSITORY_PATTERN: "repository-pattern",
AGGREGATE_BOUNDARY: "aggregate-boundary",
SECRET_EXPOSURE: "secret-exposure",
} as const
/**