fix: redact secrets in markdown reports
This commit is contained in:
1 parent
a5667374bf
commit
a4ccbc6432
2 files changed
+103
-14
No files matched your search
@@ -0,0 +1,87 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { AnalysisEngine } from '../analysis/index.js';
|
||||||
|
import type { Report } from '../types/index.js';
|
||||||
|
import { KillChainPhase } from '../types/index.js';
|
||||||
|
|
||||||
|
function makeReport(): Report {
|
||||||
|
return {
|
||||||
|
id: 'report-1',
|
||||||
|
missionId: 'mission-1',
|
||||||
|
type: 'full_report',
|
||||||
|
generatedAt: 123,
|
||||||
|
summary: {
|
||||||
|
overview: 'Summary includes Bearer summaryabcdefghijklmnop',
|
||||||
|
riskRating: 'critical',
|
||||||
|
criticalFindings: 1,
|
||||||
|
highFindings: 0,
|
||||||
|
mediumFindings: 0,
|
||||||
|
lowFindings: 0,
|
||||||
|
infoFindings: 0,
|
||||||
|
successfulExploits: 1,
|
||||||
|
credentialsHarvested: 1,
|
||||||
|
systemsCompromised: 1,
|
||||||
|
},
|
||||||
|
findings: [
|
||||||
|
{
|
||||||
|
id: 'finding-1',
|
||||||
|
title: 'Leaked token',
|
||||||
|
description: 'Captured Anthropic key sk-ant-api03-ABCDEFGHIJKLMNOP in config',
|
||||||
|
severity: 'critical',
|
||||||
|
targetId: 'target-1',
|
||||||
|
operatorId: 'operator-1',
|
||||||
|
phase: KillChainPhase.EXPLOIT,
|
||||||
|
evidence: [
|
||||||
|
{
|
||||||
|
type: 'output',
|
||||||
|
content: 'curl -H "Authorization: Bearer evidenceabcdefghijklmnop" https://svc.local',
|
||||||
|
timestamp: 123,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
remediation: 'Rotate token=rawsecretvalue123456 immediately',
|
||||||
|
discoveredAt: 123,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
attackPaths: [
|
||||||
|
{
|
||||||
|
id: 'path-1',
|
||||||
|
name: 'Use https://admin:hunter2@example.test/login',
|
||||||
|
description: 'Pivot with Bearer attackpathabcdefghijklmnop',
|
||||||
|
steps: ['Replay sk-ant-api03-QRSTUVWXABCDEFGHIJKLMNOP against API'],
|
||||||
|
findings: ['finding-1'],
|
||||||
|
impactLevel: 'critical',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
recommendations: [
|
||||||
|
{
|
||||||
|
id: 'rec-1',
|
||||||
|
findingId: 'finding-1',
|
||||||
|
priority: 'immediate',
|
||||||
|
title: 'Remove API key',
|
||||||
|
description: 'Delete api_key=anotherrawsecretvalue from docs',
|
||||||
|
effort: 'low',
|
||||||
|
impact: 'high',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('AnalysisEngine report redaction', () => {
|
||||||
|
it('redacts secrets before exporting Markdown reports', () => {
|
||||||
|
const engine = Object.create(AnalysisEngine.prototype) as AnalysisEngine;
|
||||||
|
const markdown = engine.exportToMarkdown(makeReport());
|
||||||
|
|
||||||
|
for (const raw of [
|
||||||
|
'sk-ant-api03-ABCDEFGHIJKLMNOP',
|
||||||
|
'Bearer summaryabcdefghijklmnop',
|
||||||
|
'Bearer evidenceabcdefghijklmnop',
|
||||||
|
'rawsecretvalue123456',
|
||||||
|
'hunter2',
|
||||||
|
'attackpathabcdefghijklmnop',
|
||||||
|
'sk-ant-api03-QRSTUVWXABCDEFGHIJKLMNOP',
|
||||||
|
'anotherrawsecretvalue',
|
||||||
|
]) {
|
||||||
|
expect(markdown).not.toContain(raw);
|
||||||
|
}
|
||||||
|
expect(markdown).toContain('[redacted]');
|
||||||
|
});
|
||||||
|
});
|
||||||
+16
-14
@@ -18,6 +18,7 @@ import type { MissionControl } from '../mission/index.js';
|
|||||||
import type { OpsecController } from '../opsec/index.js';
|
import type { OpsecController } from '../opsec/index.js';
|
||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { SEVERITY_SCORES } from '../evidence/index.js';
|
import { SEVERITY_SCORES } from '../evidence/index.js';
|
||||||
|
import { redactString } from '../redact.js';
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// ANALYSIS ENGINE
|
// ANALYSIS ENGINE
|
||||||
@@ -251,7 +252,7 @@ export class AnalysisEngine {
|
|||||||
// Executive Summary
|
// Executive Summary
|
||||||
lines.push('## Executive Summary');
|
lines.push('## Executive Summary');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(report.summary.overview);
|
lines.push(redactString(report.summary.overview));
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('### Risk Overview');
|
lines.push('### Risk Overview');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
@@ -282,26 +283,27 @@ export class AnalysisEngine {
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const finding of sortedFindings) {
|
for (const finding of sortedFindings) {
|
||||||
lines.push(`### ${finding.title}`);
|
lines.push(`### ${redactString(finding.title)}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
|
lines.push(`**Severity:** ${finding.severity.toUpperCase()}`);
|
||||||
if (finding.cvss) lines.push(`**CVSS:** ${finding.cvss}`);
|
if (finding.cvss) lines.push(`**CVSS:** ${finding.cvss}`);
|
||||||
if (finding.cve?.length) lines.push(`**CVE:** ${finding.cve.join(', ')}`);
|
if (finding.cve?.length) lines.push(`**CVE:** ${finding.cve.join(', ')}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('**Description:**');
|
lines.push('**Description:**');
|
||||||
lines.push(finding.description);
|
lines.push(redactString(finding.description));
|
||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
if (finding.remediation) {
|
if (finding.remediation) {
|
||||||
lines.push('**Remediation:**');
|
lines.push('**Remediation:**');
|
||||||
lines.push(finding.remediation);
|
lines.push(redactString(finding.remediation));
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (finding.evidence.length > 0) {
|
if (finding.evidence.length > 0) {
|
||||||
lines.push('**Evidence:**');
|
lines.push('**Evidence:**');
|
||||||
for (const evidence of finding.evidence) {
|
for (const evidence of finding.evidence) {
|
||||||
lines.push(`- ${evidence.type}: \`${evidence.content.substring(0, 100)}${evidence.content.length > 100 ? '...' : ''}\``);
|
const content = redactString(evidence.content);
|
||||||
|
lines.push(`- ${evidence.type}: \`${content.substring(0, 100)}${content.length > 100 ? '...' : ''}\``);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
@@ -313,15 +315,15 @@ export class AnalysisEngine {
|
|||||||
lines.push('');
|
lines.push('');
|
||||||
|
|
||||||
for (const path of report.attackPaths) {
|
for (const path of report.attackPaths) {
|
||||||
lines.push(`### ${path.name}`);
|
lines.push(`### ${redactString(path.name)}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(`**Impact Level:** ${path.impactLevel.toUpperCase()}`);
|
lines.push(`**Impact Level:** ${path.impactLevel.toUpperCase()}`);
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push(path.description);
|
lines.push(redactString(path.description));
|
||||||
lines.push('');
|
lines.push('');
|
||||||
lines.push('**Steps:**');
|
lines.push('**Steps:**');
|
||||||
for (let i = 0; i < path.steps.length; i++) {
|
for (let i = 0; i < path.steps.length; i++) {
|
||||||
lines.push(`${i + 1}. ${path.steps[i]}`);
|
lines.push(`${i + 1}. ${redactString(path.steps[i])}`);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
@@ -342,8 +344,8 @@ export class AnalysisEngine {
|
|||||||
lines.push('### Immediate Priority');
|
lines.push('### Immediate Priority');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
for (const rec of byPriority.immediate) {
|
for (const rec of byPriority.immediate) {
|
||||||
lines.push(`- **${rec.title}** (Effort: ${rec.effort})`);
|
lines.push(`- **${redactString(rec.title)}** (Effort: ${rec.effort})`);
|
||||||
lines.push(` ${rec.description}`);
|
lines.push(` ${redactString(rec.description)}`);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
@@ -352,8 +354,8 @@ export class AnalysisEngine {
|
|||||||
lines.push('### Short-Term Priority');
|
lines.push('### Short-Term Priority');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
for (const rec of byPriority.short_term) {
|
for (const rec of byPriority.short_term) {
|
||||||
lines.push(`- **${rec.title}** (Effort: ${rec.effort})`);
|
lines.push(`- **${redactString(rec.title)}** (Effort: ${rec.effort})`);
|
||||||
lines.push(` ${rec.description}`);
|
lines.push(` ${redactString(rec.description)}`);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
@@ -362,8 +364,8 @@ export class AnalysisEngine {
|
|||||||
lines.push('### Long-Term Priority');
|
lines.push('### Long-Term Priority');
|
||||||
lines.push('');
|
lines.push('');
|
||||||
for (const rec of byPriority.long_term) {
|
for (const rec of byPriority.long_term) {
|
||||||
lines.push(`- **${rec.title}** (Effort: ${rec.effort})`);
|
lines.push(`- **${redactString(rec.title)}** (Effort: ${rec.effort})`);
|
||||||
lines.push(` ${rec.description}`);
|
lines.push(` ${redactString(rec.description)}`);
|
||||||
}
|
}
|
||||||
lines.push('');
|
lines.push('');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user