@wrongstack/plugins 0.307.1 → 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
|
@@ -1,8 +1,383 @@
|
|
|
1
1
|
// src/license-audit-gate/index.ts
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { resolve } from "node:path";
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
// src/dep-guard/index.ts
|
|
5
6
|
var state = {
|
|
7
|
+
invocations: 0,
|
|
8
|
+
installsSeen: 0,
|
|
9
|
+
blocks: 0,
|
|
10
|
+
warns: 0,
|
|
11
|
+
llmConfirmCount: 0,
|
|
12
|
+
llmConfirmErrors: 0,
|
|
13
|
+
lastBlock: null,
|
|
14
|
+
hookUnregister: null
|
|
15
|
+
};
|
|
16
|
+
var DEFAULTS = {
|
|
17
|
+
enabled: true,
|
|
18
|
+
mode: "block",
|
|
19
|
+
deny: [],
|
|
20
|
+
allow: [],
|
|
21
|
+
warnOnUnpinned: false,
|
|
22
|
+
typosquatCheck: true,
|
|
23
|
+
confirmTyposquatsWithLlm: false
|
|
24
|
+
};
|
|
25
|
+
function readConfig(raw) {
|
|
26
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS };
|
|
27
|
+
const r = raw;
|
|
28
|
+
const strings = (v) => Array.isArray(v) ? v.filter((s) => typeof s === "string" && s.length > 0) : [];
|
|
29
|
+
return {
|
|
30
|
+
enabled: r["enabled"] !== false,
|
|
31
|
+
mode: r["mode"] === "warn" ? "warn" : "block",
|
|
32
|
+
deny: strings(r["deny"]),
|
|
33
|
+
allow: strings(r["allow"]),
|
|
34
|
+
warnOnUnpinned: r["warnOnUnpinned"] === true,
|
|
35
|
+
typosquatCheck: r["typosquatCheck"] !== false,
|
|
36
|
+
confirmTyposquatsWithLlm: r["confirmTyposquatsWithLlm"] === true
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
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;
|
|
40
|
+
function parseInstallCommands(command) {
|
|
41
|
+
const out = [];
|
|
42
|
+
INSTALL_RE.lastIndex = 0;
|
|
43
|
+
let m = INSTALL_RE.exec(command);
|
|
44
|
+
while (m !== null) {
|
|
45
|
+
const manager = (m[1] ?? m[3] ?? m[5] ?? "").toLowerCase();
|
|
46
|
+
const argString = m[2] ?? m[4] ?? m[6] ?? "";
|
|
47
|
+
const packages = [];
|
|
48
|
+
for (const token of argString.split(/\s+/)) {
|
|
49
|
+
if (!token || token.startsWith("-")) continue;
|
|
50
|
+
if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
|
|
51
|
+
const cleaned = token.replace(/^['"]|['"]$/g, "");
|
|
52
|
+
if (!cleaned) continue;
|
|
53
|
+
let name = cleaned;
|
|
54
|
+
let version = null;
|
|
55
|
+
const pipMatch = /^([A-Za-z0-9_.-]+(?:\[[^\]]+\])?)\s*(==|>=|<=|~=|!=|>|<)\s*(.+)$/.exec(cleaned);
|
|
56
|
+
if (pipMatch?.[1]) {
|
|
57
|
+
name = pipMatch[1];
|
|
58
|
+
const op = pipMatch[2] ?? "";
|
|
59
|
+
const ver = (pipMatch[3] ?? "").trim();
|
|
60
|
+
version = op === "==" || op === "" ? ver || null : `${op}${ver}`;
|
|
61
|
+
} else {
|
|
62
|
+
const at = cleaned.lastIndexOf("@");
|
|
63
|
+
if (at > 0) {
|
|
64
|
+
name = cleaned.slice(0, at);
|
|
65
|
+
version = cleaned.slice(at + 1) || null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (name) packages.push({ name, version });
|
|
69
|
+
}
|
|
70
|
+
out.push({ manager, packages });
|
|
71
|
+
m = INSTALL_RE.exec(command);
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|
|
75
|
+
function matchesPattern(name, pattern) {
|
|
76
|
+
if (pattern.endsWith("*"))
|
|
77
|
+
return name.toLowerCase().startsWith(pattern.slice(0, -1).toLowerCase());
|
|
78
|
+
return name.toLowerCase() === pattern.toLowerCase();
|
|
79
|
+
}
|
|
80
|
+
var POPULAR_PACKAGES = [
|
|
81
|
+
"react",
|
|
82
|
+
"react-dom",
|
|
83
|
+
"express",
|
|
84
|
+
"lodash",
|
|
85
|
+
"axios",
|
|
86
|
+
"typescript",
|
|
87
|
+
"vite",
|
|
88
|
+
"vitest",
|
|
89
|
+
"next",
|
|
90
|
+
"vue",
|
|
91
|
+
"svelte",
|
|
92
|
+
"zod",
|
|
93
|
+
"prettier",
|
|
94
|
+
"eslint",
|
|
95
|
+
"jest",
|
|
96
|
+
"webpack",
|
|
97
|
+
"commander",
|
|
98
|
+
"chalk",
|
|
99
|
+
"dotenv",
|
|
100
|
+
"requests",
|
|
101
|
+
"numpy",
|
|
102
|
+
"pandas",
|
|
103
|
+
"flask",
|
|
104
|
+
"django",
|
|
105
|
+
"serde",
|
|
106
|
+
"tokio"
|
|
107
|
+
];
|
|
108
|
+
function editDistance(a, b) {
|
|
109
|
+
if (a === b) return 0;
|
|
110
|
+
if (Math.abs(a.length - b.length) > 2) return 3;
|
|
111
|
+
const d = Array.from({ length: a.length + 1 }, (_, i) => {
|
|
112
|
+
const row = new Array(b.length + 1).fill(0);
|
|
113
|
+
row[0] = i;
|
|
114
|
+
return row;
|
|
115
|
+
});
|
|
116
|
+
for (let j = 0; j <= b.length; j++) d[0][j] = j;
|
|
117
|
+
for (let i = 1; i <= a.length; i++) {
|
|
118
|
+
for (let j = 1; j <= b.length; j++) {
|
|
119
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
120
|
+
const row = d[i];
|
|
121
|
+
const prevRow = d[i - 1];
|
|
122
|
+
row[j] = Math.min(
|
|
123
|
+
prevRow[j] + 1,
|
|
124
|
+
row[j - 1] + 1,
|
|
125
|
+
prevRow[j - 1] + cost
|
|
126
|
+
);
|
|
127
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
128
|
+
row[j] = Math.min(row[j], d[i - 2][j - 2] + 1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return d[a.length][b.length];
|
|
133
|
+
}
|
|
134
|
+
function typosquatOf(name) {
|
|
135
|
+
const lower = name.toLowerCase().replace(/^@[^/]+\//, "");
|
|
136
|
+
if (POPULAR_PACKAGES.includes(lower)) return null;
|
|
137
|
+
for (const popular of POPULAR_PACKAGES) {
|
|
138
|
+
if (editDistance(lower, popular) === 1) return popular;
|
|
139
|
+
}
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
var plugin = {
|
|
143
|
+
name: "dep-guard",
|
|
144
|
+
version: "0.1.0",
|
|
145
|
+
description: "Supervises dependency installs: blocks deny-listed packages, flags typosquat lookalikes, and optionally warns on unpinned versions",
|
|
146
|
+
apiVersion: "^0.1.10",
|
|
147
|
+
capabilities: { tools: true, hooks: true, llm: true },
|
|
148
|
+
defaultConfig: { ...DEFAULTS },
|
|
149
|
+
configSchema: {
|
|
150
|
+
type: "object",
|
|
151
|
+
properties: {
|
|
152
|
+
enabled: { type: "boolean", default: true, description: "Master switch." },
|
|
153
|
+
mode: {
|
|
154
|
+
type: "string",
|
|
155
|
+
enum: ["block", "warn"],
|
|
156
|
+
default: "block",
|
|
157
|
+
description: "How deny-list hits are handled."
|
|
158
|
+
},
|
|
159
|
+
deny: {
|
|
160
|
+
type: "array",
|
|
161
|
+
items: { type: "string" },
|
|
162
|
+
default: [],
|
|
163
|
+
description: 'Package names (exact) or prefix globs ("@evil/*") that must not be installed.'
|
|
164
|
+
},
|
|
165
|
+
allow: {
|
|
166
|
+
type: "array",
|
|
167
|
+
items: { type: "string" },
|
|
168
|
+
default: [],
|
|
169
|
+
description: "Exemptions that override deny."
|
|
170
|
+
},
|
|
171
|
+
warnOnUnpinned: {
|
|
172
|
+
type: "boolean",
|
|
173
|
+
default: false,
|
|
174
|
+
description: "Warn when a package is installed without an explicit version."
|
|
175
|
+
},
|
|
176
|
+
typosquatCheck: {
|
|
177
|
+
type: "boolean",
|
|
178
|
+
default: true,
|
|
179
|
+
description: "Warn when a package name is one edit away from a well-known package."
|
|
180
|
+
},
|
|
181
|
+
confirmTyposquatsWithLlm: {
|
|
182
|
+
type: "boolean",
|
|
183
|
+
default: false,
|
|
184
|
+
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."
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
setup(api) {
|
|
189
|
+
state.invocations = 0;
|
|
190
|
+
state.installsSeen = 0;
|
|
191
|
+
state.blocks = 0;
|
|
192
|
+
state.warns = 0;
|
|
193
|
+
state.llmConfirmCount = 0;
|
|
194
|
+
state.llmConfirmErrors = 0;
|
|
195
|
+
state.lastBlock = null;
|
|
196
|
+
if (state.hookUnregister) {
|
|
197
|
+
try {
|
|
198
|
+
state.hookUnregister();
|
|
199
|
+
} catch {
|
|
200
|
+
}
|
|
201
|
+
state.hookUnregister = null;
|
|
202
|
+
}
|
|
203
|
+
const cfg = readConfig(api.config.extensions?.["dep-guard"]);
|
|
204
|
+
const hook = async (input) => {
|
|
205
|
+
if (!cfg.enabled) return;
|
|
206
|
+
state.invocations += 1;
|
|
207
|
+
const ti = input.toolInput ?? {};
|
|
208
|
+
const command = typeof ti["command"] === "string" ? ti["command"] : "";
|
|
209
|
+
if (!command) return;
|
|
210
|
+
const installs = parseInstallCommands(command);
|
|
211
|
+
const packages = installs.flatMap((i) => i.packages);
|
|
212
|
+
if (packages.length === 0) return;
|
|
213
|
+
state.installsSeen += 1;
|
|
214
|
+
api.metrics.counter("installs_seen");
|
|
215
|
+
const notes = [];
|
|
216
|
+
for (const pkg of packages) {
|
|
217
|
+
const allowed = cfg.allow.some((p) => matchesPattern(pkg.name, p));
|
|
218
|
+
const denied = !allowed && cfg.deny.some((p) => matchesPattern(pkg.name, p));
|
|
219
|
+
if (denied) {
|
|
220
|
+
if (cfg.mode === "block") {
|
|
221
|
+
state.blocks += 1;
|
|
222
|
+
state.lastBlock = {
|
|
223
|
+
pkg: pkg.name,
|
|
224
|
+
command: command.slice(0, 200),
|
|
225
|
+
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
226
|
+
};
|
|
227
|
+
api.metrics.counter("blocks");
|
|
228
|
+
return {
|
|
229
|
+
decision: "block",
|
|
230
|
+
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.`
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
notes.push(
|
|
234
|
+
`"${pkg.name}" is DENY-LISTED \u2014 do not add it without explicit user approval.`
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
if (cfg.typosquatCheck) {
|
|
238
|
+
const lookalike = typosquatOf(pkg.name);
|
|
239
|
+
if (lookalike) {
|
|
240
|
+
const baseNote = `"${pkg.name}" is one edit away from the well-known package "${lookalike}" \u2014 possible typosquat. Verify the name before installing.`;
|
|
241
|
+
if (cfg.confirmTyposquatsWithLlm && api.llm) {
|
|
242
|
+
try {
|
|
243
|
+
const question = `Classify whether npm package "${pkg.name}" is likely a typo or typosquat of "${lookalike}".`;
|
|
244
|
+
const council = api.llm.council ? await api.llm.council(question, {
|
|
245
|
+
context: "The only supplied evidence is that the names have edit distance 1. Do not invent registry, download, ownership, or provenance facts.",
|
|
246
|
+
profile: "risk-review",
|
|
247
|
+
options: [
|
|
248
|
+
{ id: "typo", label: "Likely typo or typosquat" },
|
|
249
|
+
{ id: "real", label: "Likely distinct real package" },
|
|
250
|
+
{ id: "uncertain", label: "Insufficient evidence" }
|
|
251
|
+
]
|
|
252
|
+
}) : null;
|
|
253
|
+
const councilVerdict = council?.status === "decided" && council.optionId ? `${council.optionId.toUpperCase()}: ${council.reason ?? council.answer ?? "no rationale"}` : null;
|
|
254
|
+
const t = councilVerdict ? councilVerdict : (await api.llm.complete(
|
|
255
|
+
`${question} Reply with ONE sentence starting with "TYPO:", "REAL:", or "UNCERTAIN:".`,
|
|
256
|
+
{
|
|
257
|
+
system: "You are a supply-chain security assistant. Use only supplied evidence and preserve uncertainty.",
|
|
258
|
+
role: "security-reviewer",
|
|
259
|
+
maxTokens: 100
|
|
260
|
+
}
|
|
261
|
+
)).text.trim();
|
|
262
|
+
if (t) {
|
|
263
|
+
state.llmConfirmCount += 1;
|
|
264
|
+
api.metrics.counter("llm_confirm");
|
|
265
|
+
notes.push(`${baseNote} LLM verdict: ${t.slice(0, 300)}`);
|
|
266
|
+
} else {
|
|
267
|
+
notes.push(baseNote);
|
|
268
|
+
}
|
|
269
|
+
} catch {
|
|
270
|
+
state.llmConfirmErrors += 1;
|
|
271
|
+
notes.push(baseNote);
|
|
272
|
+
}
|
|
273
|
+
} else {
|
|
274
|
+
notes.push(baseNote);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
if (cfg.warnOnUnpinned && pkg.version === null) {
|
|
279
|
+
notes.push(`"${pkg.name}" has no pinned version \u2014 consider "${pkg.name}@<version>".`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (notes.length > 0) {
|
|
283
|
+
state.warns += 1;
|
|
284
|
+
api.metrics.counter("warns");
|
|
285
|
+
return {
|
|
286
|
+
decision: "allow",
|
|
287
|
+
additionalContext: `dep-guard:
|
|
288
|
+
${notes.map((n) => ` - ${n}`).join("\n")}`
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
return {
|
|
292
|
+
decision: "allow",
|
|
293
|
+
additionalContext: `dep-guard: this command adds ${packages.length} dependenc${packages.length === 1 ? "y" : "ies"}: ${packages.map((p) => p.name).join(", ")}. Confirm each is intentional.`
|
|
294
|
+
};
|
|
295
|
+
};
|
|
296
|
+
state.hookUnregister = api.registerHook("PreToolUse", "bash|exec", hook, {
|
|
297
|
+
name: "dep-guard",
|
|
298
|
+
stage: "validate",
|
|
299
|
+
failurePolicy: "closed",
|
|
300
|
+
policy: true
|
|
301
|
+
});
|
|
302
|
+
api.tools.register({
|
|
303
|
+
name: "dep_guard_status",
|
|
304
|
+
description: "Reports dep-guard state: deny/allow lists, mode, and counters (installs seen, blocks, warns).",
|
|
305
|
+
inputSchema: { type: "object", properties: {} },
|
|
306
|
+
permission: "auto",
|
|
307
|
+
category: "Diagnostics",
|
|
308
|
+
mutating: false,
|
|
309
|
+
async execute() {
|
|
310
|
+
return {
|
|
311
|
+
ok: true,
|
|
312
|
+
enabled: cfg.enabled,
|
|
313
|
+
mode: cfg.mode,
|
|
314
|
+
deny: cfg.deny,
|
|
315
|
+
allow: cfg.allow,
|
|
316
|
+
warnOnUnpinned: cfg.warnOnUnpinned,
|
|
317
|
+
typosquatCheck: cfg.typosquatCheck,
|
|
318
|
+
counters: {
|
|
319
|
+
invocations: state.invocations,
|
|
320
|
+
installsSeen: state.installsSeen,
|
|
321
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
322
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
323
|
+
blocks: state.blocks,
|
|
324
|
+
warns: state.warns
|
|
325
|
+
},
|
|
326
|
+
lastBlock: state.lastBlock
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
api.log.info("dep-guard plugin loaded", {
|
|
331
|
+
version: "0.1.0",
|
|
332
|
+
enabled: cfg.enabled,
|
|
333
|
+
mode: cfg.mode,
|
|
334
|
+
denyCount: cfg.deny.length
|
|
335
|
+
});
|
|
336
|
+
},
|
|
337
|
+
teardown(api) {
|
|
338
|
+
if (state.hookUnregister) {
|
|
339
|
+
try {
|
|
340
|
+
state.hookUnregister();
|
|
341
|
+
} catch {
|
|
342
|
+
}
|
|
343
|
+
state.hookUnregister = null;
|
|
344
|
+
}
|
|
345
|
+
const final = {
|
|
346
|
+
invocations: state.invocations,
|
|
347
|
+
installsSeen: state.installsSeen,
|
|
348
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
349
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
350
|
+
blocks: state.blocks,
|
|
351
|
+
warns: state.warns
|
|
352
|
+
};
|
|
353
|
+
state.invocations = 0;
|
|
354
|
+
state.installsSeen = 0;
|
|
355
|
+
state.blocks = 0;
|
|
356
|
+
state.warns = 0;
|
|
357
|
+
state.llmConfirmCount = 0;
|
|
358
|
+
state.llmConfirmErrors = 0;
|
|
359
|
+
state.lastBlock = null;
|
|
360
|
+
api.log.info("dep-guard: teardown complete", { final });
|
|
361
|
+
},
|
|
362
|
+
async health() {
|
|
363
|
+
return {
|
|
364
|
+
ok: true,
|
|
365
|
+
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}`,
|
|
366
|
+
counters: {
|
|
367
|
+
invocations: state.invocations,
|
|
368
|
+
installsSeen: state.installsSeen,
|
|
369
|
+
llmConfirmCount: state.llmConfirmCount,
|
|
370
|
+
llmConfirmErrors: state.llmConfirmErrors,
|
|
371
|
+
blocks: state.blocks,
|
|
372
|
+
warns: state.warns
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// src/license-audit-gate/index.ts
|
|
379
|
+
var API_VERSION = "^0.1.10";
|
|
380
|
+
var state2 = {
|
|
6
381
|
invocations: 0,
|
|
7
382
|
installsSeen: 0,
|
|
8
383
|
packagesAudited: 0,
|
|
@@ -13,7 +388,7 @@ var state = {
|
|
|
13
388
|
lastResult: null,
|
|
14
389
|
hookUnregister: null
|
|
15
390
|
};
|
|
16
|
-
var
|
|
391
|
+
var DEFAULTS2 = {
|
|
17
392
|
enabled: true,
|
|
18
393
|
allowedLicenses: ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "0BSD", "Unlicense"],
|
|
19
394
|
block: true
|
|
@@ -22,13 +397,13 @@ function normalizeStrings(v) {
|
|
|
22
397
|
if (!Array.isArray(v)) return [];
|
|
23
398
|
return v.filter((s) => typeof s === "string" && s.length > 0).map((s) => s.trim());
|
|
24
399
|
}
|
|
25
|
-
function
|
|
26
|
-
if (!raw || typeof raw !== "object") return { ...
|
|
400
|
+
function readConfig2(raw) {
|
|
401
|
+
if (!raw || typeof raw !== "object") return { ...DEFAULTS2 };
|
|
27
402
|
const r = raw;
|
|
28
403
|
const allowed = normalizeStrings(r["allowedLicenses"]);
|
|
29
404
|
return {
|
|
30
405
|
enabled: r["enabled"] !== false,
|
|
31
|
-
allowedLicenses: allowed.length > 0 ? allowed : [...
|
|
406
|
+
allowedLicenses: allowed.length > 0 ? allowed : [...DEFAULTS2.allowedLicenses],
|
|
32
407
|
block: r["block"] !== false
|
|
33
408
|
};
|
|
34
409
|
}
|
|
@@ -50,24 +425,10 @@ function extractLicenseStrings(pkg) {
|
|
|
50
425
|
}
|
|
51
426
|
return [...new Set(out)];
|
|
52
427
|
}
|
|
53
|
-
var INSTALL_RE = /(?:^|[;&|]\s*)(npm|pnpm|yarn|bun)\s+(?:install|i|add)\s+([^;&|]+)/gi;
|
|
54
428
|
function parsePackageNames(command) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const argString = m[2] ?? "";
|
|
59
|
-
for (const token of argString.split(/\s+/)) {
|
|
60
|
-
if (!token || token.startsWith("-")) continue;
|
|
61
|
-
if (/^(\.|\/|file:|git\+|https?:)/i.test(token) || token.endsWith(".tgz")) continue;
|
|
62
|
-
const cleaned = token.replace(/^['"]|['"]$/g, "");
|
|
63
|
-
if (!cleaned) continue;
|
|
64
|
-
let name = cleaned;
|
|
65
|
-
const at = cleaned.lastIndexOf("@");
|
|
66
|
-
if (at > 0) name = cleaned.slice(0, at);
|
|
67
|
-
if (name) names.push(name);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return [...new Set(names)];
|
|
429
|
+
return [
|
|
430
|
+
...new Set(parseInstallCommands(command).flatMap((entry) => entry.packages.map((pkg) => pkg.name)))
|
|
431
|
+
];
|
|
71
432
|
}
|
|
72
433
|
function auditPackages(names, allowedLicenses) {
|
|
73
434
|
const results = [];
|
|
@@ -88,13 +449,13 @@ function auditPackages(names, allowedLicenses) {
|
|
|
88
449
|
const ok = errors.length === 0 && results.every((r) => r.allowed);
|
|
89
450
|
return { ok, results, errors };
|
|
90
451
|
}
|
|
91
|
-
var
|
|
452
|
+
var plugin2 = {
|
|
92
453
|
name: "license-audit-gate",
|
|
93
454
|
version: "0.1.0",
|
|
94
455
|
description: "PostToolUse hook that audits dependency licenses after package-manager install/add commands and blocks disallowed licenses",
|
|
95
456
|
apiVersion: API_VERSION,
|
|
96
457
|
capabilities: { tools: true, hooks: true },
|
|
97
|
-
defaultConfig: { ...
|
|
458
|
+
defaultConfig: { ...DEFAULTS2 },
|
|
98
459
|
configSchema: {
|
|
99
460
|
type: "object",
|
|
100
461
|
properties: {
|
|
@@ -106,7 +467,7 @@ var plugin = {
|
|
|
106
467
|
allowedLicenses: {
|
|
107
468
|
type: "array",
|
|
108
469
|
items: { type: "string" },
|
|
109
|
-
default:
|
|
470
|
+
default: DEFAULTS2.allowedLicenses,
|
|
110
471
|
description: "List of allowed SPDX/license identifiers."
|
|
111
472
|
},
|
|
112
473
|
block: {
|
|
@@ -117,39 +478,39 @@ var plugin = {
|
|
|
117
478
|
}
|
|
118
479
|
},
|
|
119
480
|
setup(api) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
481
|
+
state2.invocations = 0;
|
|
482
|
+
state2.installsSeen = 0;
|
|
483
|
+
state2.packagesAudited = 0;
|
|
484
|
+
state2.allowedCount = 0;
|
|
485
|
+
state2.deniedCount = 0;
|
|
486
|
+
state2.blockedCount = 0;
|
|
487
|
+
state2.errorCount = 0;
|
|
488
|
+
state2.lastResult = null;
|
|
489
|
+
if (state2.hookUnregister) {
|
|
129
490
|
try {
|
|
130
|
-
|
|
491
|
+
state2.hookUnregister();
|
|
131
492
|
} catch {
|
|
132
493
|
}
|
|
133
|
-
|
|
494
|
+
state2.hookUnregister = null;
|
|
134
495
|
}
|
|
135
|
-
const cfg =
|
|
496
|
+
const cfg = readConfig2(api.config.extensions?.["license-audit-gate"]);
|
|
136
497
|
const hook = (input) => {
|
|
137
498
|
if (!cfg.enabled) return;
|
|
138
499
|
if (input.toolResult?.isError) return;
|
|
139
500
|
const ti = input.toolInput ?? {};
|
|
140
501
|
const command = typeof ti["command"] === "string" ? ti["command"] : "";
|
|
141
502
|
if (!command) return;
|
|
142
|
-
|
|
503
|
+
state2.invocations += 1;
|
|
143
504
|
const names = parsePackageNames(command);
|
|
144
505
|
if (names.length === 0) return;
|
|
145
|
-
|
|
506
|
+
state2.installsSeen += 1;
|
|
146
507
|
const audit = auditPackages(names, cfg.allowedLicenses);
|
|
147
|
-
|
|
508
|
+
state2.packagesAudited += names.length;
|
|
148
509
|
const denied = audit.results.filter((r) => !r.allowed).map((r) => r.name);
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
510
|
+
state2.allowedCount += audit.results.filter((r) => r.allowed).length;
|
|
511
|
+
state2.deniedCount += denied.length;
|
|
512
|
+
state2.errorCount += audit.errors.length;
|
|
513
|
+
state2.lastResult = {
|
|
153
514
|
passed: audit.ok,
|
|
154
515
|
denied,
|
|
155
516
|
when: (/* @__PURE__ */ new Date()).toISOString()
|
|
@@ -167,7 +528,7 @@ var plugin = {
|
|
|
167
528
|
Allowed licenses: ${cfg.allowedLicenses.join(", ")}
|
|
168
529
|
` + lines.join("\n");
|
|
169
530
|
if (cfg.block) {
|
|
170
|
-
|
|
531
|
+
state2.blockedCount += 1;
|
|
171
532
|
return {
|
|
172
533
|
decision: "block",
|
|
173
534
|
reason: message,
|
|
@@ -176,7 +537,7 @@ Allowed licenses: ${cfg.allowedLicenses.join(", ")}
|
|
|
176
537
|
}
|
|
177
538
|
return { additionalContext: message };
|
|
178
539
|
};
|
|
179
|
-
|
|
540
|
+
state2.hookUnregister = api.registerHook("PostToolUse", "bash|exec", hook);
|
|
180
541
|
api.tools.register({
|
|
181
542
|
name: "license_audit_status",
|
|
182
543
|
description: "Reports license-audit-gate state: allowlist, block mode, and per-session audit counters.",
|
|
@@ -191,15 +552,15 @@ Allowed licenses: ${cfg.allowedLicenses.join(", ")}
|
|
|
191
552
|
allowedLicenses: cfg.allowedLicenses,
|
|
192
553
|
block: cfg.block,
|
|
193
554
|
counters: {
|
|
194
|
-
invocations:
|
|
195
|
-
installsSeen:
|
|
196
|
-
packagesAudited:
|
|
197
|
-
allowed:
|
|
198
|
-
denied:
|
|
199
|
-
blocked:
|
|
200
|
-
errors:
|
|
555
|
+
invocations: state2.invocations,
|
|
556
|
+
installsSeen: state2.installsSeen,
|
|
557
|
+
packagesAudited: state2.packagesAudited,
|
|
558
|
+
allowed: state2.allowedCount,
|
|
559
|
+
denied: state2.deniedCount,
|
|
560
|
+
blocked: state2.blockedCount,
|
|
561
|
+
errors: state2.errorCount
|
|
201
562
|
},
|
|
202
|
-
lastResult:
|
|
563
|
+
lastResult: state2.lastResult
|
|
203
564
|
};
|
|
204
565
|
}
|
|
205
566
|
});
|
|
@@ -210,50 +571,51 @@ Allowed licenses: ${cfg.allowedLicenses.join(", ")}
|
|
|
210
571
|
});
|
|
211
572
|
},
|
|
212
573
|
teardown(api) {
|
|
213
|
-
if (
|
|
574
|
+
if (state2.hookUnregister) {
|
|
214
575
|
try {
|
|
215
|
-
|
|
576
|
+
state2.hookUnregister();
|
|
216
577
|
} catch {
|
|
217
578
|
}
|
|
218
|
-
|
|
579
|
+
state2.hookUnregister = null;
|
|
219
580
|
}
|
|
220
581
|
const final = {
|
|
221
|
-
invocations:
|
|
222
|
-
installsSeen:
|
|
223
|
-
packagesAudited:
|
|
224
|
-
allowed:
|
|
225
|
-
denied:
|
|
226
|
-
blocked:
|
|
227
|
-
errors:
|
|
582
|
+
invocations: state2.invocations,
|
|
583
|
+
installsSeen: state2.installsSeen,
|
|
584
|
+
packagesAudited: state2.packagesAudited,
|
|
585
|
+
allowed: state2.allowedCount,
|
|
586
|
+
denied: state2.deniedCount,
|
|
587
|
+
blocked: state2.blockedCount,
|
|
588
|
+
errors: state2.errorCount
|
|
228
589
|
};
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
590
|
+
state2.invocations = 0;
|
|
591
|
+
state2.installsSeen = 0;
|
|
592
|
+
state2.packagesAudited = 0;
|
|
593
|
+
state2.allowedCount = 0;
|
|
594
|
+
state2.deniedCount = 0;
|
|
595
|
+
state2.blockedCount = 0;
|
|
596
|
+
state2.errorCount = 0;
|
|
597
|
+
state2.lastResult = null;
|
|
237
598
|
api.log.info("license-audit-gate: teardown complete", { final });
|
|
238
599
|
},
|
|
239
600
|
async health() {
|
|
240
601
|
return {
|
|
241
602
|
ok: true,
|
|
242
|
-
message:
|
|
603
|
+
message: state2.lastResult ? `license-audit-gate: ${state2.installsSeen} install command(s) seen, ${state2.deniedCount} denied, ${state2.blockedCount} blocked` : `license-audit-gate: ${state2.invocations} invocation(s), ${state2.packagesAudited} package(s) audited`,
|
|
243
604
|
counters: {
|
|
244
|
-
invocations:
|
|
245
|
-
installsSeen:
|
|
246
|
-
packagesAudited:
|
|
247
|
-
allowed:
|
|
248
|
-
denied:
|
|
249
|
-
blocked:
|
|
250
|
-
errors:
|
|
605
|
+
invocations: state2.invocations,
|
|
606
|
+
installsSeen: state2.installsSeen,
|
|
607
|
+
packagesAudited: state2.packagesAudited,
|
|
608
|
+
allowed: state2.allowedCount,
|
|
609
|
+
denied: state2.deniedCount,
|
|
610
|
+
blocked: state2.blockedCount,
|
|
611
|
+
errors: state2.errorCount
|
|
251
612
|
},
|
|
252
|
-
lastResult:
|
|
613
|
+
lastResult: state2.lastResult
|
|
253
614
|
};
|
|
254
615
|
}
|
|
255
616
|
};
|
|
256
|
-
var license_audit_gate_default =
|
|
617
|
+
var license_audit_gate_default = plugin2;
|
|
257
618
|
export {
|
|
258
|
-
license_audit_gate_default as default
|
|
619
|
+
license_audit_gate_default as default,
|
|
620
|
+
parsePackageNames
|
|
259
621
|
};
|
|
@@ -26,6 +26,19 @@
|
|
|
26
26
|
* @public
|
|
27
27
|
*/
|
|
28
28
|
import type { Plugin } from '@wrongstack/core/types';
|
|
29
|
+
interface ResolvedLinter {
|
|
30
|
+
/** Always the current Node executable; never a shell or package-manager shim. */
|
|
31
|
+
cmd: string;
|
|
32
|
+
/** Local package bin entry followed by linter-specific arguments. */
|
|
33
|
+
args: string[];
|
|
34
|
+
name: 'biome' | 'eslint';
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a linter's project-local JavaScript bin entry from package metadata.
|
|
38
|
+
* Using `node <entry>` avoids npx, shell configuration, and Windows `.cmd`
|
|
39
|
+
* shims while still respecting Node's normal project-local package lookup.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveLocalLinter(name: 'biome' | 'eslint', cwd: string): ResolvedLinter | null;
|
|
29
42
|
declare const plugin: Plugin;
|
|
30
43
|
export default plugin;
|
|
31
44
|
//# sourceMappingURL=index.d.ts.map
|