fix(worker): correct PDF finding reporting

- Render OWASP category, authentication state, and remediation
- Omit the redundant per-finding exploited status
- Preserve canonical category and field ordering across report modes
- Continue Proof of Impact numbering across embedded code blocks
- Wrap long PDF code lines without changing canonical report content
This commit is contained in:
ajmallesh
2026-08-30 14:08:32 -07:00
parent 602bc27bbc
commit ddfd026f93
3 changed files with 153 additions and 81 deletions
@@ -16,7 +16,12 @@
* pipeline knows nothing about the Typst shape.
*/
import type { AddFindingInput, AdditionalSection, StepItem, StructuredStep } from '../collectors/finding-collector.js';
import type {
AddFindingInput,
AdditionalSection,
StepItem as CollectorStepItem,
StructuredStep,
} from '../collectors/finding-collector.js';
import { orderFindings } from './finding-order.js';
import type {
ExploitsReportData,
@@ -26,6 +31,7 @@ import type {
ReportData as TypstReportData,
TypstSeverity,
TypstStatus,
StepItem as TypstStepItem,
} from './report-output-schema.js';
import type { ReportData } from './report-renderer.js';
@@ -85,14 +91,40 @@ function toTypstCategory(s: string): TypstCategory {
// STEP / ITEM TRANSFORMS
// ============================================================================
// StepItem is currently identical on both sides of the adapter boundary, so this is a no-op.
// It stays as an explicit seam rather than being inlined so a future divergence between the
// collector's StepItem and the Typst schema's StepItem has a single place to add the conversion.
function adaptStepItem(item: StepItem): StepItem {
return item;
// Typst raw blocks do not wrap long lines. Keep the exact payload in `content` and derive a separate
// display-only projection with inserted line breaks for the PDF. Canonical JSON, Markdown, SARIF,
// and the exact Typst-side payload remain byte-for-byte intact.
const PDF_CODE_LINE_COLUMNS = 84;
function wrapCodeForPdf(content: string): string {
return content
.split('\n')
.flatMap((line) => {
const characters = Array.from(line);
if (characters.length <= PDF_CODE_LINE_COLUMNS) return [line];
const wrapped: string[] = [];
for (let offset = 0; offset < characters.length; offset += PDF_CODE_LINE_COLUMNS) {
wrapped.push(characters.slice(offset, offset + PDF_CODE_LINE_COLUMNS).join(''));
}
return wrapped;
})
.join('\n');
}
function adaptStep(step: StructuredStep, index: number): { number: number; title?: string; items: StepItem[] } {
function adaptStepItem(item: CollectorStepItem): TypstStepItem {
if (item.kind === 'prose') return item;
return {
kind: 'code',
block: {
language: item.block.language,
content: item.block.content,
displayContent: wrapCodeForPdf(item.block.content),
},
};
}
function adaptStep(step: StructuredStep, index: number): { number: number; title?: string; items: TypstStepItem[] } {
return {
number: index + 1,
...(step.title && { title: step.title }),
@@ -100,7 +132,7 @@ function adaptStep(step: StructuredStep, index: number): { number: number; title
};
}
function adaptAdditionalSection(section: AdditionalSection): { heading: string; items: StepItem[] } {
function adaptAdditionalSection(section: AdditionalSection): { heading: string; items: TypstStepItem[] } {
return {
heading: section.heading,
items: section.items.map(adaptStepItem),
@@ -201,7 +233,8 @@ function adaptExploitsMode(data: ReportData): ExploitsReportData {
title: f.title,
category: toTypstCategory(f.category),
severity: toTypstSeverity(f.severity),
status: toTypstStatus(f.status ?? 'exploited'),
owaspCategory: f.owasp_category,
...(f.auth_state && { authState: f.auth_state }),
summary: {
vulnerableLocation: f.vulnerable_location,
overview: f.overview,
@@ -212,6 +245,7 @@ function adaptExploitsMode(data: ReportData): ExploitsReportData {
prerequisites: f.prerequisites ?? '',
exploitationSteps: (f.exploitation_steps ?? []).map(adaptStep),
proofOfImpact: (f.proof_of_impact ?? []).map(adaptStepItem),
remediation: f.remediation,
...(f.notes && f.notes.length > 0 && { notes: f.notes.map(adaptStepItem) }),
...(f.additional_sections &&
f.additional_sections.length > 0 && {
@@ -277,11 +311,13 @@ function adaptFindingsMode(data: ReportData): FindingsReportData {
category: toTypstCategory(f.category),
severity: toTypstSeverity(f.severity),
confidence: toTypstConfidence(f.confidence ?? 'medium'),
owaspCategory: f.owasp_category,
summary: {
vulnerableLocation: f.vulnerable_location,
overview: f.overview,
impact: f.impact,
},
remediation: f.remediation,
...(f.notes && f.notes.length > 0 && { notes: f.notes.map(adaptStepItem) }),
...(f.additional_sections &&
f.additional_sections.length > 0 && {
@@ -21,6 +21,8 @@ export type TypstCategory = 'Authentication' | 'Authorization' | 'XSS' | 'Inject
export interface CodeBlock {
readonly language: string;
readonly content: string;
/** PDF-only projection with inserted visual line breaks; `content` remains exact. */
readonly displayContent: string;
}
export type StepItem =
@@ -84,11 +86,13 @@ export interface ExploitFinding {
readonly title: string;
readonly category: TypstCategory;
readonly severity: TypstSeverity;
readonly status: TypstStatus;
readonly owaspCategory: string;
readonly authState?: string;
readonly summary: FindingSummary;
readonly prerequisites: string;
readonly exploitationSteps: readonly Step[];
readonly proofOfImpact: readonly StepItem[];
readonly remediation: string;
readonly notes?: readonly StepItem[];
readonly additionalSections?: readonly AdditionalSection[];
}
@@ -136,7 +140,9 @@ export interface AnalysisFinding {
readonly category: TypstCategory;
readonly severity: TypstSeverity;
readonly confidence: TypstConfidence;
readonly owaspCategory: string;
readonly summary: FindingSummary;
readonly remediation: string;
readonly notes?: readonly StepItem[];
readonly additionalSections?: readonly AdditionalSection[];
}