auditor-lambda 0.2.6 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -7
- package/audit-code-wrapper-lib.mjs +1605 -330
- package/dist/adapters/eslint.js +9 -5
- package/dist/cli.d.ts +42 -1
- package/dist/cli.js +192 -80
- package/dist/coverage.d.ts +2 -2
- package/dist/coverage.js +5 -5
- package/dist/extractors/bucketing.d.ts +4 -0
- package/dist/extractors/bucketing.js +6 -2
- package/dist/extractors/disposition.d.ts +4 -0
- package/dist/extractors/disposition.js +15 -2
- package/dist/extractors/fileInventory.js +24 -28
- package/dist/extractors/flows.d.ts +5 -0
- package/dist/extractors/flows.js +25 -39
- package/dist/extractors/pathPatterns.d.ts +13 -3
- package/dist/extractors/pathPatterns.js +116 -53
- package/dist/extractors/risk.js +7 -1
- package/dist/extractors/surfaces.d.ts +4 -0
- package/dist/extractors/surfaces.js +11 -11
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/io/artifacts.d.ts +59 -44
- package/dist/io/artifacts.js +80 -120
- package/dist/io/json.d.ts +2 -0
- package/dist/io/json.js +65 -19
- package/dist/io/runArtifacts.d.ts +2 -1
- package/dist/io/runArtifacts.js +44 -7
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +579 -0
- package/dist/orchestrator/advance.js +84 -56
- package/dist/orchestrator/dependencyMap.js +9 -13
- package/dist/orchestrator/executors.js +7 -2
- package/dist/orchestrator/flowCoverage.js +11 -5
- package/dist/orchestrator/flowPlanning.d.ts +7 -2
- package/dist/orchestrator/flowPlanning.js +46 -21
- package/dist/orchestrator/flowRequeue.js +29 -9
- package/dist/orchestrator/internalExecutors.d.ts +2 -1
- package/dist/orchestrator/internalExecutors.js +130 -69
- package/dist/orchestrator/planning.js +25 -3
- package/dist/orchestrator/requeue.js +20 -5
- package/dist/orchestrator/resultIngestion.js +5 -6
- package/dist/orchestrator/runtimeValidation.d.ts +7 -2
- package/dist/orchestrator/runtimeValidation.js +61 -49
- package/dist/orchestrator/runtimeValidationUpdate.js +2 -4
- package/dist/orchestrator/state.js +18 -13
- package/dist/orchestrator/taskBuilder.d.ts +4 -2
- package/dist/orchestrator/taskBuilder.js +153 -52
- package/dist/orchestrator/trivialAudit.js +8 -5
- package/dist/orchestrator/unitBuilder.d.ts +3 -1
- package/dist/orchestrator/unitBuilder.js +24 -16
- package/dist/prompts/renderWorkerPrompt.d.ts +1 -1
- package/dist/prompts/renderWorkerPrompt.js +19 -10
- package/dist/providers/claudeCodeProvider.d.ts +4 -1
- package/dist/providers/claudeCodeProvider.js +8 -5
- package/dist/providers/localSubprocessProvider.d.ts +4 -0
- package/dist/providers/localSubprocessProvider.js +7 -2
- package/dist/providers/spawnLoggedCommand.d.ts +9 -1
- package/dist/providers/spawnLoggedCommand.js +77 -29
- package/dist/reporting/mergeFindings.js +0 -11
- package/dist/reporting/synthesis.d.ts +26 -21
- package/dist/reporting/synthesis.js +97 -61
- package/dist/reporting/workBlocks.d.ts +12 -3
- package/dist/reporting/workBlocks.js +124 -70
- package/dist/supervisor/operatorHandoff.js +48 -18
- package/dist/supervisor/runLedger.d.ts +1 -1
- package/dist/supervisor/runLedger.js +112 -5
- package/dist/supervisor/sessionConfig.js +10 -10
- package/dist/types/externalAnalyzer.d.ts +3 -0
- package/dist/types/flowCoverage.d.ts +5 -1
- package/dist/types/flowCoverage.js +5 -1
- package/dist/types/flows.d.ts +6 -0
- package/dist/types/flows.js +1 -1
- package/dist/types/runLedger.d.ts +5 -1
- package/dist/types/runLedger.js +6 -1
- package/dist/types/runtimeValidation.d.ts +13 -3
- package/dist/types/runtimeValidation.js +16 -1
- package/dist/types/sessionConfig.d.ts +15 -2
- package/dist/types/sessionConfig.js +15 -1
- package/dist/types/surfaces.d.ts +4 -1
- package/dist/types/surfaces.js +1 -1
- package/dist/types/workerSession.d.ts +9 -0
- package/dist/types/workerSession.js +5 -1
- package/dist/types.d.ts +4 -7
- package/dist/validation/artifacts.d.ts +1 -1
- package/dist/validation/artifacts.js +33 -20
- package/dist/validation/auditResults.d.ts +2 -2
- package/dist/validation/auditResults.js +71 -114
- package/dist/validation/basic.d.ts +9 -1
- package/dist/validation/basic.js +40 -3
- package/dist/validation/sessionConfig.d.ts +4 -2
- package/dist/validation/sessionConfig.js +62 -15
- package/docs/agent-integrations.md +67 -38
- package/docs/artifacts.md +16 -56
- package/docs/bootstrap-install.md +60 -30
- package/docs/contract.md +22 -205
- package/docs/next-steps.md +76 -44
- package/docs/packaging.md +27 -3
- package/docs/product-direction.md +22 -0
- package/docs/production-launch-bar.md +4 -2
- package/docs/production-readiness.md +9 -5
- package/docs/releasing.md +98 -0
- package/docs/remediation-baseline.md +75 -0
- package/docs/run-flow.md +23 -11
- package/docs/session-config.md +50 -5
- package/docs/supervisor.md +7 -0
- package/docs/workflow-refactor-brief.md +177 -0
- package/package.json +4 -1
- package/schemas/audit_result.schema.json +8 -7
- package/schemas/audit_task.schema.json +3 -1
- package/schemas/coverage_matrix.schema.json +3 -3
- package/schemas/critical_flows.schema.json +6 -2
- package/schemas/file_disposition.schema.json +2 -2
- package/schemas/finding.schema.json +9 -4
- package/schemas/flow_coverage.schema.json +2 -2
- package/schemas/repo_manifest.schema.json +4 -4
- package/schemas/risk_register.schema.json +2 -2
- package/schemas/runtime_validation_report.schema.json +3 -3
- package/schemas/runtime_validation_tasks.schema.json +8 -2
- package/schemas/surface_manifest.schema.json +6 -3
- package/schemas/unit_manifest.schema.json +3 -2
- package/skills/audit-code/SKILL.md +16 -2
- package/skills/audit-code/audit-code.prompt.md +5 -8
- package/schemas/merged_findings.schema.json +0 -19
- package/schemas/root_cause_clusters.schema.json +0 -28
- package/schemas/synthesis_report.schema.json +0 -61
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { createWriteStream } from "node:fs";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
|
+
const TERMINATION_SIGNAL = "SIGTERM";
|
|
4
|
+
const FORCE_KILL_SIGNAL = "SIGKILL";
|
|
5
|
+
const FORCE_KILL_GRACE_MS = 1_000;
|
|
3
6
|
function tee(write, chunk) {
|
|
4
7
|
write.write(chunk);
|
|
5
8
|
}
|
|
@@ -7,22 +10,77 @@ function tee(write, chunk) {
|
|
|
7
10
|
// does not consult PATH for executables without a shell. Callers should use
|
|
8
11
|
// `platformCommand()` (scripts/smoke-packaged-audit-code.mjs) or similar to
|
|
9
12
|
// supply the correct command form for the host OS.
|
|
10
|
-
export async function spawnLoggedCommand(command, args, input, env) {
|
|
13
|
+
export async function spawnLoggedCommand(command, args, input, env, options = {}) {
|
|
14
|
+
const openWriteStream = options.createWriteStream ?? createWriteStream;
|
|
15
|
+
const spawnProcess = options.spawn ?? spawn;
|
|
16
|
+
const killGraceMs = options.killGraceMs ?? FORCE_KILL_GRACE_MS;
|
|
11
17
|
return await new Promise((resolve, reject) => {
|
|
12
|
-
const stdoutLog =
|
|
13
|
-
const stderrLog =
|
|
18
|
+
const stdoutLog = openWriteStream(input.stdoutPath, { flags: "a" });
|
|
19
|
+
const stderrLog = openWriteStream(input.stderrPath, { flags: "a" });
|
|
14
20
|
const startedAt = Date.now();
|
|
15
21
|
let timedOut = false;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
+
let settled = false;
|
|
23
|
+
let child = null;
|
|
24
|
+
let timer;
|
|
25
|
+
let heartbeat;
|
|
26
|
+
let forceKillTimer;
|
|
27
|
+
const cleanup = () => {
|
|
28
|
+
if (timer) {
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
}
|
|
31
|
+
if (heartbeat) {
|
|
32
|
+
clearInterval(heartbeat);
|
|
33
|
+
}
|
|
34
|
+
if (forceKillTimer) {
|
|
35
|
+
clearTimeout(forceKillTimer);
|
|
36
|
+
}
|
|
37
|
+
stdoutLog.end();
|
|
38
|
+
stderrLog.end();
|
|
39
|
+
};
|
|
40
|
+
const settle = (callback) => {
|
|
41
|
+
if (settled) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
settled = true;
|
|
45
|
+
cleanup();
|
|
46
|
+
callback();
|
|
47
|
+
};
|
|
48
|
+
const fail = (error) => {
|
|
49
|
+
if (child && !child.killed) {
|
|
50
|
+
child.kill(FORCE_KILL_SIGNAL);
|
|
51
|
+
}
|
|
52
|
+
const normalized = error instanceof Error ? error : new Error(String(error));
|
|
53
|
+
settle(() => reject(normalized));
|
|
54
|
+
};
|
|
55
|
+
stdoutLog.on("error", fail);
|
|
56
|
+
stderrLog.on("error", fail);
|
|
57
|
+
let spawnedChild;
|
|
58
|
+
try {
|
|
59
|
+
spawnedChild = spawnProcess(command, args, {
|
|
60
|
+
cwd: input.repoRoot,
|
|
61
|
+
env: { ...process.env, ...env },
|
|
62
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
63
|
+
});
|
|
64
|
+
child = spawnedChild;
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
fail(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
if (!spawnedChild.stdout || !spawnedChild.stderr) {
|
|
71
|
+
fail(new Error(`Fresh session spawn for run ${input.runId} did not provide pipe-backed stdout/stderr streams.`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
timer = setTimeout(() => {
|
|
22
75
|
timedOut = true;
|
|
23
|
-
|
|
76
|
+
spawnedChild.kill(TERMINATION_SIGNAL);
|
|
77
|
+
forceKillTimer = setTimeout(() => {
|
|
78
|
+
if (!settled) {
|
|
79
|
+
spawnedChild.kill(FORCE_KILL_SIGNAL);
|
|
80
|
+
}
|
|
81
|
+
}, killGraceMs);
|
|
24
82
|
}, input.timeoutMs);
|
|
25
|
-
|
|
83
|
+
heartbeat = setInterval(() => {
|
|
26
84
|
const elapsedMs = Date.now() - startedAt;
|
|
27
85
|
const message = `[provider] run ${input.runId} still running after ${elapsedMs}ms\n`;
|
|
28
86
|
tee(stderrLog, message);
|
|
@@ -30,40 +88,30 @@ export async function spawnLoggedCommand(command, args, input, env) {
|
|
|
30
88
|
process.stderr.write(message);
|
|
31
89
|
}
|
|
32
90
|
}, 30_000);
|
|
33
|
-
|
|
91
|
+
spawnedChild.stdout.on("data", (chunk) => {
|
|
34
92
|
tee(stdoutLog, chunk);
|
|
35
93
|
if (input.uiMode === "visible") {
|
|
36
94
|
process.stdout.write(chunk);
|
|
37
95
|
}
|
|
38
96
|
});
|
|
39
|
-
|
|
97
|
+
spawnedChild.stderr.on("data", (chunk) => {
|
|
40
98
|
tee(stderrLog, chunk);
|
|
41
99
|
if (input.uiMode === "visible") {
|
|
42
100
|
process.stderr.write(chunk);
|
|
43
101
|
}
|
|
44
102
|
});
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
clearInterval(heartbeat);
|
|
48
|
-
stdoutLog.end();
|
|
49
|
-
stderrLog.end();
|
|
50
|
-
reject(error);
|
|
51
|
-
});
|
|
52
|
-
child.on("exit", (code, signal) => {
|
|
53
|
-
clearTimeout(timer);
|
|
54
|
-
clearInterval(heartbeat);
|
|
55
|
-
stdoutLog.end();
|
|
56
|
-
stderrLog.end();
|
|
103
|
+
spawnedChild.on("error", fail);
|
|
104
|
+
spawnedChild.on("exit", (code, signal) => {
|
|
57
105
|
if (timedOut) {
|
|
58
|
-
reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`));
|
|
106
|
+
settle(() => reject(new Error(`Fresh session timed out after ${input.timeoutMs}ms for run ${input.runId}.`)));
|
|
59
107
|
return;
|
|
60
108
|
}
|
|
61
|
-
resolve({
|
|
109
|
+
settle(() => resolve({
|
|
62
110
|
accepted: true,
|
|
63
|
-
processId:
|
|
111
|
+
processId: spawnedChild.pid,
|
|
64
112
|
exitCode: code,
|
|
65
113
|
signal,
|
|
66
|
-
});
|
|
114
|
+
}));
|
|
67
115
|
});
|
|
68
116
|
});
|
|
69
117
|
}
|
|
@@ -82,9 +82,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
82
82
|
...analyzerEvidence,
|
|
83
83
|
]),
|
|
84
84
|
],
|
|
85
|
-
related_findings: finding.related_findings && finding.related_findings.length > 0
|
|
86
|
-
? [...new Set(finding.related_findings)]
|
|
87
|
-
: undefined,
|
|
88
85
|
});
|
|
89
86
|
continue;
|
|
90
87
|
}
|
|
@@ -110,14 +107,6 @@ export function mergeFindings(results, runtimeReport, externalAnalyzerResults) {
|
|
|
110
107
|
...analyzerEvidence,
|
|
111
108
|
]),
|
|
112
109
|
];
|
|
113
|
-
const related = new Set([
|
|
114
|
-
...(existing.related_findings ?? []),
|
|
115
|
-
...(finding.related_findings ?? []),
|
|
116
|
-
existing.id,
|
|
117
|
-
finding.id,
|
|
118
|
-
]);
|
|
119
|
-
existing.related_findings =
|
|
120
|
-
related.size > 0 ? [...related].sort() : undefined;
|
|
121
110
|
}
|
|
122
111
|
}
|
|
123
112
|
return [...merged.values()].sort((a, b) => {
|
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
import type { AuditResult } from "../types.js";
|
|
1
|
+
import type { AuditResult, CoverageMatrix, Finding, UnitManifest } from "../types.js";
|
|
2
2
|
import type { ExternalAnalyzerResults } from "../types/externalAnalyzer.js";
|
|
3
|
+
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
4
|
+
import type { GraphBundle } from "../types/graph.js";
|
|
3
5
|
import type { RuntimeValidationReport } from "../types/runtimeValidation.js";
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
runtime_validation_status_breakdown: Record<string, number>;
|
|
13
|
-
notes: string[];
|
|
14
|
-
severity_breakdown: Record<string, number>;
|
|
15
|
-
external_analyzer_summary: {
|
|
16
|
-
tool_count: number;
|
|
17
|
-
result_count: number;
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
merged_findings: ReturnType<typeof mergeFindings>;
|
|
21
|
-
root_cause_clusters: ReturnType<typeof buildRootCauseClusters>;
|
|
22
|
-
work_blocks: ReturnType<typeof buildWorkBlocks>;
|
|
6
|
+
import { type WorkBlock } from "./workBlocks.js";
|
|
7
|
+
export interface AuditReportSummary {
|
|
8
|
+
finding_count: number;
|
|
9
|
+
work_block_count: number;
|
|
10
|
+
severity_breakdown: Record<string, number>;
|
|
11
|
+
audited_file_count: number;
|
|
12
|
+
excluded_file_count: number;
|
|
13
|
+
runtime_validation_status_breakdown: Record<string, number>;
|
|
23
14
|
}
|
|
24
|
-
export
|
|
15
|
+
export interface AuditReportModel {
|
|
16
|
+
summary: AuditReportSummary;
|
|
17
|
+
findings: Finding[];
|
|
18
|
+
work_blocks: WorkBlock[];
|
|
19
|
+
}
|
|
20
|
+
export declare function buildAuditReportModel(params: {
|
|
21
|
+
results: AuditResult[];
|
|
22
|
+
unitManifest?: UnitManifest;
|
|
23
|
+
graphBundle?: GraphBundle;
|
|
24
|
+
criticalFlows?: CriticalFlowManifest;
|
|
25
|
+
coverageMatrix?: CoverageMatrix;
|
|
26
|
+
runtimeValidationReport?: RuntimeValidationReport;
|
|
27
|
+
externalAnalyzerResults?: ExternalAnalyzerResults;
|
|
28
|
+
}): AuditReportModel;
|
|
29
|
+
export declare function renderAuditReportMarkdown(model: AuditReportModel): string;
|
|
@@ -1,77 +1,113 @@
|
|
|
1
|
-
import { mergeFindings } from "./mergeFindings.js";
|
|
2
|
-
import { buildRootCauseClusters } from "./rootCause.js";
|
|
3
1
|
import { buildWorkBlocks } from "./workBlocks.js";
|
|
4
|
-
|
|
2
|
+
import { mergeFindings } from "./mergeFindings.js";
|
|
3
|
+
function countBy(items, selectKey) {
|
|
5
4
|
const breakdown = {};
|
|
6
|
-
for (const
|
|
7
|
-
|
|
5
|
+
for (const item of items) {
|
|
6
|
+
const key = selectKey(item);
|
|
7
|
+
if (!key) {
|
|
8
|
+
continue;
|
|
9
|
+
}
|
|
10
|
+
breakdown[key] = (breakdown[key] ?? 0) + 1;
|
|
8
11
|
}
|
|
9
12
|
return breakdown;
|
|
10
13
|
}
|
|
11
14
|
function severityBreakdown(findings) {
|
|
12
|
-
|
|
13
|
-
for (const finding of findings) {
|
|
14
|
-
breakdown[finding.severity] = (breakdown[finding.severity] ?? 0) + 1;
|
|
15
|
-
}
|
|
16
|
-
return breakdown;
|
|
15
|
+
return countBy(findings, (finding) => finding.severity);
|
|
17
16
|
}
|
|
18
|
-
function
|
|
19
|
-
|
|
20
|
-
const findingsByLens = new Map();
|
|
21
|
-
for (const result of results) {
|
|
22
|
-
tasksByLens.set(result.lens, (tasksByLens.get(result.lens) ?? 0) + 1);
|
|
23
|
-
findingsByLens.set(result.lens, (findingsByLens.get(result.lens) ?? 0) + result.findings.length);
|
|
24
|
-
}
|
|
25
|
-
const zeroLenses = [...tasksByLens.entries()]
|
|
26
|
-
.filter(([lens, count]) => count > 0 && (findingsByLens.get(lens) ?? 0) === 0)
|
|
27
|
-
.map(([lens]) => lens)
|
|
28
|
-
.sort();
|
|
29
|
-
if (zeroLenses.length === 0)
|
|
30
|
-
return [];
|
|
31
|
-
return [
|
|
32
|
-
`Zero findings across all reviewed tasks for lens(es): ${zeroLenses.join(", ")}. Verify these tasks were genuinely reviewed rather than batch-generated.`,
|
|
33
|
-
];
|
|
17
|
+
function runtimeStatusBreakdown(report) {
|
|
18
|
+
return countBy(report?.results ?? [], (result) => result.status);
|
|
34
19
|
}
|
|
35
|
-
function
|
|
36
|
-
|
|
37
|
-
return { tool_count: 0, result_count: 0 };
|
|
38
|
-
}
|
|
20
|
+
function coverageSummary(coverage) {
|
|
21
|
+
const files = coverage?.files ?? [];
|
|
39
22
|
return {
|
|
40
|
-
|
|
41
|
-
|
|
23
|
+
audited_file_count: files.filter((file) => file.audit_status === "complete").length,
|
|
24
|
+
excluded_file_count: files.filter((file) => file.audit_status === "excluded").length,
|
|
42
25
|
};
|
|
43
26
|
}
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
27
|
+
function formatSeverityList(summary) {
|
|
28
|
+
const ordered = ["critical", "high", "medium", "low", "info"];
|
|
29
|
+
const parts = ordered
|
|
30
|
+
.filter((severity) => (summary[severity] ?? 0) > 0)
|
|
31
|
+
.map((severity) => `${severity}: ${summary[severity]}`);
|
|
32
|
+
return parts.length > 0 ? parts.join(", ") : "none";
|
|
33
|
+
}
|
|
34
|
+
export function buildAuditReportModel(params) {
|
|
35
|
+
const findings = mergeFindings(params.results, params.runtimeValidationReport, params.externalAnalyzerResults);
|
|
36
|
+
const workBlocks = buildWorkBlocks({
|
|
37
|
+
findings,
|
|
38
|
+
unitManifest: params.unitManifest,
|
|
39
|
+
graphBundle: params.graphBundle,
|
|
40
|
+
criticalFlows: params.criticalFlows,
|
|
41
|
+
});
|
|
42
|
+
const coverage = coverageSummary(params.coverageMatrix);
|
|
51
43
|
return {
|
|
52
44
|
summary: {
|
|
53
|
-
finding_count:
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
notes: [
|
|
60
|
-
...(Object.keys(runtimeBreakdown).length === 0
|
|
61
|
-
? ["No runtime validation evidence attached."]
|
|
62
|
-
: [
|
|
63
|
-
"Runtime validation evidence has been incorporated into synthesis.",
|
|
64
|
-
]),
|
|
65
|
-
...(extSummary.result_count > 0
|
|
66
|
-
? [
|
|
67
|
-
`External analyzer signals incorporated: ${extSummary.result_count} result(s) from ${externalAnalyzerResults?.tool}.`,
|
|
68
|
-
]
|
|
69
|
-
: []),
|
|
70
|
-
...zeroFindingLensNotes(results),
|
|
71
|
-
],
|
|
45
|
+
finding_count: findings.length,
|
|
46
|
+
work_block_count: workBlocks.length,
|
|
47
|
+
severity_breakdown: severityBreakdown(findings),
|
|
48
|
+
audited_file_count: coverage.audited_file_count,
|
|
49
|
+
excluded_file_count: coverage.excluded_file_count,
|
|
50
|
+
runtime_validation_status_breakdown: runtimeStatusBreakdown(params.runtimeValidationReport),
|
|
72
51
|
},
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
work_blocks,
|
|
52
|
+
findings,
|
|
53
|
+
work_blocks: workBlocks,
|
|
76
54
|
};
|
|
77
55
|
}
|
|
56
|
+
export function renderAuditReportMarkdown(model) {
|
|
57
|
+
const lines = [
|
|
58
|
+
"# Audit Report",
|
|
59
|
+
"",
|
|
60
|
+
"## Summary",
|
|
61
|
+
"",
|
|
62
|
+
`- Findings: ${model.summary.finding_count}`,
|
|
63
|
+
`- Work blocks: ${model.summary.work_block_count}`,
|
|
64
|
+
`- Severity breakdown: ${formatSeverityList(model.summary.severity_breakdown)}`,
|
|
65
|
+
`- Fully audited files: ${model.summary.audited_file_count}`,
|
|
66
|
+
`- Excluded non-auditable files: ${model.summary.excluded_file_count}`,
|
|
67
|
+
"",
|
|
68
|
+
"## Work Blocks",
|
|
69
|
+
"",
|
|
70
|
+
];
|
|
71
|
+
if (model.work_blocks.length === 0) {
|
|
72
|
+
lines.push("No remediation work blocks were generated.", "");
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
for (const block of model.work_blocks) {
|
|
76
|
+
lines.push(`### ${block.id}`);
|
|
77
|
+
lines.push("");
|
|
78
|
+
lines.push(`- Max severity: ${block.max_severity}`);
|
|
79
|
+
lines.push(`- Units: ${block.unit_ids.join(", ")}`);
|
|
80
|
+
lines.push(`- Owned files: ${block.owned_files.join(", ")}`);
|
|
81
|
+
lines.push(`- Findings: ${block.finding_ids.join(", ")}`);
|
|
82
|
+
lines.push(`- Depends on: ${block.depends_on.length > 0 ? block.depends_on.join(", ") : "none"}`);
|
|
83
|
+
lines.push(`- Rationale: ${block.rationale}`);
|
|
84
|
+
lines.push("");
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
lines.push("## Findings", "");
|
|
88
|
+
if (model.findings.length === 0) {
|
|
89
|
+
lines.push("No findings were recorded.", "");
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
for (const finding of model.findings) {
|
|
93
|
+
lines.push(`### ${finding.id} — ${finding.title}`);
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push(`- Severity: ${finding.severity}`);
|
|
96
|
+
lines.push(`- Confidence: ${finding.confidence}`);
|
|
97
|
+
lines.push(`- Lens: ${finding.lens}`);
|
|
98
|
+
lines.push(`- Files: ${finding.affected_files.map((file) => file.path).join(", ")}`);
|
|
99
|
+
lines.push(`- Summary: ${finding.summary}`);
|
|
100
|
+
if (finding.evidence && finding.evidence.length > 0) {
|
|
101
|
+
lines.push("- Evidence:");
|
|
102
|
+
for (const evidence of finding.evidence) {
|
|
103
|
+
lines.push(` - ${evidence}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push("");
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
lines.push("## Scope and Coverage", "");
|
|
110
|
+
lines.push("This report is deterministic output from the completed audit. Non-auditable files were excluded from scope before task generation.");
|
|
111
|
+
lines.push("");
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import type { Finding } from "../types.js";
|
|
1
|
+
import type { Finding, UnitManifest } from "../types.js";
|
|
2
|
+
import type { CriticalFlowManifest } from "../types/flows.js";
|
|
3
|
+
import type { GraphBundle } from "../types/graph.js";
|
|
2
4
|
export interface WorkBlock {
|
|
3
5
|
id: string;
|
|
4
6
|
finding_ids: string[];
|
|
5
|
-
|
|
7
|
+
unit_ids: string[];
|
|
8
|
+
owned_files: string[];
|
|
6
9
|
max_severity: Finding["severity"];
|
|
7
10
|
rationale: string;
|
|
11
|
+
depends_on: string[];
|
|
8
12
|
}
|
|
9
|
-
export declare function buildWorkBlocks(
|
|
13
|
+
export declare function buildWorkBlocks(params: {
|
|
14
|
+
findings: Finding[];
|
|
15
|
+
unitManifest?: UnitManifest;
|
|
16
|
+
graphBundle?: GraphBundle;
|
|
17
|
+
criticalFlows?: CriticalFlowManifest;
|
|
18
|
+
}): WorkBlock[];
|
|
@@ -12,98 +12,152 @@ function severityRank(severity) {
|
|
|
12
12
|
return 1;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
function buildFileUnitMap(unitManifest) {
|
|
16
|
+
const map = new Map();
|
|
17
|
+
for (const unit of unitManifest?.units ?? []) {
|
|
18
|
+
for (const path of unit.files) {
|
|
19
|
+
if (!map.has(path)) {
|
|
20
|
+
map.set(path, unit.unit_id);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return map;
|
|
25
|
+
}
|
|
26
|
+
function normalizeOwnedUnits(finding, fileUnitMap) {
|
|
27
|
+
const unitIds = new Set();
|
|
28
|
+
for (const file of finding.affected_files) {
|
|
29
|
+
const mapped = fileUnitMap.get(file.path);
|
|
30
|
+
unitIds.add(mapped ?? `file:${file.path}`);
|
|
31
|
+
}
|
|
32
|
+
return [...unitIds].sort();
|
|
33
|
+
}
|
|
34
|
+
function computeDependencies(params) {
|
|
35
|
+
const blockByFile = new Map();
|
|
36
|
+
for (const block of params.blocks) {
|
|
37
|
+
for (const path of block.owned_files) {
|
|
38
|
+
blockByFile.set(path, block.id);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const dependsOn = new Map();
|
|
42
|
+
for (const block of params.blocks) {
|
|
43
|
+
dependsOn.set(block.id, new Set());
|
|
44
|
+
}
|
|
45
|
+
const graphEdges = [
|
|
46
|
+
...(params.graphBundle?.graphs.imports ?? []),
|
|
47
|
+
...(params.graphBundle?.graphs.calls ?? []),
|
|
48
|
+
];
|
|
49
|
+
for (const edge of graphEdges) {
|
|
50
|
+
const fromBlock = blockByFile.get(edge.from);
|
|
51
|
+
const toBlock = blockByFile.get(edge.to);
|
|
52
|
+
if (fromBlock && toBlock && fromBlock !== toBlock) {
|
|
53
|
+
dependsOn.get(fromBlock)?.add(toBlock);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
for (const flow of params.criticalFlows?.flows ?? []) {
|
|
57
|
+
const flowBlocks = new Set();
|
|
58
|
+
for (const path of flow.paths) {
|
|
59
|
+
const blockId = blockByFile.get(path);
|
|
60
|
+
if (blockId) {
|
|
61
|
+
flowBlocks.add(blockId);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const ordered = [...flowBlocks].sort();
|
|
65
|
+
for (let i = 1; i < ordered.length; i++) {
|
|
66
|
+
dependsOn.get(ordered[i - 1])?.add(ordered[i]);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return params.blocks.map((block) => ({
|
|
70
|
+
...block,
|
|
71
|
+
depends_on: [...(dependsOn.get(block.id) ?? [])].sort(),
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
export function buildWorkBlocks(params) {
|
|
75
|
+
if (params.findings.length === 0) {
|
|
17
76
|
return [];
|
|
77
|
+
}
|
|
78
|
+
const fileUnitMap = buildFileUnitMap(params.unitManifest);
|
|
18
79
|
const parent = new Map();
|
|
80
|
+
const findingUnits = new Map();
|
|
19
81
|
function find(id) {
|
|
20
82
|
if (!parent.has(id))
|
|
21
83
|
parent.set(id, id);
|
|
22
|
-
const
|
|
23
|
-
if (
|
|
84
|
+
const current = parent.get(id);
|
|
85
|
+
if (current === id)
|
|
24
86
|
return id;
|
|
25
|
-
const root = find(
|
|
87
|
+
const root = find(current);
|
|
26
88
|
parent.set(id, root);
|
|
27
89
|
return root;
|
|
28
90
|
}
|
|
29
91
|
function union(a, b) {
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
if (
|
|
33
|
-
parent.set(
|
|
34
|
-
}
|
|
35
|
-
for (const finding of findings)
|
|
36
|
-
find(finding.id);
|
|
37
|
-
// Union findings that share affected files
|
|
38
|
-
const fileToIds = new Map();
|
|
39
|
-
for (const finding of findings) {
|
|
40
|
-
for (const af of finding.affected_files) {
|
|
41
|
-
const ids = fileToIds.get(af.path) ?? [];
|
|
42
|
-
ids.push(finding.id);
|
|
43
|
-
fileToIds.set(af.path, ids);
|
|
92
|
+
const rootA = find(a);
|
|
93
|
+
const rootB = find(b);
|
|
94
|
+
if (rootA !== rootB) {
|
|
95
|
+
parent.set(rootA, rootB);
|
|
44
96
|
}
|
|
45
97
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
98
|
+
const unitToFindingIds = new Map();
|
|
99
|
+
for (const finding of params.findings) {
|
|
100
|
+
parent.set(finding.id, finding.id);
|
|
101
|
+
const ownedUnits = normalizeOwnedUnits(finding, fileUnitMap);
|
|
102
|
+
findingUnits.set(finding.id, ownedUnits);
|
|
103
|
+
for (const unitId of ownedUnits) {
|
|
104
|
+
const ids = unitToFindingIds.get(unitId) ?? [];
|
|
105
|
+
ids.push(finding.id);
|
|
106
|
+
unitToFindingIds.set(unitId, ids);
|
|
49
107
|
}
|
|
50
108
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
for (const relId of finding.related_findings ?? []) {
|
|
55
|
-
if (knownIds.has(relId))
|
|
56
|
-
union(finding.id, relId);
|
|
109
|
+
for (const ids of unitToFindingIds.values()) {
|
|
110
|
+
for (let index = 1; index < ids.length; index++) {
|
|
111
|
+
union(ids[0], ids[index]);
|
|
57
112
|
}
|
|
58
113
|
}
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
for (const finding of findings) {
|
|
114
|
+
const grouped = new Map();
|
|
115
|
+
for (const finding of params.findings) {
|
|
62
116
|
const root = find(finding.id);
|
|
63
|
-
const group =
|
|
117
|
+
const group = grouped.get(root) ?? [];
|
|
64
118
|
group.push(finding);
|
|
65
|
-
|
|
119
|
+
grouped.set(root, group);
|
|
66
120
|
}
|
|
67
|
-
const blocks = []
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return sevDelta;
|
|
74
|
-
const sysA = a.systemic ? 1 : 0;
|
|
75
|
-
const sysB = b.systemic ? 1 : 0;
|
|
76
|
-
if (sysA !== sysB)
|
|
77
|
-
return sysB - sysA;
|
|
78
|
-
return a.title.localeCompare(b.title);
|
|
121
|
+
const blocks = [...grouped.values()].map((group, index) => {
|
|
122
|
+
const orderedFindings = [...group].sort((a, b) => {
|
|
123
|
+
const severityDelta = severityRank(b.severity) - severityRank(a.severity);
|
|
124
|
+
if (severityDelta !== 0)
|
|
125
|
+
return severityDelta;
|
|
126
|
+
return a.id.localeCompare(b.id);
|
|
79
127
|
});
|
|
80
|
-
const
|
|
81
|
-
...new Set(
|
|
128
|
+
const unitIds = [
|
|
129
|
+
...new Set(group.flatMap((finding) => findingUnits.get(finding.id) ?? [])),
|
|
82
130
|
].sort();
|
|
83
|
-
const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
max_severity:
|
|
92
|
-
rationale
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
131
|
+
const ownedFiles = [
|
|
132
|
+
...new Set(group.flatMap((finding) => finding.affected_files.map((file) => file.path))),
|
|
133
|
+
].sort();
|
|
134
|
+
return {
|
|
135
|
+
id: `block-${index + 1}`,
|
|
136
|
+
finding_ids: orderedFindings.map((finding) => finding.id),
|
|
137
|
+
unit_ids: unitIds,
|
|
138
|
+
owned_files: ownedFiles,
|
|
139
|
+
max_severity: orderedFindings[0].severity,
|
|
140
|
+
rationale: unitIds.length === 1
|
|
141
|
+
? "All findings map to the same owned unit and should be remediated together."
|
|
142
|
+
: "Findings share owned units transitively and should remain one non-overlapping remediation block.",
|
|
143
|
+
depends_on: [],
|
|
144
|
+
};
|
|
145
|
+
});
|
|
96
146
|
blocks.sort((a, b) => {
|
|
97
|
-
const
|
|
98
|
-
if (
|
|
99
|
-
return
|
|
100
|
-
const
|
|
101
|
-
if (
|
|
102
|
-
return
|
|
103
|
-
return
|
|
147
|
+
const severityDelta = severityRank(b.max_severity) - severityRank(a.max_severity);
|
|
148
|
+
if (severityDelta !== 0)
|
|
149
|
+
return severityDelta;
|
|
150
|
+
const findingDelta = b.finding_ids.length - a.finding_ids.length;
|
|
151
|
+
if (findingDelta !== 0)
|
|
152
|
+
return findingDelta;
|
|
153
|
+
return a.id.localeCompare(b.id);
|
|
104
154
|
});
|
|
105
|
-
for (let
|
|
106
|
-
blocks[
|
|
155
|
+
for (let index = 0; index < blocks.length; index++) {
|
|
156
|
+
blocks[index].id = `block-${index + 1}`;
|
|
107
157
|
}
|
|
108
|
-
return
|
|
158
|
+
return computeDependencies({
|
|
159
|
+
blocks,
|
|
160
|
+
graphBundle: params.graphBundle,
|
|
161
|
+
criticalFlows: params.criticalFlows,
|
|
162
|
+
});
|
|
109
163
|
}
|