ktg-plugin-marketplace/plugins/llm-security/commands/scan.md
Kjell Tore Guttormsen 03b8885b6e chore(llm-security): v7.7.2 — language consistency pass
~/.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

8.7 KiB

name description allowed-tools model
security:scan Scan files, directories, or GitHub repos for security issues — secrets, injection vulnerabilities, supply chain risks, OWASP LLM patterns Read, Glob, Grep, Bash, Agent sonnet

/security scan [path|url]

Scan target for security issues. Accepts local paths or GitHub URLs. Delegates to specialized agents sequentially.

Step 1: Resolve Target

  • If $ARGUMENTS contains --deep → strip it, set run_deep_scan = true
  • If $ARGUMENTS contains --branch <name> → strip it, set branch = <name>
  • If $ARGUMENTS is empty → target = ".", clone_path = null
  • If $ARGUMENTS starts with https://github.com/ or git@github.com: → Run: node <plugin-root>/scanners/lib/git-clone.mjs clone "<url>" [--branch <branch>] If exit code != 0 → show error to user and STOP Set clone_path = stdout (trimmed), target = clone_path Set remote_url = <url> for display
  • Otherwise → target = $ARGUMENTS, clone_path = null

IMPORTANT: Cleanup Guarantee (remote scans)

If clone_path != null, the following cleanup MUST run regardless of scan outcome. If ANY step between clone and cleanup fails or errors, STILL run cleanup before stopping:

  1. node <plugin-root>/scanners/lib/git-clone.mjs cleanup "<clone_path>"
  2. node <plugin-root>/scanners/lib/fs-utils.mjs cleanup "<evidence_file>" (if evidence_file is set)

Step 1.5: Pre-extraction (remote scans only)

If clone_path != null (target is a cloned remote repo): Get temp path: node <plugin-root>/scanners/lib/fs-utils.mjs tmppath "content-extract.json" Run: node <plugin-root>/scanners/content-extractor.mjs "<target>" --output-file "<evidence_file>" If exit code != 0 → warn user, set evidence_file = null (fall back to direct scan) Otherwise set evidence_file = the temp path. Print the compact summary line to user.

Step 2: Detect Scan Type

Single .md file: run_skill_scan = true, run_mcp_scan = false

Directory: Glob for **/commands/*.md, **/agents/*.md, **/skills/*/SKILL.mdrun_skill_scan = true. Glob for **/.mcp.json, **/package.json, **/.claude/settings.json with mcpServers → run_mcp_scan = true. Neither → skill scan only.

Record ISO 8601 timestamp.

Step 3: Plugin Root

This file is at <plugin-root>/commands/scan.md. Use absolute paths for knowledge files.

Step 3.5: Registry Check (local scans only)

If clone_path == null (local scan) and run_skill_scan == true:

node -e "
import { fingerprintSkill, checkRegistry } from '<plugin-root>/scanners/lib/skill-registry.mjs';
const r = fingerprintSkill('<target>');
const c = checkRegistry(r.fingerprint, '<plugin-root>');
console.log(JSON.stringify({ fingerprint: r.fingerprint, name: r.name, files: r.files, ...c }));
" --input-type=module

If found == true and stale == false: display cached result and set skip_skill_scan = true:

**Registry hit:** <name> (fingerprint: <first 12 chars>)
Verdict: <verdict> | Risk: <score>/100 | Last scanned: <date> | Scans: <count>
(Use `/security registry scan <target>` to force re-scan)

Otherwise set skip_skill_scan = false and store registry_fingerprint and registry_name for post-scan registration.

Step 4: Spawn Agents Sequentially

Use registered subagent types — they contain full scan procedures as system prompt.

Skill Scanner (if run_skill_scan = true AND skip_skill_scan != true): subagent_type: "llm-security:skill-scanner-agent", model: "sonnet":

If evidence_file is set (remote scan — evidence-package mode):

EVIDENCE-PACKAGE MODE. Read the pre-extracted evidence at: <evidence_file> Read knowledge: <plugin-root>/knowledge/skill-threat-patterns.md, <plugin-root>/knowledge/secrets-patterns.md Analyze the JSON sections: injection_findings, frontmatter_inventory, shell_commands, credential_references, persistence_signals, claude_md_analysis, cross_instruction_flags. DO NOT use Read/Glob/Grep on the target directory — all evidence is in the package. [INJECTION-PATTERN-STRIPPED] markers are confirmed findings — report them. Return findings with severity, category, file, line, OWASP ref, evidence, remediation. End with JSON: {"scanner":"skill-scanner","verdict":"ALLOW|WARNING|BLOCK","risk_score":N,"counts":{"critical":0,"high":0,"medium":0,"low":0,"info":0},"files_scanned":N}

