Skip to content

feat(scan): per-finding evidence (matched text) — phase 2 of #80#81

Merged
askalf merged 2 commits into
masterfrom
feature/scan-evidence
Jul 21, 2026
Merged

feat(scan): per-finding evidence (matched text) — phase 2 of #80#81
askalf merged 2 commits into
masterfrom
feature/scan-evidence

Conversation

@askalf

@askalf askalf commented Jul 21, 2026

Copy link
Copy Markdown
Owner

Phase 2 of #80. Bumps redstamp pin to cf2ce75 (phase-1 merged: detectors return hits[]). scanSkill passes redstamp findings through, so hits:[{flag,match}] now appear in scan --json — the exact substring each flag matched (e.g. instruction-override → "ignore previous instructions"). Additive, verdicts unchanged. 150/150 + evidence-flow test.

Next (phase 3): marketplace-watch.mjs publishes evidence[] into advisoryDetail/acceptedDetail/flagged + re-reads the pinned bytes to verify each match exists at file:line (drops unverifiable + counts evidenceMismatches — the confabulation guard). Then the site renders it.

🤖 Generated with Claude Code

…hits

Phase 2 of #80. Bumps redstamp pin to cf2ce75 (phase-1 merge: detectors now
return hits[]). scanSkill already passes redstamp finding objects through, so
'hits':[{flag,match}] now flow into 'scan --json' — the exact substring each
flag matched. Additive; verdicts unchanged. Suite 150/150 + evidence-flow test.

Next: watch publishes evidence[] into results.json + re-reads pinned bytes to
verify each match (drops unverifiable, counts evidenceMismatches — confab guard).
@askalf
askalf enabled auto-merge (squash) July 21, 2026 02:58
Phase 3 of #80. support/evidence.mjs: locate each redstamp hit's matched substring
in the pinned source pieces, returning {file,line} — and DROP any that don't exist
in the bytes (counted as evidenceMismatches). marketplace-watch attaches
evidence:[{flag,text,file,line}] to advisoryDetail/acceptedDetail/flagged and
publishes evidenceMismatches in the summary.

The self-check IS the anti-confabulation guard: the feed (and site) can only show a
fragment that provably exists at the linked line; a nonzero mismatch count means the
detector claimed a match the source lacks — surfaced, not hidden.

Tests: evidence module (locate/self-check/cap) + integration (scan→evidence→true
line). Watch test stages evidence.mjs. Full suite 155/155. Manual run verified:
results.json carries evidence @ real file:line, evidenceMismatches 0.
Comment thread support/evidence.test.mjs

@sprayberry-reviewer sprayberry-reviewer left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated review from the Sprayberry Labs fleet code reviewer.

Verdict: No blocking findings — approving. Clean, well-guarded change with a genuine confabulation safeguard; one non-blocking nit noted below.

What I checked

  • gh pr diff 81 (full unified diff, all 7 changed/added files)
  • support/evidence.mjs + support/marketplace-watch.mjs against surrounding context in a shallow clone (src/skill.mjs, src/scan.mjs, src/index.mjs, src/claude.mjs) to confirm where scanPieces/hits/match actually originate
  • gh pr checks 81 — all 9 checks pass (CodeQL, analyze, test × 6 platforms, verify pinned skills)

Security review (the point of this PR — evidence text leaking secrets)
Traced the data flow: evidenceOf(items, skill) in support/evidence.mjs:27-38 only ever publishes h.match after locate() (support/evidence.mjs:20-25) confirms that exact substring exists verbatim in skill.scanPieces[].text — which is the static file content of the scanned marketplace skill itself (src/skill.mjs:108-114, read via fs.readFileSync), never process.env or any runtime secret value. So a hit like ${API_KEY} in the test (support/evidence.test.mjs:17) is the literal string as it appears in the skill's own SKILL.md/description — evidence of a mention, not an expanded secret. Nothing in this diff reads or forwards actual environment/credential values. An unverifiable hit (redstamp claims a match the bytes don't contain) is dropped and counted, not published (support/evidence.mjs:33-36) — this is a solid anti-confabulation guard and matches the PR's stated intent.

Correctness
No issues found in the changed logic. locate() correctly computes 1-based line numbers via a leading-newline count, empty/non-string matches are skipped before lookup (evidence.mjs:31), and the EVIDENCE_CAP truncation is tested for both the capped and uncapped paths.

Minor (non-blocking)

  • support/evidence.mjs:12cap() truncates with s.slice(0, EVIDENCE_CAP - 1), a plain UTF-16 code-unit slice. If a matched string's 160-code-unit boundary lands inside a surrogate pair (e.g. an emoji or other astral character in the matched text), the truncated output would contain a lone surrogate before the . Low likelihood given typical injection-prompt text, and not something the tests exercise — flagging only as a nit, not a blocker.

What's good

  • The verify-before-publish design (locate → drop-if-unverifiable → count as mismatches → surfaced in the published summary.evidenceMismatches) is exactly the right shape for a scanner that must not let a detector's claim become an unverified public assertion.
  • New tests (support/evidence.test.mjs, evidence-flow.test.mjs) cover the verified-hit path, the dropped/mismatched path, the length cap, and an end-to-end real-skill-dir integration case that checks the actual line number — solid coverage for new behavior.
  • CI is fully green across all platforms plus CodeQL.

@askalf
askalf merged commit 3cb3d04 into master Jul 21, 2026
9 checks passed
@askalf
askalf deleted the feature/scan-evidence branch July 21, 2026 10:51
askalf added a commit that referenced this pull request Jul 21, 2026
…solve (#82)

Phase 2/3 (#81) published evidence.file relative to the skill's own
directory (what scanPieces uses), e.g. bare "SKILL.md" — but the site
resolves a per-plugin source link from the plugin's REPO root, and a
skill is usually nested under it (skills/<name>/…). A bare skill-
relative file can't be appended to that link to build a working
github.com/.../blob/<sha>/<file>#L<line> deep link.

Thread through skillPath (the skill's own dir relative to the repo
root discoverMarketplaceSkills resolved it under) and join it onto
each evidence entry's file before publishing, in both corpus and
legacy scan modes.

Closes #80 (phase 4's actual requirement — the tree/ -> blob/#Lline
swap — needs this on the data side; site-side render in a
truecopy-site PR).
askalf added a commit that referenced this pull request Jul 21, 2026
#83)

#81 and #82 shipped user-facing behavior with no changelog entry:
`scan --json` findings/advisories now carry `evidence[]`, and the watch
publishes `evidenceMismatches` plus repo-relative evidence paths. Both
belong under Unreleased so the next release notes are complete.

Documentation only — no code or behavior change.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants