@shomra/agent 0.3.17 → 0.3.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +57 -57
  3. package/package.json +3 -9
  4. package/shomra.mjs +9 -7168
  5. package/src/agents/hook-command.mjs +19 -0
  6. package/src/agents/hook-files.mjs +41 -0
  7. package/src/agents/installers.mjs +203 -0
  8. package/src/artifacts/matchers.mjs +59 -0
  9. package/src/artifacts/report.mjs +50 -0
  10. package/src/cli/flags.mjs +68 -0
  11. package/src/cli/help-sections.mjs +309 -0
  12. package/src/cli/help.mjs +27 -0
  13. package/src/cli/main.mjs +55 -0
  14. package/src/cli/registry.mjs +80 -0
  15. package/src/cli/suggestions.mjs +33 -0
  16. package/src/commands/add.mjs +149 -0
  17. package/src/commands/agent-identity.mjs +46 -0
  18. package/src/commands/check.mjs +194 -0
  19. package/src/commands/corpus.mjs +126 -0
  20. package/src/commands/design.mjs +168 -0
  21. package/src/commands/doctor.mjs +209 -0
  22. package/src/commands/fix.mjs +115 -0
  23. package/src/commands/gate.mjs +154 -0
  24. package/src/commands/git-hooks.mjs +163 -0
  25. package/src/commands/init.mjs +36 -0
  26. package/src/commands/install-hook.mjs +51 -0
  27. package/src/commands/llm-proxy.mjs +153 -0
  28. package/src/commands/mcp-add.mjs +185 -0
  29. package/src/commands/mcp.mjs +143 -0
  30. package/src/commands/memory-scan.mjs +181 -0
  31. package/src/commands/model-scan.mjs +99 -0
  32. package/src/commands/models.mjs +145 -0
  33. package/src/commands/new.mjs +64 -0
  34. package/src/commands/plan.mjs +87 -0
  35. package/src/commands/pr.mjs +249 -0
  36. package/src/commands/protect.mjs +38 -0
  37. package/src/commands/provenance.mjs +91 -0
  38. package/src/commands/redteam.mjs +166 -0
  39. package/src/commands/rules.mjs +220 -0
  40. package/src/commands/run.mjs +128 -0
  41. package/src/commands/scan-zip.mjs +118 -0
  42. package/src/commands/scan.mjs +102 -0
  43. package/src/commands/secrets.mjs +99 -0
  44. package/src/commands/status.mjs +50 -0
  45. package/src/commands/why.mjs +88 -0
  46. package/src/core/api-client.mjs +66 -0
  47. package/src/core/api-key.mjs +6 -0
  48. package/src/core/circuit-breaker.mjs +42 -0
  49. package/src/core/config.mjs +37 -0
  50. package/src/core/exit-codes.mjs +9 -0
  51. package/src/core/json-file.mjs +13 -0
  52. package/src/core/numbers.mjs +4 -0
  53. package/src/core/package-root.mjs +10 -0
  54. package/src/core/terminal.mjs +16 -0
  55. package/src/core/version.mjs +14 -0
  56. package/src/core/wire-limits.mjs +53 -0
  57. package/src/corpus/screening.mjs +127 -0
  58. package/{ai-usage.mjs → src/detect/ai-usage.mjs} +0 -27
  59. package/src/detect/code-sast.mjs +2 -0
  60. package/{design.mjs → src/detect/design.mjs} +17 -106
  61. package/src/detect/guard-signals.mjs +18 -0
  62. package/{model-refs.mjs → src/detect/model-refs.mjs} +18 -77
  63. package/src/detect/sast/chains.mjs +30 -0
  64. package/src/detect/sast/path-expressions.mjs +76 -0
  65. package/src/detect/sast/rules-chains.mjs +33 -0
  66. package/src/detect/sast/rules-config.mjs +51 -0
  67. package/src/detect/sast/rules-javascript.mjs +109 -0
  68. package/src/detect/sast/rules-python.mjs +292 -0
  69. package/src/detect/sast/scanner.mjs +104 -0
  70. package/src/detect/sast/source-lines.mjs +115 -0
  71. package/src/detect/sast/taint.mjs +71 -0
  72. package/src/detect/signals/artifacts.mjs +113 -0
  73. package/src/detect/signals/autonomy.mjs +55 -0
  74. package/src/detect/signals/config-markers.mjs +28 -0
  75. package/src/detect/signals/credential-harvest.mjs +64 -0
  76. package/src/detect/signals/durable-claims.mjs +73 -0
  77. package/src/detect/signals/egress.mjs +56 -0
  78. package/src/detect/signals/execution-hijack.mjs +128 -0
  79. package/src/detect/signals/gate.mjs +91 -0
  80. package/src/detect/signals/injection.mjs +55 -0
  81. package/src/detect/signals/lines.mjs +42 -0
  82. package/src/detect/signals/masking.mjs +99 -0
  83. package/src/detect/signals/memory.mjs +357 -0
  84. package/src/detect/signals/packages.mjs +45 -0
  85. package/src/detect/signals/propagation.mjs +86 -0
  86. package/src/detect/signals/prose-context.mjs +82 -0
  87. package/src/detect/signals/scan.mjs +91 -0
  88. package/src/detect/signals/secrets.mjs +85 -0
  89. package/src/detect/signals/sensitive.mjs +9 -0
  90. package/src/detect/signals/severity.mjs +10 -0
  91. package/src/detect/signals/shell.mjs +96 -0
  92. package/src/detect/signals/staged-fetch.mjs +66 -0
  93. package/src/detect/signals/text-match.mjs +35 -0
  94. package/src/gate/batch.mjs +157 -0
  95. package/src/gate/environment.mjs +122 -0
  96. package/src/gate/repo-policy.mjs +65 -0
  97. package/src/gate/result.mjs +53 -0
  98. package/src/gate/sarif.mjs +33 -0
  99. package/src/gate/sast.mjs +64 -0
  100. package/src/gate/suppressions.mjs +0 -0
  101. package/src/guard/classify.mjs +50 -0
  102. package/src/guard/emit.mjs +51 -0
  103. package/src/guard/ignore.mjs +24 -0
  104. package/src/guard/ledger.mjs +112 -0
  105. package/src/guard/model-load.mjs +50 -0
  106. package/src/guard/normalize.mjs +77 -0
  107. package/src/guard/options.mjs +10 -0
  108. package/src/guard/prompt-guard.mjs +184 -0
  109. package/src/guard/report.mjs +35 -0
  110. package/src/guard/result-guard.mjs +140 -0
  111. package/src/guard/tool-guard.mjs +166 -0
  112. package/src/inventory/agent-artifacts.mjs +5 -0
  113. package/src/inventory/agent-posture.mjs +249 -0
  114. package/src/inventory/artifacts/classify.mjs +27 -0
  115. package/src/inventory/artifacts/discover.mjs +187 -0
  116. package/src/inventory/artifacts/file-read.mjs +42 -0
  117. package/src/inventory/artifacts/hooks.mjs +14 -0
  118. package/src/inventory/artifacts/limits.mjs +37 -0
  119. package/src/inventory/artifacts/marketplaces.mjs +45 -0
  120. package/src/inventory/artifacts/roots.mjs +20 -0
  121. package/src/inventory/artifacts/walk.mjs +36 -0
  122. package/src/inventory/discovery/ai-dependencies.mjs +161 -0
  123. package/src/inventory/discovery/ai-tools.mjs +23 -0
  124. package/src/inventory/discovery/all.mjs +40 -0
  125. package/src/inventory/discovery/coding-agents.mjs +77 -0
  126. package/src/inventory/discovery/fs-read.mjs +36 -0
  127. package/src/inventory/discovery/local-runtimes.mjs +53 -0
  128. package/src/inventory/discovery/mcp-clients.mjs +67 -0
  129. package/src/inventory/discovery/mcp-servers.mjs +78 -0
  130. package/src/inventory/discovery/model-keys.mjs +97 -0
  131. package/src/inventory/discovery/platform.mjs +16 -0
  132. package/src/inventory/discovery/rules-files.mjs +25 -0
  133. package/src/inventory/discovery/vector-stores.mjs +176 -0
  134. package/src/inventory/discovery/workspace.mjs +124 -0
  135. package/src/inventory/discovery.mjs +10 -0
  136. package/src/mcp/child-process.mjs +50 -0
  137. package/src/mcp/config-wrapping.mjs +75 -0
  138. package/src/mcp/connect-gate.mjs +45 -0
  139. package/src/mcp/hosts.mjs +16 -0
  140. package/src/mcp/jsonrpc.mjs +48 -0
  141. package/src/mcp/lookup.mjs +50 -0
  142. package/src/mcp/screening.mjs +103 -0
  143. package/src/mcp/server-tools.mjs +97 -0
  144. package/src/mcp/server.mjs +102 -0
  145. package/src/mcp/shim.mjs +205 -0
  146. package/src/models/lookup.mjs +79 -0
  147. package/src/models/references.mjs +103 -0
  148. package/src/rules/context.mjs +98 -0
  149. package/src/rules/generate.mjs +103 -0
  150. package/src/rules/sections.mjs +145 -0
  151. package/src/scaffold/agent-project.mjs +185 -0
  152. package/src/scaffold/artifact-templates.mjs +35 -0
  153. package/code-sast.mjs +0 -1063
  154. package/discovery.mjs +0 -977
  155. package/guard-ledger.mjs +0 -239
  156. package/guard-signals.mjs +0 -2055
