mirror of
https://github.com/samiyev/puaros.git
synced 2025-12-27 23:06:54 +05:00
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:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]}`)
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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.)",
|
||||
}
|
||||
|
||||
34
packages/guardian/src/domain/services/ISecretDetector.ts
Normal file
34
packages/guardian/src/domain/services/ISecretDetector.ts
Normal 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[]>
|
||||
}
|
||||
198
packages/guardian/src/domain/value-objects/SecretViolation.ts
Normal file
198
packages/guardian/src/domain/value-objects/SecretViolation.ts
Normal 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.`
|
||||
}
|
||||
}
|
||||
167
packages/guardian/src/infrastructure/analyzers/SecretDetector.ts
Normal file
167
packages/guardian/src/infrastructure/analyzers/SecretDetector.ts
Normal 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"
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user