Skip to content

Commit cedc501

Browse files
committed
fix: classify credential gated component audits
Signed-off-by: zebbern <185730623+zebbern@users.noreply.github.com>
1 parent 5867f5a commit cedc501

3 files changed

Lines changed: 127 additions & 20 deletions

File tree

scripts/__tests__/security-component-audit-utils.test.ts

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
SECURITY_COMPONENT_IDS,
44
createSecurityComponentFingerprint,
55
renderSecurityComponentLedgerFreshness,
6+
shouldSkipSecurityComponentLiveAudit,
67
summarizeSecurityComponentLedgerFreshness,
78
SECURITY_COMPONENT_LIVE_FIXTURES,
89
} from '../security-component-audit-utils';
@@ -28,4 +29,91 @@ describe('security-component-audit-utils', () => {
2829
expect(summary.items[0]?.status).toBe('missing');
2930
expect(renderSecurityComponentLedgerFreshness(summary)).toContain('CURRENT');
3031
});
32+
33+
it('skips credential-gated fixtures with missing secrets even when force is requested', () => {
34+
const fixture = SECURITY_COMPONENT_LIVE_FIXTURES['security.virustotal.lookup'];
35+
const fingerprint = createSecurityComponentFingerprint('security.virustotal.lookup', fixture);
36+
const originalValue = process.env.VIRUSTOTAL_API_KEY;
37+
delete process.env.VIRUSTOTAL_API_KEY;
38+
39+
try {
40+
const skipped = shouldSkipSecurityComponentLiveAudit({
41+
ledger: undefined,
42+
componentId: 'security.virustotal.lookup',
43+
fingerprint,
44+
force: true,
45+
fixture,
46+
});
47+
48+
expect(skipped).toMatchObject({
49+
componentId: 'security.virustotal.lookup',
50+
fingerprint,
51+
tier: 'C',
52+
status: 'skipped',
53+
error: 'Requires VirusTotal API key',
54+
});
55+
} finally {
56+
if (originalValue === undefined) {
57+
delete process.env.VIRUSTOTAL_API_KEY;
58+
} else {
59+
process.env.VIRUSTOTAL_API_KEY = originalValue;
60+
}
61+
}
62+
});
63+
64+
it('lets forced credential-gated fixtures run when all required secrets are present', () => {
65+
const fixture = SECURITY_COMPONENT_LIVE_FIXTURES['security.virustotal.lookup'];
66+
const fingerprint = createSecurityComponentFingerprint('security.virustotal.lookup', fixture);
67+
const originalValue = process.env.VIRUSTOTAL_API_KEY;
68+
process.env.VIRUSTOTAL_API_KEY = 'present';
69+
70+
try {
71+
const skipped = shouldSkipSecurityComponentLiveAudit({
72+
ledger: undefined,
73+
componentId: 'security.virustotal.lookup',
74+
fingerprint,
75+
force: true,
76+
fixture,
77+
});
78+
79+
expect(skipped).toBeUndefined();
80+
} finally {
81+
if (originalValue === undefined) {
82+
delete process.env.VIRUSTOTAL_API_KEY;
83+
} else {
84+
process.env.VIRUSTOTAL_API_KEY = originalValue;
85+
}
86+
}
87+
});
88+
89+
it('skips missing required secrets even without a custom skip reason', () => {
90+
const fixture = {
91+
tier: 'C' as const,
92+
inputs: {},
93+
params: {},
94+
requiresSecrets: ['CUSTOM_REQUIRED_SECRET'],
95+
};
96+
const fingerprint = createSecurityComponentFingerprint('sentris.subfinder.run', fixture);
97+
const originalValue = process.env.CUSTOM_REQUIRED_SECRET;
98+
delete process.env.CUSTOM_REQUIRED_SECRET;
99+
100+
try {
101+
const skipped = shouldSkipSecurityComponentLiveAudit({
102+
ledger: undefined,
103+
componentId: 'sentris.subfinder.run',
104+
fingerprint,
105+
force: true,
106+
fixture,
107+
});
108+
109+
expect(skipped?.status).toBe('skipped');
110+
expect(skipped?.error).toBe('Missing required secrets: CUSTOM_REQUIRED_SECRET');
111+
} finally {
112+
if (originalValue === undefined) {
113+
delete process.env.CUSTOM_REQUIRED_SECRET;
114+
} else {
115+
process.env.CUSTOM_REQUIRED_SECRET = originalValue;
116+
}
117+
}
118+
});
31119
});

scripts/security-component-audit-utils.ts

Lines changed: 22 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ export interface SecurityComponentAuditCliOptions {
4646
sequential: boolean;
4747
}
4848

