Report what the scanner skipped, filtered, and could not read, in every format - #4
Merged
Merged
Conversation
A fresh-user release test on the 1.3.0 candidate found five blocking defects. The tag was never created. Each is fixed here with a regression test confirmed to fail against the pre-fix code. Version provenance. One binary gave four answers: the version command said 1.3.0 while SARIF and CBOM both claimed 1.0.0 and JSON carried none. SARIF and CBOM are provenance artifacts, so a stale literal there is a false record of what produced the document. pkg/version is now the single source of truth, fed once from the values the linker injects into main. Silently skipped manifests. A tree with a good and a corrupt package.json scanned only the good one and reported a clean summary, so a manifest broken by a bad merge became invisible and CI went green. The cause was not the parse-error path: validateManifest rejected the file during discovery, before any parser ran, so no parse error ever existed. Unreadable manifests are now named with their reason in every output format and always exit 2. False clean verdicts. A scan where every dependency was absent from the database reported "No cryptographic usage detected" having examined nothing. The three cases are now distinguished, and the --deep hints the analyzer always generated are finally printed. Inert filters. --risk and --min-severity were stored and never read, so every value including a misspelt one produced byte-identical output. They now filter, unknown values are rejected, and the summary and exit code are derived from what survives. Filtering to empty reports the exclusion rather than a clean scan, which is the same false verdict reached from a different direction. SARIF locations. Multi-project runs flattened every project into one synthetic result whose manifest was the string "multiple", so every alert pointed at a path that does not exist. Results now carry their own manifest, relative to the scan root and declared through SRCROOT. Also: output is deterministic across all five formats (map iteration and a non-total sort were shuffling it), emoji are replaced by the ASCII markers the section headers already use, and a runtime error no longer buries itself under the flag list.
The header claimed 2024-2025 on every Go file and 2024 in LICENSE. The repo's first commit is 2025-12-26 and it has been edited through 2026, so the start year predated the work and the end year was stale. A notice covers the years a work was authored, not the year the organisation was founded. Also standardises the entity string on "CyberSecurity NonProfit (CSNP)", which is the spelling already used in the sibling tool repos and their LICENSE files.
An adversarial review of the previous two commits found that the false-clean fix reached one of five output formats, and that the new skip handling had turned every polyglot repository into an analysis error. Both are the defect class the branch set out to remove, reintroduced through a different door. A repository containing an unsupported manifest type always exited 2. Discovery recognised Cargo.toml, Gemfile, composer.json and the Gradle files, but no parser exists for any of them, so each became a reported skip, and a skip forces exit 2. A tree with a go.mod beside a Cargo.toml reported an analysis error instead of the exit 1 its two real quantum-vulnerable findings had earned. SupportedManifests has never listed those names, so the tool was erroring on files it never claimed to read. Discovery is now driven by which names actually have a parser. A filtered scan reported clean everywhere except the table. Markdown still printed the exact sentence the table fix removed, SARIF asserted executionSuccessful over an empty result set, and CBOM emitted no components and said nothing, so a consumer of any of them read a clean bill of health from a scan that had withheld every finding. All five formats now classify through one shared function in pkg/output/verdict.go, so a case cannot reach one format and miss the others. SARIF records coverage as toolExecutionNotifications and CBOM as metadata.properties; both still validate against their published schemas. AggregateResults summed nine summary fields and not FilteredOut, so a workspace scan reported no withheld findings in the totals a reader actually looks at while the per-project summaries reported dozens. --min-severity discarded findings whose severity was not upper case. The rank map is keyed by the upper-case constants and a Go map returns zero for an absent key, so an unrecognised severity ranked as INFO and any higher threshold dropped it without counting it. Records arrive from a remote feed with no normalisation, so a record carrying "critical" was discarded by the very filter a user reaches for to see critical findings. Ranking is now case-insensitive and an unrankable severity is reported rather than withheld: it has not been shown to be below the threshold. analyze <manifest-file> emitted SARIF whose SRCROOT base was the manifest itself, so every result resolved to the literal ".", the same unusable-literal defect as the "multiple" path it replaced. The markdown remediation table was the one format still ranging over a map after the determinism work; ten runs of one scan produced ten documents. The GitHub Action read .summary from JSON while workspace discovery is the default and emits .totalSummary, so its published vulnerable-count was always 0. Its SARIF step also treated any non-zero exit as a step failure, skipping the upload for exactly the incomplete scans most worth reporting. Every fix has a regression test confirmed to fail against the previous commit at runtime, alongside guards that fail if the fix is achieved by disabling the check. Zero findings lost or gained against the previous binary on four real trees. All five formats remain deterministic and free of emoji and ANSI. TestDiscoveryOrderIsStable claimed to guard a nondeterminism that never existed and used a fixture that never engaged the workspace layer. Its comment now says what it actually guards, and its fixture declares a workspace. Also corrects the README package count, which claimed 1,100+ where the shipped database holds 901, and formats the tree with gofmt.
A second adversarial pass found that the previous commit fixed the reported defects and introduced four more. Recording them plainly, because the shape repeats: each was a correct-sounding fix applied at the wrong layer. SARIF and CBOM did not call classifyNoFindings. The previous commit said they did. They re-derived the conditions inline, over the whole run rather than per project, and without checking whether the project had findings at all. Two consequences, in opposite directions. A --deep scan that found MD5 and SHA-1 in packages absent from the database emitted both findings AND a notice saying no conclusion about cryptographic usage could be drawn, so the document contradicted itself. A workspace with one analysed project and one entirely unexamined emitted no coverage statement at all, because the aggregate no longer satisfied notInDatabase == total, while the table reported it plainly. Coverage is now a coverageNote produced by one function from the shared classification, per project, only for projects with no findings, and both formats render it. Narrowing discovery to parsable names stopped the spurious exit 2 by making those files disappear. A build.gradle declaring bouncycastle vanished from all five formats at exit 0. That is the silent skip this branch exists to remove, and it contradicted the tool's own words in PrintSkipped: a silently skipped manifest is how a scanner reports a clean tree it never read. Unsupported ecosystems are discovered and reported again, marked Unsupported, and only a manifest that should have been readable and was not marks the scan incomplete. The withheld-findings test asserted strings.Contains(out, "9"). The CBOM's random v4 serialNumber and the JSON scanDate satisfy that by accident, so the test passed against an implementation with the CBOM property and the JSON aggregation both disabled, and was flaky besides. It now parses each format and asserts the actual field or sentence. A count was the wrong assertion here in the same way a count is the wrong assertion for a set. The discovery-parser invariant was guarded in one direction only, so flipping pom.xml out of the parsable set disabled all Maven scanning with the suite green. It now checks both directions and that every name SupportedManifests advertises is reachable from discovery. classifyNoFindings documents its ordering as the safety property and nothing tested it; demoting the filter case below the all-unknown case left the suite green while a real --deep scan stopped reporting two withheld findings. Now pinned. The action's SARIF steps could never run: the analysis step ends with exit $EXIT_CODE and its default threshold exits 1 on any vulnerable finding, so a composite step's success() default skipped both. They now carry always(). Verified: all six findings reproduced before fixing and re-checked after. Every new guard confirmed to catch the mutation it exists for, including the ordering demotion and both directions of the parser invariant. Ten SARIF and CBOM documents covering plain, skipped, unsupported, filtered, mixed-workspace and --deep scans validate against the official upstream schemas. Zero findings lost or gained against the previous binary on four real trees. All five formats deterministic, emoji-free and ANSI-free.
Third adversarial pass. It fixed the six from pass two and introduced two more,
in the same shape as both previous rounds: a condition evaluated one layer away
from where it belongs.
Withheld findings vanished from SARIF and CBOM as soon as one finding survived
the filter. FilteredOut was routed through classifyNoFindings, which by contract
only speaks about scans that produced nothing, so a partially filtered scan said
nothing at all: 28 withheld beside 38 reported, and neither machine-readable
format mentioned it. Withholding is a property of the scan, not of an empty
report, so it is now asked before and independently of the no-findings
classification, and attributed per manifest.
An unsupported manifest that was also empty or unreadable forced exit 2 again.
Unsupported was set at the parser-lookup site, but validateManifest runs earlier
in discovery, so an empty Cargo.toml was classified as an unread manifest and
undid the polyglot fix for exactly the trees that fix was written for. Whether a
skip is unsupported is a property of the FILE, so every skip site now goes
through one newSkip constructor that asks getParserForPath, the same function
discovery uses. Asking a parallel predicate would have got requirements/base.txt
wrong, which is parsable only because of its parent directory.
Also from the same pass. A tree whose only manifest is unsupported claimed the
file "could not be read", which is the precise claim IncompleteScan exists to
gate and which was false. Table and markdown had never learned the distinction,
so they counted an unreadable manifest and an unsupported one together and
disagreed numerically with the CBOM for the same run. CBOM coverage notes
carried absolute local paths, publishing the operator's home directory into a
document meant to be shared. Every no-findings note was a SARIF warning, so a
healthy npm workspace produced three warnings saying a package.json declares no
dependencies and none saying findings were withheld.
The action's SARIF steps used always(), which GitHub documents as running even
on cancellation, so a cancelled run would still push a report to the Security
tab. Now !cancelled(), wrapped in ${{ }}: a bare ! is a YAML tag indicator and
the file did not parse.
Tests. The CBOM withheld assertion still passed against an implementation
multiplying the count by 100, and the SARIF one asserted no count at all; both
now pin the exact figure. The per-project test had one note-producing project,
so an implementation returning after the first note passed it; the fixture now
has three and asserts each is attributed. Added the case the whole filtered-scan
fix missed: findings withheld while others survive.
Verified: both HIGH findings reproduced before fixing and re-checked after.
Eleven SARIF and CBOM documents covering plain, skipped, unsupported,
fully-filtered, partially-filtered and all-unknown scans validate against the
official upstream schemas. No absolute path reaches any CBOM. Zero findings lost
or gained against the pre-branch binary on real trees. All five formats
deterministic.
…ip split The fourth adversarial pass over d64a907 found one behavioural defect and six places where a behaviour the branch depends on had no test at all. The defect is the same shape as the three before it. The CBOM stopped publishing the operator's home directory by relativizing manifest paths against the scan root, but it compared an absolutized manifest path against the root exactly as the user typed it. `cryptodeps analyze /abs/path` produced repository-relative paths; `cryptodeps analyze .`, which is the default invocation and the one the GitHub Action runs, found no common prefix and fell through to the absolute path. The fix reached the rendering site instead of the point where the two forms diverge. SARIF had always absolutized the root before comparing, so the same run produced repository-relative SARIF and absolute CBOM. Both normalizations now live in one place, scanRootDir and relativeToRoot in pkg/output/paths.go, which both formatters call. That also gives CBOM the root-is-a-file handling SARIF had, and replaces its HasPrefix(rel, "..") test, which would have rejected a directory legitimately named something like "..config". The guard gaps, each closed and each confirmed to catch the mutation it exists for: deleting the unsupported-ecosystem section from the table and from markdown outright left the whole suite green, which is how a build.gradle full of crypto dependencies could go back to vanishing from the report at exit 0; conflating the unread and unsupported counts in the table and in the CBOM left it green; levelling every coverage note as a warning left it green; and removing the wording branch that stops a healthy Cargo.toml being called unreadable left it green. TestWithheldFindingsAreReportedEvenWhenSomeSurvive asserted strings.Contains(out, "7"), which the scan timestamp satisfies on its own. It passed against an implementation reporting no withheld findings at all, which is the entire defect it was written for. This is the third instance of that assertion shape on this branch. Every withheld assertion is now the named field of the format it belongs to, bound to the real count. Verified: TestCBOMPathsAreRelativeUnderARelativeScanRoot fails against d64a907 at runtime with the new API backported and d64a907's semantics left in place. The other new tests pass there, because they guard behaviour d64a907 already had; each is proven by mutating that behaviour away and watching the test go red. Finding sets are identical to the pre-branch binary at 89c986f on four real trees carrying 51, 16, 9 and 3 findings: zero lost, zero gained. Table output is byte-identical to d64a907 on every fixture, and the CBOM differs only where the scan root is relative. All five formats deterministic, ANSI-free and emoji-free. Fourteen SARIF and CBOM documents, including a real twelve-project tree scanned as ".", validate against the official SARIF 2.1.0 and CycloneDX 1.6 schemas.
The pre-push gate found that every filesystem path reaches the table and markdown reports uninterpolated, and a path is attacker-controlled input. The tool is pointed at repositories it does not trust, and the bundled Action publishes the markdown report, so the tree being judged could write the judgement. Reproduced end to end. A directory named with embedded newlines and the text "## Scan result: CLEAN" put exactly that heading in the markdown report, on its own line, above the corrupt manifest the report exists to disclose. The same name broke the table report into free-standing lines. A directory named "a|b" shifted the Reason column into a third cell of the "Not analyzed" table, because GitHub-flavoured markdown splits a cell on an unescaped pipe even inside a code span. The three machine-readable formats were never affected: encoding/json escapes what it emits. The vector predates this branch, through the markdown project list and the Root Path row, which is why it is fixed here rather than left for the release it would ship in. This branch had added two more interpolation sites to it. Every path and every skip reason in the two human formats now goes through one pair of helpers. A path carrying a control character, a backtick or a pipe is rendered as a Go-quoted string: single-line, unambiguous, and reversible, so the file is still named rather than dropped or truncated. Markdown additionally escapes the backtick and the pipe, which a code span and a table cell need even though the quoting has already made the string safe to print. Verified: the four new tests fail against the previous commit at runtime with reportSafe reduced to the identity function, and the json, cbom and sarif subtests correctly stay green there, because those formats were never vulnerable. Escaping is invisible for ordinary paths, including paths with spaces and Windows separators, so all five formats are byte-identical to 777e1f7 on every fixture.
…changes The changelog stopped at the output-format work and never recorded the two newest fixes: the CBOM publishing absolute local paths for the default invocation, and a scanned repository being able to inject its own headings into the markdown report. The roadmap called v1.2 the current release and listed a v1.3 of four features that this release does not contain, so tagging v1.3.0 against it would have promised work that has not been done. v1.3 now lists what it actually is, which is correctness of what the tool reports rather than new surfaces, and the four feature items moved to v1.4.
…t has no fallback
Two findings from the pre-push gate's adversarial pass, both reproduced before
being acted on.
The path consolidation reached CBOM and SARIF and stopped there. The table kept
getRelativePath, a third implementation of the same comparison, and markdown did
not relativize at all, so the previous commit's claim that the two formats
"cannot disagree again" was true only of those two. Measured on three real trees
scanned as ".", the operator's absolute path appeared 18 times in the table and
18 times in markdown while the CBOM carried none. The markdown report is the one
the bundled Action publishes, so the privacy rationale applied to it most.
getRelativePath was also wrong on its own terms. It tested a string prefix, so
getRelativePath("/repo", "/repository/go.mod") returned "./sitory/go.mod", a path
that does not exist. It now asks relativeToRoot like everything else.
Every surface of one run now names a manifest the same way: one absolute anchor
per document, and every path relative to it. The scan root travels into the
per-project render as an argument rather than on the formatter, matching what
cbom.go already did, so a standalone single-project report still prints the
absolute path, which is the only anchor it has. Table and markdown go from 18
absolute lines to one, their declared root. JSON stays absolute throughout and
self-anchored on rootPath.
The second finding is a test-depth gap of the same family as the assertion this
branch has already fixed twice. assertWithheldSentence accepted any one of four
sentences, and markdown states the withheld count twice in a workspace report, so
either statement could be deleted with the suite green. That is not cosmetic:
the per-project line is the only one that exists in the single-project renderer
that --no-workspaces uses, and it had no test at all. Deleting it made a
--no-workspaces markdown scan read as complete while findings were withheld.
Each format now asserts every statement it is supposed to make, and the
single-project renderer has its own case.
Verified: the two mutations the adversarial pass found surviving are now caught,
as are three new ones covering the relativization in the table and in markdown.
Finding sets unchanged against the pre-branch binary at 89c986f on four real
trees. All five formats deterministic, twelve SARIF and CBOM documents validate
against the official schemas, and the forged-heading fixture still produces no
injected heading in any format.
Known and not addressed: a CBOM now carries no absolute path at all, so a
consumer has nothing to resolve its relative paths against. Declaring the root
would reintroduce exactly what was removed; the CycloneDX answer is a
metadata.component describing the subject, which is its own change.
`cryptodeps status` announced 1731 packages for a database holding 901, and every per-ecosystem number was wrong in the same way: go 28 against 21, maven 121 against 68, npm 1526 against 773, pypi 56 against 39. addToIndex deliberately files every package under two keys, "name@version" and "name", so that a lookup succeeds whether or not the caller has a version. Stats reported the length of that index, so every versioned package counted twice. The two keys collapse into one for a package with no version, which is why the inflation was not a clean doubling and why 1731 was plausible enough to survive until someone compared it with the file. Verified against two independent sources: the shipped database's own stats block (901; go 21, maven 68, npm 773, pypi 39) and a direct count of its records (901). The status command now agrees with both. The ecosystem list was also printed straight out of a map, so it came out in a different order on almost every run. That is the nondeterminism the five output formats were already fixed for, on a surface the fix had missed.
…y like any other A sixth adversarial pass found that the previous two commits, both written to fix layer errors, each contained one. The injection fix escaped characters instead of closing the context. It handled control characters, backticks and pipes, which is the right set for the inside of a code span, and then interpolated the result into a bare `##` heading and a bare table cell, where every markdown construct is live. Verified against GitHub's own renderer by the reviewer: a directory named `**CLEAN**` rendered as bold in the report heading, one named `[no findings](https://...)` rendered as a link, and an `<img src=...>` survived the sanitizer as a beacon that fires when the report is viewed. Every untrusted string in the markdown report now sits inside a code span, which leaves exactly two active characters for markdownSafe to handle. The escape set also grew: U+2028 and U+2029 are line breaks to a renderer and injected lines just as \n did, the bidi overrides reverse the visible order of a filename so the report displays a file it is not talking about, and the zero-width characters make two different paths render identically. The path consolidation missed the most common repository shape. table.go's FormatMulti short-circuits a one-project workspace straight to the single-project renderer, which takes no scan root, so a repository with exactly one manifest printed an absolute path in the table while markdown printed a relative one for the same run, and the skip list printed above it in the same document was relative too. Measured on a real single-manifest tree before and after: the table now says ./package.json where it said the operator's absolute path. Two claims added to the README last night were false, which is the defect this release is about. "Every output format reports the tool version" is true of the three machine-readable formats only; the table and markdown mention no version at all. "Byte-identical output across runs of the same scan, in all five formats" is true of three: the JSON scanDate and the CBOM serialNumber necessarily differ, the latter by a deliberate change on this branch. Both corrected to what is actually true, and the changelog now records the table and markdown relativization and the breaking change to the exported PrintSkipped. Six mutations the reviewer found surviving are now caught, along with three more covering the markdown contexts. The Stats doc comment said it counts distinct packages when it counts distinct records, which is the right identity for agreeing with the database's own stats block but not what the comment claimed; the database holds two records each for PGPy and PyNaCl. cmd/gendb still printed its ecosystem summary straight out of a map, the same nondeterminism the status command was fixed for. Verified: finding sets identical to the pre-branch binary at 89c986f on four real trees carrying 51, 16, 9 and 3 findings, zero lost and zero gained; ordering stable across runs in all five formats, on output confirmed non-empty first; twelve SARIF and CBOM documents validate against the official schemas; the forged-heading fixture still produces no injected heading; suite green under -race.
The seventh adversarial pass found that the report-forgery fix covered one of the two channels into the same document, and that the test certifying it killed almost nothing. A dependency name and version come from the manifest under scan, so they are attacker-controlled in exactly the way a path is, and they are a strictly easier channel: no filesystem write, no directory named with embedded newlines, just an entry in a package.json. The database lookup falls back from "name@version" to the name alone, so a real package with a version of "1.3.1\n\n## Scan result: CLEAN" still resolved, reached the findings table, and put that heading in the markdown report seven times, between the rows of findings that contradict it. Present at 89c986f as well, so this is not a regression repair, but the changelog and the tests on this branch claimed the vector was closed when it was closed for paths only. Both formats now render the dependency label through the same code spans every path uses, built once in dependencyLabel rather than assembled separately in each. The escaping is now split by context, which is what the previous commit's own reasoning argued for and did not do. markdownCode escapes the backtick, which is all that is active inside a code span. markdownCell adds the pipe escape, which is a GitHub-flavoured markdown table rule and is consumed only in a table row: applied to a bullet or a heading the backslash rendered literally, so the report displayed a name the filesystem does not have and the quoted form no longer round-tripped through strconv.Unquote. needsEscaping now asks strconv.IsPrint instead of enumerating the non-ASCII controls by hand, which the reviewer showed had missed U+061C, U+2060, U+180E, the tag block and U+00A0, all members of the classes the enumeration named. TestMarkdownRendersUntrustedPathsInertly selected lines by the code span it was supposed to be testing for, so removing a span removed the line from the sample and the assertion never ran: five of its six mutations survived. It now finds the hostile bytes wherever they land and requires them to be inside a span, and it fails if the fixture reaches fewer than all six contexts. TestMarkdownTableCells SurviveAPipeInAPath asserted a cell count, which broke as soon as a four-column table also gained a code span and would not have caught a pipe in the fourth column; it now asserts the invariant, that no code span in a table row carries an unescaped pipe. The single-project table report had lost its only absolute path when the manifest became relative, leaving a reader nothing to resolve it against while the changelog claimed one anchor per document. It prints the scan root again. Two more claims corrected: the changelog said all five formats are byte-identical across runs, where the JSON scan timestamp and the CBOM serial number necessarily differ, and the escaping paragraph described paths only. Verified: eleven mutations caught, including the two for the dependency channel and the five markdown contexts the reviewer proved unguarded. Zero forged headings in all five formats from both the path and the dependency channel. Finding sets identical to the pre-branch binary at 89c986f on four real trees carrying 51, 16, 9 and 3 findings, on outputs confirmed non-empty. Fourteen SARIF and CBOM documents validate against the official schemas. Ordering stable across runs in all five formats. Suite green under -race.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this fixes
The v1.3.0 release test failed on five blockers. All five are fixed here, along
with everything seven review rounds found on top of them.
A scan could report a clean result for input it never read. An unparseable
package.jsonwas dropped during discovery, before any parser ran, so a treeholding a good and a corrupt manifest scanned only the good one and printed a
clean summary at exit 0. A manifest broken by a bad merge became invisible and CI
went green. Unreadable manifests are now named, with the reason, in every format,
and they exit 2.
A scan that examined nothing said so as though it had looked. A
requirements.txtwhose every dependency was absent from the database printed"No cryptographic usage detected" while the JSON for the same scan reported three
packages not in the database. The three cases (nothing declared, nothing
examined, examined and clean) are now worded differently, through one classifier
that every formatter calls.
--riskand--min-severitydid nothing. Both were stored and never read,so every value, including a misspelt one, produced byte-identical output. They
filter now, they validate their argument, and a filtered report says how much it
withheld rather than reading as complete.
Three surfaces reported the wrong tool version. One binary gave four answers:
versionsaid 1.3.0, SARIF and CBOM both claimed 1.0.0, and JSON carried none.A new
pkg/versionis the single source of truth, fed from the values GoReleaseralready injects.
Every SARIF result pointed at a literal path
"multiple". Every alert landedon a file that does not exist. Results now carry their own project's manifest,
relative to the scan root and declared through
originalUriBaseIds.Also: emoji removed from the table output in favour of the ASCII markers the
section headers already use, output ordering made stable across runs in all five
formats, and runtime errors no longer print the thirty-line flag list on top of
the message that explains the failure.
What the review rounds added
Seven adversarial passes ran over this branch. Each of the first six found real
defects in the commit before it, and the pattern was the same every time: a
correct-sounding fix applied one layer, or one context, away from where the
decision is actually made. The ones worth knowing about as a reviewer:
A scanned repository could write its own lines into the report. Every
filesystem path reached the table and markdown formats uninterpolated, and the
bundled Action publishes the markdown. A directory named with embedded newlines
and
## Scan result: CLEANput that heading in the report, above the corruptmanifest the report exists to disclose. The first fix escaped control
characters, backticks and pipes, which is the right set for the inside of a
code span, and then interpolated into a bare
##heading where all markup islive, so
**CLEAN**and[no findings](https://...)still rendered. Everyuntrusted string now sits inside a code span, and the escape set covers the
Unicode line separators, the bidi overrides and the zero-width characters as
well as ASCII controls. The three machine-readable formats were never affected.
A polyglot repository exited 2. The first silent-skip fix made any skip an
analysis error, so a
Cargo.tomlbeside ago.modmasked the exit 1 that tworeal findings had earned. Narrowing discovery to fix that made a
build.gradlefull of crypto dependencies vanish from all five formats at exit 0, which is
the silent skip this branch exists to remove. The answer was a distinction the
data model did not have: unreadable versus unsupported. Only the first means
the scan is incomplete, and only the first exits 2.
A filtered or incomplete scan read as clean in the formats nobody was
looking at. Fixes for the verdict reached the table while markdown, SARIF and
CBOM kept reporting a clean bill of health, three separate times, in three
different shapes. Coverage is now judged once, per project, and every formatter
renders that judgement.
The same run named a manifest differently in different formats. Only CBOM
and SARIF expressed paths relative to the scan root; the table compared the
root as typed against absolutized paths with a string prefix, and markdown did
not relativize at all. Every surface now agrees, with one absolute anchor per
document.
cryptodeps statusreported 1731 packages for a database of 901. The indexfiles each package under two keys so a lookup succeeds with or without a
version, and the count was of index entries.
Founder decisions recorded here
--no-colorandNO_COLORare deliberately not added. The tool emits noANSI at all (reconfirmed at gate time: zero escape bytes in all five formats at
runtime), so once the emoji were gone the flag would control nothing. Shipping
an inert flag is the defect class this branch fixes. One line to add if
cryptodeps ever gains colour, in the same change that adds it.
Copyright 2025-2026 CyberSecurity NonProfit (CSNP), corrected from2024-2025on 55 files; the repository's first commitis 2025-12-26.
statuscount fix is folded in rather than deferred, because a wrongheadline number does not belong in a release about outputs telling the truth.
Breaking change
output.PrintSkippedtakes the scan root as its second argument, so it can namea manifest the same way the rest of the report does. It is exported from an
importable package.
Verification
89c986fon four realtrees carrying 51, 16, 9 and 3 findings: zero lost, zero gained.
with the new API backported into a detached worktree where the old semantics
needed preserving. Tests that pass against the old code are labelled as
preservation guards rather than counted as regression tests, and each is proven
by mutating the behaviour away and watching it go red.
first.
CycloneDX 1.6 schemas, resolved with their
jsf-0.82andspdxreferences,using
jsonschema4.26.0 rather than the tool's own tests.go test -race,go vetandgofmtclean.Deferred, with evidence, in roadmap unit 137
SARIF results carry no
regionand JSONlocation.file/lineare still empty,which needs declaration line numbers from each parser.
performReachabilityAnalysiswrites through a pointer into the shared database. The table's unlabeled effort
column reads as severity and inverts it. The CBOM now carries no absolute anchor,
so a consumer cannot resolve its relative paths; the CycloneDX answer is a
metadata.componentdescribing the subject. And the shipped database attributesRSA, ECDSA, Ed25519 and X25519 to
@noble/post-quantumat high confidence, whichis database curation rather than code and needs a verified source before anyone
edits it.