mirror of
https://github.com/KeygraphHQ/shannon.git
synced 2026-09-22 09:40:53 +02:00
feat(worker): structure intermediate deliverables via MCP collectors (#350)
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
// Copyright (C) 2025 Keygraph, Inc.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License version 3
|
||||
// as published by the Free Software Foundation.
|
||||
|
||||
/**
|
||||
* Deterministic exploit collector → markdown renderer.
|
||||
*
|
||||
* Single entry point renderExploitDeliverable(vulnClass, state, idToType)
|
||||
* covers all 5 exploitation agents (injection, xss, auth, ssrf, authz). The
|
||||
* per-class deltas are limited to title and ID prefix; every section, label,
|
||||
* and sort rule is class-agnostic. Section headers and bolded field labels
|
||||
* give downstream report-executive — which reads prose with bolded labels —
|
||||
* a consistent structure to parse, with a single canonical label per field
|
||||
* across all classes.
|
||||
*
|
||||
* Sort order is owned by the renderer:
|
||||
* - Successfully Exploited: severity desc (critical → low), then ID asc.
|
||||
* - Potential / Validation Blocked: confidence desc (high → low), then ID asc.
|
||||
*/
|
||||
|
||||
import type { AddExploitInput, VulnClass } from '../mcp-server/exploit-collector.js';
|
||||
|
||||
// ============================================================================
|
||||
// PER-CLASS CONSTANTS
|
||||
// ============================================================================
|
||||
|
||||
const TITLES: Record<VulnClass, string> = {
|
||||
injection: 'Injection Exploitation Evidence',
|
||||
xss: 'Cross-Site Scripting (XSS) Exploitation Evidence',
|
||||
auth: 'Authentication Exploitation Evidence',
|
||||
ssrf: 'SSRF Exploitation Evidence',
|
||||
authz: 'Authorization Exploitation Evidence',
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// SORT ORDER
|
||||
// ============================================================================
|
||||
|
||||
const SEVERITY_ORDER: Record<'critical' | 'high' | 'medium' | 'low', number> = {
|
||||
critical: 0,
|
||||
high: 1,
|
||||
medium: 2,
|
||||
low: 3,
|
||||
};
|
||||
|
||||
const CONFIDENCE_ORDER: Record<'high' | 'medium' | 'low', number> = {
|
||||
high: 0,
|
||||
medium: 1,
|
||||
low: 2,
|
||||
};
|
||||
|
||||
type ExploitedEntry = Extract<AddExploitInput, { status: 'exploited' }>;
|
||||
type BlockedEntry = Extract<AddExploitInput, { status: 'blocked' }>;
|
||||
|
||||
function sortExploited(entries: readonly ExploitedEntry[]): ExploitedEntry[] {
|
||||
return [...entries].sort((a, b) => {
|
||||
const sevDiff = SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity];
|
||||
if (sevDiff !== 0) return sevDiff;
|
||||
return a.vulnerability_id.localeCompare(b.vulnerability_id);
|
||||
});
|
||||
}
|
||||
|
||||
function sortBlocked(entries: readonly BlockedEntry[]): BlockedEntry[] {
|
||||
return [...entries].sort((a, b) => {
|
||||
const confDiff = CONFIDENCE_ORDER[a.confidence] - CONFIDENCE_ORDER[b.confidence];
|
||||
if (confDiff !== 0) return confDiff;
|
||||
return a.vulnerability_id.localeCompare(b.vulnerability_id);
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// FIELD FORMATTERS
|
||||
// ============================================================================
|
||||
|
||||
function capitalize(value: string): string {
|
||||
if (value.length === 0) return value;
|
||||
return value[0]!.toUpperCase() + value.slice(1);
|
||||
}
|
||||
|
||||
function renderNumberedList(steps: readonly string[]): string {
|
||||
return steps.map((step, idx) => `${idx + 1}. ${step}`).join('\n\n');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PER-FINDING RENDERERS
|
||||
// ============================================================================
|
||||
|
||||
function renderExploitedFinding(entry: ExploitedEntry): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`### ${entry.vulnerability_id}: ${entry.title}`);
|
||||
lines.push('');
|
||||
lines.push('**Summary:**');
|
||||
lines.push(`- **Vulnerable location:** ${entry.vulnerable_location}`);
|
||||
lines.push(`- **Overview:** ${entry.overview}`);
|
||||
lines.push(`- **Impact:** ${entry.impact}`);
|
||||
lines.push(`- **Severity:** ${capitalize(entry.severity)}`);
|
||||
lines.push('');
|
||||
if (entry.prerequisites != null && entry.prerequisites.length > 0) {
|
||||
lines.push('**Prerequisites:**');
|
||||
lines.push(entry.prerequisites);
|
||||
lines.push('');
|
||||
}
|
||||
lines.push('**Exploitation Steps:**');
|
||||
lines.push(renderNumberedList(entry.exploitation_steps));
|
||||
lines.push('');
|
||||
lines.push('**Proof of Impact:**');
|
||||
lines.push(entry.proof_of_impact);
|
||||
if (entry.notes != null && entry.notes.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('**Notes:**');
|
||||
lines.push(entry.notes);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function renderBlockedFinding(entry: BlockedEntry): string {
|
||||
const lines: string[] = [];
|
||||
lines.push(`### ${entry.vulnerability_id}: ${entry.title}`);
|
||||
lines.push('');
|
||||
lines.push('**Summary:**');
|
||||
lines.push(`- **Vulnerable location:** ${entry.vulnerable_location}`);
|
||||
lines.push(`- **Current Blocker:** ${entry.current_blocker}`);
|
||||
lines.push(`- **Potential Impact:** ${entry.potential_impact}`);
|
||||
lines.push(`- **Confidence:** ${entry.confidence.toUpperCase()}`);
|
||||
lines.push('');
|
||||
if (entry.prerequisites != null && entry.prerequisites.length > 0) {
|
||||
lines.push('**Prerequisites:**');
|
||||
lines.push(entry.prerequisites);
|
||||
lines.push('');
|
||||
}
|
||||
lines.push('**Evidence of Vulnerability:**');
|
||||
lines.push(entry.evidence_of_vulnerability);
|
||||
lines.push('');
|
||||
lines.push('**What We Tried:**');
|
||||
lines.push(entry.what_we_tried);
|
||||
lines.push('');
|
||||
lines.push('**How This Would Be Exploited:**');
|
||||
lines.push(renderNumberedList(entry.how_this_would_be_exploited));
|
||||
lines.push('');
|
||||
lines.push('**Expected Impact:**');
|
||||
lines.push(entry.expected_impact);
|
||||
if (entry.notes != null && entry.notes.length > 0) {
|
||||
lines.push('');
|
||||
lines.push('**Notes:**');
|
||||
lines.push(entry.notes);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// SECTION RENDERERS
|
||||
// ============================================================================
|
||||
|
||||
function renderExploitedSection(entries: readonly ExploitedEntry[]): string {
|
||||
const heading = '## Successfully Exploited Vulnerabilities';
|
||||
if (entries.length === 0) {
|
||||
return [heading, '', '*No findings reached a definitive verdict in this category.*'].join('\n');
|
||||
}
|
||||
const blocks = sortExploited(entries).map(renderExploitedFinding);
|
||||
return [heading, '', blocks.join('\n\n')].join('\n');
|
||||
}
|
||||
|
||||
function renderBlockedSection(entries: readonly BlockedEntry[]): string {
|
||||
const heading = '## Potential Vulnerabilities (Validation Blocked)';
|
||||
if (entries.length === 0) {
|
||||
return [heading, '', '*No findings reached a definitive verdict in this category.*'].join('\n');
|
||||
}
|
||||
const blocks = sortBlocked(entries).map(renderBlockedFinding);
|
||||
return [heading, '', blocks.join('\n\n')].join('\n');
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// PUBLIC ENTRY POINT
|
||||
// ============================================================================
|
||||
|
||||
export function renderExploitDeliverable(
|
||||
vulnClass: VulnClass,
|
||||
state: readonly AddExploitInput[],
|
||||
idToType: ReadonlyMap<string, string>,
|
||||
): string {
|
||||
const title = `# ${TITLES[vulnClass]}`;
|
||||
|
||||
if (state.length === 0 && idToType.size === 0) {
|
||||
const body = '*No vulnerabilities were available in the queue for exploitation.*';
|
||||
return `${title}\n\n${body}\n`;
|
||||
}
|
||||
|
||||
const exploited = state.filter((e): e is ExploitedEntry => e.status === 'exploited');
|
||||
const blocked = state.filter((e): e is BlockedEntry => e.status === 'blocked');
|
||||
|
||||
const sections: string[] = [title, '', renderExploitedSection(exploited), '', renderBlockedSection(blocked)];
|
||||
|
||||
return `${sections.join('\n').trimEnd()}\n`;
|
||||
}
|
||||
Reference in New Issue
Block a user