fix(report): emit SARIF by default for exploit runs (#431)

* fix(report): emit SARIF by default for exploit runs, opt out with report.sarif: false

* docs: describe SARIF as on-by-default for exploit runs
This commit is contained in:
ezl-keygraph
2026-08-26 19:25:16 +05:30
committed by GitHub
parent f64a30040e
commit ed5659e2e2
12 changed files with 35 additions and 30 deletions
+1 -1
View File
@@ -164,7 +164,7 @@
"sarif": {
"type": "string",
"enum": ["true", "false"],
"description": "Emit a SARIF 2.1.0 log (report.sarif) beside the report. Requires exploit=true; ignored otherwise."
"description": "Emit a SARIF 2.1.0 log (report.sarif) beside the report. On by default for exploit runs; set \"false\" to opt out. Ignored when exploit=false."
}
},
"additionalProperties": false
+3 -2
View File
@@ -96,8 +96,9 @@ rules:
# Report filters applied by the report agent when assembling the final report (optional).
# Example below is illustrative; edit, remove, or add sections as needed.
# report:
# # Emit a SARIF 2.1.0 log (report.sarif) beside the report. Requires exploit: "true".
# sarif: "true"
# # SARIF 2.1.0 log (report.sarif) beside the report. On by default for exploit runs;
# # set "false" to opt out. Ignored when exploit is "false".
# sarif: "false"
# min_severity: low
# min_confidence: low
# guidance: |
+2 -1
View File
@@ -679,7 +679,8 @@ export const distributeConfig = (config: Config | null): DistributedConfig => {
const exploit = config?.exploit !== undefined ? config.exploit === 'true' : true;
const report = {
sarif: config?.report?.sarif === 'true',
// Default on; only an explicit "false" opts out.
sarif: config?.report?.sarif !== 'false',
...(config?.report?.min_severity && { min_severity: config.report.min_severity }),
...(config?.report?.min_confidence && { min_confidence: config.report.min_confidence }),
...(config?.report?.guidance && { guidance: config.report.guidance.trim() }),
+1 -1
View File
@@ -43,7 +43,7 @@ export const FINAL_REPORT_MD_FILENAME = 'Security-Assessment-Report.md';
/** Structured findings the report agent emits; the markdown report is rendered from it. */
export const REPORT_JSON_FILENAME = 'report.json';
/** SARIF 2.1.0 log, written only for exploit=true runs when report.sarif is enabled. */
/** SARIF 2.1.0 log, written for exploit=true runs unless report.sarif is set to false. */
export const SARIF_FILENAME = 'report.sarif';
/**
+1 -1
View File
@@ -29,7 +29,7 @@ export function getAgentGitPaths(agentName: AgentName): string[] {
paths.push(queueFilename);
}
// The report agent also emits the structured findings the markdown is rendered from, and the
// SARIF log when enabled. Listing the log unconditionally is harmless when it was not written,
// SARIF log when produced. Listing the log unconditionally is harmless when it was not written,
// and keeps a stale one from surviving the rollback of a failed attempt.
if (agentName === 'report') {
paths.push(REPORT_JSON_FILENAME);
+1 -1
View File
@@ -181,7 +181,7 @@ export async function injectModelIntoReport(
*
* The SARIF log is surfaced beside it when present, since a CI step consuming it needs a stable
* path and cannot be expected to reach into the internals directory. It is absent whenever the
* run was analysis-only or `report.sarif` was not enabled.
* run was analysis-only or `report.sarif` was set to false.
*/
export async function copyReportToRunRoot(
repoPath: string,
+9 -7
View File
@@ -450,13 +450,14 @@ export async function runAuthzExploitAgent(input: ActivityInput): Promise<AgentM
}
/**
* Write report.sarif when the run is exploitative and the operator asked for it.
* Write report.sarif for exploitative runs unless the operator opted out with report.sarif: false.
*
* Skipped entirely for analysis-only runs. The original reason was that those findings carried
* no severity, so every `result.level` would have been invented; since severity is recorded in
* both modes an analysis run could now populate `level`, but it would report an assessed
* severity as a measured one, so the gate stays. Failures are logged and swallowed — the SARIF
* log is a secondary artifact and must not fail a run whose report is already written.
* On by default so a CI step consuming the log always finds one. Skipped for analysis-only runs.
* The original reason was that those findings carried no severity, so every `result.level` would
* have been invented; since severity is recorded in both modes an analysis run could now populate
* `level`, but it would report an assessed severity as a measured one, so the gate stays. Failures
* are logged and swallowed — the SARIF log is a secondary artifact and must not fail a run whose
* report is already written.
*/
async function writeSarifIfEnabled(
input: ActivityInput,
@@ -469,7 +470,8 @@ async function writeSarifIfEnabled(
const container = getOrCreateContainer(input.workflowId, buildSessionMetadata(input), buildContainerConfig(input));
const configResult = await container.configLoader.loadOptional(input.configPath, undefined, input.configYAML);
if (isErr(configResult) || configResult.value?.report?.sarif !== true) return;
// Only an explicit false opts out; a missing config keeps the default on.
if (isErr(configResult) || configResult.value?.report?.sarif === false) return;
try {
const { renderSarif } = await import('../services/sarif-renderer.js');
+4 -1
View File
@@ -32,7 +32,10 @@ export interface ReportConfig {
min_severity?: Severity;
min_confidence?: Confidence;
guidance?: string;
/** Emit report.sarif alongside the markdown report. Ignored when exploit is false. */
/**
* Emit report.sarif alongside the markdown report. On by default for exploit runs; set 'false'
* to opt out. Ignored when exploit is false.
*/
sarif?: 'true' | 'false';
}