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:
344
packages/guardian/tests/unit/domain/SecretViolation.test.ts
Normal file
344
packages/guardian/tests/unit/domain/SecretViolation.test.ts
Normal file
@@ -0,0 +1,344 @@
|
||||
import { describe, it, expect } from "vitest"
|
||||
import { SecretViolation } from "../../../src/domain/value-objects/SecretViolation"
|
||||
|
||||
describe("SecretViolation", () => {
|
||||
describe("create", () => {
|
||||
it("should create a secret violation with all properties", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"AKIA1234567890ABCDEF",
|
||||
)
|
||||
|
||||
expect(violation.file).toBe("src/config/aws.ts")
|
||||
expect(violation.line).toBe(10)
|
||||
expect(violation.column).toBe(15)
|
||||
expect(violation.secretType).toBe("AWS Access Key")
|
||||
expect(violation.matchedPattern).toBe("AKIA1234567890ABCDEF")
|
||||
})
|
||||
|
||||
it("should create a secret violation with GitHub token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/github.ts",
|
||||
5,
|
||||
20,
|
||||
"GitHub Personal Access Token",
|
||||
"ghp_1234567890abcdefghijklmnopqrstuv",
|
||||
)
|
||||
|
||||
expect(violation.secretType).toBe("GitHub Personal Access Token")
|
||||
expect(violation.file).toBe("src/config/github.ts")
|
||||
})
|
||||
|
||||
it("should create a secret violation with NPM token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
".npmrc",
|
||||
1,
|
||||
1,
|
||||
"NPM Token",
|
||||
"npm_abc123xyz",
|
||||
)
|
||||
|
||||
expect(violation.secretType).toBe("NPM Token")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getters", () => {
|
||||
it("should return file path", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.file).toBe("src/config/aws.ts")
|
||||
})
|
||||
|
||||
it("should return line number", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.line).toBe(10)
|
||||
})
|
||||
|
||||
it("should return column number", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.column).toBe(15)
|
||||
})
|
||||
|
||||
it("should return secret type", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.secretType).toBe("AWS Access Key")
|
||||
})
|
||||
|
||||
it("should return matched pattern", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"AKIA1234567890ABCDEF",
|
||||
)
|
||||
|
||||
expect(violation.matchedPattern).toBe("AKIA1234567890ABCDEF")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getMessage", () => {
|
||||
it("should return formatted message for AWS Access Key", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.getMessage()).toBe("Hardcoded AWS Access Key detected")
|
||||
})
|
||||
|
||||
it("should return formatted message for GitHub token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/github.ts",
|
||||
5,
|
||||
20,
|
||||
"GitHub Token",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.getMessage()).toBe("Hardcoded GitHub Token detected")
|
||||
})
|
||||
|
||||
it("should return formatted message for NPM token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
".npmrc",
|
||||
1,
|
||||
1,
|
||||
"NPM Token",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.getMessage()).toBe("Hardcoded NPM Token detected")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSuggestion", () => {
|
||||
it("should return multi-line suggestion", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
const suggestion = violation.getSuggestion()
|
||||
|
||||
expect(suggestion).toContain("1. Use environment variables")
|
||||
expect(suggestion).toContain("2. Use secret management services")
|
||||
expect(suggestion).toContain("3. Never commit secrets")
|
||||
expect(suggestion).toContain("4. If secret was committed, rotate it immediately")
|
||||
expect(suggestion).toContain("5. Add secret files to .gitignore")
|
||||
})
|
||||
|
||||
it("should return the same suggestion for all secret types", () => {
|
||||
const awsViolation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
const githubViolation = SecretViolation.create(
|
||||
"src/config/github.ts",
|
||||
5,
|
||||
20,
|
||||
"GitHub Token",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(awsViolation.getSuggestion()).toBe(githubViolation.getSuggestion())
|
||||
})
|
||||
})
|
||||
|
||||
describe("getExampleFix", () => {
|
||||
it("should return AWS-specific example for AWS Access Key", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("AWS")
|
||||
expect(example).toContain("process.env.AWS_ACCESS_KEY_ID")
|
||||
expect(example).toContain("fromEnv")
|
||||
})
|
||||
|
||||
it("should return GitHub-specific example for GitHub token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/github.ts",
|
||||
5,
|
||||
20,
|
||||
"GitHub Token",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("GitHub")
|
||||
expect(example).toContain("process.env.GITHUB_TOKEN")
|
||||
expect(example).toContain("GitHub Apps")
|
||||
})
|
||||
|
||||
it("should return NPM-specific example for NPM token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
".npmrc",
|
||||
1,
|
||||
1,
|
||||
"NPM Token",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("NPM")
|
||||
expect(example).toContain(".npmrc")
|
||||
expect(example).toContain("process.env.NPM_TOKEN")
|
||||
})
|
||||
|
||||
it("should return SSH-specific example for SSH Private Key", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/ssh.ts",
|
||||
1,
|
||||
1,
|
||||
"SSH Private Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("SSH")
|
||||
expect(example).toContain("readFileSync")
|
||||
expect(example).toContain("SSH_KEY_PATH")
|
||||
})
|
||||
|
||||
it("should return SSH RSA-specific example for SSH RSA Private Key", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/ssh.ts",
|
||||
1,
|
||||
1,
|
||||
"SSH RSA Private Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("SSH")
|
||||
expect(example).toContain("RSA PRIVATE KEY")
|
||||
})
|
||||
|
||||
it("should return Slack-specific example for Slack token", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/slack.ts",
|
||||
1,
|
||||
1,
|
||||
"Slack Bot Token",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("Slack")
|
||||
expect(example).toContain("process.env.SLACK_BOT_TOKEN")
|
||||
})
|
||||
|
||||
it("should return API Key example for generic API key", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/api.ts",
|
||||
1,
|
||||
1,
|
||||
"API Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("API")
|
||||
expect(example).toContain("process.env.API_KEY")
|
||||
expect(example).toContain("SecretsManager")
|
||||
})
|
||||
|
||||
it("should return generic example for unknown secret type", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/unknown.ts",
|
||||
1,
|
||||
1,
|
||||
"Unknown Secret",
|
||||
"test",
|
||||
)
|
||||
|
||||
const example = violation.getExampleFix()
|
||||
|
||||
expect(example).toContain("process.env.SECRET_KEY")
|
||||
expect(example).toContain("secret management")
|
||||
})
|
||||
})
|
||||
|
||||
describe("getSeverity", () => {
|
||||
it("should always return critical severity", () => {
|
||||
const violation = SecretViolation.create(
|
||||
"src/config/aws.ts",
|
||||
10,
|
||||
15,
|
||||
"AWS Access Key",
|
||||
"test",
|
||||
)
|
||||
|
||||
expect(violation.getSeverity()).toBe("critical")
|
||||
})
|
||||
|
||||
it("should return critical severity for all secret types", () => {
|
||||
const types = [
|
||||
"AWS Access Key",
|
||||
"GitHub Token",
|
||||
"NPM Token",
|
||||
"SSH Private Key",
|
||||
"Slack Token",
|
||||
"API Key",
|
||||
]
|
||||
|
||||
types.forEach((type) => {
|
||||
const violation = SecretViolation.create("test.ts", 1, 1, type, "test")
|
||||
expect(violation.getSeverity()).toBe("critical")
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,277 @@
|
||||
import { describe, it, expect, beforeEach } from "vitest"
|
||||
import { SecretDetector } from "../../../src/infrastructure/analyzers/SecretDetector"
|
||||
|
||||
describe("SecretDetector", () => {
|
||||
let detector: SecretDetector
|
||||
|
||||
beforeEach(() => {
|
||||
detector = new SecretDetector()
|
||||
})
|
||||
|
||||
describe("detectAll", () => {
|
||||
it("should return empty array for code without secrets", async () => {
|
||||
const code = `
|
||||
const greeting = "Hello World"
|
||||
const count = 42
|
||||
function test() {
|
||||
return true
|
||||
}
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "test.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should return empty array for normal environment variable usage", async () => {
|
||||
const code = `
|
||||
const apiKey = process.env.API_KEY
|
||||
const dbUrl = process.env.DATABASE_URL
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "config.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle empty code", async () => {
|
||||
const violations = await detector.detectAll("", "empty.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle code with only comments", async () => {
|
||||
const code = `
|
||||
// This is a comment
|
||||
/* Multi-line
|
||||
comment */
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "comments.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle multiline strings without secrets", async () => {
|
||||
const code = `
|
||||
const template = \`
|
||||
Hello World
|
||||
This is a test
|
||||
No secrets here
|
||||
\`
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "template.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle code with URLs", async () => {
|
||||
const code = `
|
||||
const apiUrl = "https://api.example.com/v1"
|
||||
const websiteUrl = "http://localhost:3000"
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "urls.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle imports and requires", async () => {
|
||||
const code = `
|
||||
import { something } from "some-package"
|
||||
const fs = require('fs')
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "imports.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should return violations with correct file path", async () => {
|
||||
const code = `const secret = "test-secret-value"`
|
||||
const filePath = "src/config/secrets.ts"
|
||||
|
||||
const violations = await detector.detectAll(code, filePath)
|
||||
|
||||
violations.forEach((v) => {
|
||||
expect(v.file).toBe(filePath)
|
||||
})
|
||||
})
|
||||
|
||||
it("should handle .js files", async () => {
|
||||
const code = `const test = "value"`
|
||||
|
||||
const violations = await detector.detectAll(code, "test.js")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should handle .jsx files", async () => {
|
||||
const code = `const Component = () => <div>Test</div>`
|
||||
|
||||
const violations = await detector.detectAll(code, "Component.jsx")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should handle .tsx files", async () => {
|
||||
const code = `const Component: React.FC = () => <div>Test</div>`
|
||||
|
||||
const violations = await detector.detectAll(code, "Component.tsx")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should handle errors gracefully", async () => {
|
||||
const code = null as unknown as string
|
||||
|
||||
const violations = await detector.detectAll(code, "test.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle malformed code gracefully", async () => {
|
||||
const code = "const = = ="
|
||||
|
||||
const violations = await detector.detectAll(code, "malformed.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
|
||||
describe("parseOutputToViolations", () => {
|
||||
it("should parse empty output", async () => {
|
||||
const code = ""
|
||||
|
||||
const violations = await detector.detectAll(code, "test.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
|
||||
it("should handle whitespace-only output", async () => {
|
||||
const code = " \n \n "
|
||||
|
||||
const violations = await detector.detectAll(code, "test.ts")
|
||||
|
||||
expect(violations).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe("extractSecretType", () => {
|
||||
it("should handle various secret types correctly", async () => {
|
||||
const code = `const value = "test"`
|
||||
|
||||
const violations = await detector.detectAll(code, "test.ts")
|
||||
|
||||
violations.forEach((v) => {
|
||||
expect(v.secretType).toBeTruthy()
|
||||
expect(typeof v.secretType).toBe("string")
|
||||
expect(v.secretType.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("integration", () => {
|
||||
it("should work with TypeScript code", async () => {
|
||||
const code = `
|
||||
interface Config {
|
||||
apiKey: string
|
||||
}
|
||||
|
||||
const config: Config = {
|
||||
apiKey: process.env.API_KEY || "default"
|
||||
}
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "config.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should work with ES6+ syntax", async () => {
|
||||
const code = `
|
||||
const fetchData = async () => {
|
||||
const response = await fetch(url)
|
||||
return response.json()
|
||||
}
|
||||
|
||||
const [data, setData] = useState(null)
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "hooks.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should work with JSX/TSX", async () => {
|
||||
const code = `
|
||||
export const Button = ({ onClick }: Props) => {
|
||||
return <button onClick={onClick}>Click me</button>
|
||||
}
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "Button.tsx")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should handle concurrent detections", async () => {
|
||||
const code1 = "const test1 = 'value1'"
|
||||
const code2 = "const test2 = 'value2'"
|
||||
const code3 = "const test3 = 'value3'"
|
||||
|
||||
const [result1, result2, result3] = await Promise.all([
|
||||
detector.detectAll(code1, "file1.ts"),
|
||||
detector.detectAll(code2, "file2.ts"),
|
||||
detector.detectAll(code3, "file3.ts"),
|
||||
])
|
||||
|
||||
expect(result1).toBeInstanceOf(Array)
|
||||
expect(result2).toBeInstanceOf(Array)
|
||||
expect(result3).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
|
||||
describe("edge cases", () => {
|
||||
it("should handle very long code", async () => {
|
||||
const longCode = "const value = 'test'\n".repeat(1000)
|
||||
|
||||
const violations = await detector.detectAll(longCode, "long.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should handle special characters in code", async () => {
|
||||
const code = `
|
||||
const special = "!@#$%^&*()_+-=[]{}|;:',.<>?"
|
||||
const unicode = "日本語 🚀"
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "special.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should handle code with regex patterns", async () => {
|
||||
const code = `
|
||||
const pattern = /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,}$/i
|
||||
const matches = text.match(pattern)
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "regex.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
|
||||
it("should handle code with template literals", async () => {
|
||||
const code = `
|
||||
const message = \`Hello \${name}, your balance is \${balance}\`
|
||||
`
|
||||
|
||||
const violations = await detector.detectAll(code, "template.ts")
|
||||
|
||||
expect(violations).toBeInstanceOf(Array)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user