@shomra/agent 0.2.1

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/model-refs.mjs ADDED
@@ -0,0 +1,130 @@
1
+ /**
2
+ * Model-reference extractor — finds the AI MODELS a dev's code loads, so the CLI
3
+ * can look each one up in the platform's Model Security Index (GET /models/lookup)
4
+ * and surface known vulnerabilities. This is the "you reference a model → we tell
5
+ * you if it's dangerous" path: you can't read a model's weights from source, but
6
+ * you CAN see which model id the code pulls, and the platform has already scanned
7
+ * the popular ones.
8
+ *
9
+ * Dependency-free, line-oriented, low-false-positive: model ids are only extracted
10
+ * from lines that carry a real loader hint (from_pretrained / SentenceTransformer /
11
+ * hf_hub_download / snapshot_download / pipeline(model=…) / a huggingface.co URL /
12
+ * torch.hub.load / ollama pull|run), never from arbitrary "a/b" strings (which are
13
+ * usually file paths or npm packages).
14
+ */
15
+
16
+ // A line must carry one of these to be considered a model load.
17
+ const LOADER_HINT = /\b(from_pretrained|SentenceTransformer|CrossEncoder|hf_hub_download|snapshot_download|InferenceClient|AutoModel\w*|AutoTokenizer|AutoConfig|AutoProcessor|AutoFeatureExtractor|from_hf_hub|hf_hub|load_dataset|torch\.hub\.load|ollama)\b|\bpipeline\s*\(|\bmodel\s*=\s*['"]|huggingface\.co|\bhf\.co\b/i;
18
+
19
+ // A quoted HF-style id: "org/model" (one slash, HF-legal chars, no path/URL/ext).
20
+ const QUOTED_ID = /['"]([A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*)['"]/g;
21
+ // High-confidence positions whose quoted value IS a model id — so we can also
22
+ // accept BARE ids (no org, e.g. "gpt2", "distilbert-base-uncased") from them.
23
+ const FROM_PRETRAINED_ARG = /\bfrom_pretrained\s*\(\s*(?:[A-Za-z_][\w.]*\s*,\s*)?['"]([\w./-]+)['"]/g;
24
+ const ST_ARG = /\b(?:SentenceTransformer|CrossEncoder)\s*\(\s*['"]([\w./-]+)['"]/g;
25
+ const KW_ID = /\b(?:model|repo_id|model_name|model_id|model_name_or_path|pretrained_model_name_or_path|checkpoint|base_model)\s*=\s*['"]([\w./-]+)['"]/gi;
26
+ // A pinned revision/commit in the same call.
27
+ const REVISION = /\b(?:revision|commit|sha)\s*=\s*['"]([\w.-]{4,})['"]/i;
28
+ // Bare-id positions can accidentally grab a pipeline TASK / device / dtype — drop those.
29
+ const ID_STOPWORDS = new Set([
30
+ 'auto', 'cpu', 'cuda', 'mps', 'none', 'true', 'false', 'main', 'default',
31
+ 'text-classification', 'token-classification', 'question-answering', 'fill-mask',
32
+ 'summarization', 'translation', 'text-generation', 'text2text-generation',
33
+ 'feature-extraction', 'sentence-similarity', 'zero-shot-classification',
34
+ 'image-classification', 'object-detection', 'automatic-speech-recognition',
35
+ 'conversational', 'ner', 'sentiment-analysis', 'embeddings', 'chat', 'completion',
36
+ ]);
37
+ // Full huggingface.co / hf.co model URLs.
38
+ const HF_URL = /https?:\/\/(?:www\.)?(?:huggingface\.co|hf\.co)\/([A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*)(?:\/tree\/([\w.-]+))?/gi;
39
+ // `ollama pull llama3:8b` / `ollama run mistral` — local runtime models (no slash).
40
+ const OLLAMA = /\bollama\s+(?:pull|run|cp|create)\s+([a-z0-9][\w.:\/-]*)/gi;
41
+ // torch.hub.load("pytorch/vision", …) — a GitHub owner/repo that runs hubconf.py.
42
+ const TORCH_HUB = /torch\.hub\.load\s*\(\s*['"]([A-Za-z0-9][\w.-]*\/[A-Za-z0-9][\w.-]*)['"]/g;
43
+
44
+ // Reject ids that are really file paths, packages, or non-model strings.
45
+ const ASSET_EXT = /\.(py|pyc|ipynb|[mc]?[jt]sx?|json|ya?ml|toml|txt|md|lock|cfg|ini|sh|env|png|jpg|svg|css|html?|csv|tsv|parquet)$/i;
46
+ function looksLikeModelId(id) {
47
+ if (!id || id.startsWith('@') || id.startsWith('.') || id.startsWith('/')) return false;
48
+ if (id.includes('..') || id.split('/').length !== 2) return false;
49
+ if (ASSET_EXT.test(id)) return false; // a model id never ends in a code/asset ext
50
+ const [a, b] = id.split('/');
51
+ if (!/[A-Za-z]/.test(a) || !/[A-Za-z]/.test(b)) return false; // kills "123/456"
52
+ return true;
53
+ }
54
+ // A bare id (no org) from a high-confidence position — accept unless it's clearly
55
+ // a task/device token or an asset path.
56
+ function validBareId(id) {
57
+ if (!id || id.startsWith('@') || id.startsWith('.') || id.startsWith('/') || id.includes('..')) return false;
58
+ if (ID_STOPWORDS.has(id.toLowerCase())) return false;
59
+ if (id.includes('/')) return looksLikeModelId(id);
60
+ if (ASSET_EXT.test(id) || id.length < 2 || !/[A-Za-z]/.test(id)) return false;
61
+ return true;
62
+ }
63
+
64
+ /**
65
+ * Extract model references from one source file's text. Returns
66
+ * `[{ id, revision?, source, line, via }]` — `source` is the origin registry
67
+ * ('hf' | 'github' | 'ollama'), `via` names the matched loader for evidence.
68
+ * De-duplicated per (id, revision) within the file, keeping the first line.
69
+ */
70
+ export function scanModelRefs(text, file = '') {
71
+ if (!text) return [];
72
+ const out = [];
73
+ const seen = new Set();
74
+ // `bare` allows an org-less id ("gpt2") when it came from a high-confidence
75
+ // position (from_pretrained/SentenceTransformer/model=); ollama ids are freeform.
76
+ const add = (id, { revision, source, line, via, bare }) => {
77
+ if (!id) return;
78
+ if (source !== 'ollama' && !(bare ? validBareId(id) : looksLikeModelId(id))) return;
79
+ const key = `${source}:${id}:${revision || ''}`;
80
+ if (seen.has(key)) return;
81
+ seen.add(key);
82
+ out.push({ id, ...(revision ? { revision } : {}), source, line, via, file });
83
+ };
84
+
85
+ const lines = text.split(/\r?\n/);
86
+ for (let i = 0; i < lines.length; i++) {
87
+ const raw = lines[i];
88
+ const trimmed = raw.trim();
89
+ if (!trimmed) continue;
90
+ const ln = i + 1;
91
+
92
+ // 1. Full HF URLs — scanned on EVERY line (incl. comments / README / markdown
93
+ // headers), since a huggingface.co URL is an unambiguous model reference.
94
+ for (const m of raw.matchAll(HF_URL)) add(m[1], { revision: m[2], source: 'hf', line: ln, via: 'huggingface.co URL' });
95
+
96
+ // Code-oriented extractors below skip pure comment lines (a commented-out load).
97
+ if (trimmed.startsWith('#') || trimmed.startsWith('//') || trimmed.startsWith('*')) continue;
98
+
99
+ // 2. torch.hub.load("owner/repo", …) → GitHub-hosted, runs hubconf.py.
100
+ const isTorchHub = /torch\.hub\.load/.test(raw);
101
+ for (const m of raw.matchAll(TORCH_HUB)) add(m[1], { source: 'github', line: ln, via: 'torch.hub.load' });
102
+
103
+ // 3. ollama pull/run <model>.
104
+ const isOllama = /\bollama\b/.test(raw);
105
+ for (const m of raw.matchAll(OLLAMA)) add(m[1], { source: 'ollama', line: ln, via: 'ollama' });
106
+
107
+ // Lines already fully handled by a specific matcher shouldn't also be mined
108
+ // by the generic HF extractors (avoids torch.hub's owner/repo re-added as hf).
109
+ if (isTorchHub || isOllama || /huggingface\.co|hf\.co/.test(raw)) continue;
110
+
111
+ const rev = (raw.match(REVISION) || [])[1];
112
+ // 4a. High-confidence positions — accept BARE ids (org-less) too.
113
+ for (const m of raw.matchAll(FROM_PRETRAINED_ARG)) add(m[1], { revision: rev, source: 'hf', line: ln, via: 'from_pretrained', bare: true });
114
+ for (const m of raw.matchAll(ST_ARG)) add(m[1], { revision: rev, source: 'hf', line: ln, via: 'sentence-transformers', bare: true });
115
+ for (const m of raw.matchAll(KW_ID)) add(m[1], { revision: rev, source: 'hf', line: ln, via: 'model= keyword', bare: true });
116
+
117
+ // 4b. Any other loader line with a quoted org/model id (hf_hub_download,
118
+ // snapshot_download, InferenceClient(model=…), etc.).
119
+ if (LOADER_HINT.test(raw)) {
120
+ for (const m of raw.matchAll(QUOTED_ID)) add(m[1], { revision: rev, source: 'hf', line: ln, via: 'model loader' });
121
+ }
122
+ }
123
+ return out;
124
+ }
125
+
126
+ /** Extensions worth scanning for model references. */
127
+ const SCAN_EXT = /\.(py|ipynb|[mc]?[jt]sx?|ya?ml|yml|toml|txt|md|env|cfg|ini|json)$/i;
128
+ export function isModelRefScannable(file) {
129
+ return SCAN_EXT.test(String(file || ''));
130
+ }
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@shomra/agent",
3
+ "version": "0.2.1",
4
+ "description": "Shomra — a local-first security scanner and runtime firewall for AI agents, MCP servers, prompts, and models. Gates AI artifacts in your editor and CI.",
5
+ "type": "module",
6
+ "bin": {
7
+ "shomra": "./shomra.mjs"
8
+ },
9
+ "engines": {
10
+ "node": ">=18"
11
+ },
12
+ "scripts": {
13
+ "test": "node --test \"tests/**/*.test.mjs\""
14
+ },
15
+ "files": [
16
+ "shomra.mjs",
17
+ "discovery.mjs",
18
+ "guard-signals.mjs",
19
+ "code-sast.mjs",
20
+ "model-refs.mjs",
21
+ "README.md",
22
+ "LICENSE",
23
+ "NOTICE"
24
+ ],
25
+ "keywords": [
26
+ "ai-security",
27
+ "mcp",
28
+ "prompt-injection",
29
+ "sast",
30
+ "static-analysis",
31
+ "supply-chain",
32
+ "ci",
33
+ "sarif",
34
+ "llm",
35
+ "agent-security",
36
+ "devsecops"
37
+ ],
38
+ "homepage": "https://shomra.dev",
39
+ "repository": {
40
+ "type": "git",
41
+ "url": "git+https://github.com/shomra/agent.git"
42
+ },
43
+ "bugs": {
44
+ "url": "https://github.com/shomra/agent/issues"
45
+ },
46
+ "author": "Shomra",
47
+ "license": "Apache-2.0",
48
+ "publishConfig": {
49
+ "access": "public"
50
+ }
51
+ }