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
35 KiB
SkillSpector-Inspired Scanners: TRG, SIG, AST
Plan quality: B (86/100) — APPROVE_WITH_NOTES (revised after adversarial review)
Generated by trekplan v2.0 on 2026-06-20 —
plan_version: 1.7
Context
llm-security is a broad runtime + posture defense for Claude Code. A gap analysis against
NVIDIA/SkillSpector (docs/plans/skillspector-gap-analysis.md) confirmed three genuine holes in our
static analysis, each to be closed our own dep-free way:
- #3 TRG — trigger/activation-abuse: no scanner inspects command/agent/skill
name+descriptionfrontmatter for broad triggers, keyword-baiting, or built-in shadowing. Skills auto-activate on description, so this is a real, uncovered, skill-native attack surface. - #1 SIG — signature engine: we detect shape (entropy/obfuscation/taint) but never known-bad
identity (webshells, reverse shells, cryptominers, hacktools). Our differentiator: run signatures
against the existing
normalizeForScan()decode pipeline so obfuscated known-malware is caught — something raw YARA (which SkillSpector ships) misses. - #2 AST — Python AST taint:
taint-tracer.mjsis regex (~70% recall, no scope/cross-file). A shippedpython3AST helper raises recall/precision for Python skill code, with graceful fallback to the regex tracer when python3 is absent.
Hard invariant: no new npm dependency — package.json has no dependencies key today and must
keep none. The 1820+ test suite must stay green.
Architecture Diagram
graph TD
subgraph "Changes in this plan"
ORCH["scanners/scan-orchestrator.mjs<br/>(SCANNERS array + imports)"]
TRG["scanners/trigger-scanner.mjs (NEW)<br/>prefix TRG"]
SIG["scanners/signature-scanner.mjs (NEW)<br/>prefix SIG"]
AST["scanners/ast-taint-scanner.mjs (NEW)<br/>prefix AST"]
PYH["scanners/lib/py-ast-taint.py (NEW)<br/>parse-only helper"]
SIGDATA["knowledge/signatures.json (NEW)"]
POL["scanners/lib/policy-loader.mjs<br/>+trg/+sig/+ast sections"]
SEV["scanners/lib/severity.mjs<br/>4 OWASP maps +TRG/+SIG/+AST"]
OUT["scanners/lib/output.mjs<br/>finding() / scannerResult() (reused)"]
NORM["scanners/lib/string-utils.mjs<br/>normalizeForScan() (reused)"]
FM["scanners/lib/yaml-frontmatter.mjs<br/>parseFrontmatter() (reused)"]
ORCH --> TRG & SIG & AST
AST --> PYH
SIG --> SIGDATA
SIG --> NORM
TRG --> NORM & FM
TRG & SIG & AST --> OUT
ORCH --> POL
TRG & SIG & AST --> SEV
end
Codebase Analysis
- Tech stack: Node.js ESM (
.mjs), zero npm dependencies (package.jsonhas nodependencieskey). Test runner:node --test+node:assert/strict. Optionalpython3shell-outs already used (dep-auditor→ pip). - Key patterns: every deterministic scanner exports
async function scan(targetPath, discovery), loopsdiscovery.files(FileInfo {absPath, relPath, ext, size}), emitsfinding(...), returnsscannerResult(...). The orchestrator resets the finding-ID counter per scanner. - Relevant files (verified against source):
- Orchestrator:
scanners/scan-orchestrator.mjs(NOT repo root). Imports at:101-111,SCANNERSarray at:113-125. Sibling scanners imported as./unicode-scanner.mjs(so a new scanner inscanners/imports as./trigger-scanner.mjs). - Template scanner:
scanners/memory-poisoning-scanner.mjs:386(scan signature +discovery.filesloop + dual-return). - Output:
scanners/lib/output.mjs:30(finding()),:57(scannerResult()),:12(resetCounter()). - Policy:
scanners/lib/policy-loader.mjs:13-68(DEFAULT_POLICY),:150(getPolicyValue(section, key, default, projectRoot)— 4th arg is the project root, resolved to<root>/.llm-security/policy.json). - OWASP maps:
scanners/lib/severity.mjs— four frozen maps with array values:OWASP_MAP(:132, LLM),OWASP_AGENTIC_MAP(:151, ASI),OWASP_SKILLS_MAP(:170, AST),OWASP_MCP_MAP(:189, MCP). Every scanner prefix has a key in all four (empty array where N/A, e.g.WFL: []in the skills map).owaspCategorizespreadsOWASP_MAP[prefix](:227). - Frontmatter:
scanners/lib/yaml-frontmatter.mjs:13exportsparseFrontmatter(content)(already used bypermission-mapper.mjs:10). TRG reuses it directly — no inline parser needed. - Knowledge loader model:
scanners/dep-auditor.mjs:20-42(cached__dirname-relative JSON loader, graceful fallback). - Decode API:
scanners/lib/string-utils.mjs:512(normalizeForScan()→string),:486(foldHomoglyphs),:474(rot13). - File discovery:
scanners/lib/file-discovery.mjs:52(discoverFiles),readTextFile;.md/.py/.phpare inTEXT_EXTENSIONSso TRG/SIG/AST fixtures are discoverable. package.jsonfiles:["bin/","scanners/","LICENSE","README.md","CONTRIBUTING.md","SECURITY.md","CHANGELOG.md"]—knowledge/absent. Moot for runtime (marketplace = git-clone of whole repo; existing scanners already loadknowledge/*.json), but Step 7 addsknowledge/for correctness.
- Orchestrator:
- Reusable code:
finding/scannerResult/resetCounter(lib/output.mjs);discoverFiles/readTextFile(lib/file-discovery.mjs);normalizeForScan/foldHomoglyphs/rot13(lib/string-utils.mjs);parseFrontmatter(lib/yaml-frontmatter.mjs);getPolicyValue(lib/policy-loader.mjs); the four OWASP maps +SEVERITY(lib/severity.mjs). - Test infra: tests in
tests/scanners/*.test.mjs; committed fixtures undertests/fixtures/<name>-scan/{clean,poisoned}/(model:tests/fixtures/memory-scan/).resetCounter()inbeforeEach. Single-file run:node --test tests/scanners/<name>.test.mjs. No lint/coverage gate. Registration-test model assertsdeepEqualon all four OWASP maps (tests/scanners/workflow-scanner.test.mjs:196-216). Binary-absent skip:which python3guard + earlyreturn(tests/scanners/vsix-sandbox.test.mjs:27-30) and gracefulstatus:'skipped'envelope (tests/scanners/dep.test.mjs:117-124).
Implementation Plan
Each scanner's core (new files only — revert-safe) lands before its wiring (small edits to
shared scanners/scan-orchestrator.mjs / scanners/lib/policy-loader.mjs / scanners/lib/severity.mjs).
Build order follows the brief: TRG → SIG → AST. Step 8 (release) is operator-gated.
Step 1: Add TRG trigger-abuse scanner with tests and fixtures
- Files:
scanners/trigger-scanner.mjs(new),tests/scanners/trigger-scanner.test.mjs(new),tests/fixtures/trigger-scan/clean/(new),tests/fixtures/trigger-scan/poisoned/(new) - Changes: Create
scan(targetPath, discovery)mirroringmemory-poisoning-scanner.mjs:386. Filterdiscovery.filestocommands/*.md,agents/*.md,skills/**/SKILL.md. Parse frontmatter viaparseFrontmatterfromscanners/lib/yaml-frontmatter.mjs. Emit three rule classes: TRG-broad (single common-word/≤2-charname; description claiming universal applicability), TRG-baiting (maximally-activating phrases: "anything","everything","always","all files/messages","whenever" — list from policy), TRG-shadow (namecolliding with a configurable built-in command/tool list). Run the description throughnormalizeForScan()before phrase matching so obfuscated baiting (zero-width/homoglyph) still trips. Default severity MEDIUM; broad-name + universal-claim combination → HIGH. Emit viafinding({scanner:'TRG', owasp:'LLM06', ...}); returnscannerResult('trigger-scanner', ...). Fixtures:poisoned/= a skill namedrunwith "use for anything" + an obfuscated-baiting case;clean/= a scoped, specifically-described skill. (new files) - Reuses:
scan(targetPath, discovery)+discovery.filesloop (memory-poisoning-scanner.mjs:386);finding/scannerResult/resetCounter(lib/output.mjs);readTextFile/discoverFiles(lib/file-discovery.mjs);normalizeForScan(lib/string-utils.mjs);parseFrontmatter(lib/yaml-frontmatter.mjs). - Test first:
- File:
tests/scanners/trigger-scanner.test.mjs(new) - Verifies:
clean/→status:'ok',findings.length === 0;poisoned/→ TRG-shadow + TRG-baiting fire, obfuscated baiting still flagged, allidstartDS-TRG-,scanner==='TRG', required fields present. - Pattern:
tests/scanners/memory-poisoning.test.mjs(resetCounter()inbeforeEach)
- File:
- Verify:
node --test tests/scanners/trigger-scanner.test.mjs→ expected:fail 0 - On failure: revert —
git checkout -- scanners/trigger-scanner.mjs tests/scanners/trigger-scanner.test.mjs && git clean -fd tests/fixtures/trigger-scan - Checkpoint:
git commit -m "feat(llm-security): add TRG trigger-abuse scanner" - Manifest:
manifest: expected_paths: - scanners/trigger-scanner.mjs - tests/scanners/trigger-scanner.test.mjs min_file_count: 2 commit_message_pattern: "^feat\\(llm-security\\): add TRG trigger-abuse scanner$" bash_syntax_check: [] forbidden_paths: - scanners/scan-orchestrator.mjs must_contain: - path: scanners/trigger-scanner.mjs pattern: "'TRG'" - path: scanners/trigger-scanner.mjs pattern: "normalizeForScan" - path: scanners/trigger-scanner.mjs pattern: "parseFrontmatter"
Step 2: Wire TRG into orchestrator, policy, and the four OWASP maps
- Files:
scanners/scan-orchestrator.mjs,scanners/lib/policy-loader.mjs,scanners/lib/severity.mjs - Changes: Add
import { scan as trgScan } from './trigger-scanner.mjs';to the import block (scan-orchestrator.mjs:101-111) and{ name: 'trg', fn: trgScan }to theSCANNERSarray (:113-125). Add a frozentrgsection toDEFAULT_POLICY(policy-loader.mjs:13-68):{ mode:'warn', baiting_phrases:[...], builtin_names:[...], broad_single_words:[...] }; the scanner reads these viagetPolicyValue('trg', key, default, targetPath)(targetPath is the scan root where.llm-security/policy.jsonlives). Add array entries to all four maps inseverity.mjs:OWASP_MAP.TRG=['LLM06'],OWASP_AGENTIC_MAP.TRG=[],OWASP_SKILLS_MAP.TRG=['AST04'](brief's primary framework),OWASP_MCP_MAP.TRG=[]. Add registration assertions to the TRG test (registered inSCANNERS;assert.deepEqual(OWASP_SKILLS_MAP.TRG, ['AST04']); policy override ofbaiting_phraseschanges detection). - Reuses:
SCANNERSregistration (scan-orchestrator.mjs:113);DEFAULT_POLICY/getPolicyValue(policy-loader.mjs:13,150); the four OWASP maps (severity.mjs:132-189). - Test first:
- File:
tests/scanners/trigger-scanner.test.mjs(existing — extend) - Verifies: TRG in orchestrator
SCANNERS;deepEqual(OWASP_SKILLS_MAP.TRG, ['AST04'])anddeepEqual(OWASP_MAP.TRG, ['LLM06']); policy override changes detection. - Pattern:
tests/scanners/workflow-scanner.test.mjs:196-216
- File:
- Verify:
npm test→ expected:fail 0 - On failure: revert —
git checkout -- scanners/scan-orchestrator.mjs scanners/lib/policy-loader.mjs scanners/lib/severity.mjs tests/scanners/trigger-scanner.test.mjs - Checkpoint:
git commit -m "feat(llm-security): wire TRG scanner into orchestrator and policy" - Manifest:
manifest: expected_paths: - scanners/scan-orchestrator.mjs - scanners/lib/policy-loader.mjs - scanners/lib/severity.mjs min_file_count: 3 commit_message_pattern: "^feat\\(llm-security\\): wire TRG scanner into orchestrator and policy$" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: scanners/scan-orchestrator.mjs pattern: "trgScan" - path: scanners/lib/severity.mjs pattern: "TRG"
Step 3: Add SIG signature scanner with rule data, tests and fixtures
- Files:
scanners/signature-scanner.mjs(new),knowledge/signatures.json(new),tests/scanners/signature-scanner.test.mjs(new),tests/fixtures/signature-scan/clean/(new),tests/fixtures/signature-scan/poisoned/(new) - Changes: Create
knowledge/signatures.jsonwith a small, high-confidence, family-grouped ruleset (webshell,reverse_shell,cryptominer,hacktool) — each rule{ id, family, severity, pattern, description, provenance }. Createscan(targetPath, discovery)that, for each file indiscovery.files, builds the decoded variant set (content,normalizeForScan(content),foldHomoglyphs(content),rot13(content)) and tests every signature regex against all variants — a match on a decoded-only variant is the differentiator vs raw YARA. De-dup per (file, rule). Emitfinding({scanner:'SIG', owasp:'LLM03', ...}). Load the ruleset via the cached__dirname-relative loader copied fromdep-auditor.mjs:20-42; graceful empty-ruleset fallback. Path-excludeknowledge/,tests/,docs/. Fixtures:poisoned/= a PHP webshell sample AND a base64-wrapped variant of it;clean/= benign prose mentioning "shell". (new files) - Reuses: scan/loop/return (
memory-poisoning-scanner.mjs:386); JSON loader (dep-auditor.mjs:20-42);normalizeForScan/foldHomoglyphs/rot13(string-utils.mjs);finding/scannerResult(output.mjs). - Test first:
- File:
tests/scanners/signature-scanner.test.mjs(new) - Verifies:
poisoned/→ webshell flagged AND its base64-wrapped variant flagged (decode-pipeline integration);clean/→ 0 findings;scanner==='SIG', ids startDS-SIG-. - Pattern:
tests/scanners/memory-poisoning.test.mjs
- File:
- Verify:
node --test tests/scanners/signature-scanner.test.mjs→ expected:fail 0 - On failure: revert —
git checkout -- scanners/signature-scanner.mjs knowledge/signatures.json tests/scanners/signature-scanner.test.mjs && git clean -fd tests/fixtures/signature-scan - Checkpoint:
git commit -m "feat(llm-security): add SIG signature scanner with decode-pipeline matching" - Manifest:
manifest: expected_paths: - scanners/signature-scanner.mjs - knowledge/signatures.json - tests/scanners/signature-scanner.test.mjs min_file_count: 3 commit_message_pattern: "^feat\\(llm-security\\): add SIG signature scanner with decode-pipeline matching$" bash_syntax_check: [] forbidden_paths: - scanners/scan-orchestrator.mjs must_contain: - path: scanners/signature-scanner.mjs pattern: "'SIG'" - path: scanners/signature-scanner.mjs pattern: "normalizeForScan"
Step 4: Wire SIG into orchestrator, policy, and the four OWASP maps
- Files:
scanners/scan-orchestrator.mjs,scanners/lib/policy-loader.mjs,scanners/lib/severity.mjs - Changes: Add
import { scan as sigScan } from './signature-scanner.mjs';and{ name: 'sig', fn: sigScan }toSCANNERS. Add a frozensigsection toDEFAULT_POLICY:{ enabled_families:['webshell','reverse_shell','cryptominer','hacktool'], custom_rules_path:null }, read viagetPolicyValue('sig', ...). Add array entries to all four maps:OWASP_MAP.SIG=['LLM03','LLM02'],OWASP_AGENTIC_MAP.SIG=['ASI04'],OWASP_SKILLS_MAP.SIG=[],OWASP_MCP_MAP.SIG=[]. Add registration assertions (incl.deepEqual(OWASP_MAP.SIG, ['LLM03','LLM02'])and a family-disable policy test) to the SIG test. - Reuses: same wiring sites as Step 2.
- Test first:
- File:
tests/scanners/signature-scanner.test.mjs(existing — extend) - Verifies: SIG in
SCANNERS;deepEqual(OWASP_MAP.SIG, ['LLM03','LLM02']); disabling a family via policy suppresses its findings. - Pattern:
tests/scanners/workflow-scanner.test.mjs:196-216
- File:
- Verify:
npm test→ expected:fail 0 - On failure: revert —
git checkout -- scanners/scan-orchestrator.mjs scanners/lib/policy-loader.mjs scanners/lib/severity.mjs tests/scanners/signature-scanner.test.mjs - Checkpoint:
git commit -m "feat(llm-security): wire SIG scanner into orchestrator and policy" - Manifest:
manifest: expected_paths: - scanners/scan-orchestrator.mjs - scanners/lib/policy-loader.mjs - scanners/lib/severity.mjs min_file_count: 3 commit_message_pattern: "^feat\\(llm-security\\): wire SIG scanner into orchestrator and policy$" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: scanners/scan-orchestrator.mjs pattern: "sigScan" - path: scanners/lib/severity.mjs pattern: "SIG"
Step 5: Add AST Python-taint helper and scanner with graceful fallback
- Files:
scanners/lib/py-ast-taint.py(new),scanners/ast-taint-scanner.mjs(new),tests/scanners/ast-taint-scanner.test.mjs(new),tests/fixtures/ast-scan/(new) - Changes: Create
scanners/lib/py-ast-taint.py: reads a target.pypath, runsast.parse(PARSE-ONLY — neverexec/eval/compile/import the target), walks the tree for variable-level taint (sources:os.environ/os.getenv/open/requests/sys.stdin/input; sinks:requests.post/put,exec/eval/subprocess/os.system, file writes), and prints to stdout a single JSON object with this contract:{ "status": "ok", "findings": [ { "rule": "TT3", "severity": "critical", "line": <int>, "source": <str>, "sink": <str>, "message": <str> } ] }; on a parse error it prints{ "status": "error", "message": <str> }and exits non-zero. Createscanners/ast-taint-scanner.mjs: for each.pyindiscovery.files, callspawnSync('python3', [helperPath, file], { encoding:'utf8', timeout: 5000 }). Handle every branch: ENOENT /python3absent → returnscannerResult('ast-taint-scanner', 'skipped', ...); non-zero exit, timeout, or unparseable stdout → record that file as aninfo-level scan note and continue (never throw); valid{status:'ok'}→ map each helper finding tofinding({scanner:'AST', owasp:'LLM01', ...}). Fixtures:creds-net.py(k=os.environ["AWS_SECRET"]; requests.post(url,data=k)),scope.py(inputreused across two functions, only one tainted), andsentinel.py(containsos.system("touch SENTINEL")to prove parse-only safety — the file must never run). - Reuses: scan/return shape (
memory-poisoning-scanner.mjs:386);spawnSyncgraceful-skip precedent (dep-auditor.mjs/git-forensics.mjs);finding/scannerResult(output.mjs). - Test first:
- File:
tests/scanners/ast-taint-scanner.test.mjs(new) - Verifies: (a)
which python3guard, earlyreturnif absent; with python3 → creds→net flagged through the intermediate variable, scopedinputonly flags the tainted use, ids startDS-AST-; (b) python3-absent path →status:'skipped', no crash; (c) parse-only safety: after scanningsentinel.py, assert noSENTINELfile exists; (d) a deliberately malformed.py→ scan completes without throwing. - Pattern:
tests/scanners/vsix-sandbox.test.mjs:27-30(which-guard) +tests/scanners/dep.test.mjs:117-124(skipped envelope)
- File:
- Verify:
node --test tests/scanners/ast-taint-scanner.test.mjs→ expected:fail 0 - On failure: revert —
git checkout -- scanners/lib/py-ast-taint.py scanners/ast-taint-scanner.mjs tests/scanners/ast-taint-scanner.test.mjs && git clean -fd tests/fixtures/ast-scan - Checkpoint:
git commit -m "feat(llm-security): add AST Python-taint scanner with python3 fallback" - Manifest:
manifest: expected_paths: - scanners/lib/py-ast-taint.py - scanners/ast-taint-scanner.mjs - tests/scanners/ast-taint-scanner.test.mjs min_file_count: 3 commit_message_pattern: "^feat\\(llm-security\\): add AST Python-taint scanner with python3 fallback$" bash_syntax_check: [] forbidden_paths: - scanners/scan-orchestrator.mjs must_contain: - path: scanners/lib/py-ast-taint.py pattern: "ast.parse" - path: scanners/ast-taint-scanner.mjs pattern: "'AST'" - path: scanners/ast-taint-scanner.mjs pattern: "timeout"
Step 6: Wire AST into orchestrator, policy, and the four OWASP maps
- Files:
scanners/scan-orchestrator.mjs,scanners/lib/policy-loader.mjs,scanners/lib/severity.mjs - Changes: Add
import { scan as astScan } from './ast-taint-scanner.mjs';and{ name: 'ast', fn: astScan }toSCANNERS. Add a frozenastsection toDEFAULT_POLICY:{ enabled:true, python_path:'python3', timeout_ms:5000 }, read viagetPolicyValue('ast', ...). Add array entries to all four maps:OWASP_MAP.AST=['LLM01','LLM02'],OWASP_AGENTIC_MAP.AST=[],OWASP_SKILLS_MAP.AST=['AST02'],OWASP_MCP_MAP.AST=[]. Add registration assertions (incl.deepEqual(OWASP_MAP.AST, ['LLM01','LLM02'])andenabled:false→skipped) to the AST test. - Reuses: same wiring sites as Step 2.
- Test first:
- File:
tests/scanners/ast-taint-scanner.test.mjs(existing — extend) - Verifies: AST in
SCANNERS;deepEqual(OWASP_MAP.AST, ['LLM01','LLM02']);enabled:falsepolicy short-circuits toskipped. - Pattern:
tests/scanners/workflow-scanner.test.mjs:196-216
- File:
- Verify:
npm test→ expected:fail 0 - On failure: revert —
git checkout -- scanners/scan-orchestrator.mjs scanners/lib/policy-loader.mjs scanners/lib/severity.mjs tests/scanners/ast-taint-scanner.test.mjs - Checkpoint:
git commit -m "feat(llm-security): wire AST scanner into orchestrator and policy" - Manifest:
manifest: expected_paths: - scanners/scan-orchestrator.mjs - scanners/lib/policy-loader.mjs - scanners/lib/severity.mjs min_file_count: 3 commit_message_pattern: "^feat\\(llm-security\\): wire AST scanner into orchestrator and policy$" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: scanners/scan-orchestrator.mjs pattern: "astScan" - path: scanners/lib/severity.mjs pattern: "AST"
Step 7: Docs, scanner inventory, packaging, and full-suite gate
- Files:
CLAUDE.md,docs/scanner-reference.md,scanners/lib/output.mjs,package.json - Changes: Add TRG/SIG/AST rows + update the deterministic-scanner counts in
CLAUDE.mdanddocs/scanner-reference.md(grep current numeric counts and bump each to reflect 3 new scanners). AddTRG/SIG/ASTto the valid-prefix list in thefinding()JSDoc (output.mjs:19). Fix the stale header comment inscanners/scan-orchestrator.mjs("all 7 scanners" → actual count). Add"knowledge/"to thefilesarray inpackage.json(correctness; moot for git-clone distribution). Run the full suite as the integration gate. No version bump here — that is Step 8. - Reuses: existing doc structure in
docs/scanner-reference.md; JSDoc prefix list (output.mjs:19). - Test first: n/a (docs/comment/packaging). Verification = full suite + presence greps.
- Verify:
npm test→ expected:fail 0; thengrep -q '"knowledge/"' package.json && grep -q 'TRG' docs/scanner-reference.md→ expected: both match - On failure: revert —
git checkout -- CLAUDE.md docs/scanner-reference.md scanners/lib/output.mjs scanners/scan-orchestrator.mjs package.json - Checkpoint:
git commit -m "docs(llm-security): document TRG/SIG/AST scanners and package knowledge/" - Manifest:
manifest: expected_paths: - CLAUDE.md - docs/scanner-reference.md - scanners/lib/output.mjs - package.json min_file_count: 4 commit_message_pattern: "^docs\\(llm-security\\): document TRG/SIG/AST scanners and package knowledge/$" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: package.json pattern: "knowledge/" - path: docs/scanner-reference.md pattern: "TRG"
Step 8: Release v7.8.0 — version sync (OPERATOR-GATED, beyond brief scope)
- Files:
.claude-plugin/plugin.json,package.json,README.md,CHANGELOG.md,docs/version-history.md,CLAUDE.md - Changes: Bump
7.7.2 → 7.8.0in.claude-plugin/plugin.jsonandpackage.json; update the README badge (README.md:9); add a[7.8.0]CHANGELOG entry and adocs/version-history.mdv7.8.0 section describing the three scanners; bump theCLAUDE.mdheader version. The brief's success criteria do not require a release — this step exists only because the global version-sync convention demands that if we bump, we bump everywhere. Skip this step if the operator prefers to defer the release. - Reuses: prior version-bump commit as the model (e.g.
3db6b65,b626ba0). - Test first: n/a. Verification = version-consistency grep.
- Verify:
npm testthengrep -rl "7\\.7\\.2" .claude-plugin/plugin.json package.json README.md CHANGELOG.md CLAUDE.md docs/version-history.md→ expected: testsfail 0, grep prints nothing (exit 1 = no stale version left) - On failure: revert —
git checkout -- .claude-plugin/plugin.json package.json README.md CHANGELOG.md docs/version-history.md CLAUDE.md - Checkpoint:
git commit -m "chore(llm-security): v7.8.0 — TRG/SIG/AST scanners" - Manifest:
manifest: expected_paths: - .claude-plugin/plugin.json - package.json - README.md - CHANGELOG.md min_file_count: 4 commit_message_pattern: "^chore\\(llm-security\\): v7\\.8\\.0" bash_syntax_check: [] forbidden_paths: [] must_contain: - path: .claude-plugin/plugin.json pattern: "7.8.0" - path: README.md pattern: "7.8.0"
Alternatives Considered
| Approach | Pros | Cons | Why rejected |
|---|---|---|---|
Fold AST findings into existing taint-tracer.mjs (no new scanner) |
Fewer files | Mixes regex + AST provenance; harder to toggle/skip; muddier reporting | Separate AST prefix keeps provenance and graceful-skip clean |
| Ship a real JS/TS AST parser for #2 | Higher JS recall | Requires an npm dep or vendoring a parser — breaks the zero-dep invariant | Python-first (dep-free via python3); JS AST phased/deferred per brief |
| Use YARA binary as the SIG engine | Mature rulesets | Hard dependency; matches raw bytes only (misses obfuscation) | Pure-Node engine over the decode pipeline is dep-free and catches obfuscated malware; optional yara shell-out can be added later |
Extend permission-mapper.mjs for trigger-abuse instead of a new scanner |
One fewer file | Overloads a permission scanner with activation-surface concerns; harder to policy-gate independently | Dedicated TRG scanner is clearer and independently configurable |
Test Strategy
- Framework:
node --test+node:assert/strict. No lint/coverage gate. - Existing patterns: committed
clean/+poisoned/fixture dirs (modeltests/fixtures/memory-scan/);resetCounter()inbeforeEach; assert onresult.status,findings.length,finding.scanner/severity/id/owasp/evidence; registration tests useassert.deepEqualon the four OWASP maps. - New tests: 3 scanner test files (TRG, SIG, AST), each covering true-positive, false-positive (clean), obfuscation/scope edge cases, parse-only safety (AST), and orchestrator+OWASP registration.
Tests to write
| Type | File | Verifies | Model test |
|---|---|---|---|
| Unit | tests/scanners/trigger-scanner.test.mjs |
broad/baiting/shadow + obfuscated baiting + 4-map registration | tests/scanners/memory-poisoning.test.mjs + workflow-scanner.test.mjs:196 |
| Unit | tests/scanners/signature-scanner.test.mjs |
webshell + base64 variant + clean + family-disable + registration | tests/scanners/memory-poisoning.test.mjs |
| Unit | tests/scanners/ast-taint-scanner.test.mjs |
creds→net via var, scope, python3-absent skip, parse-only safety, malformed-input resilience | tests/scanners/vsix-sandbox.test.mjs + tests/scanners/dep.test.mjs |
Risks and Mitigations
| Priority | Risk | Location | Impact | Mitigation |
|---|---|---|---|---|
| High | AST helper executes untrusted target code | scanners/lib/py-ast-taint.py |
RCE on scan | PARSE-ONLY invariant (ast.parse, never exec/eval/import); sentinel.py fixture + test assert no side-effect file created; 5s spawnSync timeout |
| Medium | AST helper hangs or emits malformed JSON | scanners/ast-taint-scanner.mjs |
Scan stalls/crashes | timeout: 5000; non-zero/timeout/unparseable → info note, never throw; tested with malformed .py |
| Medium | SIG false positives on security tooling / docs | scanners/signature-scanner.mjs |
Noise, eroded trust | Small high-confidence ruleset; path-exclude knowledge//tests//docs/; family toggles in policy |
| Medium | TRG flags legitimate broad utilities | scanners/trigger-scanner.mjs |
False positives | Default MEDIUM advisory; HIGH only on broad-name + universal-claim combo; policy allowlist |
| Medium | python3 absent in CI/user env | scanners/ast-taint-scanner.mjs |
AST coverage lost | Graceful status:'skipped'; regex taint-tracer still runs; both paths tested |
| Low | Wiring edits to shared files conflict if steps reordered | scanners/scan-orchestrator.mjs etc. |
Merge friction | Sequential execution; each wiring step isolated; forbidden_paths fences core steps off shared files |
Assumptions
| # | Assumption | Why unverifiable | Impact if wrong |
|---|---|---|---|
| 1 | node --test per-file output reports fail 0 on success |
Standard node:test behavior | Verify string adjusted to the actual output marker |
| 2 | Brief framework codes for the skills map (TRG→AST04, AST→AST02) match OWASP_SKILLS_MAP's code set |
Skills-map code list not fully enumerated in exploration | Adjust the specific code in Step 2/6 to a valid entry; localized, no structural impact |
Verification
End-to-end checks crossing step boundaries (per-step manifests are checked automatically during execution):
npm test→ expected: all tests pass (fail 0), no regression in the existing 1820+ suitenode scanners/scan-orchestrator.mjs <a sample project> --format json→ expected: JSONresultsincludetrg,sig,astkeys! grep -q '"dependencies"' package.json→ expected: exit 0 (nodependencieskey was introduced — zero-dep invariant holds)node --test tests/scanners/ast-taint-scanner.test.mjswithpython3present and (simulated) absent → expected: both pass; absent path yieldsstatus:'skipped'
Estimated Scope
- Files to modify: ~7 (
scanners/scan-orchestrator.mjs,scanners/lib/policy-loader.mjs,scanners/lib/severity.mjs,scanners/lib/output.mjs,CLAUDE.md,docs/scanner-reference.md,package.json; +6 more in optional Step 8) - Files to create: ~11 (3 scanners, 1 python helper, 1 knowledge JSON, 3 test files, fixture dirs)
- Complexity: medium
Execution Strategy
Three independent scanner tracks, a docs/packaging wrap-up, and an operator-gated release. Core steps
touch only new files (parallel-safe); wiring steps touch shared scanners/scan-orchestrator.mjs /
policy-loader.mjs / severity.mjs and must serialize. Default execution is sequential in brief
order (TRG → SIG → AST → wrap-up → release).
Session 1: TRG scanner
- Steps: 1, 2
- Wave: 1
- Depends on: none
- Scope fence: Touch
scanners/trigger-scanner.mjs, its test+fixtures, shared wiring files (Step 2). Never touch SIG/AST scanner files.
Session 2: SIG scanner
- Steps: 3, 4
- Wave: 1
- Depends on: none (core); wiring serializes after Session 1's wiring
- Scope fence: Touch
scanners/signature-scanner.mjs,knowledge/signatures.json, its test+fixtures, shared wiring files (Step 4). Never touch TRG/AST scanner files.
Session 3: AST scanner
- Steps: 5, 6
- Wave: 1
- Depends on: none (core); wiring serializes after Session 2's wiring
- Scope fence: Touch
scanners/lib/py-ast-taint.py,scanners/ast-taint-scanner.mjs, its test+fixtures, shared wiring files (Step 6). Never touch TRG/SIG scanner files.
Session 4: Docs + packaging
- Steps: 7
- Wave: 2
- Depends on: Sessions 1–3
- Scope fence: Touch docs +
output.mjsJSDoc + orchestrator comment +package.jsonfiles. Never touch scanner logic.
Session 5: Release (operator-gated)
- Steps: 8
- Wave: 3
- Depends on: Session 4
- Scope fence: Version files only.
Execution Order
- Wave 1: Sessions 1, 2, 3 — core steps parallel-safe; wiring steps serialize on shared files
- Wave 2: Session 4 (after Wave 1)
- Wave 3: Session 5 (operator-gated; optional)
Grouping rules applied
- Steps sharing files → same session (each scanner's core + wiring)
- Independent scanners → separate sessions (parallelizable cores)
- Wrap-up and release depend on all scanners → later waves
Plan Quality Score
| Dimension | Weight | Score | Notes |
|---|---|---|---|
| Structural integrity | 0.15 | 88 | core-before-wiring TDD ordering; parallel-safe waves; correct file paths |
| Step quality | 0.20 | 85 | concrete reuse citations; correct imports/arg types post-revision |
| Coverage completeness | 0.20 | 88 | all 3 build items; four OWASP maps; packaging; brief success criteria mapped |
| Specification quality | 0.15 | 85 | no placeholders; AST JSON contract + timeout specified |
| Risk & pre-mortem | 0.15 | 85 | parse-only RCE, timeout, malformed-JSON, python3-absent all handled |
| Headless readiness | 0.10 | 85 | On failure + Checkpoint per step; revert paths corrected |
| Manifest quality | 0.05 | 85 | correct expected/forbidden paths; lenient must_contain |
| Weighted total | 1.00 | 86 | Grade: B |
Adversarial review:
- Plan critic: 3 blockers + 7 majors + 5 minors found on the first draft (score 59/D); all addressed — see Revisions.
- Scope guardian: MIXED — three build items correctly scoped, non-goals respected; OWASP multi-map gap and path/import errors addressed; version bump isolated to operator-gated Step 8.
Revisions
| # | Finding | Severity | Resolution |
|---|---|---|---|
| 1 | Orchestrator path is scanners/scan-orchestrator.mjs, not repo root |
blocker | All references, expected/forbidden paths, and revert commands corrected |
| 2 | Wiring imports used ./scanners/…; siblings import as ./… |
blocker | Imports changed to ./trigger-scanner.mjs etc. |
| 3 | OWASP_MAP values are arrays, asserts must use deepEqual |
blocker | All entries are arrays; tests use assert.deepEqual |
| 4 | Three sibling OWASP maps (AGENTIC/SKILLS/MCP) never updated | major | Steps 2/4/6 wire all four maps; brief's primary frameworks (TRG→AST04, SIG→ASI04) included |
| 5 | knowledge/ not in package.json files |
major | Step 7 adds knowledge/ (noted moot for git-clone distribution) |
| 6 | Zero-dep verify checked a non-existent dependencies object |
major | Verify changed to ! grep -q '"dependencies"' package.json |
| 7 | getPolicyValue 4th arg is projectRoot, not targetPath |
major | Clarified: pass scan-root targetPath (where .llm-security/policy.json lives) |
| 8 | README/CHANGELOG omitted from version bump | major | Folded into operator-gated Step 8 with a full version-consistency grep |
| 9 | Step 7 manifest orphan path + ambiguous "update count" | major | Step 7 rescoped; expected_paths aligned; counts bumped by grep |
| 10 | AST helper JSON contract / timeout / failure path unspecified | major | Step 5 defines the JSON contract, 5s timeout, and non-zero/malformed handling |
| 11 | parseFrontmatter already exported (Assumption #1 pessimistic) |
minor | TRG reuses scanners/lib/yaml-frontmatter.mjs; assumption removed |
| 12 | finding() JSDoc prefix list, orchestrator header comment drift | minor | Updated in Step 7 |
| 13 | must_contain whitespace fragility; verify orchestrator path |
minor | Patterns loosened to tokens; verify uses scanners/scan-orchestrator.mjs |
| 14 | Version bump is beyond brief scope | minor (scope) | Isolated into operator-gated Step 8 |