@@ -0,0 +1,309 @@
1
+ import { LLM_PROVIDERS } from '../commands/llm-proxy.mjs';
2
+ import { bold, cyan, dim, green } from '../core/terminal.mjs';
3
+ import { VERSION } from '../core/version.mjs';
4
+
5
+ const BANNER = () => `
6
+ ${bold(cyan('Shomra'))} ${dim('- adversarial assurance for AI agents · v' + VERSION)}
7
+ `;
8
+
9
+ const USAGE = () => `${bold('USAGE')}
10
+ shomra <command> [options]
11
+ `;
12
+
13
+ const MODES = () => `${bold('MODES')} ${dim('- local-first: everything that can run on your machine does, with no account')}
14
+ ${cyan('Local')} ${dim('(no key)')} check · gate · doctor · protect · design · plan · corpus · rules · add · secrets · models · new · mcp
15
+ ${dim('Fully on-machine. Nothing leaves your machine. Your lead-in - no signup.')}
16
+ ${green('Enrolled')} ${dim('(shm_live_)')} adds org policy, AI ${bold('fix')}/${bold('why')}, deep scans (zip/model/memory) & the dashboard
17
+ ${green('CI')} ${dim('(shm_ci_)')} scoped, revocable pipeline key for ${bold('pr')} / ${bold('check')} in CI
18
+ ${dim('Enroll with')} ${bold('shomra init --key shm_…')}${dim('; generate keys in the platform → Settings → API Keys.')}
19
+ `;
20
+
21
+ const COMMANDS = () => `${bold('COMMANDS')}
22
+ ${dim('Daily - the verbs you live in')}
23
+ ${cyan('check')} ${bold('Is my repo safe?')} Gate every AI artifact ${dim('[dir] [--staged|--changed] [--fix] [--strict] [--json]')}
24
+ ${cyan('fix')} Remediate an artifact in place (AI) ${dim('<file> [--apply] [--kind …] [--json]')}
25
+ ${cyan('why')} Explain a finding + false-positive read ${dim('<file> [--kind …] [--json]')}
26
+ ${cyan('gate')} Vet ONE AI artifact before install ${dim('<file> [--kind …] [--strict] [--json] · --all for a whole repo (CI)')}
27
+ ${cyan('scan')} Discover AI tooling on this machine ${dim('[--report] [--json] [--path <dir>]')}
28
+ ${cyan('report')} Discover + send inventory to your Shomra org ${dim('(alias: scan --report) [--json]')}
29
+ ${cyan('status')} Show config, enrollment + firewall health
30
+ ${cyan('run')} ${bold('Run a whole assurance playbook')} ${dim('<id> [--input k=v]… [--project <id>] [--json] · --list for the catalog')}
31
+ ${dim('scan → red-team → harden → compliance → gate, as one command. Exits')}
32
+ ${dim('non-zero when a gate holds, so a pipeline can block the release.')}
33
+
34
+ ${dim('Setup - run once per machine / repo')}
35
+ ${cyan('init')} Configure + enroll this machine ${dim('--key shm_live_… [--url <backend>]')}
36
+ ${cyan('protect')} Wire the runtime firewall for every coding agent ${dim('[--local] [--force]')}
37
+ ${cyan('install-hook')} Wire the runtime firewall into ONE agent ${dim('[--agent claude|cursor|windsurf|gemini|codex|copilot|cline|aider|all] [--global]')}
38
+ ${cyan('provenance')} Which changed files an AI agent wrote ${dim('[--staged | --base main] [--trailer] [--fail-on-blocked] [--json]')}
39
+ ${cyan('install-precommit')} Gate staged AI artifacts on git commit ${dim('[dir] [--force] · --pre-receive for the un-skippable server-side hook')}
40
+ ${cyan('doctor')} ${bold('Am I safe?')} Posture of this machine's AI setup ${dim('[--json]')}
41
+
42
+ ${dim('Prevention - get in front of the model, not just behind it')}
43
+ ${cyan('design')} ${bold('Threat-model a system before it exists')} ${dim('<file|dir|-> [--save --subject KIND:id] [--checklist] [--strict] [--json]')}
44
+ ${dim('Reads an RFC / design doc / ticket and says whether it closes a path from')}
45
+ ${dim('untrusted input to a consequence, plus what must be true before it ships.')}
46
+ ${dim('Pipe a ticket straight in: ')}${bold('gh issue view 42 --json body -q .body | shomra design -')}
47
+ ${cyan('plan')} ${bold('Threat-model what an agent is about to build')} ${dim('<file|-> [--strict] [--json]')}
48
+ ${dim('Same engine as design, on the agent\'s own plan. Also an MCP tool')}
49
+ ${dim('(')}${bold('shomra_review_plan')}${dim(') so every agent can call it mid-task, and a hook.')}
50
+ ${cyan('corpus')} ${bold('Screen RAG documents before they are indexed')} ${dim('<dir|file> [--chunk-size N] [--manifest <f>] [--strict] [--json]')}
51
+ ${dim('A poisoned doc never enters the store. Reports the CHUNK a payload would')}
52
+ ${dim('land in, and counts every file it could not read as NOT covered.')}
53
+ ${cyan('add')} ${bold('Vet anything BEFORE it lands')} ${dim('mcp|skill|model|package <ref> [--force] [--strict] [--json]')}
54
+ ${dim('One gate for every acquisition channel an agent has.')}
55
+ ${cyan('rules')} ${bold('Teach the agent what gets blocked')} ${dim('[dir] [--write] [--check] [--agent claude,codex,cursor,gemini,copilot,windsurf,cline|all] [--json]')}
56
+ ${dim('Compiles what Shomra enforces + what this repo already trips into CLAUDE.md /')}
57
+ ${dim('AGENTS.md / .cursor/rules / copilot-instructions, inside a managed block that never')}
58
+ ${dim('touches your own text. --check fails CI when it goes stale.')}
59
+ ${cyan('mcp install')} Register Shomra AS an MCP server with your agents ${dim('[--agent claude,cursor,gemini,windsurf|all] [--global]')}
60
+ ${dim('Lets the model call ')}${bold('shomra_review_change')}${dim(' on content BEFORE it writes it.')}
61
+
62
+ ${dim('CI & repo hygiene')}
63
+ ${cyan('pr')} Review a PR - inline findings on the diff ${dim('(CI) [--init] [--strict] [--dry-run]')}
64
+ ${cyan('baseline')} Accept current findings; only NEW ones fail ${dim('[dir]')}
65
+ ${cyan('secrets')} Scan working tree + git history for leaked keys ${dim('[dir] [--history] [--depth N]')}
66
+ ${cyan('models')} Find models the code loads + look up known vulns ${dim('[dir] [--strict] [--dry-run]')}
67
+
68
+ ${dim('Build safely')}
69
+ ${cyan('new')} Scaffold a secure-by-default artifact ${dim('skill|command|subagent|agent-card|mcp|rules [name]')}
70
+ ${cyan('new agent')} Scaffold a whole agent project that starts compliant ${dim('[name] [--framework vercel-ai]')}
71
+ ${cyan('mcp add')} Vet an MCP server, then add it to a config ${dim('<name> <command…>|--url <url> [--config <f>] [--force]')}
72
+ ${cyan('mcp list')} List the MCP servers in a config ${dim('[--config <f>] [--json]')}
73
+ ${cyan('mcp serve')} Run Shomra AS an MCP server so agents call its checks ${dim('(review_change/rules/check/scan_models/fix/explain)')}
74
+
75
+ ${dim('Governance & advanced')} ${dim('→')} ${bold('shomra admin')} ${dim('for the full list')}
76
+ ${cyan('admin')} Deep scans, red-team, hardening, agent identity, LLM proxy
77
+ ${dim('scan-zip · model-scan · memory-scan · redteam · campaign · harden · agent-identity · llm-proxy')}
78
+
79
+ ${dim('(internal hook handlers, invoked by install-hook - not run by hand: tool-guard, result-guard, prompt-guard, plan-guard)')}
80
+ `;
81
+
82
+ const GATE = () => `${bold('GATE')}
83
+ Checks an MCP config / Skill / slash command / hook / rules file BEFORE it
84
+ lands on the machine. Exit 0 = allowed, 1 = blocked (2 = flagged with --strict)
85
+ - wire it into pre-commit or CI. Nothing is executed; analysis is static.
86
+
87
+ ${bold('Works offline.')} Real static analysis (dangerous shell, prompt injection,
88
+ secrets, exfil sinks, over-permissioned tool grants, install-lure prose) runs
89
+ ON-MACHINE, so ${bold('gate')} returns a genuine verdict with no backend and no key.
90
+ When enrolled + reachable, the backend layers your ORG POLICY + governance on
91
+ top. If the backend is down it falls back to the local verdict (and says so);
92
+ ${bold('--strict')} instead fails closed (exit 1) because org policy couldn't be verified.
93
+
94
+ ${bold('--all')} walks a repo/dir and gates every AI artifact at once - drop it in
95
+ a CI job to fail the build on risky artifacts. CI environment (provider, repo,
96
+ branch, commit) is auto-detected and recorded for local-vs-CI gate activity.
97
+ `;
98
+
99
+ const CHECK = () => `${bold('CHECK')} ${dim('- the one command a developer runs')}
100
+ ${bold('shomra check')} answers "is my repo safe?" in one shot: it finds every AI
101
+ artifact in the tree (MCP configs, Skills, slash commands, hooks, rules files)
102
+ and gates them together, ${bold('local-first')} - a real on-machine verdict with no
103
+ backend or key; enrolling layers your org policy on top. It is ${bold('gate --all')}
104
+ with dev ergonomics:
105
+ ${dim('shomra check')} every AI artifact under the repo
106
+ ${dim('shomra check --staged')} only what's git-staged ${dim('(wire into pre-commit / on-save)')}
107
+ ${dim('shomra check --changed')} only what changed vs HEAD
108
+ ${dim('shomra check --fix')} gate, then remediate what isn't clean, in place
109
+ ${dim('shomra check --json')} machine-readable - what an IDE extension calls
110
+ ${dim('shomra check --sarif')} SARIF 2.1.0 - upload for native GitHub/GitLab PR annotations
111
+ Exit 0 = clean, 1 = blocked, 2 = flagged with --strict.
112
+ `;
113
+
114
+ const BASELINE_AND_SUPPRESSION = () => `${bold('BASELINE & SUPPRESSION')} ${dim('- adopt on a messy repo; silence a false positive')}
115
+ ${bold('shomra baseline')} records the current findings as accepted (.shomra/baseline.json,
116
+ line-independent) so only findings introduced AFTER it fail - commit it to share
117
+ with the team. Silence individual findings three ways:
118
+ ${dim('.shomraignore')} a repo file: ${dim('path/glob')} (skip file) or ${dim('path/glob :: title-substring')}
119
+ ${dim('inline comment')} ${bold('// shomra-ignore')} / ${bold('# shomra-ignore')} on the finding's line or the one above
120
+ ${dim('whole file')} ${bold('shomra-ignore-file')} in the first lines (works in JSON too)
121
+ Any suppression re-grades the artifact, so a fully-suppressed file drops to ALLOW.
122
+ ${dim('--no-suppress')} ignores all of the above; ${dim('--no-baseline')} ignores just the baseline.
123
+ `;
124
+
125
+ const POLICY_AS_CODE = () => `${bold('POLICY-AS-CODE')} ${dim('- team gate rules, versioned in the repo')}
126
+ ${bold('.shomra/policy.yml')} (or .json) sets your team's thresholds, reviewed in PRs:
127
+ ${dim('block: high')} min severity that BLOCKS ${dim('(critical|high|medium|low|none)')}
128
+ ${dim('flag: medium')} min severity that FLAGS
129
+ ${dim('allow: ["IPv4 address"]')} finding titles to always downgrade
130
+ For a local verdict the repo policy fully re-grades; when the backend returned an
131
+ org decision it can only make it STRICTER (worst-wins). ${dim('--no-policy')} skips it.
132
+ `;
133
+
134
+ const FIX = () => `${bold('FIX')} ${dim('- remediate without leaving your editor')}
135
+ ${bold('shomra fix <file>')} generates a MINIMAL fix for whatever the gate flags in
136
+ that artifact and shows it as a unified diff; ${bold('--apply')} writes it back to the
137
+ local file. The fix is produced on the platform with your org's AI key (so no
138
+ provider key sits on the dev machine) - enrollment is required. When the
139
+ server has no AI configured it degrades to printing the deterministic
140
+ remediation guidance to apply by hand. Nothing is committed or pushed; the
141
+ edit lands in your working tree for you to review and commit.
142
+ `;
143
+
144
+ const WHY = () => `${bold('WHY')} ${dim('- decide if a finding is real')}
145
+ ${bold('shomra why <file>')} is the developer shape of "investigate": for each finding
146
+ it gives a plain-English why-it-matters, a one-line exploit scenario, and an
147
+ honest true/false-positive read - the conclusion, not a tool-call timeline.
148
+ AI-distilled when enrolled; offline it prints the on-machine findings and their
149
+ fixes. Use it when the gate flags something you think is a false positive.
150
+ `;
151
+
152
+ const INSTALL_PRECOMMIT = () => `${bold('INSTALL-PRECOMMIT')}
153
+ ${bold('shomra install-precommit')} writes a ${dim('.git/hooks/pre-commit')} that runs
154
+ ${bold('check --staged')}, so a risky MCP config / skill / rules file is caught before
155
+ it commits. A BLOCK stops the commit; flags warn but don't. Existing hooks are
156
+ never clobbered (it tells you the one line to add, or ${bold('--force')} replaces with
157
+ a backup). Override a single commit with ${bold('git commit --no-verify')}.
158
+ `;
159
+
160
+ const MODEL_SCAN = () => `${bold('MODEL-SCAN')}
161
+ Runs SAST over a public AI model's SOURCE - the custom .py files transformers
162
+ imports under trust_remote_code and the config.json/tokenizer that bind them.
163
+ Flags eval/exec/os.system/subprocess, pickle/torch.load deserialization,
164
+ __reduce__ gadgets, network egress and auto_map (AutoModel/AutoTokenizer)
165
+ usage, each with a rule id, file:line and code snippet. Weights are never
166
+ downloaded and nothing is executed. Findings land in your Shomra dashboard.
167
+ `;
168
+
169
+ const MEMORY_SCAN = () => `${bold('MEMORY-SCAN')}
170
+ Persistent agent memory (MEMORY.md, .claude/memory/…, mem0/letta stores) AND
171
+ rules/instruction files (CLAUDE.md, AGENTS.md, .cursorrules, copilot-instructions,
172
+ …) are re-fed to the model as trusted context every session - so a single
173
+ poisoned entry (OWASP ASI06 / the MemoryTrap class) persists across sessions and
174
+ reboots. Rules files are graded against an instruction baseline (standing
175
+ directives are legitimate there; only hijack / conceal-from-user / staged-payload
176
+ / exfil phrasing is poison), memory against a fact baseline. memory-scan reports
177
+ each write (with provenance) so Shomra can track drift from an approved baseline
178
+ and roll back a poisoned store. Once ${bold('install-hook')} is wired, the agent's own
179
+ memory and rules-file writes are captured automatically. Analysis is static.
180
+ `;
181
+
182
+ const REDTEAM = () => `${bold('REDTEAM')}
183
+ Replays a library of adversarial scenarios (goal hijack, indirect injection,
184
+ system-prompt leak, data exfil, tool escalation, jailbreak, secret extraction,
185
+ memory poisoning) against your OWN LLM Guard (in probe mode - never logged as a
186
+ real attack) or model, scores a resilience %, and flags REGRESSIONS vs the last
187
+ run. Authorized testing of your own stack; nothing is executed and no attack
188
+ leaves the platform. Add ${bold('--evolve')} to turn on the evolutionary attacker: a
189
+ population-based genetic search that breeds evasive variants (obfuscation,
190
+ encoding, wrapping, splitting) against any scenario the fixed set can't crack,
191
+ learning what beats YOUR guard and opening with it next time. Works with AI on
192
+ or off. In CI, gate the pipeline with ${bold('--min <resilience>')} and/or
193
+ ${bold('--fail-on-regression')} (exit 2 fails the build). Run it on a schedule so a model
194
+ or policy change can't silently weaken a defense.
195
+ `;
196
+
197
+ const HARDEN = () => `${bold('HARDEN')}
198
+ The self-hardening flywheel - turns a red-team breach into a defense. Runs a
199
+ red-team (or reuses one with ${bold('--run <id>')}), asks Shomra to propose high-precision
200
+ detection signatures for whatever got through, and VERIFIES each against a
201
+ benign corpus: a candidate must catch the attack AND fire on zero legitimate
202
+ messages, so the guard can only ever get tighter. With ${bold('--apply')} the survivors
203
+ go live as a signature pack - no redeploy - and a confirmation re-run proves
204
+ the resilience lift. Without AI configured it still works, mining signatures
205
+ deterministically from the breaching prompts. Pair it with ${bold('redteam')} in CI.
206
+ `;
207
+
208
+ const AGENT_IDENTITY = () => `${bold('AGENT IDENTITY')}
209
+ Give each non-human agent a first-class identity with a least-privilege
210
+ capability policy - which providers/models it may call, which tools / MCP
211
+ servers it may invoke, whether it may run shell. ${bold('agent-identity register')} mints
212
+ its shm_agt_ credential; export ${bold('SHOMRA_AGENT')} so ${bold('llm-proxy')} and the runtime
213
+ firewall present it, and every call is authorized against its policy at the two
214
+ runtime chokepoints (identity axis) on top of content screening. Govern,
215
+ approve break-glass requests, and revoke (a live kill-switch) in the dashboard
216
+ → Agent Identities. Unknown agents are auto-discovered there for visibility.
217
+ `;
218
+
219
+ const LLM_PROXY = () => `${bold('LLM-PROXY')}
220
+ Runs a local guard in front of your LLM providers. Point your SDK's base URL
221
+ at it (OPENAI_BASE_URL / ANTHROPIC_BASE_URL, the Google GenAI base URL, or any
222
+ OpenAI-compatible SDK's baseURL) - every prompt and completion is screened
223
+ against your org's policies; violations are blocked with HTTP 403 and logged
224
+ to the LLM Guard dashboard. Supported providers:
225
+ ${dim(LLM_PROVIDERS.join(' · '))}
226
+ openai + the OpenAI-compatible ones share the /<provider>/v1 path shape;
227
+ anthropic and gemini use their own (/anthropic, /gemini).
228
+ `;
229
+
230
+ const RUNTIME_FIREWALL = () => `${bold('RUNTIME FIREWALL (multi-agent)')}
231
+ ${bold('shomra install-hook')} wires Shomra into a coding agent's own hook system so
232
+ it screens both channels - the pre-tool-call hook BEFORE a shell command,
233
+ artifact write (adding an MCP/skill/command/hook/rules file), or MCP call
234
+ runs, and the post-tool-call hook that screens content (WebFetch/Read/MCP
235
+ responses) coming BACK into the agent's context for prompt injection, exfil
236
+ sinks, and hidden payloads before the model acts on them.
237
+
238
+ Default target is Claude Code (unchanged for existing installs). Add
239
+ ${bold('--agent <name>')} (comma-separated, or ${bold('all')}) to also wire in:
240
+ ${dim('claude')} (Claude Code) · ${dim('cursor')} (Cursor) · ${dim('windsurf')} (Windsurf/Cascade) ·
241
+ ${dim('gemini')} (Gemini CLI) · ${dim('codex')} (OpenAI Codex CLI) · ${dim('copilot')} (GitHub Copilot CLI) ·
242
+ ${dim('cline')} (Cline) · ${dim('aider')} (Aider - no tool hooks, so it is routed through the LLM Guard proxy)
243
+ e.g. ${dim('shomra install-hook --agent cursor,windsurf')} or ${dim('shomra install-hook --agent all')}.
244
+ Windsurf's post-hooks can flag/log but not withhold a result (vendor limit).
245
+
246
+ Risky calls/results are blocked and every decision lands in Gate Activity,
247
+ tagged with which agent triggered it.
248
+
249
+ ${bold('Tiered enforcement (fast + unbreakable).')} The guard decides the dangerous
250
+ majority ON-MACHINE with zero network - curl|sh, reverse shells, base64 RCE,
251
+ live secrets, injection - so protection survives a slow, down, or blocked
252
+ backend and adds no latency to ordinary calls. Only policy-relevant calls
253
+ (artifact installs, MCP calls, agent-identity, network egress, or anything
254
+ the local tier flags) escalate to the server for the full org-policy /
255
+ identity / governance / flow engine, with a short timeout + a circuit breaker
256
+ that skips a known-down backend. Fail-open by default (the local tier is still
257
+ enforcing); SHOMRA_GUARD_STRICT=1 to also fail-closed on the server tier.
258
+ `;
259
+
260
+ const EXIT_CODES = () => `${bold('EXIT CODES')} ${dim('- one convention across every command')}
261
+ 0 clean / pass
262
+ 1 hard fail - BLOCK, vulnerable model, secret found, FAIL verdict, below --min, regression
263
+ 2 soft fail - FLAG under --strict (REVIEW when strict)
264
+ 3 usage / config error - not configured, bad flags, unknown command
265
+ `;
266
+
267
+ const ENV = () => `${bold('ENV')}
268
+ SHOMRA_API_KEY API key (overrides config)
269
+ SHOMRA_URL Backend URL (overrides config)
270
+ SHOMRA_API_TIMEOUT_MS=30000 Per-request backend timeout for scan/gate/report (never hangs)
271
+ SHOMRA_AGENT Agent-identity handle presented as x-shomra-agent (llm-proxy + firewall)
272
+ SHOMRA_GATE_CONCURRENCY=8 Parallel backend gate/model-lookup calls in batch runs (1-32)
273
+ SHOMRA_GH_TOKEN GitHub token for \`shomra pr\` (falls back to GITHUB_TOKEN)
274
+ SHOMRA_GUARD_STRICT=1 Fail-closed on the server tier if the backend is unreachable
275
+ SHOMRA_GUARD_LOCAL=0 Disable the on-machine Tier-0 guard (route everything to the server)
276
+ SHOMRA_GUARD_IGNORE=<globs> Comma-separated file globs the runtime guard treats as known-safe (never
277
+ withheld) - plus any .shomraignore in the working dir. For files with
278
+ benign patterns in source (detection code, fixtures, docs).
279
+ SHOMRA_GUARD_ALWAYS_ESCALATE=1 Send every call to the server (full telemetry, higher overhead)
280
+ SHOMRA_GUARD_TIMEOUT_MS=2000 Per-call server timeout budget (default 2000)
281
+ SHOMRA_GUARD_BREAKER_MS=30000 Skip the server for this long after a failure (0 disables)
282
+ SHOMRA_LLM_PROXY_BASE Proxy base URL install-hook writes for Aider (default http://127.0.0.1:4141/openai/v1)
283
+ SHOMRA_MODEL_GUARD=0 Disable the model-load screen in the PreToolUse hook
284
+ SHOMRA_MODEL_CACHE=0 Disable the on-machine model-index verdict cache
285
+ SHOMRA_MODEL_CACHE_TTL_MS Model-cache freshness window (default 7 days)
286
+ `;
287
+
288
+ export const HELP_SECTIONS = [
289
+ BANNER,
290
+ USAGE,
291
+ MODES,
292
+ COMMANDS,
293
+ GATE,
294
+ CHECK,
295
+ BASELINE_AND_SUPPRESSION,
296
+ POLICY_AS_CODE,
297
+ FIX,
298
+ WHY,
299
+ INSTALL_PRECOMMIT,
300
+ MODEL_SCAN,
301
+ MEMORY_SCAN,
302
+ REDTEAM,
303
+ HARDEN,
304
+ AGENT_IDENTITY,
305
+ LLM_PROXY,
306
+ RUNTIME_FIREWALL,
307
+ EXIT_CODES,
308
+ ENV,
309
+ ];
@@ -0,0 +1,27 @@
1
+ import { HELP_SECTIONS } from './help-sections.mjs';
2
+ import { bold, cyan, dim } from '../core/terminal.mjs';
3
+
4
+ export function cmdHelp() {
5
+ console.log(HELP_SECTIONS.map((section) => section()).join('\n'));
6
+ }
7
+
8
+ export function cmdAdminHelp() {
9
+ console.log(`
10
+ ${bold(cyan('shomra admin'))} ${dim('- governance & advanced security operations')}
11
+
12
+ ${dim('Deep scans (backend + key)')}
13
+ ${cyan('scan-zip')} Static-scan a workspace ZIP ${dim('<file.zip> [--project <id>] [--json]')}
14
+ ${cyan('model-scan')} SAST-scan a public AI model ${dim('<hf-url | owner/model | github-url> [--project <id>] [--json]')}
15
+ ${cyan('memory-scan')} Scan memory + rules files for poisoning ${dim('[path] [--scope …] [--writer …] [--json]')}
16
+
17
+ ${dim('Offense & runtime identity')}
18
+ ${cyan('redteam')} Continuously red-team your guardrails ${dim('[--target llm-guard|model] [--evolve] [--min 80] [--fail-on-regression] [--json]')}
19
+ ${cyan('campaign')} Autonomous multi-turn adversary run ${dim('[--objectives exfil-canary,tool-abuse] [--turns 6] [--min 80] [--json]')}
20
+ ${cyan('harden')} Auto-fix what the red-team breached ${dim('[--run <id>] [--target llm-guard|model] [--apply] [--json]')}
21
+ ${cyan('agent-identity')} Register a non-human agent identity ${dim('register --name "…" --type coding-agent [--json]')}
22
+ ${cyan('llm-proxy')} Guardrail live LLM traffic ${dim('[--port 4141] [--project <id>] [--agent-id <handle>]')}
23
+
24
+ ${dim('Each also runs as a bare top-level verb (e.g.')} ${dim(bold('shomra redteam'))}${dim(') for back-compat.')}
25
+ ${dim('Full details for any command:')} ${bold('shomra help')}
26
+ `);
27
+ }
@@ -0,0 +1,55 @@
1
+ import { EXIT_USAGE } from '../core/exit-codes.mjs';
2
+ import { bold, dim, red } from '../core/terminal.mjs';
3
+ import { VERSION } from '../core/version.mjs';
4
+ import { KNOWN_FLAGS, parseFlags } from './flags.mjs';
5
+ import { cmdAdminHelp, cmdHelp } from './help.mjs';
6
+ import { ADMIN_VERBS, COMMANDS } from './registry.mjs';
7
+ import { didYouMean } from './suggestions.mjs';
8
+
9
+ export async function main() {
10
+ const [, , command, ...rest] = process.argv;
11
+
12
+ const sep = rest.indexOf('--');
13
+ const ours = sep === -1 ? rest : rest.slice(0, sep);
14
+ const { flags, positional, unknown } = parseFlags(ours);
15
+
16
+ if (command === 'help' || command === undefined || command === '--help' || command === '-h') {
17
+ return cmdHelp();
18
+ }
19
+ if (command === '--version' || command === '-v' || command === 'version') {
20
+ console.log(VERSION);
21
+ return;
22
+ }
23
+
24
+ const guardCmd = command === 'tool-guard' || command === 'result-guard' || command === 'prompt-guard' || command === 'plan-guard';
25
+ if (unknown.length && !guardCmd) {
26
+ for (const u of unknown) {
27
+ const near = didYouMean(u, [...KNOWN_FLAGS]);
28
+ console.error(red(`✗ Unknown flag: --${u}`) + (near ? dim(` (did you mean --${near}?)`) : ''));
29
+ }
30
+ console.error(dim('Run `shomra help` for the full option list.'));
31
+ process.exit(EXIT_USAGE);
32
+ }
33
+
34
+ if (command === 'admin') {
35
+ const sub = positional[0];
36
+ if (!sub || sub === 'help' || flags.help) return cmdAdminHelp();
37
+ const fn = COMMANDS[sub];
38
+ if (!fn || !ADMIN_VERBS.has(sub)) {
39
+ const near = didYouMean(sub, [...ADMIN_VERBS]);
40
+ console.error(red(`✗ Unknown admin command: ${sub ?? ''}`) + (near ? ` did you mean ${bold(near)}?` : ''));
41
+ console.error(dim('Run `shomra admin` for the list.'));
42
+ process.exit(EXIT_USAGE);
43
+ }
44
+ return fn(flags, positional.slice(1));
45
+ }
46
+
47
+ const fn = COMMANDS[command];
48
+ if (!fn) {
49
+ const near = didYouMean(command, [...Object.keys(COMMANDS), 'help', 'version', 'admin']);
50
+ console.error(red(`✗ Unknown command: ${command}`) + (near ? ` did you mean ${bold(near)}?` : ''));
51
+ console.error(dim('Run `shomra help` for the full command list.'));
52
+ process.exit(EXIT_USAGE);
53
+ }
54
+ return fn(flags, positional);
55
+ }
@@ -0,0 +1,80 @@
1
+ import { cmdAdd } from '../commands/add.mjs';
2
+ import { cmdAgentIdentity } from '../commands/agent-identity.mjs';
3
+ import { cmdBaseline, cmdCheck } from '../commands/check.mjs';
4
+ import { cmdCorpus } from '../commands/corpus.mjs';
5
+ import { cmdDesign } from '../commands/design.mjs';
6
+ import { cmdDoctor } from '../commands/doctor.mjs';
7
+ import { cmdFix } from '../commands/fix.mjs';
8
+ import { cmdGate } from '../commands/gate.mjs';
9
+ import { cmdInstallPrecommit } from '../commands/git-hooks.mjs';
10
+ import { cmdInit } from '../commands/init.mjs';
11
+ import { cmdInstallHook } from '../commands/install-hook.mjs';
12
+ import { cmdLlmProxy } from '../commands/llm-proxy.mjs';
13
+ import { cmdMcp, cmdMcpGuard } from '../commands/mcp.mjs';
14
+ import { cmdMemoryScan } from '../commands/memory-scan.mjs';
15
+ import { cmdModelScan } from '../commands/model-scan.mjs';
16
+ import { cmdModels } from '../commands/models.mjs';
17
+ import { cmdNew } from '../commands/new.mjs';
18
+ import { cmdPlan, cmdPlanGuard } from '../commands/plan.mjs';
19
+ import { cmdPr } from '../commands/pr.mjs';
20
+ import { cmdProtect } from '../commands/protect.mjs';
21
+ import { cmdProvenance } from '../commands/provenance.mjs';
22
+ import { cmdCampaign, cmdHarden, cmdRedteam } from '../commands/redteam.mjs';
23
+ import { cmdRules } from '../commands/rules.mjs';
24
+ import { cmdRun } from '../commands/run.mjs';
25
+ import { cmdScanZip } from '../commands/scan-zip.mjs';
26
+ import { cmdScan } from '../commands/scan.mjs';
27
+ import { cmdSecrets } from '../commands/secrets.mjs';
28
+ import { cmdStatus } from '../commands/status.mjs';
29
+ import { cmdWhy } from '../commands/why.mjs';
30
+ import { cmdPromptGuard } from '../guard/prompt-guard.mjs';
31
+ import { cmdResultGuard } from '../guard/result-guard.mjs';
32
+ import { cmdToolGuard } from '../guard/tool-guard.mjs';
33
+
34
+ export const COMMANDS = {
35
+ init: (f) => cmdInit(f),
36
+ scan: (f) => cmdScan(f),
37
+ report: (f) => cmdScan({ ...f, report: true }),
38
+ gate: (f, p) => cmdGate(f, p),
39
+ run: (f, p) => cmdRun(f, p),
40
+ check: (f, p) => cmdCheck(f, p),
41
+ pr: (f, p) => cmdPr(f, p),
42
+ baseline: (f, p) => cmdBaseline(f, p),
43
+ fix: (f, p) => cmdFix(f, p),
44
+ why: (f, p) => cmdWhy(f, p),
45
+ provenance: (f, p) => cmdProvenance(f, p),
46
+ 'install-precommit': (f, p) => cmdInstallPrecommit(f, p),
47
+ 'scan-zip': (f, p) => cmdScanZip(f, p),
48
+ 'model-scan': (f, p) => cmdModelScan(f, p),
49
+ models: (f, p) => cmdModels(f, p),
50
+ 'memory-scan': (f, p) => cmdMemoryScan(f, p),
51
+ redteam: (f) => cmdRedteam(f),
52
+ campaign: (f) => cmdCampaign(f),
53
+ harden: (f) => cmdHarden(f),
54
+ 'agent-identity': (f, p) => cmdAgentIdentity(f, p),
55
+ 'agent-id': (f, p) => cmdAgentIdentity(f, p),
56
+ 'llm-proxy': (f) => cmdLlmProxy(f),
57
+ 'tool-guard': (f) => cmdToolGuard(f),
58
+ 'mcp-guard': (f, p) => cmdMcpGuard(f, p),
59
+ 'result-guard': (f) => cmdResultGuard(f),
60
+ 'prompt-guard': (f) => cmdPromptGuard(f),
61
+ 'plan-guard': (f) => cmdPlanGuard(f),
62
+ plan: (f, p) => cmdPlan(f, p),
63
+ corpus: (f, p) => cmdCorpus(f, p),
64
+ rules: (f, p) => cmdRules(f, p),
65
+ design: (f, p) => cmdDesign(f, p),
66
+ add: (f, p) => cmdAdd(f, p),
67
+ 'install-hook': (f) => cmdInstallHook(f),
68
+ protect: (f) => cmdProtect(f),
69
+ doctor: (f) => cmdDoctor(f),
70
+ new: (f, p) => cmdNew(f, p),
71
+ mcp: (f, p) => cmdMcp(f, p),
72
+ secrets: (f, p) => cmdSecrets(f, p),
73
+ status: () => cmdStatus(),
74
+ };
75
+
76
+ export const ADMIN_VERBS = new Set([
77
+ 'scan-zip', 'model-scan', 'memory-scan',
78
+ 'redteam', 'campaign', 'harden',
79
+ 'agent-identity', 'agent-id', 'llm-proxy',
80
+ ]);
@@ -0,0 +1,33 @@
1
+ export function levenshtein(a, b) {
2
+ if (!a.length) return b.length;
3
+ if (!b.length) return a.length;
4
+
5
+ let previousRow = Array.from({ length: b.length + 1 }, (_, index) => index);
6
+ for (let i = 1; i <= a.length; i += 1) {
7
+ const currentRow = [i];
8
+ for (let j = 1; j <= b.length; j += 1) {
9
+ const substitution = previousRow[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1);
10
+ currentRow[j] = Math.min(previousRow[j] + 1, currentRow[j - 1] + 1, substitution);
11
+ }
12
+ previousRow = currentRow;
13
+ }
14
+ return previousRow[b.length];
15
+ }
16
+
17
+ export function didYouMean(input, candidates) {
18
+ const needle = String(input).toLowerCase();
19
+ let best = null;
20
+ let bestDistance = Infinity;
21
+
22
+ for (const candidate of candidates) {
23
+ if (candidate.startsWith(needle) || needle.startsWith(candidate)) return candidate;
24
+ const distance = levenshtein(needle, candidate);
25
+ if (distance < bestDistance) {
26
+ bestDistance = distance;
27
+ best = candidate;
28
+ }
29
+ }
30
+
31
+ const tolerance = Math.max(2, Math.floor(needle.length / 3));
32
+ return bestDistance <= tolerance ? best : null;
33
+ }