security(audit): detect API keys in auth profile names
Add comprehensive secret pattern detection for auth profile names. Introduces shared secret-patterns.ts with patterns for 40+ services including Anthropic, OpenAI, Google, GitHub, Slack, AWS, Stripe, etc. The audit now warns when profile names contain what looks like an actual API key, since profile names appear in logs, UI dropdowns, and error messages. Closes #2321 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
f5c90f0e5c
commit
e90d8406b4
@ -28,6 +28,7 @@ import {
|
|||||||
safeStat,
|
safeStat,
|
||||||
} from "./audit-fs.js";
|
} from "./audit-fs.js";
|
||||||
import type { ExecFn } from "./windows-acl.js";
|
import type { ExecFn } from "./windows-acl.js";
|
||||||
|
import { getDefaultRedactPatterns } from "../logging/redact.js";
|
||||||
|
|
||||||
export type SecurityAuditFinding = {
|
export type SecurityAuditFinding = {
|
||||||
checkId: string;
|
checkId: string;
|
||||||
@ -127,6 +128,69 @@ function looksLikeEnvRef(value: string): boolean {
|
|||||||
return v.startsWith("${") && v.endsWith("}");
|
return v.startsWith("${") && v.endsWith("}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function hasHighEntropy(value: string, minLength = 40, minRatio = 0.4): boolean {
|
||||||
|
if (value.length < minLength) return false;
|
||||||
|
if (!/^[A-Za-z0-9_-]+$/.test(value)) return false;
|
||||||
|
const entropy = new Set(value).size / value.length;
|
||||||
|
return entropy > minRatio;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function collectApiKeyInProfileNameFindings(cfg: ClawdbotConfig): SecurityAuditFinding[] {
|
||||||
|
const findings: SecurityAuditFinding[] = [];
|
||||||
|
const profiles = cfg.auth?.profiles;
|
||||||
|
if (!profiles || typeof profiles !== "object") return findings;
|
||||||
|
|
||||||
|
// Compile redact patterns for matching
|
||||||
|
const patterns = getDefaultRedactPatterns()
|
||||||
|
.map((p) => {
|
||||||
|
try {
|
||||||
|
return new RegExp(p, "i");
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.filter((r): r is RegExp => r !== null);
|
||||||
|
|
||||||
|
const matches: string[] = [];
|
||||||
|
|
||||||
|
for (const profileKey of Object.keys(profiles)) {
|
||||||
|
// Profile keys are like "provider:name" - check the name part
|
||||||
|
const parts = profileKey.split(":");
|
||||||
|
const namePart = parts.length > 1 ? parts.slice(1).join(":") : profileKey;
|
||||||
|
|
||||||
|
// Check against redact patterns (same patterns used for log redaction)
|
||||||
|
let matched = false;
|
||||||
|
for (const pattern of patterns) {
|
||||||
|
if (pattern.test(namePart)) {
|
||||||
|
matches.push(profileKey);
|
||||||
|
matched = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: high-entropy check for unknown secret formats
|
||||||
|
if (!matched && hasHighEntropy(namePart)) {
|
||||||
|
matches.push(profileKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (matches.length > 0) {
|
||||||
|
const lines = matches.map((m) => `- "${m}"`).join("\n");
|
||||||
|
findings.push({
|
||||||
|
checkId: "auth.profiles.key_in_name",
|
||||||
|
severity: "warn",
|
||||||
|
title: "API key detected in auth profile name",
|
||||||
|
detail:
|
||||||
|
`The following auth profile names appear to contain API keys:\n${lines}\n` +
|
||||||
|
"Profile names are not secrets and may appear in logs, error messages, and UI dropdowns.",
|
||||||
|
remediation:
|
||||||
|
'Rename profiles to descriptive IDs like "anthropic:personal" or "openai:work" instead of using the actual key as the name.',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return findings;
|
||||||
|
}
|
||||||
|
|
||||||
export function collectSecretsInConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding[] {
|
export function collectSecretsInConfigFindings(cfg: ClawdbotConfig): SecurityAuditFinding[] {
|
||||||
const findings: SecurityAuditFinding[] = [];
|
const findings: SecurityAuditFinding[] = [];
|
||||||
const password =
|
const password =
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { formatCliCommand } from "../cli/command-format.js";
|
|||||||
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
import { buildGatewayConnectionDetails } from "../gateway/call.js";
|
||||||
import { probeGateway } from "../gateway/probe.js";
|
import { probeGateway } from "../gateway/probe.js";
|
||||||
import {
|
import {
|
||||||
|
collectApiKeyInProfileNameFindings,
|
||||||
collectAttackSurfaceSummaryFindings,
|
collectAttackSurfaceSummaryFindings,
|
||||||
collectExposureMatrixFindings,
|
collectExposureMatrixFindings,
|
||||||
collectHooksHardeningFindings,
|
collectHooksHardeningFindings,
|
||||||
@ -911,6 +912,7 @@ export async function runSecurityAudit(opts: SecurityAuditOptions): Promise<Secu
|
|||||||
findings.push(...collectElevatedFindings(cfg));
|
findings.push(...collectElevatedFindings(cfg));
|
||||||
findings.push(...collectHooksHardeningFindings(cfg));
|
findings.push(...collectHooksHardeningFindings(cfg));
|
||||||
findings.push(...collectSecretsInConfigFindings(cfg));
|
findings.push(...collectSecretsInConfigFindings(cfg));
|
||||||
|
findings.push(...collectApiKeyInProfileNameFindings(cfg));
|
||||||
findings.push(...collectModelHygieneFindings(cfg));
|
findings.push(...collectModelHygieneFindings(cfg));
|
||||||
findings.push(...collectSmallModelRiskFindings({ cfg, env }));
|
findings.push(...collectSmallModelRiskFindings({ cfg, env }));
|
||||||
findings.push(...collectExposureMatrixFindings(cfg));
|
findings.push(...collectExposureMatrixFindings(cfg));
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user