Otherwise (local scan — direct mode):

Scan target: <target> Read: <plugin-root>/knowledge/skill-threat-patterns.md, <plugin-root>/knowledge/secrets-patterns.md Return findings with severity, category, file, line, OWASP ref, evidence, remediation. End with JSON: {"scanner":"skill-scanner","verdict":"ALLOW|WARNING|BLOCK","risk_score":N,"counts":{"critical":0,"high":0,"medium":0,"low":0,"info":0},"files_scanned":N}

MCP Scanner (if run_mcp_scan = true, run AFTER skill scanner): subagent_type: "llm-security:mcp-scanner-agent", model: "sonnet":

If evidence_file is set (remote scan — evidence-package mode):

EVIDENCE-PACKAGE MODE. Read the pre-extracted evidence at: <evidence_file> Read: <plugin-root>/knowledge/mcp-threat-patterns.md Analyze: mcp_tool_descriptions (check hidden instructions, length >500, injection_detected), shell_commands, credential_references. DO NOT use Read/Glob/Grep on the target directory. Return findings with severity, category, evidence, remediation. End with JSON: {"scanner":"mcp-scanner","verdict":"ALLOW|WARNING|BLOCK","risk_score":N,"counts":{"critical":0,"high":0,"medium":0,"low":0,"info":0},"files_scanned":N}

Otherwise (local scan — direct mode):

Scan target: <target> Read: <plugin-root>/knowledge/mcp-threat-patterns.md Return findings with severity, category, server name, evidence, remediation. End with JSON: {"scanner":"mcp-scanner","verdict":"ALLOW|WARNING|BLOCK","risk_score":N,"counts":{"critical":0,"high":0,"medium":0,"low":0,"info":0},"files_scanned":N}

Step 5: Aggregate and Report

Combine counts. risk_score = riskScore(counts) (severity-dominated v2 model — see scanners/lib/severity.mjs). Verdict: critical ≥ 1 OR score ≥ 65 → BLOCK; high ≥ 1 OR score ≥ 15 → WARNING; else ALLOW.

Output banner then all findings grouped by severity (critical→info). Each finding: ### [SEV] Title with Category, File:line, OWASP, Evidence, Remediation.

For TFA (Toxic Flow Analysis) findings, render the chain description prominently:

  • Show the 3 trifecta legs (Input, Access, Exfil) with their evidence
  • Note mitigation status (which hooks are active)
  • Group direct trifectas separately from cross-component trifectas

Step 5.5: Register in Skill Registry (local scans only)

If clone_path == null and skip_skill_scan != true and registry_fingerprint is set:

node -e "
import { registerScan } from '<plugin-root>/scanners/lib/skill-registry.mjs';
registerScan({
  skillPath: '<target>',
  fingerprint: '<registry_fingerprint>',
  name: '<registry_name>',
  files: <registry_files_json>,
  verdict: '<computed_verdict>',
  risk_score: <computed_risk_score>,
  counts: <computed_counts_json>,
  files_scanned: <files_scanned>
}, '<plugin-root>');
" --input-type=module

Step 6: Deep Scan (only if --deep)

If run_deep_scan = true, run /security deep-scan <target> logic: Get temp path, run node <plugin-root>/scanners/scan-orchestrator.mjs "<target>" --output-file "<tmp>". Parse stdout aggregate JSON. Merge with LLM findings. Re-evaluate verdict. Output "Deep Scan Findings" section with CRITICAL/HIGH only.

Step 7: Cleanup (only if remote)

If clone_path != null: Run: node <plugin-root>/scanners/lib/git-clone.mjs cleanup "<clone_path>" If cleanup fails → warn: "Could not remove temp dir <clone_path> — remove manually."

If evidence_file != null: Run: node <plugin-root>/scanners/lib/fs-utils.mjs cleanup "<evidence_file>"

Step 8: HTML Report

After producing the markdown report (Step 5) and any cleanup (Step 7):

  1. Compute a temp markdown path:

    node -p "require('path').join(require('os').tmpdir(), 'sec-scan-' + Date.now() + '.md')"
    
  2. Use the Write tool to save the entire markdown report you just produced (banner + all findings sections + any Deep Scan section) to that temp path. Do not summarize — write it verbatim.

  3. Run the renderer:

    node <plugin-root>/scripts/render-report.mjs scan --in "<temp-md-path>"
    

    The CLI writes reports/scan-<YYYYMMDD-HHmmss>.html relative to CWD and prints file:///abs/path.html on stdout (one line).

  4. Append this to your response (markdown link, no bare URL):

    HTML report: Open in browser

If the CLI exits non-zero, mention the error but do not block — the markdown report above is the primary deliverable.