Commit graph

186 commits

Author SHA1 Message Date
0e2601ff55 chore(llm-security): STATE — v8 planning unparked; strategy docs moved to *.local.md
Operator decision 2026-07-18: v8 strategy docs are LOCAL-ONLY. All 4
(FAMILY-MAP, JOBS-TO-BE-DONE, commons-extraction-plan, roadmap) renamed
to docs/*.local.md (gitignored); roadmap.md had been hidden only by the
accidental case-insensitive ROADMAP.md ignore match.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 11:17:27 +02:00
887691490d chore(llm-security): STATE — v7.8.3 shipped; next = v8.0.0 planning (parked)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 11:03:32 +02:00
32199965f2 fix(llm-security): post-mcp-verify reads live PostToolUse tool_response field
The PostToolUse hook read input.tool_output, but live Claude Code delivers the tool result as tool_response — so the indirect-injection scan on MCP tool output silently never fired outside the test harness (which sent tool_output). The hook now reads tool_response with a tool_output fallback for older harnesses/fixtures. +3 tests covering string and object tool_response and the precedence. Found via a live-session check during the v7.8.3 sweep; not one of the 52 MEDIUM-tier findings. Suite 2016/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 11:00:34 +02:00
d2648931ef chore(llm-security): STATE — v7.8.3 code-complete locally, held for release decision
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:54:03 +02:00
4cffa30725 chore(llm-security): v7.8.3 — docs consistency + version bump (#8,#44,#45,#46,#47)
Bumps plugin to 7.8.3 (package.json, plugin.json, README version badge) and syncs the documentation surfaces that had drifted: orchestrated scanner count 9/10/13 to 14 (#8; the WFL/workflow scanner was omitted from the reference list), posture categories 13 to 16 (#44), red-team scenarios 64 to 72 (#45), tests badge/prose 1822 to 2013 (#46). Corrects the pathguard hook-table matcher and header comment to Edit|Write (follows the #9 fix) and removes the two dangling ROADMAP.md references (#47; the roadmap is not a public file). Adds the v7.8.3 CHANGELOG entry, README Recent-versions row, and CLAUDE.md highlights. The '23 scanners' total is a separate module count and is left unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:50:50 +02:00
14070160ba test(llm-security): cover TRG/SIG/AST in e2e pipeline + SIG miner/hacktool/rot13 families (#58,#59)
#58 the e2e scan-pipeline suite omitted trg/sig/ast from EXPECTED_SCANNERS and no fixture carried trigger-abuse/known-signature/python-taint content, so the three v7.8.0 scanners' surfacing through the aggregate was never asserted; added a fixture (shadowing command name → TRG, webshell → SIG, tainted os.environ→requests.post → AST, python3-guarded) that asserts each appears in the envelope and the rolled-up counts/owasp_breakdown. #59 the SIG cryptominer (SIG-MINER-001/002) and hacktool (SIG-HACKTOOL-001) families and non-base64 decode variants had no test; added family-attribution tests and a rot13-decode assertion. Test-only, suite 2013/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:46:19 +02:00
66e8ce2740 fix(llm-security): misc scanner correctness — reflog FP, diff exact-pass, hex dedupe, SARIF version (#20,#22,#50,#54,#56)
#20 git-forensics' reflog force-push detector had a redundant bare 'reset' term subsuming 'reset:', so any commit subject containing 'reset' tripped it; removed. #22 diff-engine's per-current moved-fallback greedily consumed a baseline candidate a later byte-exact match needed (mislabeling unchanged as new/moved on duplicate fingerprints); a global exact pass now runs before the moved-fallback. #54 memory-poisoning double-reported a 64+ char hex token as both base64 and hex; the base64 check now skips pure-hex tokens.

#56 SARIF output hardcoded driver.version 6.0.0 because the orchestrator called toSARIF() without a version; it now passes the real plugin version from package.json. #50 the VS Code known-malicious blocklist was empty with no explanation (unlike the JetBrains file's 'empty by design' note); added a matching blocklist_note. Suite 2004/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:36:51 +02:00
8a59d616fb fix(llm-security): hook coverage — pathguard on Edit, trifecta window, pipe-to-shell, provider keys (#9,#10,#12,#13,#11-doc)
#9 pathguard registered matcher Write only, so an Edit/MultiEdit to an existing protected file (settings.json, .env, .ssh, the hooks themselves) bypassed it entirely; matcher now Edit|Write (the script already reads only tool_input.file_path). #10 the primary trifecta detector's 20-entry window counted marker lines, so accumulated markers scrolled a real leg out (false negative); the window now counts tool-call entries. #13 pre-edit-secrets caught bare provider keys only inside a quoted label assignment; added anchored patterns for Anthropic sk-ant, OpenAI sk-proj, fine-grained github_pat_, Google AIza, and JWT eyJ (minimum-length guarded against prose false positives).

#12 the remote-pipe-to-shell block required a shell immediately after the first pipe, so xargs/sudo/tee/env interposition evaded it and the comment falsely claimed xargs was caught; broadened to reach a shell through intermediate segments while leaving shell-OR fallbacks unblocked. #11 (doc only): knowledge/owasp-skills-top10.md claimed pre-bash-destructive blocks persistence commands — it does not; corrected to mark persistence detection as unimplemented/future (the detector itself is deferred to v8). Suite 2004/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:36:39 +02:00
21c6c2b534 fix(llm-security): normalization/discovery evasion + SIG embedded-base64 & custom rules (#21,#23,#30,#36,#42,#52,#55)
#21 the bash normalizer decoded only \xHH, leaving ANSI-C octal/\u/\U forms literal so canonical rm/curl never surfaced; now decodes all three. #23 file-discovery keyed on extname so .env.local/.env.example (extname .local/.example) were silently skipped; now matches multi-part suffixes. #42 a legitimate leading UTF-8 BOM was flagged HIGH (and the tool's own auto-cleaner refused to strip it); pos-0 BOM now excepted. #52 collapseLetterSpacing used a literal space, letting multi-space/tab spacing evade; now [ \t]+. #55 redact(_,60,0) did slice(-0) and leaked the whole unredacted URL; showEnd===0 now means no tail.

#30 embedded base64 (const x = "<base64>") never satisfied the whole-string decode, so the SIG identity engine never saw it; added decodeEmbeddedBase64 as an OPT-IN param on normalizeForScan (default off — appending a decoded copy would double-count per-match findings, e.g. content-extractor's injection scan) and enabled it only in signature-scanner, which dedups variants. #36 signature-scanner ignored the documented sig.custom_rules_path policy option; now loads+merges custom rules through the same family filter. Suite 2004/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:35:56 +02:00
b224e18b42 fix(llm-security): YAML/workflow parser divergence — block scalars + bare if: (#32,#33,#43)
#33 the frontmatter parser collected a block-scalar body (description: |) but did not skip it, so an indented name:/allowed_tools: inside the body re-matched as a top-level key and overrode the real values TRG-shadow and permission checks depend on; the parser now consumes block-scalar bodies as opaque content. #32 block-scalar headers carrying indentation/chomping indicators (|2, >-, |-2) were not recognized, so their bodies never reached the run: injection sink; now matched via a proper indicator/chomping regex.

#43 the B4 actor auth-bypass detector inspected only braced ${{ }} expressions, missing the canonical bare 'if: github.actor == ...' form (Synacktiv Dependabot-spoof false negative); bare if: expressions now emit a synthetic event the detector reads. Suite 2004/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:35:56 +02:00
207385fbbe fix(llm-security): scanner robustness — ReDoS, MCP-stdout DoS, redirect loop, atomic writes (#24,#53,#31,#25,#51)
#24 the HTML-obfuscation injection patterns had overlapping unbounded runs plus a required closing quote, so a non-closing input backtracked O(N^2) (~28.7s at the 512KB cap); quantifiers bounded, pathological input now 4ms. #53 mcp-live-inspect buffered MCP-server stdout via readline with no cap, so a hostile stdio server could exhaust memory / throw an uncaught RangeError; replaced with manual line buffering capped at 4MB that rejects pending RPCs and destroys stdout. #31 vsix-fetch's same-host redirect follower had no depth cap (loop hang); added depth>=5 cap mirroring the sibling fetcher.

#25/#51 mcp-description-cache and skill-registry wrote JSON via bare writeFileSync (non-atomic: concurrent load-modify-save loses updates, a torn read silently yields an empty registry); both now write a temp file then renameSync. Suite 1931/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:15:11 +02:00
f3aaf5479f fix(llm-security): scanner false-positives/negatives — trigger, toxic-flow, policy-loader (#38-#41,#57,#26)
#38 toxic-flow matched trifecta-leg keywords with bare includes(), so substrings ('url' in 'curl', 'key' in 'monkey', 'auth' in 'author') fabricated CRITICAL trifectas on benign components; now word-boundary matched. #40 TRG-broad fired HIGH on a bare any/all/every anywhere ('fix any lint errors'); the universal-claim regex now requires genuine universal phrasing. #41 TRG-baiting substring-matched ('any file' in 'many files'); now boundary-anchored. #39 the broad-name list missed multi-char generic names (helper/assistant/auto/general/agent/tool); widened coherently so it does not reintroduce #40. #57 the '(recovered from obfuscation)' label compared raw against a lowercased normal form, firing on any uppercase char; now gated on an explicit decode-changed flag.

#26 (same file) getPolicyValue used 'key in sectionObj' with no type guard, so a scalar section override in policy.json (e.g. {"injection":"block"}) threw an uncaught TypeError; now guarded to fall back to the default. Suite 1931/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:15:10 +02:00
196517f38a fix(llm-security): supply-chain gate bypasses — npm/yarn blocklist + pip-audit (#14-#19,#48)
#18 dep-auditor called 'pip audit' (no such subcommand) so Python CVE detection was a permanent silent no-op; now spawns 'pip-audit' with an argv array. #19 it audited the scanner HOST's env with results mislabelled to the target's requirements.txt; now audits the target via -r and skips cleanly when absent.

#14 the offline npm blocklist was skipped for bare/range/tag installs because it used the (null) parsed-spec version; now re-checks the resolved version before the OSV network path. #15 non-hoisted nested lockfile keys derived the wrong package name (leading-only node_modules/ strip); now strips to the last segment. #48 lockfileVersion-1 nested dependency trees are now walked recursively.

#16/#17 the yarn.lock matcher paired two unassociated whole-file substrings with an unanchored pkg@ (false BLOCK of a legit package) and only matched Yarn Classic quoted versions (Berry known-malware allowed); rewritten as a per-entry parser that associates version to its own entry, anchors the name, and matches both Classic and Berry. Suite 1931/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:14:51 +02:00
76f190cf78 fix(llm-security): AST-taint clears taint on reassignment (#29) + sink test coverage (#28)
py-ast-taint.py Assign handler never removed a name from the tainted set on reassignment, so a source-then-constant/sanitizer rebind (g=os.getenv(); g='safe'; os.system(g), or x=shlex.quote(x)) kept stale taint and fired false AST-CMD-EXEC findings. Assign now clears taint for target names when the RHS is not a source. No cross-expression propagation added — the f-string/concat/alias recall gap (#27) stays deferred to v8.

#28: added fixtures+assertions locking the tainted subprocess/os.system AST-CMD-EXEC sink and the open(...,'w') AST-FILE-WRITE sink. Suite 1931/0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 10:14:51 +02:00
629a5e6e8c chore(llm-security): STATE — v7.8.2 shipped; next = the 52 MEDIUM
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:35:21 +02:00
f4c65070ff chore(llm-security): v7.8.2 — security patch release
Bumps package.json, .claude-plugin/plugin.json and the README badge to
7.8.2; adds the release entry to CHANGELOG.md, docs/version-history.md,
the README recent-versions table and the CLAUDE.md highlights block.

Also fixes test pollution introduced with the ide-extension regression
suite: its temp roots used an `llmsec-jb-plugin-` prefix, and
jetbrains-parser.test.mjs asserts globally that no `llmsec-jb-*`
directory survives anywhere in tmpdir. The shared prefix made that
assertion fail depending on test order — it passed on the first full run
and failed on the next. Prefix is now `llmsec-nullmanifest-`.

npm test: 1901/1901 green, three consecutive runs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:33:40 +02:00
566c281b56 chore(llm-security): remove leftover debug probe from tests/
probe-rm.mjs was self-labelled "Temporary probe — delete after
debugging". It is not a test (no node:test harness, no assertions — it
prints exit codes), it ran against a hardcoded absolute path into the
INSTALLED marketplace copy rather than this repo, and it exercised the
rm-block cases that are now covered properly by
tests/hooks/pre-bash-destructive.test.mjs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:24:45 +02:00
a035455979 fix(llm-security): out-of-range char-ref silently emptied a plugin.xml field
decodeEntities guarded parseInt with Number.isFinite, which bounds
nothing: any numeric character reference above 0x10FFFF produced a
finite integer that String.fromCodePoint rejects with RangeError.

Correction to the review's framing: this does NOT break parsePluginXml's
no-throw contract. The per-field safe() wrapper catches it. The actual
defect is quieter — the affected field is discarded and replaced with
'', with only a warning. A plugin whose <name> carries one out-of-range
reference parses as name: "" while pluginId and every other field
survive, so name-based checks (JetBrains typosquat detection against the
top-plugin list) run against an empty string.

Severity is below the HIGH it was filed as: a document containing a code
point above 0x10FFFF is not well-formed XML, so IntelliJ would reject
such a plugin too — the evasion yields a plugin that does not load. The
fix is still correct and one line of logic.

- isDecodableCodePoint(): integer, >= 0, <= 0x10FFFF.
- Undecodable references are now left literal, matching how lenient
  parsers treat unrecognised entities and how this same function already
  treats unknown named entities.

Regression test drives parsePluginXml with hex and decimal references
just past and far past the maximum, asserts the no-throw contract still
holds, and pins that valid references, the maximum valid code point, and
named entities still decode.

npm test: 1901/1901 green (1890 + 11 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:21:56 +02:00
4f21374e8c fix(llm-security): HIGH — obfuscated injections were reported but not stripped
stripInjection is the remote-scan indirection layer: its `sanitized`
output is what an LLM agent actually reads, verbatim, via
sanitized_content in the evidence package. It scanned two variants of
each file — the raw text and normalizeForScan(text) — but removed
matches with `sanitized.replace(match[0], ...)` against the RAW text
only.

For a match found in the decoded variant, match[0] IS the decoded
string, which by construction does not occur in the raw text. The
replace was therefore a silent no-op: the finding was reported while the
encoded payload was passed to the agent untouched. Every obfuscation the
normalizer exists to defeat — HTML entities, URL encoding, \u escapes,
hex, base64, letter-spacing, Unicode tags — reached the agent intact.
The worst case is the intended one: detection said "critical injection
found" and shipped the injection along with the verdict.

- Pass 1 redacts the whole source LINE whose own normalized form carries
  the pattern. Line granularity is deliberate: decoding is not
  length-preserving, so decoded match offsets cannot be mapped back onto
  the original text.
- Pass 2 keeps the existing literal replacement and finding collection.
- Residual gap, made explicit rather than silent: a payload encoded
  across MULTIPLE lines matches whole-text normalization but no single
  line, so it cannot be attributed. Those findings now carry
  `unstripped: true`. Whole-file redaction was considered and rejected —
  normalizeForScan base64-decodes any long blob, so a benign asset could
  blank an entire file's evidence.
- stripInjection exported via __testing, and main() is now behind the
  standard isMain guard (copied from dashboard-aggregator.mjs) so
  importing the module does not execute the CLI. CLI verified unchanged
  against the evil-project-health fixture: 7 files, 6 injection
  findings, risk_level critical.

This boundary had no direct test coverage before this commit.

npm test: 1890/1890 green (1884 + 6 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:18:56 +02:00
4e16ea0e72 fix(llm-security): HIGH — one unparseable JetBrains plugin crashed ide-scan
The two manifest parsers disagree on how they signal failure:
parseVSCodeExtension returns a bare null, parseIntelliJPlugin returns a
TRUTHY { manifest: null, warnings }. scanOneExtension guarded only the
bare-null form, so every JetBrains failure path — lib/ missing, lib/ not
a directory, lib/ unreadable, no jars in lib/, no jar extractable — fell
through the guard and dereferenced `manifest.hasSignature`.

The resulting TypeError propagated out of scanOneExtension into
mapConcurrent, which awaited fn() with no per-item try/catch, so
Promise.all rejected and the ENTIRE ide-scan aborted. One malformed
plugin directory on disk was enough to take down the scan of every
other installed extension — including, in an audit context, a plugin
that is malformed precisely because it is hostile.

- scanOneExtension: guard is now `!parsed || !parsed.manifest`, and the
  parser's own warnings are carried into the result so the reason for
  the skip survives instead of being replaced by a generic message.
- unscannableExtension(): the result envelope is extracted so the early
  return and the isolation belt below produce the same shape.
- Belt (defense-in-depth): the scanOneExtension call site catches per
  extension and yields an unscannable result. mapConcurrent stays
  generic — the isolation lives at the call site, not in the helper.

Regression test drives scanOneExtension across all four JetBrains parse
failure paths plus a no-invented-findings assertion.

npm test: 1884/1884 green (1879 + 5 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:14:12 +02:00
f031dd496f fix(llm-security): HIGH — entropy suppression keyed off the absolute path
Rule 4 in isFalsePositive ("test/fixture files intentionally contain
example secrets") matched /(test|spec|fixture|mock|__test__|__spec__)/i
against absPath — the ABSOLUTE path. Any directory name above the scan
root therefore silenced every entropy finding in the entire target: a
repo cloned to a CI workspace, checked out under a parent folder named
`testing`, or scanned from any path with one of those substrings
reported zero secrets and still exited status 'ok'. The failure is
silent — indistinguishable from a clean scan.

- isFalsePositive takes relPath and rule 4 keys off it. relPath was
  already computed and passed down to scanFileContent; only the
  suppression check was reading the wrong one.
- classifyFileContext keeps using absPath: it reads the basename
  extension only, so directory names above the root cannot affect it.
- Rules 1-3 and 5-13 are untouched.

Regression test drives scan() end-to-end against temp roots named
llmsec-test-*, llmsec-spec-*, llmsec-fixture-* and llmsec-mock-*, with a
neutral-root control proving the payload is detectable, plus two
guards that genuine suppression still works (a *.test.mjs file and a
file under a relative tests/ directory).

npm test: 1879/1879 green (1872 + 7 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:11:04 +02:00
a92b3c5962 fix(llm-security): HIGH — bare root/home targets bypassed the rm block
The BLOCK rule named "Filesystem root destruction" did not block the bare
root target it is named for. The target alternation ended in a shared `\b`,
and a word boundary cannot hold after `/` or `~` at end-of-command.
Measured against the shipped pattern:

  bare `/` target        NOT blocked      `$HOME` target   blocked
  bare `~` target        NOT blocked      `/usr` target    blocked
  `/*` glob target       NOT blocked
  swapped flags (-fr)    NOT blocked
  sudo-prefixed          NOT blocked

Only targets whose first character is a word character ever satisfied the
assertion, so the rule caught `/etc` but not bare root. These fell through
to WARN (exit 0) — advisory only, command executed.

- Pattern: `\b` moved onto the `$HOME` alternative alone, where it is
  meaningful (it ends in a word char, so `$HOMEDIR` is still not
  swallowed). Dropped from `/` and `~`. Nothing else changes: `/etc`,
  `/home`, `./build` behave exactly as before.
- The old test file encoded this defect as expected behaviour, with a NOTE
  claiming the pattern "requires separate flag groups (e.g. -f -r, not -rf
  combined)". That diagnosis was wrong — the `/etc` case blocks fine with
  merged flags. Comment replaced with the real root cause.

npm test: 1872/1872 green (1865 + 7 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:08:12 +02:00
24949860f5 chore(llm-security): STATE — v7.8.1 shipped; next = the ~6 HIGH (v7.8.2)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:02:02 +02:00
55d01d1656 chore(llm-security): v7.8.1 — security patch release
Version bump + release notes for the auto-cleaner command-injection fix
(f083cda). Security patch only; no feature changes. 1865 tests, 0 fail.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 09:00:18 +02:00
f083cdad2e fix(llm-security): CRITICAL — command injection in auto-cleaner (v7.8.1)
validateContent() syntax-checked .mjs/.js/.cjs candidates via
execSync(`node --check "${tmpPath}"`), where tmpPath derives from the
untrusted scanned-repo FILENAME. The F-2 guard checks path containment
but never quotes or strips shell metacharacters, so a file named
`x";<cmd>;".mjs` in a scanned repo turned `/security clean` (live is the
documented default) into arbitrary command execution. Verified with a
live PoC before the fix.

- validateContent: spawnSync('node', ['--check', tmpPath]) — no shell.
- CLI orchestrator fallback: same treatment (argv array, explicit
  status/error handling instead of execSync's throw-on-nonzero).
- Belt (defense-in-depth): applyFixes now refuses findings whose `file`
  carries shell or control metacharacters, reported as skipped.
- Regression tests cover both layers separately, so the belt cannot mask
  a re-introduced shell in the sink.

node --test: 1865/1865 green (1863 + 2 new).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcQyMTQfyrsAapaCMPxTtQ
2026-07-18 08:48:09 +02:00
0dd46da2f7 chore(llm-security): STATE — v7.8.0 tagged + catalog ref synced 2026-06-20 11:59:03 +02:00
731d61f801 chore(llm-security): STATE — codename scrub complete (history rewritten + force-pushed) 2026-06-20 11:40:27 +02:00
a19e9eb8b2 docs(llm-security): drop internal codename from v7.8.0 notes
Reword the v7.8.0 release notes to refer to the scanners as TRG/SIG/AST only;
the internal codename is removed from CHANGELOG, CLAUDE.md, README, the
version-history section, and STATE. No behaviour or version change.
2026-06-20 11:37:28 +02:00
405874c0c4 chore(llm-security): STATE — v7.8.0 released (Session C); only F-4 (optional) left 2026-06-20 11:23:42 +02:00
6d3c4b5052 chore(llm-security): v7.8.0 — TRG/SIG/AST scanners v7.8.0
Session C of the security-fix track: cut the TRG/SIG/AST release now that the
F-1/F-2/F-3 security gates pass. Pure version-sync — the three scanners shipped
in Steps 1-7 (616e7ff..fc7cf57); no production code changes here.

Version-sync 7.7.2 -> 7.8.0 across every current pointer: .claude-plugin/
plugin.json, package.json, README.md badge, and the CLAUDE.md header + "release
notes" range sentinel. Narrative additions (prior releases kept as history):
- CHANGELOG.md: new [7.8.0] entry (Added: TRG/SIG/AST; Changed: prefix
  registration + knowledge/ packaging).
- docs/version-history.md: new v7.8.0 section describing the three scanners.
- README.md "Recent versions": new 7.8.0 row.
- CLAUDE.md: new v7.8.0 highlights paragraph above the v7.7.2 one.

TRG/SIG/AST recap (built Steps 1-7, behind the security-fix gate):
- TRG (scanners/trigger-scanner.mjs) — trigger/activation-abuse: TRG-shadow /
  TRG-baiting / TRG-broad over name+description frontmatter, decode pipeline
  first (LLM06/AST04).
- SIG (scanners/signature-scanner.mjs) — pure-Node known-malware identity engine
  over raw bytes + decode pipeline; rules in knowledge/signatures.json
  (LLM03/LLM02).
- AST (scanners/ast-taint-scanner.mjs) — parse-only python3 taint helper
  (scanners/lib/py-ast-taint.py) with graceful regex fallback (LLM01/LLM02/AST02).

Release gate: full node --test suite 1863/0 after the doc edits. No scanner,
hook, or command behaviour changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 11:23:11 +02:00
614971c71a chore(llm-security): STATE — Session B (F-2 + F-3) done; next = Session C (release)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 11:10:24 +02:00
dfb663fa61 fix(llm-security): F-2/F-3 — contain path traversal + kill npm-view shell injection
Session B of the security-fix track. Both sinks trusted untrusted strings at a
filesystem/subprocess boundary; same fix family as F-1.

F-2 (HIGH, arbitrary file write) — scanners/auto-cleaner.mjs: applyFixes() did
resolve(targetPath, f.file) with no containment, then wrote the cleaned content
back. f.file is untrusted (scanned-repo filenames, or a fully attacker-chosen
--findings file), so file: "../../.claude/settings.json" let the cleaner modify
files OUTSIDE the scanned tree. Add a prefix-containment check before grouping:
absPath must equal targetPath or start with targetPath + sep, else the finding
is refused and reported as skipped. (Documented residual gap: prefix containment
does not stop a symlink inside the tree pointing out — noted inline.)

F-3 (HIGH, command injection, pre-confirmation) — hooks/scripts/
pre-install-supply-chain.mjs: inspectNpmPackage ran execSafe(`npm view ${spec}
--json`), a shell string. spec derives from package tokens parsed out of the
scanned Bash command, so a metachar-bearing token reached the shell on
PreToolUse(Bash) — BEFORE the install, so it ran even if the user then denied
the command. Switch to spawnSync('npm', ['view', spec, '--json']) (no shell);
spec is passed as one argv element. The static `npm audit --json` execSafe call
is left as-is (no interpolation).

TDD (repro -> red -> green):
- tests/scanners/auto-cleaner-traversal.test.mjs: a "../secret.txt" finding must
  not rewrite the outside file; contained files still get cleaned.
- tests/hooks/supply-chain-injection.test.mjs: `npm install $(>/abs/PWNED)`
  (redirect-only $(...) survives normalizeBashExpansion + the whitespace split)
  must not create the sentinel.

Closing gates: full node --test suite 1863/0 (was 1860; +3); gitleaks clean;
F-1 regression re-run GREEN (sink still closed).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 11:09:13 +02:00
f5988f9500 chore(llm-security): STATE — Session A (F-1 + F-5/F-6) done; next = Session B
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 10:19:20 +02:00
e46b5a3256 fix(llm-security): F-1 — eliminate shell-injection RCE in git-forensics scanner
git-forensics ran execSync(`git ${cmd}`), interpolating attacker-controlled
filenames from the SCANNED repo (git ls-files / git log --name-only) into a
shell string. A hostile repo containing a file named `commands/$(touch X).md`
achieved zero-interaction RCE on `/security scan <url>`: gitScan is in the
default scanner array and runs OUTSIDE the git-clone OS sandbox (which wraps
only the clone), so it executed unsandboxed on all platforms.

Convert the git() helper to spawnSync('git', [...args]) with no shell; every
call site now passes discrete tokens (shell quoting removed — git does its own
pathspec globbing). The helper throws on non-zero exit, preserving existing
per-category/per-file try/catch semantics.

TDD: adds a failing-first regression (tests/scanners/git-injection.test.mjs)
that builds a hostile-filename fixture repo and asserts the injected command
never runs. RED against execSync, GREEN after the fix.

Also removes two stray committed root artifacts (F-5/F-6): `--json`
(0-byte redirect husk) and .orphaned_at.

Closing gates: full node --test suite 1860/0; gitleaks clean. F-2/F-3 follow
in Session B.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 10:18:41 +02:00
a96e3b947f chore(llm-security): STATE — security-fix track plan (F-1/F-2/F-3 must-fix) over multiple sessions
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 10:09:11 +02:00
089623af89 chore(llm-security): STATE — TRG/SIG/AST done; v7.8.0 deferred behind security-fix brief
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:51:28 +02:00
fc7cf57da6 docs(llm-security): document TRG/SIG/AST scanners and package knowledge/
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:46:14 +02:00
b0314418f4 feat(llm-security): wire AST scanner into orchestrator and policy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:41:32 +02:00
40a1e34ee9 feat(llm-security): add AST Python-taint scanner with python3 fallback
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:38:36 +02:00
e46bf12b86 feat(llm-security): wire SIG scanner into orchestrator and policy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:32:23 +02:00
9e0a09ab1f feat(llm-security): add SIG signature scanner with decode-pipeline matching
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:28:52 +02:00
9f1156866c feat(llm-security): wire TRG scanner into orchestrator and policy
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:24:23 +02:00
92bd99f2a8 docs: security fix brief for next session — F-1 RCE + F-2/F-3 sinks
Action brief from the marketplace-wide review (see docs/review-2026-06-20.md). Three shell/path
injection sinks: F-1 CRITICAL zero-interaction RCE in git-forensics.mjs (execSync shell string +
attacker-controlled filenames; reproduced), F-2 HIGH path-containment write in auto-cleaner.mjs,
F-3 HIGH command injection in the supply-chain hook. Common fix: execSync(string) -> spawnSync(array)
/ input containment. DO AFTER the current active session finishes.

Disclosure hold: this brief + review-2026-06-20.md (ff9bd13) are committed locally but NOT pushed;
push both only together with the F-1 fix (the brief carries a working RCE repro payload).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 09:23:19 +02:00
616e7ff18c feat(llm-security): add TRG trigger-abuse scanner
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:19:54 +02:00
ff9bd13c6f docs: add full-depth plugin review (2026-06-20)
Grade B− — exceptionally disciplined design, but one CRITICAL zero-interaction RCE (F-1) plus two HIGH shell/path sinks. PUSH PARKED pending responsible-disclosure decision. Part of the marketplace-wide review (config-audit v5.4.0 + llm-security + structure + version). Read-only; this file is the only artifact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ter3E2JSi1Khgmuf2kady8
2026-06-20 09:14:11 +02:00
7736c0bde2 docs(llm-security): TRG/SIG/AST gap analysis + reviewed execution plan
Brief + 8-step trekplan for 3 new dep-free scanners: TRG (trigger-abuse),
SIG (signature engine over the decode pipeline), AST (Python AST taint).
Adversarial review fixed 3 blockers + 7 majors on draft 1 (59/D -> 86/B).

STATE.md prepped as a self-contained cold-start for /trekexecute next session.
Un-gitignore STATE.md (global ~/.claude rule overrides old polyrepo convention);
ignore generated plan annotation HTML. No scanner code yet.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01V3s6WnubSSrFjAQTLQdVbG
2026-06-20 09:00:24 +02:00
0ef780fd69 chore(gitignore): add session/local-state baseline (polyrepo split) 2026-06-18 10:21:13 +02:00
3db6b65aba chore(llm-security): v7.7.2 — language consistency pass v7.7.2
~/.claude/CLAUDE.md specifies English for code and documentation,
Norwegian for dialog only. Norwegian had crept into surface text
across v7.5-v7.7. Translated to English in eight surfaces.

No scanner, hook, or behavior changes — purely surface text.

- 18 skill commands: the HTML Report-step now reads "HTML report:
  [Open in browser]" instead of "HTML-rapport: [Åpne i nettleser]"
- scripts/lib/report-renderers.mjs: key-stat labels, lede defaults,
  table headers, maturity-ladder descriptions, action-tier labels,
  clean buckets, dry-run/apply copy, and JS comments. Regex
  alternations /^high|^høy/ and /resolution|løsning/i preserved.
- playground/llm-security-playground.html: same renderer changes
  mirrored bit-identical, plus playground-only UI strings (catalog,
  breadcrumb aria-label, theme toggle, builder-modal hint,
  guide-panel "no projects yet", delete confirmation, alert/copy).
  Demo-state fixture content for dft-komplett-demo preserved
  (intentional Norwegian persona).
- agents/skill-scanner-agent.md + agents/mcp-scanner-agent.md:
  Generaliseringsgrense + Parallell Read-strategi sections translated
  to Generalization boundary + Parallel Read strategy.
- README.md: playground architecture prose + Recent versions table
  (v7.5.0 — v7.7.1).
- CLAUDE.md: v7.7.1 highlights translated, new v7.7.2 highlights
  added.
- ../../README.md: llm-security v7.5.0 — v7.7.1 bullets.
- ../../CLAUDE.md: llm-security catalog entry.
- docs/scanner-reference.md: six runnable-examples table cells.
- docs/version-history.md: new v7.7.2 entry. v7.5-v7.7 narrative
  sections left in original language (deferred per operator).
- Version bumped 7.7.1 → 7.7.2 in package.json,
  .claude-plugin/plugin.json, README badge + Recent versions,
  CLAUDE.md header + state, docs/version-history.md, playground
  renderHome hardcoded string, root README + CLAUDE.md llm-security
  entries.

Tests: 1820/1820 green. CLI smoke-test: 18/18 commandIds produce
>138 KB self-contained HTML. Browser-dogfood verified.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 06:47:44 +02:00
b626ba0c5e chore(llm-security): v7.7.1 release — formaliser playground UX-strip-fixes
Tre fixes commited etter v7.7.0-tagen (adffb60 + 52afd12 + 02a66f3) viste
versjons-inkonsistens: package.json + plugin.json + README badge + CLAUDE.md
header satt fortsatt på v7.7.0 mens commit-meldinger og inline-kommentarer
refererte v7.7.1 som om det var en release. Per feedback_version_sync.md
skal alle versjonsreferanser stemme — denne commiten lukker gapet.

Endringer:
- package.json: 7.7.0 → 7.7.1
- .claude-plugin/plugin.json: 7.7.0 → 7.7.1
- plugin README badge: version-7.7.0-blue → version-7.7.1-blue
- plugin README "Recent versions"-tabell: ny [7.7.1]-rad
- plugin CLAUDE.md header + v7.7.1-highlights state-seksjon
- docs/version-history.md: ny v7.7.1-seksjon
- playground HTML linje 6935: 'Plugin v7.7.0' → 'Plugin v7.7.1'
  (samme template-litteral som v7.7.0-bumpen ikke fanget, nå synket)
- CHANGELOG.md: ny [7.7.1]-seksjon med full Changed/Fixed/Notes
- rot README llm-security-entry: v7.7.0 → v7.7.1 + ny v7.7.1-bullet
- rot CLAUDE.md plugin-katalog: v7.7.1-bump

Verifisert:
- 1820/1820 tester grønne (pre-compact-flake fyrte ikke)
- CLI rapporterer fornuftig feilmelding på tom input
- Ingen kildefil-treff på 7.7.0 utenfor CHANGELOG/version-history/REMEMBER/TODO/ROADMAP

Ingen ny atferd. Kun versjons-synking + dokumentasjon av tre fixes som var
deployert som ad-hoc-commits.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:38:27 +02:00
02a66f3c77 fix(llm-security): playground topbar — fjern orgName fra breadcrumb
Etter v7.7.1-strippen ble onboarding fjernet, men topbar viste fortsatt
demo-state-organisasjonsnavnet ('Direktoratet for digital tjeneste-
utvikling') hentet fra shared.organization. Det er ikke meningsfullt
lenger siden onboarding ikke kan endre verdien.

Erstattet med statisk 'llm-security' som nøytralt scope-anker. Crumb-
parameteren (f.eks. 'Katalog') beholdes som suffix.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-18 21:09:02 +02:00