@profullstack/threatcrush 0.11.1 → 0.11.2

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/daemon.js CHANGED
@@ -2022,8 +2022,8 @@ var require_toml = __commonJS({
2022
2022
  });
2023
2023
 
2024
2024
  // src/daemon/index.ts
2025
- var import_node_fs15 = require("fs");
2026
- var import_node_path10 = require("path");
2025
+ var import_node_fs17 = require("fs");
2026
+ var import_node_path12 = require("path");
2027
2027
 
2028
2028
  // src/daemon/paths.ts
2029
2029
  var import_node_fs = require("fs");
@@ -2107,7 +2107,28 @@ function findRunningDaemon() {
2107
2107
 
2108
2108
  // src/daemon/ipc-server.ts
2109
2109
  var import_node_net = require("net");
2110
+ var import_node_fs4 = require("fs");
2111
+
2112
+ // src/daemon/control-token.ts
2113
+ var import_node_crypto = require("crypto");
2110
2114
  var import_node_fs3 = require("fs");
2115
+ var import_node_path2 = require("path");
2116
+ var CONTROL_TOKEN_FILE = (0, import_node_path2.join)(PATHS.runDir, "control.token");
2117
+ function issueControlToken() {
2118
+ const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
2119
+ (0, import_node_fs3.writeFileSync)(CONTROL_TOKEN_FILE, token, { mode: 384 });
2120
+ try {
2121
+ (0, import_node_fs3.chmodSync)(CONTROL_TOKEN_FILE, 384);
2122
+ } catch {
2123
+ }
2124
+ return token;
2125
+ }
2126
+ function tokensMatch(expected, provided) {
2127
+ if (typeof provided !== "string" || !provided) return false;
2128
+ const a = Buffer.from(expected);
2129
+ const b = Buffer.from(provided);
2130
+ return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
2131
+ }
2111
2132
 
2112
2133
  // src/daemon/event-bus.ts
2113
2134
  var import_node_events = require("events");
@@ -2303,10 +2324,12 @@ var IpcServer = class {
2303
2324
  nextClientId = 1;
2304
2325
  startedAt = /* @__PURE__ */ new Date();
2305
2326
  counters = { events: 0, threats: 0, alerts: 0 };
2327
+ controlToken = "";
2306
2328
  async start() {
2307
- if ((0, import_node_fs3.existsSync)(PATHS.socket)) {
2329
+ this.controlToken = issueControlToken();
2330
+ if ((0, import_node_fs4.existsSync)(PATHS.socket)) {
2308
2331
  try {
2309
- (0, import_node_fs3.unlinkSync)(PATHS.socket);
2332
+ (0, import_node_fs4.unlinkSync)(PATHS.socket);
2310
2333
  } catch {
2311
2334
  }
2312
2335
  }
@@ -2342,14 +2365,14 @@ var IpcServer = class {
2342
2365
  return new Promise((resolve3) => {
2343
2366
  if (!this.server) {
2344
2367
  try {
2345
- if ((0, import_node_fs3.existsSync)(PATHS.socket)) (0, import_node_fs3.unlinkSync)(PATHS.socket);
2368
+ if ((0, import_node_fs4.existsSync)(PATHS.socket)) (0, import_node_fs4.unlinkSync)(PATHS.socket);
2346
2369
  } catch {
2347
2370
  }
2348
2371
  return resolve3();
2349
2372
  }
2350
2373
  this.server.close(() => {
2351
2374
  try {
2352
- if ((0, import_node_fs3.existsSync)(PATHS.socket)) (0, import_node_fs3.unlinkSync)(PATHS.socket);
2375
+ if ((0, import_node_fs4.existsSync)(PATHS.socket)) (0, import_node_fs4.unlinkSync)(PATHS.socket);
2353
2376
  } catch {
2354
2377
  }
2355
2378
  resolve3();
@@ -2435,6 +2458,13 @@ var IpcServer = class {
2435
2458
  for (const ch of req.params.channels) client.subscriptions.add(ch);
2436
2459
  return this.send(client, { id: req.id, ok: true, result: { subscribed: [...client.subscriptions] } });
2437
2460
  case "shutdown":
2461
+ if (!tokensMatch(this.controlToken, req.params?.token)) {
2462
+ return this.send(client, {
2463
+ id: req.id,
2464
+ ok: false,
2465
+ error: "shutdown requires the daemon control token (run as root, or use systemctl)"
2466
+ });
2467
+ }
2438
2468
  this.send(client, { id: req.id, ok: true, result: "shutting down" });
2439
2469
  setTimeout(() => process.emit("SIGTERM"), 50);
2440
2470
  return;
@@ -2455,13 +2485,134 @@ var IpcServer = class {
2455
2485
  };
2456
2486
 
2457
2487
  // src/daemon/module-host.ts
2458
- var import_node_fs8 = require("fs");
2459
- var import_node_path3 = require("path");
2488
+ var import_node_fs10 = require("fs");
2489
+ var import_node_path5 = require("path");
2460
2490
  var import_node_url = require("url");
2461
2491
  var import_toml2 = __toESM(require_toml());
2462
2492
 
2493
+ // src/daemon/module-trust.ts
2494
+ var import_node_crypto2 = require("crypto");
2495
+ var import_node_fs5 = require("fs");
2496
+ var import_node_path3 = require("path");
2497
+ var TRUST_FILE = (0, import_node_path3.join)(PATHS.configDir, "trusted-modules.json");
2498
+ var PUBLISHER_KEYS_FILE = (0, import_node_path3.join)(PATHS.configDir, "publisher-keys.json");
2499
+ var DIGEST_EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git"]);
2500
+ function collectFiles(root, dir = root, out = []) {
2501
+ for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true }).sort(
2502
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
2503
+ )) {
2504
+ const full = (0, import_node_path3.join)(dir, entry.name);
2505
+ if (entry.isSymbolicLink()) {
2506
+ out.push({ path: full, isSymlink: true });
2507
+ } else if (entry.isDirectory()) {
2508
+ if (DIGEST_EXCLUDED_DIRS.has(entry.name)) continue;
2509
+ collectFiles(root, full, out);
2510
+ } else if (entry.isFile()) {
2511
+ out.push({ path: full, isSymlink: false });
2512
+ }
2513
+ }
2514
+ return out;
2515
+ }
2516
+ function computeModuleDigest(modulePath) {
2517
+ const hash = (0, import_node_crypto2.createHash)("sha256");
2518
+ for (const entry of collectFiles(modulePath)) {
2519
+ const relPath = (0, import_node_path3.relative)(modulePath, entry.path).split(import_node_path3.sep).join("/");
2520
+ hash.update(relPath);
2521
+ hash.update("\0");
2522
+ if (entry.isSymlink) {
2523
+ hash.update("symlink:");
2524
+ hash.update((0, import_node_fs5.readlinkSync)(entry.path));
2525
+ } else {
2526
+ hash.update("file:");
2527
+ hash.update((0, import_node_fs5.readFileSync)(entry.path));
2528
+ }
2529
+ hash.update("\0");
2530
+ }
2531
+ return hash.digest("hex");
2532
+ }
2533
+ function readTrustFile() {
2534
+ if (!(0, import_node_fs5.existsSync)(TRUST_FILE)) return { version: 1, modules: {} };
2535
+ try {
2536
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(TRUST_FILE, "utf-8"));
2537
+ if (parsed.version !== 1 || typeof parsed.modules !== "object" || !parsed.modules) {
2538
+ return { version: 1, modules: {} };
2539
+ }
2540
+ return { version: 1, modules: parsed.modules };
2541
+ } catch {
2542
+ return { version: 1, modules: {} };
2543
+ }
2544
+ }
2545
+ function readPublisherKeys() {
2546
+ if (!(0, import_node_fs5.existsSync)(PUBLISHER_KEYS_FILE)) return {};
2547
+ try {
2548
+ const parsed = JSON.parse((0, import_node_fs5.readFileSync)(PUBLISHER_KEYS_FILE, "utf-8"));
2549
+ return parsed && typeof parsed === "object" ? parsed : {};
2550
+ } catch {
2551
+ return {};
2552
+ }
2553
+ }
2554
+ function verifyModuleSignature(modulePath, digest) {
2555
+ const keys = readPublisherKeys();
2556
+ const pinnedKeyIds = Object.keys(keys);
2557
+ const sigPath = (0, import_node_path3.join)(modulePath, "mod.sig");
2558
+ if (pinnedKeyIds.length === 0) {
2559
+ return { ok: true };
2560
+ }
2561
+ if (!(0, import_node_fs5.existsSync)(sigPath)) {
2562
+ return { ok: false, reason: "publisher keys are pinned but the module ships no mod.sig" };
2563
+ }
2564
+ let parsed;
2565
+ try {
2566
+ parsed = JSON.parse((0, import_node_fs5.readFileSync)(sigPath, "utf-8"));
2567
+ } catch {
2568
+ return { ok: false, reason: "mod.sig is not valid JSON" };
2569
+ }
2570
+ if (!parsed.keyId || !parsed.signature) {
2571
+ return { ok: false, reason: "mod.sig is missing keyId or signature" };
2572
+ }
2573
+ const publicKey = keys[parsed.keyId];
2574
+ if (!publicKey) {
2575
+ return { ok: false, reason: `mod.sig references unpinned key "${parsed.keyId}"` };
2576
+ }
2577
+ try {
2578
+ const valid = (0, import_node_crypto2.verify)(
2579
+ null,
2580
+ Buffer.from(digest, "hex"),
2581
+ publicKey,
2582
+ Buffer.from(parsed.signature, "base64")
2583
+ );
2584
+ return valid ? { ok: true } : { ok: false, reason: "mod.sig signature does not match" };
2585
+ } catch (err) {
2586
+ return { ok: false, reason: `signature check failed: ${String(err.message || err)}` };
2587
+ }
2588
+ }
2589
+ function verifyModuleTrust(name, modulePath) {
2590
+ let digest;
2591
+ try {
2592
+ digest = computeModuleDigest(modulePath);
2593
+ } catch (err) {
2594
+ return { ok: false, reason: `could not hash module: ${String(err.message || err)}` };
2595
+ }
2596
+ const signature = verifyModuleSignature(modulePath, digest);
2597
+ if (!signature.ok) return signature;
2598
+ const record = readTrustFile().modules[name];
2599
+ if (!record) {
2600
+ return {
2601
+ ok: false,
2602
+ reason: `not trusted \u2014 review it, then run: threatcrush modules trust ${name}`
2603
+ };
2604
+ }
2605
+ if (record.digest !== digest) {
2606
+ return {
2607
+ ok: false,
2608
+ reason: `contents changed since it was trusted on ${record.trustedAt} \u2014 re-review it, then run: threatcrush modules trust ${name}`
2609
+ };
2610
+ }
2611
+ return { ok: true };
2612
+ }
2613
+
2463
2614
  // src/daemon/watchers/log-watcher.ts
2464
- var import_node_fs4 = require("fs");
2615
+ var import_node_fs6 = require("fs");
2465
2616
  var import_node_readline = require("readline");
2466
2617
 
2467
2618
  // src/core/log-parser.ts
@@ -2618,9 +2769,9 @@ var LogWatcher = class {
2618
2769
  start() {
2619
2770
  const started = [];
2620
2771
  for (const src of this.sources) {
2621
- if (!(0, import_node_fs4.existsSync)(src.path)) continue;
2772
+ if (!(0, import_node_fs6.existsSync)(src.path)) continue;
2622
2773
  try {
2623
- (0, import_node_fs4.accessSync)(src.path, import_node_fs4.constants.R_OK);
2774
+ (0, import_node_fs6.accessSync)(src.path, import_node_fs6.constants.R_OK);
2624
2775
  } catch {
2625
2776
  continue;
2626
2777
  }
@@ -2640,7 +2791,7 @@ var LogWatcher = class {
2640
2791
  }
2641
2792
  tail(src) {
2642
2793
  try {
2643
- this.positions.set(src.path, (0, import_node_fs4.statSync)(src.path).size);
2794
+ this.positions.set(src.path, (0, import_node_fs6.statSync)(src.path).size);
2644
2795
  } catch {
2645
2796
  this.positions.set(src.path, 0);
2646
2797
  }
@@ -2651,7 +2802,7 @@ var LogWatcher = class {
2651
2802
  poll(src) {
2652
2803
  let stat;
2653
2804
  try {
2654
- stat = (0, import_node_fs4.statSync)(src.path);
2805
+ stat = (0, import_node_fs6.statSync)(src.path);
2655
2806
  } catch {
2656
2807
  return;
2657
2808
  }
@@ -2661,7 +2812,7 @@ var LogWatcher = class {
2661
2812
  return;
2662
2813
  }
2663
2814
  if (stat.size === prev) return;
2664
- const stream = (0, import_node_fs4.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
2815
+ const stream = (0, import_node_fs6.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
2665
2816
  stream.on("error", () => this.positions.set(src.path, stat.size));
2666
2817
  const rl = (0, import_node_readline.createInterface)({ input: stream });
2667
2818
  rl.on("error", () => {
@@ -2853,7 +3004,7 @@ function realtimeToDate(rt) {
2853
3004
 
2854
3005
  // src/modules/network-monitor/index.ts
2855
3006
  var import_node_child_process2 = require("child_process");
2856
- var import_node_fs5 = require("fs");
3007
+ var import_node_fs7 = require("fs");
2857
3008
  var NetworkMonitor = class {
2858
3009
  constructor(bus2) {
2859
3010
  this.bus = bus2;
@@ -2891,7 +3042,7 @@ var NetworkMonitor = class {
2891
3042
  hasConntrackOrSs() {
2892
3043
  const ss = (0, import_node_child_process2.spawnSync)("ss", ["--version"], { stdio: "pipe" });
2893
3044
  if (ss.status === 0) return true;
2894
- return (0, import_node_fs5.existsSync)("/proc/net/tcp");
3045
+ return (0, import_node_fs7.existsSync)("/proc/net/tcp");
2895
3046
  }
2896
3047
  poll() {
2897
3048
  try {
@@ -3030,7 +3181,7 @@ var NetworkMonitor = class {
3030
3181
  };
3031
3182
 
3032
3183
  // src/modules/dns-monitor/index.ts
3033
- var import_node_fs6 = require("fs");
3184
+ var import_node_fs8 = require("fs");
3034
3185
  var import_node_readline2 = require("readline");
3035
3186
  var DNS_LOG_SOURCES = [
3036
3187
  "/var/log/syslog",
@@ -3064,9 +3215,9 @@ var DnsMonitor = class {
3064
3215
  entropyThreshold = 3.5;
3065
3216
  start() {
3066
3217
  const sources = DNS_LOG_SOURCES.filter((p) => {
3067
- if (!(0, import_node_fs6.existsSync)(p)) return false;
3218
+ if (!(0, import_node_fs8.existsSync)(p)) return false;
3068
3219
  try {
3069
- (0, import_node_fs6.accessSync)(p, import_node_fs6.constants.R_OK);
3220
+ (0, import_node_fs8.accessSync)(p, import_node_fs8.constants.R_OK);
3070
3221
  return true;
3071
3222
  } catch {
3072
3223
  return false;
@@ -3090,7 +3241,7 @@ var DnsMonitor = class {
3090
3241
  }
3091
3242
  tailLog(path) {
3092
3243
  try {
3093
- this.positions.set(path, (0, import_node_fs6.statSync)(path).size);
3244
+ this.positions.set(path, (0, import_node_fs8.statSync)(path).size);
3094
3245
  } catch {
3095
3246
  this.positions.set(path, 0);
3096
3247
  }
@@ -3100,7 +3251,7 @@ var DnsMonitor = class {
3100
3251
  pollLog(path) {
3101
3252
  let stat;
3102
3253
  try {
3103
- stat = (0, import_node_fs6.statSync)(path);
3254
+ stat = (0, import_node_fs8.statSync)(path);
3104
3255
  } catch {
3105
3256
  return;
3106
3257
  }
@@ -3110,7 +3261,7 @@ var DnsMonitor = class {
3110
3261
  return;
3111
3262
  }
3112
3263
  if (stat.size === prev) return;
3113
- const stream = (0, import_node_fs6.createReadStream)(path, { start: prev, encoding: "utf-8" });
3264
+ const stream = (0, import_node_fs8.createReadStream)(path, { start: prev, encoding: "utf-8" });
3114
3265
  stream.on("error", () => this.positions.set(path, stat.size));
3115
3266
  const rl = (0, import_node_readline2.createInterface)({ input: stream });
3116
3267
  rl.on("line", (line) => this.parseDnsLine(line));
@@ -3239,8 +3390,8 @@ var DnsMonitor = class {
3239
3390
  };
3240
3391
 
3241
3392
  // src/core/config.ts
3242
- var import_node_fs7 = require("fs");
3243
- var import_node_path2 = require("path");
3393
+ var import_node_fs9 = require("fs");
3394
+ var import_node_path4 = require("path");
3244
3395
  var import_toml = __toESM(require_toml());
3245
3396
  var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
3246
3397
  var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
@@ -3266,11 +3417,11 @@ var DEFAULT_CONFIG = {
3266
3417
  };
3267
3418
  function loadConfig(configPath) {
3268
3419
  const path = configPath || DEFAULT_CONFIG_PATH;
3269
- if (!(0, import_node_fs7.existsSync)(path)) {
3420
+ if (!(0, import_node_fs9.existsSync)(path)) {
3270
3421
  return { ...DEFAULT_CONFIG };
3271
3422
  }
3272
3423
  try {
3273
- const raw = (0, import_node_fs7.readFileSync)(path, "utf-8");
3424
+ const raw = (0, import_node_fs9.readFileSync)(path, "utf-8");
3274
3425
  const parsed = import_toml.default.parse(raw);
3275
3426
  return {
3276
3427
  daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
@@ -3286,13 +3437,13 @@ function loadConfig(configPath) {
3286
3437
  function loadModuleConfigs(confDir) {
3287
3438
  const dir = confDir || DEFAULT_CONFDIR;
3288
3439
  const configs = /* @__PURE__ */ new Map();
3289
- if (!(0, import_node_fs7.existsSync)(dir)) {
3440
+ if (!(0, import_node_fs9.existsSync)(dir)) {
3290
3441
  return configs;
3291
3442
  }
3292
- const files = (0, import_node_fs7.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
3443
+ const files = (0, import_node_fs9.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
3293
3444
  for (const file of files) {
3294
3445
  try {
3295
- const raw = (0, import_node_fs7.readFileSync)((0, import_node_path2.join)(dir, file), "utf-8");
3446
+ const raw = (0, import_node_fs9.readFileSync)((0, import_node_path4.join)(dir, file), "utf-8");
3296
3447
  const parsed = import_toml.default.parse(raw);
3297
3448
  for (const [name, config] of Object.entries(parsed)) {
3298
3449
  configs.set(name, config);
@@ -3406,15 +3557,15 @@ var ModuleHost = class {
3406
3557
  for (const m of builtins) this.modules.set(m.name, m);
3407
3558
  }
3408
3559
  async discoverAndStartInstalled() {
3409
- if (!(0, import_node_fs8.existsSync)(PATHS.moduleDir)) return;
3560
+ if (!(0, import_node_fs10.existsSync)(PATHS.moduleDir)) return;
3410
3561
  const configs = loadModuleConfigs(PATHS.confD);
3411
- const entries = (0, import_node_fs8.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
3562
+ const entries = (0, import_node_fs10.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
3412
3563
  for (const entry of entries) {
3413
3564
  if (!entry.isDirectory()) continue;
3414
- const manifestPath = (0, import_node_path3.join)(PATHS.moduleDir, entry.name, "mod.toml");
3415
- if (!(0, import_node_fs8.existsSync)(manifestPath)) continue;
3565
+ const manifestPath = (0, import_node_path5.join)(PATHS.moduleDir, entry.name, "mod.toml");
3566
+ if (!(0, import_node_fs10.existsSync)(manifestPath)) continue;
3416
3567
  try {
3417
- const manifest = import_toml2.default.parse((0, import_node_fs8.readFileSync)(manifestPath, "utf-8"));
3568
+ const manifest = import_toml2.default.parse((0, import_node_fs10.readFileSync)(manifestPath, "utf-8"));
3418
3569
  const name = manifest.module?.name || entry.name;
3419
3570
  const defaults = manifest.module?.config?.defaults || {};
3420
3571
  const config = {
@@ -3428,7 +3579,7 @@ var ModuleHost = class {
3428
3579
  source: "installed",
3429
3580
  status: config.enabled === false ? "disabled" : "loaded",
3430
3581
  events: 0,
3431
- path: (0, import_node_path3.join)(PATHS.moduleDir, entry.name),
3582
+ path: (0, import_node_path5.join)(PATHS.moduleDir, entry.name),
3432
3583
  config
3433
3584
  };
3434
3585
  this.modules.set(name, hosted);
@@ -3443,7 +3594,7 @@ var ModuleHost = class {
3443
3594
  status: "error",
3444
3595
  events: 0,
3445
3596
  detail: `manifest load failed: ${String(err.message || err)}`,
3446
- path: (0, import_node_path3.join)(PATHS.moduleDir, entry.name)
3597
+ path: (0, import_node_path5.join)(PATHS.moduleDir, entry.name)
3447
3598
  });
3448
3599
  }
3449
3600
  }
@@ -3455,6 +3606,13 @@ var ModuleHost = class {
3455
3606
  hosted.detail = "no built entrypoint found; run npm install && npm run build in the module directory";
3456
3607
  return;
3457
3608
  }
3609
+ const trust = verifyModuleTrust(hosted.name, hosted.path);
3610
+ if (!trust.ok) {
3611
+ hosted.status = "error";
3612
+ hosted.detail = `refusing to load: ${trust.reason}`;
3613
+ this.bus.announceModule(hosted.name, "error", hosted.detail);
3614
+ return;
3615
+ }
3458
3616
  try {
3459
3617
  const imported = await import((0, import_node_url.pathToFileURL)(entrypoint).href);
3460
3618
  const exported = imported.default || imported.module || imported;
@@ -3475,17 +3633,17 @@ var ModuleHost = class {
3475
3633
  }
3476
3634
  }
3477
3635
  installedEntrypoint(modulePath) {
3478
- const packageJson = (0, import_node_path3.join)(modulePath, "package.json");
3636
+ const packageJson = (0, import_node_path5.join)(modulePath, "package.json");
3479
3637
  const candidates = [];
3480
- if ((0, import_node_fs8.existsSync)(packageJson)) {
3638
+ if ((0, import_node_fs10.existsSync)(packageJson)) {
3481
3639
  try {
3482
- const pkg = JSON.parse((0, import_node_fs8.readFileSync)(packageJson, "utf-8"));
3483
- if (pkg.main) candidates.push((0, import_node_path3.join)(modulePath, pkg.main));
3640
+ const pkg = JSON.parse((0, import_node_fs10.readFileSync)(packageJson, "utf-8"));
3641
+ if (pkg.main) candidates.push((0, import_node_path5.join)(modulePath, pkg.main));
3484
3642
  } catch {
3485
3643
  }
3486
3644
  }
3487
- candidates.push((0, import_node_path3.join)(modulePath, "dist", "index.js"), (0, import_node_path3.join)(modulePath, "index.js"));
3488
- return candidates.find((candidate) => (0, import_node_fs8.existsSync)(candidate)) || null;
3645
+ candidates.push((0, import_node_path5.join)(modulePath, "dist", "index.js"), (0, import_node_path5.join)(modulePath, "index.js"));
3646
+ return candidates.find((candidate) => (0, import_node_fs10.existsSync)(candidate)) || null;
3489
3647
  }
3490
3648
  isThreatCrushModule(value) {
3491
3649
  return Boolean(
@@ -3757,14 +3915,14 @@ function slackChannel(webhookUrl) {
3757
3915
  }
3758
3916
 
3759
3917
  // src/core/cli-config.ts
3760
- var import_node_fs9 = require("fs");
3761
- var import_node_path4 = require("path");
3918
+ var import_node_fs11 = require("fs");
3919
+ var import_node_path6 = require("path");
3762
3920
  var import_node_os2 = require("os");
3763
- var CLI_CONFIG_DIR = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".threatcrush");
3764
- var CLI_CONFIG_PATH = (0, import_node_path4.join)(CLI_CONFIG_DIR, "config.json");
3921
+ var CLI_CONFIG_DIR = (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".threatcrush");
3922
+ var CLI_CONFIG_PATH = (0, import_node_path6.join)(CLI_CONFIG_DIR, "config.json");
3765
3923
  function readCliConfig() {
3766
3924
  try {
3767
- return JSON.parse((0, import_node_fs9.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
3925
+ return JSON.parse((0, import_node_fs11.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
3768
3926
  } catch {
3769
3927
  return {};
3770
3928
  }
@@ -3783,8 +3941,8 @@ function authHeaders() {
3783
3941
  }
3784
3942
 
3785
3943
  // src/commands/scan.ts
3786
- var import_node_fs12 = require("fs");
3787
- var import_node_path8 = require("path");
3944
+ var import_node_fs14 = require("fs");
3945
+ var import_node_path10 = require("path");
3788
3946
 
3789
3947
  // ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
3790
3948
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -4527,7 +4685,17 @@ var NODE_RULES = [
4527
4685
  // arrangement of nearby lines that makes an echoed origin safe — except a
4528
4686
  // membership test on the value, which is what the guard looks for.
4529
4687
  inherent: true,
4530
- guard: ORIGIN_ALLOWLIST_GUARD
4688
+ guard: ORIGIN_ALLOWLIST_GUARD,
4689
+ /**
4690
+ * Logging the origin is not reflecting it.
4691
+ *
4692
+ * `origin: request.headers.origin` is the CORS mistake *and* the ordinary
4693
+ * way to record who called — the two are character-for-character
4694
+ * identical, and only what encloses them differs. A response header goes
4695
+ * out to the browser; a log line goes to stdout, where it grants nobody
4696
+ * anything.
4697
+ */
4698
+ enclosingCallGuard: /(?:^|\.)(?:log|debug|info|warn|error|trace|verbose|fatal)$/
4531
4699
  },
4532
4700
  // ── Server-side request forgery ──────────────────────────────────────────
4533
4701
  {
@@ -4776,7 +4944,8 @@ var CODE_RULES = [
4776
4944
  cwe: "CWE-78",
4777
4945
  severity: "critical",
4778
4946
  languages: ["javascript", "typescript"],
4779
- pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/
4947
+ pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/,
4948
+ constantInterpolationGuard: true
4780
4949
  },
4781
4950
  {
4782
4951
  id: "py-shell-command-string",
@@ -5675,7 +5844,26 @@ var CODE_RULES = [
5675
5844
  cwe: "CWE-346",
5676
5845
  severity: "high",
5677
5846
  languages: ["javascript", "typescript"],
5678
- pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b/
5847
+ pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b/,
5848
+ /**
5849
+ * Parsing the request's own URL is not building a link from the Host
5850
+ * header, even though it is spelled with one.
5851
+ *
5852
+ * const url = new URL(request.url, `http://${request.headers.host}`);
5853
+ * const token = url.searchParams.get('token');
5854
+ *
5855
+ * `request.url` on a Node server is a path — `/ws?token=…` — and `new URL`
5856
+ * refuses a relative input without a base. The base exists to satisfy the
5857
+ * parser and is thrown away; only the path and query are ever read. Every
5858
+ * Node HTTP handler that wants a query parameter is written this way, so
5859
+ * the rule fired on the framework idiom rather than on the defect.
5860
+ *
5861
+ * Narrow on purpose: the first argument must be `req.url` itself. The
5862
+ * dangerous shape passes a *path* the application chose —
5863
+ * `new URL('/reset?t=…', `https://${req.headers.host}`)` — and that is what
5864
+ * produces an attacker-controlled link. It does not match this guard.
5865
+ */
5866
+ lineGuard: /\bnew\s+URL\s*\(\s*(?:req|request|ctx)(?:uest)?\s*\.\s*url\b\s*,/
5679
5867
  },
5680
5868
  // A recursive-merge prototype-pollution rule (`target[key] = source[key]`
5681
5869
  // with no `__proto__` guard) was built and dropped. The bare copy-by-key is
@@ -5743,6 +5931,55 @@ function fileTextOf(lines) {
5743
5931
  function withoutSingleQuoted(text) {
5744
5932
  return text.replace(/'[^'\n]*'/g, "''");
5745
5933
  }
5934
+ function calleeEndingAt(text, open) {
5935
+ let end = open;
5936
+ while (end > 0 && (text[end - 1] === " " || text[end - 1] === " ")) end -= 1;
5937
+ let start = end;
5938
+ while (start > 0 && /[\w$.]/.test(text[start - 1])) start -= 1;
5939
+ return text.slice(start, end);
5940
+ }
5941
+ function enclosingCallees(lines, index, back) {
5942
+ const before = lines.slice(Math.max(0, index - back), index).join("\n");
5943
+ const stack = [];
5944
+ let quote = null;
5945
+ for (let i = 0; i < before.length; i += 1) {
5946
+ const ch = before[i];
5947
+ if (quote) {
5948
+ if (ch === "\\") i += 1;
5949
+ else if (ch === quote) quote = null;
5950
+ continue;
5951
+ }
5952
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
5953
+ else if (ch === "(") stack.push(calleeEndingAt(before, i));
5954
+ else if (ch === ")") stack.pop();
5955
+ }
5956
+ return stack;
5957
+ }
5958
+ function interpolations(line) {
5959
+ const found = [];
5960
+ for (let i = 0; ; ) {
5961
+ const start = line.indexOf("${", i);
5962
+ if (start === -1) break;
5963
+ const end = line.indexOf("}", start + 2);
5964
+ if (end === -1) return null;
5965
+ found.push(line.slice(start + 2, end).trim());
5966
+ i = end + 1;
5967
+ }
5968
+ return found.length > 0 ? found : null;
5969
+ }
5970
+ function isConstantString(name, fileText) {
5971
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
5972
+ return new RegExp(
5973
+ `\\bconst\\s+${escaped}\\s*(?::[^=\\n]+)?=\\s*(?:'[^'\\n]*'|"[^"\\n]*"|\`[^\`$\\n]*\`)`
5974
+ ).test(fileText);
5975
+ }
5976
+ function interpolationsAreConstant(line, fileText) {
5977
+ const found = interpolations(line);
5978
+ if (!found) return false;
5979
+ return found.every(
5980
+ (expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
5981
+ );
5982
+ }
5746
5983
  function evaluateRule(rule, ctx) {
5747
5984
  if (rule.languages && !rule.languages.includes(ctx.language)) return null;
5748
5985
  const line = ctx.lines[ctx.index] ?? "";
@@ -5754,6 +5991,13 @@ function evaluateRule(rule, ctx) {
5754
5991
  const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
5755
5992
  if (rule.requires && !rule.requires.test(context)) return null;
5756
5993
  if (rule.lineGuard?.test(line)) return null;
5994
+ if (rule.enclosingCallGuard) {
5995
+ const callees = enclosingCallees(ctx.lines, ctx.index, back);
5996
+ if (callees.some((callee) => rule.enclosingCallGuard.test(callee))) return null;
5997
+ }
5998
+ if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
5999
+ return null;
6000
+ }
5757
6001
  const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
5758
6002
  if (guard && (guard.test(line) || guard.test(context))) return null;
5759
6003
  const untrusted = untrustedPatternFor(ctx.language);
@@ -6336,7 +6580,8 @@ var SECRET_RULES = [
6336
6580
  pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
6337
6581
  severity: "medium",
6338
6582
  cwe: "CWE-798",
6339
- consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
6583
+ consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived.",
6584
+ keywordShaped: true
6340
6585
  },
6341
6586
  {
6342
6587
  id: "secret-generic-api-key",
@@ -6347,7 +6592,8 @@ var SECRET_RULES = [
6347
6592
  pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
6348
6593
  severity: "high",
6349
6594
  cwe: "CWE-798",
6350
- consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
6595
+ consequence: "Whatever the third-party service lets the key do, for as long as it stays valid.",
6596
+ keywordShaped: true
6351
6597
  },
6352
6598
  {
6353
6599
  id: "secret-generic-credential",
@@ -6355,7 +6601,8 @@ var SECRET_RULES = [
6355
6601
  pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
6356
6602
  severity: "high",
6357
6603
  cwe: "CWE-798",
6358
- consequence: "A password in source is a password in every clone, fork and CI cache of that source."
6604
+ consequence: "A password in source is a password in every clone, fork and CI cache of that source.",
6605
+ keywordShaped: true
6359
6606
  },
6360
6607
  {
6361
6608
  id: "secret-hex-token",
@@ -6363,7 +6610,8 @@ var SECRET_RULES = [
6363
6610
  pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
6364
6611
  severity: "medium",
6365
6612
  cwe: "CWE-798",
6366
- consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
6613
+ consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign.",
6614
+ keywordShaped: true
6367
6615
  }
6368
6616
  ];
6369
6617
  var KNOWN_PLACEHOLDERS = [
@@ -6394,6 +6642,54 @@ var KNOWN_PLACEHOLDERS = [
6394
6642
  function isKnownPlaceholder(text) {
6395
6643
  return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
6396
6644
  }
6645
+ function isVariableReference(value) {
6646
+ const trimmed = value.trim();
6647
+ const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
6648
+ if (braced) {
6649
+ const fallback = braced[2];
6650
+ if (fallback === void 0 || fallback.trim() === "") return true;
6651
+ return /^\$\{?[A-Za-z_][\w.]*\}?$/.test(fallback.trim());
6652
+ }
6653
+ return /^\$[A-Za-z_]\w*$/.test(trimmed) || // $VAR
6654
+ /^\$\([\s\S]*\)$/.test(trimmed) || // $(command substitution)
6655
+ /^%[A-Za-z_]\w*%$/.test(trimmed) || // %VAR% on Windows
6656
+ /^\{\{[\s\S]*\}\}$/.test(trimmed) || // {{ template }}
6657
+ /^#\{[\s\S]*\}$/.test(trimmed) || // #{ruby}
6658
+ /^<%=?[\s\S]*%>$/.test(trimmed);
6659
+ }
6660
+ var FIXTURE_STEMS = [
6661
+ "test",
6662
+ "mock",
6663
+ "fake",
6664
+ "dummy",
6665
+ "stub",
6666
+ "sample",
6667
+ "example",
6668
+ "placeholder",
6669
+ "fixture",
6670
+ "invalid",
6671
+ "expired",
6672
+ "forged",
6673
+ "bogus",
6674
+ "notreal",
6675
+ "nonexistent",
6676
+ "changeme",
6677
+ "foobar",
6678
+ "lorem"
6679
+ ];
6680
+ var KEY_NOISE = /* @__PURE__ */ new Set(["const", "this", "return", "await", "async", "expect", "value"]);
6681
+ function words(text) {
6682
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z]+/g, " ").toLowerCase().split(" ").filter(Boolean);
6683
+ }
6684
+ function isTestFixtureValue(line, value) {
6685
+ const valueWords = words(value);
6686
+ if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
6687
+ if (value.length > 48) return false;
6688
+ const valueAt = line.lastIndexOf(value);
6689
+ const key = valueAt === -1 ? line : line.slice(0, valueAt);
6690
+ const flattened = valueWords.join("");
6691
+ return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
6692
+ }
6397
6693
  function redactSecret(line) {
6398
6694
  return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
6399
6695
  if (match.length <= 12) return match;
@@ -6612,6 +6908,9 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
6612
6908
  if (!match) continue;
6613
6909
  if (isKnownPlaceholder(match[0])) continue;
6614
6910
  if (isSuppressed(suppressions, index, rule.id)) continue;
6911
+ const value = match[1] ?? match[0];
6912
+ if (isVariableReference(value)) continue;
6913
+ if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
6615
6914
  const marked = foreignCredentialMark(line);
6616
6915
  findings.push({
6617
6916
  ruleId: rule.id,
@@ -6690,8 +6989,8 @@ function scanManifest(relativePath, filename, text) {
6690
6989
  }
6691
6990
 
6692
6991
  // ../../packages/scan/src/node/walk.ts
6693
- var import_node_fs10 = require("fs");
6694
- var import_node_path5 = require("path");
6992
+ var import_node_fs12 = require("fs");
6993
+ var import_node_path7 = require("path");
6695
6994
  function compileExcludes(patterns) {
6696
6995
  const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
6697
6996
  if (matchers.length === 0) return () => false;
@@ -6762,7 +7061,7 @@ function matchPrefix(parts, segs) {
6762
7061
  }
6763
7062
  function readIgnoreFile(root) {
6764
7063
  try {
6765
- return (0, import_node_fs10.readFileSync)((0, import_node_path5.join)(root, ".threatcrushignore"), "utf-8").split("\n");
7064
+ return (0, import_node_fs12.readFileSync)((0, import_node_path7.join)(root, ".threatcrushignore"), "utf-8").split("\n");
6766
7065
  } catch {
6767
7066
  return [];
6768
7067
  }
@@ -6778,16 +7077,16 @@ function scanPath(targetPath, options = {}) {
6778
7077
  let excluded = 0;
6779
7078
  const rootIsDirectory = (() => {
6780
7079
  try {
6781
- return (0, import_node_fs10.statSync)(targetPath).isDirectory();
7080
+ return (0, import_node_fs12.statSync)(targetPath).isDirectory();
6782
7081
  } catch {
6783
7082
  return true;
6784
7083
  }
6785
7084
  })();
6786
- const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path5.dirname)(targetPath);
7085
+ const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path7.dirname)(targetPath);
6787
7086
  const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
6788
7087
  const scanFile = (fullPath, filename) => {
6789
7088
  const relativePath = toRelative(walkRoot, fullPath);
6790
- const extension = (0, import_node_path5.extname)(filename).toLowerCase();
7089
+ const extension = (0, import_node_path7.extname)(filename).toLowerCase();
6791
7090
  const isManifest = filename === "package.json" || filename === "requirements.txt";
6792
7091
  const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
6793
7092
  const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
@@ -6799,26 +7098,26 @@ function scanPath(targetPath, options = {}) {
6799
7098
  let handle;
6800
7099
  let declared = null;
6801
7100
  try {
6802
- handle = (0, import_node_fs10.openSync)(fullPath, "r");
7101
+ handle = (0, import_node_fs12.openSync)(fullPath, "r");
6803
7102
  } catch {
6804
7103
  unreadable.push(relativePath);
6805
7104
  return;
6806
7105
  }
6807
7106
  try {
6808
- if ((0, import_node_fs10.fstatSync)(handle).size > maxFileBytes) return;
7107
+ if ((0, import_node_fs12.fstatSync)(handle).size > maxFileBytes) return;
6809
7108
  if (mayDeclareInterpreter) {
6810
7109
  const prefix = Buffer.alloc(128);
6811
- const read = (0, import_node_fs10.readSync)(handle, prefix, 0, prefix.length, 0);
7110
+ const read = (0, import_node_fs12.readSync)(handle, prefix, 0, prefix.length, 0);
6812
7111
  declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
6813
7112
  if (!declared) return;
6814
7113
  }
6815
- text = (0, import_node_fs10.readFileSync)(handle, "utf-8");
7114
+ text = (0, import_node_fs12.readFileSync)(handle, "utf-8");
6816
7115
  } catch {
6817
7116
  unreadable.push(relativePath);
6818
7117
  return;
6819
7118
  } finally {
6820
7119
  try {
6821
- (0, import_node_fs10.closeSync)(handle);
7120
+ (0, import_node_fs12.closeSync)(handle);
6822
7121
  } catch {
6823
7122
  }
6824
7123
  }
@@ -6836,13 +7135,13 @@ function scanPath(targetPath, options = {}) {
6836
7135
  const walk = (currentPath) => {
6837
7136
  let entries;
6838
7137
  try {
6839
- entries = (0, import_node_fs10.readdirSync)(currentPath, { withFileTypes: true });
7138
+ entries = (0, import_node_fs12.readdirSync)(currentPath, { withFileTypes: true });
6840
7139
  } catch {
6841
7140
  unreadable.push(toRelative(walkRoot, currentPath));
6842
7141
  return;
6843
7142
  }
6844
7143
  for (const entry of entries) {
6845
- const fullPath = (0, import_node_path5.join)(currentPath, entry.name);
7144
+ const fullPath = (0, import_node_path7.join)(currentPath, entry.name);
6846
7145
  const relativePath = toRelative(walkRoot, fullPath);
6847
7146
  if (entry.isDirectory()) {
6848
7147
  if (SKIP_DIRS.has(entry.name)) continue;
@@ -6866,7 +7165,7 @@ function scanPath(targetPath, options = {}) {
6866
7165
  } else if (isExcluded(toRelative(walkRoot, targetPath))) {
6867
7166
  excluded += 1;
6868
7167
  } else {
6869
- scanFile(targetPath, (0, import_node_path5.basename)(targetPath));
7168
+ scanFile(targetPath, (0, import_node_path7.basename)(targetPath));
6870
7169
  }
6871
7170
  if (options.missingControls) findings.push(...controls.findings());
6872
7171
  const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
@@ -6898,32 +7197,37 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
6898
7197
  }
6899
7198
  }
6900
7199
  function toRelative(base, target) {
6901
- const rel = (0, import_node_path5.relative)(base, target);
6902
- return (rel === "" ? target : rel).split(import_node_path5.sep).join("/");
7200
+ const rel = (0, import_node_path7.relative)(base, target);
7201
+ return (rel === "" ? target : rel).split(import_node_path7.sep).join("/");
6903
7202
  }
6904
7203
 
6905
7204
  // ../../packages/scan/src/node/dependencies.ts
6906
- var import_node_fs11 = require("fs");
6907
- var import_node_path6 = require("path");
6908
- var LOCKFILES = [
6909
- { file: "package-lock.json", ecosystem: "npm" },
6910
- { file: "pnpm-lock.yaml", ecosystem: "npm" },
6911
- { file: "yarn.lock", ecosystem: "npm" },
6912
- { file: "requirements.txt", ecosystem: "PyPI" },
6913
- { file: "Pipfile.lock", ecosystem: "PyPI" }
6914
- ];
7205
+ var import_node_fs13 = require("fs");
7206
+ var import_node_path8 = require("path");
6915
7207
  var MAX_DEPS_PER_LOCKFILE = 50;
6916
7208
  async function scanDependencies(targetPath) {
6917
7209
  const findings = [];
6918
- for (const { file, ecosystem } of LOCKFILES) {
6919
- const lockPath = (0, import_node_path6.join)(targetPath, file);
6920
- if (!(0, import_node_fs11.existsSync)(lockPath)) continue;
7210
+ for (const { file, ecosystem, parse } of LOCKFILES) {
7211
+ const lockPath = (0, import_node_path8.join)(targetPath, file);
7212
+ if (!(0, import_node_fs13.existsSync)(lockPath)) continue;
6921
7213
  let deps;
6922
7214
  try {
6923
- deps = parseDependencies(lockPath, file);
7215
+ deps = dedupe(parse((0, import_node_fs13.readFileSync)(lockPath, "utf-8")));
6924
7216
  } catch {
6925
7217
  continue;
6926
7218
  }
7219
+ if (deps.length === 0) {
7220
+ findings.push(incompleteFinding(file, "No dependencies could be read from this lockfile."));
7221
+ continue;
7222
+ }
7223
+ if (deps.length > MAX_DEPS_PER_LOCKFILE) {
7224
+ findings.push(
7225
+ incompleteFinding(
7226
+ file,
7227
+ `Only the first ${MAX_DEPS_PER_LOCKFILE} of ${deps.length} locked packages were checked against OSV.`
7228
+ )
7229
+ );
7230
+ }
6927
7231
  for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
6928
7232
  let vulns;
6929
7233
  try {
@@ -6950,6 +7254,20 @@ async function scanDependencies(targetPath) {
6950
7254
  }
6951
7255
  return findings;
6952
7256
  }
7257
+ function incompleteFinding(file, message) {
7258
+ return {
7259
+ ruleId: "dependency-scan-incomplete",
7260
+ title: "Dependency scan incomplete",
7261
+ file,
7262
+ line: 1,
7263
+ severity: "low",
7264
+ confidence: "evidence",
7265
+ message,
7266
+ consequence: "Advisories affecting the unchecked packages would not appear in this report.",
7267
+ excerpt: file,
7268
+ category: "dependency"
7269
+ };
7270
+ }
6953
7271
  function severityFromCvss(score) {
6954
7272
  if (!score) return "medium";
6955
7273
  const value = Number.parseFloat(score);
@@ -6959,26 +7277,131 @@ function severityFromCvss(score) {
6959
7277
  if (value >= 4) return "medium";
6960
7278
  return "low";
6961
7279
  }
6962
- function parseDependencies(lockPath, filename) {
7280
+ function dedupe(deps) {
7281
+ const seen = /* @__PURE__ */ new Set();
7282
+ const unique = [];
7283
+ for (const dep of deps) {
7284
+ const key = `${dep.name}@${dep.version}`;
7285
+ if (seen.has(key)) continue;
7286
+ seen.add(key);
7287
+ unique.push(dep);
7288
+ }
7289
+ return unique;
7290
+ }
7291
+ function splitNameVersion(spec) {
7292
+ const at = spec.lastIndexOf("@");
7293
+ if (at <= 0) return null;
7294
+ const name = spec.slice(0, at);
7295
+ const version = spec.slice(at + 1);
7296
+ if (!name || !version) return null;
7297
+ return { name, version };
7298
+ }
7299
+ function exactVersion(raw) {
7300
+ const version = raw.trim().replace(/^[=v]+/, "");
7301
+ return /^[0-9][0-9a-zA-Z.+-]*$/.test(version) ? version : null;
7302
+ }
7303
+ function parsePackageLock(content) {
7304
+ const lock = JSON.parse(content);
7305
+ const packages = lock.packages ?? lock.dependencies ?? {};
7306
+ const deps = [];
7307
+ for (const [key, value] of Object.entries(packages)) {
7308
+ const name = key.replace(/^.*node_modules\//, "");
7309
+ const version = value?.version;
7310
+ if (name && version && !name.startsWith(".")) deps.push({ name, version });
7311
+ }
7312
+ return deps;
7313
+ }
7314
+ function parsePnpmLock(content) {
6963
7315
  const deps = [];
6964
- if (filename === "package-lock.json") {
6965
- const lock = JSON.parse((0, import_node_fs11.readFileSync)(lockPath, "utf-8"));
6966
- const packages = lock.packages ?? lock.dependencies ?? {};
6967
- for (const [key, value] of Object.entries(packages)) {
6968
- const name = key.replace(/^node_modules\//, "");
6969
- const version = value?.version;
6970
- if (name && version && !name.startsWith(".")) deps.push({ name, version });
7316
+ let inPackages = false;
7317
+ for (const line of content.split("\n")) {
7318
+ if (/^[a-zA-Z]/.test(line)) {
7319
+ inPackages = line.startsWith("packages:");
7320
+ continue;
7321
+ }
7322
+ if (!inPackages) continue;
7323
+ const match = /^ {2}(?! )(.+):\s*$/.exec(line);
7324
+ if (!match?.[1]) continue;
7325
+ let key = match[1].trim().replace(/^['"]|['"]$/g, "");
7326
+ key = key.replace(/^\//, "");
7327
+ key = key.replace(/\(.*$/, "");
7328
+ let name;
7329
+ let rawVersion;
7330
+ const slashed = /^(@?[^@]+)\/([0-9][^/]*)$/.exec(key);
7331
+ if (slashed?.[1] && slashed[2]) {
7332
+ name = slashed[1];
7333
+ rawVersion = slashed[2];
7334
+ } else {
7335
+ const dep = splitNameVersion(key);
7336
+ if (!dep) continue;
7337
+ name = dep.name;
7338
+ rawVersion = dep.version;
6971
7339
  }
6972
- return deps;
7340
+ const version = exactVersion(rawVersion.replace(/_.*$/, ""));
7341
+ if (version) deps.push({ name, version });
6973
7342
  }
6974
- if (filename === "requirements.txt") {
6975
- for (const line of (0, import_node_fs11.readFileSync)(lockPath, "utf-8").split("\n")) {
6976
- const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
6977
- if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
7343
+ return deps;
7344
+ }
7345
+ function parseYarnLock(content) {
7346
+ const deps = [];
7347
+ let pendingName = null;
7348
+ for (const line of content.split("\n")) {
7349
+ if (line.startsWith("#") || line.trim() === "") continue;
7350
+ if (!/^\s/.test(line)) {
7351
+ pendingName = null;
7352
+ const header = line.replace(/:\s*$/, "");
7353
+ const first = header.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
7354
+ if (!first) continue;
7355
+ if (!first.includes("@") || first === "__metadata") continue;
7356
+ if (/@(?:workspace|file|link|portal|exec|patch):/.test(first)) continue;
7357
+ const dep = splitNameVersion(first.replace(/@npm:/, "@"));
7358
+ if (dep) pendingName = dep.name;
7359
+ continue;
6978
7360
  }
7361
+ if (!pendingName) continue;
7362
+ const version = /^\s+version:?\s+["']?([^"'\s]+)["']?\s*$/.exec(line);
7363
+ if (!version?.[1]) continue;
7364
+ const exact = exactVersion(version[1]);
7365
+ if (exact) deps.push({ name: pendingName, version: exact });
7366
+ pendingName = null;
6979
7367
  }
6980
7368
  return deps;
6981
7369
  }
7370
+ function parsePipfileLock(content) {
7371
+ const lock = JSON.parse(content);
7372
+ const deps = [];
7373
+ for (const section of ["default", "develop"]) {
7374
+ const packages = lock[section];
7375
+ if (!packages || typeof packages !== "object") continue;
7376
+ for (const [name, value] of Object.entries(packages)) {
7377
+ const version = exactVersion(String(value?.version ?? "").replace(/^==/, ""));
7378
+ if (name && version) deps.push({ name, version });
7379
+ }
7380
+ }
7381
+ return deps;
7382
+ }
7383
+ function parseRequirementsTxt(content) {
7384
+ const deps = [];
7385
+ for (const raw of content.split("\n")) {
7386
+ const line = raw.split("#")[0]?.split(";")[0]?.trim();
7387
+ if (!line || line.startsWith("-")) continue;
7388
+ const match = /^([a-zA-Z0-9._-]+)\s*(?:\[[^\]]*\])?\s*==\s*([^\s,]+)/.exec(line);
7389
+ if (!match?.[1] || !match[2]) continue;
7390
+ const version = exactVersion(match[2]);
7391
+ if (version) deps.push({ name: match[1], version });
7392
+ }
7393
+ return deps;
7394
+ }
7395
+ var LOCKFILES = [
7396
+ { file: "package-lock.json", ecosystem: "npm", parse: parsePackageLock },
7397
+ { file: "pnpm-lock.yaml", ecosystem: "npm", parse: parsePnpmLock },
7398
+ { file: "yarn.lock", ecosystem: "npm", parse: parseYarnLock },
7399
+ { file: "requirements.txt", ecosystem: "PyPI", parse: parseRequirementsTxt },
7400
+ { file: "Pipfile.lock", ecosystem: "PyPI", parse: parsePipfileLock }
7401
+ ];
7402
+ var LOCKFILE_PARSERS = Object.fromEntries(
7403
+ LOCKFILES.map((entry) => [entry.file, entry.parse])
7404
+ );
6982
7405
  function isValidPackageName(name) {
6983
7406
  return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
6984
7407
  }
@@ -7003,17 +7426,17 @@ async function queryOsv(name, version, ecosystem) {
7003
7426
  }
7004
7427
 
7005
7428
  // ../../packages/scan/src/node/sarif.ts
7006
- var import_node_crypto = require("crypto");
7007
- var import_node_path7 = require("path");
7429
+ var import_node_crypto3 = require("crypto");
7430
+ var import_node_path9 = require("path");
7008
7431
 
7009
7432
  // src/commands/scan.ts
7010
7433
  function readVersion() {
7011
7434
  for (const candidate of [
7012
- (0, import_node_path8.join)(__dirname, "..", "package.json"),
7013
- (0, import_node_path8.join)(__dirname, "..", "..", "package.json")
7435
+ (0, import_node_path10.join)(__dirname, "..", "package.json"),
7436
+ (0, import_node_path10.join)(__dirname, "..", "..", "package.json")
7014
7437
  ]) {
7015
7438
  try {
7016
- return JSON.parse((0, import_node_fs12.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
7439
+ return JSON.parse((0, import_node_fs14.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
7017
7440
  } catch {
7018
7441
  }
7019
7442
  }
@@ -7474,8 +7897,8 @@ var RuleEngine = class {
7474
7897
  };
7475
7898
 
7476
7899
  // src/daemon/rules/loader.ts
7477
- var import_node_fs13 = require("fs");
7478
- var import_node_path9 = require("path");
7900
+ var import_node_fs15 = require("fs");
7901
+ var import_node_path11 = require("path");
7479
7902
 
7480
7903
  // src/daemon/rules/default-rules.ts
7481
7904
  var DEFAULT_RULES = [
@@ -7759,11 +8182,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
7759
8182
  function loadAllRules(customDir) {
7760
8183
  const rules = [...DEFAULT_RULES];
7761
8184
  const dir = customDir || RULES_DIR;
7762
- if ((0, import_node_fs13.existsSync)(dir)) {
7763
- const files = (0, import_node_fs13.readdirSync)(dir).filter((f) => f.endsWith(".json"));
8185
+ if ((0, import_node_fs15.existsSync)(dir)) {
8186
+ const files = (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json"));
7764
8187
  for (const file of files) {
7765
8188
  try {
7766
- const raw = (0, import_node_fs13.readFileSync)((0, import_node_path9.join)(dir, file), "utf-8");
8189
+ const raw = (0, import_node_fs15.readFileSync)((0, import_node_path11.join)(dir, file), "utf-8");
7767
8190
  const parsed = JSON.parse(raw);
7768
8191
  const customRules = Array.isArray(parsed) ? parsed : [parsed];
7769
8192
  for (const rule of customRules) {
@@ -7926,7 +8349,7 @@ function detectFirewallAdapter() {
7926
8349
  }
7927
8350
 
7928
8351
  // src/daemon/firewall/remediation.ts
7929
- var import_node_fs14 = require("fs");
8352
+ var import_node_fs16 = require("fs");
7930
8353
  var DEFAULT_CONFIG2 = {
7931
8354
  enabled: true,
7932
8355
  dry_run: true,
@@ -8081,7 +8504,7 @@ var RemediationManager = class {
8081
8504
  }
8082
8505
  logLine(line) {
8083
8506
  try {
8084
- (0, import_node_fs14.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
8507
+ (0, import_node_fs16.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
8085
8508
  `);
8086
8509
  } catch {
8087
8510
  }
@@ -8137,7 +8560,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
8137
8560
  // src/daemon/index.ts
8138
8561
  function readVersion2() {
8139
8562
  try {
8140
- const pkg = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path10.join)(__dirname, "..", "package.json"), "utf-8"));
8563
+ const pkg = JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path12.join)(__dirname, "..", "package.json"), "utf-8"));
8141
8564
  return pkg.version || "0.0.0";
8142
8565
  } catch {
8143
8566
  return "0.0.0";
@@ -8145,7 +8568,7 @@ function readVersion2() {
8145
8568
  }
8146
8569
  function logLine(line) {
8147
8570
  try {
8148
- (0, import_node_fs15.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
8571
+ (0, import_node_fs17.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
8149
8572
  `);
8150
8573
  } catch {
8151
8574
  }
@@ -8173,7 +8596,7 @@ async function runDaemon() {
8173
8596
  } catch (err) {
8174
8597
  logLine(`[daemon] state db unavailable: ${err.message}`);
8175
8598
  }
8176
- const config = loadConfig((0, import_node_fs15.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
8599
+ const config = loadConfig((0, import_node_fs17.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
8177
8600
  bus.on("event", (event) => {
8178
8601
  logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
8179
8602
  });