Repository navigation
feat(scan): classify snippet matches as false positives (opt-in) - #98
Conversation
7267c59 to
345084e
Compare
Integrate github.com/scanoss/snippets-classifier to score each snippet
match on whether its reported OSS and local line ranges correspond. The
classifier is offline, but scoring needs the matched OSS file, which is
not local — so a new pipeline stage fetches it and annotates the result.
- pkg/scanoss: Contents.File fetches raw source by MD5 via
GET /v3/file-contents/{md5} (whole file or a line range).
- pkg/snippetmatch: an Annotator that reads each local file, fetches
every distinct OSS file once (cache by hash, bounded concurrency),
classifies, and sets FileEvidence.SnippetClassification. Non-fatal:
a match it cannot score is left unannotated.
- sbom: SnippetClassification {verdict, probability, model_version} on
FileEvidence, rendered in the raw output.
- scanpipeline: opt-in ClassifySnippets stage after enrichment, with a
"classify" progress layer.
- cmd: --classify-snippets flag (path scan, raw format only).
The classifier only proposes (threshold 0.7); it removes nothing.
snippets-classifier is a public, MIT licensed module (v0.2.0), so the
build needs no GOPRIVATE or token.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
345084e to
46abd2c
Compare
|
Ran this branch (46abd2c) end to end against api.scanoss.com and validated the model on code outside the training corpus. Summary below. End to end
Validation on application repos8 public repos, none in the corpus: go-shiori/shiori, gotify/server, Klipper3d/klipper, louislam/uptime-kuma, macrozheng/mall, mealie-recipes/mealie, sherlock-project/sherlock, TeamNewPipe/NewPipe. They produced 876 snippet matches, 70 of them at P >= 0.7. I reviewed a blind, stratified sample of 109 against the README criteria: all 39 in 0.1-0.7, 40 of the 70 at >= 0.7, and 30 of the 767 below 0.1. The figures are reweighted to the 876 matches. 13 cases came out
Recall holds up. Precision is well below the corpus figure (0.95 at 0.7): about 3 in 10 flagged matches are real. Where it goes wrong
What this means for the rollout
On top of this PRI'll open a stacked PR on this branch, so this one stays as it is. It adds:
|
…mmary (#129) * fix(scanoss): the file-contents id is the scan's file_hash, not an MD5 /v3/file-contents/{id} takes whatever file_hash the scan reported for the matched OSS file, today a 16-hex CRC64. The comments and parameter names said MD5. * feat(sbom): remove false-positive snippets; carry the verdict in CycloneDX Inventory.RemoveFalsePositiveSnippets drops the snippet matches the classifier proposed as false positives and any component left with no evidence. It never touches file matches, declared or transitive evidence, unscored or real snippets, or a match a bom.identify rule claimed, so the dependency graph keeps every component it names. An advisory loses the PURLs that named only removed components, and goes when none is left, so CycloneDX never emits a vulnerability that affects nothing. CycloneDX occurrences have no properties, so a scored snippet appends 'classifier <verdict> p=<0.00> model=<version>' to additionalContext, after the ranges. * feat(scan): --classify-threshold, --classify-filter and a classifier summary --classify-snippets now works with every output format. --classify-threshold (default 0.7, strictly between 0 and 1) and --classify-filter need --classify-snippets, and a bad value is refused before any upload. Classification runs before enrichment, so a component the filter removes is never sent to the decoration services. After the scan, stderr gets one line saying how many snippet matches were scored, proposed as false positives, removed and left unscored, and when the API refused file contents with 401/403, so a run that scored nothing no longer looks like a run that found nothing. Docs: CHANGELOG (covering --classify-snippets too), CLIENT_HELP and README.
What
Integrates
github.com/scanoss/snippets-classifierto score each snippet match on whether its reported OSS and local line ranges correspond — proposing wrong candidate assignments (false positives) for a human to confirm.The classifier itself is offline and embedded, but scoring a match needs three inputs: the local file, the matched OSS file, and the paired line ranges. The inventory carries the ranges and the OSS file's hash; the local file is on disk; the OSS file is fetched by hash. That fetch is the only network cost, so distinct OSS files are downloaded once and reused.
It only proposes (threshold 0.7) — it removes nothing.
Changes
pkg/scanoss/filecontents.go—Contents.File(ctx, md5, start, end)→GET /v3/file-contents/{md5}(whole file or a line range).pkg/snippetmatch/—Annotator: reads each local file, fetches every distinct OSS file once (cache by hash, bounded concurrency), classifies in parallel, setsFileEvidence.SnippetClassification. Non-fatal: a match it cannot score (no local file / no OSS content) is left unannotated and logged.pkg/sbom—SnippetClassification {verdict, probability, model_version}onFileEvidence, rendered inrawoutput.pkg/scanpipeline— opt-inClassifySnippetsstage after enrichment; newclassifyprogress layer.cmd/scan.go—--classify-snippetsflag (path scan only,rawformat only; warns if used with another format).Usage
Each snippet match in the raw output gains:
Dependency
snippets-classifieris a public, MIT licensed module (v0.2.0, modellr-d51f2e100211). The build needs noGOPRIVATEor token.Testing
go build ./...,go vet ./...,gofmtclean; fullgo test ./...and-raceon the concurrent packages all pass.snippetmatch(real vs false-positive verdicts with the classifier's own fixtures, dedup-by-hash, non-fatal skips, non-snippet evidence ignored) andContents.File(whole-file, line range, 404).main(2026-10-26); build,gofmtand the tests ofcmd,sbom,scanoss,scanpipelineandsnippetmatchpass again.🤖 Generated with Claude Code