fix: keep scan history rendering total over workbench payloads - #121
fix: keep scan history rendering total over workbench payloads#121mldangelo wants to merge 2 commits into
Conversation
runWorkbench only checks that the workbench response parsed as a JSON object, so nothing validates the shape between the Python workbench and the history renderer. The renderer then cast fields unconditionally, so a plugin or database that drifted from the expected shape surfaced as an unhandled TypeError with a stack trace instead of a usable view. Confirmed crashes: a finding whose `severity` is absent or null threw on `severity.level`; a `scans` row without `progress` threw on `progress.status`; a `knownSince` that is not a parsable timestamp threw RangeError out of Intl.DateTimeFormat. Read every field through small optional accessors instead. Missing data now renders as empty or `UNKNOWN`, an unparsable `knownSince` falls back to its raw value, and an unrecognized severity sorts below the known levels rather than above CRITICAL, where indexOf's -1 had placed it. Output for well-formed payloads is unchanged; the existing renderer tests assert exact text and pass unmodified.
14c2f2c to
3dc0894
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0282909557
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const knowledgeBase = recipe["knowledgeBasePaths"] as string[] | undefined; | ||
| if (knowledgeBase?.length) { | ||
| lines.push( | ||
| ` ${strong("KNOWLEDGE BASE")} ${knowledgeBase.map((path) => dim(clean(path))).join(", ")}`, |
There was a problem hiding this comment.
Validate knowledge-base paths before mapping
When a malformed or version-skewed workbench payload supplies the optional recipe.knowledgeBasePaths as a nonempty string (or an object with a truthy length) instead of an array, the length check passes and .map(...) throws a TypeError, aborting the entire history show rendering. Normalize this field with an array/type guard, as is already done for findings and matches, so one malformed optional value cannot crash the view.
Useful? React with 👍 / 👎.
| ` ${strong(clean(basename(text(result["targetPath"]))))} ${accent("·")} ${clean(result["scanId"])}`, | ||
| ` ${paint(`${status === "complete" ? "✓" : "●"} ${status.toUpperCase()}`, statusColor)} ${accent("·")} ${clean(result["mode"])}`, |
There was a problem hiding this comment.
Avoid rendering missing fields as undefined
When a malformed show payload omits optional scalar fields such as scanId or mode, the new total-rendering path no longer throws but passes those values to clean, which turns them into the literal text undefined; an empty payload consequently displays · undefined and UNKNOWN · undefined rather than degrading to blank or UNKNOWN values. The same pattern truncates absent comparison IDs to undefine, so these remaining scalar reads should use the new fallback-aware accessor before presentation.
Useful? React with 👍 / 👎.
| const summary = record(result["severityCounts"]); | ||
| if (Object.keys(summary).length > 0) { |
There was a problem hiding this comment.
Sanitize severity-count keys before output
When a malformed or hand-edited workbench database contains a severity value with terminal control sequences, severityCounts exposes that value as an object key and this normalization accepts it unchanged; the rendering loop then uppercases the key and interpolates it without calling clean. On the TTY-only TOON path, a key such as evil\u001b[2J\u001b[H therefore emits active clear-screen and cursor-positioning escapes, so severity keys need the same sanitization as counts and other workbench-derived labels.
Useful? React with 👍 / 👎.
|
Security review completed. No security issues were found in this pull request. Reviewed commit: Only the user who started this review can view the report in Codex. ℹ️ About Codex security reviews in GitHubThis is an experimental Codex feature. Security reviews are triggered when:
Once complete, Codex will leave suggestions, or a comment if no findings are found. |
Summary
runWorkbench(src/runtime.ts) validates only that the workbench response parsed as JSON andisRecord(result). Nothing checks the shape between the Python workbench andrenderScanHistory, and the renderer then cast every field unconditionally. Any drift — a plugin/database version skew, a partially migrated sqlite row, a hand-editedworkbench.sqlite3— surfaced as an unhandledTypeErrorwith a stack trace rather than a usable view or an actionable error.Three crashes confirmed by calling the renderer directly:
severityabsentTypeError: undefined is not an object (evaluating 'severity.level')severity: nulltypeof null === "object"scansrow withoutprogressTypeError: undefined is not an object (evaluating 'scan.progress.status')knownSince: "not-a-timestamp"RangeError: Invalid time valuefromIntl.DateTimeFormat.formatScope — please read before reviewing as a severity call
These are not reachable with the plugin as currently bundled. I checked
_bundled_plugin/scripts/workbench_db.py:severityis always emitted as{level: ...}andprogressis always populated, and every timestamp comes fromnow()=datetime.now(timezone.utc).isoformat(), soknownSinceis always ISO-8601. The renderer also only runs on a TTY with--format toon, so--format jsonwas never affected.So this is drift resilience, not a live bug. I'm raising it because the class is one bad row away, and a read-only history view crashing is a poor failure mode for a command whose whole job is to tell you what happened.
Approach: make the renderer total, don't validate at the boundary
I considered adding a schema check in
runWorkbenchinstead. I did not, for two reasons: the workbench payload has a large optional surface, so a schema would carry real risk of rejecting valid data and breaking working installs; and a display layer's correct response to a field it cannot read is to omit it, not to refuse the whole view.So every read goes through three small accessors —
record(),records(),text()— and the behavior is:progress.status→UNKNOWNknownSince→ the raw value, bounded to 32 chars (information preserved rather than dropped)Object.keys(SEVERITY_COLORS).indexOf(...)returned-1, which sorted an unknown severity above CRITICAL — a separate real ordering bug fixed here viaseverityRank().Testing / QA instructions
Baseline before this branch: 470 pass / 6 skip / 0 fail. After: 475 pass / 6 skip / 0 fail (five added tests).
The regression guard that matters most
CI=true bun test --timeout 30000 ./tests-ts/scan-history-renderer.test.tsExpect
11 pass / 0 fail. The six pre-existing tests in this file assert exact rendered text (viastripVTControlCharacters) across compare/list/show/match-all, narrow and wide widths, and dim-styling rules. They are unmodified and still pass, which is the proof that output for well-formed payloads did not change. If any of those six fail, the accessors changed real behavior and the diff is wrong.New coverage —
scan history renderer resiliencerenders comparison findings with a missing or null severity— both shapes; asserts the title andsrc/search.ts:10still render.sorts an unrecognized severity below every known severity— asserts CRITICAL appears before the unknown-severity finding in the output. Fails onmain(unknown sorts first).lists scans that carry no progress record— asserts the scan id renders and status showsUNKNOWN.falls back to the raw value for an unparsable knownSince— assertsKnown since not-a-timestamprather than aRangeError.renders every command from an empty payload—{}through all four commands, asserting none throw. This is the broad net for the casts I did not individually reproduce (result["coverage"],result["findings"],result["severityCounts"],basename(result["targetPath"]),basename(result["repository"])).Verifying the crashes exist on main
Note for maintainers
This file is also touched by PR "fix: align the severity badge column in finding tables". The two diffs are in different regions (accessors + helper functions here; badge padding and its dependent indents there) but both are small, so whichever lands second may need a trivial rebase.