fix(review): convert refuses a partial or scoped Claude Security report; the stamp check is a trace (decisions 223, 224)
Two fail-closed checks in security-engine.mjs convert (decision 224), both "not measured" (exit 2, the reason on stderr, nothing on stdout): - the stamp's findings.total is not a non-negative integer, or the JSONL holds another number of records (a JSONL that lost the only BLOCKER line converted clean before; part 3 verifier, F3); - the stamp's scope is not an empty list: a scan limited by --scope, an absent scope or a non-list scope (Claude Security 0.12.0 writes scope: [] for an unscoped scan; F4). The real Claude Security 0.12.0 probe report still converts: exit 0, one BLOCKER, two below_threshold. Decision 223, text only: the module header and SECURITY.md keep the head/base check and say it is a trace, not a signature: code the reviewed run executed can write a report folder of its own (convert reads the matching one whose name sorts last) or delete the real one first (F2, a known issue). /trekreview's not-measured list names the two new reasons. The test helper writes the real stamp shape by default (findings.total = the records in the JSONL written, scope: []). New tests for behaviour that had none (F6): no fallback from an unverified newest match, a two-stamp folder passed over, a symlinked stamp, JSONL or folder refused, a malformed or absent scan_prefix, an unrecorded empty_diff or empty_scope. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
3be2bd3386
commit
f6e8529bdd
5 changed files with 179 additions and 13 deletions
|
|
@ -51,13 +51,22 @@
|
|||
// stamp (an empty diff writes none), verification.status not "verified",
|
||||
// researchers_returned or researchers_dispatched missing or returned <
|
||||
// dispatched, run_shape.empty_diff or empty_scope true or not recorded, a
|
||||
// malformed scan_prefix, a missing or unreadable JSONL, a record that does not
|
||||
// parse or lacks its file, title or a confidence of low|medium|high.
|
||||
// malformed scan_prefix, findings.total not a non-negative integer, a scope
|
||||
// that is not an empty list (a scan limited by `--scope` looked at part of
|
||||
// the range; Claude Security 0.12.0 writes `scope: []` for an unscoped scan;
|
||||
// an absent or non-list scope counts the same) (decision 224), a missing or
|
||||
// unreadable JSONL, a record that does not parse or lacks its file, title or
|
||||
// a confidence of low|medium|high, a JSONL whose number of records is not
|
||||
// the stamp's findings.total (decision 224).
|
||||
// exit 1 = the scope list is empty (as engine-findings.mjs); bad usage = 2.
|
||||
//
|
||||
// The report folder lives in the repo under review, so its text is data: a
|
||||
// stamp counts only when it names this exact head and merge base, and a
|
||||
// record's text is carried, never followed.
|
||||
// record's text is carried, never followed. That check is a trace, not a
|
||||
// signature (decision 223, a known issue): code the reviewed run executed can
|
||||
// write a report folder of its own (convert reads the matching one whose name
|
||||
// sorts last) or delete the real one before convert reads it, and nothing
|
||||
// here detects either.
|
||||
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
|
||||
|
|
@ -192,6 +201,13 @@ function stampProblem(stamp) {
|
|||
|| prefix.includes('\\') || prefix.split('/').slice(0, -1).some((seg) => seg === '' || seg === '.' || seg === '..')))) {
|
||||
return `scan_prefix ${JSON.stringify(prefix ?? null)} is not a path prefix`;
|
||||
}
|
||||
const total = isPlainObject(stamp.findings) ? stamp.findings.total : undefined;
|
||||
if (!Number.isInteger(total) || total < 0) {
|
||||
return `findings.total ${JSON.stringify(total ?? null)} is not a count of findings`;
|
||||
}
|
||||
if (!Array.isArray(stamp.scope) || stamp.scope.length !== 0) {
|
||||
return `scope ${JSON.stringify(stamp.scope ?? null)} is not [] (a scan limited by --scope, or none recorded), not a scan of the whole range`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -247,6 +263,9 @@ export function convertReport({ reportDir, base, head, scopeFiles } = {}) {
|
|||
if (problem) return notMeasured(`${found.name}/${found.stampName}: ${problem}`);
|
||||
const read = readRecords(found.folder, found.stamp.scan_prefix);
|
||||
if (read.reason) return notMeasured(`${found.name}: ${read.reason}`);
|
||||
if (read.records.length !== found.stamp.findings.total) {
|
||||
return notMeasured(`${found.name}: ${JSONL_NAME} holds ${read.records.length} record(s), the stamp's findings.total is ${found.stamp.findings.total}`);
|
||||
}
|
||||
const high = read.records.filter((r) => r.confidence === 'high').map((r) => r.finding);
|
||||
const rest = read.records.filter((r) => r.confidence !== 'high').map((r) => r.finding);
|
||||
const counted = toReviewerPayload({ engine: ENGINE, findings: high }, { scopeFiles });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue