@wrongstack/plugins 0.307.0 → 0.308.0
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/dist/accessibility-auditor/index.d.ts +5 -0
- package/dist/accessibility-auditor.js +67 -11
- package/dist/code-metrics/index.d.ts +9 -0
- package/dist/code-metrics.js +42 -11
- package/dist/dep-guard.js +4 -2
- package/dist/dependency-vulnerability-gate/index.d.ts +4 -0
- package/dist/dependency-vulnerability-gate.js +435 -62
- package/dist/file-watcher.js +25 -10
- package/dist/format-on-save.js +10 -4
- package/dist/import-organizer/index.d.ts +4 -0
- package/dist/import-organizer.js +22 -1
- package/dist/index.js +509 -206
- package/dist/license-audit-gate/index.d.ts +1 -0
- package/dist/license-audit-gate.js +445 -83
- package/dist/lint-gate/index.d.ts +13 -0
- package/dist/lint-gate.js +13 -10
- package/dist/path-guard/glob.d.ts +23 -0
- package/dist/path-guard/index.d.ts +2 -0
- package/dist/path-guard.js +214 -32
- package/dist/prompt-firewall/index.d.ts +7 -0
- package/dist/prompt-firewall.js +92 -2
- package/dist/runtime/h1-state.d.ts +62 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/redos-guard.d.ts +69 -0
- package/dist/runtime/sandbox.d.ts +59 -0
- package/dist/runtime.js +210 -19
- package/dist/secret-scanner.js +16 -2
- package/dist/type-gate.js +33 -3
- package/package.json +3 -3
|
@@ -18,9 +18,382 @@ function resolveExecInvocation(command, args = []) {
|
|
|
18
18
|
// src/runtime/index.ts
|
|
19
19
|
var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
20
20
|
|
|
21
|
+
// src/dep-guard/index.ts
|
|
22
|
+
var state = {
|
|
23
|
+
invocations: 0,
|
|
24
|
+
installsSeen: 0,
|
|
25
|
+
blocks: 0,
|
|
26
|
+
warns: 0,
|
|
27
|
+
llmConfirmCount: 0,
|
|
28
|
+
llmConfirmErrors: 0,
|
|
29
|
+
lastBlock: null,
|
|
30
|
+
hookUnregister: null
|
|
31
|
+
};
|
|
32
|
+
var DEFAULTS = {
|
|
33
|
+
enabled: true,
|
|
34
|
+
mode: "block",
|
|
35
|
+
deny: [],
|
|
36
|
+
allow: [],
|
|
37
|
+
warnOnUnpinned: false,
|
|
38
|
+
typosquatCheck: true,
|
|
39
|
+
confirmTyposquatsWithLlm: false
|
|
40
|
+
};
|
|
41
|
+
function readConfig(raw) {
|
|
42
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
43
|
+
const r = raw;
|
|
44
|
+
const strings = (v) => Array.isArray(v) ? v.filter((s) => typeof s === "string" && s.length > 0) : [];
|
|
45
|
+
return {
|
|
46
|
+
enabled: r["enabled"] !== false,
|
|
47
|
+
mode: r["mode"] === "warn" ? "warn" : "block",
|
|
48
|
+
deny: strings(r["deny"]),
|
|
49
|
+
allow: strings(r["allow"]),
|
|
50
|
+
warnOnUnpinned: r["warnOnUnpinned"] === true,
|
|
51
|
+
typosquatCheck: r["typosquatCheck"] !== false,
|
|
52
|
+
confirmTyposquatsWithLlm: r["confirmTyposquatsWithLlm"] === true
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
var INSTALL_RE = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)|(?:^|[;&|]\s*)(pip3?|uv)\s+(?:pip\s+)?install\s+([^;&|]+)|(?:^|[;&|]\s*)(cargo)\s+add\s+([^;&|]+)/gi;
|
|
56
|
+
function parseInstallCommands(command) {
|
|
57
|
+
const out = [];
|
|
58
|
+
INSTALL_RE.lastIndex = 0;
|
|
59
|
+
let m = INSTALL_RE.exec(command);
|
|
60
|
+
while (m !== null) {
|
|
61
|
+
const manager = (m[1] ?? m[3] ?? m[5] ?? "").toLowerCase();
|
|
62
|
+
const argString = m[2] ?? m[4] ?? m[6] ?? "";
|
|
63
|
+
const packages = [];
|
|
64
|
+
for (const token of argString.split(/\s+/)) {
|
|
65
|
+
if (!token || token.startsWith("-")) continue;
|
|
66
|
+
if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
|
|
67
|
+
const cleaned = token.replace(/^['"]|['"]$/g, "");
|
|
68
|
+
if (!cleaned) continue;
|
|
69
|
+
let name = cleaned;
|
|
70
|
+
let version = null;
|
|
71
|
+
const pipMatch = /^([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
|
|
72
|
+
if (pipMatch?.[1]) {
|
|
73
|
+
name = pipMatch[1];
|
|
74
|
+
const op = pipMatch[2] ?? "";
|
|
75
|
+
const ver = (pipMatch[3] ?? "").trim();
|
|
76
|
+
version = op === "==" || op === "" ? ver || null : `${op}${ver}`;
|
|
77
|
+
} else {
|
|
78
|
+
const at = cleaned.lastIndexOf("@");
|
|
79
|
+
if (at > 0) {
|
|
80
|
+
name = cleaned.slice(0, at);
|
|
81
|
+
version = cleaned.slice(at + 1) || null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (name) packages.push({ name, version });
|
|
85
|
+
}
|
|
86
|
+
out.push({ manager, packages });
|
|
87
|
+
m = INSTALL_RE.exec(command);
|
|
88
|
+
}
|
|
89
|
+
return out;
|
|
90
|
+
}
|
|
91
|
+
function matchesPattern(name, pattern) {
|
|
92
|
+
if (pattern.endsWith("*"))
|
|
93
|
+
return name.toLowerCase().startsWith(pattern.slice(0, -1).toLowerCase());
|
|
94
|
+
return name.toLowerCase() === pattern.toLowerCase();
|
|
95
|
+
}
|
|
96
|
+
var POPULAR_PACKAGES = [
|
|
97
|
+
"react",
|
|
98
|
+
"react-dom",
|
|
99
|
+
"express",
|
|
100
|
+
"lodash",
|
|
101
|
+
"axios",
|
|
102
|
+
"typescript",
|
|
103
|
+
"vite",
|
|
104
|
+
"vitest",
|
|
105
|
+
"next",
|
|
106
|
+
"vue",
|
|
107
|
+
"svelte",
|
|
108
|
+
"zod",
|
|
109
|
+
"prettier",
|
|
110
|
+
"eslint",
|
|
111
|
+
"jest",
|
|
112
|
+
"webpack",
|
|
113
|
+
"commander",
|
|
114
|
+
"chalk",
|
|
115
|
+
"dotenv",
|
|
116
|
+
"requests",
|
|
117
|
+
"numpy",
|
|
118
|
+
"pandas",
|
|
119
|
+
"flask",
|
|
120
|
+
"django",
|
|
121
|
+
"serde",
|
|
122
|
+
"tokio"
|
|
123
|
+
];
|
|
124
|
+
function editDistance(a, b) {
|
|
125
|
+
if (a === b) return 0;
|
|
126
|
+
if (Math.abs(a.length - b.length) > 2) return 3;
|
|
127
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => {
|
|
128
|
+
const row = new Array(b.length + 1).fill(0);
|
|
129
|
+
row[0] = i;
|
|
130
|
+
return row;
|
|
131
|
+
});
|
|
132
|
+
for (let j = 0; j <= b.length; j++) d[0][j] = j;
|
|
133
|
+
for (let i = 1; i <= a.length; i++) {
|
|
134
|
+
for (let j = 1; j <= b.length; j++) {
|
|
135
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
136
|
+
const row = d[i];
|
|
137
|
+
const prevRow = d[i - 1];
|
|
138
|
+
row[j] = Math.min(
|
|
139
|
+
prevRow[j] + 1,
|
|
140
|
+
row[j - 1] + 1,
|
|
141
|
+
prevRow[j - 1] + cost
|
|
142
|
+
);
|
|
143
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
144
|
+
row[j] = Math.min(row[j], d[i - 2][j - 2] + 1);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return d[a.length][b.length];
|
|
149
|
+
}
|
|
150
|
+
function typosquatOf(name) {
|
|
151
|
+
const lower = name.toLowerCase().replace(/^@[^/]+\//, "");
|
|
152
|
+
if (POPULAR_PACKAGES.includes(lower)) return null;
|
|
153
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
154
|
+
if (editDistance(lower, popular) === 1) return popular;
|
|
155
|
+
}
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
var plugin = {
|
|
159
|
+
name: "dep-guard",
|
|
160
|
+
version: "0.1.0",
|
|
161
|
+
description: "Supervises dependency installs: blocks deny-listed packages, flags typosquat lookalikes, and optionally warns on unpinned versions",
|
|
162
|
+
apiVersion: "^0.1.10",
|
|
163
|
+
capabilities: { tools: true, hooks: true, llm: true },
|
|
164
|
+
defaultConfig: { ...DEFAULTS },
|
|
165
|
+
configSchema: {
|
|
166
|
+
type: "object",
|
|
167
|
+
properties: {
|
|
168
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
169
|
+
mode: {
|
|
170
|
+
type: "string",
|
|
171
|
+
enum: ["block", "warn"],
|
|
172
|
+
default: "block",
|
|
173
|
+
description: "How deny-list hits are handled."
|
|
174
|
+
},
|
|
175
|
+
deny: {
|
|
176
|
+
type: "array",
|
|
177
|
+
items: { type: "string" },
|
|
178
|
+
default: [],
|
|
179
|
+
description: 'Package names (exact) or prefix globs ("@evil/*") that must not be installed.'
|
|
180
|
+
},
|
|
181
|
+
allow: {
|
|
182
|
+
type: "array",
|
|
183
|
+
items: { type: "string" },
|
|
184
|
+
default: [],
|
|
185
|
+
description: "Exemptions that override deny."
|
|
186
|
+
},
|
|
187
|
+
warnOnUnpinned: {
|
|
188
|
+
type: "boolean",
|
|
189
|
+
default: false,
|
|
190
|
+
description: "Warn when a package is installed without an explicit version."
|
|
191
|
+
},
|
|
192
|
+
typosquatCheck: {
|
|
193
|
+
type: "boolean",
|
|
194
|
+
default: true,
|
|
195
|
+
description: "Warn when a package name is one edit away from a well-known package."
|
|
196
|
+
},
|
|
197
|
+
confirmTyposquatsWithLlm: {
|
|
198
|
+
type: "boolean",
|
|
199
|
+
default: false,
|
|
200
|
+
description: "Ask the risk-review Council to assess a flagged typosquat, with One Shot fallback. Appended to the warn context, never escalates to a block. Off by default."
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
},
|
|
204
|
+
setup(api) {
|
|
205
|
+
state.invocations = 0;
|
|
206
|
+
state.installsSeen = 0;
|
|
207
|
+
state.blocks = 0;
|
|
208
|
+
state.warns = 0;
|
|
209
|
+
state.llmConfirmCount = 0;
|
|
210
|
+
state.llmConfirmErrors = 0;
|
|
211
|
+
state.lastBlock = null;
|
|
212
|
+
if (state.hookUnregister) {
|
|
213
|
+
try {
|
|
214
|
+
state.hookUnregister();
|
|
215
|
+
} catch {
|
|
216
|
+
}
|
|
217
|
+
state.hookUnregister = null;
|
|
218
|
+
}
|
|
219
|
+
const cfg = readConfig(api.config.extensions?.["dep-guard"]);
|
|
220
|
+
const hook = async (input) => {
|
|
221
|
+
if (!cfg.enabled) return;
|
|
222
|
+
state.invocations += 1;
|
|
223
|
+
const ti = input.toolInput ?? {};
|
|
224
|
+
const command = typeof ti["command"] === "string" ? ti["command"] : "";
|
|
225
|
+
if (!command) return;
|
|
226
|
+
const installs = parseInstallCommands(command);
|
|
227
|
+
const packages = installs.flatMap((i) => i.packages);
|
|
228
|
+
if (packages.length === 0) return;
|
|
229
|
+
state.installsSeen += 1;
|
|
230
|
+
api.metrics.counter("installs_seen");
|
|
231
|
+
const notes = [];
|
|
232
|
+
for (const pkg of packages) {
|
|
233
|
+
const allowed = cfg.allow.some((p) => matchesPattern(pkg.name, p));
|
|
234
|
+
const denied = !allowed && cfg.deny.some((p) => matchesPattern(pkg.name, p));
|
|
235
|
+
if (denied) {
|
|
236
|
+
if (cfg.mode === "block") {
|
|
237
|
+
state.blocks += 1;
|
|
238
|
+
state.lastBlock = {
|
|
239
|
+
pkg: pkg.name,
|
|
240
|
+
command: command.slice(0, 200),
|
|
241
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
242
|
+
};
|
|
243
|
+
api.metrics.counter("blocks");
|
|
244
|
+
return {
|
|
245
|
+
decision: "block",
|
|
246
|
+
reason: `dep-guard: package "${pkg.name}" is on the deny list (config.extensions["dep-guard"].deny) \u2014 install refused. Ask the user before adding this dependency, or add an \`allow\` entry.`
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
notes.push(
|
|
250
|
+
`"${pkg.name}" is DENY-LISTED \u2014 do not add it without explicit user approval.`
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
if (cfg.typosquatCheck) {
|
|
254
|
+
const lookalike = typosquatOf(pkg.name);
|
|
255
|
+
if (lookalike) {
|
|
256
|
+
const baseNote = `"${pkg.name}" is one edit away from the well-known package "${lookalike}" \u2014 possible typosquat. Verify the name before installing.`;
|
|
257
|
+
if (cfg.confirmTyposquatsWithLlm && api.llm) {
|
|
258
|
+
try {
|
|
259
|
+
const question = `Classify whether npm package "${pkg.name}" is likely a typo or typosquat of "${lookalike}".`;
|
|
260
|
+
const council = api.llm.council ? await api.llm.council(question, {
|
|
261
|
+
context: "The only supplied evidence is that the names have edit distance 1. Do not invent registry, download, ownership, or provenance facts.",
|
|
262
|
+
profile: "risk-review",
|
|
263
|
+
options: [
|
|
264
|
+
{ id: "typo", label: "Likely typo or typosquat" },
|
|
265
|
+
{ id: "real", label: "Likely distinct real package" },
|
|
266
|
+
{ id: "uncertain", label: "Insufficient evidence" }
|
|
267
|
+
]
|
|
268
|
+
}) : null;
|
|
269
|
+
const councilVerdict = council?.status === "decided" && council.optionId ? `${council.optionId.toUpperCase()}: ${council.reason ?? council.answer ?? "no rationale"}` : null;
|
|
270
|
+
const t = councilVerdict ? councilVerdict : (await api.llm.complete(
|
|
271
|
+
`${question} Reply with ONE sentence starting with "TYPO:", "REAL:", or "UNCERTAIN:".`,
|
|
272
|
+
{
|
|
273
|
+
system: "You are a supply-chain security assistant. Use only supplied evidence and preserve uncertainty.",
|
|
274
|
+
role: "security-reviewer",
|
|
275
|
+
maxTokens: 100
|
|
276
|
+
}
|
|
277
|
+
)).text.trim();
|
|
278
|
+
if (t) {
|
|
279
|
+
state.llmConfirmCount += 1;
|
|
280
|
+
api.metrics.counter("llm_confirm");
|
|
281
|
+
notes.push(`${baseNote} LLM verdict: ${t.slice(0, 300)}`);
|
|
282
|
+
} else {
|
|
283
|
+
notes.push(baseNote);
|
|
284
|
+
}
|
|
285
|
+
} catch {
|
|
286
|
+
state.llmConfirmErrors += 1;
|
|
287
|
+
notes.push(baseNote);
|
|
288
|
+
}
|
|
289
|
+
} else {
|
|
290
|
+
notes.push(baseNote);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
if (cfg.warnOnUnpinned && pkg.version === null) {
|
|
295
|
+
notes.push(`"${pkg.name}" has no pinned version \u2014 consider "${pkg.name}@<version>".`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
if (notes.length > 0) {
|
|
299
|
+
state.warns += 1;
|
|
300
|
+
api.metrics.counter("warns");
|
|
301
|
+
return {
|
|
302
|
+
decision: "allow",
|
|
303
|
+
additionalContext: `dep-guard:
|
|
304
|
+
${notes.map((n) => ` - ${n}`).join("\n")}`
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return {
|
|
308
|
+
decision: "allow",
|
|
309
|
+
additionalContext: `dep-guard: this command adds ${packages.length} dependenc${packages.length === 1 ? "y" : "ies"}: ${packages.map((p) => p.name).join(", ")}. Confirm each is intentional.`
|
|
310
|
+
};
|
|
311
|
+
};
|
|
312
|
+
state.hookUnregister = api.registerHook("PreToolUse", "bash|exec", hook, {
|
|
313
|
+
name: "dep-guard",
|
|
314
|
+
stage: "validate",
|
|
315
|
+
failurePolicy: "closed",
|
|
316
|
+
policy: true
|
|
317
|
+
});
|
|
318
|
+
api.tools.register({
|
|
319
|
+
name: "dep_guard_status",
|
|
320
|
+
description: "Reports dep-guard state: deny/allow lists, mode, and counters (installs seen, blocks, warns).",
|
|
321
|
+
inputSchema: { type: "object", properties: {} },
|
|
322
|
+
permission: "auto",
|
|
323
|
+
category: "Diagnostics",
|
|
324
|
+
mutating: false,
|
|
325
|
+
async execute() {
|
|
326
|
+
return {
|
|
327
|
+
ok: true,
|
|
328
|
+
enabled: cfg.enabled,
|
|
329
|
+
mode: cfg.mode,
|
|
330
|
+
deny: cfg.deny,
|
|
331
|
+
allow: cfg.allow,
|
|
332
|
+
warnOnUnpinned: cfg.warnOnUnpinned,
|
|
333
|
+
typosquatCheck: cfg.typosquatCheck,
|
|
334
|
+
counters: {
|
|
335
|
+
invocations: state.invocations,
|
|
336
|
+
installsSeen: state.installsSeen,
|
|
337
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
338
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
339
|
+
blocks: state.blocks,
|
|
340
|
+
warns: state.warns
|
|
341
|
+
},
|
|
342
|
+
lastBlock: state.lastBlock
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
api.log.info("dep-guard plugin loaded", {
|
|
347
|
+
version: "0.1.0",
|
|
348
|
+
enabled: cfg.enabled,
|
|
349
|
+
mode: cfg.mode,
|
|
350
|
+
denyCount: cfg.deny.length
|
|
351
|
+
});
|
|
352
|
+
},
|
|
353
|
+
teardown(api) {
|
|
354
|
+
if (state.hookUnregister) {
|
|
355
|
+
try {
|
|
356
|
+
state.hookUnregister();
|
|
357
|
+
} catch {
|
|
358
|
+
}
|
|
359
|
+
state.hookUnregister = null;
|
|
360
|
+
}
|
|
361
|
+
const final = {
|
|
362
|
+
invocations: state.invocations,
|
|
363
|
+
installsSeen: state.installsSeen,
|
|
364
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
365
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
366
|
+
blocks: state.blocks,
|
|
367
|
+
warns: state.warns
|
|
368
|
+
};
|
|
369
|
+
state.invocations = 0;
|
|
370
|
+
state.installsSeen = 0;
|
|
371
|
+
state.blocks = 0;
|
|
372
|
+
state.warns = 0;
|
|
373
|
+
state.llmConfirmCount = 0;
|
|
374
|
+
state.llmConfirmErrors = 0;
|
|
375
|
+
state.lastBlock = null;
|
|
376
|
+
api.log.info("dep-guard: teardown complete", { final });
|
|
377
|
+
},
|
|
378
|
+
async health() {
|
|
379
|
+
return {
|
|
380
|
+
ok: true,
|
|
381
|
+
message: state.lastBlock === null ? `dep-guard: ${state.installsSeen} install command(s) seen, ${state.blocks} block(s), ${state.warns} warn(s)` : `dep-guard: last block on "${state.lastBlock.pkg}" at ${state.lastBlock.when}`,
|
|
382
|
+
counters: {
|
|
383
|
+
invocations: state.invocations,
|
|
384
|
+
installsSeen: state.installsSeen,
|
|
385
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
386
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
387
|
+
blocks: state.blocks,
|
|
388
|
+
warns: state.warns
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
|
|
21
394
|
// src/dependency-vulnerability-gate/index.ts
|
|
22
395
|
var API_VERSION = "^0.1.10";
|
|
23
|
-
var
|
|
396
|
+
var state2 = {
|
|
24
397
|
invocations: 0,
|
|
25
398
|
installsSeen: 0,
|
|
26
399
|
auditsRun: 0,
|
|
@@ -30,7 +403,7 @@ var state = {
|
|
|
30
403
|
lastResult: null,
|
|
31
404
|
hookUnregister: null
|
|
32
405
|
};
|
|
33
|
-
var
|
|
406
|
+
var DEFAULTS2 = {
|
|
34
407
|
enabled: true,
|
|
35
408
|
severityThreshold: "high",
|
|
36
409
|
block: true,
|
|
@@ -43,15 +416,15 @@ var SEVERITY_RANK = {
|
|
|
43
416
|
high: 3,
|
|
44
417
|
critical: 4
|
|
45
418
|
};
|
|
46
|
-
function
|
|
47
|
-
if (!raw || typeof raw !== "object") return { ...
|
|
419
|
+
function readConfig2(raw) {
|
|
420
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS2 };
|
|
48
421
|
const r = raw;
|
|
49
|
-
const threshold = typeof r["severityThreshold"] === "string" && ["low", "moderate", "high", "critical"].includes(r["severityThreshold"]) ? r["severityThreshold"] :
|
|
422
|
+
const threshold = typeof r["severityThreshold"] === "string" && ["low", "moderate", "high", "critical"].includes(r["severityThreshold"]) ? r["severityThreshold"] : DEFAULTS2.severityThreshold;
|
|
50
423
|
return {
|
|
51
424
|
enabled: r["enabled"] !== false,
|
|
52
425
|
severityThreshold: threshold,
|
|
53
426
|
block: r["block"] !== false,
|
|
54
|
-
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] :
|
|
427
|
+
timeoutMs: typeof r["timeoutMs"] === "number" && r["timeoutMs"] > 0 ? r["timeoutMs"] : DEFAULTS2.timeoutMs
|
|
55
428
|
};
|
|
56
429
|
}
|
|
57
430
|
function collectSeverities(out, severity) {
|
|
@@ -122,13 +495,12 @@ function exceedsThreshold(report, threshold) {
|
|
|
122
495
|
if (thresholdRank == null || maxRank == null) return false;
|
|
123
496
|
return maxRank >= thresholdRank;
|
|
124
497
|
}
|
|
125
|
-
var INSTALL_RE = /\b(?:npm\s+(?:install|i)|pnpm\s+add|yarn\s+add)\b/i;
|
|
126
498
|
function isInstallCommand(input) {
|
|
127
499
|
if (input.toolName === "install") return true;
|
|
128
500
|
if (input.toolName !== "bash" && input.toolName !== "exec") return false;
|
|
129
501
|
const ti = input.toolInput ?? {};
|
|
130
502
|
const command = typeof ti["command"] === "string" ? ti["command"] : "";
|
|
131
|
-
return
|
|
503
|
+
return parseInstallCommands(command).length > 0;
|
|
132
504
|
}
|
|
133
505
|
var LOCKFILE_MANAGERS = [
|
|
134
506
|
{ file: "pnpm-lock.yaml", manager: "pnpm" },
|
|
@@ -183,13 +555,13 @@ async function runAudit(cfg) {
|
|
|
183
555
|
if (!report) return null;
|
|
184
556
|
return { manager, report, durationMs: Date.now() - start };
|
|
185
557
|
}
|
|
186
|
-
var
|
|
558
|
+
var plugin2 = {
|
|
187
559
|
name: "dependency-vulnerability-gate",
|
|
188
560
|
version: "0.1.0",
|
|
189
561
|
description: "PostToolUse hook that runs npm/pnpm audit after dependency installs and blocks or warns on vulnerabilities above a severity threshold",
|
|
190
562
|
apiVersion: API_VERSION,
|
|
191
563
|
capabilities: { tools: true, hooks: true },
|
|
192
|
-
defaultConfig: { ...
|
|
564
|
+
defaultConfig: { ...DEFAULTS2 },
|
|
193
565
|
configSchema: {
|
|
194
566
|
type: "object",
|
|
195
567
|
properties: {
|
|
@@ -218,35 +590,35 @@ var plugin = {
|
|
|
218
590
|
}
|
|
219
591
|
},
|
|
220
592
|
setup(api) {
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
if (
|
|
593
|
+
state2.invocations = 0;
|
|
594
|
+
state2.installsSeen = 0;
|
|
595
|
+
state2.auditsRun = 0;
|
|
596
|
+
state2.blocks = 0;
|
|
597
|
+
state2.warns = 0;
|
|
598
|
+
state2.errors = 0;
|
|
599
|
+
state2.lastResult = null;
|
|
600
|
+
if (state2.hookUnregister) {
|
|
229
601
|
try {
|
|
230
|
-
|
|
602
|
+
state2.hookUnregister();
|
|
231
603
|
} catch {
|
|
232
604
|
}
|
|
233
|
-
|
|
605
|
+
state2.hookUnregister = null;
|
|
234
606
|
}
|
|
235
|
-
const cfg =
|
|
607
|
+
const cfg = readConfig2(api.config.extensions?.["dependency-vulnerability-gate"]);
|
|
236
608
|
const hook = async (input) => {
|
|
237
609
|
if (!cfg.enabled) return;
|
|
238
|
-
|
|
610
|
+
state2.invocations += 1;
|
|
239
611
|
if (input.toolResult?.isError) return;
|
|
240
612
|
if (!isInstallCommand(input)) return;
|
|
241
|
-
|
|
613
|
+
state2.installsSeen += 1;
|
|
242
614
|
api.metrics.counter("installs_seen");
|
|
243
615
|
const result = await runAudit(cfg);
|
|
244
616
|
if (!result) {
|
|
245
|
-
|
|
617
|
+
state2.errors += 1;
|
|
246
618
|
return;
|
|
247
619
|
}
|
|
248
|
-
|
|
249
|
-
|
|
620
|
+
state2.auditsRun += 1;
|
|
621
|
+
state2.lastResult = {
|
|
250
622
|
manager: result.manager,
|
|
251
623
|
exceeded: exceedsThreshold(result.report, cfg.severityThreshold),
|
|
252
624
|
maxSeverity: result.report.maxSeverity,
|
|
@@ -264,18 +636,18 @@ var plugin = {
|
|
|
264
636
|
Counts: ${countsText || "unknown"}
|
|
265
637
|
Review the audit output or adjust the dependency choice.`;
|
|
266
638
|
if (cfg.block) {
|
|
267
|
-
|
|
639
|
+
state2.blocks += 1;
|
|
268
640
|
api.metrics.counter("blocks");
|
|
269
641
|
return {
|
|
270
642
|
decision: "block",
|
|
271
643
|
reason: message
|
|
272
644
|
};
|
|
273
645
|
}
|
|
274
|
-
|
|
646
|
+
state2.warns += 1;
|
|
275
647
|
api.metrics.counter("warns");
|
|
276
648
|
return { additionalContext: message };
|
|
277
649
|
};
|
|
278
|
-
|
|
650
|
+
state2.hookUnregister = api.registerHook("PostToolUse", "install|bash|exec", hook);
|
|
279
651
|
api.tools.register({
|
|
280
652
|
name: "dependency_audit_status",
|
|
281
653
|
description: "Reports dependency-vulnerability-gate state: threshold, block mode, and per-session counters.",
|
|
@@ -291,14 +663,14 @@ Review the audit output or adjust the dependency choice.`;
|
|
|
291
663
|
block: cfg.block,
|
|
292
664
|
timeoutMs: cfg.timeoutMs,
|
|
293
665
|
counters: {
|
|
294
|
-
invocations:
|
|
295
|
-
installsSeen:
|
|
296
|
-
auditsRun:
|
|
297
|
-
blocks:
|
|
298
|
-
warns:
|
|
299
|
-
errors:
|
|
666
|
+
invocations: state2.invocations,
|
|
667
|
+
installsSeen: state2.installsSeen,
|
|
668
|
+
auditsRun: state2.auditsRun,
|
|
669
|
+
blocks: state2.blocks,
|
|
670
|
+
warns: state2.warns,
|
|
671
|
+
errors: state2.errors
|
|
300
672
|
},
|
|
301
|
-
lastResult:
|
|
673
|
+
lastResult: state2.lastResult
|
|
302
674
|
};
|
|
303
675
|
}
|
|
304
676
|
});
|
|
@@ -309,47 +681,48 @@ Review the audit output or adjust the dependency choice.`;
|
|
|
309
681
|
});
|
|
310
682
|
},
|
|
311
683
|
teardown(api) {
|
|
312
|
-
if (
|
|
684
|
+
if (state2.hookUnregister) {
|
|
313
685
|
try {
|
|
314
|
-
|
|
686
|
+
state2.hookUnregister();
|
|
315
687
|
} catch {
|
|
316
688
|
}
|
|
317
|
-
|
|
689
|
+
state2.hookUnregister = null;
|
|
318
690
|
}
|
|
319
691
|
const final = {
|
|
320
|
-
invocations:
|
|
321
|
-
installsSeen:
|
|
322
|
-
auditsRun:
|
|
323
|
-
blocks:
|
|
324
|
-
warns:
|
|
325
|
-
errors:
|
|
692
|
+
invocations: state2.invocations,
|
|
693
|
+
installsSeen: state2.installsSeen,
|
|
694
|
+
auditsRun: state2.auditsRun,
|
|
695
|
+
blocks: state2.blocks,
|
|
696
|
+
warns: state2.warns,
|
|
697
|
+
errors: state2.errors
|
|
326
698
|
};
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
699
|
+
state2.invocations = 0;
|
|
700
|
+
state2.installsSeen = 0;
|
|
701
|
+
state2.auditsRun = 0;
|
|
702
|
+
state2.blocks = 0;
|
|
703
|
+
state2.warns = 0;
|
|
704
|
+
state2.errors = 0;
|
|
705
|
+
state2.lastResult = null;
|
|
334
706
|
api.log.info("dependency-vulnerability-gate: teardown complete", { final });
|
|
335
707
|
},
|
|
336
708
|
async health() {
|
|
337
709
|
return {
|
|
338
710
|
ok: true,
|
|
339
|
-
message:
|
|
711
|
+
message: state2.lastResult ? `dependency-vulnerability-gate: ${state2.auditsRun} audit(s), last ${state2.lastResult.exceeded ? "EXCEEDED" : "clean"} (${state2.lastResult.maxSeverity})` : `dependency-vulnerability-gate: ${state2.installsSeen} install(s) seen, ${state2.auditsRun} audit(s)`,
|
|
340
712
|
counters: {
|
|
341
|
-
invocations:
|
|
342
|
-
installsSeen:
|
|
343
|
-
auditsRun:
|
|
344
|
-
blocks:
|
|
345
|
-
warns:
|
|
346
|
-
errors:
|
|
713
|
+
invocations: state2.invocations,
|
|
714
|
+
installsSeen: state2.installsSeen,
|
|
715
|
+
auditsRun: state2.auditsRun,
|
|
716
|
+
blocks: state2.blocks,
|
|
717
|
+
warns: state2.warns,
|
|
718
|
+
errors: state2.errors
|
|
347
719
|
},
|
|
348
|
-
lastResult:
|
|
720
|
+
lastResult: state2.lastResult
|
|
349
721
|
};
|
|
350
722
|
}
|
|
351
723
|
};
|
|
352
|
-
var dependency_vulnerability_gate_default =
|
|
724
|
+
var dependency_vulnerability_gate_default = plugin2;
|
|
353
725
|
export {
|
|
354
|
-
dependency_vulnerability_gate_default as default
|
|
726
|
+
dependency_vulnerability_gate_default as default,
|
|
727
|
+
isInstallCommand
|
|
355
728
|
};
|
package/dist/file-watcher.js
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
// src/file-watcher/index.ts
|
|
2
2
|
import { watch as fsWatch } from "node:fs";
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
// src/runtime/index.ts
|
|
6
|
+
import { basename, extname, isAbsolute, relative, resolve } from "node:path";
|
|
7
|
+
|
|
8
|
+
// src/runtime/local-bin.ts
|
|
9
|
+
import { buildWin32CmdShimInvocation, resolveWin32Command } from "@wrongstack/tools/win32";
|
|
10
|
+
|
|
11
|
+
// src/runtime/index.ts
|
|
12
|
+
var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
13
|
+
function hasLeadingDash(arg) {
|
|
14
|
+
return arg.length > 0 && arg.startsWith("-");
|
|
15
|
+
}
|
|
16
|
+
function withinProjectPath(projectRoot, candidate) {
|
|
17
|
+
if (candidate.length === 0 || candidate.length > 4096) return false;
|
|
18
|
+
if (hasLeadingDash(candidate)) return false;
|
|
19
|
+
const resolved = isAbsolute(candidate) ? resolve(candidate) : resolve(projectRoot, candidate);
|
|
20
|
+
const rel = relative(projectRoot, resolved);
|
|
21
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute(rel);
|
|
22
|
+
}
|
|
5
23
|
function withinProject(p) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const resolved = isAbsolute(p) ? resolve(p) : resolve(root, p);
|
|
9
|
-
const rel = relative(root, resolved);
|
|
10
|
-
if (rel === "" || rel === ".") return true;
|
|
11
|
-
if (rel.startsWith("..")) return false;
|
|
12
|
-
if (isAbsolute(rel)) return false;
|
|
13
|
-
return true;
|
|
24
|
+
const cwd = process.cwd();
|
|
25
|
+
return withinProjectPath(cwd, p) || relative(cwd, p) === ".";
|
|
14
26
|
}
|
|
27
|
+
|
|
28
|
+
// src/file-watcher/index.ts
|
|
29
|
+
var API_VERSION = "^0.1.10";
|
|
15
30
|
var watch_idCounter = 0;
|
|
16
31
|
function nextId() {
|
|
17
32
|
return `watch_${++watch_idCounter}_${Date.now().toString(36)}`;
|