feat(worker): structure intermediate deliverables via MCP collectors (#350)

This commit is contained in:
ezl-keygraph
2026-06-05 14:50:43 +05:30
committed by GitHub
parent a6f004cd25
commit 0a1a2eb1c1
26 changed files with 4139 additions and 1300 deletions
@@ -54,6 +54,7 @@ export interface AgentExecutionInput {
apiKey?: string | undefined;
promptDir?: string | undefined;
providerConfig?: import('../types/config.js').ProviderConfig | undefined;
mcpServers?: Record<string, import('@anthropic-ai/claude-agent-sdk').McpServerConfig>;
}
interface FailAgentOpts {
@@ -108,6 +109,7 @@ export class AgentExecutionService {
apiKey,
promptDir,
providerConfig,
mcpServers,
} = input;
// 1. Load config (pre-parsed configData → raw YAML → file path)
@@ -176,6 +178,7 @@ export class AgentExecutionService {
apiKey,
path.relative(repoPath, deliverablesPath),
providerConfig,
mcpServers,
);
// 6. Spending cap check - defense-in-depth
@@ -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`;
}
@@ -0,0 +1,328 @@
// 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 pre-recon collector → markdown renderer.
*
* Converts the typed payload bag harvested from the pre-recon-collector MCP
* server into the pre_recon_deliverable.md Markdown layout. No LLM in the
* loop; section ordering, headings, and template are owned here.
*
* Any tool the agent skips becomes a `[Section X: not provided]` placeholder
* rather than an activity failure. Every section renderer accepts the
* corresponding collected field as possibly undefined and falls back to the
* placeholder helper when absent.
*/
import type {
ApplicationIntelligenceInput,
AuthDeepDiveInput,
CodebaseIndexingInput,
CriticalFilePathsInput,
ExecutiveSummaryInput,
PreReconData,
SinkRef,
SsrfSinksInput,
XssSinksInput,
} from '../mcp-server/pre-recon-collector.js';
// ============================================================================
// STATIC PROSE
// ============================================================================
const SCOPE_AND_BOUNDARIES = `# Penetration Test Scope & Boundaries
**Primary Directive:** Your analysis is strictly limited to the **network-accessible attack surface** of the application. All subsequent tasks must adhere to this scope. Before reporting any finding (e.g., an entry point, a vulnerability sink), you must first verify it meets the "In-Scope" criteria.
### In-Scope: Network-Reachable Components
A component is considered **in-scope** if its execution can be initiated, directly or indirectly, by a network request that the deployed application server is capable of receiving. This includes:
- Publicly exposed web pages and API endpoints.
- Endpoints requiring authentication via the application's standard login mechanisms.
- Any developer utility, debug console, or script that has been mistakenly exposed through a route or is otherwise callable from other in-scope, network-reachable code.
### Out-of-Scope: Locally Executable Only
A component is **out-of-scope** if it **cannot** be invoked through the running application's network interface and requires an execution context completely external to the application's request-response cycle. This includes tools that must be run via:
- A command-line interface (e.g., \`go run ./cmd/...\`, \`python scripts/...\`).
- A development environment's internal tooling (e.g., a "run script" button in an IDE).
- CI/CD pipeline scripts or build tools (e.g., Dagger build definitions).
- Database migration scripts, backup tools, or maintenance utilities.
- Local development servers, test harnesses, or debugging utilities.
- Static files or scripts that require manual opening in a browser (not served by the application).`;
// ============================================================================
// SHARED HELPERS
// ============================================================================
function placeholder(sectionLabel: string, toolName: string): string {
return `_[${sectionLabel}: not provided — \`${toolName}\` was not called]_`;
}
function bulletField(label: string, value: string): string {
return `- **${label}:** ${value}`;
}
function bulletPaths(label: string, paths: readonly string[]): string {
if (paths.length === 0) {
return `- **${label}:** *(none identified)*`;
}
const formatted = paths.map((p) => `\`${p}\``).join(', ');
return `- **${label}:** ${formatted}`;
}
function renderSinkList(sinks: readonly SinkRef[]): string {
if (sinks.length === 0) {
return '*(scanned, no sinks of this kind found)*';
}
return sinks
.map((sink) => {
const head = `- **${sink.sink_function}** at \`${sink.location}\``;
if (sink.notes && sink.notes.trim() !== '') {
return `${head} — ${sink.notes.trim()}`;
}
return head;
})
.join('\n');
}
// ============================================================================
// SECTION RENDERERS
// ============================================================================
function renderExecutiveSummarySection(data: ExecutiveSummaryInput | undefined): string {
if (!data) {
return ['## 1. Executive Summary', '', placeholder('Section 1', 'set_executive_summary')].join('\n');
}
return ['## 1. Executive Summary', '', data.text].join('\n');
}
function renderArchitectureSection(intel: ApplicationIntelligenceInput | undefined): string {
if (!intel) {
return ['## 2. Architecture & Technology Stack', '', placeholder('Section 2', 'set_application_intelligence')].join(
'\n',
);
}
const { architecture: a } = intel;
return [
'## 2. Architecture & Technology Stack',
'',
bulletField('Framework & Language', a.framework_and_language),
bulletField('Architectural Pattern', a.architectural_pattern),
bulletField('Critical Security Components', a.critical_security_components),
].join('\n');
}
function renderAuthSection(auth: AuthDeepDiveInput | undefined): string {
if (!auth) {
return ['## 3. Authentication & Authorization Deep Dive', '', placeholder('Section 3', 'set_auth_deep_dive')].join(
'\n',
);
}
const ssoLine = auth.sso_oauth_oidc
? bulletField('SSO/OAuth/OIDC Flows', auth.sso_oauth_oidc)
: bulletField('SSO/OAuth/OIDC Flows', 'Not applicable — no SSO/OAuth/OIDC integration detected.');
return [
'## 3. Authentication & Authorization Deep Dive',
'',
bulletField('Authentication Mechanisms', auth.authentication_mechanisms),
bulletField('Session Management', auth.session_management),
bulletField('Authorization Model', auth.authz_model),
bulletField('Multi-tenancy', auth.multi_tenancy),
ssoLine,
].join('\n');
}
function renderDataSecuritySection(intel: ApplicationIntelligenceInput | undefined): string {
if (!intel) {
return ['## 4. Data Security & Storage', '', placeholder('Section 4', 'set_application_intelligence')].join('\n');
}
const { data_security: d } = intel;
return [
'## 4. Data Security & Storage',
'',
bulletField('Database Security', d.database_security),
bulletField('Data Flow Security', d.data_flow_security),
bulletField('Multi-tenant Data Isolation', d.multi_tenant_isolation),
].join('\n');
}
function renderAttackSurfaceSection(intel: ApplicationIntelligenceInput | undefined): string {
if (!intel) {
return ['## 5. Attack Surface Analysis', '', placeholder('Section 5', 'set_application_intelligence')].join('\n');
}
const { attack_surface: s } = intel;
return [
'## 5. Attack Surface Analysis',
'',
bulletField('External Entry Points', s.external_entry_points),
bulletField('Internal Service Communication', s.internal_service_communication),
bulletField('Input Validation Patterns', s.input_validation_patterns),
bulletField('Background Processing', s.background_processing),
].join('\n');
}
function renderInfrastructureSection(intel: ApplicationIntelligenceInput | undefined): string {
if (!intel) {
return [
'## 6. Infrastructure & Operational Security',
'',
placeholder('Section 6', 'set_application_intelligence'),
].join('\n');
}
const { infrastructure: i } = intel;
return [
'## 6. Infrastructure & Operational Security',
'',
bulletField('Secrets Management', i.secrets_management),
bulletField('Configuration Security', i.configuration_security),
bulletField('External Dependencies', i.external_dependencies),
bulletField('Monitoring & Logging', i.monitoring_and_logging),
].join('\n');
}
function renderCodebaseIndexingSection(data: CodebaseIndexingInput | undefined): string {
if (!data) {
return ['## 7. Overall Codebase Indexing', '', placeholder('Section 7', 'set_codebase_indexing')].join('\n');
}
return ['## 7. Overall Codebase Indexing', '', data.text].join('\n');
}
function renderCriticalFilePathsSection(paths: CriticalFilePathsInput | undefined): string {
if (!paths) {
return ['## 8. Critical File Paths', '', placeholder('Section 8', 'set_critical_file_paths')].join('\n');
}
return [
'## 8. Critical File Paths',
'',
bulletPaths('Configuration', paths.configuration),
bulletPaths('Authentication & Authorization', paths.authentication_and_authorization),
bulletPaths('API & Routing', paths.api_and_routing),
bulletPaths('Data Models & DB Interaction', paths.data_models_and_db),
bulletPaths('Dependency Manifests', paths.dependency_manifests),
bulletPaths('Sensitive Data & Secrets Handling', paths.sensitive_data_and_secrets),
bulletPaths('Middleware & Input Validation', paths.middleware_and_input_validation),
bulletPaths('Logging & Monitoring', paths.logging_and_monitoring),
bulletPaths('Infrastructure & Deployment', paths.infrastructure_and_deployment),
].join('\n');
}
function renderXssSection(xss: XssSinksInput | undefined): string {
if (!xss) {
return ['## 9. XSS Sinks and Render Contexts', '', placeholder('Section 9', 'set_xss_sinks')].join('\n');
}
if (!xss.applicable) {
return [
'## 9. XSS Sinks and Render Contexts',
'',
'*(N/A — the application has no web frontend; XSS sink analysis does not apply.)*',
].join('\n');
}
return [
'## 9. XSS Sinks and Render Contexts',
'',
'### HTML Body Context',
renderSinkList(xss.html_body),
'',
'### HTML Attribute Context',
renderSinkList(xss.html_attribute),
'',
'### JavaScript Context',
renderSinkList(xss.javascript),
'',
'### CSS Context',
renderSinkList(xss.css),
'',
'### URL Context',
renderSinkList(xss.url),
].join('\n');
}
function renderSsrfSection(ssrf: SsrfSinksInput | undefined): string {
if (!ssrf) {
return ['## 10. SSRF Sinks', '', placeholder('Section 10', 'set_ssrf_sinks')].join('\n');
}
if (!ssrf.applicable) {
return [
'## 10. SSRF Sinks',
'',
'*(N/A — the application makes no outbound requests; SSRF sink analysis does not apply.)*',
].join('\n');
}
return [
'## 10. SSRF Sinks',
'',
'### HTTP(S) Clients',
renderSinkList(ssrf.http_clients),
'',
'### Raw Sockets & Connect APIs',
renderSinkList(ssrf.raw_sockets),
'',
'### URL Openers & File Includes',
renderSinkList(ssrf.url_openers),
'',
'### Redirect & "Next URL" Handlers',
renderSinkList(ssrf.redirect_handlers),
'',
'### Headless Browsers & Render Engines',
renderSinkList(ssrf.headless_browsers),
'',
'### Media Processors',
renderSinkList(ssrf.media_processors),
'',
'### Link Preview & Unfurlers',
renderSinkList(ssrf.link_preview),
'',
'### Webhook Testers & Callback Verifiers',
renderSinkList(ssrf.webhook_testers),
'',
'### SSO/OIDC Discovery & JWKS Fetchers',
renderSinkList(ssrf.sso_oidc_discovery),
'',
'### Importers & Data Loaders',
renderSinkList(ssrf.importers),
'',
'### Package/Plugin/Theme Installers',
renderSinkList(ssrf.package_installers),
'',
'### Monitoring & Health Check Frameworks',
renderSinkList(ssrf.monitoring_and_health),
'',
'### Cloud Metadata Helpers',
renderSinkList(ssrf.cloud_metadata),
].join('\n');
}
// ============================================================================
// PUBLIC ENTRY POINT
// ============================================================================
export function renderPreRecon(data: PreReconData): string {
const sections: string[] = [
SCOPE_AND_BOUNDARIES,
'---',
'',
renderExecutiveSummarySection(data.executive_summary),
'',
renderArchitectureSection(data.application_intelligence),
'',
renderAuthSection(data.auth_deep_dive),
'',
renderDataSecuritySection(data.application_intelligence),
'',
renderAttackSurfaceSection(data.application_intelligence),
'',
renderInfrastructureSection(data.application_intelligence),
'',
renderCodebaseIndexingSection(data.codebase_indexing),
'',
renderCriticalFilePathsSection(data.critical_file_paths),
'',
renderXssSection(data.xss_sinks),
'',
renderSsrfSection(data.ssrf_sinks),
'',
];
return `${sections.join('\n').trimEnd()}\n`;
}
+561
View File
@@ -0,0 +1,561 @@
// 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 recon collector → markdown renderer.
*
* Converts the typed payload bag harvested from the recon-collector MCP server
* into the recon_deliverable.md Markdown layout. No LLM in the loop; section
* ordering, headings, sort, and the Section 0 boilerplate are owned here.
*
* Any tool the agent skips becomes a `[Section X: not provided]` placeholder
* rather than an activity failure. Every section renderer accepts its input as
* optional.
*/
import type {
AuthenticationInput,
AuthzCandidatesInput,
ContextCandidate,
Endpoint,
Entity,
ExecutiveSummaryInput,
Flow,
Guard,
HorizontalCandidate,
InjectionSourcesInput,
InputVectorsInput,
NetworkMapInput,
Priority,
ReconData,
Role,
RoleArchitectureInput,
SinkRef,
TechnologyStackInput,
VerticalCandidate,
} from '../mcp-server/recon-collector.js';
type RoleSwitchingImpersonation = AuthenticationInput['role_switching_impersonation'];
type EntityZone = Entity['zone'];
// ============================================================================
// STATIC PROSE
// ============================================================================
const HOW_TO_READ_THIS = `## 0) HOW TO READ THIS
This reconnaissance report provides a comprehensive map of the application's attack surface, with special emphasis on authorization and privilege escalation opportunities for the Authorization Analysis Specialist.
**Key Sections for Authorization Analysis:**
- **Section 4 (API Endpoint Inventory):** Contains authorization details for each endpoint - focus on "Required Role" and "Object ID Parameters" columns to identify IDOR candidates.
- **Section 6.4 (Guards Directory):** Catalog of authorization controls - understand what each guard means before analyzing vulnerabilities.
- **Section 7 (Role & Privilege Architecture):** Complete role hierarchy and privilege mapping - use this to understand the privilege lattice and identify escalation targets.
- **Section 8 (Authorization Vulnerability Candidates):** Pre-prioritized lists of endpoints for horizontal, vertical, and context-based authorization testing.
**How to Use the Network Mapping (Section 6):** The entity/flow mapping shows system boundaries and data sensitivity levels. Pay special attention to flows marked with authorization guards and entities handling PII/sensitive data.
**Priority Order for Testing:** Start with Section 8's High-priority horizontal candidates, then vertical escalation endpoints for each role level, finally context-based workflow bypasses.`;
// ============================================================================
// SORT ORDER CONSTANTS
// ============================================================================
// Zones are sorted by exposure (Internet → Edge → ... → ThirdParty), not alphabetically,
// per the design doc's "clusters by zone" requirement. A reader scanning the entities
// table sees external surface first, internal trust core last.
const ZONE_ORDER: Record<EntityZone, number> = {
Internet: 0,
Edge: 1,
App: 2,
Data: 3,
Admin: 4,
BuildCI: 5,
ThirdParty: 6,
};
const PRIORITY_ORDER: Record<Priority, number> = {
High: 0,
Medium: 1,
Low: 2,
};
// ============================================================================
// SHARED HELPERS
// ============================================================================
function placeholder(sectionLabel: string, toolName: string): string {
return `_[${sectionLabel}: not provided — \`${toolName}\` was not called]_`;
}
function bulletField(label: string, value: string): string {
return `- **${label}:** ${value}`;
}
function bulletList(label: string, items: readonly string[]): string {
if (items.length === 0) {
return `- **${label}:** *(none identified)*`;
}
return `- **${label}:**\n${items.map((entry) => ` - ${entry}`).join('\n')}`;
}
function escapePipe(value: string): string {
return value.replace(/\|/g, '\\|');
}
function renderTable(headers: readonly string[], rows: readonly (readonly string[])[]): string {
const headerRow = `| ${headers.map(escapePipe).join(' | ')} |`;
const separator = `| ${headers.map(() => '---').join(' | ')} |`;
const body = rows.map((row) => `| ${row.map(escapePipe).join(' | ')} |`).join('\n');
return [headerRow, separator, body].filter((line) => line.length > 0).join('\n');
}
function renderSinkList(sinks: readonly SinkRef[]): string {
if (sinks.length === 0) {
return '*(scanned, no sources of this kind found)*';
}
return sinks
.map((sink) => {
const head = `- **${sink.sink_function}** at \`${sink.location}\``;
if (sink.notes && sink.notes.trim() !== '') {
return `${head} — ${sink.notes.trim()}`;
}
return head;
})
.join('\n');
}
// ============================================================================
// SECTION RENDERERS
// ============================================================================
function renderHowToReadThis(): string {
return HOW_TO_READ_THIS;
}
function renderExecutiveSummary(data: ExecutiveSummaryInput | undefined): string {
if (!data) {
return ['## 1. Executive Summary', '', placeholder('Section 1', 'set_executive_summary')].join('\n');
}
return ['## 1. Executive Summary', '', data.text].join('\n');
}
function renderTechnologyStack(data: TechnologyStackInput | undefined): string {
if (!data) {
return ['## 2. Technology & Service Map', '', placeholder('Section 2', 'set_technology_stack')].join('\n');
}
return [
'## 2. Technology & Service Map',
'',
bulletField('Frontend', data.frontend),
bulletField('Backend', data.backend),
bulletField('Infrastructure', data.infrastructure),
].join('\n');
}
function renderRoleSwitching(rs: RoleSwitchingImpersonation): string {
if (!rs.applicable) {
return [
'### 3.3 Role Switching & Impersonation',
'',
'*(Not applicable — no impersonation, sudo mode, or role-switching features were identified.)*',
].join('\n');
}
return [
'### 3.3 Role Switching & Impersonation',
'',
bulletField('Impersonation Features', rs.impersonation_features ?? '*(not specified)*'),
bulletField('Role Switching', rs.role_switching ?? '*(not specified)*'),
bulletField('Audit Trail', rs.audit_trail ?? '*(not specified)*'),
bulletField('Code Implementation', rs.code_implementation ?? '*(not specified)*'),
].join('\n');
}
function renderAuthentication(data: AuthenticationInput | undefined): string {
if (!data) {
return ['## 3. Authentication & Session Management Flow', '', placeholder('Section 3', 'set_authentication')].join(
'\n',
);
}
const { session_flow: sf, role_assignment: ra, privilege_storage: ps } = data;
return [
'## 3. Authentication & Session Management Flow',
'',
bulletField('Entry Points', sf.entry_points),
bulletField('Mechanism', sf.mechanism),
bulletField('Code Pointers', sf.code_pointers),
'',
'### 3.1 Role Assignment Process',
'',
bulletField('Role Determination', ra.role_determination),
bulletField('Default Role', ra.default_role),
bulletField('Role Upgrade Path', ra.role_upgrade_path),
bulletField('Code Implementation', ra.code_implementation),
'',
'### 3.2 Privilege Storage & Validation',
'',
bulletField('Storage Location', ps.storage_location),
bulletField('Validation Points', ps.validation_points),
bulletField('Cache/Session Persistence', ps.cache_session_persistence),
bulletField('Code Pointers', ps.code_pointers),
'',
renderRoleSwitching(data.role_switching_impersonation),
].join('\n');
}
function sortEndpoints(endpoints: readonly Endpoint[]): Endpoint[] {
return [...endpoints].sort((a, b) => {
if (a.path !== b.path) return a.path.localeCompare(b.path);
return a.method.localeCompare(b.method);
});
}
function renderEndpoints(endpoints: readonly Endpoint[] | undefined): string {
if (!endpoints || endpoints.length === 0) {
return ['## 4. API Endpoint Inventory', '', placeholder('Section 4', 'add_endpoints')].join('\n');
}
const sorted = sortEndpoints(endpoints);
const rows = sorted.map((e) => [
e.method,
e.path,
e.required_role,
e.object_id_parameters.length > 0 ? e.object_id_parameters.join(', ') : 'None',
e.authorization_mechanism,
`${e.description} (${e.code_pointer})`,
]);
return [
'## 4. API Endpoint Inventory',
'',
renderTable(
[
'Method',
'Endpoint Path',
'Required Role',
'Object ID Parameters',
'Authorization Mechanism',
'Description & Code Pointer',
],
rows,
),
].join('\n');
}
function renderInputVectors(data: InputVectorsInput | undefined): string {
if (!data) {
return [
'## 5. Potential Input Vectors for Vulnerability Analysis',
'',
placeholder('Section 5', 'set_input_vectors'),
].join('\n');
}
return [
'## 5. Potential Input Vectors for Vulnerability Analysis',
'',
bulletList('URL Parameters', data.url_parameters),
bulletList('POST Body Fields (JSON/Form)', data.post_body_fields),
bulletList('HTTP Headers', data.http_headers),
bulletList('Cookie Values', data.cookie_values),
].join('\n');
}
function sortEntities(entities: readonly Entity[]): Entity[] {
return [...entities].sort((a, b) => {
const zoneDiff = ZONE_ORDER[a.zone] - ZONE_ORDER[b.zone];
if (zoneDiff !== 0) return zoneDiff;
if (a.type !== b.type) return a.type.localeCompare(b.type);
return a.title.localeCompare(b.title);
});
}
function sortFlows(flows: readonly Flow[]): Flow[] {
return [...flows].sort((a, b) => {
if (a.from !== b.from) return a.from.localeCompare(b.from);
if (a.to !== b.to) return a.to.localeCompare(b.to);
return a.path_port.localeCompare(b.path_port);
});
}
function sortGuards(guards: readonly Guard[]): Guard[] {
return [...guards].sort((a, b) => {
if (a.category !== b.category) return a.category.localeCompare(b.category);
return a.name.localeCompare(b.name);
});
}
function renderEntitiesTable(entities: readonly Entity[]): string {
const rows = entities.map((e) => [e.title, e.type, e.zone, e.tech, e.data.join(', '), e.notes]);
return renderTable(['Title', 'Type', 'Zone', 'Tech', 'Data', 'Notes'], rows);
}
function renderEntityMetadataTable(entities: readonly Entity[]): string {
const rows = entities.map((e) => {
const metadataLine =
e.metadata.length > 0 ? e.metadata.map(({ key, value }) => `${key}: ${value}`).join('; ') : '*(none)*';
return [e.title, metadataLine];
});
return renderTable(['Title', 'Metadata'], rows);
}
function renderFlowsTable(flows: readonly Flow[]): string {
const rows = flows.map((f) => [
`${f.from} → ${f.to}`,
f.channel,
f.path_port,
f.guards.length > 0 ? f.guards.join(', ') : 'None',
f.touches.length > 0 ? f.touches.join(', ') : 'Public',
]);
return renderTable(['FROM → TO', 'Channel', 'Path/Port', 'Guards', 'Touches'], rows);
}
function renderGuardsTable(guards: readonly Guard[]): string {
const rows = guards.map((g) => [g.name, g.category, g.statement]);
return renderTable(['Guard Name', 'Category', 'Statement'], rows);
}
function renderNetworkMap(data: NetworkMapInput | undefined): string {
if (!data) {
return ['## 6. Network & Interaction Map', '', placeholder('Section 6', 'set_network_map')].join('\n');
}
const entities = sortEntities(data.entities);
const flows = sortFlows(data.flows);
const guards = sortGuards(data.guards);
return [
'## 6. Network & Interaction Map',
'',
'### 6.1 Entities',
'',
entities.length > 0 ? renderEntitiesTable(entities) : '*(no entities recorded)*',
'',
'### 6.2 Entity Metadata',
'',
entities.length > 0 ? renderEntityMetadataTable(entities) : '*(no entities recorded)*',
'',
'### 6.3 Flows (Connections)',
'',
flows.length > 0 ? renderFlowsTable(flows) : '*(no flows recorded)*',
'',
'### 6.4 Guards Directory',
'',
guards.length > 0 ? renderGuardsTable(guards) : '*(no guards recorded)*',
].join('\n');
}
function sortRoles(roles: readonly Role[]): Role[] {
return [...roles].sort((a, b) => {
if (a.privilege_level !== b.privilege_level) return a.privilege_level - b.privilege_level;
return a.name.localeCompare(b.name);
});
}
function renderRoleArchitecture(data: RoleArchitectureInput | undefined): string {
if (!data) {
return ['## 7. Role & Privilege Architecture', '', placeholder('Section 7', 'set_role_architecture')].join('\n');
}
const roles = sortRoles(data.roles);
const discoveredRows = roles.map((r) => [r.name, String(r.privilege_level), r.scope_domain, r.code_implementation]);
const entryPointRows = roles.map((r) => [
r.name,
r.default_landing_page,
r.accessible_route_patterns.length > 0 ? r.accessible_route_patterns.join(', ') : 'None',
r.authentication_method,
]);
const codeMappingRows = roles.map((r) => [r.name, r.middleware_guards, r.permission_checks, r.storage_location]);
const lattice = data.privilege_lattice;
const latticeBlock = [
'```',
`Privilege Ordering (→ means "can access resources of"):`,
lattice.ordering_diagram,
'',
`Parallel Isolation (|| means "not ordered relative to each other"):`,
lattice.parallel_isolation_notes,
'```',
].join('\n');
const sections = [
'## 7. Role & Privilege Architecture',
'',
'### 7.1 Discovered Roles',
'',
roles.length > 0
? renderTable(['Role Name', 'Privilege Level', 'Scope/Domain', 'Code Implementation'], discoveredRows)
: '*(no roles recorded)*',
'',
'### 7.2 Privilege Lattice',
'',
latticeBlock,
];
if (lattice.role_switching_notes && lattice.role_switching_notes.trim() !== '') {
sections.push('', `**Note:** ${lattice.role_switching_notes.trim()}`);
}
sections.push(
'',
'### 7.3 Role Entry Points',
'',
roles.length > 0
? renderTable(
['Role', 'Default Landing Page', 'Accessible Route Patterns', 'Authentication Method'],
entryPointRows,
)
: '*(no roles recorded)*',
'',
'### 7.4 Role-to-Code Mapping',
'',
roles.length > 0
? renderTable(['Role', 'Middleware/Guards', 'Permission Checks', 'Storage Location'], codeMappingRows)
: '*(no roles recorded)*',
);
return sections.join('\n');
}
function sortHorizontal(items: readonly HorizontalCandidate[]): HorizontalCandidate[] {
return [...items].sort((a, b) => {
const pri = PRIORITY_ORDER[a.priority] - PRIORITY_ORDER[b.priority];
if (pri !== 0) return pri;
return a.endpoint_pattern.localeCompare(b.endpoint_pattern);
});
}
function sortVertical(items: readonly VerticalCandidate[]): VerticalCandidate[] {
return [...items].sort((a, b) => {
const pri = PRIORITY_ORDER[a.risk_level] - PRIORITY_ORDER[b.risk_level];
if (pri !== 0) return pri;
return a.endpoint_pattern.localeCompare(b.endpoint_pattern);
});
}
function sortContext(items: readonly ContextCandidate[]): ContextCandidate[] {
return [...items].sort((a, b) => a.endpoint.localeCompare(b.endpoint));
}
function renderAuthzCandidates(data: AuthzCandidatesInput | undefined): string {
if (!data) {
return ['## 8. Authorization Vulnerability Candidates', '', placeholder('Section 8', 'set_authz_candidates')].join(
'\n',
);
}
const horizontal = sortHorizontal(data.horizontal);
const vertical = sortVertical(data.vertical);
const context = sortContext(data.context);
let idCounter = 0;
const nextId = (): string => {
idCounter += 1;
return `AUTHZ-CAND-${String(idCounter).padStart(2, '0')}`;
};
const horizontalRows = horizontal.map((c) => [
nextId(),
c.priority,
c.endpoint_pattern,
c.object_id_parameter,
c.data_type,
c.sensitivity,
]);
const verticalRows = vertical.map((c) => [
nextId(),
c.target_role,
c.endpoint_pattern,
c.functionality,
c.risk_level,
]);
const contextRows = context.map((c) => [
nextId(),
c.workflow,
c.endpoint,
c.expected_prior_state,
c.bypass_potential,
]);
return [
'## 8. Authorization Vulnerability Candidates',
'',
'### 8.1 Horizontal Privilege Escalation Candidates',
'',
horizontal.length > 0
? renderTable(
['ID', 'Priority', 'Endpoint Pattern', 'Object ID Parameter', 'Data Type', 'Sensitivity'],
horizontalRows,
)
: '*(no horizontal candidates identified)*',
'',
'### 8.2 Vertical Privilege Escalation Candidates',
'',
vertical.length > 0
? renderTable(['ID', 'Target Role', 'Endpoint Pattern', 'Functionality', 'Risk Level'], verticalRows)
: '*(no vertical candidates identified)*',
'',
'### 8.3 Context-Based Authorization Candidates',
'',
context.length > 0
? renderTable(['ID', 'Workflow', 'Endpoint', 'Expected Prior State', 'Bypass Potential'], contextRows)
: '*(no context-based candidates identified)*',
].join('\n');
}
function renderInjectionSources(data: InjectionSourcesInput | undefined): string {
const heading =
'## 9. Injection Sources (Command Injection, SQL Injection, LFI/RFI, SSTI, Path Traversal, Deserialization)';
if (!data) {
return [heading, '', placeholder('Section 9', 'set_injection_sources')].join('\n');
}
if (!data.applicable) {
return [
heading,
'',
'*(Not applicable — this application has no network-accessible code paths to dangerous sinks.)*',
].join('\n');
}
return [
heading,
'',
'### Command Injection',
renderSinkList(data.command_injection),
'',
'### SQL Injection',
renderSinkList(data.sql_injection),
'',
'### LFI/RFI',
renderSinkList(data.lfi_rfi),
'',
'### Path Traversal',
renderSinkList(data.path_traversal),
'',
'### SSTI',
renderSinkList(data.ssti),
'',
'### Deserialization',
renderSinkList(data.deserialization),
].join('\n');
}
// ============================================================================
// PUBLIC ENTRY POINT
// ============================================================================
export function renderRecon(data: ReconData): string {
const sections: string[] = [
'# Reconnaissance Deliverable:',
'',
renderHowToReadThis(),
'',
renderExecutiveSummary(data.executive_summary),
'',
renderTechnologyStack(data.technology_stack),
'',
renderAuthentication(data.authentication),
'',
renderEndpoints(data.endpoints),
'',
renderInputVectors(data.input_vectors),
'',
renderNetworkMap(data.network_map),
'',
renderRoleArchitecture(data.role_architecture),
'',
renderAuthzCandidates(data.authz_candidates),
'',
renderInjectionSources(data.injection_sources),
'',
];
return `${sections.join('\n').trimEnd()}\n`;
}
+229
View File
@@ -0,0 +1,229 @@
// 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 vuln collector → markdown renderer.
*
* Single entry point renderVulnDeliverable(vulnClass, data) covers all 5
* vulnerability classes (injection, xss, auth, ssrf, authz). Per-class title,
* §3 sub-header set, §4 column shape, and §4 section heading are selected by
* branching on vulnClass.
*
* Missing tools surface as placeholder sections, not activity failures.
* Required tools (set_findings_summary, set_strategic_intelligence) produce
* loud `[Section X: not provided]` placeholders; recommended tools
* (set_safe_vectors, set_blind_spots) produce quiet "None identified" prose.
*/
import type {
BlindSpotsInput,
FindingsSummaryInput,
SafeVectorsInput,
StrategicIntelligenceInput,
VulnClass,
VulnCollectorData,
} from '../mcp-server/vuln-collector.js';
import { BLIND_SPOTS_CLASSES } from '../mcp-server/vuln-collector.js';
// ============================================================================
// PER-CLASS CONSTANTS
// ============================================================================
const TITLES: Record<VulnClass, string> = {
injection: 'Injection Analysis Report',
xss: 'Cross-Site Scripting (XSS) Analysis Report',
auth: 'Authentication Analysis Report',
ssrf: 'SSRF Analysis Report',
authz: 'Authorization Analysis Report',
};
const SECTION_FOUR_HEADING: Record<VulnClass, string> = {
injection: '4. Vectors Analyzed and Confirmed Secure',
xss: '4. Vectors Analyzed and Confirmed Secure',
auth: '4. Secure by Design: Validated Components',
ssrf: '4. Secure by Design: Validated Components',
authz: '4. Vectors Analyzed and Confirmed Secure',
};
const STRATEGIC_INTEL_SUBHEADERS: Record<VulnClass, ReadonlyArray<readonly [string, string]>> = {
injection: [
['defensive_evasion_waf', 'Defensive Evasion (WAF Analysis)'],
['error_based_potential', 'Error-Based Injection Potential'],
['confirmed_database_technology', 'Confirmed Database Technology'],
],
xss: [
['csp_analysis', 'Content Security Policy (CSP) Analysis'],
['cookie_security', 'Cookie Security'],
],
auth: [
['authentication_method', 'Authentication Method'],
['session_token_details', 'Session Token Details'],
['password_policy', 'Password Policy'],
],
ssrf: [
['http_client_library', 'HTTP Client Library'],
['request_architecture', 'Request Architecture'],
['internal_services', 'Internal Services'],
],
authz: [
['session_management_architecture', 'Session Management Architecture'],
['role_permission_model', 'Role/Permission Model'],
['resource_access_patterns', 'Resource Access Patterns'],
['workflow_implementation', 'Workflow Implementation'],
],
};
// Per-class column shape for §4. The first label is the subject column name
// (varies by class — "Source" vs "Component/Flow" vs "Endpoint"); the location
// column name also varies for authz ("Guard Location"); XSS gets an extra
// "Render Context" column between defense and verdict.
interface ColumnSpec {
readonly subject: string;
readonly location: string;
readonly includeRenderContext: boolean;
}
const SECTION_FOUR_COLUMNS: Record<VulnClass, ColumnSpec> = {
injection: { subject: 'Source', location: 'Endpoint/File Location', includeRenderContext: false },
xss: { subject: 'Source', location: 'Endpoint/File Location', includeRenderContext: true },
auth: { subject: 'Component/Flow', location: 'Endpoint/File Location', includeRenderContext: false },
ssrf: { subject: 'Component/Flow', location: 'Endpoint/File Location', includeRenderContext: false },
authz: { subject: 'Endpoint', location: 'Guard Location', includeRenderContext: false },
};
// ============================================================================
// SHARED HELPERS
// ============================================================================
function placeholder(sectionLabel: string, toolName: string): string {
return `_[${sectionLabel}: not provided — \`${toolName}\` was not called]_`;
}
function escapePipe(value: string): string {
return value.replace(/\|/g, '\\|');
}
function renderTable(headers: readonly string[], rows: readonly (readonly string[])[]): string {
const headerRow = `| ${headers.map(escapePipe).join(' | ')} |`;
const separator = `| ${headers.map(() => '---').join(' | ')} |`;
const body = rows.map((row) => `| ${row.map(escapePipe).join(' | ')} |`).join('\n');
return [headerRow, separator, body].filter((line) => line.length > 0).join('\n');
}
// ============================================================================
// SECTION RENDERERS
// ============================================================================
function renderTitle(vulnClass: VulnClass): string {
return `# ${TITLES[vulnClass]}`;
}
function renderExecutiveSummary(summary: FindingsSummaryInput | undefined): string {
if (!summary) {
return ['## 1. Executive Summary', '', placeholder('Section 1', 'set_findings_summary')].join('\n');
}
return ['## 1. Executive Summary', '', summary.key_outcome].join('\n');
}
function renderDominantPatterns(summary: FindingsSummaryInput | undefined): string {
if (!summary) {
return ['## 2. Dominant Vulnerability Patterns', '', placeholder('Section 2', 'set_findings_summary')].join('\n');
}
if (summary.patterns.length === 0) {
return ['## 2. Dominant Vulnerability Patterns', '', '*No dominant patterns identified.*'].join('\n');
}
const blocks = summary.patterns.map((p, index) => {
const ids = p.representative_finding_ids.map((id) => `\`${id}\``).join(', ');
return [
`### Pattern ${index + 1}: ${p.name}`,
`- **Description:** ${p.description}`,
`- **Implication:** ${p.implication}`,
`- **Representative Findings:** ${ids}`,
].join('\n');
});
return ['## 2. Dominant Vulnerability Patterns', '', blocks.join('\n\n')].join('\n');
}
function renderStrategicIntelligence(vulnClass: VulnClass, intel: StrategicIntelligenceInput | undefined): string {
if (!intel) {
return [
'## 3. Strategic Intelligence for Exploitation',
'',
placeholder('Section 3', 'set_strategic_intelligence'),
].join('\n');
}
const subheaders = STRATEGIC_INTEL_SUBHEADERS[vulnClass];
const intelRecord = intel as unknown as Record<string, string>;
const blocks = subheaders.map(([fieldName, header]) => {
const value = intelRecord[fieldName] ?? '*(not provided)*';
return [`### ${header}`, value].join('\n');
});
return ['## 3. Strategic Intelligence for Exploitation', '', blocks.join('\n\n')].join('\n');
}
function sortSafeVectors(vectors: SafeVectorsInput['vectors']): SafeVectorsInput['vectors'] {
return [...vectors].sort((a, b) => {
if (a.subject !== b.subject) return a.subject.localeCompare(b.subject);
return a.location.localeCompare(b.location);
});
}
function renderSafeVectors(vulnClass: VulnClass, data: SafeVectorsInput | undefined): string {
const heading = `## ${SECTION_FOUR_HEADING[vulnClass]}`;
if (!data || data.vectors.length === 0) {
return [heading, '', '*No vectors confirmed secure during analysis.*'].join('\n');
}
const cols = SECTION_FOUR_COLUMNS[vulnClass];
const headers: string[] = [cols.subject, cols.location, 'Defense Mechanism'];
if (cols.includeRenderContext) {
headers.push('Render Context');
}
headers.push('Verdict');
const sorted = sortSafeVectors(data.vectors);
const rows = sorted.map((v) => {
const row: string[] = [v.subject, v.location, v.defense_mechanism];
if (cols.includeRenderContext) {
row.push(v.render_context ?? '');
}
row.push('SAFE');
return row;
});
return [heading, '', renderTable(headers, rows)].join('\n');
}
function renderBlindSpots(data: BlindSpotsInput | undefined): string {
const heading = '## 5. Analysis Constraints and Blind Spots';
if (!data || data.items.length === 0) {
return [heading, '', '*No analysis constraints or blind spots identified.*'].join('\n');
}
const blocks = data.items.map((item) => [`### ${item.heading}`, item.description].join('\n'));
return [heading, '', blocks.join('\n\n')].join('\n');
}
// ============================================================================
// PUBLIC ENTRY POINT
// ============================================================================
export function renderVulnDeliverable(vulnClass: VulnClass, data: VulnCollectorData): string {
const sections: string[] = [
renderTitle(vulnClass),
'',
renderExecutiveSummary(data.findings_summary),
'',
renderDominantPatterns(data.findings_summary),
'',
renderStrategicIntelligence(vulnClass, data.strategic_intelligence),
'',
renderSafeVectors(vulnClass, data.safe_vectors),
'',
];
if (BLIND_SPOTS_CLASSES.has(vulnClass)) {
sections.push(renderBlindSpots(data.blind_spots), '');
}
return `${sections.join('\n').trimEnd()}\n`;
}