49-
export function parseSecurityComponentAuditCliOptions(argv: string[]): SecurityComponentAuditCliOptions {
49+
export function parseSecurityComponentAuditCliOptions(
50+
argv: string[],
51+
): SecurityComponentAuditCliOptions {
5052
const componentIds = new Set<string>();
5153
let force = false;
5254
let ledgerCheckOnly = false;
@@ -334,9 +336,7 @@ export function summarizeSecurityComponentLedgerFreshness(
334336
const entry = ledger?.entries[componentId];
335337

336338
if (fixture.skipReason || (fixture.requiresSecrets?.length ?? 0) > 0) {
337-
const hasSecrets = (fixture.requiresSecrets ?? []).every(
338-
(name) => process.env[name]?.trim(),
339-
);
339+
const hasSecrets = (fixture.requiresSecrets ?? []).every((name) => process.env[name]?.trim());
340340
if (!hasSecrets) {
341341
return {
342342
componentId,
@@ -403,10 +403,7 @@ export function summarizeSecurityComponentLedgerFreshness(
403403
});
404404

405405
const allCurrent = items.every(
406-
(item) =>
407-
item.status === 'current' ||
408-
item.status === 'skipped' ||
409-
item.status === 'missing',
406+
(item) => item.status === 'current' || item.status === 'skipped' || item.status === 'missing',
410407
);
411408

412409
return { allCurrent, items };
@@ -423,9 +420,10 @@ export function renderSecurityComponentLedgerFreshness(summary: {
423420
`- ${item.componentId} [${item.tier}] ${item.status}${item.verifiedAt ? ` (${item.verifiedAt})` : ''}: ${item.rationale}`,
424421
);
425422

426-
return [`Security component ledger: ${summary.allCurrent ? 'CURRENT' : 'NEEDS ATTENTION'}`, ...lines].join(
427-
'\n',
428-
);
423+
return [
424+
`Security component ledger: ${summary.allCurrent ? 'CURRENT' : 'NEEDS ATTENTION'}`,
425+
...lines,
426+
].join('\n');
429427
}
430428

431429
export function shouldSkipSecurityComponentLiveAudit(options: {
@@ -435,21 +433,29 @@ export function shouldSkipSecurityComponentLiveAudit(options: {
435433
force: boolean;
436434
fixture: SecurityComponentAuditFixture;
437435
}): SecurityComponentLedgerEntry | undefined {
438-
if (options.force) {
439-
return undefined;
440-
}
436+
const requiredSecrets = options.fixture.requiresSecrets ?? [];
437+
const missingRequiredSecrets = requiredSecrets.filter((name) => !process.env[name]?.trim());
438+
const missingCredentialGate =
439+
missingRequiredSecrets.length > 0 ||
440+
(requiredSecrets.length === 0 && options.fixture.skipReason !== undefined);
441441

442-
if (options.fixture.skipReason && !(options.fixture.requiresSecrets ?? []).some((name) => process.env[name]?.trim())) {
442+
if (missingCredentialGate) {
443443
return {
444444
componentId: options.componentId,
445445
fingerprint: options.fingerprint,
446446
tier: options.fixture.tier,
447447
status: 'skipped',
448-
error: options.fixture.skipReason,
448+
error:
449+
options.fixture.skipReason ??
450+
`Missing required secrets: ${missingRequiredSecrets.join(', ')}`,
449451
verifiedAt: new Date().toISOString(),
450452
};
451453
}
452454

455+
if (options.force) {
456+
return undefined;
457+
}
458+
453459
const existing = options.ledger?.entries[options.componentId];
454460
if (existing?.fingerprint === options.fingerprint && existing.status === 'passed') {
455461
return existing;

scripts/security-component-live-audit.ts

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ if (cli.ledgerCheckOnly) {
4040
process.exit(process.exitCode ?? 0);
4141
}
4242

43-
const outputRoot = join(process.cwd(), '.cache', 'security-component-audits', new Date().toISOString().slice(0, 10));
43+
const outputRoot = join(
44+
process.cwd(),
45+
'.cache',
46+
'security-component-audits',
47+
new Date().toISOString().slice(0, 10),
48+
);
4449
mkdirSync(outputRoot, { recursive: true });
4550

4651
interface AuditResult {
@@ -63,14 +68,17 @@ for (const componentId of selectedIds) {
6368
fixture,
6469
});
6570

66-
if (skipped && !cli.force) {
71+
if (skipped) {
6772
console.log(`SKIP ${componentId}: ${skipped.error ?? 'ledger current'}`);
6873
results.push({
6974
componentId,
7075
status: skipped.status === 'passed' ? 'passed' : 'skipped',
7176
durationMs: skipped.durationMs,
7277
error: skipped.error,
7378
});
79+
if (skipped.status === 'skipped') {
80+
ledger = upsertSecurityComponentLedgerEntry(ledger, skipped);
81+
}
7482
continue;
7583
}
7684

@@ -126,10 +134,15 @@ for (const componentId of selectedIds) {
126134
}
127135

128136
writeSecurityComponentLedger(ledger, getDefaultLedgerPath());
129-
writeFileSync(join(outputRoot, 'security-component-live-audit.json'), JSON.stringify({ results }, null, 2));
137+
writeFileSync(
138+
join(outputRoot, 'security-component-live-audit.json'),
139+
JSON.stringify({ results }, null, 2),
140+
);
130141

131142
const failures = results.filter((result) => result.status === 'failed');
132143
if (failures.length > 0) {
133144
process.exitCode = 1;
134-
console.error(`Security component live audit failures: ${failures.map((item) => item.componentId).join(', ')}`);
145+
console.error(
146+
`Security component live audit failures: ${failures.map((item) => item.componentId).join(', ')}`,
147+
);
135148
}

0 commit comments

Comments
 (0)