@vola-trebla/croak 0.1.0 → 0.1.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/dist/cli.js +1 -27
- package/dist/crypto/der.js +0 -13
- package/dist/crypto/fields.js +0 -11
- package/dist/crypto/registry-signature.js +0 -12
- package/dist/crypto/rekor.js +0 -13
- package/dist/crypto/sigstore.js +0 -9
- package/dist/detectors/git-config.js +0 -4
- package/dist/detectors/hidden-code.js +0 -7
- package/dist/detectors/ide-autorun.js +0 -9
- package/dist/detectors/index.js +0 -1
- package/dist/detectors/lifecycle-script.js +2 -41
- package/dist/detectors/node-modules-present.js +0 -7
- package/dist/detectors/obfuscation.js +0 -26
- package/dist/detectors/unscanned.js +0 -8
- package/dist/hook/hook.js +2 -45
- package/dist/hook/install-commands.js +1 -36
- package/dist/install/content-cache.js +0 -8
- package/dist/install/content.js +0 -10
- package/dist/install/detectors.js +0 -6
- package/dist/install/installed.js +3 -19
- package/dist/install/lockfile.js +0 -7
- package/dist/install/metadata.js +1 -31
- package/dist/install/plan.js +0 -10
- package/dist/install/pnpm-builds.js +0 -13
- package/dist/install/pnpm-lockfile.js +0 -14
- package/dist/install/popular.js +0 -5
- package/dist/install/project.js +0 -20
- package/dist/install/provenance.js +0 -21
- package/dist/install/registry.js +0 -33
- package/dist/install/sanitize.js +0 -14
- package/dist/install/scan.js +0 -31
- package/dist/install/signature.js +0 -7
- package/dist/install/tar.js +1 -8
- package/dist/install/workspace.js +0 -1
- package/dist/install/yaml.js +0 -26
- package/dist/llm/client.js +0 -8
- package/dist/llm/cost.js +0 -2
- package/dist/llm/diff.js +0 -14
- package/dist/llm/plan-step.js +0 -5
- package/dist/llm/select.js +0 -6
- package/dist/llm/triage.js +0 -10
- package/dist/repo-report.js +0 -11
- package/dist/repo-snapshot.js +0 -17
- package/dist/scan.js +0 -5
- package/dist/signals/code.js +0 -29
- package/dist/signals/git-config.js +0 -7
- package/dist/signals/hidden.js +0 -17
- package/dist/signals/jsonc.js +0 -10
- package/dist/signals/obfuscation.js +0 -11
- package/dist/signals/shell.js +0 -40
- package/dist/signals/text.js +0 -1
- package/dist/ui.js +0 -1
- package/dist/verdict.js +0 -1
- package/package.json +1 -1
package/dist/signals/shell.js
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A small, defensive shell parser for package.json scripts. It understands
|
|
3
|
-
* quoting, pipes, command lists, `$(...)`, backticks, `<(...)`, subshells,
|
|
4
|
-
* redirects, and `sh -c '...'` — enough to see what a script really runs,
|
|
5
|
-
* without one fragile regex. It never executes anything.
|
|
6
|
-
*
|
|
7
|
-
* Linear scanning only; input length and nesting depth are capped so hostile
|
|
8
|
-
* scripts cannot make it hang.
|
|
9
|
-
*/
|
|
10
1
|
export const MAX_SCRIPT_LENGTH = 10_000;
|
|
11
2
|
const MAX_DEPTH = 8;
|
|
12
3
|
const DOWNLOADERS = new Set([
|
|
@@ -24,7 +15,6 @@ const DOWNLOADERS = new Set([
|
|
|
24
15
|
"certutil",
|
|
25
16
|
"bitsadmin",
|
|
26
17
|
]);
|
|
27
|
-
/** Commands that execute code they are given via stdin, arguments, or files. */
|
|
28
18
|
const INTERPRETERS = new Set([
|
|
29
19
|
"sh",
|
|
30
20
|
"bash",
|
|
@@ -75,7 +65,6 @@ const WRAPPERS = new Set([
|
|
|
75
65
|
"setsid",
|
|
76
66
|
"ionice",
|
|
77
67
|
]);
|
|
78
|
-
/** Shell keywords that start or end a compound command, not a command name. */
|
|
79
68
|
const RESERVED = new Set([
|
|
80
69
|
"{",
|
|
81
70
|
"}",
|
|
@@ -96,7 +85,6 @@ const RESERVED = new Set([
|
|
|
96
85
|
]);
|
|
97
86
|
const PACKAGE_RUNNERS = new Set(["npm", "pnpm", "yarn"]);
|
|
98
87
|
const ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
99
|
-
/** Package-manager subcommands that are not package.json script names. */
|
|
100
88
|
const PM_BUILTINS = new Set([
|
|
101
89
|
"install",
|
|
102
90
|
"i",
|
|
@@ -129,9 +117,7 @@ const PM_BUILTINS = new Set([
|
|
|
129
117
|
"set",
|
|
130
118
|
"help",
|
|
131
119
|
]);
|
|
132
|
-
/** npm runs these scripts without `run`. */
|
|
133
120
|
const NPM_SCRIPT_ALIASES = new Set(["test", "start", "stop", "restart"]);
|
|
134
|
-
/** The package.json script a package-manager command runs, if any. */
|
|
135
121
|
function scriptName(name, args) {
|
|
136
122
|
if (!PACKAGE_RUNNERS.has(name) && name !== "bun")
|
|
137
123
|
return undefined;
|
|
@@ -147,7 +133,6 @@ function scriptName(name, args) {
|
|
|
147
133
|
}
|
|
148
134
|
class ParseError extends Error {
|
|
149
135
|
}
|
|
150
|
-
/** Index just past the `close` that balances the opener before `start`. */
|
|
151
136
|
function findClose(input, start, open, close) {
|
|
152
137
|
let depth = 1;
|
|
153
138
|
for (let i = start; i < input.length; i++) {
|
|
@@ -260,7 +245,6 @@ function parse(input, depth) {
|
|
|
260
245
|
}
|
|
261
246
|
else if (ch === '"') {
|
|
262
247
|
const end = findDoubleQuoteEnd(input, i + 1);
|
|
263
|
-
// Inside double quotes, $(...) and backticks still run.
|
|
264
248
|
const body = input.slice(i + 1, end);
|
|
265
249
|
for (let j = 0; j < body.length; j++) {
|
|
266
250
|
if (body[j] === "\\") {
|
|
@@ -300,7 +284,6 @@ function parse(input, depth) {
|
|
|
300
284
|
i = close;
|
|
301
285
|
}
|
|
302
286
|
else if (ch === "#" && !inWord) {
|
|
303
|
-
// A comment runs to the end of the line, quotes and all.
|
|
304
287
|
const newline = input.indexOf("\n", i);
|
|
305
288
|
i = (newline === -1 ? input.length : newline) - 1;
|
|
306
289
|
}
|
|
@@ -308,7 +291,6 @@ function parse(input, depth) {
|
|
|
308
291
|
!inWord &&
|
|
309
292
|
words.length === 0 &&
|
|
310
293
|
(next === " " || next === "\t" || next === "\n")) {
|
|
311
|
-
// { …; } groups commands like a subshell does.
|
|
312
294
|
const close = findClose(input, i + 1, "{", "}");
|
|
313
295
|
subshell = parse(input.slice(i + 1, close - 1), depth + 1);
|
|
314
296
|
i = close - 1;
|
|
@@ -335,7 +317,6 @@ function parse(input, depth) {
|
|
|
335
317
|
endPipeline();
|
|
336
318
|
}
|
|
337
319
|
else if (ch === ">" || ch === "<") {
|
|
338
|
-
// A leading fd number (2>) belongs to the redirect, not to a word.
|
|
339
320
|
if (inWord && /^\d+$/.test(text) && subs.length === 0) {
|
|
340
321
|
text = "";
|
|
341
322
|
inWord = false;
|
|
@@ -359,14 +340,12 @@ function parse(input, depth) {
|
|
|
359
340
|
endPipeline();
|
|
360
341
|
return script;
|
|
361
342
|
}
|
|
362
|
-
// ---------------------------------------------------------------- analysis
|
|
363
343
|
const basename = (path) => path.slice(path.lastIndexOf("/") + 1);
|
|
364
344
|
const normalizeName = (word) => basename(word)
|
|
365
345
|
.toLowerCase()
|
|
366
346
|
.replace(/\.exe$/, "");
|
|
367
347
|
const stripDot = (path) => path.startsWith("./") ? path.slice(2) : path;
|
|
368
348
|
const urlFileName = (url) => basename(url.split(/[?#]/)[0] ?? "");
|
|
369
|
-
/** The real command after env assignments and wrappers, with its arguments. */
|
|
370
349
|
const NPX_LIKE = new Set(["npx", "pnpx", "bunx"]);
|
|
371
350
|
const WORKSPACE_FLAGS = new Set([
|
|
372
351
|
"-r",
|
|
@@ -379,7 +358,6 @@ const WORKSPACE_FLAGS = new Set([
|
|
|
379
358
|
"-ws",
|
|
380
359
|
"--if-present",
|
|
381
360
|
]);
|
|
382
|
-
/** The real command after env assignments and wrappers, with its arguments. */
|
|
383
361
|
function resolveCommand(command) {
|
|
384
362
|
const words = [...command.words];
|
|
385
363
|
const modifiers = [];
|
|
@@ -400,7 +378,6 @@ function resolveCommand(command) {
|
|
|
400
378
|
if (WRAPPERS.has(name)) {
|
|
401
379
|
words.shift();
|
|
402
380
|
const flags = [];
|
|
403
|
-
// Skip wrapper flags (sudo -u user, nice -n 5, npx --yes).
|
|
404
381
|
while (words[0]?.text.startsWith("-")) {
|
|
405
382
|
const flag = words.shift()?.text ?? "";
|
|
406
383
|
flags.push(flag);
|
|
@@ -409,10 +386,8 @@ function resolveCommand(command) {
|
|
|
409
386
|
words.shift();
|
|
410
387
|
}
|
|
411
388
|
}
|
|
412
|
-
// timeout's first operand is the duration, not the command.
|
|
413
389
|
if (name === "timeout" && /^\d/.test(words[0]?.text ?? ""))
|
|
414
390
|
words.shift();
|
|
415
|
-
// A bare npx runs a local bin; npx with flags or any other wrapper changes what runs.
|
|
416
391
|
if (!NPX_LIKE.has(name) || flags.length > 0)
|
|
417
392
|
modifiers.push(`runs through ${name}${flags.length > 0 ? ` ${flags.join(" ")}` : ""}`);
|
|
418
393
|
continue;
|
|
@@ -459,7 +434,6 @@ function addLocal(acc, path) {
|
|
|
459
434
|
if (!acc.localFiles.includes(clean))
|
|
460
435
|
acc.localFiles.push(clean);
|
|
461
436
|
}
|
|
462
|
-
/** Files a downloader writes, so a later command that runs them is caught. */
|
|
463
437
|
function downloadTargets(name, args, redirects) {
|
|
464
438
|
const targets = [];
|
|
465
439
|
const url = args.find((a) => /^[a-z]+:\/\//i.test(a.text))?.text ?? "";
|
|
@@ -468,7 +442,6 @@ function downloadTargets(name, args, redirects) {
|
|
|
468
442
|
const flag = args[i]?.text ?? "";
|
|
469
443
|
const value = args[i + 1]?.text;
|
|
470
444
|
if (name === "curl") {
|
|
471
|
-
// -o FILE, also inside a cluster such as -sSLo FILE
|
|
472
445
|
if ((flag === "--output" || /^-[a-zA-Z]*o$/.test(flag)) &&
|
|
473
446
|
value !== undefined)
|
|
474
447
|
targets.push(value);
|
|
@@ -476,7 +449,6 @@ function downloadTargets(name, args, redirects) {
|
|
|
476
449
|
targets.push(urlFileName(url));
|
|
477
450
|
}
|
|
478
451
|
if (name === "wget") {
|
|
479
|
-
// -O FILE, -qO FILE, -qO- (stdout), --output-document=FILE
|
|
480
452
|
const cluster = /^-[a-zA-Z]*O(.*)$/.exec(flag);
|
|
481
453
|
const long = flag.startsWith("--output-document")
|
|
482
454
|
? (flag.split("=")[1] ?? value)
|
|
@@ -522,7 +494,6 @@ function walkCommand(command, acc, depth) {
|
|
|
522
494
|
const script = scriptName(name, args);
|
|
523
495
|
if (script !== undefined)
|
|
524
496
|
acc.runScripts.push(script);
|
|
525
|
-
// Running a file the script downloaded earlier.
|
|
526
497
|
if (acc.downloaded.has(stripDot(raw)) ||
|
|
527
498
|
args.some((a) => INTERPRETERS.has(name) && acc.downloaded.has(stripDot(a.text)))) {
|
|
528
499
|
acc.downloadExecute = true;
|
|
@@ -535,7 +506,6 @@ function walkCommand(command, acc, depth) {
|
|
|
535
506
|
}
|
|
536
507
|
const interpreter = INTERPRETERS.has(name);
|
|
537
508
|
if (interpreter) {
|
|
538
|
-
// bash <(curl …), sh -c "$(curl …)", eval "$(curl …)"
|
|
539
509
|
const fed = [...args, ...command.redirects.map((r) => r.target)].some((w) => w.subs.some(hasNetwork));
|
|
540
510
|
if (fed)
|
|
541
511
|
acc.downloadExecute = true;
|
|
@@ -543,7 +513,6 @@ function walkCommand(command, acc, depth) {
|
|
|
543
513
|
if (raw.includes("/") && !interpreter && !downloader)
|
|
544
514
|
addLocal(acc, raw);
|
|
545
515
|
if (SHELLS.has(name)) {
|
|
546
|
-
// -c, also inside a cluster such as -ec or -xc
|
|
547
516
|
const c = args.findIndex((a) => /^-[a-zA-Z]*c[a-zA-Z]*$/.test(a.text));
|
|
548
517
|
const body = c === -1 ? undefined : args[c + 1];
|
|
549
518
|
if (body !== undefined) {
|
|
@@ -595,7 +564,6 @@ function walkCommand(command, acc, depth) {
|
|
|
595
564
|
}
|
|
596
565
|
return { downloader, interpreter };
|
|
597
566
|
}
|
|
598
|
-
/** A command with a process substitution whose first command is an interpreter, as in `tee >(sh)`. */
|
|
599
567
|
function feedsInterpreter(command) {
|
|
600
568
|
return command.words.some((word) => word.subs.some((sub) => {
|
|
601
569
|
const first = sub[0]?.[0];
|
|
@@ -608,7 +576,6 @@ function walkScript(script, acc, depth) {
|
|
|
608
576
|
let downloaded = false;
|
|
609
577
|
for (const command of pipeline) {
|
|
610
578
|
const { downloader, interpreter } = walkCommand(command, acc, depth);
|
|
611
|
-
// curl … | sh, curl … | tee f | sh, curl … | tee >(sh)
|
|
612
579
|
if ((interpreter || feedsInterpreter(command)) && downloaded)
|
|
613
580
|
acc.downloadExecute = true;
|
|
614
581
|
if (downloader)
|
|
@@ -678,11 +645,6 @@ export function analyzeShell(command) {
|
|
|
678
645
|
unparsable: false,
|
|
679
646
|
};
|
|
680
647
|
}
|
|
681
|
-
/**
|
|
682
|
-
* Commands in the order the shell would run them (pipelines and lists
|
|
683
|
-
* flattened), for callers that need argument-level detail, such as the agent
|
|
684
|
-
* hook. Undefined when the text cannot be parsed or is longer than maxLength.
|
|
685
|
-
*/
|
|
686
648
|
export function shellCommands(input, maxLength = MAX_SCRIPT_LENGTH) {
|
|
687
649
|
if (input.length > maxLength)
|
|
688
650
|
return undefined;
|
|
@@ -695,8 +657,6 @@ export function shellCommands(input, maxLength = MAX_SCRIPT_LENGTH) {
|
|
|
695
657
|
return undefined;
|
|
696
658
|
throw error;
|
|
697
659
|
}
|
|
698
|
-
// $(…) and backticks run before their command, in a subshell: each comes
|
|
699
|
-
// first as a group of its own, so an install inside one is seen.
|
|
700
660
|
const flatten = (s) => s.flatMap((pipeline) => pipeline.flatMap((command) => {
|
|
701
661
|
const subs = [
|
|
702
662
|
...command.words.flatMap((w) => w.subs),
|
package/dist/signals/text.js
CHANGED
package/dist/ui.js
CHANGED
package/dist/verdict.js
CHANGED
|
@@ -11,7 +11,6 @@ const SEVERITY_VERDICT = {
|
|
|
11
11
|
warn: "warn",
|
|
12
12
|
block: "block",
|
|
13
13
|
};
|
|
14
|
-
/** The verdict is the most severe finding; info findings and no findings mean allow. */
|
|
15
14
|
export function aggregate(findings) {
|
|
16
15
|
return findings.reduce((worst, finding) => {
|
|
17
16
|
const verdict = SEVERITY_VERDICT[finding.severity];
|