@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
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redos guard — run a regex inside a `node:worker_threads` worker so
|
|
3
|
+
* the host can actually terminate it on a wall-clock budget.
|
|
4
|
+
*
|
|
5
|
+
* Why a worker thread?
|
|
6
|
+
* A `setTimeout`-based watchdog cannot interrupt a synchronous
|
|
7
|
+
* CPU-bound regex in Node.js's single-threaded event loop. The
|
|
8
|
+
* `setImmediate`/`setTimeout` race fires whichever wins the next
|
|
9
|
+
* event-loop tick; if the regex blocks the loop synchronously for
|
|
10
|
+
* 7 seconds, the timer has long since fired and the regex still
|
|
11
|
+
* returns a result — only after the loop is unblocked does the
|
|
12
|
+
* `setImmediate` callback resume and resolve `{ timedOut: false }`.
|
|
13
|
+
*
|
|
14
|
+
* The only honest fix is to run the regex in a separate thread that
|
|
15
|
+
* the host can `worker.terminate()`. `node:worker_threads` gives us
|
|
16
|
+
* that, and the per-thread cost is amortized by the runtime helper
|
|
17
|
+
* itself (the host doesn't pay for the thread except when it
|
|
18
|
+
* invokes `withReDoSGuard`).
|
|
19
|
+
*
|
|
20
|
+
* Why is this here, not inside each plugin?
|
|
21
|
+
* Three plugins (`secret-scanner`, `prompt-firewall`, `path-guard`)
|
|
22
|
+
* need the same contract. Three copies would drift; one copy is
|
|
23
|
+
* auditable and testable.
|
|
24
|
+
*
|
|
25
|
+
* Contract:
|
|
26
|
+
* `withReDoSGuard(re, input, ms)` returns:
|
|
27
|
+
* { timedOut: false, match: RegExpExecArray | null } on normal completion
|
|
28
|
+
* { timedOut: true, match: null } on timeout
|
|
29
|
+
*/
|
|
30
|
+
export interface ReDoSResult {
|
|
31
|
+
/** True when the regex did not complete within the wall-clock budget. */
|
|
32
|
+
timedOut: boolean;
|
|
33
|
+
/** The match result (groups, indices) when `timedOut === false`; null otherwise. */
|
|
34
|
+
match: RegExpExecArray | null;
|
|
35
|
+
}
|
|
36
|
+
export interface ReDoSOptions {
|
|
37
|
+
/** Wall-clock budget in ms. Default 50. */
|
|
38
|
+
budgetMs?: number;
|
|
39
|
+
/**
|
|
40
|
+
* Optional hook invoked exactly once when the budget is exceeded.
|
|
41
|
+
* Called synchronously after the regex is terminated. Default: no-op.
|
|
42
|
+
*/
|
|
43
|
+
onTimeout?: (info: {
|
|
44
|
+
regex: RegExp;
|
|
45
|
+
input: string;
|
|
46
|
+
budgetMs: number;
|
|
47
|
+
elapsedMs: number;
|
|
48
|
+
}) => void;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Run `re.exec(input)` inside a worker thread with a wall-clock
|
|
52
|
+
* watchdog. The worker is terminated when the budget elapses; the
|
|
53
|
+
* regex cannot keep running.
|
|
54
|
+
*
|
|
55
|
+
* Returns a Promise; resolved with `{ timedOut, match }`.
|
|
56
|
+
*/
|
|
57
|
+
export declare function withReDoSGuard(re: RegExp, input: string, budgetMs?: number, options?: ReDoSOptions): Promise<ReDoSResult>;
|
|
58
|
+
/**
|
|
59
|
+
* Convenience: build a guarded matcher.
|
|
60
|
+
*
|
|
61
|
+
* ```ts
|
|
62
|
+
* const matchCredential = guardedMatcher(/AKIA[0-9A-Z]{16}/g, 25);
|
|
63
|
+
* const r = await matchCredential(line);
|
|
64
|
+
* if (r.timedOut) counters.redosTimeouts++;
|
|
65
|
+
* else if (r.match) report(r.match);
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function guardedMatcher(re: RegExp, budgetMs?: number, onTimeout?: ReDoSOptions['onTimeout']): (input: string) => Promise<ReDoSResult>;
|
|
69
|
+
//# sourceMappingURL=redos-guard.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sandbox — canonical project-path validation with symlink resolution.
|
|
3
|
+
*
|
|
4
|
+
* Why a new helper when `runtime/index.ts` already exports
|
|
5
|
+
* `withinProject` and `sanitizeRunnerPath`?
|
|
6
|
+
*
|
|
7
|
+
* - `withinProject` does NOT resolve symlinks: a path like
|
|
8
|
+
* `project/link-to-/etc/passwd` reports as inside the project
|
|
9
|
+
* even though the resolved target is outside.
|
|
10
|
+
* - `sanitizeRunnerPath` is for runner argv (linter binaries),
|
|
11
|
+
* not user-supplied tool input. The two paths share the
|
|
12
|
+
* leading-dash + length checks but `sanitizeRunnerPath`
|
|
13
|
+
* rejects a `cwd` for which it cannot resolve; user-input
|
|
14
|
+
* sandbox needs canonicalization instead.
|
|
15
|
+
*
|
|
16
|
+
* The previous design (per SAGE memory T-03) relied on three
|
|
17
|
+
* duplicate `withinProject` copies in `runtime/index.ts`,
|
|
18
|
+
* `file-watcher/index.ts`, and `path-guard/glob.ts`. They drifted
|
|
19
|
+
* on edge cases. This helper is the single replacement.
|
|
20
|
+
*
|
|
21
|
+
* Contract:
|
|
22
|
+
* `safePath(input, projectRoot?)` returns the canonical absolute
|
|
23
|
+
* path inside the project on success, or `null` on rejection.
|
|
24
|
+
*
|
|
25
|
+
* Rejection cases:
|
|
26
|
+
* - empty string
|
|
27
|
+
* - length > 4096 bytes (matches `runtime.withinProject`)
|
|
28
|
+
* - leading-dash (option smuggling)
|
|
29
|
+
* - cannot be resolved (path doesn't exist or EPERM)
|
|
30
|
+
* - resolved path escapes the project root
|
|
31
|
+
* - symlink target is outside the project
|
|
32
|
+
*/
|
|
33
|
+
export interface SafePathOptions {
|
|
34
|
+
/**
|
|
35
|
+
* Project root for the sandbox. Defaults to `process.cwd()`. Pass
|
|
36
|
+
* the session cwd from the plugin host when available so that
|
|
37
|
+
* tools running in a subdirectory are scoped to that directory.
|
|
38
|
+
*/
|
|
39
|
+
projectRoot?: string;
|
|
40
|
+
/**
|
|
41
|
+
* If true (default), follow symlinks via `realpathSync`. Set false
|
|
42
|
+
* for plugins that need to record the literal path the user wrote
|
|
43
|
+
* (e.g. checkpoint capture, git diff).
|
|
44
|
+
*/
|
|
45
|
+
followSymlinks?: boolean;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Resolve `input` to an absolute path inside the project root.
|
|
49
|
+
*
|
|
50
|
+
* Returns `null` for empty/oversized/leading-dash inputs and for
|
|
51
|
+
* paths whose real target escapes the project.
|
|
52
|
+
*/
|
|
53
|
+
export declare function safePath(input: string, options?: SafePathOptions): string | null;
|
|
54
|
+
/**
|
|
55
|
+
* Boolean convenience for callers that don't need the canonical path.
|
|
56
|
+
* Equivalent to `safePath(input, options) !== null`.
|
|
57
|
+
*/
|
|
58
|
+
export declare function isInsideProject(input: string, options?: SafePathOptions): boolean;
|
|
59
|
+
//# sourceMappingURL=sandbox.d.ts.map
|
package/dist/runtime.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// src/runtime/index.ts
|
|
2
2
|
import { execFile } from "node:child_process";
|
|
3
3
|
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
4
|
-
import { basename, extname as extname2, isAbsolute as
|
|
4
|
+
import { basename, extname as extname2, isAbsolute as isAbsolute3, relative as relative3, resolve as resolve3 } from "node:path";
|
|
5
5
|
|
|
6
6
|
// src/runtime/llm.ts
|
|
7
7
|
function stripOuterMarkdownFence(text) {
|
|
@@ -347,6 +347,162 @@ function releaseHandles(state, keys) {
|
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
349
|
|
|
350
|
+
// src/runtime/redos-guard.ts
|
|
351
|
+
import { Worker } from "node:worker_threads";
|
|
352
|
+
function withReDoSGuard(re, input, budgetMs = 50, options = {}) {
|
|
353
|
+
const opts = { budgetMs, ...options };
|
|
354
|
+
const start = Date.now();
|
|
355
|
+
const workerSource = buildWorkerSource(re.source, input, re.flags);
|
|
356
|
+
const worker = new Worker(workerSource, {
|
|
357
|
+
eval: true,
|
|
358
|
+
name: `redos-guard:${re.source.slice(0, 32)}`
|
|
359
|
+
});
|
|
360
|
+
return new Promise((resolve4) => {
|
|
361
|
+
let settled = false;
|
|
362
|
+
const onMessage = (msg) => {
|
|
363
|
+
if (settled) return;
|
|
364
|
+
settled = true;
|
|
365
|
+
clearTimeout(timer);
|
|
366
|
+
worker.terminate().catch(() => {
|
|
367
|
+
});
|
|
368
|
+
if (!msg.ok) {
|
|
369
|
+
resolve4({ timedOut: true, match: null });
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
resolve4({ timedOut: false, match: msg.match });
|
|
373
|
+
};
|
|
374
|
+
const onError = () => {
|
|
375
|
+
if (settled) return;
|
|
376
|
+
settled = true;
|
|
377
|
+
clearTimeout(timer);
|
|
378
|
+
worker.terminate().catch(() => {
|
|
379
|
+
});
|
|
380
|
+
resolve4({ timedOut: true, match: null });
|
|
381
|
+
};
|
|
382
|
+
const timer = setTimeout(() => {
|
|
383
|
+
if (settled) return;
|
|
384
|
+
settled = true;
|
|
385
|
+
const elapsedMs = Date.now() - start;
|
|
386
|
+
worker.terminate().catch(() => {
|
|
387
|
+
});
|
|
388
|
+
try {
|
|
389
|
+
opts.onTimeout?.({
|
|
390
|
+
regex: re,
|
|
391
|
+
input,
|
|
392
|
+
budgetMs: opts.budgetMs,
|
|
393
|
+
elapsedMs
|
|
394
|
+
});
|
|
395
|
+
} catch {
|
|
396
|
+
}
|
|
397
|
+
resolve4({ timedOut: true, match: null });
|
|
398
|
+
}, opts.budgetMs);
|
|
399
|
+
timer.unref?.();
|
|
400
|
+
worker.on("message", onMessage);
|
|
401
|
+
worker.on("error", onError);
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
function buildWorkerSource(source, input, flags) {
|
|
405
|
+
const S = JSON.stringify(source);
|
|
406
|
+
const I = JSON.stringify(input);
|
|
407
|
+
const F = JSON.stringify(flags);
|
|
408
|
+
return `
|
|
409
|
+
const { parentPort } = require('node:worker_threads');
|
|
410
|
+
const source = ${S};
|
|
411
|
+
const input = ${I};
|
|
412
|
+
const flags = ${F};
|
|
413
|
+
try {
|
|
414
|
+
const re = new RegExp(source, flags);
|
|
415
|
+
const match = re.exec(input);
|
|
416
|
+
// parentPort.postMessage, NOT bare postMessage: with eval:true
|
|
417
|
+
// workers this Node version does not expose the bare postMessage
|
|
418
|
+
// global \u2014 the worker throws ReferenceError at startup and the
|
|
419
|
+
// host misreads it as a timeout (positive-path regression).
|
|
420
|
+
parentPort.postMessage({ ok: true, match });
|
|
421
|
+
} catch (err) {
|
|
422
|
+
parentPort.postMessage({ ok: false, error: err && err.message ? err.message : String(err) });
|
|
423
|
+
}
|
|
424
|
+
`;
|
|
425
|
+
}
|
|
426
|
+
function guardedMatcher(re, budgetMs = 50, onTimeout) {
|
|
427
|
+
return (input) => withReDoSGuard(re, input, budgetMs, onTimeout ? { onTimeout } : {});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// src/runtime/sandbox.ts
|
|
431
|
+
import { realpathSync } from "node:fs";
|
|
432
|
+
import { isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2 } from "node:path";
|
|
433
|
+
var MAX_PATH_BYTES = 4096;
|
|
434
|
+
function safePath(input, options = {}) {
|
|
435
|
+
if (typeof input !== "string") return null;
|
|
436
|
+
if (input.length === 0 || input.length > MAX_PATH_BYTES) return null;
|
|
437
|
+
if (input.startsWith("-")) return null;
|
|
438
|
+
const projectRoot = resolve2(options.projectRoot ?? process.cwd());
|
|
439
|
+
const lexical = isAbsolute2(input) ? resolve2(input) : resolve2(projectRoot, input);
|
|
440
|
+
if (!withinLexical(projectRoot, lexical)) return null;
|
|
441
|
+
if (options.followSymlinks !== false) {
|
|
442
|
+
let real;
|
|
443
|
+
try {
|
|
444
|
+
real = realpathSync(lexical);
|
|
445
|
+
} catch {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
if (!withinLexical(projectRoot, real)) return null;
|
|
449
|
+
return real;
|
|
450
|
+
}
|
|
451
|
+
return lexical;
|
|
452
|
+
}
|
|
453
|
+
function withinLexical(projectRoot, candidate) {
|
|
454
|
+
const rel = relative2(projectRoot, candidate);
|
|
455
|
+
if (rel === "" || rel === ".") return true;
|
|
456
|
+
if (rel.startsWith("..")) return false;
|
|
457
|
+
if (isAbsolute2(rel)) return false;
|
|
458
|
+
return true;
|
|
459
|
+
}
|
|
460
|
+
function isInsideProject(input, options = {}) {
|
|
461
|
+
return safePath(input, options) !== null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/runtime/h1-state.ts
|
|
465
|
+
function createH1State(initial) {
|
|
466
|
+
const handles = /* @__PURE__ */ new Map();
|
|
467
|
+
const safeRelease = (unregister) => {
|
|
468
|
+
try {
|
|
469
|
+
unregister();
|
|
470
|
+
} catch {
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
return {
|
|
474
|
+
state: initial,
|
|
475
|
+
register(key, unregister) {
|
|
476
|
+
const prior = handles.get(key);
|
|
477
|
+
if (prior) {
|
|
478
|
+
safeRelease(prior);
|
|
479
|
+
handles.delete(key);
|
|
480
|
+
}
|
|
481
|
+
if (unregister) {
|
|
482
|
+
handles.set(key, unregister);
|
|
483
|
+
}
|
|
484
|
+
},
|
|
485
|
+
release(key) {
|
|
486
|
+
const prior = handles.get(key);
|
|
487
|
+
if (!prior) return;
|
|
488
|
+
handles.delete(key);
|
|
489
|
+
safeRelease(prior);
|
|
490
|
+
},
|
|
491
|
+
releaseAll() {
|
|
492
|
+
for (const unregister of handles.values()) {
|
|
493
|
+
safeRelease(unregister);
|
|
494
|
+
}
|
|
495
|
+
handles.clear();
|
|
496
|
+
},
|
|
497
|
+
size() {
|
|
498
|
+
return handles.size;
|
|
499
|
+
},
|
|
500
|
+
keys() {
|
|
501
|
+
return [...handles.keys()];
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
350
506
|
// src/runtime/index.ts
|
|
351
507
|
var META_CHARS = /["'`;&|<>\r\n]/;
|
|
352
508
|
var MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
@@ -361,9 +517,9 @@ function safeSplit(command) {
|
|
|
361
517
|
function withinProjectPath(projectRoot, candidate) {
|
|
362
518
|
if (candidate.length === 0 || candidate.length > 4096) return false;
|
|
363
519
|
if (hasLeadingDash(candidate)) return false;
|
|
364
|
-
const resolved =
|
|
365
|
-
const rel =
|
|
366
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
520
|
+
const resolved = isAbsolute3(candidate) ? resolve3(candidate) : resolve3(projectRoot, candidate);
|
|
521
|
+
const rel = relative3(projectRoot, resolved);
|
|
522
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
367
523
|
}
|
|
368
524
|
function everyFlagAllowed(allowed, args) {
|
|
369
525
|
for (const arg of args) {
|
|
@@ -375,14 +531,14 @@ function everyFlagAllowed(allowed, args) {
|
|
|
375
531
|
}
|
|
376
532
|
function sanitizeRunnerPath(value, options = {}) {
|
|
377
533
|
if (!value || hasLeadingDash(value)) return null;
|
|
378
|
-
const projectRoot =
|
|
534
|
+
const projectRoot = resolve3(options.projectRoot ?? process.cwd());
|
|
379
535
|
if (!withinProjectPath(projectRoot, value)) return null;
|
|
380
|
-
return
|
|
536
|
+
return isAbsolute3(value) ? resolve3(value) : resolve3(projectRoot, value);
|
|
381
537
|
}
|
|
382
538
|
function resolveRunnerCommand(runtime, command, options = {}) {
|
|
383
539
|
const tokens = safeSplit(command);
|
|
384
540
|
if (!tokens || tokens.length === 0) return null;
|
|
385
|
-
const projectRoot =
|
|
541
|
+
const projectRoot = resolve3(options.projectRoot ?? process.cwd());
|
|
386
542
|
const launcher = runtime.packageManager;
|
|
387
543
|
const [head, second, ...rest] = tokens;
|
|
388
544
|
if (!head) return null;
|
|
@@ -413,7 +569,7 @@ function resolveRunnerCommand(runtime, command, options = {}) {
|
|
|
413
569
|
return { cmd: head, args: [second, exe, ...tail], display };
|
|
414
570
|
}
|
|
415
571
|
}
|
|
416
|
-
if (
|
|
572
|
+
if (isAbsolute3(head)) {
|
|
417
573
|
if (!withinProjectPath(projectRoot, head)) return null;
|
|
418
574
|
const base = basename(head);
|
|
419
575
|
if (base !== runtime.executable && base !== launcher) return null;
|
|
@@ -434,7 +590,7 @@ function runRunnerCommand(argv, options) {
|
|
|
434
590
|
});
|
|
435
591
|
}
|
|
436
592
|
return new Promise((resolvePromise) => {
|
|
437
|
-
const projectRoot =
|
|
593
|
+
const projectRoot = resolve3(options.projectRoot ?? process.cwd());
|
|
438
594
|
const trimmedCwd = options.cwd.trim();
|
|
439
595
|
if (!withinProjectPath(projectRoot, trimmedCwd)) {
|
|
440
596
|
resolvePromise({
|
|
@@ -448,6 +604,7 @@ function runRunnerCommand(argv, options) {
|
|
|
448
604
|
}
|
|
449
605
|
let timedOut = false;
|
|
450
606
|
let spawnErrored = false;
|
|
607
|
+
const start = Date.now();
|
|
451
608
|
const stdoutChunks = [];
|
|
452
609
|
const stderrChunks = [];
|
|
453
610
|
let stdoutBytes = 0;
|
|
@@ -477,6 +634,15 @@ function runRunnerCommand(argv, options) {
|
|
|
477
634
|
timeout: options.timeoutMs,
|
|
478
635
|
signal: options.signal,
|
|
479
636
|
maxBuffer: MAX_BUFFER_BYTES,
|
|
637
|
+
// execFile defaults `encoding` to 'utf8', which makes the
|
|
638
|
+
// stdout/stderr `data` events emit *strings*. The chunk arrays
|
|
639
|
+
// below are typed Buffer[] and every consumer runs them through
|
|
640
|
+
// Buffer.concat(...).toString('utf8'), which throws
|
|
641
|
+
// ERR_INVALID_ARG_TYPE on any non-empty string output. Pin the
|
|
642
|
+
// streams to buffers so the declared contract holds (regression:
|
|
643
|
+
// runRunnerCommand crashed on any child that actually wrote
|
|
644
|
+
// output; only the maxBuffer fixture exercised this path).
|
|
645
|
+
encoding: "buffer",
|
|
480
646
|
windowsHide: true,
|
|
481
647
|
shell: false,
|
|
482
648
|
...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
|
|
@@ -505,11 +671,31 @@ function runRunnerCommand(argv, options) {
|
|
|
505
671
|
}
|
|
506
672
|
if (err) {
|
|
507
673
|
const anyErr = err;
|
|
674
|
+
if ((anyErr.killed === true || anyErr.signal === "SIGTERM") && // maxBuffer overflow must NOT be misreported as a timeout
|
|
675
|
+
// — it's a real failure (the child wrote too much) and
|
|
676
|
+
// downstream callers (type-gate/index.ts:227) return
|
|
677
|
+
// null on timedOut=true, which would silently swallow
|
|
678
|
+
// maxBuffer overflow into a confusing empty-output
|
|
679
|
+
// result. Skip the timeout resolve when the err shape
|
|
680
|
+
// names maxBuffer explicitly.
|
|
681
|
+
!/maxBuffer length exceeded/i.test(anyErr.message ?? "") && // And only count it as a timeout if the wall clock has
|
|
682
|
+
// actually elapsed past the budget. External SIGTERMs and
|
|
683
|
+
// races against the exit handler don't satisfy this.
|
|
684
|
+
Date.now() - start >= options.timeoutMs) {
|
|
685
|
+
resolvePromise({
|
|
686
|
+
code: null,
|
|
687
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
688
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
689
|
+
timedOut: true,
|
|
690
|
+
spawnError: false
|
|
691
|
+
});
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
508
694
|
const code = typeof anyErr.code === "number" ? anyErr.code : 1;
|
|
509
695
|
resolvePromise({
|
|
510
696
|
code,
|
|
511
|
-
stdout: Buffer.concat(stdoutChunks).toString("
|
|
512
|
-
stderr: Buffer.concat(stderrChunks).toString("
|
|
697
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
698
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
513
699
|
timedOut: false,
|
|
514
700
|
spawnError: false
|
|
515
701
|
});
|
|
@@ -525,7 +711,7 @@ function runRunnerCommand(argv, options) {
|
|
|
525
711
|
}
|
|
526
712
|
);
|
|
527
713
|
child.on("exit", (_code, signal) => {
|
|
528
|
-
if (signal !== null) {
|
|
714
|
+
if (signal !== null && Date.now() - start >= options.timeoutMs) {
|
|
529
715
|
timedOut = true;
|
|
530
716
|
}
|
|
531
717
|
});
|
|
@@ -555,14 +741,14 @@ async function probeRunner(runtime, probeArg = "--version", options) {
|
|
|
555
741
|
}
|
|
556
742
|
function withinProject(p) {
|
|
557
743
|
const cwd = process.cwd();
|
|
558
|
-
return withinProjectPath(cwd, p) ||
|
|
744
|
+
return withinProjectPath(cwd, p) || relative3(cwd, p) === ".";
|
|
559
745
|
}
|
|
560
746
|
function locateRunnerEntry(runtime, projectRoot) {
|
|
561
|
-
const root =
|
|
747
|
+
const root = resolve3(projectRoot);
|
|
562
748
|
const candidates = [
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
749
|
+
resolve3(root, "node_modules", ".bin", runtime.executable),
|
|
750
|
+
resolve3(root, "node_modules", ".bin", `${runtime.executable}.cmd`),
|
|
751
|
+
resolve3(root, "node_modules", ".bin", `${runtime.executable}.ps1`)
|
|
566
752
|
];
|
|
567
753
|
for (const c of candidates) {
|
|
568
754
|
if (existsSync(c)) return c;
|
|
@@ -592,7 +778,7 @@ function collectSourceFiles(root, opts) {
|
|
|
592
778
|
entries.sort();
|
|
593
779
|
for (const entry of entries) {
|
|
594
780
|
if (excludeSet.has(entry)) continue;
|
|
595
|
-
const full =
|
|
781
|
+
const full = resolve3(dir, entry);
|
|
596
782
|
let st;
|
|
597
783
|
try {
|
|
598
784
|
st = statSync(full);
|
|
@@ -635,7 +821,7 @@ async function collectSourceFilesAsync(root, opts) {
|
|
|
635
821
|
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
636
822
|
for (const entry of entries) {
|
|
637
823
|
if (excludeSet.has(entry.name)) continue;
|
|
638
|
-
const full =
|
|
824
|
+
const full = resolve3(dir, entry.name);
|
|
639
825
|
if (entry.isDirectory()) {
|
|
640
826
|
await walk(full, depth + 1);
|
|
641
827
|
} else if (entry.isFile() && matchesExtension(full, opts.extensions)) {
|
|
@@ -656,7 +842,10 @@ export {
|
|
|
656
842
|
clearLocalBinCache,
|
|
657
843
|
collectSourceFiles,
|
|
658
844
|
collectSourceFilesAsync,
|
|
845
|
+
createH1State,
|
|
659
846
|
findOnPath,
|
|
847
|
+
guardedMatcher,
|
|
848
|
+
isInsideProject,
|
|
660
849
|
locateRunnerEntry,
|
|
661
850
|
matchesExtension,
|
|
662
851
|
parseLlmJsonObject,
|
|
@@ -672,7 +861,9 @@ export {
|
|
|
672
861
|
runOptionalPluginLlm,
|
|
673
862
|
runRunnerCommand,
|
|
674
863
|
safeJsonStringify,
|
|
864
|
+
safePath,
|
|
675
865
|
sanitizeRunnerPath,
|
|
676
866
|
stripOuterMarkdownFence,
|
|
867
|
+
withReDoSGuard,
|
|
677
868
|
withinProject
|
|
678
869
|
};
|
package/dist/secret-scanner.js
CHANGED
|
@@ -188,6 +188,7 @@ function createState() {
|
|
|
188
188
|
allowCount: 0,
|
|
189
189
|
/** PostToolUse: secrets detected in tool output. */
|
|
190
190
|
leakCount: 0,
|
|
191
|
+
timeoutCount: 0,
|
|
191
192
|
/** Most recent PreToolUse block — surfaced by `secret_scanner_status`. */
|
|
192
193
|
lastBlock: null,
|
|
193
194
|
/** Most recent PostToolUse leak — surfaced by `secret_scanner_status`. */
|
|
@@ -356,7 +357,19 @@ function buildHook(cfg, log, runtime) {
|
|
|
356
357
|
const { state } = runtime;
|
|
357
358
|
if (!cfg.enabled) return;
|
|
358
359
|
const toolName = input.toolName ?? "unknown";
|
|
359
|
-
|
|
360
|
+
let matched;
|
|
361
|
+
try {
|
|
362
|
+
matched = scanInput(input.toolInput);
|
|
363
|
+
} catch (err) {
|
|
364
|
+
if (String(err).includes("ReDoS")) {
|
|
365
|
+
state.timeoutCount += 1;
|
|
366
|
+
return {
|
|
367
|
+
decision: "block",
|
|
368
|
+
reason: "secret-scanner: ReDoS timeout \u2014 regex scan exceeded the wall-clock budget. Fail-closed: treated as a block."
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
throw err;
|
|
372
|
+
}
|
|
360
373
|
if (!matched) return;
|
|
361
374
|
const summary = matched.join(", ");
|
|
362
375
|
const when = (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -534,7 +547,8 @@ var plugin = {
|
|
|
534
547
|
block: state.blockCount,
|
|
535
548
|
redact: state.redactCount,
|
|
536
549
|
allow: state.allowCount,
|
|
537
|
-
leak: state.leakCount
|
|
550
|
+
leak: state.leakCount,
|
|
551
|
+
timeoutCount: state.timeoutCount
|
|
538
552
|
},
|
|
539
553
|
lastBlock: state.lastBlock,
|
|
540
554
|
lastLeak: state.lastLeak
|
package/dist/type-gate.js
CHANGED
|
@@ -177,6 +177,7 @@ function runRunnerCommand(argv, options) {
|
|
|
177
177
|
}
|
|
178
178
|
let timedOut = false;
|
|
179
179
|
let spawnErrored = false;
|
|
180
|
+
const start = Date.now();
|
|
180
181
|
const stdoutChunks = [];
|
|
181
182
|
const stderrChunks = [];
|
|
182
183
|
let stdoutBytes = 0;
|
|
@@ -206,6 +207,15 @@ function runRunnerCommand(argv, options) {
|
|
|
206
207
|
timeout: options.timeoutMs,
|
|
207
208
|
signal: options.signal,
|
|
208
209
|
maxBuffer: MAX_BUFFER_BYTES,
|
|
210
|
+
// execFile defaults `encoding` to 'utf8', which makes the
|
|
211
|
+
// stdout/stderr `data` events emit *strings*. The chunk arrays
|
|
212
|
+
// below are typed Buffer[] and every consumer runs them through
|
|
213
|
+
// Buffer.concat(...).toString('utf8'), which throws
|
|
214
|
+
// ERR_INVALID_ARG_TYPE on any non-empty string output. Pin the
|
|
215
|
+
// streams to buffers so the declared contract holds (regression:
|
|
216
|
+
// runRunnerCommand crashed on any child that actually wrote
|
|
217
|
+
// output; only the maxBuffer fixture exercised this path).
|
|
218
|
+
encoding: "buffer",
|
|
209
219
|
windowsHide: true,
|
|
210
220
|
shell: false,
|
|
211
221
|
...invocation.windowsVerbatimArguments ? { windowsVerbatimArguments: true } : {}
|
|
@@ -234,11 +244,31 @@ function runRunnerCommand(argv, options) {
|
|
|
234
244
|
}
|
|
235
245
|
if (err) {
|
|
236
246
|
const anyErr = err;
|
|
247
|
+
if ((anyErr.killed === true || anyErr.signal === "SIGTERM") && // maxBuffer overflow must NOT be misreported as a timeout
|
|
248
|
+
// — it's a real failure (the child wrote too much) and
|
|
249
|
+
// downstream callers (type-gate/index.ts:227) return
|
|
250
|
+
// null on timedOut=true, which would silently swallow
|
|
251
|
+
// maxBuffer overflow into a confusing empty-output
|
|
252
|
+
// result. Skip the timeout resolve when the err shape
|
|
253
|
+
// names maxBuffer explicitly.
|
|
254
|
+
!/maxBuffer length exceeded/i.test(anyErr.message ?? "") && // And only count it as a timeout if the wall clock has
|
|
255
|
+
// actually elapsed past the budget. External SIGTERMs and
|
|
256
|
+
// races against the exit handler don't satisfy this.
|
|
257
|
+
Date.now() - start >= options.timeoutMs) {
|
|
258
|
+
resolvePromise({
|
|
259
|
+
code: null,
|
|
260
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
261
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
262
|
+
timedOut: true,
|
|
263
|
+
spawnError: false
|
|
264
|
+
});
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
237
267
|
const code = typeof anyErr.code === "number" ? anyErr.code : 1;
|
|
238
268
|
resolvePromise({
|
|
239
269
|
code,
|
|
240
|
-
stdout: Buffer.concat(stdoutChunks).toString("
|
|
241
|
-
stderr: Buffer.concat(stderrChunks).toString("
|
|
270
|
+
stdout: Buffer.concat(stdoutChunks).toString("utf-8"),
|
|
271
|
+
stderr: Buffer.concat(stderrChunks).toString("utf-8"),
|
|
242
272
|
timedOut: false,
|
|
243
273
|
spawnError: false
|
|
244
274
|
});
|
|
@@ -254,7 +284,7 @@ function runRunnerCommand(argv, options) {
|
|
|
254
284
|
}
|
|
255
285
|
);
|
|
256
286
|
child.on("exit", (_code, signal) => {
|
|
257
|
-
if (signal !== null) {
|
|
287
|
+
if (signal !== null && Date.now() - start >= options.timeoutMs) {
|
|
258
288
|
timedOut = true;
|
|
259
289
|
}
|
|
260
290
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wrongstack/plugins",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.308.0",
|
|
4
4
|
"description": "Official WrongStack collection of focused plugins for code quality, security, observability, planning, and agent coordination",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ECOSTACK TECHNOLOGY OÜ",
|
|
@@ -303,8 +303,8 @@
|
|
|
303
303
|
"vitest": "^4.1.10"
|
|
304
304
|
},
|
|
305
305
|
"dependencies": {
|
|
306
|
-
"@wrongstack/
|
|
307
|
-
"@wrongstack/
|
|
306
|
+
"@wrongstack/tools": "0.308.0",
|
|
307
|
+
"@wrongstack/core": "0.308.0"
|
|
308
308
|
},
|
|
309
309
|
"scripts": {
|
|
310
310
|
"build": "node ../../scripts/build-package.mjs",
|