pompelmi 0.31.0 → 0.32.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.
@@ -6,7 +6,96 @@ import * as path from 'path';
6
6
  function toScanFn(s) {
7
7
  return (typeof s === "function" ? s : s.scan);
8
8
  }
9
- function composeScanners(...scanners) {
9
+ /** Map a Match's severity field to a Verdict for stopOn comparison. */
10
+ function matchToVerdict(m) {
11
+ const s = m.severity;
12
+ if (s === "critical" || s === "high" || s === "malicious")
13
+ return "malicious";
14
+ if (s === "medium" || s === "low" || s === "suspicious" || s === "info")
15
+ return "suspicious";
16
+ return "clean";
17
+ }
18
+ /** Highest verdict across all matches in the list. */
19
+ function highestSeverity(matches) {
20
+ if (matches.length === 0)
21
+ return null;
22
+ if (matches.some((m) => matchToVerdict(m) === "malicious"))
23
+ return "malicious";
24
+ if (matches.some((m) => matchToVerdict(m) === "suspicious"))
25
+ return "suspicious";
26
+ return "clean";
27
+ }
28
+ const SEVERITY_RANK = { malicious: 2, suspicious: 1, clean: 0 };
29
+ function shouldStop(matches, stopOn) {
30
+ if (!stopOn)
31
+ return false;
32
+ const highest = highestSeverity(matches);
33
+ if (!highest)
34
+ return false;
35
+ return SEVERITY_RANK[highest] >= SEVERITY_RANK[stopOn];
36
+ }
37
+ async function runWithTimeout(fn, timeoutMs) {
38
+ if (!timeoutMs)
39
+ return fn();
40
+ return new Promise((resolve, reject) => {
41
+ const timer = setTimeout(() => reject(new Error("scanner timeout")), timeoutMs);
42
+ fn().then((v) => { clearTimeout(timer); resolve(v); }, (e) => { clearTimeout(timer); reject(e); });
43
+ });
44
+ }
45
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
+ function composeScanners(...args) {
47
+ const first = args[0];
48
+ const rest = args.slice(1);
49
+ // ── Named-scanner array form ──────────────────────────────────────────────
50
+ if (Array.isArray(first) &&
51
+ (first.length === 0 || (Array.isArray(first[0]) && typeof first[0][0] === "string"))) {
52
+ const entries = first;
53
+ const opts = rest.length > 0 && !Array.isArray(rest[0]) && typeof rest[0] !== "function" &&
54
+ !(typeof rest[0] === "object" && rest[0] !== null && "scan" in rest[0])
55
+ ? rest[0]
56
+ : {};
57
+ return async (input, ctx) => {
58
+ const all = [];
59
+ if (opts.parallel) {
60
+ // Parallel execution — collect all results then return
61
+ const results = await Promise.allSettled(entries.map(([name, scanner]) => runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner)));
62
+ for (let i = 0; i < results.length; i++) {
63
+ const result = results[i];
64
+ if (result.status === "fulfilled" && Array.isArray(result.value)) {
65
+ const matches = opts.tagSourceName
66
+ ? result.value.map((m) => ({
67
+ ...m,
68
+ meta: { ...m.meta, _sourceName: entries[i][0] },
69
+ }))
70
+ : result.value;
71
+ all.push(...matches);
72
+ }
73
+ }
74
+ }
75
+ else {
76
+ // Sequential execution with optional stopOn short-circuit
77
+ for (const [name, scanner] of entries) {
78
+ try {
79
+ const out = await runWithTimeout(() => toScanFn(scanner)(input, ctx), opts.timeoutMsPerScanner);
80
+ if (Array.isArray(out)) {
81
+ const matches = opts.tagSourceName
82
+ ? out.map((m) => ({ ...m, meta: { ...m.meta, _sourceName: name } }))
83
+ : out;
84
+ all.push(...matches);
85
+ if (shouldStop(all, opts.stopOn))
86
+ break;
87
+ }
88
+ }
89
+ catch {
90
+ // individual scanner failure is non-fatal
91
+ }
92
+ }
93
+ }
94
+ return all;
95
+ };
96
+ }
97
+ // ── Variadic form (backward-compatible) ───────────────────────────────────
98
+ const scanners = [first, ...rest].filter(Boolean);
10
99
  return async (input, ctx) => {
11
100
  const all = [];
12
101
  for (const s of scanners) {