Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions src/__tests__/scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,59 @@ describe('INSECURE_TRANSPORT rule', () => {
});
});

// ─── EXPOSED_SECRETS rule ─────────────────────────────────────────────────────

describe('EXPOSED_SECRETS rule', () => {
// Synthetic fixtures: each value is assembled from its prefix plus a filler
// body so that no contiguous, real-looking credential literal ever appears in
// this source file (which would otherwise trip secret-scanning push
// protection). The filler still satisfies each pattern's character class and
// length requirement.
const body = (n: number) => 'a'.repeat(n);
const secrets: Array<[string, string]> = [
['legacy OpenAI key', 'sk-' + body(40)],
['OpenAI project key', 'sk-' + 'proj-' + body(40)],
['OpenAI service-account key', 'sk-' + 'svcacct-' + body(40)],
['Anthropic key', 'sk-' + 'ant-' + body(40)],
['GitHub classic PAT', 'ghp' + '_' + body(36)],
['GitHub OAuth token', 'gho' + '_' + body(36)],
['GitHub fine-grained PAT', 'github' + '_pat_' + body(82)],
['AWS access key id', 'AKIA' + 'A'.repeat(16)],
['Google API key', 'AIza' + body(35)],
['Slack bot token', 'xoxb' + '-1234567890-' + body(16)],
['Stripe live key', 'sk' + '_live_' + body(24)],
['password assignment', 'password=' + body(14)],
];

it.each(secrets)('fires for a %s', (_label, value) => {
const config: ParsedMcpConfig = { rawStrings: [value] };
const findings = runRuntimeRule(RuleId.EXPOSED_SECRETS, config);
expect(findings).toHaveLength(1);
expect(findings[0].ruleId).toBe(RuleId.EXPOSED_SECRETS);
expect(findings[0].severity).toBe(Severity.CRITICAL);
});

it('does not fire for benign config strings', () => {
const config: ParsedMcpConfig = {
rawStrings: [
'https://secure-mcp.example.com',
'bearer',
'filesystem:read',
// JWT-style bearer token (assembled from parts) — not a known secret
// format, must not trigger a false positive.
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9' + '.' + 'c2VjcmV0LXRva2VuLWZvcnR5LXR3by1jaGFycw',
],
};
const findings = runRuntimeRule(RuleId.EXPOSED_SECRETS, config);
expect(findings).toHaveLength(0);
});

it('does not fire when rawStrings is absent', () => {
const findings = runRuntimeRule(RuleId.EXPOSED_SECRETS, {});
expect(findings).toHaveLength(0);
});
});

// ─── failOn behaviour ─────────────────────────────────────────────────────────

describe('failOn config', () => {
Expand Down
25 changes: 21 additions & 4 deletions src/rules/runtime-rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,27 @@ import { ParsedMcpConfig } from '../parser.js';
import { Rule } from './index.js';

const SECRET_PATTERNS: RegExp[] = [
/sk-[a-zA-Z0-9]{20,}/i, // OpenAI API key
/ghp_[a-zA-Z0-9]{36}/i, // GitHub PAT
/AKIA[0-9A-Z]{16}/i, // AWS access key
/[Pp]assword\s*[=:]\s*\S{8,}/, // Password assignment
// OpenAI / Anthropic keys. Modern keys carry a prefix and may contain
// hyphens/underscores in the body: project (sk-proj-), service account
// (sk-svcacct-), admin (sk-admin-), and Anthropic (sk-ant-) keys.
/sk-(?:proj|svcacct|admin|ant)-[A-Za-z0-9_-]{20,}/i,
// Legacy unprefixed OpenAI keys (sk- followed by a long alphanumeric run).
/sk-[A-Za-z0-9]{32,}/i,
// GitHub tokens: PAT (ghp_), OAuth (gho_), user-to-server (ghu_),
// server-to-server (ghs_), and refresh (ghr_) tokens.
/gh[pousr]_[A-Za-z0-9]{36,}/,
// GitHub fine-grained personal access tokens.
/github_pat_[A-Za-z0-9_]{22,}/,
// AWS access key ID.
/AKIA[0-9A-Z]{16}/,
// Google API key.
/AIza[0-9A-Za-z_-]{35}/,
// Slack tokens (bot/user/app/refresh).
/xox[baprs]-[0-9A-Za-z-]{10,}/,
// Stripe live secret / restricted keys.
/(?:sk|rk)_live_[0-9A-Za-z]{16,}/,
// Password assignment.
/[Pp]assword\s*[=:]\s*\S{8,}/,
];

export const runtimeRules: Rule[] = [
Expand Down
Loading