- Add scanError field and scannedSuccessfully flag to SecretScanResult - File read errors no longer falsely report as "clean" - Callers can distinguish clean files from scan failures - Update getScanSummary to track filesWithErrors count - SecretsDetectedError now reports files that couldn't be scanned - Add tests verifying error handling behavior for file access issues Refs #337 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
778 lines
24 KiB
TypeScript
778 lines
24 KiB
TypeScript
import { ConfigService } from "@nestjs/config";
|
|
import { describe, it, expect, beforeEach, vi } from "vitest";
|
|
import { SecretScannerService } from "./secret-scanner.service";
|
|
import { SecretsDetectedError } from "./types";
|
|
|
|
describe("SecretScannerService", () => {
|
|
let service: SecretScannerService;
|
|
let mockConfigService: ConfigService;
|
|
|
|
beforeEach(() => {
|
|
// Reset all mocks
|
|
vi.clearAllMocks();
|
|
|
|
// Create mock config service
|
|
mockConfigService = {
|
|
get: vi.fn().mockReturnValue(undefined),
|
|
} as unknown as ConfigService;
|
|
|
|
// Create service with mock
|
|
service = new SecretScannerService(mockConfigService);
|
|
});
|
|
|
|
it("should be defined", () => {
|
|
expect(service).toBeDefined();
|
|
});
|
|
|
|
describe("scanContent", () => {
|
|
describe("AWS Access Keys", () => {
|
|
it("should detect real AWS access keys", () => {
|
|
const content = 'const AWS_KEY = "AKIAREALKEY123456789";';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.count).toBe(1);
|
|
expect(result.matches).toHaveLength(1);
|
|
expect(result.matches[0].patternName).toBe("AWS Access Key");
|
|
expect(result.matches[0].severity).toBe("critical");
|
|
});
|
|
|
|
it("should not detect fake AWS keys with wrong format", () => {
|
|
const content = 'const FAKE_KEY = "AKIA1234";'; // Too short
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
expect(result.count).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("Claude API Keys", () => {
|
|
it("should detect Claude API keys", () => {
|
|
const content = 'CLAUDE_API_KEY="sk-ant-abc123def456ghi789jkl012mno345pqr678stu901vwx";';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.count).toBeGreaterThan(0);
|
|
const claudeMatch = result.matches.find((m) => m.patternName.includes("Claude"));
|
|
expect(claudeMatch).toBeDefined();
|
|
expect(claudeMatch?.severity).toBe("critical");
|
|
});
|
|
|
|
it("should not detect placeholder Claude keys", () => {
|
|
const content = 'CLAUDE_API_KEY="sk-ant-xxxx-your-key-here"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Generic API Keys", () => {
|
|
it("should detect API keys with various formats", () => {
|
|
const testCases = [
|
|
'api_key = "abc123def456"',
|
|
"apiKey: 'xyz789uvw123'",
|
|
'API_KEY="prod123key456"',
|
|
];
|
|
|
|
testCases.forEach((testCase) => {
|
|
const result = service.scanContent(testCase);
|
|
expect(result.hasSecrets).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("Passwords", () => {
|
|
it("should detect password assignments", () => {
|
|
const content = 'password = "mySecretPassword123"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
const passwordMatch = result.matches.find((m) =>
|
|
m.patternName.toLowerCase().includes("password")
|
|
);
|
|
expect(passwordMatch).toBeDefined();
|
|
});
|
|
|
|
it("should not detect password placeholders", () => {
|
|
const content = 'password = "your-password-here"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("Private Keys", () => {
|
|
it("should detect RSA private keys", () => {
|
|
const content = `-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEpAIBAAKCAQEA1234567890abcdef
|
|
-----END RSA PRIVATE KEY-----`;
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
const privateKeyMatch = result.matches.find((m) =>
|
|
m.patternName.toLowerCase().includes("private key")
|
|
);
|
|
expect(privateKeyMatch).toBeDefined();
|
|
expect(privateKeyMatch?.severity).toBe("critical");
|
|
});
|
|
|
|
it("should detect various private key types", () => {
|
|
const keyTypes = [
|
|
"RSA PRIVATE KEY",
|
|
"PRIVATE KEY",
|
|
"EC PRIVATE KEY",
|
|
"OPENSSH PRIVATE KEY",
|
|
];
|
|
|
|
keyTypes.forEach((keyType) => {
|
|
const content = `-----BEGIN ${keyType}-----
|
|
MIIEpAIBAAKCAQEA1234567890abcdef
|
|
-----END ${keyType}-----`;
|
|
const result = service.scanContent(content);
|
|
expect(result.hasSecrets).toBe(true);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("JWT Tokens", () => {
|
|
it("should detect JWT tokens", () => {
|
|
const content =
|
|
'token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
const jwtMatch = result.matches.find((m) => m.patternName.toLowerCase().includes("jwt"));
|
|
expect(jwtMatch).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("Bearer Tokens", () => {
|
|
it("should detect Bearer tokens", () => {
|
|
const content = "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9";
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
const bearerMatch = result.matches.find((m) =>
|
|
m.patternName.toLowerCase().includes("bearer")
|
|
);
|
|
expect(bearerMatch).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("Multiple Secrets", () => {
|
|
it("should detect multiple secrets in the same content", () => {
|
|
const content = `
|
|
const config = {
|
|
awsKey: "AKIAREALKEY123456789",
|
|
apiKey: "abc123def456",
|
|
password: "mySecret123"
|
|
};
|
|
`;
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.count).toBeGreaterThanOrEqual(3);
|
|
});
|
|
});
|
|
|
|
describe("Line and Column Tracking", () => {
|
|
it("should track line numbers correctly", () => {
|
|
const content = `line 1
|
|
line 2
|
|
const secret = "AKIAREALKEY123456789";
|
|
line 4`;
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.matches[0].line).toBe(3);
|
|
expect(result.matches[0].column).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should provide context for matches", () => {
|
|
const content = 'const key = "AKIAREALKEY123456789";';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.matches[0].context).toBeDefined();
|
|
});
|
|
});
|
|
|
|
describe("Clean Content", () => {
|
|
it("should return no secrets for clean content", () => {
|
|
const content = `
|
|
const greeting = "Hello World";
|
|
const number = 42;
|
|
function add(a, b) { return a + b; }
|
|
`;
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
expect(result.count).toBe(0);
|
|
expect(result.matches).toHaveLength(0);
|
|
});
|
|
|
|
it("should handle empty content", () => {
|
|
const result = service.scanContent("");
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
expect(result.count).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("Whitelisting", () => {
|
|
it("should not flag .env.example placeholder values", () => {
|
|
const content = `
|
|
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
|
|
API_KEY=your-api-key-here
|
|
SECRET_KEY=xxxxxxxxxxxx
|
|
`;
|
|
const result = service.scanContent(content, ".env.example");
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should flag real secrets even in .env files", () => {
|
|
const content = 'API_KEY="AKIAIOSFODNN7REALKEY123"';
|
|
const result = service.scanContent(content, ".env");
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
});
|
|
|
|
it("should whitelist placeholders in example files", () => {
|
|
const content = 'API_KEY="xxxxxxxxxxxx"';
|
|
const result = service.scanContent(content, "config.example.ts");
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should whitelist obvious placeholders like xxxx", () => {
|
|
const content = 'secret="xxxxxxxxxxxxxxxxxxxx"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should whitelist your-*-here patterns", () => {
|
|
const content = 'secret="your-secret-here"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should whitelist AWS EXAMPLE keys (official AWS documentation)", () => {
|
|
const content = 'const AWS_KEY = "AKIAIOSFODNN7EXAMPLE";';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should whitelist AWS keys with TEST suffix", () => {
|
|
const content = "AWS_ACCESS_KEY_ID=AKIATESTSECRET123456";
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should whitelist AWS keys with SAMPLE suffix", () => {
|
|
const content = 'key="AKIASAMPLEKEY1234567"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should whitelist AWS keys with DEMO suffix", () => {
|
|
const content = 'const demo = "AKIADEMOKEY123456789";';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should still detect real AWS keys without example markers", () => {
|
|
const content = "AWS_ACCESS_KEY_ID=AKIAREALKEY123456789";
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
});
|
|
|
|
it("should whitelist test/demo/sample placeholder patterns", () => {
|
|
const testCases = [
|
|
'password="test-password-123"',
|
|
'api_key="demo-api-key"',
|
|
'secret="sample-secret-value"',
|
|
];
|
|
|
|
testCases.forEach((testCase) => {
|
|
const result = service.scanContent(testCase);
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("should whitelist multiple xxxx patterns", () => {
|
|
const content = 'token="xxxx-some-text-xxxx"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
|
|
it("should not whitelist real secrets just because they contain word test", () => {
|
|
// "test" in the key name should not whitelist the actual secret value
|
|
const content = 'test_password="MyRealPassword123"';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
});
|
|
|
|
it("should handle case-insensitive EXAMPLE detection", () => {
|
|
const testCases = [
|
|
'key="AKIAexample12345678"',
|
|
'key="AKIAEXAMPLE12345678"',
|
|
'key="AKIAExample12345678"',
|
|
];
|
|
|
|
testCases.forEach((testCase) => {
|
|
const result = service.scanContent(testCase);
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
});
|
|
|
|
it("should not flag placeholder secrets in example files even without obvious patterns", () => {
|
|
const content = `
|
|
API_KEY=your-api-key-here
|
|
PASSWORD=change-me
|
|
SECRET=replace-me
|
|
`;
|
|
const result = service.scanContent(content, "config.example.yml");
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("scanFile", () => {
|
|
it("should scan a file and return results with secrets", async () => {
|
|
// Create a temp file with secrets
|
|
const fs = await import("fs/promises");
|
|
const path = await import("path");
|
|
const os = await import("os");
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
|
|
const testFile = path.join(tmpDir, "test.ts");
|
|
|
|
await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');
|
|
|
|
const result = await service.scanFile(testFile);
|
|
|
|
expect(result.filePath).toBe(testFile);
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.count).toBeGreaterThan(0);
|
|
|
|
// Cleanup
|
|
await fs.unlink(testFile);
|
|
await fs.rmdir(tmpDir);
|
|
});
|
|
|
|
it("should handle files without secrets", async () => {
|
|
const fs = await import("fs/promises");
|
|
const path = await import("path");
|
|
const os = await import("os");
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
|
|
const testFile = path.join(tmpDir, "clean.ts");
|
|
|
|
await fs.writeFile(testFile, 'const message = "Hello World";\n');
|
|
|
|
const result = await service.scanFile(testFile);
|
|
|
|
expect(result.filePath).toBe(testFile);
|
|
expect(result.hasSecrets).toBe(false);
|
|
expect(result.count).toBe(0);
|
|
|
|
// Cleanup
|
|
await fs.unlink(testFile);
|
|
await fs.rmdir(tmpDir);
|
|
});
|
|
|
|
it("should return error state for non-existent files", async () => {
|
|
const result = await service.scanFile("/non/existent/file.ts");
|
|
|
|
expect(result.hasSecrets).toBe(false);
|
|
expect(result.count).toBe(0);
|
|
expect(result.scannedSuccessfully).toBe(false);
|
|
expect(result.scanError).toBeDefined();
|
|
expect(result.scanError).toContain("ENOENT");
|
|
});
|
|
|
|
it("should return scannedSuccessfully true for successful scans", async () => {
|
|
const fs = await import("fs/promises");
|
|
const path = await import("path");
|
|
const os = await import("os");
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
|
|
const testFile = path.join(tmpDir, "clean.ts");
|
|
|
|
await fs.writeFile(testFile, 'const message = "Hello World";\n');
|
|
|
|
const result = await service.scanFile(testFile);
|
|
|
|
expect(result.scannedSuccessfully).toBe(true);
|
|
expect(result.scanError).toBeUndefined();
|
|
|
|
// Cleanup
|
|
await fs.unlink(testFile);
|
|
await fs.rmdir(tmpDir);
|
|
});
|
|
|
|
it("should return error state for unreadable files", async () => {
|
|
const fs = await import("fs/promises");
|
|
const path = await import("path");
|
|
const os = await import("os");
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
|
|
const testFile = path.join(tmpDir, "unreadable.ts");
|
|
|
|
await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');
|
|
// Remove read permissions
|
|
await fs.chmod(testFile, 0o000);
|
|
|
|
const result = await service.scanFile(testFile);
|
|
|
|
expect(result.scannedSuccessfully).toBe(false);
|
|
expect(result.scanError).toBeDefined();
|
|
expect(result.hasSecrets).toBe(false); // Not "clean", just unscanned
|
|
|
|
// Cleanup - restore permissions first
|
|
await fs.chmod(testFile, 0o644);
|
|
await fs.unlink(testFile);
|
|
await fs.rmdir(tmpDir);
|
|
});
|
|
});
|
|
|
|
describe("scanFiles", () => {
|
|
it("should scan multiple files", async () => {
|
|
const fs = await import("fs/promises");
|
|
const path = await import("path");
|
|
const os = await import("os");
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
|
|
const file1 = path.join(tmpDir, "file1.ts");
|
|
const file2 = path.join(tmpDir, "file2.ts");
|
|
|
|
await fs.writeFile(file1, 'const key = "AKIAREALKEY123456789";\n');
|
|
await fs.writeFile(file2, 'const msg = "Hello";\n');
|
|
|
|
const results = await service.scanFiles([file1, file2]);
|
|
|
|
expect(results).toHaveLength(2);
|
|
expect(results[0].hasSecrets).toBe(true);
|
|
expect(results[1].hasSecrets).toBe(false);
|
|
|
|
// Cleanup
|
|
await fs.unlink(file1);
|
|
await fs.unlink(file2);
|
|
await fs.rmdir(tmpDir);
|
|
});
|
|
});
|
|
|
|
describe("getScanSummary", () => {
|
|
it("should provide summary of scan results", () => {
|
|
const results = [
|
|
{
|
|
filePath: "file1.ts",
|
|
hasSecrets: true,
|
|
count: 2,
|
|
scannedSuccessfully: true,
|
|
matches: [
|
|
{
|
|
patternName: "AWS Access Key",
|
|
match: "AKIA...",
|
|
line: 1,
|
|
column: 1,
|
|
severity: "critical" as const,
|
|
},
|
|
{
|
|
patternName: "API Key",
|
|
match: "api_key",
|
|
line: 2,
|
|
column: 1,
|
|
severity: "high" as const,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
filePath: "file2.ts",
|
|
hasSecrets: false,
|
|
count: 0,
|
|
scannedSuccessfully: true,
|
|
matches: [],
|
|
},
|
|
];
|
|
|
|
const summary = service.getScanSummary(results);
|
|
|
|
expect(summary.totalFiles).toBe(2);
|
|
expect(summary.filesWithSecrets).toBe(1);
|
|
expect(summary.totalSecrets).toBe(2);
|
|
expect(summary.filesWithErrors).toBe(0);
|
|
expect(summary.bySeverity.critical).toBe(1);
|
|
expect(summary.bySeverity.high).toBe(1);
|
|
expect(summary.bySeverity.medium).toBe(0);
|
|
});
|
|
|
|
it("should count files with scan errors", () => {
|
|
const results = [
|
|
{
|
|
filePath: "file1.ts",
|
|
hasSecrets: true,
|
|
count: 1,
|
|
scannedSuccessfully: true,
|
|
matches: [
|
|
{
|
|
patternName: "AWS Access Key",
|
|
match: "AKIA...",
|
|
line: 1,
|
|
column: 1,
|
|
severity: "critical" as const,
|
|
},
|
|
],
|
|
},
|
|
{
|
|
filePath: "file2.ts",
|
|
hasSecrets: false,
|
|
count: 0,
|
|
scannedSuccessfully: false,
|
|
scanError: "ENOENT: no such file or directory",
|
|
matches: [],
|
|
},
|
|
{
|
|
filePath: "file3.ts",
|
|
hasSecrets: false,
|
|
count: 0,
|
|
scannedSuccessfully: false,
|
|
scanError: "EACCES: permission denied",
|
|
matches: [],
|
|
},
|
|
];
|
|
|
|
const summary = service.getScanSummary(results);
|
|
|
|
expect(summary.totalFiles).toBe(3);
|
|
expect(summary.filesWithSecrets).toBe(1);
|
|
expect(summary.filesWithErrors).toBe(2);
|
|
expect(summary.totalSecrets).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("SecretsDetectedError", () => {
|
|
it("should create error with results", () => {
|
|
const results = [
|
|
{
|
|
filePath: "test.ts",
|
|
hasSecrets: true,
|
|
count: 1,
|
|
scannedSuccessfully: true,
|
|
matches: [
|
|
{
|
|
patternName: "AWS Access Key",
|
|
match: "AKIAREALKEY123456789",
|
|
line: 1,
|
|
column: 10,
|
|
severity: "critical" as const,
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const error = new SecretsDetectedError(results);
|
|
|
|
expect(error.results).toBe(results);
|
|
expect(error.message).toContain("Secrets detected");
|
|
});
|
|
|
|
it("should provide detailed error message", () => {
|
|
const results = [
|
|
{
|
|
filePath: "config.ts",
|
|
hasSecrets: true,
|
|
count: 1,
|
|
scannedSuccessfully: true,
|
|
matches: [
|
|
{
|
|
patternName: "API Key",
|
|
match: "abc123",
|
|
line: 5,
|
|
column: 15,
|
|
severity: "high" as const,
|
|
context: 'const apiKey = "abc123"',
|
|
},
|
|
],
|
|
},
|
|
];
|
|
|
|
const error = new SecretsDetectedError(results);
|
|
const detailed = error.getDetailedMessage();
|
|
|
|
expect(detailed).toContain("SECRETS DETECTED");
|
|
expect(detailed).toContain("config.ts");
|
|
expect(detailed).toContain("Line 5:15");
|
|
expect(detailed).toContain("API Key");
|
|
});
|
|
|
|
it("should include scan errors in detailed message", () => {
|
|
const results = [
|
|
{
|
|
filePath: "config.ts",
|
|
hasSecrets: true,
|
|
count: 1,
|
|
scannedSuccessfully: true,
|
|
matches: [
|
|
{
|
|
patternName: "API Key",
|
|
match: "abc123",
|
|
line: 5,
|
|
column: 15,
|
|
severity: "high" as const,
|
|
context: 'const apiKey = "abc123"',
|
|
},
|
|
],
|
|
},
|
|
{
|
|
filePath: "unreadable.ts",
|
|
hasSecrets: false,
|
|
count: 0,
|
|
scannedSuccessfully: false,
|
|
scanError: "EACCES: permission denied",
|
|
matches: [],
|
|
},
|
|
];
|
|
|
|
const error = new SecretsDetectedError(results);
|
|
const detailed = error.getDetailedMessage();
|
|
|
|
expect(detailed).toContain("SECRETS DETECTED");
|
|
expect(detailed).toContain("config.ts");
|
|
expect(detailed).toContain("could not be scanned");
|
|
expect(detailed).toContain("unreadable.ts");
|
|
expect(detailed).toContain("EACCES: permission denied");
|
|
});
|
|
});
|
|
|
|
describe("Custom Patterns", () => {
|
|
it("should support adding custom patterns via config", () => {
|
|
// Create service with custom patterns
|
|
const customMockConfig = {
|
|
get: vi.fn((key: string) => {
|
|
if (key === "orchestrator.secretScanner.customPatterns") {
|
|
return [
|
|
{
|
|
name: "Custom Token",
|
|
pattern: /CUSTOM-[A-Z0-9]{10}/g,
|
|
description: "Custom token pattern",
|
|
severity: "high",
|
|
},
|
|
];
|
|
}
|
|
return undefined;
|
|
}),
|
|
} as unknown as ConfigService;
|
|
|
|
const customService = new SecretScannerService(customMockConfig);
|
|
const result = customService.scanContent("token = CUSTOM-ABCD123456");
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.matches.some((m) => m.patternName === "Custom Token")).toBe(true);
|
|
});
|
|
|
|
it("should respect exclude patterns from config", async () => {
|
|
const fs = await import("fs/promises");
|
|
const path = await import("path");
|
|
const os = await import("os");
|
|
|
|
const excludeMockConfig = {
|
|
get: vi.fn((key: string) => {
|
|
if (key === "orchestrator.secretScanner.excludePatterns") {
|
|
return ["*.test.ts"];
|
|
}
|
|
return undefined;
|
|
}),
|
|
} as unknown as ConfigService;
|
|
|
|
const excludeService = new SecretScannerService(excludeMockConfig);
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
|
|
const testFile = path.join(tmpDir, "file.test.ts");
|
|
|
|
await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');
|
|
|
|
const result = await excludeService.scanFile(testFile);
|
|
|
|
expect(result.hasSecrets).toBe(false); // Excluded files return no secrets
|
|
|
|
// Cleanup
|
|
await fs.unlink(testFile);
|
|
await fs.rmdir(tmpDir);
|
|
});
|
|
|
|
it("should respect max file size limit", async () => {
|
|
const fs = await import("fs/promises");
|
|
const path = await import("path");
|
|
const os = await import("os");
|
|
|
|
const sizeMockConfig = {
|
|
get: vi.fn((key: string) => {
|
|
if (key === "orchestrator.secretScanner.maxFileSize") {
|
|
return 10; // 10 bytes max
|
|
}
|
|
return undefined;
|
|
}),
|
|
} as unknown as ConfigService;
|
|
|
|
const sizeService = new SecretScannerService(sizeMockConfig);
|
|
|
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "secret-test-"));
|
|
const testFile = path.join(tmpDir, "large.ts");
|
|
|
|
// Create a file larger than 10 bytes
|
|
await fs.writeFile(testFile, 'const key = "AKIAREALKEY123456789";\n');
|
|
|
|
const result = await sizeService.scanFile(testFile);
|
|
|
|
expect(result.hasSecrets).toBe(false); // Large files are skipped
|
|
|
|
// Cleanup
|
|
await fs.unlink(testFile);
|
|
await fs.rmdir(tmpDir);
|
|
});
|
|
});
|
|
|
|
describe("Edge Cases", () => {
|
|
it("should handle very long lines", () => {
|
|
const longLine = "a".repeat(10000) + 'key="AKIAREALKEY123456789"';
|
|
const result = service.scanContent(longLine);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
});
|
|
|
|
it("should handle multiline private keys correctly", () => {
|
|
const content = `
|
|
Some text before
|
|
-----BEGIN RSA PRIVATE KEY-----
|
|
MIIEpAIBAAKCAQEA1234567890abcdef
|
|
ghijklmnopqrstuvwxyz123456789012
|
|
-----END RSA PRIVATE KEY-----
|
|
Some text after
|
|
`;
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
expect(result.count).toBeGreaterThan(0);
|
|
});
|
|
|
|
it("should handle content with special characters", () => {
|
|
const content = 'key="AKIAREALKEY123456789" # Comment with émojis 🔑';
|
|
const result = service.scanContent(content);
|
|
|
|
expect(result.hasSecrets).toBe(true);
|
|
});
|
|
});
|
|
});
|