@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.
- package/NOTICE +1 -1
- package/README.md +57 -57
- package/package.json +3 -9
- package/shomra.mjs +9 -7168
- package/src/agents/hook-command.mjs +19 -0
- package/src/agents/hook-files.mjs +41 -0
- package/src/agents/installers.mjs +203 -0
- package/src/artifacts/matchers.mjs +59 -0
- package/src/artifacts/report.mjs +50 -0
- package/src/cli/flags.mjs +68 -0
- package/src/cli/help-sections.mjs +309 -0
- package/src/cli/help.mjs +27 -0
- package/src/cli/main.mjs +55 -0
- package/src/cli/registry.mjs +80 -0
- package/src/cli/suggestions.mjs +33 -0
- package/src/commands/add.mjs +149 -0
- package/src/commands/agent-identity.mjs +46 -0
- package/src/commands/check.mjs +194 -0
- package/src/commands/corpus.mjs +126 -0
- package/src/commands/design.mjs +168 -0
- package/src/commands/doctor.mjs +209 -0
- package/src/commands/fix.mjs +115 -0
- package/src/commands/gate.mjs +154 -0
- package/src/commands/git-hooks.mjs +163 -0
- package/src/commands/init.mjs +36 -0
- package/src/commands/install-hook.mjs +51 -0
- package/src/commands/llm-proxy.mjs +153 -0
- package/src/commands/mcp-add.mjs +185 -0
- package/src/commands/mcp.mjs +143 -0
- package/src/commands/memory-scan.mjs +181 -0
- package/src/commands/model-scan.mjs +99 -0
- package/src/commands/models.mjs +145 -0
- package/src/commands/new.mjs +64 -0
- package/src/commands/plan.mjs +87 -0
- package/src/commands/pr.mjs +249 -0
- package/src/commands/protect.mjs +38 -0
- package/src/commands/provenance.mjs +91 -0
- package/src/commands/redteam.mjs +166 -0
- package/src/commands/rules.mjs +220 -0
- package/src/commands/run.mjs +128 -0
- package/src/commands/scan-zip.mjs +118 -0
- package/src/commands/scan.mjs +102 -0
- package/src/commands/secrets.mjs +99 -0
- package/src/commands/status.mjs +50 -0
- package/src/commands/why.mjs +88 -0
- package/src/core/api-client.mjs +66 -0
- package/src/core/api-key.mjs +6 -0
- package/src/core/circuit-breaker.mjs +42 -0
- package/src/core/config.mjs +37 -0
- package/src/core/exit-codes.mjs +9 -0
- package/src/core/json-file.mjs +13 -0
- package/src/core/numbers.mjs +4 -0
- package/src/core/package-root.mjs +10 -0
- package/src/core/terminal.mjs +16 -0
- package/src/core/version.mjs +14 -0
- package/src/core/wire-limits.mjs +53 -0
- package/src/corpus/screening.mjs +127 -0
- package/{ai-usage.mjs → src/detect/ai-usage.mjs} +0 -27
- package/src/detect/code-sast.mjs +2 -0
- package/{design.mjs → src/detect/design.mjs} +17 -106
- package/src/detect/guard-signals.mjs +18 -0
- package/{model-refs.mjs → src/detect/model-refs.mjs} +18 -77
- package/src/detect/sast/chains.mjs +30 -0
- package/src/detect/sast/path-expressions.mjs +76 -0
- package/src/detect/sast/rules-chains.mjs +33 -0
- package/src/detect/sast/rules-config.mjs +51 -0
- package/src/detect/sast/rules-javascript.mjs +109 -0
- package/src/detect/sast/rules-python.mjs +292 -0
- package/src/detect/sast/scanner.mjs +104 -0
- package/src/detect/sast/source-lines.mjs +115 -0
- package/src/detect/sast/taint.mjs +71 -0
- package/src/detect/signals/artifacts.mjs +113 -0
- package/src/detect/signals/autonomy.mjs +55 -0
- package/src/detect/signals/config-markers.mjs +28 -0
- package/src/detect/signals/credential-harvest.mjs +64 -0
- package/src/detect/signals/durable-claims.mjs +73 -0
- package/src/detect/signals/egress.mjs +56 -0
- package/src/detect/signals/execution-hijack.mjs +128 -0
- package/src/detect/signals/gate.mjs +91 -0
- package/src/detect/signals/injection.mjs +55 -0
- package/src/detect/signals/lines.mjs +42 -0
- package/src/detect/signals/masking.mjs +99 -0
- package/src/detect/signals/memory.mjs +357 -0
- package/src/detect/signals/packages.mjs +45 -0
- package/src/detect/signals/propagation.mjs +86 -0
- package/src/detect/signals/prose-context.mjs +82 -0
- package/src/detect/signals/scan.mjs +91 -0
- package/src/detect/signals/secrets.mjs +85 -0
- package/src/detect/signals/sensitive.mjs +9 -0
- package/src/detect/signals/severity.mjs +10 -0
- package/src/detect/signals/shell.mjs +96 -0
- package/src/detect/signals/staged-fetch.mjs +66 -0
- package/src/detect/signals/text-match.mjs +35 -0
- package/src/gate/batch.mjs +157 -0
- package/src/gate/environment.mjs +122 -0
- package/src/gate/repo-policy.mjs +65 -0
- package/src/gate/result.mjs +53 -0
- package/src/gate/sarif.mjs +33 -0
- package/src/gate/sast.mjs +64 -0
- package/src/gate/suppressions.mjs +0 -0
- package/src/guard/classify.mjs +50 -0
- package/src/guard/emit.mjs +51 -0
- package/src/guard/ignore.mjs +24 -0
- package/src/guard/ledger.mjs +112 -0
- package/src/guard/model-load.mjs +50 -0
- package/src/guard/normalize.mjs +77 -0
- package/src/guard/options.mjs +10 -0
- package/src/guard/prompt-guard.mjs +184 -0
- package/src/guard/report.mjs +35 -0
- package/src/guard/result-guard.mjs +140 -0
- package/src/guard/tool-guard.mjs +166 -0
- package/src/inventory/agent-artifacts.mjs +5 -0
- package/src/inventory/agent-posture.mjs +249 -0
- package/src/inventory/artifacts/classify.mjs +27 -0
- package/src/inventory/artifacts/discover.mjs +187 -0
- package/src/inventory/artifacts/file-read.mjs +42 -0
- package/src/inventory/artifacts/hooks.mjs +14 -0
- package/src/inventory/artifacts/limits.mjs +37 -0
- package/src/inventory/artifacts/marketplaces.mjs +45 -0
- package/src/inventory/artifacts/roots.mjs +20 -0
- package/src/inventory/artifacts/walk.mjs +36 -0
- package/src/inventory/discovery/ai-dependencies.mjs +161 -0
- package/src/inventory/discovery/ai-tools.mjs +23 -0
- package/src/inventory/discovery/all.mjs +40 -0
- package/src/inventory/discovery/coding-agents.mjs +77 -0
- package/src/inventory/discovery/fs-read.mjs +36 -0
- package/src/inventory/discovery/local-runtimes.mjs +53 -0
- package/src/inventory/discovery/mcp-clients.mjs +67 -0
- package/src/inventory/discovery/mcp-servers.mjs +78 -0
- package/src/inventory/discovery/model-keys.mjs +97 -0
- package/src/inventory/discovery/platform.mjs +16 -0
- package/src/inventory/discovery/rules-files.mjs +25 -0
- package/src/inventory/discovery/vector-stores.mjs +176 -0
- package/src/inventory/discovery/workspace.mjs +124 -0
- package/src/inventory/discovery.mjs +10 -0
- package/src/mcp/child-process.mjs +50 -0
- package/src/mcp/config-wrapping.mjs +75 -0
- package/src/mcp/connect-gate.mjs +45 -0
- package/src/mcp/hosts.mjs +16 -0
- package/src/mcp/jsonrpc.mjs +48 -0
- package/src/mcp/lookup.mjs +50 -0
- package/src/mcp/screening.mjs +103 -0
- package/src/mcp/server-tools.mjs +97 -0
- package/src/mcp/server.mjs +102 -0
- package/src/mcp/shim.mjs +205 -0
- package/src/models/lookup.mjs +79 -0
- package/src/models/references.mjs +103 -0
- package/src/rules/context.mjs +98 -0
- package/src/rules/generate.mjs +103 -0
- package/src/rules/sections.mjs +145 -0
- package/src/scaffold/agent-project.mjs +185 -0
- package/src/scaffold/artifact-templates.mjs +35 -0
- package/code-sast.mjs +0 -1063
- package/discovery.mjs +0 -977
- package/guard-ledger.mjs +0 -239
- 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
|
+
];
|
package/src/cli/help.mjs
ADDED
|
@@ -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
|
+
}
|
package/src/cli/main.mjs
ADDED
|
@@ -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
|
+
}
|