diff --git a/src/__tests__/analysis-report-redaction.test.ts b/src/__tests__/analysis-report-redaction.test.ts new file mode 100644 index 0000000..af0ae34 --- /dev/null +++ b/src/__tests__/analysis-report-redaction.test.ts @@ -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]'); + }); +}); diff --git a/src/analysis/index.ts b/src/analysis/index.ts index 462f1c7..0619719 100755 --- a/src/analysis/index.ts +++ b/src/analysis/index.ts @@ -18,6 +18,7 @@ import type { MissionControl } from '../mission/index.js'; import type { OpsecController } from '../opsec/index.js'; import { randomUUID } from 'crypto'; import { SEVERITY_SCORES } from '../evidence/index.js'; +import { redactString } from '../redact.js'; // ============================================================================= // ANALYSIS ENGINE @@ -251,7 +252,7 @@ export class AnalysisEngine { // Executive Summary lines.push('## Executive Summary'); lines.push(''); - lines.push(report.summary.overview); + lines.push(redactString(report.summary.overview)); lines.push(''); lines.push('### Risk Overview'); lines.push(''); @@ -282,26 +283,27 @@ export class AnalysisEngine { ); for (const finding of sortedFindings) { - lines.push(`### ${finding.title}`); + lines.push(`### ${redactString(finding.title)}`); lines.push(''); lines.push(`**Severity:** ${finding.severity.toUpperCase()}`); if (finding.cvss) lines.push(`**CVSS:** ${finding.cvss}`); if (finding.cve?.length) lines.push(`**CVE:** ${finding.cve.join(', ')}`); lines.push(''); lines.push('**Description:**'); - lines.push(finding.description); + lines.push(redactString(finding.description)); lines.push(''); if (finding.remediation) { lines.push('**Remediation:**'); - lines.push(finding.remediation); + lines.push(redactString(finding.remediation)); lines.push(''); } if (finding.evidence.length > 0) { lines.push('**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(''); } @@ -313,15 +315,15 @@ export class AnalysisEngine { lines.push(''); for (const path of report.attackPaths) { - lines.push(`### ${path.name}`); + lines.push(`### ${redactString(path.name)}`); lines.push(''); lines.push(`**Impact Level:** ${path.impactLevel.toUpperCase()}`); lines.push(''); - lines.push(path.description); + lines.push(redactString(path.description)); lines.push(''); lines.push('**Steps:**'); 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(''); } @@ -342,8 +344,8 @@ export class AnalysisEngine { lines.push('### Immediate Priority'); lines.push(''); for (const rec of byPriority.immediate) { - lines.push(`- **${rec.title}** (Effort: ${rec.effort})`); - lines.push(` ${rec.description}`); + lines.push(`- **${redactString(rec.title)}** (Effort: ${rec.effort})`); + lines.push(` ${redactString(rec.description)}`); } lines.push(''); } @@ -352,8 +354,8 @@ export class AnalysisEngine { lines.push('### Short-Term Priority'); lines.push(''); for (const rec of byPriority.short_term) { - lines.push(`- **${rec.title}** (Effort: ${rec.effort})`); - lines.push(` ${rec.description}`); + lines.push(`- **${redactString(rec.title)}** (Effort: ${rec.effort})`); + lines.push(` ${redactString(rec.description)}`); } lines.push(''); } @@ -362,8 +364,8 @@ export class AnalysisEngine { lines.push('### Long-Term Priority'); lines.push(''); for (const rec of byPriority.long_term) { - lines.push(`- **${rec.title}** (Effort: ${rec.effort})`); - lines.push(` ${rec.description}`); + lines.push(`- **${redactString(rec.title)}** (Effort: ${rec.effort})`); + lines.push(` ${redactString(rec.description)}`); } lines.push(''); }