@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/index.js CHANGED
@@ -3891,14 +3891,52 @@ var init_paths = __esm({
3891
3891
  }
3892
3892
  });
3893
3893
 
3894
+ // src/daemon/control-token.ts
3895
+ function issueControlToken() {
3896
+ const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
3897
+ (0, import_node_fs2.writeFileSync)(CONTROL_TOKEN_FILE, token, { mode: 384 });
3898
+ try {
3899
+ (0, import_node_fs2.chmodSync)(CONTROL_TOKEN_FILE, 384);
3900
+ } catch {
3901
+ }
3902
+ return token;
3903
+ }
3904
+ function readControlToken() {
3905
+ if (!(0, import_node_fs2.existsSync)(CONTROL_TOKEN_FILE)) return null;
3906
+ try {
3907
+ const token = (0, import_node_fs2.readFileSync)(CONTROL_TOKEN_FILE, "utf-8").trim();
3908
+ return token || null;
3909
+ } catch {
3910
+ return null;
3911
+ }
3912
+ }
3913
+ function tokensMatch(expected, provided) {
3914
+ if (typeof provided !== "string" || !provided) return false;
3915
+ const a = Buffer.from(expected);
3916
+ const b = Buffer.from(provided);
3917
+ return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
3918
+ }
3919
+ var import_node_crypto, import_node_fs2, import_node_path2, CONTROL_TOKEN_FILE;
3920
+ var init_control_token = __esm({
3921
+ "src/daemon/control-token.ts"() {
3922
+ "use strict";
3923
+ import_node_crypto = require("crypto");
3924
+ import_node_fs2 = require("fs");
3925
+ import_node_path2 = require("path");
3926
+ init_paths();
3927
+ CONTROL_TOKEN_FILE = (0, import_node_path2.join)(PATHS.runDir, "control.token");
3928
+ }
3929
+ });
3930
+
3894
3931
  // src/core/ipc-client.ts
3895
- var import_node_net, import_node_fs2, IpcClient;
3932
+ var import_node_net, import_node_fs3, IpcClient;
3896
3933
  var init_ipc_client = __esm({
3897
3934
  "src/core/ipc-client.ts"() {
3898
3935
  "use strict";
3899
3936
  import_node_net = require("net");
3900
- import_node_fs2 = require("fs");
3937
+ import_node_fs3 = require("fs");
3901
3938
  init_paths();
3939
+ init_control_token();
3902
3940
  IpcClient = class {
3903
3941
  constructor(opts = {}) {
3904
3942
  this.opts = opts;
@@ -3911,10 +3949,10 @@ var init_ipc_client = __esm({
3911
3949
  pending = /* @__PURE__ */ new Map();
3912
3950
  socketPath;
3913
3951
  static isDaemonRunning(socketPath = resolveClientSocket()) {
3914
- return (0, import_node_fs2.existsSync)(socketPath);
3952
+ return (0, import_node_fs3.existsSync)(socketPath);
3915
3953
  }
3916
3954
  async connect(timeoutMs = 2e3) {
3917
- if (!(0, import_node_fs2.existsSync)(this.socketPath)) {
3955
+ if (!(0, import_node_fs3.existsSync)(this.socketPath)) {
3918
3956
  throw new Error(`threatcrushd socket not found at ${this.socketPath}`);
3919
3957
  }
3920
3958
  return new Promise((resolve5, reject) => {
@@ -3975,7 +4013,7 @@ var init_ipc_client = __esm({
3975
4013
  await this.request("subscribe", { channels });
3976
4014
  }
3977
4015
  async shutdown() {
3978
- await this.request("shutdown");
4016
+ await this.request("shutdown", { token: readControlToken() });
3979
4017
  }
3980
4018
  onData(chunk) {
3981
4019
  this.buffer += chunk;
@@ -4018,17 +4056,17 @@ var init_ipc_client = __esm({
4018
4056
  // src/daemon/pidfile.ts
4019
4057
  function writePidFile() {
4020
4058
  ensureRuntimeDirs();
4021
- (0, import_node_fs3.writeFileSync)(PATHS.pidFile, String(process.pid), "utf-8");
4059
+ (0, import_node_fs4.writeFileSync)(PATHS.pidFile, String(process.pid), "utf-8");
4022
4060
  }
4023
4061
  function readPidFile() {
4024
- if (!(0, import_node_fs3.existsSync)(PATHS.pidFile)) return null;
4025
- const raw = (0, import_node_fs3.readFileSync)(PATHS.pidFile, "utf-8").trim();
4062
+ if (!(0, import_node_fs4.existsSync)(PATHS.pidFile)) return null;
4063
+ const raw = (0, import_node_fs4.readFileSync)(PATHS.pidFile, "utf-8").trim();
4026
4064
  const pid = parseInt(raw, 10);
4027
4065
  return Number.isFinite(pid) ? pid : null;
4028
4066
  }
4029
4067
  function removePidFile() {
4030
4068
  try {
4031
- if ((0, import_node_fs3.existsSync)(PATHS.pidFile)) (0, import_node_fs3.unlinkSync)(PATHS.pidFile);
4069
+ if ((0, import_node_fs4.existsSync)(PATHS.pidFile)) (0, import_node_fs4.unlinkSync)(PATHS.pidFile);
4032
4070
  } catch {
4033
4071
  }
4034
4072
  }
@@ -4047,11 +4085,11 @@ function findRunningDaemon() {
4047
4085
  if (pid) removePidFile();
4048
4086
  return null;
4049
4087
  }
4050
- var import_node_fs3;
4088
+ var import_node_fs4;
4051
4089
  var init_pidfile = __esm({
4052
4090
  "src/daemon/pidfile.ts"() {
4053
4091
  "use strict";
4054
- import_node_fs3 = require("fs");
4092
+ import_node_fs4 = require("fs");
4055
4093
  init_paths();
4056
4094
  }
4057
4095
  });
@@ -8260,12 +8298,12 @@ var source_default = chalk;
8260
8298
  // src/index.ts
8261
8299
  var import_readline = __toESM(require("readline"));
8262
8300
  var import_node_child_process10 = require("child_process");
8263
- var import_node_fs29 = require("fs");
8264
- var import_node_path19 = require("path");
8301
+ var import_node_fs31 = require("fs");
8302
+ var import_node_path21 = require("path");
8265
8303
  var import_node_os8 = require("os");
8266
8304
 
8267
8305
  // src/commands/monitor.ts
8268
- var import_node_fs4 = require("fs");
8306
+ var import_node_fs5 = require("fs");
8269
8307
  var import_node_readline = require("readline");
8270
8308
 
8271
8309
  // src/core/logger.ts
@@ -8473,9 +8511,9 @@ async function monitorCommand(options) {
8473
8511
  const unreadable = [];
8474
8512
  for (const s of LOG_SOURCES) {
8475
8513
  if (moduleFilter && !moduleFilter.includes(s.name)) continue;
8476
- if (!(0, import_node_fs4.existsSync)(s.path)) continue;
8514
+ if (!(0, import_node_fs5.existsSync)(s.path)) continue;
8477
8515
  try {
8478
- (0, import_node_fs4.accessSync)(s.path, import_node_fs4.constants.R_OK);
8516
+ (0, import_node_fs5.accessSync)(s.path, import_node_fs5.constants.R_OK);
8479
8517
  availableSources.push(s);
8480
8518
  } catch {
8481
8519
  unreadable.push(s);
@@ -8492,11 +8530,11 @@ async function monitorCommand(options) {
8492
8530
  logger.warn("No readable log files found to monitor.");
8493
8531
  logger.info("Available log paths checked:");
8494
8532
  for (const s of LOG_SOURCES) {
8495
- const exists = (0, import_node_fs4.existsSync)(s.path);
8533
+ const exists = (0, import_node_fs5.existsSync)(s.path);
8496
8534
  let readable = false;
8497
8535
  if (exists) {
8498
8536
  try {
8499
- (0, import_node_fs4.accessSync)(s.path, import_node_fs4.constants.R_OK);
8537
+ (0, import_node_fs5.accessSync)(s.path, import_node_fs5.constants.R_OK);
8500
8538
  readable = true;
8501
8539
  } catch {
8502
8540
  readable = false;
@@ -8528,16 +8566,16 @@ async function monitorCommand(options) {
8528
8566
  }
8529
8567
  function tailLog(source) {
8530
8568
  const { path, name, category } = source;
8531
- const stat = (0, import_node_fs4.statSync)(path);
8569
+ const stat = (0, import_node_fs5.statSync)(path);
8532
8570
  let position = stat.size;
8533
8571
  const checkForNewData = () => {
8534
8572
  try {
8535
- const currentStat = (0, import_node_fs4.statSync)(path);
8573
+ const currentStat = (0, import_node_fs5.statSync)(path);
8536
8574
  if (currentStat.size <= position) {
8537
8575
  if (currentStat.size < position) position = 0;
8538
8576
  return;
8539
8577
  }
8540
- const stream = (0, import_node_fs4.createReadStream)(path, { start: position, encoding: "utf-8" });
8578
+ const stream = (0, import_node_fs5.createReadStream)(path, { start: position, encoding: "utf-8" });
8541
8579
  stream.on("error", (err) => {
8542
8580
  logger.warn(`stopped tailing ${path}: ${err.code || err.message}`);
8543
8581
  position = currentStat.size;
@@ -8639,8 +8677,8 @@ async function runDemoMode() {
8639
8677
  }
8640
8678
 
8641
8679
  // src/commands/scan.ts
8642
- var import_node_fs7 = require("fs");
8643
- var import_node_path5 = require("path");
8680
+ var import_node_fs8 = require("fs");
8681
+ var import_node_path6 = require("path");
8644
8682
 
8645
8683
  // ../../node_modules/.pnpm/ora@8.2.0/node_modules/ora/index.js
8646
8684
  var import_node_process7 = __toESM(require("process"), 1);
@@ -9797,7 +9835,17 @@ var NODE_RULES = [
9797
9835
  // arrangement of nearby lines that makes an echoed origin safe — except a
9798
9836
  // membership test on the value, which is what the guard looks for.
9799
9837
  inherent: true,
9800
- guard: ORIGIN_ALLOWLIST_GUARD
9838
+ guard: ORIGIN_ALLOWLIST_GUARD,
9839
+ /**
9840
+ * Logging the origin is not reflecting it.
9841
+ *
9842
+ * `origin: request.headers.origin` is the CORS mistake *and* the ordinary
9843
+ * way to record who called — the two are character-for-character
9844
+ * identical, and only what encloses them differs. A response header goes
9845
+ * out to the browser; a log line goes to stdout, where it grants nobody
9846
+ * anything.
9847
+ */
9848
+ enclosingCallGuard: /(?:^|\.)(?:log|debug|info|warn|error|trace|verbose|fatal)$/
9801
9849
  },
9802
9850
  // ── Server-side request forgery ──────────────────────────────────────────
9803
9851
  {
@@ -10046,7 +10094,8 @@ var CODE_RULES = [
10046
10094
  cwe: "CWE-78",
10047
10095
  severity: "critical",
10048
10096
  languages: ["javascript", "typescript"],
10049
- pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/
10097
+ pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/,
10098
+ constantInterpolationGuard: true
10050
10099
  },
10051
10100
  {
10052
10101
  id: "py-shell-command-string",
@@ -10945,7 +10994,26 @@ var CODE_RULES = [
10945
10994
  cwe: "CWE-346",
10946
10995
  severity: "high",
10947
10996
  languages: ["javascript", "typescript"],
10948
- 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/
10997
+ 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/,
10998
+ /**
10999
+ * Parsing the request's own URL is not building a link from the Host
11000
+ * header, even though it is spelled with one.
11001
+ *
11002
+ * const url = new URL(request.url, `http://${request.headers.host}`);
11003
+ * const token = url.searchParams.get('token');
11004
+ *
11005
+ * `request.url` on a Node server is a path — `/ws?token=…` — and `new URL`
11006
+ * refuses a relative input without a base. The base exists to satisfy the
11007
+ * parser and is thrown away; only the path and query are ever read. Every
11008
+ * Node HTTP handler that wants a query parameter is written this way, so
11009
+ * the rule fired on the framework idiom rather than on the defect.
11010
+ *
11011
+ * Narrow on purpose: the first argument must be `req.url` itself. The
11012
+ * dangerous shape passes a *path* the application chose —
11013
+ * `new URL('/reset?t=…', `https://${req.headers.host}`)` — and that is what
11014
+ * produces an attacker-controlled link. It does not match this guard.
11015
+ */
11016
+ lineGuard: /\bnew\s+URL\s*\(\s*(?:req|request|ctx)(?:uest)?\s*\.\s*url\b\s*,/
10949
11017
  },
10950
11018
  // A recursive-merge prototype-pollution rule (`target[key] = source[key]`
10951
11019
  // with no `__proto__` guard) was built and dropped. The bare copy-by-key is
@@ -11013,6 +11081,55 @@ function fileTextOf(lines) {
11013
11081
  function withoutSingleQuoted(text) {
11014
11082
  return text.replace(/'[^'\n]*'/g, "''");
11015
11083
  }
11084
+ function calleeEndingAt(text, open) {
11085
+ let end = open;
11086
+ while (end > 0 && (text[end - 1] === " " || text[end - 1] === " ")) end -= 1;
11087
+ let start = end;
11088
+ while (start > 0 && /[\w$.]/.test(text[start - 1])) start -= 1;
11089
+ return text.slice(start, end);
11090
+ }
11091
+ function enclosingCallees(lines, index, back) {
11092
+ const before = lines.slice(Math.max(0, index - back), index).join("\n");
11093
+ const stack = [];
11094
+ let quote = null;
11095
+ for (let i = 0; i < before.length; i += 1) {
11096
+ const ch = before[i];
11097
+ if (quote) {
11098
+ if (ch === "\\") i += 1;
11099
+ else if (ch === quote) quote = null;
11100
+ continue;
11101
+ }
11102
+ if (ch === '"' || ch === "'" || ch === "`") quote = ch;
11103
+ else if (ch === "(") stack.push(calleeEndingAt(before, i));
11104
+ else if (ch === ")") stack.pop();
11105
+ }
11106
+ return stack;
11107
+ }
11108
+ function interpolations(line) {
11109
+ const found = [];
11110
+ for (let i = 0; ; ) {
11111
+ const start = line.indexOf("${", i);
11112
+ if (start === -1) break;
11113
+ const end = line.indexOf("}", start + 2);
11114
+ if (end === -1) return null;
11115
+ found.push(line.slice(start + 2, end).trim());
11116
+ i = end + 1;
11117
+ }
11118
+ return found.length > 0 ? found : null;
11119
+ }
11120
+ function isConstantString(name, fileText) {
11121
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
11122
+ return new RegExp(
11123
+ `\\bconst\\s+${escaped}\\s*(?::[^=\\n]+)?=\\s*(?:'[^'\\n]*'|"[^"\\n]*"|\`[^\`$\\n]*\`)`
11124
+ ).test(fileText);
11125
+ }
11126
+ function interpolationsAreConstant(line, fileText) {
11127
+ const found = interpolations(line);
11128
+ if (!found) return false;
11129
+ return found.every(
11130
+ (expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
11131
+ );
11132
+ }
11016
11133
  function evaluateRule(rule, ctx) {
11017
11134
  if (rule.languages && !rule.languages.includes(ctx.language)) return null;
11018
11135
  const line = ctx.lines[ctx.index] ?? "";
@@ -11024,6 +11141,13 @@ function evaluateRule(rule, ctx) {
11024
11141
  const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
11025
11142
  if (rule.requires && !rule.requires.test(context)) return null;
11026
11143
  if (rule.lineGuard?.test(line)) return null;
11144
+ if (rule.enclosingCallGuard) {
11145
+ const callees = enclosingCallees(ctx.lines, ctx.index, back);
11146
+ if (callees.some((callee) => rule.enclosingCallGuard.test(callee))) return null;
11147
+ }
11148
+ if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
11149
+ return null;
11150
+ }
11027
11151
  const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
11028
11152
  if (guard && (guard.test(line) || guard.test(context))) return null;
11029
11153
  const untrusted = untrustedPatternFor(ctx.language);
@@ -11606,7 +11730,8 @@ var SECRET_RULES = [
11606
11730
  pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
11607
11731
  severity: "medium",
11608
11732
  cwe: "CWE-798",
11609
- consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
11733
+ consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived.",
11734
+ keywordShaped: true
11610
11735
  },
11611
11736
  {
11612
11737
  id: "secret-generic-api-key",
@@ -11617,7 +11742,8 @@ var SECRET_RULES = [
11617
11742
  pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
11618
11743
  severity: "high",
11619
11744
  cwe: "CWE-798",
11620
- consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
11745
+ consequence: "Whatever the third-party service lets the key do, for as long as it stays valid.",
11746
+ keywordShaped: true
11621
11747
  },
11622
11748
  {
11623
11749
  id: "secret-generic-credential",
@@ -11625,7 +11751,8 @@ var SECRET_RULES = [
11625
11751
  pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
11626
11752
  severity: "high",
11627
11753
  cwe: "CWE-798",
11628
- consequence: "A password in source is a password in every clone, fork and CI cache of that source."
11754
+ consequence: "A password in source is a password in every clone, fork and CI cache of that source.",
11755
+ keywordShaped: true
11629
11756
  },
11630
11757
  {
11631
11758
  id: "secret-hex-token",
@@ -11633,7 +11760,8 @@ var SECRET_RULES = [
11633
11760
  pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
11634
11761
  severity: "medium",
11635
11762
  cwe: "CWE-798",
11636
- consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
11763
+ consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign.",
11764
+ keywordShaped: true
11637
11765
  }
11638
11766
  ];
11639
11767
  var KNOWN_PLACEHOLDERS = [
@@ -11664,6 +11792,54 @@ var KNOWN_PLACEHOLDERS = [
11664
11792
  function isKnownPlaceholder(text) {
11665
11793
  return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
11666
11794
  }
11795
+ function isVariableReference(value) {
11796
+ const trimmed = value.trim();
11797
+ const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
11798
+ if (braced) {
11799
+ const fallback2 = braced[2];
11800
+ if (fallback2 === void 0 || fallback2.trim() === "") return true;
11801
+ return /^\$\{?[A-Za-z_][\w.]*\}?$/.test(fallback2.trim());
11802
+ }
11803
+ return /^\$[A-Za-z_]\w*$/.test(trimmed) || // $VAR
11804
+ /^\$\([\s\S]*\)$/.test(trimmed) || // $(command substitution)
11805
+ /^%[A-Za-z_]\w*%$/.test(trimmed) || // %VAR% on Windows
11806
+ /^\{\{[\s\S]*\}\}$/.test(trimmed) || // {{ template }}
11807
+ /^#\{[\s\S]*\}$/.test(trimmed) || // #{ruby}
11808
+ /^<%=?[\s\S]*%>$/.test(trimmed);
11809
+ }
11810
+ var FIXTURE_STEMS = [
11811
+ "test",
11812
+ "mock",
11813
+ "fake",
11814
+ "dummy",
11815
+ "stub",
11816
+ "sample",
11817
+ "example",
11818
+ "placeholder",
11819
+ "fixture",
11820
+ "invalid",
11821
+ "expired",
11822
+ "forged",
11823
+ "bogus",
11824
+ "notreal",
11825
+ "nonexistent",
11826
+ "changeme",
11827
+ "foobar",
11828
+ "lorem"
11829
+ ];
11830
+ var KEY_NOISE = /* @__PURE__ */ new Set(["const", "this", "return", "await", "async", "expect", "value"]);
11831
+ function words(text) {
11832
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z]+/g, " ").toLowerCase().split(" ").filter(Boolean);
11833
+ }
11834
+ function isTestFixtureValue(line, value) {
11835
+ const valueWords = words(value);
11836
+ if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
11837
+ if (value.length > 48) return false;
11838
+ const valueAt = line.lastIndexOf(value);
11839
+ const key = valueAt === -1 ? line : line.slice(0, valueAt);
11840
+ const flattened = valueWords.join("");
11841
+ return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
11842
+ }
11667
11843
  function redactSecret(line) {
11668
11844
  return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
11669
11845
  if (match.length <= 12) return match;
@@ -11882,6 +12058,9 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
11882
12058
  if (!match) continue;
11883
12059
  if (isKnownPlaceholder(match[0])) continue;
11884
12060
  if (isSuppressed(suppressions, index, rule.id)) continue;
12061
+ const value = match[1] ?? match[0];
12062
+ if (isVariableReference(value)) continue;
12063
+ if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
11885
12064
  const marked = foreignCredentialMark(line);
11886
12065
  findings.push({
11887
12066
  ruleId: rule.id,
@@ -11965,8 +12144,8 @@ function meetsFailThreshold(findings, threshold) {
11965
12144
  }
11966
12145
 
11967
12146
  // ../../packages/scan/src/node/walk.ts
11968
- var import_node_fs5 = require("fs");
11969
- var import_node_path2 = require("path");
12147
+ var import_node_fs6 = require("fs");
12148
+ var import_node_path3 = require("path");
11970
12149
  function compileExcludes(patterns) {
11971
12150
  const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
11972
12151
  if (matchers.length === 0) return () => false;
@@ -12037,7 +12216,7 @@ function matchPrefix(parts, segs) {
12037
12216
  }
12038
12217
  function readIgnoreFile(root) {
12039
12218
  try {
12040
- return (0, import_node_fs5.readFileSync)((0, import_node_path2.join)(root, ".threatcrushignore"), "utf-8").split("\n");
12219
+ return (0, import_node_fs6.readFileSync)((0, import_node_path3.join)(root, ".threatcrushignore"), "utf-8").split("\n");
12041
12220
  } catch {
12042
12221
  return [];
12043
12222
  }
@@ -12053,16 +12232,16 @@ function scanPath(targetPath, options = {}) {
12053
12232
  let excluded = 0;
12054
12233
  const rootIsDirectory = (() => {
12055
12234
  try {
12056
- return (0, import_node_fs5.statSync)(targetPath).isDirectory();
12235
+ return (0, import_node_fs6.statSync)(targetPath).isDirectory();
12057
12236
  } catch {
12058
12237
  return true;
12059
12238
  }
12060
12239
  })();
12061
- const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path2.dirname)(targetPath);
12240
+ const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path3.dirname)(targetPath);
12062
12241
  const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
12063
12242
  const scanFile = (fullPath, filename) => {
12064
12243
  const relativePath = toRelative(walkRoot, fullPath);
12065
- const extension = (0, import_node_path2.extname)(filename).toLowerCase();
12244
+ const extension = (0, import_node_path3.extname)(filename).toLowerCase();
12066
12245
  const isManifest = filename === "package.json" || filename === "requirements.txt";
12067
12246
  const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
12068
12247
  const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
@@ -12074,26 +12253,26 @@ function scanPath(targetPath, options = {}) {
12074
12253
  let handle;
12075
12254
  let declared = null;
12076
12255
  try {
12077
- handle = (0, import_node_fs5.openSync)(fullPath, "r");
12256
+ handle = (0, import_node_fs6.openSync)(fullPath, "r");
12078
12257
  } catch {
12079
12258
  unreadable.push(relativePath);
12080
12259
  return;
12081
12260
  }
12082
12261
  try {
12083
- if ((0, import_node_fs5.fstatSync)(handle).size > maxFileBytes) return;
12262
+ if ((0, import_node_fs6.fstatSync)(handle).size > maxFileBytes) return;
12084
12263
  if (mayDeclareInterpreter) {
12085
12264
  const prefix = Buffer.alloc(128);
12086
- const read = (0, import_node_fs5.readSync)(handle, prefix, 0, prefix.length, 0);
12265
+ const read = (0, import_node_fs6.readSync)(handle, prefix, 0, prefix.length, 0);
12087
12266
  declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
12088
12267
  if (!declared) return;
12089
12268
  }
12090
- text = (0, import_node_fs5.readFileSync)(handle, "utf-8");
12269
+ text = (0, import_node_fs6.readFileSync)(handle, "utf-8");
12091
12270
  } catch {
12092
12271
  unreadable.push(relativePath);
12093
12272
  return;
12094
12273
  } finally {
12095
12274
  try {
12096
- (0, import_node_fs5.closeSync)(handle);
12275
+ (0, import_node_fs6.closeSync)(handle);
12097
12276
  } catch {
12098
12277
  }
12099
12278
  }
@@ -12111,13 +12290,13 @@ function scanPath(targetPath, options = {}) {
12111
12290
  const walk = (currentPath) => {
12112
12291
  let entries;
12113
12292
  try {
12114
- entries = (0, import_node_fs5.readdirSync)(currentPath, { withFileTypes: true });
12293
+ entries = (0, import_node_fs6.readdirSync)(currentPath, { withFileTypes: true });
12115
12294
  } catch {
12116
12295
  unreadable.push(toRelative(walkRoot, currentPath));
12117
12296
  return;
12118
12297
  }
12119
12298
  for (const entry of entries) {
12120
- const fullPath = (0, import_node_path2.join)(currentPath, entry.name);
12299
+ const fullPath = (0, import_node_path3.join)(currentPath, entry.name);
12121
12300
  const relativePath = toRelative(walkRoot, fullPath);
12122
12301
  if (entry.isDirectory()) {
12123
12302
  if (SKIP_DIRS.has(entry.name)) continue;
@@ -12141,7 +12320,7 @@ function scanPath(targetPath, options = {}) {
12141
12320
  } else if (isExcluded(toRelative(walkRoot, targetPath))) {
12142
12321
  excluded += 1;
12143
12322
  } else {
12144
- scanFile(targetPath, (0, import_node_path2.basename)(targetPath));
12323
+ scanFile(targetPath, (0, import_node_path3.basename)(targetPath));
12145
12324
  }
12146
12325
  if (options.missingControls) findings.push(...controls.findings());
12147
12326
  const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
@@ -12173,32 +12352,37 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
12173
12352
  }
12174
12353
  }
12175
12354
  function toRelative(base, target) {
12176
- const rel = (0, import_node_path2.relative)(base, target);
12177
- return (rel === "" ? target : rel).split(import_node_path2.sep).join("/");
12355
+ const rel = (0, import_node_path3.relative)(base, target);
12356
+ return (rel === "" ? target : rel).split(import_node_path3.sep).join("/");
12178
12357
  }
12179
12358
 
12180
12359
  // ../../packages/scan/src/node/dependencies.ts
12181
- var import_node_fs6 = require("fs");
12182
- var import_node_path3 = require("path");
12183
- var LOCKFILES = [
12184
- { file: "package-lock.json", ecosystem: "npm" },
12185
- { file: "pnpm-lock.yaml", ecosystem: "npm" },
12186
- { file: "yarn.lock", ecosystem: "npm" },
12187
- { file: "requirements.txt", ecosystem: "PyPI" },
12188
- { file: "Pipfile.lock", ecosystem: "PyPI" }
12189
- ];
12360
+ var import_node_fs7 = require("fs");
12361
+ var import_node_path4 = require("path");
12190
12362
  var MAX_DEPS_PER_LOCKFILE = 50;
12191
12363
  async function scanDependencies(targetPath) {
12192
12364
  const findings = [];
12193
- for (const { file, ecosystem } of LOCKFILES) {
12194
- const lockPath = (0, import_node_path3.join)(targetPath, file);
12195
- if (!(0, import_node_fs6.existsSync)(lockPath)) continue;
12365
+ for (const { file, ecosystem, parse } of LOCKFILES) {
12366
+ const lockPath = (0, import_node_path4.join)(targetPath, file);
12367
+ if (!(0, import_node_fs7.existsSync)(lockPath)) continue;
12196
12368
  let deps;
12197
12369
  try {
12198
- deps = parseDependencies(lockPath, file);
12370
+ deps = dedupe(parse((0, import_node_fs7.readFileSync)(lockPath, "utf-8")));
12199
12371
  } catch {
12200
12372
  continue;
12201
12373
  }
12374
+ if (deps.length === 0) {
12375
+ findings.push(incompleteFinding(file, "No dependencies could be read from this lockfile."));
12376
+ continue;
12377
+ }
12378
+ if (deps.length > MAX_DEPS_PER_LOCKFILE) {
12379
+ findings.push(
12380
+ incompleteFinding(
12381
+ file,
12382
+ `Only the first ${MAX_DEPS_PER_LOCKFILE} of ${deps.length} locked packages were checked against OSV.`
12383
+ )
12384
+ );
12385
+ }
12202
12386
  for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
12203
12387
  let vulns;
12204
12388
  try {
@@ -12225,6 +12409,20 @@ async function scanDependencies(targetPath) {
12225
12409
  }
12226
12410
  return findings;
12227
12411
  }
12412
+ function incompleteFinding(file, message) {
12413
+ return {
12414
+ ruleId: "dependency-scan-incomplete",
12415
+ title: "Dependency scan incomplete",
12416
+ file,
12417
+ line: 1,
12418
+ severity: "low",
12419
+ confidence: "evidence",
12420
+ message,
12421
+ consequence: "Advisories affecting the unchecked packages would not appear in this report.",
12422
+ excerpt: file,
12423
+ category: "dependency"
12424
+ };
12425
+ }
12228
12426
  function severityFromCvss(score) {
12229
12427
  if (!score) return "medium";
12230
12428
  const value = Number.parseFloat(score);
@@ -12234,26 +12432,131 @@ function severityFromCvss(score) {
12234
12432
  if (value >= 4) return "medium";
12235
12433
  return "low";
12236
12434
  }
12237
- function parseDependencies(lockPath, filename) {
12435
+ function dedupe(deps) {
12436
+ const seen = /* @__PURE__ */ new Set();
12437
+ const unique = [];
12438
+ for (const dep of deps) {
12439
+ const key = `${dep.name}@${dep.version}`;
12440
+ if (seen.has(key)) continue;
12441
+ seen.add(key);
12442
+ unique.push(dep);
12443
+ }
12444
+ return unique;
12445
+ }
12446
+ function splitNameVersion(spec) {
12447
+ const at = spec.lastIndexOf("@");
12448
+ if (at <= 0) return null;
12449
+ const name = spec.slice(0, at);
12450
+ const version = spec.slice(at + 1);
12451
+ if (!name || !version) return null;
12452
+ return { name, version };
12453
+ }
12454
+ function exactVersion(raw) {
12455
+ const version = raw.trim().replace(/^[=v]+/, "");
12456
+ return /^[0-9][0-9a-zA-Z.+-]*$/.test(version) ? version : null;
12457
+ }
12458
+ function parsePackageLock(content) {
12459
+ const lock = JSON.parse(content);
12460
+ const packages = lock.packages ?? lock.dependencies ?? {};
12461
+ const deps = [];
12462
+ for (const [key, value] of Object.entries(packages)) {
12463
+ const name = key.replace(/^.*node_modules\//, "");
12464
+ const version = value?.version;
12465
+ if (name && version && !name.startsWith(".")) deps.push({ name, version });
12466
+ }
12467
+ return deps;
12468
+ }
12469
+ function parsePnpmLock(content) {
12470
+ const deps = [];
12471
+ let inPackages = false;
12472
+ for (const line of content.split("\n")) {
12473
+ if (/^[a-zA-Z]/.test(line)) {
12474
+ inPackages = line.startsWith("packages:");
12475
+ continue;
12476
+ }
12477
+ if (!inPackages) continue;
12478
+ const match = /^ {2}(?! )(.+):\s*$/.exec(line);
12479
+ if (!match?.[1]) continue;
12480
+ let key = match[1].trim().replace(/^['"]|['"]$/g, "");
12481
+ key = key.replace(/^\//, "");
12482
+ key = key.replace(/\(.*$/, "");
12483
+ let name;
12484
+ let rawVersion;
12485
+ const slashed = /^(@?[^@]+)\/([0-9][^/]*)$/.exec(key);
12486
+ if (slashed?.[1] && slashed[2]) {
12487
+ name = slashed[1];
12488
+ rawVersion = slashed[2];
12489
+ } else {
12490
+ const dep = splitNameVersion(key);
12491
+ if (!dep) continue;
12492
+ name = dep.name;
12493
+ rawVersion = dep.version;
12494
+ }
12495
+ const version = exactVersion(rawVersion.replace(/_.*$/, ""));
12496
+ if (version) deps.push({ name, version });
12497
+ }
12498
+ return deps;
12499
+ }
12500
+ function parseYarnLock(content) {
12238
12501
  const deps = [];
12239
- if (filename === "package-lock.json") {
12240
- const lock = JSON.parse((0, import_node_fs6.readFileSync)(lockPath, "utf-8"));
12241
- const packages = lock.packages ?? lock.dependencies ?? {};
12242
- for (const [key, value] of Object.entries(packages)) {
12243
- const name = key.replace(/^node_modules\//, "");
12244
- const version = value?.version;
12245
- if (name && version && !name.startsWith(".")) deps.push({ name, version });
12502
+ let pendingName = null;
12503
+ for (const line of content.split("\n")) {
12504
+ if (line.startsWith("#") || line.trim() === "") continue;
12505
+ if (!/^\s/.test(line)) {
12506
+ pendingName = null;
12507
+ const header = line.replace(/:\s*$/, "");
12508
+ const first = header.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
12509
+ if (!first) continue;
12510
+ if (!first.includes("@") || first === "__metadata") continue;
12511
+ if (/@(?:workspace|file|link|portal|exec|patch):/.test(first)) continue;
12512
+ const dep = splitNameVersion(first.replace(/@npm:/, "@"));
12513
+ if (dep) pendingName = dep.name;
12514
+ continue;
12246
12515
  }
12247
- return deps;
12516
+ if (!pendingName) continue;
12517
+ const version = /^\s+version:?\s+["']?([^"'\s]+)["']?\s*$/.exec(line);
12518
+ if (!version?.[1]) continue;
12519
+ const exact = exactVersion(version[1]);
12520
+ if (exact) deps.push({ name: pendingName, version: exact });
12521
+ pendingName = null;
12248
12522
  }
12249
- if (filename === "requirements.txt") {
12250
- for (const line of (0, import_node_fs6.readFileSync)(lockPath, "utf-8").split("\n")) {
12251
- const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
12252
- if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
12523
+ return deps;
12524
+ }
12525
+ function parsePipfileLock(content) {
12526
+ const lock = JSON.parse(content);
12527
+ const deps = [];
12528
+ for (const section of ["default", "develop"]) {
12529
+ const packages = lock[section];
12530
+ if (!packages || typeof packages !== "object") continue;
12531
+ for (const [name, value] of Object.entries(packages)) {
12532
+ const version = exactVersion(String(value?.version ?? "").replace(/^==/, ""));
12533
+ if (name && version) deps.push({ name, version });
12253
12534
  }
12254
12535
  }
12255
12536
  return deps;
12256
12537
  }
12538
+ function parseRequirementsTxt(content) {
12539
+ const deps = [];
12540
+ for (const raw of content.split("\n")) {
12541
+ const line = raw.split("#")[0]?.split(";")[0]?.trim();
12542
+ if (!line || line.startsWith("-")) continue;
12543
+ const match = /^([a-zA-Z0-9._-]+)\s*(?:\[[^\]]*\])?\s*==\s*([^\s,]+)/.exec(line);
12544
+ if (!match?.[1] || !match[2]) continue;
12545
+ const version = exactVersion(match[2]);
12546
+ if (version) deps.push({ name: match[1], version });
12547
+ }
12548
+ return deps;
12549
+ }
12550
+ var LOCKFILES = [
12551
+ { file: "package-lock.json", ecosystem: "npm", parse: parsePackageLock },
12552
+ { file: "pnpm-lock.yaml", ecosystem: "npm", parse: parsePnpmLock },
12553
+ { file: "yarn.lock", ecosystem: "npm", parse: parseYarnLock },
12554
+ { file: "requirements.txt", ecosystem: "PyPI", parse: parseRequirementsTxt },
12555
+ { file: "Pipfile.lock", ecosystem: "PyPI", parse: parsePipfileLock }
12556
+ ];
12557
+ var LOCKFILE_PARSERS = Object.fromEntries(
12558
+ LOCKFILES.map((entry) => [entry.file, entry.parse])
12559
+ );
12257
12560
  function isValidPackageName(name) {
12258
12561
  return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
12259
12562
  }
@@ -12278,12 +12581,12 @@ async function queryOsv(name, version, ecosystem) {
12278
12581
  }
12279
12582
 
12280
12583
  // ../../packages/scan/src/node/sarif.ts
12281
- var import_node_crypto = require("crypto");
12282
- var import_node_path4 = require("path");
12584
+ var import_node_crypto2 = require("crypto");
12585
+ var import_node_path5 = require("path");
12283
12586
  var FINGERPRINT_KEY = "threatcrush/contentHash/v1";
12284
12587
  function fingerprintOf(finding) {
12285
12588
  const content = finding.excerpt.replace(/\s+/g, " ").trim();
12286
- return (0, import_node_crypto.createHash)("sha256").update(`${finding.ruleId}
12589
+ return (0, import_node_crypto2.createHash)("sha256").update(`${finding.ruleId}
12287
12590
  ${finding.file}
12288
12591
  ${content}`).digest("hex").slice(0, 32);
12289
12592
  }
@@ -12317,11 +12620,11 @@ function securitySeverity(severity) {
12317
12620
  }
12318
12621
  }
12319
12622
  function toArtifactUri(filePath, base, prefix = "", root = base) {
12320
- const absolute = (0, import_node_path4.isAbsolute)(filePath) ? filePath : (0, import_node_path4.resolve)(root, filePath);
12321
- const relativePath = (0, import_node_path4.relative)(base, absolute);
12623
+ const absolute = (0, import_node_path5.isAbsolute)(filePath) ? filePath : (0, import_node_path5.resolve)(root, filePath);
12624
+ const relativePath = (0, import_node_path5.relative)(base, absolute);
12322
12625
  const escapedOut = relativePath.startsWith("..") || relativePath === "";
12323
12626
  const chosen = escapedOut ? absolute : relativePath;
12324
- const posix = chosen.split(import_node_path4.sep).join("/").replace(/^\.\//, "");
12627
+ const posix = chosen.split(import_node_path5.sep).join("/").replace(/^\.\//, "");
12325
12628
  if (!prefix || escapedOut) return posix;
12326
12629
  const trimmed = prefix.replace(/^\/+|\/+$/g, "");
12327
12630
  return trimmed ? `${trimmed}/${posix}` : posix;
@@ -12411,11 +12714,11 @@ ${finding.consequence}` : `**${finding.title}**`
12411
12714
  // src/commands/scan.ts
12412
12715
  function readVersion() {
12413
12716
  for (const candidate of [
12414
- (0, import_node_path5.join)(__dirname, "..", "package.json"),
12415
- (0, import_node_path5.join)(__dirname, "..", "..", "package.json")
12717
+ (0, import_node_path6.join)(__dirname, "..", "package.json"),
12718
+ (0, import_node_path6.join)(__dirname, "..", "..", "package.json")
12416
12719
  ]) {
12417
12720
  try {
12418
- return JSON.parse((0, import_node_fs7.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
12721
+ return JSON.parse((0, import_node_fs8.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
12419
12722
  } catch {
12420
12723
  }
12421
12724
  }
@@ -12482,7 +12785,7 @@ async function scanCommand(targetPath, options = {}) {
12482
12785
  const say = machineReadable ? (line) => process.stderr.write(`${line}
12483
12786
  `) : (line) => process.stdout.write(`${line}
12484
12787
  `);
12485
- if (!(0, import_node_fs7.existsSync)(targetPath)) {
12788
+ if (!(0, import_node_fs8.existsSync)(targetPath)) {
12486
12789
  say(source_default.red(`Scan target does not exist: ${targetPath}`));
12487
12790
  process.exitCode = 2;
12488
12791
  return failedResult(targetPath, `no such path: ${targetPath}`);
@@ -12578,7 +12881,7 @@ function emitMachineReadable(format, outcome, targetPath, options, say) {
12578
12881
  // and it fails silently. `--path-prefix` covers the remaining case:
12579
12882
  // a scan run from inside the subdirectory it is scanning.
12580
12883
  base: process.cwd(),
12581
- root: (0, import_node_path5.resolve)(outcome.root)
12884
+ root: (0, import_node_path6.resolve)(outcome.root)
12582
12885
  }) : {
12583
12886
  tool: "threatcrush",
12584
12887
  version: PKG_VERSION,
@@ -12592,8 +12895,8 @@ function emitMachineReadable(format, outcome, targetPath, options, say) {
12592
12895
  const serialized = `${JSON.stringify(payload, null, 2)}
12593
12896
  `;
12594
12897
  if (options.output) {
12595
- (0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)((0, import_node_path5.resolve)(options.output)), { recursive: true });
12596
- (0, import_node_fs7.writeFileSync)(options.output, serialized, "utf-8");
12898
+ (0, import_node_fs8.mkdirSync)((0, import_node_path6.dirname)((0, import_node_path6.resolve)(options.output)), { recursive: true });
12899
+ (0, import_node_fs8.writeFileSync)(options.output, serialized, "utf-8");
12597
12900
  say(
12598
12901
  source_default.gray(
12599
12902
  ` ${format.toUpperCase()} written to ${options.output} (${outcome.findings.length} finding(s))`
@@ -12645,13 +12948,13 @@ function printHuman(outcome) {
12645
12948
  }
12646
12949
 
12647
12950
  // src/commands/init.ts
12648
- var import_node_fs10 = require("fs");
12951
+ var import_node_fs11 = require("fs");
12649
12952
  var import_node_child_process = require("child_process");
12650
12953
  var import_node_readline3 = __toESM(require("readline"));
12651
12954
 
12652
12955
  // src/core/config.ts
12653
- var import_node_fs8 = require("fs");
12654
- var import_node_path6 = require("path");
12956
+ var import_node_fs9 = require("fs");
12957
+ var import_node_path7 = require("path");
12655
12958
  var import_toml = __toESM(require_toml());
12656
12959
  var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
12657
12960
  var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
@@ -12677,11 +12980,11 @@ var DEFAULT_CONFIG = {
12677
12980
  };
12678
12981
  function loadConfig(configPath) {
12679
12982
  const path = configPath || DEFAULT_CONFIG_PATH;
12680
- if (!(0, import_node_fs8.existsSync)(path)) {
12983
+ if (!(0, import_node_fs9.existsSync)(path)) {
12681
12984
  return { ...DEFAULT_CONFIG };
12682
12985
  }
12683
12986
  try {
12684
- const raw = (0, import_node_fs8.readFileSync)(path, "utf-8");
12987
+ const raw = (0, import_node_fs9.readFileSync)(path, "utf-8");
12685
12988
  const parsed = import_toml.default.parse(raw);
12686
12989
  return {
12687
12990
  daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
@@ -12697,13 +13000,13 @@ function loadConfig(configPath) {
12697
13000
  function loadModuleConfigs(confDir) {
12698
13001
  const dir = confDir || DEFAULT_CONFDIR;
12699
13002
  const configs = /* @__PURE__ */ new Map();
12700
- if (!(0, import_node_fs8.existsSync)(dir)) {
13003
+ if (!(0, import_node_fs9.existsSync)(dir)) {
12701
13004
  return configs;
12702
13005
  }
12703
- const files = (0, import_node_fs8.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
13006
+ const files = (0, import_node_fs9.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
12704
13007
  for (const file of files) {
12705
13008
  try {
12706
- const raw = (0, import_node_fs8.readFileSync)((0, import_node_path6.join)(dir, file), "utf-8");
13009
+ const raw = (0, import_node_fs9.readFileSync)((0, import_node_path7.join)(dir, file), "utf-8");
12707
13010
  const parsed = import_toml.default.parse(raw);
12708
13011
  for (const [name, config] of Object.entries(parsed)) {
12709
13012
  configs.set(name, config);
@@ -12732,23 +13035,23 @@ function generateModuleConfig(moduleName, defaults = {}) {
12732
13035
  }
12733
13036
 
12734
13037
  // src/core/cli-config.ts
12735
- var import_node_fs9 = require("fs");
12736
- var import_node_path7 = require("path");
13038
+ var import_node_fs10 = require("fs");
13039
+ var import_node_path8 = require("path");
12737
13040
  var import_node_os4 = require("os");
12738
- var CLI_CONFIG_DIR = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".threatcrush");
12739
- var CLI_CONFIG_PATH = (0, import_node_path7.join)(CLI_CONFIG_DIR, "config.json");
13041
+ var CLI_CONFIG_DIR = (0, import_node_path8.join)((0, import_node_os4.homedir)(), ".threatcrush");
13042
+ var CLI_CONFIG_PATH = (0, import_node_path8.join)(CLI_CONFIG_DIR, "config.json");
12740
13043
  function readCliConfig() {
12741
13044
  try {
12742
- return JSON.parse((0, import_node_fs9.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
13045
+ return JSON.parse((0, import_node_fs10.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
12743
13046
  } catch {
12744
13047
  return {};
12745
13048
  }
12746
13049
  }
12747
13050
  function writeCliConfig(config) {
12748
- if (!(0, import_node_fs9.existsSync)(CLI_CONFIG_DIR)) (0, import_node_fs9.mkdirSync)(CLI_CONFIG_DIR, { recursive: true });
12749
- (0, import_node_fs9.writeFileSync)(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
13051
+ if (!(0, import_node_fs10.existsSync)(CLI_CONFIG_DIR)) (0, import_node_fs10.mkdirSync)(CLI_CONFIG_DIR, { recursive: true });
13052
+ (0, import_node_fs10.writeFileSync)(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
12750
13053
  try {
12751
- (0, import_node_fs9.chmodSync)(CLI_CONFIG_PATH, 384);
13054
+ (0, import_node_fs10.chmodSync)(CLI_CONFIG_PATH, 384);
12752
13055
  } catch {
12753
13056
  }
12754
13057
  }
@@ -12993,7 +13296,7 @@ function binaryExists(name) {
12993
13296
  }
12994
13297
  }
12995
13298
  function findLogPath(paths) {
12996
- return paths.find((p) => (0, import_node_fs10.existsSync)(p));
13299
+ return paths.find((p) => (0, import_node_fs11.existsSync)(p));
12997
13300
  }
12998
13301
  async function promptYesNo(question, fallback2) {
12999
13302
  const rl = import_node_readline3.default.createInterface({ input: process.stdin, output: process.stdout });
@@ -13109,11 +13412,11 @@ async function initCommand() {
13109
13412
  }
13110
13413
  } else {
13111
13414
  const spinner2 = ora({ text: "Writing configuration files...", color: "green" }).start();
13112
- (0, import_node_fs10.mkdirSync)(confDDir, { recursive: true });
13113
- (0, import_node_fs10.mkdirSync)("/var/log/threatcrush", { recursive: true });
13114
- (0, import_node_fs10.mkdirSync)("/var/lib/threatcrush", { recursive: true });
13415
+ (0, import_node_fs11.mkdirSync)(confDDir, { recursive: true });
13416
+ (0, import_node_fs11.mkdirSync)("/var/log/threatcrush", { recursive: true });
13417
+ (0, import_node_fs11.mkdirSync)("/var/lib/threatcrush", { recursive: true });
13115
13418
  const mainConfig = generateDefaultConfig(detected.map((d) => d.name));
13116
- (0, import_node_fs10.writeFileSync)(`${configDir}/threatcrushd.conf`, mainConfig);
13419
+ (0, import_node_fs11.writeFileSync)(`${configDir}/threatcrushd.conf`, mainConfig);
13117
13420
  for (const svc of detected) {
13118
13421
  const svcDef = SERVICES_TO_DETECT.find((s) => s.name === svc.name);
13119
13422
  if (!svcDef) continue;
@@ -13122,7 +13425,7 @@ async function initCommand() {
13122
13425
  ...svcDef.moduleConfig,
13123
13426
  log_path: svc.logPath || svcDef.logPaths[0]
13124
13427
  });
13125
- (0, import_node_fs10.writeFileSync)(`${confDDir}/${modName}.conf`, modConfig);
13428
+ (0, import_node_fs11.writeFileSync)(`${confDDir}/${modName}.conf`, modConfig);
13126
13429
  }
13127
13430
  spinner2.succeed("Configuration written successfully");
13128
13431
  console.log();
@@ -13140,8 +13443,8 @@ async function initCommand() {
13140
13443
  }
13141
13444
  function checkWriteAccess(dir) {
13142
13445
  try {
13143
- if (!(0, import_node_fs10.existsSync)(dir)) {
13144
- (0, import_node_fs10.mkdirSync)(dir, { recursive: true });
13446
+ if (!(0, import_node_fs11.existsSync)(dir)) {
13447
+ (0, import_node_fs11.mkdirSync)(dir, { recursive: true });
13145
13448
  }
13146
13449
  return true;
13147
13450
  } catch {
@@ -13150,8 +13453,8 @@ function checkWriteAccess(dir) {
13150
13453
  }
13151
13454
 
13152
13455
  // src/core/module-loader.ts
13153
- var import_node_fs11 = require("fs");
13154
- var import_node_path8 = require("path");
13456
+ var import_node_fs12 = require("fs");
13457
+ var import_node_path9 = require("path");
13155
13458
  var import_toml2 = __toESM(require_toml());
13156
13459
  init_paths();
13157
13460
  function discoverModules(moduleDir, confDir) {
@@ -13159,22 +13462,22 @@ function discoverModules(moduleDir, confDir) {
13159
13462
  const configs = loadModuleConfigs(confDir || PATHS.confD);
13160
13463
  const searchPaths = [
13161
13464
  moduleDir || PATHS.moduleDir,
13162
- (0, import_node_path8.resolve)(process.cwd(), "modules")
13465
+ (0, import_node_path9.resolve)(process.cwd(), "modules")
13163
13466
  ];
13164
- const builtinDir = (0, import_node_path8.resolve)(__dirname || ".", "..", "modules");
13165
- if ((0, import_node_fs11.existsSync)(builtinDir)) {
13467
+ const builtinDir = (0, import_node_path9.resolve)(__dirname || ".", "..", "modules");
13468
+ if ((0, import_node_fs12.existsSync)(builtinDir)) {
13166
13469
  searchPaths.push(builtinDir);
13167
13470
  }
13168
13471
  for (const basePath of searchPaths) {
13169
- if (!(0, import_node_fs11.existsSync)(basePath)) continue;
13170
- const entries = (0, import_node_fs11.readdirSync)(basePath, { withFileTypes: true });
13472
+ if (!(0, import_node_fs12.existsSync)(basePath)) continue;
13473
+ const entries = (0, import_node_fs12.readdirSync)(basePath, { withFileTypes: true });
13171
13474
  for (const entry of entries) {
13172
13475
  if (!entry.isDirectory()) continue;
13173
- const modPath = (0, import_node_path8.join)(basePath, entry.name);
13174
- const manifestPath = (0, import_node_path8.join)(modPath, "mod.toml");
13175
- if (!(0, import_node_fs11.existsSync)(manifestPath)) continue;
13476
+ const modPath = (0, import_node_path9.join)(basePath, entry.name);
13477
+ const manifestPath = (0, import_node_path9.join)(modPath, "mod.toml");
13478
+ if (!(0, import_node_fs12.existsSync)(manifestPath)) continue;
13176
13479
  try {
13177
- const raw = (0, import_node_fs11.readFileSync)(manifestPath, "utf-8");
13480
+ const raw = (0, import_node_fs12.readFileSync)(manifestPath, "utf-8");
13178
13481
  const manifest = import_toml2.default.parse(raw);
13179
13482
  const config = configs.get(manifest.module.name) || { enabled: true };
13180
13483
  modules.push({
@@ -13293,11 +13596,163 @@ function formatUptime(seconds) {
13293
13596
 
13294
13597
  // src/commands/modules.ts
13295
13598
  var import_node_child_process2 = require("child_process");
13296
- var import_node_fs12 = require("fs");
13297
- var import_node_path9 = require("path");
13599
+ var import_node_fs14 = require("fs");
13600
+ var import_node_path11 = require("path");
13298
13601
  var import_toml3 = __toESM(require_toml());
13299
13602
  init_paths();
13300
13603
  init_pidfile();
13604
+
13605
+ // src/daemon/module-trust.ts
13606
+ var import_node_crypto3 = require("crypto");
13607
+ var import_node_fs13 = require("fs");
13608
+ var import_node_path10 = require("path");
13609
+ init_paths();
13610
+ var TRUST_FILE = (0, import_node_path10.join)(PATHS.configDir, "trusted-modules.json");
13611
+ var PUBLISHER_KEYS_FILE = (0, import_node_path10.join)(PATHS.configDir, "publisher-keys.json");
13612
+ var DIGEST_EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git"]);
13613
+ function collectFiles(root, dir = root, out = []) {
13614
+ for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).sort(
13615
+ (a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
13616
+ )) {
13617
+ const full = (0, import_node_path10.join)(dir, entry.name);
13618
+ if (entry.isSymbolicLink()) {
13619
+ out.push({ path: full, isSymlink: true });
13620
+ } else if (entry.isDirectory()) {
13621
+ if (DIGEST_EXCLUDED_DIRS.has(entry.name)) continue;
13622
+ collectFiles(root, full, out);
13623
+ } else if (entry.isFile()) {
13624
+ out.push({ path: full, isSymlink: false });
13625
+ }
13626
+ }
13627
+ return out;
13628
+ }
13629
+ function computeModuleDigest(modulePath) {
13630
+ const hash = (0, import_node_crypto3.createHash)("sha256");
13631
+ for (const entry of collectFiles(modulePath)) {
13632
+ const relPath = (0, import_node_path10.relative)(modulePath, entry.path).split(import_node_path10.sep).join("/");
13633
+ hash.update(relPath);
13634
+ hash.update("\0");
13635
+ if (entry.isSymlink) {
13636
+ hash.update("symlink:");
13637
+ hash.update((0, import_node_fs13.readlinkSync)(entry.path));
13638
+ } else {
13639
+ hash.update("file:");
13640
+ hash.update((0, import_node_fs13.readFileSync)(entry.path));
13641
+ }
13642
+ hash.update("\0");
13643
+ }
13644
+ return hash.digest("hex");
13645
+ }
13646
+ function readTrustFile() {
13647
+ if (!(0, import_node_fs13.existsSync)(TRUST_FILE)) return { version: 1, modules: {} };
13648
+ try {
13649
+ const parsed = JSON.parse((0, import_node_fs13.readFileSync)(TRUST_FILE, "utf-8"));
13650
+ if (parsed.version !== 1 || typeof parsed.modules !== "object" || !parsed.modules) {
13651
+ return { version: 1, modules: {} };
13652
+ }
13653
+ return { version: 1, modules: parsed.modules };
13654
+ } catch {
13655
+ return { version: 1, modules: {} };
13656
+ }
13657
+ }
13658
+ function writeTrustFile(file) {
13659
+ (0, import_node_fs13.writeFileSync)(TRUST_FILE, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
13660
+ try {
13661
+ (0, import_node_fs13.chmodSync)(TRUST_FILE, 384);
13662
+ } catch {
13663
+ }
13664
+ }
13665
+ function trustModule(name, modulePath, source) {
13666
+ const file = readTrustFile();
13667
+ const record = {
13668
+ digest: computeModuleDigest(modulePath),
13669
+ trustedAt: (/* @__PURE__ */ new Date()).toISOString(),
13670
+ source
13671
+ };
13672
+ file.modules[name] = record;
13673
+ writeTrustFile(file);
13674
+ return record;
13675
+ }
13676
+ function untrustModule(name) {
13677
+ const file = readTrustFile();
13678
+ if (!file.modules[name]) return false;
13679
+ delete file.modules[name];
13680
+ writeTrustFile(file);
13681
+ return true;
13682
+ }
13683
+ function listTrustedModules() {
13684
+ return readTrustFile().modules;
13685
+ }
13686
+ function readPublisherKeys() {
13687
+ if (!(0, import_node_fs13.existsSync)(PUBLISHER_KEYS_FILE)) return {};
13688
+ try {
13689
+ const parsed = JSON.parse((0, import_node_fs13.readFileSync)(PUBLISHER_KEYS_FILE, "utf-8"));
13690
+ return parsed && typeof parsed === "object" ? parsed : {};
13691
+ } catch {
13692
+ return {};
13693
+ }
13694
+ }
13695
+ function verifyModuleSignature(modulePath, digest) {
13696
+ const keys = readPublisherKeys();
13697
+ const pinnedKeyIds = Object.keys(keys);
13698
+ const sigPath = (0, import_node_path10.join)(modulePath, "mod.sig");
13699
+ if (pinnedKeyIds.length === 0) {
13700
+ return { ok: true };
13701
+ }
13702
+ if (!(0, import_node_fs13.existsSync)(sigPath)) {
13703
+ return { ok: false, reason: "publisher keys are pinned but the module ships no mod.sig" };
13704
+ }
13705
+ let parsed;
13706
+ try {
13707
+ parsed = JSON.parse((0, import_node_fs13.readFileSync)(sigPath, "utf-8"));
13708
+ } catch {
13709
+ return { ok: false, reason: "mod.sig is not valid JSON" };
13710
+ }
13711
+ if (!parsed.keyId || !parsed.signature) {
13712
+ return { ok: false, reason: "mod.sig is missing keyId or signature" };
13713
+ }
13714
+ const publicKey = keys[parsed.keyId];
13715
+ if (!publicKey) {
13716
+ return { ok: false, reason: `mod.sig references unpinned key "${parsed.keyId}"` };
13717
+ }
13718
+ try {
13719
+ const valid = (0, import_node_crypto3.verify)(
13720
+ null,
13721
+ Buffer.from(digest, "hex"),
13722
+ publicKey,
13723
+ Buffer.from(parsed.signature, "base64")
13724
+ );
13725
+ return valid ? { ok: true } : { ok: false, reason: "mod.sig signature does not match" };
13726
+ } catch (err) {
13727
+ return { ok: false, reason: `signature check failed: ${String(err.message || err)}` };
13728
+ }
13729
+ }
13730
+ function verifyModuleTrust(name, modulePath) {
13731
+ let digest;
13732
+ try {
13733
+ digest = computeModuleDigest(modulePath);
13734
+ } catch (err) {
13735
+ return { ok: false, reason: `could not hash module: ${String(err.message || err)}` };
13736
+ }
13737
+ const signature = verifyModuleSignature(modulePath, digest);
13738
+ if (!signature.ok) return signature;
13739
+ const record = readTrustFile().modules[name];
13740
+ if (!record) {
13741
+ return {
13742
+ ok: false,
13743
+ reason: `not trusted \u2014 review it, then run: threatcrush modules trust ${name}`
13744
+ };
13745
+ }
13746
+ if (record.digest !== digest) {
13747
+ return {
13748
+ ok: false,
13749
+ reason: `contents changed since it was trusted on ${record.trustedAt} \u2014 re-review it, then run: threatcrush modules trust ${name}`
13750
+ };
13751
+ }
13752
+ return { ok: true };
13753
+ }
13754
+
13755
+ // src/commands/modules.ts
13301
13756
  var API_URL2 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
13302
13757
  function modulesDir() {
13303
13758
  ensureRuntimeDirs();
@@ -13310,7 +13765,7 @@ function safeModuleDirName(name, label = "module name") {
13310
13765
  return name;
13311
13766
  }
13312
13767
  function moduleDestination(dir, name, label) {
13313
- return (0, import_node_path9.join)(dir, safeModuleDirName(name, label));
13768
+ return (0, import_node_path11.join)(dir, safeModuleDirName(name, label));
13314
13769
  }
13315
13770
  function assertSafeTarballEntries(tarPath) {
13316
13771
  const listing = (0, import_node_child_process2.execFileSync)("tar", ["-tzf", tarPath], { encoding: "utf-8" });
@@ -13323,12 +13778,12 @@ function assertSafeTarballEntries(tarPath) {
13323
13778
  }
13324
13779
  }
13325
13780
  function validateManifest(modPath) {
13326
- const manifestPath = (0, import_node_path9.join)(modPath, "mod.toml");
13327
- if (!(0, import_node_fs12.existsSync)(manifestPath)) {
13781
+ const manifestPath = (0, import_node_path11.join)(modPath, "mod.toml");
13782
+ if (!(0, import_node_fs14.existsSync)(manifestPath)) {
13328
13783
  return { ok: false, error: `mod.toml not found at ${manifestPath}` };
13329
13784
  }
13330
13785
  try {
13331
- const raw = (0, import_node_fs12.readFileSync)(manifestPath, "utf-8");
13786
+ const raw = (0, import_node_fs14.readFileSync)(manifestPath, "utf-8");
13332
13787
  const parsed = import_toml3.default.parse(raw);
13333
13788
  const name = parsed.module?.name;
13334
13789
  const version = parsed.module?.version;
@@ -13339,6 +13794,12 @@ function validateManifest(modPath) {
13339
13794
  return { ok: false, error: `Invalid mod.toml: ${err.message}` };
13340
13795
  }
13341
13796
  }
13797
+ function printTrustRequired(name) {
13798
+ console.log();
13799
+ console.log(source_default.yellow(` ! ${name} is installed but NOT trusted, so threatcrushd will not load it.`));
13800
+ console.log(source_default.dim(" Review the module source, then run:"));
13801
+ console.log(source_default.dim(` ${source_default.white(`threatcrush modules trust ${name}`)}`));
13802
+ }
13342
13803
  function notifyDaemonIfRunning() {
13343
13804
  if (!findRunningDaemon()) return;
13344
13805
  console.log(source_default.dim(` \u2139 threatcrushd is running \u2014 restart it to load/unload modules:`));
@@ -13382,8 +13843,8 @@ async function modulesInstallCommand(source) {
13382
13843
  console.log();
13383
13844
  const dir = modulesDir();
13384
13845
  if (source.startsWith("./") || source.startsWith("/") || source.startsWith("~")) {
13385
- const absPath = (0, import_node_path9.resolve)(source.startsWith("~") ? source.replace("~", process.env.HOME || "") : source);
13386
- if (!(0, import_node_fs12.existsSync)(absPath)) {
13846
+ const absPath = (0, import_node_path11.resolve)(source.startsWith("~") ? source.replace("~", process.env.HOME || "") : source);
13847
+ if (!(0, import_node_fs14.existsSync)(absPath)) {
13387
13848
  console.log(source_default.red(` \u2717 Path not found: ${absPath}
13388
13849
  `));
13389
13850
  return;
@@ -13402,7 +13863,7 @@ async function modulesInstallCommand(source) {
13402
13863
  `));
13403
13864
  return;
13404
13865
  }
13405
- if ((0, import_node_fs12.existsSync)(dest2)) {
13866
+ if ((0, import_node_fs14.existsSync)(dest2)) {
13406
13867
  console.log(source_default.yellow(` ! ${check.name} is already installed at ${dest2}`));
13407
13868
  console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${check.name}`)} first.
13408
13869
  `));
@@ -13410,12 +13871,13 @@ async function modulesInstallCommand(source) {
13410
13871
  }
13411
13872
  const spinner2 = ora({ text: `Copying module files...`, color: "green" }).start();
13412
13873
  try {
13413
- (0, import_node_fs12.cpSync)(absPath, dest2, { recursive: true, dereference: true });
13874
+ (0, import_node_fs14.cpSync)(absPath, dest2, { recursive: true, dereference: true });
13414
13875
  spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
13415
13876
  } catch (err) {
13416
13877
  spinner2.fail(`Copy failed: ${err.message}`);
13417
13878
  return;
13418
13879
  }
13880
+ printTrustRequired(check.name);
13419
13881
  notifyDaemonIfRunning();
13420
13882
  console.log();
13421
13883
  return;
@@ -13425,14 +13887,14 @@ async function modulesInstallCommand(source) {
13425
13887
  let name;
13426
13888
  let dest2;
13427
13889
  try {
13428
- name = safeModuleDirName((0, import_node_path9.basename)(gitUrl.replace(/\.git$/, "")), "repository name");
13890
+ name = safeModuleDirName((0, import_node_path11.basename)(gitUrl.replace(/\.git$/, "")), "repository name");
13429
13891
  dest2 = moduleDestination(dir, name);
13430
13892
  } catch (err) {
13431
13893
  console.log(source_default.red(` x ${err.message}
13432
13894
  `));
13433
13895
  return;
13434
13896
  }
13435
- if ((0, import_node_fs12.existsSync)(dest2)) {
13897
+ if ((0, import_node_fs14.existsSync)(dest2)) {
13436
13898
  console.log(source_default.yellow(` ! ${name} is already installed at ${dest2}`));
13437
13899
  console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${name}`)} first.
13438
13900
  `));
@@ -13449,12 +13911,13 @@ async function modulesInstallCommand(source) {
13449
13911
  if (!check.ok) {
13450
13912
  spinner2.fail(check.error);
13451
13913
  try {
13452
- (0, import_node_fs12.rmSync)(dest2, { recursive: true, force: true });
13914
+ (0, import_node_fs14.rmSync)(dest2, { recursive: true, force: true });
13453
13915
  } catch {
13454
13916
  }
13455
13917
  return;
13456
13918
  }
13457
13919
  spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
13920
+ printTrustRequired(check.name);
13458
13921
  notifyDaemonIfRunning();
13459
13922
  console.log();
13460
13923
  return;
@@ -13494,7 +13957,7 @@ async function modulesInstallCommand(source) {
13494
13957
  `));
13495
13958
  return;
13496
13959
  }
13497
- if ((0, import_node_fs12.existsSync)(dest)) {
13960
+ if ((0, import_node_fs14.existsSync)(dest)) {
13498
13961
  console.log(source_default.yellow(` ! ${mod.slug} is already installed at ${dest}
13499
13962
  `));
13500
13963
  return;
@@ -13520,7 +13983,7 @@ async function modulesInstallCommand(source) {
13520
13983
  if (!check.ok) {
13521
13984
  cloneSpinner.fail(check.error);
13522
13985
  try {
13523
- (0, import_node_fs12.rmSync)(dest, { recursive: true, force: true });
13986
+ (0, import_node_fs14.rmSync)(dest, { recursive: true, force: true });
13524
13987
  } catch {
13525
13988
  }
13526
13989
  return;
@@ -13534,11 +13997,11 @@ async function modulesInstallCommand(source) {
13534
13997
  dlSpinner.fail(`HTTP ${res.status}`);
13535
13998
  return;
13536
13999
  }
13537
- const tar = (0, import_node_path9.join)(dir, `${mod.slug}.tar.gz`);
13538
- (0, import_node_fs12.writeFileSync)(tar, Buffer.from(await res.arrayBuffer()));
14000
+ const tar = (0, import_node_path11.join)(dir, `${mod.slug}.tar.gz`);
14001
+ (0, import_node_fs14.writeFileSync)(tar, Buffer.from(await res.arrayBuffer()));
13539
14002
  assertSafeTarballEntries(tar);
13540
14003
  (0, import_node_child_process2.execFileSync)("tar", ["-xzf", tar, "-C", dir], { stdio: "pipe" });
13541
- (0, import_node_fs12.rmSync)(tar, { force: true });
14004
+ (0, import_node_fs14.rmSync)(tar, { force: true });
13542
14005
  const check = validateManifest(dest);
13543
14006
  if (!check.ok) {
13544
14007
  dlSpinner.fail(check.error);
@@ -13553,6 +14016,7 @@ async function modulesInstallCommand(source) {
13553
14016
  logger.info("No installable artifact provided for this module.");
13554
14017
  return;
13555
14018
  }
14019
+ printTrustRequired(mod.slug);
13556
14020
  notifyDaemonIfRunning();
13557
14021
  console.log();
13558
14022
  }
@@ -13569,7 +14033,7 @@ async function modulesRemoveCommand(name) {
13569
14033
  `));
13570
14034
  return;
13571
14035
  }
13572
- if (!(0, import_node_fs12.existsSync)(target)) {
14036
+ if (!(0, import_node_fs14.existsSync)(target)) {
13573
14037
  console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
13574
14038
  `));
13575
14039
  return;
@@ -13579,7 +14043,8 @@ async function modulesRemoveCommand(name) {
13579
14043
  console.log(source_default.yellow(` ! Directory name "${name}" does not match manifest name "${check.name}"`));
13580
14044
  }
13581
14045
  try {
13582
- (0, import_node_fs12.rmSync)(target, { recursive: true, force: true });
14046
+ (0, import_node_fs14.rmSync)(target, { recursive: true, force: true });
14047
+ untrustModule(name);
13583
14048
  console.log(source_default.green(` \u2713 Removed ${name} from ${dir}
13584
14049
  `));
13585
14050
  } catch (err) {
@@ -13589,6 +14054,72 @@ async function modulesRemoveCommand(name) {
13589
14054
  }
13590
14055
  notifyDaemonIfRunning();
13591
14056
  }
14057
+ async function modulesTrustCommand(name) {
14058
+ banner();
14059
+ console.log(source_default.green.bold(" Trust Module"));
14060
+ console.log(source_default.gray(" " + "\u2500".repeat(50)));
14061
+ console.log();
14062
+ const dir = modulesDir();
14063
+ let target;
14064
+ try {
14065
+ target = moduleDestination(dir, name);
14066
+ } catch (err) {
14067
+ console.log(source_default.red(` x ${err.message}
14068
+ `));
14069
+ return;
14070
+ }
14071
+ if (!(0, import_node_fs14.existsSync)(target)) {
14072
+ console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
14073
+ `));
14074
+ return;
14075
+ }
14076
+ const check = validateManifest(target);
14077
+ if (!check.ok) {
14078
+ console.log(source_default.red(` \u2717 ${check.error}
14079
+ `));
14080
+ return;
14081
+ }
14082
+ const record = trustModule(name, target, target);
14083
+ console.log(source_default.green(` \u2713 Trusted ${source_default.white(name)} v${check.version}`));
14084
+ console.log(source_default.dim(` digest ${record.digest.slice(0, 16)}\u2026`));
14085
+ console.log(
14086
+ source_default.dim(" threatcrushd will refuse to load it again if its contents change.\n")
14087
+ );
14088
+ notifyDaemonIfRunning();
14089
+ }
14090
+ async function modulesUntrustCommand(name) {
14091
+ banner();
14092
+ console.log(source_default.green.bold(" Revoke Module Trust"));
14093
+ console.log(source_default.gray(" " + "\u2500".repeat(50)));
14094
+ console.log();
14095
+ if (untrustModule(name)) {
14096
+ console.log(source_default.green(` \u2713 Revoked trust for ${source_default.white(name)}
14097
+ `));
14098
+ notifyDaemonIfRunning();
14099
+ } else {
14100
+ console.log(source_default.yellow(` ! ${name} was not trusted
14101
+ `));
14102
+ }
14103
+ }
14104
+ async function modulesTrustedCommand() {
14105
+ banner();
14106
+ console.log(source_default.green.bold(" Trusted Modules"));
14107
+ console.log(source_default.gray(" " + "\u2500".repeat(50)));
14108
+ console.log();
14109
+ const trusted = Object.entries(listTrustedModules());
14110
+ if (trusted.length === 0) {
14111
+ console.log(source_default.yellow(" No modules are trusted."));
14112
+ console.log(source_default.dim(" Installed modules stay dormant until you run:"));
14113
+ console.log(source_default.dim(` ${source_default.white("threatcrush modules trust <name>")}
14114
+ `));
14115
+ return;
14116
+ }
14117
+ for (const [name, record] of trusted) {
14118
+ console.log(` ${source_default.white(name)} ${source_default.dim(record.digest.slice(0, 16) + "\u2026")}`);
14119
+ console.log(source_default.dim(` trusted ${record.trustedAt}`));
14120
+ }
14121
+ console.log();
14122
+ }
13592
14123
  async function modulesCommand(opts) {
13593
14124
  const action = opts.action || "list";
13594
14125
  switch (action) {
@@ -13622,10 +14153,31 @@ async function modulesCommand(opts) {
13622
14153
  }
13623
14154
  await modulesRemoveCommand(opts.name);
13624
14155
  break;
14156
+ case "trust":
14157
+ if (!opts.name) {
14158
+ banner();
14159
+ console.log(source_default.red(" Module name required."));
14160
+ console.log(source_default.gray(" Usage: threatcrush modules trust <name>\n"));
14161
+ return;
14162
+ }
14163
+ await modulesTrustCommand(opts.name);
14164
+ break;
14165
+ case "untrust":
14166
+ if (!opts.name) {
14167
+ banner();
14168
+ console.log(source_default.red(" Module name required."));
14169
+ console.log(source_default.gray(" Usage: threatcrush modules untrust <name>\n"));
14170
+ return;
14171
+ }
14172
+ await modulesUntrustCommand(opts.name);
14173
+ break;
14174
+ case "trusted":
14175
+ await modulesTrustedCommand();
14176
+ break;
13625
14177
  default:
13626
14178
  banner();
13627
14179
  console.log(source_default.yellow(` Unknown action: ${action}`));
13628
- console.log(source_default.gray(" Available actions: list, install, remove\n"));
14180
+ console.log(source_default.gray(" Available actions: list, install, remove, trust, untrust, trusted\n"));
13629
14181
  await modulesListCommand();
13630
14182
  break;
13631
14183
  }
@@ -13952,21 +14504,21 @@ async function pentestCommand(targetUrl) {
13952
14504
 
13953
14505
  // src/commands/orgs.ts
13954
14506
  var import_node_os5 = require("os");
13955
- var import_node_fs13 = require("fs");
13956
- var import_node_path10 = require("path");
14507
+ var import_node_fs15 = require("fs");
14508
+ var import_node_path12 = require("path");
13957
14509
  var API_URL3 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
13958
- var CONFIG_PATH = (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".threatcrush", "config.json");
14510
+ var CONFIG_PATH = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".threatcrush", "config.json");
13959
14511
  function readConfig() {
13960
14512
  try {
13961
- return JSON.parse((0, import_node_fs13.readFileSync)(CONFIG_PATH, "utf-8"));
14513
+ return JSON.parse((0, import_node_fs15.readFileSync)(CONFIG_PATH, "utf-8"));
13962
14514
  } catch {
13963
14515
  return {};
13964
14516
  }
13965
14517
  }
13966
14518
  function writeConfig(config) {
13967
- const dir = (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".threatcrush");
13968
- if (!(0, import_node_fs13.existsSync)(dir)) (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
13969
- (0, import_node_fs13.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
14519
+ const dir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".threatcrush");
14520
+ if (!(0, import_node_fs15.existsSync)(dir)) (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
14521
+ (0, import_node_fs15.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
13970
14522
  }
13971
14523
  function getAuthHeaders() {
13972
14524
  const config = readConfig();
@@ -14102,13 +14654,13 @@ async function useOrganization(slug) {
14102
14654
 
14103
14655
  // src/commands/servers.ts
14104
14656
  var import_node_os6 = require("os");
14105
- var import_node_fs14 = require("fs");
14106
- var import_node_path11 = require("path");
14657
+ var import_node_fs16 = require("fs");
14658
+ var import_node_path13 = require("path");
14107
14659
  var API_URL4 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
14108
- var CONFIG_PATH2 = (0, import_node_path11.join)((0, import_node_os6.homedir)(), ".threatcrush", "config.json");
14660
+ var CONFIG_PATH2 = (0, import_node_path13.join)((0, import_node_os6.homedir)(), ".threatcrush", "config.json");
14109
14661
  function readConfig2() {
14110
14662
  try {
14111
- return JSON.parse((0, import_node_fs14.readFileSync)(CONFIG_PATH2, "utf-8"));
14663
+ return JSON.parse((0, import_node_fs16.readFileSync)(CONFIG_PATH2, "utf-8"));
14112
14664
  } catch {
14113
14665
  return {};
14114
14666
  }
@@ -14211,14 +14763,14 @@ function timeAgo(dateStr) {
14211
14763
 
14212
14764
  // src/commands/connect.ts
14213
14765
  var import_node_os7 = require("os");
14214
- var import_node_fs15 = require("fs");
14215
- var import_node_path12 = require("path");
14766
+ var import_node_fs17 = require("fs");
14767
+ var import_node_path14 = require("path");
14216
14768
  var import_node_child_process3 = require("child_process");
14217
14769
  var API_URL5 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
14218
- var CONFIG_PATH3 = (0, import_node_path12.join)((0, import_node_os7.homedir)(), ".threatcrush", "config.json");
14770
+ var CONFIG_PATH3 = (0, import_node_path14.join)((0, import_node_os7.homedir)(), ".threatcrush", "config.json");
14219
14771
  function readConfig3() {
14220
14772
  try {
14221
- return JSON.parse((0, import_node_fs15.readFileSync)(CONFIG_PATH3, "utf-8"));
14773
+ return JSON.parse((0, import_node_fs17.readFileSync)(CONFIG_PATH3, "utf-8"));
14222
14774
  } catch {
14223
14775
  return {};
14224
14776
  }
@@ -14373,20 +14925,21 @@ async function sshConnect(options) {
14373
14925
 
14374
14926
  // src/commands/daemon.ts
14375
14927
  var import_node_child_process7 = require("child_process");
14376
- var import_node_fs24 = require("fs");
14377
- var import_node_path16 = require("path");
14378
- var import_node_fs25 = require("fs");
14928
+ var import_node_fs26 = require("fs");
14929
+ var import_node_path18 = require("path");
14930
+ var import_node_fs27 = require("fs");
14379
14931
 
14380
14932
  // src/daemon/index.ts
14381
- var import_node_fs23 = require("fs");
14382
- var import_node_path15 = require("path");
14933
+ var import_node_fs25 = require("fs");
14934
+ var import_node_path17 = require("path");
14383
14935
  init_paths();
14384
14936
  init_pidfile();
14385
14937
 
14386
14938
  // src/daemon/ipc-server.ts
14387
14939
  var import_node_net2 = require("net");
14388
- var import_node_fs16 = require("fs");
14940
+ var import_node_fs18 = require("fs");
14389
14941
  init_paths();
14942
+ init_control_token();
14390
14943
 
14391
14944
  // src/daemon/event-bus.ts
14392
14945
  var import_node_events = require("events");
@@ -14431,10 +14984,12 @@ var IpcServer = class {
14431
14984
  nextClientId = 1;
14432
14985
  startedAt = /* @__PURE__ */ new Date();
14433
14986
  counters = { events: 0, threats: 0, alerts: 0 };
14987
+ controlToken = "";
14434
14988
  async start() {
14435
- if ((0, import_node_fs16.existsSync)(PATHS.socket)) {
14989
+ this.controlToken = issueControlToken();
14990
+ if ((0, import_node_fs18.existsSync)(PATHS.socket)) {
14436
14991
  try {
14437
- (0, import_node_fs16.unlinkSync)(PATHS.socket);
14992
+ (0, import_node_fs18.unlinkSync)(PATHS.socket);
14438
14993
  } catch {
14439
14994
  }
14440
14995
  }
@@ -14470,14 +15025,14 @@ var IpcServer = class {
14470
15025
  return new Promise((resolve5) => {
14471
15026
  if (!this.server) {
14472
15027
  try {
14473
- if ((0, import_node_fs16.existsSync)(PATHS.socket)) (0, import_node_fs16.unlinkSync)(PATHS.socket);
15028
+ if ((0, import_node_fs18.existsSync)(PATHS.socket)) (0, import_node_fs18.unlinkSync)(PATHS.socket);
14474
15029
  } catch {
14475
15030
  }
14476
15031
  return resolve5();
14477
15032
  }
14478
15033
  this.server.close(() => {
14479
15034
  try {
14480
- if ((0, import_node_fs16.existsSync)(PATHS.socket)) (0, import_node_fs16.unlinkSync)(PATHS.socket);
15035
+ if ((0, import_node_fs18.existsSync)(PATHS.socket)) (0, import_node_fs18.unlinkSync)(PATHS.socket);
14481
15036
  } catch {
14482
15037
  }
14483
15038
  resolve5();
@@ -14563,6 +15118,13 @@ var IpcServer = class {
14563
15118
  for (const ch of req.params.channels) client.subscriptions.add(ch);
14564
15119
  return this.send(client, { id: req.id, ok: true, result: { subscribed: [...client.subscriptions] } });
14565
15120
  case "shutdown":
15121
+ if (!tokensMatch(this.controlToken, req.params?.token)) {
15122
+ return this.send(client, {
15123
+ id: req.id,
15124
+ ok: false,
15125
+ error: "shutdown requires the daemon control token (run as root, or use systemctl)"
15126
+ });
15127
+ }
14566
15128
  this.send(client, { id: req.id, ok: true, result: "shutting down" });
14567
15129
  setTimeout(() => process.emit("SIGTERM"), 50);
14568
15130
  return;
@@ -14583,14 +15145,14 @@ var IpcServer = class {
14583
15145
  };
14584
15146
 
14585
15147
  // src/daemon/module-host.ts
14586
- var import_node_fs20 = require("fs");
14587
- var import_node_path13 = require("path");
15148
+ var import_node_fs22 = require("fs");
15149
+ var import_node_path15 = require("path");
14588
15150
  var import_node_url = require("url");
14589
15151
  var import_toml4 = __toESM(require_toml());
14590
15152
  init_paths();
14591
15153
 
14592
15154
  // src/daemon/watchers/log-watcher.ts
14593
- var import_node_fs17 = require("fs");
15155
+ var import_node_fs19 = require("fs");
14594
15156
  var import_node_readline4 = require("readline");
14595
15157
  init_state();
14596
15158
  var DEFAULT_SOURCES = [
@@ -14612,9 +15174,9 @@ var LogWatcher = class {
14612
15174
  start() {
14613
15175
  const started = [];
14614
15176
  for (const src of this.sources) {
14615
- if (!(0, import_node_fs17.existsSync)(src.path)) continue;
15177
+ if (!(0, import_node_fs19.existsSync)(src.path)) continue;
14616
15178
  try {
14617
- (0, import_node_fs17.accessSync)(src.path, import_node_fs17.constants.R_OK);
15179
+ (0, import_node_fs19.accessSync)(src.path, import_node_fs19.constants.R_OK);
14618
15180
  } catch {
14619
15181
  continue;
14620
15182
  }
@@ -14634,7 +15196,7 @@ var LogWatcher = class {
14634
15196
  }
14635
15197
  tail(src) {
14636
15198
  try {
14637
- this.positions.set(src.path, (0, import_node_fs17.statSync)(src.path).size);
15199
+ this.positions.set(src.path, (0, import_node_fs19.statSync)(src.path).size);
14638
15200
  } catch {
14639
15201
  this.positions.set(src.path, 0);
14640
15202
  }
@@ -14645,7 +15207,7 @@ var LogWatcher = class {
14645
15207
  poll(src) {
14646
15208
  let stat;
14647
15209
  try {
14648
- stat = (0, import_node_fs17.statSync)(src.path);
15210
+ stat = (0, import_node_fs19.statSync)(src.path);
14649
15211
  } catch {
14650
15212
  return;
14651
15213
  }
@@ -14655,7 +15217,7 @@ var LogWatcher = class {
14655
15217
  return;
14656
15218
  }
14657
15219
  if (stat.size === prev) return;
14658
- const stream = (0, import_node_fs17.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
15220
+ const stream = (0, import_node_fs19.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
14659
15221
  stream.on("error", () => this.positions.set(src.path, stat.size));
14660
15222
  const rl = (0, import_node_readline4.createInterface)({ input: stream });
14661
15223
  rl.on("error", () => {
@@ -14848,7 +15410,7 @@ function realtimeToDate(rt) {
14848
15410
 
14849
15411
  // src/modules/network-monitor/index.ts
14850
15412
  var import_node_child_process5 = require("child_process");
14851
- var import_node_fs18 = require("fs");
15413
+ var import_node_fs20 = require("fs");
14852
15414
  init_state();
14853
15415
  var NetworkMonitor = class {
14854
15416
  constructor(bus2) {
@@ -14887,7 +15449,7 @@ var NetworkMonitor = class {
14887
15449
  hasConntrackOrSs() {
14888
15450
  const ss = (0, import_node_child_process5.spawnSync)("ss", ["--version"], { stdio: "pipe" });
14889
15451
  if (ss.status === 0) return true;
14890
- return (0, import_node_fs18.existsSync)("/proc/net/tcp");
15452
+ return (0, import_node_fs20.existsSync)("/proc/net/tcp");
14891
15453
  }
14892
15454
  poll() {
14893
15455
  try {
@@ -15026,7 +15588,7 @@ var NetworkMonitor = class {
15026
15588
  };
15027
15589
 
15028
15590
  // src/modules/dns-monitor/index.ts
15029
- var import_node_fs19 = require("fs");
15591
+ var import_node_fs21 = require("fs");
15030
15592
  var import_node_readline5 = require("readline");
15031
15593
  init_state();
15032
15594
  var DNS_LOG_SOURCES = [
@@ -15061,9 +15623,9 @@ var DnsMonitor = class {
15061
15623
  entropyThreshold = 3.5;
15062
15624
  start() {
15063
15625
  const sources = DNS_LOG_SOURCES.filter((p) => {
15064
- if (!(0, import_node_fs19.existsSync)(p)) return false;
15626
+ if (!(0, import_node_fs21.existsSync)(p)) return false;
15065
15627
  try {
15066
- (0, import_node_fs19.accessSync)(p, import_node_fs19.constants.R_OK);
15628
+ (0, import_node_fs21.accessSync)(p, import_node_fs21.constants.R_OK);
15067
15629
  return true;
15068
15630
  } catch {
15069
15631
  return false;
@@ -15087,7 +15649,7 @@ var DnsMonitor = class {
15087
15649
  }
15088
15650
  tailLog(path) {
15089
15651
  try {
15090
- this.positions.set(path, (0, import_node_fs19.statSync)(path).size);
15652
+ this.positions.set(path, (0, import_node_fs21.statSync)(path).size);
15091
15653
  } catch {
15092
15654
  this.positions.set(path, 0);
15093
15655
  }
@@ -15097,7 +15659,7 @@ var DnsMonitor = class {
15097
15659
  pollLog(path) {
15098
15660
  let stat;
15099
15661
  try {
15100
- stat = (0, import_node_fs19.statSync)(path);
15662
+ stat = (0, import_node_fs21.statSync)(path);
15101
15663
  } catch {
15102
15664
  return;
15103
15665
  }
@@ -15107,7 +15669,7 @@ var DnsMonitor = class {
15107
15669
  return;
15108
15670
  }
15109
15671
  if (stat.size === prev) return;
15110
- const stream = (0, import_node_fs19.createReadStream)(path, { start: prev, encoding: "utf-8" });
15672
+ const stream = (0, import_node_fs21.createReadStream)(path, { start: prev, encoding: "utf-8" });
15111
15673
  stream.on("error", () => this.positions.set(path, stat.size));
15112
15674
  const rl = (0, import_node_readline5.createInterface)({ input: stream });
15113
15675
  rl.on("line", (line) => this.parseDnsLine(line));
@@ -15339,15 +15901,15 @@ var ModuleHost = class {
15339
15901
  for (const m of builtins) this.modules.set(m.name, m);
15340
15902
  }
15341
15903
  async discoverAndStartInstalled() {
15342
- if (!(0, import_node_fs20.existsSync)(PATHS.moduleDir)) return;
15904
+ if (!(0, import_node_fs22.existsSync)(PATHS.moduleDir)) return;
15343
15905
  const configs = loadModuleConfigs(PATHS.confD);
15344
- const entries = (0, import_node_fs20.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
15906
+ const entries = (0, import_node_fs22.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
15345
15907
  for (const entry of entries) {
15346
15908
  if (!entry.isDirectory()) continue;
15347
- const manifestPath = (0, import_node_path13.join)(PATHS.moduleDir, entry.name, "mod.toml");
15348
- if (!(0, import_node_fs20.existsSync)(manifestPath)) continue;
15909
+ const manifestPath = (0, import_node_path15.join)(PATHS.moduleDir, entry.name, "mod.toml");
15910
+ if (!(0, import_node_fs22.existsSync)(manifestPath)) continue;
15349
15911
  try {
15350
- const manifest = import_toml4.default.parse((0, import_node_fs20.readFileSync)(manifestPath, "utf-8"));
15912
+ const manifest = import_toml4.default.parse((0, import_node_fs22.readFileSync)(manifestPath, "utf-8"));
15351
15913
  const name = manifest.module?.name || entry.name;
15352
15914
  const defaults = manifest.module?.config?.defaults || {};
15353
15915
  const config = {
@@ -15361,7 +15923,7 @@ var ModuleHost = class {
15361
15923
  source: "installed",
15362
15924
  status: config.enabled === false ? "disabled" : "loaded",
15363
15925
  events: 0,
15364
- path: (0, import_node_path13.join)(PATHS.moduleDir, entry.name),
15926
+ path: (0, import_node_path15.join)(PATHS.moduleDir, entry.name),
15365
15927
  config
15366
15928
  };
15367
15929
  this.modules.set(name, hosted);
@@ -15376,7 +15938,7 @@ var ModuleHost = class {
15376
15938
  status: "error",
15377
15939
  events: 0,
15378
15940
  detail: `manifest load failed: ${String(err.message || err)}`,
15379
- path: (0, import_node_path13.join)(PATHS.moduleDir, entry.name)
15941
+ path: (0, import_node_path15.join)(PATHS.moduleDir, entry.name)
15380
15942
  });
15381
15943
  }
15382
15944
  }
@@ -15388,6 +15950,13 @@ var ModuleHost = class {
15388
15950
  hosted.detail = "no built entrypoint found; run npm install && npm run build in the module directory";
15389
15951
  return;
15390
15952
  }
15953
+ const trust = verifyModuleTrust(hosted.name, hosted.path);
15954
+ if (!trust.ok) {
15955
+ hosted.status = "error";
15956
+ hosted.detail = `refusing to load: ${trust.reason}`;
15957
+ this.bus.announceModule(hosted.name, "error", hosted.detail);
15958
+ return;
15959
+ }
15391
15960
  try {
15392
15961
  const imported = await import((0, import_node_url.pathToFileURL)(entrypoint).href);
15393
15962
  const exported = imported.default || imported.module || imported;
@@ -15408,17 +15977,17 @@ var ModuleHost = class {
15408
15977
  }
15409
15978
  }
15410
15979
  installedEntrypoint(modulePath) {
15411
- const packageJson = (0, import_node_path13.join)(modulePath, "package.json");
15980
+ const packageJson = (0, import_node_path15.join)(modulePath, "package.json");
15412
15981
  const candidates = [];
15413
- if ((0, import_node_fs20.existsSync)(packageJson)) {
15982
+ if ((0, import_node_fs22.existsSync)(packageJson)) {
15414
15983
  try {
15415
- const pkg = JSON.parse((0, import_node_fs20.readFileSync)(packageJson, "utf-8"));
15416
- if (pkg.main) candidates.push((0, import_node_path13.join)(modulePath, pkg.main));
15984
+ const pkg = JSON.parse((0, import_node_fs22.readFileSync)(packageJson, "utf-8"));
15985
+ if (pkg.main) candidates.push((0, import_node_path15.join)(modulePath, pkg.main));
15417
15986
  } catch {
15418
15987
  }
15419
15988
  }
15420
- candidates.push((0, import_node_path13.join)(modulePath, "dist", "index.js"), (0, import_node_path13.join)(modulePath, "index.js"));
15421
- return candidates.find((candidate) => (0, import_node_fs20.existsSync)(candidate)) || null;
15989
+ candidates.push((0, import_node_path15.join)(modulePath, "dist", "index.js"), (0, import_node_path15.join)(modulePath, "index.js"));
15990
+ return candidates.find((candidate) => (0, import_node_fs22.existsSync)(candidate)) || null;
15422
15991
  }
15423
15992
  isThreatCrushModule(value) {
15424
15993
  return Boolean(
@@ -15939,8 +16508,8 @@ var RuleEngine = class {
15939
16508
  };
15940
16509
 
15941
16510
  // src/daemon/rules/loader.ts
15942
- var import_node_fs21 = require("fs");
15943
- var import_node_path14 = require("path");
16511
+ var import_node_fs23 = require("fs");
16512
+ var import_node_path16 = require("path");
15944
16513
 
15945
16514
  // src/daemon/rules/default-rules.ts
15946
16515
  var DEFAULT_RULES = [
@@ -16224,11 +16793,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
16224
16793
  function loadAllRules(customDir) {
16225
16794
  const rules = [...DEFAULT_RULES];
16226
16795
  const dir = customDir || RULES_DIR;
16227
- if ((0, import_node_fs21.existsSync)(dir)) {
16228
- const files = (0, import_node_fs21.readdirSync)(dir).filter((f) => f.endsWith(".json"));
16796
+ if ((0, import_node_fs23.existsSync)(dir)) {
16797
+ const files = (0, import_node_fs23.readdirSync)(dir).filter((f) => f.endsWith(".json"));
16229
16798
  for (const file of files) {
16230
16799
  try {
16231
- const raw = (0, import_node_fs21.readFileSync)((0, import_node_path14.join)(dir, file), "utf-8");
16800
+ const raw = (0, import_node_fs23.readFileSync)((0, import_node_path16.join)(dir, file), "utf-8");
16232
16801
  const parsed = JSON.parse(raw);
16233
16802
  const customRules = Array.isArray(parsed) ? parsed : [parsed];
16234
16803
  for (const rule of customRules) {
@@ -16391,7 +16960,7 @@ function detectFirewallAdapter() {
16391
16960
  }
16392
16961
 
16393
16962
  // src/daemon/firewall/remediation.ts
16394
- var import_node_fs22 = require("fs");
16963
+ var import_node_fs24 = require("fs");
16395
16964
  init_state();
16396
16965
  init_paths();
16397
16966
  var DEFAULT_CONFIG2 = {
@@ -16548,7 +17117,7 @@ var RemediationManager = class {
16548
17117
  }
16549
17118
  logLine(line) {
16550
17119
  try {
16551
- (0, import_node_fs22.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
17120
+ (0, import_node_fs24.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
16552
17121
  `);
16553
17122
  } catch {
16554
17123
  }
@@ -16607,7 +17176,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
16607
17176
  // src/daemon/index.ts
16608
17177
  function readVersion2() {
16609
17178
  try {
16610
- const pkg = JSON.parse((0, import_node_fs23.readFileSync)((0, import_node_path15.join)(__dirname, "..", "package.json"), "utf-8"));
17179
+ const pkg = JSON.parse((0, import_node_fs25.readFileSync)((0, import_node_path17.join)(__dirname, "..", "package.json"), "utf-8"));
16611
17180
  return pkg.version || "0.0.0";
16612
17181
  } catch {
16613
17182
  return "0.0.0";
@@ -16615,7 +17184,7 @@ function readVersion2() {
16615
17184
  }
16616
17185
  function logLine(line) {
16617
17186
  try {
16618
- (0, import_node_fs23.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
17187
+ (0, import_node_fs25.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
16619
17188
  `);
16620
17189
  } catch {
16621
17190
  }
@@ -16643,7 +17212,7 @@ async function runDaemon() {
16643
17212
  } catch (err) {
16644
17213
  logLine(`[daemon] state db unavailable: ${err.message}`);
16645
17214
  }
16646
- const config = loadConfig((0, import_node_fs23.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
17215
+ const config = loadConfig((0, import_node_fs25.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
16647
17216
  bus.on("event", (event) => {
16648
17217
  logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
16649
17218
  });
@@ -16735,7 +17304,7 @@ async function runDaemon() {
16735
17304
  init_paths();
16736
17305
  init_pidfile();
16737
17306
  init_ipc_client();
16738
- var DAEMON_ENTRY = (0, import_node_path16.join)(__dirname, "daemon.js");
17307
+ var DAEMON_ENTRY = (0, import_node_path18.join)(__dirname, "daemon.js");
16739
17308
  async function daemonForeground() {
16740
17309
  await runDaemon();
16741
17310
  }
@@ -16746,7 +17315,7 @@ async function daemonStart() {
16746
17315
  return;
16747
17316
  }
16748
17317
  ensureRuntimeDirs();
16749
- if (!(0, import_node_fs24.existsSync)(DAEMON_ENTRY)) {
17318
+ if (!(0, import_node_fs26.existsSync)(DAEMON_ENTRY)) {
16750
17319
  console.log(source_default.red(` Daemon entry not found at ${DAEMON_ENTRY}.`));
16751
17320
  console.log(source_default.dim(" Reinstall with `threatcrush update` or `pnpm run build` in the cli package."));
16752
17321
  return;
@@ -16754,8 +17323,8 @@ async function daemonStart() {
16754
17323
  let out;
16755
17324
  let err;
16756
17325
  try {
16757
- out = (0, import_node_fs25.openSync)(PATHS.logFile, "a");
16758
- err = (0, import_node_fs25.openSync)(PATHS.logFile, "a");
17326
+ out = (0, import_node_fs27.openSync)(PATHS.logFile, "a");
17327
+ err = (0, import_node_fs27.openSync)(PATHS.logFile, "a");
16759
17328
  } catch (e) {
16760
17329
  const code = e.code;
16761
17330
  console.log(source_default.red(` \u2717 Cannot write daemon log at ${PATHS.logFile} (${code ?? "error"}).`));
@@ -16834,19 +17403,19 @@ async function daemonStop() {
16834
17403
 
16835
17404
  // src/commands/service.ts
16836
17405
  var import_node_child_process8 = require("child_process");
16837
- var import_node_fs26 = require("fs");
16838
- var import_node_path17 = require("path");
17406
+ var import_node_fs28 = require("fs");
17407
+ var import_node_path19 = require("path");
16839
17408
  var UNIT_PATH = "/etc/systemd/system/threatcrushd.service";
16840
17409
  function resolveTemplate() {
16841
- const templatePath = (0, import_node_path17.join)(__dirname, "systemd", "threatcrushd.service");
16842
- if (!(0, import_node_fs26.existsSync)(templatePath)) {
17410
+ const templatePath = (0, import_node_path19.join)(__dirname, "systemd", "threatcrushd.service");
17411
+ if (!(0, import_node_fs28.existsSync)(templatePath)) {
16843
17412
  throw new Error(`systemd unit template not found at ${templatePath}`);
16844
17413
  }
16845
- return (0, import_node_fs26.readFileSync)(templatePath, "utf-8");
17414
+ return (0, import_node_fs28.readFileSync)(templatePath, "utf-8");
16846
17415
  }
16847
17416
  function resolveBinPath() {
16848
17417
  const arg = process.argv[1];
16849
- if (arg && (0, import_node_fs26.existsSync)(arg)) return arg;
17418
+ if (arg && (0, import_node_fs28.existsSync)(arg)) return arg;
16850
17419
  try {
16851
17420
  return (0, import_node_child_process8.execSync)("command -v threatcrush", { encoding: "utf-8" }).trim();
16852
17421
  } catch {
@@ -16867,7 +17436,7 @@ async function installServiceCommand() {
16867
17436
  return;
16868
17437
  }
16869
17438
  const unit = resolveTemplate().replace("{{BIN_PATH}}", resolveBinPath());
16870
- (0, import_node_fs26.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
17439
+ (0, import_node_fs28.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
16871
17440
  console.log(source_default.green(` \u2713 Installed unit file: ${UNIT_PATH}`));
16872
17441
  ensureSystemDirs();
16873
17442
  try {
@@ -16882,29 +17451,31 @@ async function installServiceCommand() {
16882
17451
  }
16883
17452
  function ensureSystemDirs() {
16884
17453
  const dirs = [
16885
- { path: "/etc/threatcrush" },
16886
- { path: "/etc/threatcrush/modules", sticky: true },
16887
- { path: "/etc/threatcrush/threatcrushd.conf.d" },
16888
- { path: "/var/log/threatcrush" },
16889
- { path: "/var/lib/threatcrush" },
16890
- { path: "/var/run/threatcrush" }
17454
+ { path: "/etc/threatcrush", groupWritable: true },
17455
+ { path: "/etc/threatcrush/modules", groupWritable: true, sticky: true },
17456
+ { path: "/etc/threatcrush/threatcrushd.conf.d", groupWritable: true },
17457
+ { path: "/var/log/threatcrush", groupWritable: false },
17458
+ { path: "/var/lib/threatcrush", groupWritable: false },
17459
+ { path: "/var/run/threatcrush", groupWritable: false }
16891
17460
  ];
16892
17461
  let admGid = null;
16893
17462
  try {
16894
- admGid = (0, import_node_fs26.statSync)("/var/log/auth.log").gid;
17463
+ admGid = (0, import_node_fs28.statSync)("/var/log/auth.log").gid;
16895
17464
  } catch {
16896
17465
  }
16897
- for (const { path, sticky } of dirs) {
17466
+ for (const { path, groupWritable, sticky } of dirs) {
16898
17467
  try {
16899
- (0, import_node_fs26.mkdirSync)(path, { recursive: true });
17468
+ (0, import_node_fs28.mkdirSync)(path, { recursive: true });
16900
17469
  } catch {
16901
17470
  }
16902
- if (admGid !== null) {
16903
- try {
16904
- (0, import_node_fs26.chmodSync)(path, sticky ? 1533 : 509);
17471
+ try {
17472
+ if (groupWritable && admGid !== null) {
17473
+ (0, import_node_fs28.chmodSync)(path, sticky ? 1533 : 509);
16905
17474
  (0, import_node_child_process8.execSync)(`chgrp adm ${path}`, { stdio: "ignore" });
16906
- } catch {
17475
+ } else {
17476
+ (0, import_node_fs28.chmodSync)(path, 493);
16907
17477
  }
17478
+ } catch {
16908
17479
  }
16909
17480
  }
16910
17481
  console.log(source_default.green(" \u2713 Runtime dirs prepared (group `adm` may install modules / edit config without sudo)."));
@@ -16928,7 +17499,7 @@ async function uninstallServiceCommand() {
16928
17499
  } catch {
16929
17500
  }
16930
17501
  try {
16931
- if ((0, import_node_fs26.existsSync)(UNIT_PATH)) {
17502
+ if ((0, import_node_fs28.existsSync)(UNIT_PATH)) {
16932
17503
  (0, import_node_child_process8.execSync)(`rm -f ${UNIT_PATH}`);
16933
17504
  console.log(source_default.green(` \u2713 Removed unit file: ${UNIT_PATH}`));
16934
17505
  }
@@ -17027,8 +17598,8 @@ function welcomeCommand() {
17027
17598
  }
17028
17599
 
17029
17600
  // src/commands/properties.ts
17030
- var import_node_fs27 = require("fs");
17031
- var import_node_path18 = require("path");
17601
+ var import_node_fs29 = require("fs");
17602
+ var import_node_path20 = require("path");
17032
17603
  var import_node_readline6 = __toESM(require("readline"));
17033
17604
  var API_URL7 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
17034
17605
  var KINDS = ["url", "api", "domain", "ip", "repo"];
@@ -17357,8 +17928,8 @@ async function propertiesRunsCommand(opts) {
17357
17928
  }
17358
17929
  }
17359
17930
  function parseImportFile(path) {
17360
- const ext = (0, import_node_path18.extname)(path).toLowerCase();
17361
- const raw = (0, import_node_fs27.readFileSync)(path, "utf-8");
17931
+ const ext = (0, import_node_path20.extname)(path).toLowerCase();
17932
+ const raw = (0, import_node_fs29.readFileSync)(path, "utf-8");
17362
17933
  if (ext === ".json") {
17363
17934
  const parsed = JSON.parse(raw);
17364
17935
  if (!Array.isArray(parsed)) throw new Error("JSON must be an array of objects");
@@ -17548,7 +18119,7 @@ async function rulesCommand(opts) {
17548
18119
  }
17549
18120
 
17550
18121
  // src/commands/harden.ts
17551
- var import_node_fs28 = require("fs");
18122
+ var import_node_fs30 = require("fs");
17552
18123
  var import_node_child_process9 = require("child_process");
17553
18124
  function tryExec(cmd) {
17554
18125
  try {
@@ -17559,7 +18130,7 @@ function tryExec(cmd) {
17559
18130
  }
17560
18131
  function tryRead(path) {
17561
18132
  try {
17562
- return (0, import_node_fs28.readFileSync)(path, "utf-8");
18133
+ return (0, import_node_fs30.readFileSync)(path, "utf-8");
17563
18134
  } catch {
17564
18135
  return null;
17565
18136
  }
@@ -17663,8 +18234,8 @@ function checkSshWeakConfig() {
17663
18234
  };
17664
18235
  }
17665
18236
  function checkAutoUpdates() {
17666
- const unattended = (0, import_node_fs28.existsSync)("/etc/apt/apt.conf.d/20auto-upgrades") || (0, import_node_fs28.existsSync)("/etc/apt/apt.conf.d/50unattended-upgrades");
17667
- const dnfAuto = (0, import_node_fs28.existsSync)("/etc/dnf/automatic.conf");
18237
+ const unattended = (0, import_node_fs30.existsSync)("/etc/apt/apt.conf.d/20auto-upgrades") || (0, import_node_fs30.existsSync)("/etc/apt/apt.conf.d/50unattended-upgrades");
18238
+ const dnfAuto = (0, import_node_fs30.existsSync)("/etc/dnf/automatic.conf");
17668
18239
  if (unattended || dnfAuto) {
17669
18240
  return {
17670
18241
  key: "auto-updates",
@@ -17790,7 +18361,7 @@ function checkFail2ban() {
17790
18361
  explanation: "fail2ban is installed and running."
17791
18362
  };
17792
18363
  }
17793
- if ((0, import_node_fs28.existsSync)("/etc/fail2ban/fail2ban.conf")) {
18364
+ if ((0, import_node_fs30.existsSync)("/etc/fail2ban/fail2ban.conf")) {
17794
18365
  return {
17795
18366
  key: checkKey,
17796
18367
  status: "warn",
@@ -18021,7 +18592,7 @@ async function allowlistCommand(opts) {
18021
18592
  init_paths();
18022
18593
  var PKG_VERSION2 = "0.1.8";
18023
18594
  try {
18024
- const pkg = JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path19.join)(__dirname, "..", "package.json"), "utf-8"));
18595
+ const pkg = JSON.parse((0, import_node_fs31.readFileSync)((0, import_node_path21.join)(__dirname, "..", "package.json"), "utf-8"));
18025
18596
  PKG_VERSION2 = pkg.version;
18026
18597
  } catch {
18027
18598
  }
@@ -18037,7 +18608,7 @@ ${source_default.dim(" C R U S H")}
18037
18608
  var API_URL8 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
18038
18609
  var PKG_NAME = "@profullstack/threatcrush";
18039
18610
  var DESKTOP_PKG_NAME = "@profullstack/threatcrush-desktop";
18040
- var INSTALL_CONFIG_PATH = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
18611
+ var INSTALL_CONFIG_PATH = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
18041
18612
  function detectPackageManager() {
18042
18613
  try {
18043
18614
  const npmGlobal = (0, import_node_child_process10.execSync)("npm ls -g --depth=0 --json 2>/dev/null", { encoding: "utf-8" });
@@ -18063,7 +18634,7 @@ function detectPackageManager() {
18063
18634
  }
18064
18635
  function readInstallConfig() {
18065
18636
  try {
18066
- return JSON.parse((0, import_node_fs29.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
18637
+ return JSON.parse((0, import_node_fs31.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
18067
18638
  } catch {
18068
18639
  return {};
18069
18640
  }
@@ -18248,7 +18819,7 @@ program2.command("uninstall-service").description("Remove the threatcrushd syste
18248
18819
  program2.command("logs").description("Tail daemon logs").action(async () => {
18249
18820
  console.log(LOGO2);
18250
18821
  const logPath = PATHS.logFile;
18251
- if (!(0, import_node_fs29.existsSync)(logPath)) {
18822
+ if (!(0, import_node_fs31.existsSync)(logPath)) {
18252
18823
  console.log(source_default.yellow(` No log file found at ${logPath}`));
18253
18824
  console.log(source_default.dim(" Start the daemon first with `threatcrush start`\n"));
18254
18825
  return;
@@ -18453,10 +19024,10 @@ storeCmd.command("search <query>").description("Search for modules in the store"
18453
19024
  });
18454
19025
  storeCmd.command("publish <url>").description("Publish a module from a git URL or web URL").action(async (url) => {
18455
19026
  console.log(LOGO2);
18456
- const configPath = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
19027
+ const configPath = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
18457
19028
  let email = "";
18458
19029
  try {
18459
- const config = JSON.parse((0, import_node_fs29.readFileSync)(configPath, "utf-8"));
19030
+ const config = JSON.parse((0, import_node_fs31.readFileSync)(configPath, "utf-8"));
18460
19031
  email = config.email || "";
18461
19032
  } catch {
18462
19033
  }
@@ -18473,9 +19044,9 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
18473
19044
  return;
18474
19045
  }
18475
19046
  try {
18476
- const dir = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush");
18477
- if (!(0, import_node_fs29.existsSync)(dir)) (0, import_node_fs29.mkdirSync)(dir, { recursive: true });
18478
- (0, import_node_fs29.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
19047
+ const dir = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush");
19048
+ if (!(0, import_node_fs31.existsSync)(dir)) (0, import_node_fs31.mkdirSync)(dir, { recursive: true });
19049
+ (0, import_node_fs31.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
18479
19050
  console.log(source_default.dim(` Saved email to ${configPath}`));
18480
19051
  } catch {
18481
19052
  }