@profullstack/threatcrush 0.2.1 → 0.2.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
@@ -3834,16 +3834,14 @@ var init_app = __esm({
3834
3834
  });
3835
3835
 
3836
3836
  // src/daemon/paths.ts
3837
- function canWriteSystemPaths() {
3838
- if (process.platform !== "linux") return false;
3839
- if (process.getuid && process.getuid() === 0) return true;
3840
- try {
3841
- if (!(0, import_node_fs.existsSync)("/etc/threatcrush")) return false;
3842
- (0, import_node_fs.mkdirSync)("/etc/threatcrush/.probe", { recursive: true });
3843
- return true;
3844
- } catch {
3845
- return false;
3846
- }
3837
+ function isRoot() {
3838
+ return process.platform === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
3839
+ }
3840
+ function resolveClientSocket() {
3841
+ if ((0, import_node_fs.existsSync)(PATHS.socket)) return PATHS.socket;
3842
+ const other = PATHS.mode === "system" ? USER_PATHS.socket : SYSTEM_PATHS.socket;
3843
+ if ((0, import_node_fs.existsSync)(other)) return other;
3844
+ return PATHS.socket;
3847
3845
  }
3848
3846
  function ensureRuntimeDirs() {
3849
3847
  for (const dir of [PATHS.configDir, PATHS.confD, PATHS.moduleDir, PATHS.logDir, PATHS.stateDir, PATHS.runDir]) {
@@ -3853,16 +3851,15 @@ function ensureRuntimeDirs() {
3853
3851
  }
3854
3852
  }
3855
3853
  }
3856
- var import_node_fs, import_node_os2, import_node_path, systemMode, userBase, PATHS;
3854
+ var import_node_fs, import_node_os2, import_node_path, userBase, SYSTEM_PATHS, USER_PATHS, PATHS;
3857
3855
  var init_paths = __esm({
3858
3856
  "src/daemon/paths.ts"() {
3859
3857
  "use strict";
3860
3858
  import_node_fs = require("fs");
3861
3859
  import_node_os2 = require("os");
3862
3860
  import_node_path = require("path");
3863
- systemMode = canWriteSystemPaths();
3864
3861
  userBase = (0, import_node_path.join)((0, import_node_os2.homedir)(), ".threatcrush");
3865
- PATHS = systemMode ? {
3862
+ SYSTEM_PATHS = {
3866
3863
  mode: "system",
3867
3864
  configDir: "/etc/threatcrush",
3868
3865
  configFile: "/etc/threatcrush/threatcrushd.conf",
@@ -3875,7 +3872,8 @@ var init_paths = __esm({
3875
3872
  runDir: "/var/run/threatcrush",
3876
3873
  pidFile: "/var/run/threatcrush/threatcrushd.pid",
3877
3874
  socket: "/var/run/threatcrush/threatcrushd.sock"
3878
- } : {
3875
+ };
3876
+ USER_PATHS = {
3879
3877
  mode: "user",
3880
3878
  configDir: userBase,
3881
3879
  configFile: (0, import_node_path.join)(userBase, "threatcrushd.conf"),
@@ -3889,6 +3887,7 @@ var init_paths = __esm({
3889
3887
  pidFile: (0, import_node_path.join)(userBase, "run", "threatcrushd.pid"),
3890
3888
  socket: (0, import_node_path.join)(userBase, "run", "threatcrushd.sock")
3891
3889
  };
3890
+ PATHS = isRoot() ? SYSTEM_PATHS : USER_PATHS;
3892
3891
  }
3893
3892
  });
3894
3893
 
@@ -3903,7 +3902,7 @@ var init_ipc_client = __esm({
3903
3902
  IpcClient = class {
3904
3903
  constructor(opts = {}) {
3905
3904
  this.opts = opts;
3906
- this.socketPath = opts.socketPath || PATHS.socket;
3905
+ this.socketPath = opts.socketPath || resolveClientSocket();
3907
3906
  }
3908
3907
  opts;
3909
3908
  socket = null;
@@ -3911,7 +3910,7 @@ var init_ipc_client = __esm({
3911
3910
  nextId = 1;
3912
3911
  pending = /* @__PURE__ */ new Map();
3913
3912
  socketPath;
3914
- static isDaemonRunning(socketPath = PATHS.socket) {
3913
+ static isDaemonRunning(socketPath = resolveClientSocket()) {
3915
3914
  return (0, import_node_fs2.existsSync)(socketPath);
3916
3915
  }
3917
3916
  async connect(timeoutMs = 2e3) {
@@ -4037,8 +4036,9 @@ function isProcessAlive(pid) {
4037
4036
  try {
4038
4037
  process.kill(pid, 0);
4039
4038
  return true;
4040
- } catch {
4041
- return false;
4039
+ } catch (err) {
4040
+ const code = err.code;
4041
+ return code === "EPERM";
4042
4042
  }
4043
4043
  }
4044
4044
  function findRunningDaemon() {
@@ -8259,9 +8259,9 @@ var source_default = chalk;
8259
8259
 
8260
8260
  // src/index.ts
8261
8261
  var import_readline = __toESM(require("readline"));
8262
- var import_node_child_process7 = require("child_process");
8263
- var import_node_fs22 = require("fs");
8264
- var import_node_path15 = require("path");
8262
+ var import_node_child_process10 = require("child_process");
8263
+ var import_node_fs27 = require("fs");
8264
+ var import_node_path16 = require("path");
8265
8265
  var import_node_os8 = require("os");
8266
8266
 
8267
8267
  // src/commands/monitor.ts
@@ -8454,16 +8454,41 @@ async function monitorCommand(options) {
8454
8454
  banner();
8455
8455
  logger.info("Starting foreground monitor...");
8456
8456
  const moduleFilter = options.module?.split(",").map((m) => m.trim());
8457
- const availableSources = LOG_SOURCES.filter((s) => {
8458
- if (moduleFilter && !moduleFilter.includes(s.name)) return false;
8459
- return (0, import_node_fs4.existsSync)(s.path);
8460
- });
8457
+ const availableSources = [];
8458
+ const unreadable = [];
8459
+ for (const s of LOG_SOURCES) {
8460
+ if (moduleFilter && !moduleFilter.includes(s.name)) continue;
8461
+ if (!(0, import_node_fs4.existsSync)(s.path)) continue;
8462
+ try {
8463
+ (0, import_node_fs4.accessSync)(s.path, import_node_fs4.constants.R_OK);
8464
+ availableSources.push(s);
8465
+ } catch {
8466
+ unreadable.push(s);
8467
+ }
8468
+ }
8469
+ if (unreadable.length > 0) {
8470
+ logger.warn(`Skipping ${unreadable.length} unreadable log source(s):`);
8471
+ for (const s of unreadable) {
8472
+ console.log(` ${source_default.yellow("!")} ${source_default.gray(s.path)} (permission denied \u2014 add yourself to the 'adm' group or run as root)`);
8473
+ }
8474
+ console.log();
8475
+ }
8461
8476
  if (availableSources.length === 0) {
8462
- logger.warn("No log files found to monitor.");
8477
+ logger.warn("No readable log files found to monitor.");
8463
8478
  logger.info("Available log paths checked:");
8464
8479
  for (const s of LOG_SOURCES) {
8465
8480
  const exists = (0, import_node_fs4.existsSync)(s.path);
8466
- console.log(` ${exists ? source_default.green("\u2713") : source_default.red("\u2717")} ${s.path}`);
8481
+ let readable = false;
8482
+ if (exists) {
8483
+ try {
8484
+ (0, import_node_fs4.accessSync)(s.path, import_node_fs4.constants.R_OK);
8485
+ readable = true;
8486
+ } catch {
8487
+ readable = false;
8488
+ }
8489
+ }
8490
+ const glyph = !exists ? source_default.red("\u2717 missing") : readable ? source_default.green("\u2713 readable") : source_default.yellow("! no read perm");
8491
+ console.log(` ${glyph} ${s.path}`);
8467
8492
  }
8468
8493
  console.log();
8469
8494
  logger.info("Starting demo mode with synthetic events...\n");
@@ -8498,7 +8523,13 @@ function tailLog(source) {
8498
8523
  return;
8499
8524
  }
8500
8525
  const stream = (0, import_node_fs4.createReadStream)(path, { start: position, encoding: "utf-8" });
8526
+ stream.on("error", (err) => {
8527
+ logger.warn(`stopped tailing ${path}: ${err.code || err.message}`);
8528
+ position = currentStat.size;
8529
+ });
8501
8530
  const rl = (0, import_node_readline.createInterface)({ input: stream });
8531
+ rl.on("error", () => {
8532
+ });
8502
8533
  rl.on("line", (line) => {
8503
8534
  if (!line.trim()) return;
8504
8535
  processLine(line, name, category);
@@ -9607,6 +9638,8 @@ async function runScan(targetPath) {
9607
9638
  try {
9608
9639
  scanDirectory(targetPath, targetPath, findings, () => {
9609
9640
  });
9641
+ const depFindings = await scanDependencies(targetPath);
9642
+ findings.push(...depFindings);
9610
9643
  } catch (err) {
9611
9644
  return {
9612
9645
  type: "scan",
@@ -9776,6 +9809,92 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
9776
9809
  }
9777
9810
  }
9778
9811
  }
9812
+ async function scanDependencies(targetPath) {
9813
+ const findings = [];
9814
+ const lockfiles = [
9815
+ { file: "package-lock.json", ecosystem: "npm" },
9816
+ { file: "pnpm-lock.yaml", ecosystem: "npm" },
9817
+ { file: "yarn.lock", ecosystem: "npm" },
9818
+ { file: "requirements.txt", ecosystem: "PyPI" },
9819
+ { file: "Pipfile.lock", ecosystem: "PyPI" }
9820
+ ];
9821
+ for (const { file, ecosystem } of lockfiles) {
9822
+ const lockPath = (0, import_node_path2.join)(targetPath, file);
9823
+ if (!(0, import_node_fs5.existsSync)(lockPath)) continue;
9824
+ try {
9825
+ const deps = parseDependencies(lockPath, file, ecosystem);
9826
+ for (const dep of deps.slice(0, 50)) {
9827
+ try {
9828
+ const vulns = await queryOsv(dep.name, dep.version, ecosystem);
9829
+ for (const vuln of vulns) {
9830
+ const cvssScore = vuln.severity?.find((s) => s.type === "CVSS_V3")?.score;
9831
+ const severity = cvssScore ? parseFloat(cvssScore) >= 9 ? "critical" : parseFloat(cvssScore) >= 7 ? "high" : parseFloat(cvssScore) >= 4 ? "medium" : "low" : "medium";
9832
+ findings.push({
9833
+ file,
9834
+ line: 0,
9835
+ type: "Dependency CVE",
9836
+ severity,
9837
+ message: `${dep.name}@${dep.version}: ${vuln.summary || vuln.id}`,
9838
+ snippet: `${vuln.id}${cvssScore ? ` (CVSS: ${cvssScore})` : ""}`
9839
+ });
9840
+ }
9841
+ } catch {
9842
+ }
9843
+ }
9844
+ } catch {
9845
+ }
9846
+ }
9847
+ return findings;
9848
+ }
9849
+ function parseDependencies(lockPath, filename, ecosystem) {
9850
+ const deps = [];
9851
+ if (filename === "package-lock.json") {
9852
+ try {
9853
+ const lock = JSON.parse((0, import_node_fs5.readFileSync)(lockPath, "utf-8"));
9854
+ const packages = lock.packages || lock.dependencies || {};
9855
+ for (const [key, value] of Object.entries(packages)) {
9856
+ const name = key.replace(/^node_modules\//, "");
9857
+ const version = value.version;
9858
+ if (name && version && !name.startsWith(".")) {
9859
+ deps.push({ name, version });
9860
+ }
9861
+ }
9862
+ } catch {
9863
+ }
9864
+ } else if (filename === "requirements.txt") {
9865
+ try {
9866
+ const content = (0, import_node_fs5.readFileSync)(lockPath, "utf-8");
9867
+ for (const line of content.split("\n")) {
9868
+ const match = line.match(/^([a-zA-Z0-9_.-]+)==([0-9.]+)/);
9869
+ if (match) deps.push({ name: match[1], version: match[2] });
9870
+ }
9871
+ } catch {
9872
+ }
9873
+ }
9874
+ return deps;
9875
+ }
9876
+ function isValidPackageName(name) {
9877
+ return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
9878
+ }
9879
+ function isValidVersion(version) {
9880
+ return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
9881
+ }
9882
+ async function queryOsv(name, version, ecosystem) {
9883
+ if (!isValidPackageName(name) || !isValidVersion(version)) return [];
9884
+ try {
9885
+ const res = await fetch("https://api.osv.dev/v1/query", {
9886
+ method: "POST",
9887
+ headers: { "Content-Type": "application/json" },
9888
+ body: JSON.stringify({ package: { name, ecosystem }, version }),
9889
+ signal: AbortSignal.timeout(5e3)
9890
+ });
9891
+ if (!res.ok) return [];
9892
+ const data = await res.json();
9893
+ return data.vulns || [];
9894
+ } catch {
9895
+ return [];
9896
+ }
9897
+ }
9779
9898
 
9780
9899
  // src/commands/init.ts
9781
9900
  var import_node_fs8 = require("fs");
@@ -10591,7 +10710,16 @@ async function modulesInstallCommand(source) {
10591
10710
  `));
10592
10711
  return;
10593
10712
  }
10594
- if (install.git_url) {
10713
+ if (install.npm_package) {
10714
+ const npmSpinner = ora({ text: `Installing ${install.npm_package}...`, color: "green" }).start();
10715
+ try {
10716
+ (0, import_node_child_process2.execSync)(`npm install -g ${install.npm_package}`, { stdio: "pipe" });
10717
+ npmSpinner.succeed(`Package ${install.npm_package} installed globally`);
10718
+ } catch (err) {
10719
+ npmSpinner.fail(`npm install failed: ${err.message}`);
10720
+ return;
10721
+ }
10722
+ } else if (install.git_url) {
10595
10723
  const cloneSpinner = ora({ text: "Cloning module repository...", color: "green" }).start();
10596
10724
  try {
10597
10725
  (0, import_node_child_process2.execSync)(`git clone --depth 1 ${install.git_url} ${dest}`, { stdio: "pipe" });
@@ -10609,15 +10737,6 @@ async function modulesInstallCommand(source) {
10609
10737
  return;
10610
10738
  }
10611
10739
  cloneSpinner.succeed(`Installed ${mod.name} v${mod.version} \u2192 ${dest}`);
10612
- } else if (install.npm_package) {
10613
- const npmSpinner = ora({ text: `Installing ${install.npm_package}...`, color: "green" }).start();
10614
- try {
10615
- (0, import_node_child_process2.execSync)(`npm install -g ${install.npm_package}`, { stdio: "pipe" });
10616
- npmSpinner.succeed(`Package ${install.npm_package} installed globally`);
10617
- } catch (err) {
10618
- npmSpinner.fail(`npm install failed: ${err.message}`);
10619
- return;
10620
- }
10621
10740
  } else if (install.tarball_url) {
10622
10741
  const dlSpinner = ora({ text: "Downloading module tarball...", color: "green" }).start();
10623
10742
  try {
@@ -10758,6 +10877,39 @@ var PENTEST_CHECKS = [
10758
10877
  test: (html) => /stack trace|traceback|exception|at\s+\w+\.\w+\(/i.test(html),
10759
10878
  severity: "medium",
10760
10879
  message: "Error page reveals internal information"
10880
+ },
10881
+ // PRD 07: Additional checks
10882
+ {
10883
+ name: "CORS Misconfiguration",
10884
+ test: (_url, _body, headers) => {
10885
+ const acao = headers["access-control-allow-origin"];
10886
+ return acao === "*" || acao === "null";
10887
+ },
10888
+ severity: "medium",
10889
+ message: "CORS allows any origin (Access-Control-Allow-Origin: *)"
10890
+ },
10891
+ {
10892
+ name: "Cookie Security",
10893
+ test: (_url, _body, headers) => {
10894
+ const setCookie = headers["set-cookie"] || "";
10895
+ return setCookie.length > 0 && (!setCookie.includes("HttpOnly") || !setCookie.includes("Secure"));
10896
+ },
10897
+ severity: "medium",
10898
+ message: "Cookies missing HttpOnly or Secure flags"
10899
+ },
10900
+ {
10901
+ name: "Content Security Policy",
10902
+ test: (_url, _body, headers) => {
10903
+ return !headers["content-security-policy"];
10904
+ },
10905
+ severity: "low",
10906
+ message: "No Content-Security-Policy header set"
10907
+ },
10908
+ {
10909
+ name: "Sensitive Path Exposure",
10910
+ test: (html) => /\.env|wp-admin|phpinfo|\.git\/config|server-status/i.test(html),
10911
+ severity: "high",
10912
+ message: "Response references sensitive paths or admin endpoints"
10761
10913
  }
10762
10914
  ];
10763
10915
  async function runPentest(rawUrl) {
@@ -11423,14 +11575,14 @@ async function sshConnect(options) {
11423
11575
  }
11424
11576
 
11425
11577
  // src/commands/daemon.ts
11426
- var import_node_child_process5 = require("child_process");
11427
- var import_node_fs18 = require("fs");
11428
- var import_node_path12 = require("path");
11429
- var import_node_fs19 = require("fs");
11578
+ var import_node_child_process7 = require("child_process");
11579
+ var import_node_fs22 = require("fs");
11580
+ var import_node_path13 = require("path");
11581
+ var import_node_fs23 = require("fs");
11430
11582
 
11431
11583
  // src/daemon/index.ts
11432
- var import_node_fs17 = require("fs");
11433
- var import_node_path11 = require("path");
11584
+ var import_node_fs21 = require("fs");
11585
+ var import_node_path12 = require("path");
11434
11586
  init_paths();
11435
11587
  init_pidfile();
11436
11588
 
@@ -11493,10 +11645,19 @@ var IpcServer = class {
11493
11645
  this.server = (0, import_node_net2.createServer)((sock) => this.handleClient(sock));
11494
11646
  this.server.on("error", reject);
11495
11647
  this.server.listen(PATHS.socket, () => {
11648
+ const nodeFs = require("fs");
11496
11649
  try {
11497
- require("fs").chmodSync(PATHS.socket, 432);
11650
+ nodeFs.chmodSync(PATHS.socket, 432);
11498
11651
  } catch {
11499
11652
  }
11653
+ const isRoot3 = process.platform === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
11654
+ if (isRoot3) {
11655
+ try {
11656
+ const { gid } = nodeFs.statSync("/var/log/auth.log");
11657
+ nodeFs.chownSync(PATHS.socket, 0, gid);
11658
+ } catch {
11659
+ }
11660
+ }
11500
11661
  resolve3();
11501
11662
  });
11502
11663
  });
@@ -11625,7 +11786,7 @@ var IpcServer = class {
11625
11786
  };
11626
11787
 
11627
11788
  // src/daemon/module-host.ts
11628
- var import_node_fs16 = require("fs");
11789
+ var import_node_fs18 = require("fs");
11629
11790
  var import_node_path10 = require("path");
11630
11791
  var import_node_url = require("url");
11631
11792
  var import_toml4 = __toESM(require_toml());
@@ -11780,8 +11941,16 @@ var JournalWatcher = class _JournalWatcher {
11780
11941
  buffer = "";
11781
11942
  moduleName = "user-journal";
11782
11943
  active = false;
11944
+ // When the daemon runs as root (system mode), tail the SYSTEM journal so
11945
+ // we pick up sshd / sudo / kernel / UFW events. Falling back to --user
11946
+ // would give us root's mostly-empty per-user journal. Otherwise we use
11947
+ // --user so the daemon can run unprivileged on a workstation.
11948
+ static scopeArgs() {
11949
+ const isRoot3 = process.platform === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
11950
+ return isRoot3 ? [] : ["--user"];
11951
+ }
11783
11952
  static isAvailable() {
11784
- const probe = (0, import_node_child_process4.spawnSync)("journalctl", ["--user", "-n", "0", "--no-pager"], {
11953
+ const probe = (0, import_node_child_process4.spawnSync)("journalctl", [...this.scopeArgs(), "-n", "0", "--no-pager"], {
11785
11954
  stdio: ["ignore", "ignore", "ignore"]
11786
11955
  });
11787
11956
  return probe.status === 0;
@@ -11790,7 +11959,7 @@ var JournalWatcher = class _JournalWatcher {
11790
11959
  if (!_JournalWatcher.isAvailable()) return false;
11791
11960
  const child = (0, import_node_child_process4.spawn)(
11792
11961
  "journalctl",
11793
- ["--user", "-o", "json", "-f", "--since", "now"],
11962
+ [..._JournalWatcher.scopeArgs(), "-o", "json", "-f", "--since", "now"],
11794
11963
  { stdio: ["ignore", "pipe", "pipe"] }
11795
11964
  );
11796
11965
  if (!child.stdout) return false;
@@ -11880,98 +12049,511 @@ function realtimeToDate(rt) {
11880
12049
  return new Date(Math.floor(us / 1e3));
11881
12050
  }
11882
12051
 
11883
- // src/daemon/module-host.ts
12052
+ // src/modules/network-monitor/index.ts
12053
+ var import_node_child_process5 = require("child_process");
12054
+ var import_node_fs16 = require("fs");
11884
12055
  init_state();
11885
- var ModuleHost = class {
12056
+ var NetworkMonitor = class {
11886
12057
  constructor(bus2) {
11887
12058
  this.bus = bus2;
11888
- bus2.on("event", (event) => {
11889
- const mod = this.modules.get(event.module);
11890
- if (mod) mod.events++;
11891
- for (const hosted of this.modules.values()) {
11892
- if (hosted.status !== "running" || !hosted.instance?.onEvent) continue;
11893
- void hosted.instance.onEvent(event).catch((err) => {
11894
- hosted.status = "error";
11895
- hosted.detail = `onEvent failed: ${String(err.message || err)}`;
11896
- this.bus.announceModule(hosted.name, "error", hosted.detail);
11897
- });
11898
- }
11899
- });
11900
12059
  }
11901
12060
  bus;
11902
- modules = /* @__PURE__ */ new Map();
11903
- logWatcher = null;
11904
- journalWatcher = null;
11905
- async start() {
11906
- this.registerBuiltins();
11907
- await this.discoverAndStartInstalled();
11908
- this.logWatcher = new LogWatcher(this.bus);
11909
- const watched = this.logWatcher.start();
11910
- for (const modName of this.logWatcher.activeModules()) {
11911
- const mod = this.modules.get(modName);
11912
- if (mod) {
11913
- mod.status = "running";
11914
- mod.detail = `watching ${watched.length} log source(s)`;
11915
- this.bus.announceModule(modName, "running", mod.detail);
12061
+ active = false;
12062
+ pollTimer = null;
12063
+ scanTrackers = /* @__PURE__ */ new Map();
12064
+ halfOpenTrackers = /* @__PURE__ */ new Map();
12065
+ lastConnections = /* @__PURE__ */ new Set();
12066
+ // Config
12067
+ pollIntervalMs = 5e3;
12068
+ portScanThreshold = 10;
12069
+ // unique ports in window
12070
+ portScanWindowMs = 3e4;
12071
+ synFloodThreshold = 50;
12072
+ // half-open connections
12073
+ synFloodWindowMs = 1e4;
12074
+ start() {
12075
+ if (!this.hasConntrackOrSs()) {
12076
+ return false;
12077
+ }
12078
+ this.active = true;
12079
+ this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
12080
+ return true;
12081
+ }
12082
+ stop() {
12083
+ if (this.pollTimer) clearInterval(this.pollTimer);
12084
+ this.pollTimer = null;
12085
+ this.active = false;
12086
+ }
12087
+ isActive() {
12088
+ return this.active;
12089
+ }
12090
+ hasConntrackOrSs() {
12091
+ const ss = (0, import_node_child_process5.spawnSync)("ss", ["--version"], { stdio: "pipe" });
12092
+ if (ss.status === 0) return true;
12093
+ return (0, import_node_fs16.existsSync)("/proc/net/tcp");
12094
+ }
12095
+ poll() {
12096
+ try {
12097
+ const connections = this.getConnections();
12098
+ this.analyzePortScans(connections);
12099
+ this.analyzeSynFlood(connections);
12100
+ this.cleanupTrackers();
12101
+ } catch {
12102
+ }
12103
+ }
12104
+ getConnections() {
12105
+ const records = [];
12106
+ const now = Date.now();
12107
+ try {
12108
+ const ct = (0, import_node_child_process5.spawnSync)("conntrack", ["-L", "-p", "tcp", "-o", "extended"], {
12109
+ encoding: "utf-8",
12110
+ stdio: ["pipe", "pipe", "pipe"],
12111
+ timeout: 3e3
12112
+ });
12113
+ if (ct.status === 0 && ct.stdout) {
12114
+ for (const line of ct.stdout.split("\n")) {
12115
+ const srcMatch = line.match(/src=(\d+\.\d+\.\d+\.\d+)/);
12116
+ const dportMatch = line.match(/dport=(\d+)/);
12117
+ if (srcMatch && dportMatch) {
12118
+ records.push({ source_ip: srcMatch[1], dest_port: parseInt(dportMatch[1]), timestamp: now });
12119
+ }
12120
+ }
12121
+ if (records.length > 0) return records;
11916
12122
  }
12123
+ } catch {
11917
12124
  }
11918
- this.journalWatcher = new JournalWatcher(this.bus);
11919
- if (this.journalWatcher.start()) {
11920
- const mod = this.modules.get("user-journal");
11921
- if (mod) {
11922
- mod.status = "running";
11923
- mod.detail = "tailing journalctl --user";
11924
- this.bus.announceModule("user-journal", "running", mod.detail);
12125
+ try {
12126
+ const ss = (0, import_node_child_process5.spawnSync)("ss", ["-tnp", "-H"], {
12127
+ encoding: "utf-8",
12128
+ stdio: ["pipe", "pipe", "pipe"],
12129
+ timeout: 3e3
12130
+ });
12131
+ if (ss.status === 0 && ss.stdout) {
12132
+ for (const line of ss.stdout.split("\n")) {
12133
+ const parts = line.trim().split(/\s+/);
12134
+ if (parts.length < 5) continue;
12135
+ const peerParts = parts[4].split(":");
12136
+ const localParts = parts[3].split(":");
12137
+ if (peerParts.length >= 2 && localParts.length >= 2) {
12138
+ const sourceIp = peerParts.slice(0, -1).join(":");
12139
+ const destPort = parseInt(localParts[localParts.length - 1]);
12140
+ if (sourceIp && !isNaN(destPort) && !this.isLocalIp(sourceIp)) {
12141
+ records.push({ source_ip: sourceIp, dest_port: destPort, timestamp: now });
12142
+ }
12143
+ }
12144
+ }
11925
12145
  }
12146
+ } catch {
11926
12147
  }
12148
+ return records;
11927
12149
  }
11928
- async stop() {
11929
- this.logWatcher?.stop();
11930
- this.journalWatcher?.stop();
11931
- for (const mod of this.modules.values()) {
11932
- try {
11933
- if (mod.instance && mod.status === "running") {
11934
- await mod.instance.stop();
12150
+ analyzePortScans(connections) {
12151
+ const now = Date.now();
12152
+ for (const conn of connections) {
12153
+ const key = conn.source_ip;
12154
+ let tracker = this.scanTrackers.get(key);
12155
+ if (!tracker) {
12156
+ tracker = { ports: /* @__PURE__ */ new Set(), firstSeen: now, lastSeen: now, count: 0 };
12157
+ this.scanTrackers.set(key, tracker);
12158
+ }
12159
+ tracker.ports.add(conn.dest_port);
12160
+ tracker.lastSeen = now;
12161
+ tracker.count++;
12162
+ if (tracker.ports.size >= this.portScanThreshold && now - tracker.firstSeen <= this.portScanWindowMs) {
12163
+ this.emitEvent(
12164
+ "high",
12165
+ `Port scan detected: ${conn.source_ip} probed ${tracker.ports.size} ports in ${Math.round((now - tracker.firstSeen) / 1e3)}s`,
12166
+ conn.source_ip,
12167
+ { ports_scanned: tracker.ports.size, window_seconds: Math.round((now - tracker.firstSeen) / 1e3) }
12168
+ );
12169
+ this.scanTrackers.delete(key);
12170
+ }
12171
+ }
12172
+ }
12173
+ analyzeSynFlood(connections) {
12174
+ try {
12175
+ const ss = (0, import_node_child_process5.spawnSync)("ss", ["-tn", "state", "syn-recv", "-H"], {
12176
+ encoding: "utf-8",
12177
+ stdio: ["pipe", "pipe", "pipe"],
12178
+ timeout: 3e3
12179
+ });
12180
+ if (ss.status !== 0 || !ss.stdout) return;
12181
+ const perSource = /* @__PURE__ */ new Map();
12182
+ for (const line of ss.stdout.split("\n")) {
12183
+ const parts = line.trim().split(/\s+/);
12184
+ if (parts.length < 5) continue;
12185
+ const peer = parts[4].split(":");
12186
+ const ip = peer.slice(0, -1).join(":");
12187
+ if (ip) perSource.set(ip, (perSource.get(ip) || 0) + 1);
12188
+ }
12189
+ for (const [ip, count] of perSource) {
12190
+ if (count >= this.synFloodThreshold) {
12191
+ this.emitEvent(
12192
+ "critical",
12193
+ `SYN flood indicators: ${count} half-open connections from ${ip}`,
12194
+ ip,
12195
+ { half_open_count: count }
12196
+ );
11935
12197
  }
11936
- } catch (err) {
11937
- mod.status = "error";
11938
- mod.detail = `stop failed: ${String(err.message || err)}`;
11939
- this.bus.announceModule(mod.name, "error", mod.detail);
11940
- continue;
11941
12198
  }
11942
- mod.status = "loaded";
11943
- this.bus.announceModule(mod.name, "stopped");
12199
+ } catch {
11944
12200
  }
11945
12201
  }
11946
- summary() {
11947
- return [...this.modules.values()].map((m) => ({
11948
- name: m.name,
11949
- status: m.status,
11950
- events: m.events,
11951
- detail: m.detail
11952
- }));
12202
+ emitEvent(severity, message, sourceIp, details) {
12203
+ const event = {
12204
+ timestamp: /* @__PURE__ */ new Date(),
12205
+ module: "network-monitor",
12206
+ category: "network",
12207
+ severity,
12208
+ message,
12209
+ source_ip: sourceIp,
12210
+ details
12211
+ };
12212
+ try {
12213
+ insertEvent(event);
12214
+ } catch {
12215
+ }
12216
+ this.bus.publish(event);
11953
12217
  }
11954
- registerBuiltins() {
11955
- const builtins = [
11956
- { name: "log-watcher", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
11957
- { name: "ssh-guard", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
11958
- { name: "user-journal", version: "0.1.0", source: "builtin", status: "loaded", events: 0 }
11959
- ];
11960
- for (const m of builtins) this.modules.set(m.name, m);
12218
+ cleanupTrackers() {
12219
+ const now = Date.now();
12220
+ for (const [key, tracker] of this.scanTrackers) {
12221
+ if (now - tracker.lastSeen > this.portScanWindowMs * 2) {
12222
+ this.scanTrackers.delete(key);
12223
+ }
12224
+ }
11961
12225
  }
11962
- async discoverAndStartInstalled() {
11963
- if (!(0, import_node_fs16.existsSync)(PATHS.moduleDir)) return;
11964
- const configs = loadModuleConfigs(PATHS.confD);
11965
- const entries = (0, import_node_fs16.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
11966
- for (const entry of entries) {
11967
- if (!entry.isDirectory()) continue;
11968
- const manifestPath = (0, import_node_path10.join)(PATHS.moduleDir, entry.name, "mod.toml");
11969
- if (!(0, import_node_fs16.existsSync)(manifestPath)) continue;
12226
+ isLocalIp(ip) {
12227
+ return ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0" || ip.startsWith("::ffff:127.");
12228
+ }
12229
+ };
12230
+
12231
+ // src/modules/dns-monitor/index.ts
12232
+ var import_node_fs17 = require("fs");
12233
+ var import_node_readline5 = require("readline");
12234
+ init_state();
12235
+ var DNS_LOG_SOURCES = [
12236
+ "/var/log/syslog",
12237
+ // systemd-resolved logs here
12238
+ "/var/log/dnsmasq.log",
12239
+ // dnsmasq
12240
+ "/var/log/named/queries.log",
12241
+ // bind9
12242
+ "/var/log/pihole.log"
12243
+ // Pi-hole
12244
+ ];
12245
+ var DnsMonitor = class {
12246
+ // Shannon entropy threshold for DGA
12247
+ constructor(bus2) {
12248
+ this.bus = bus2;
12249
+ }
12250
+ bus;
12251
+ active = false;
12252
+ timers = /* @__PURE__ */ new Map();
12253
+ positions = /* @__PURE__ */ new Map();
12254
+ // Tracking windows
12255
+ txtQueryCounts = /* @__PURE__ */ new Map();
12256
+ domainBuffer = [];
12257
+ // Config
12258
+ txtRateThreshold = 20;
12259
+ // TXT queries per source per window
12260
+ txtWindowMs = 6e4;
12261
+ dgaBurstThreshold = 15;
12262
+ // unique high-entropy domains per window
12263
+ dgaWindowMs = 6e4;
12264
+ entropyThreshold = 3.5;
12265
+ start() {
12266
+ const sources = DNS_LOG_SOURCES.filter((p) => {
12267
+ if (!(0, import_node_fs17.existsSync)(p)) return false;
11970
12268
  try {
11971
- const manifest = import_toml4.default.parse((0, import_node_fs16.readFileSync)(manifestPath, "utf-8"));
11972
- const name = manifest.module?.name || entry.name;
11973
- const defaults = manifest.module?.config?.defaults || {};
11974
- const config = {
12269
+ (0, import_node_fs17.accessSync)(p, import_node_fs17.constants.R_OK);
12270
+ return true;
12271
+ } catch {
12272
+ return false;
12273
+ }
12274
+ });
12275
+ if (sources.length === 0) return false;
12276
+ this.active = true;
12277
+ for (const src of sources) {
12278
+ this.tailLog(src);
12279
+ }
12280
+ setInterval(() => this.analyzeBuffer(), 1e4);
12281
+ return true;
12282
+ }
12283
+ stop() {
12284
+ for (const t of this.timers.values()) clearInterval(t);
12285
+ this.timers.clear();
12286
+ this.active = false;
12287
+ }
12288
+ isActive() {
12289
+ return this.active;
12290
+ }
12291
+ tailLog(path) {
12292
+ try {
12293
+ this.positions.set(path, (0, import_node_fs17.statSync)(path).size);
12294
+ } catch {
12295
+ this.positions.set(path, 0);
12296
+ }
12297
+ const timer = setInterval(() => this.pollLog(path), 2e3);
12298
+ this.timers.set(path, timer);
12299
+ }
12300
+ pollLog(path) {
12301
+ let stat;
12302
+ try {
12303
+ stat = (0, import_node_fs17.statSync)(path);
12304
+ } catch {
12305
+ return;
12306
+ }
12307
+ const prev = this.positions.get(path) ?? 0;
12308
+ if (stat.size < prev) {
12309
+ this.positions.set(path, 0);
12310
+ return;
12311
+ }
12312
+ if (stat.size === prev) return;
12313
+ const stream = (0, import_node_fs17.createReadStream)(path, { start: prev, encoding: "utf-8" });
12314
+ stream.on("error", () => this.positions.set(path, stat.size));
12315
+ const rl = (0, import_node_readline5.createInterface)({ input: stream });
12316
+ rl.on("line", (line) => this.parseDnsLine(line));
12317
+ rl.on("close", () => this.positions.set(path, stat.size));
12318
+ }
12319
+ parseDnsLine(line) {
12320
+ const resolvedMatch = line.match(/query\[(\w+)\]\s+(\S+)\s+from\s+(\S+)/i);
12321
+ if (resolvedMatch) {
12322
+ this.domainBuffer.push({
12323
+ type: resolvedMatch[1],
12324
+ domain: resolvedMatch[2],
12325
+ source_ip: resolvedMatch[3],
12326
+ timestamp: Date.now()
12327
+ });
12328
+ return;
12329
+ }
12330
+ const dnsmasqMatch = line.match(/query\[(\w+)\]\s+(\S+)\s+from\s+(\S+)/i);
12331
+ if (dnsmasqMatch) {
12332
+ this.domainBuffer.push({
12333
+ type: dnsmasqMatch[1],
12334
+ domain: dnsmasqMatch[2],
12335
+ source_ip: dnsmasqMatch[3],
12336
+ timestamp: Date.now()
12337
+ });
12338
+ return;
12339
+ }
12340
+ const genericMatch = line.match(/(?:query|lookup|resolve)[:\s]+(\S+)/i);
12341
+ if (genericMatch) {
12342
+ const typeMatch = line.match(/type[:\s]+(\w+)/i);
12343
+ this.domainBuffer.push({
12344
+ type: typeMatch?.[1] || "A",
12345
+ domain: genericMatch[1],
12346
+ timestamp: Date.now()
12347
+ });
12348
+ }
12349
+ }
12350
+ analyzeBuffer() {
12351
+ const now = Date.now();
12352
+ const cutoff = now - this.txtWindowMs;
12353
+ this.domainBuffer = this.domainBuffer.filter((q) => q.timestamp > cutoff);
12354
+ this.detectTunneling();
12355
+ this.detectDga();
12356
+ }
12357
+ detectTunneling() {
12358
+ const txtBySource = /* @__PURE__ */ new Map();
12359
+ const longLabelDomains = [];
12360
+ for (const q of this.domainBuffer) {
12361
+ if (q.type === "TXT") {
12362
+ const key = q.source_ip || "unknown";
12363
+ txtBySource.set(key, (txtBySource.get(key) || 0) + 1);
12364
+ }
12365
+ const labels = q.domain.split(".");
12366
+ const maxLabel = Math.max(...labels.map((l) => l.length));
12367
+ if (maxLabel > 50) {
12368
+ longLabelDomains.push(q.domain);
12369
+ }
12370
+ }
12371
+ for (const [source, count] of txtBySource) {
12372
+ if (count >= this.txtRateThreshold) {
12373
+ this.emitEvent(
12374
+ "high",
12375
+ `DNS tunneling indicators: ${count} TXT queries from ${source} in ${this.txtWindowMs / 1e3}s`,
12376
+ source !== "unknown" ? source : void 0,
12377
+ { txt_query_count: count, type: "tunneling" }
12378
+ );
12379
+ }
12380
+ }
12381
+ if (longLabelDomains.length >= 5) {
12382
+ this.emitEvent(
12383
+ "high",
12384
+ `DNS tunneling: ${longLabelDomains.length} queries with abnormally long labels detected`,
12385
+ void 0,
12386
+ { domains: longLabelDomains.slice(0, 5), type: "tunneling-labels" }
12387
+ );
12388
+ }
12389
+ }
12390
+ detectDga() {
12391
+ const highEntropyDomains = [];
12392
+ for (const q of this.domainBuffer) {
12393
+ const domain = q.domain.toLowerCase();
12394
+ const parts = domain.split(".");
12395
+ if (parts.length < 2) continue;
12396
+ const sld = parts[parts.length - 2];
12397
+ if (sld.length >= 8 && this.shannonEntropy(sld) >= this.entropyThreshold) {
12398
+ highEntropyDomains.push(domain);
12399
+ }
12400
+ }
12401
+ const unique = [...new Set(highEntropyDomains)];
12402
+ if (unique.length >= this.dgaBurstThreshold) {
12403
+ this.emitEvent(
12404
+ "critical",
12405
+ `DGA-like domain burst: ${unique.length} unique high-entropy domains detected`,
12406
+ void 0,
12407
+ { sample_domains: unique.slice(0, 10), type: "dga", unique_count: unique.length }
12408
+ );
12409
+ }
12410
+ }
12411
+ shannonEntropy(str) {
12412
+ const freq = /* @__PURE__ */ new Map();
12413
+ for (const ch of str) {
12414
+ freq.set(ch, (freq.get(ch) || 0) + 1);
12415
+ }
12416
+ let entropy = 0;
12417
+ for (const count of freq.values()) {
12418
+ const p = count / str.length;
12419
+ if (p > 0) entropy -= p * Math.log2(p);
12420
+ }
12421
+ return entropy;
12422
+ }
12423
+ emitEvent(severity, message, sourceIp, details) {
12424
+ const event = {
12425
+ timestamp: /* @__PURE__ */ new Date(),
12426
+ module: "dns-monitor",
12427
+ category: "network",
12428
+ severity,
12429
+ message,
12430
+ source_ip: sourceIp,
12431
+ details
12432
+ };
12433
+ try {
12434
+ insertEvent(event);
12435
+ } catch {
12436
+ }
12437
+ this.bus.publish(event);
12438
+ }
12439
+ };
12440
+
12441
+ // src/daemon/module-host.ts
12442
+ init_state();
12443
+ var ModuleHost = class {
12444
+ constructor(bus2) {
12445
+ this.bus = bus2;
12446
+ bus2.on("event", (event) => {
12447
+ const mod = this.modules.get(event.module);
12448
+ if (mod) mod.events++;
12449
+ for (const hosted of this.modules.values()) {
12450
+ if (hosted.status !== "running" || !hosted.instance?.onEvent) continue;
12451
+ void hosted.instance.onEvent(event).catch((err) => {
12452
+ hosted.status = "error";
12453
+ hosted.detail = `onEvent failed: ${String(err.message || err)}`;
12454
+ this.bus.announceModule(hosted.name, "error", hosted.detail);
12455
+ });
12456
+ }
12457
+ });
12458
+ }
12459
+ bus;
12460
+ modules = /* @__PURE__ */ new Map();
12461
+ logWatcher = null;
12462
+ journalWatcher = null;
12463
+ networkMonitor = null;
12464
+ dnsMonitor = null;
12465
+ async start() {
12466
+ this.registerBuiltins();
12467
+ await this.discoverAndStartInstalled();
12468
+ this.logWatcher = new LogWatcher(this.bus);
12469
+ const watched = this.logWatcher.start();
12470
+ for (const modName of this.logWatcher.activeModules()) {
12471
+ const mod = this.modules.get(modName);
12472
+ if (mod) {
12473
+ mod.status = "running";
12474
+ mod.detail = `watching ${watched.length} log source(s)`;
12475
+ this.bus.announceModule(modName, "running", mod.detail);
12476
+ }
12477
+ }
12478
+ this.journalWatcher = new JournalWatcher(this.bus);
12479
+ if (this.journalWatcher.start()) {
12480
+ const mod = this.modules.get("user-journal");
12481
+ if (mod) {
12482
+ mod.status = "running";
12483
+ mod.detail = `tailing ${JournalWatcher.scopeArgs().includes("--user") ? "user journal" : "system journal"}`;
12484
+ this.bus.announceModule("user-journal", "running", mod.detail);
12485
+ }
12486
+ }
12487
+ this.networkMonitor = new NetworkMonitor(this.bus);
12488
+ if (this.networkMonitor.start()) {
12489
+ const nmod = this.modules.get("network-monitor");
12490
+ if (nmod) {
12491
+ nmod.status = "running";
12492
+ nmod.detail = "monitoring connections via conntrack/ss";
12493
+ this.bus.announceModule("network-monitor", "running", nmod.detail);
12494
+ }
12495
+ }
12496
+ this.dnsMonitor = new DnsMonitor(this.bus);
12497
+ if (this.dnsMonitor.start()) {
12498
+ const dmod = this.modules.get("dns-monitor");
12499
+ if (dmod) {
12500
+ dmod.status = "running";
12501
+ dmod.detail = "monitoring DNS queries";
12502
+ this.bus.announceModule("dns-monitor", "running", dmod.detail);
12503
+ }
12504
+ }
12505
+ }
12506
+ async stop() {
12507
+ this.logWatcher?.stop();
12508
+ this.journalWatcher?.stop();
12509
+ this.networkMonitor?.stop();
12510
+ this.dnsMonitor?.stop();
12511
+ for (const mod of this.modules.values()) {
12512
+ try {
12513
+ if (mod.instance && mod.status === "running") {
12514
+ await mod.instance.stop();
12515
+ }
12516
+ } catch (err) {
12517
+ mod.status = "error";
12518
+ mod.detail = `stop failed: ${String(err.message || err)}`;
12519
+ this.bus.announceModule(mod.name, "error", mod.detail);
12520
+ continue;
12521
+ }
12522
+ mod.status = "loaded";
12523
+ this.bus.announceModule(mod.name, "stopped");
12524
+ }
12525
+ }
12526
+ summary() {
12527
+ return [...this.modules.values()].map((m) => ({
12528
+ name: m.name,
12529
+ status: m.status,
12530
+ events: m.events,
12531
+ detail: m.detail
12532
+ }));
12533
+ }
12534
+ registerBuiltins() {
12535
+ const builtins = [
12536
+ { name: "log-watcher", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
12537
+ { name: "ssh-guard", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
12538
+ { name: "user-journal", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
12539
+ { name: "network-monitor", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
12540
+ { name: "dns-monitor", version: "0.1.0", source: "builtin", status: "loaded", events: 0 }
12541
+ ];
12542
+ for (const m of builtins) this.modules.set(m.name, m);
12543
+ }
12544
+ async discoverAndStartInstalled() {
12545
+ if (!(0, import_node_fs18.existsSync)(PATHS.moduleDir)) return;
12546
+ const configs = loadModuleConfigs(PATHS.confD);
12547
+ const entries = (0, import_node_fs18.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
12548
+ for (const entry of entries) {
12549
+ if (!entry.isDirectory()) continue;
12550
+ const manifestPath = (0, import_node_path10.join)(PATHS.moduleDir, entry.name, "mod.toml");
12551
+ if (!(0, import_node_fs18.existsSync)(manifestPath)) continue;
12552
+ try {
12553
+ const manifest = import_toml4.default.parse((0, import_node_fs18.readFileSync)(manifestPath, "utf-8"));
12554
+ const name = manifest.module?.name || entry.name;
12555
+ const defaults = manifest.module?.config?.defaults || {};
12556
+ const config = {
11975
12557
  enabled: true,
11976
12558
  ...defaults,
11977
12559
  ...configs.get(name) || {}
@@ -12031,15 +12613,15 @@ var ModuleHost = class {
12031
12613
  installedEntrypoint(modulePath) {
12032
12614
  const packageJson = (0, import_node_path10.join)(modulePath, "package.json");
12033
12615
  const candidates = [];
12034
- if ((0, import_node_fs16.existsSync)(packageJson)) {
12616
+ if ((0, import_node_fs18.existsSync)(packageJson)) {
12035
12617
  try {
12036
- const pkg = JSON.parse((0, import_node_fs16.readFileSync)(packageJson, "utf-8"));
12618
+ const pkg = JSON.parse((0, import_node_fs18.readFileSync)(packageJson, "utf-8"));
12037
12619
  if (pkg.main) candidates.push((0, import_node_path10.join)(modulePath, pkg.main));
12038
12620
  } catch {
12039
12621
  }
12040
12622
  }
12041
12623
  candidates.push((0, import_node_path10.join)(modulePath, "dist", "index.js"), (0, import_node_path10.join)(modulePath, "index.js"));
12042
- return candidates.find((candidate) => (0, import_node_fs16.existsSync)(candidate)) || null;
12624
+ return candidates.find((candidate) => (0, import_node_fs18.existsSync)(candidate)) || null;
12043
12625
  }
12044
12626
  isThreatCrushModule(value) {
12045
12627
  return Boolean(
@@ -12143,6 +12725,98 @@ function smtpChannel(config) {
12143
12725
  };
12144
12726
  }
12145
12727
 
12728
+ // src/daemon/alerts/discord.ts
12729
+ var SEVERITY_RANK2 = {
12730
+ info: 0,
12731
+ low: 1,
12732
+ medium: 2,
12733
+ high: 3,
12734
+ critical: 4
12735
+ };
12736
+ var SEVERITY_COLORS2 = {
12737
+ info: 3066993,
12738
+ // green
12739
+ low: 3447003,
12740
+ // blue
12741
+ medium: 15965202,
12742
+ // orange
12743
+ high: 15158332,
12744
+ // red
12745
+ critical: 10181046
12746
+ // purple
12747
+ };
12748
+ function discordChannel(config) {
12749
+ return async (event) => {
12750
+ if (config.min_severity) {
12751
+ const eventRank = SEVERITY_RANK2[event.severity] ?? 0;
12752
+ const minRank = SEVERITY_RANK2[config.min_severity] ?? 0;
12753
+ if (eventRank < minRank) return;
12754
+ }
12755
+ const embed = {
12756
+ title: `${event.severity === "critical" ? "\u{1F6A8}" : "\u26A0\uFE0F"} [${event.severity.toUpperCase()}] ${event.module}`,
12757
+ description: event.message,
12758
+ color: SEVERITY_COLORS2[event.severity] ?? 16777215,
12759
+ fields: [
12760
+ ...event.source_ip ? [{ name: "Source IP", value: `\`${event.source_ip}\``, inline: true }] : [],
12761
+ { name: "Category", value: event.category, inline: true },
12762
+ { name: "Time", value: event.timestamp.toISOString(), inline: true }
12763
+ ],
12764
+ footer: { text: "ThreatCrush Security Alert" }
12765
+ };
12766
+ await fetch(config.webhook_url, {
12767
+ method: "POST",
12768
+ headers: { "Content-Type": "application/json" },
12769
+ body: JSON.stringify({ embeds: [embed] })
12770
+ });
12771
+ };
12772
+ }
12773
+
12774
+ // src/daemon/alerts/pagerduty.ts
12775
+ var SEVERITY_RANK3 = {
12776
+ info: 0,
12777
+ low: 1,
12778
+ medium: 2,
12779
+ high: 3,
12780
+ critical: 4
12781
+ };
12782
+ var PD_SEVERITY = {
12783
+ info: "info",
12784
+ low: "info",
12785
+ medium: "warning",
12786
+ high: "error",
12787
+ critical: "critical"
12788
+ };
12789
+ function pagerdutyChannel(config) {
12790
+ return async (event) => {
12791
+ if (config.min_severity) {
12792
+ const eventRank = SEVERITY_RANK3[event.severity] ?? 0;
12793
+ const minRank = SEVERITY_RANK3[config.min_severity] ?? 0;
12794
+ if (eventRank < minRank) return;
12795
+ }
12796
+ const payload = {
12797
+ routing_key: config.routing_key,
12798
+ event_action: "trigger",
12799
+ payload: {
12800
+ summary: `[${event.severity.toUpperCase()}] ${event.module}: ${event.message}`,
12801
+ source: "threatcrush",
12802
+ severity: PD_SEVERITY[event.severity] || "warning",
12803
+ timestamp: event.timestamp.toISOString(),
12804
+ custom_details: {
12805
+ module: event.module,
12806
+ category: event.category,
12807
+ source_ip: event.source_ip,
12808
+ details: event.details
12809
+ }
12810
+ }
12811
+ };
12812
+ await fetch("https://events.pagerduty.com/v2/enqueue", {
12813
+ method: "POST",
12814
+ headers: { "Content-Type": "application/json" },
12815
+ body: JSON.stringify(payload)
12816
+ });
12817
+ };
12818
+ }
12819
+
12146
12820
  // src/daemon/alerts/index.ts
12147
12821
  var AlertDispatcher = class {
12148
12822
  constructor(bus2, config) {
@@ -12156,6 +12830,7 @@ var AlertDispatcher = class {
12156
12830
  bus;
12157
12831
  config;
12158
12832
  channels = [];
12833
+ rateLimits = /* @__PURE__ */ new Map();
12159
12834
  bindChannels() {
12160
12835
  const alerts = this.config.alerts || {};
12161
12836
  for (const [name, raw] of Object.entries(alerts)) {
@@ -12170,11 +12845,31 @@ var AlertDispatcher = class {
12170
12845
  if (name === "email" && typeof cfg.host === "string" && typeof cfg.from === "string") {
12171
12846
  this.channels.push(smtpChannel(cfg));
12172
12847
  }
12848
+ if (name === "discord" && typeof cfg.webhook_url === "string") {
12849
+ this.channels.push(discordChannel(cfg));
12850
+ }
12851
+ if (name === "pagerduty" && typeof cfg.routing_key === "string") {
12852
+ this.channels.push(pagerdutyChannel(cfg));
12853
+ }
12173
12854
  }
12174
12855
  }
12856
+ checkRateLimit(channelIdx, maxPerHour = 60) {
12857
+ const key = String(channelIdx);
12858
+ const now = Date.now();
12859
+ const hour = 36e5;
12860
+ let timestamps = this.rateLimits.get(key) || [];
12861
+ timestamps = timestamps.filter((t) => t > now - hour);
12862
+ if (timestamps.length >= maxPerHour) return false;
12863
+ timestamps.push(now);
12864
+ this.rateLimits.set(key, timestamps);
12865
+ return true;
12866
+ }
12175
12867
  async dispatch(event) {
12176
- await Promise.all(this.channels.map((ch) => ch(event).catch(() => {
12177
- })));
12868
+ await Promise.all(this.channels.map((ch, idx) => {
12869
+ if (!this.checkRateLimit(idx)) return Promise.resolve();
12870
+ return ch(event).catch(() => {
12871
+ });
12872
+ }));
12178
12873
  }
12179
12874
  };
12180
12875
  function webhookChannel(url, secret) {
@@ -12309,28 +13004,741 @@ var RunsWorker = class {
12309
13004
  };
12310
13005
  }
12311
13006
  }
12312
- async finalize(orgId, claimed, result) {
13007
+ async finalize(orgId, claimed, result) {
13008
+ try {
13009
+ await fetch(
13010
+ `${API_URL6}/api/orgs/${orgId}/properties/${claimed.property_id}/runs/${claimed.id}`,
13011
+ {
13012
+ method: "PATCH",
13013
+ headers: authHeaders(),
13014
+ body: JSON.stringify({
13015
+ status: result.error ? "failed" : "succeeded",
13016
+ findings_count: result.findings.length,
13017
+ severity_summary: result.severity_summary,
13018
+ summary: result.summary,
13019
+ findings: result.findings,
13020
+ error: result.error,
13021
+ source: "daemon",
13022
+ worker_id: workerId()
13023
+ })
13024
+ }
13025
+ );
13026
+ } catch {
13027
+ } finally {
13028
+ this.bus.announceModule("runs-worker", "idle");
13029
+ }
13030
+ }
13031
+ };
13032
+
13033
+ // src/daemon/rules/engine.ts
13034
+ var RuleEngine = class {
13035
+ constructor(onDetection) {
13036
+ this.onDetection = onDetection;
13037
+ }
13038
+ onDetection;
13039
+ rules = [];
13040
+ windows = /* @__PURE__ */ new Map();
13041
+ loadRules(rules) {
13042
+ this.rules = rules.filter((r) => r.enabled !== false);
13043
+ }
13044
+ getRules() {
13045
+ return [...this.rules];
13046
+ }
13047
+ evaluate(event) {
13048
+ const now = Date.now();
13049
+ for (const rule of this.rules) {
13050
+ if (rule.source_types.length > 0 && !rule.source_types.includes(event.module) && !rule.source_types.includes(event.category)) {
13051
+ continue;
13052
+ }
13053
+ if (!this.matchesCondition(event, rule.match)) continue;
13054
+ const windowKey = `${rule.id}:${event.source_ip || "global"}`;
13055
+ let window = this.windows.get(windowKey);
13056
+ if (!window) {
13057
+ window = { events: [], lastAlert: 0 };
13058
+ this.windows.set(windowKey, window);
13059
+ }
13060
+ window.events.push({ timestamp: now, event });
13061
+ const cutoff = now - rule.window_seconds * 1e3;
13062
+ window.events = window.events.filter((e) => e.timestamp >= cutoff);
13063
+ if (window.events.length < rule.threshold) continue;
13064
+ if (window.lastAlert > 0 && now - window.lastAlert < rule.cooldown_seconds * 1e3) continue;
13065
+ window.lastAlert = now;
13066
+ window.events = [];
13067
+ this.onDetection({
13068
+ rule_id: rule.id,
13069
+ severity: rule.severity,
13070
+ title: rule.title,
13071
+ description: `${rule.description} (${rule.threshold} events in ${rule.window_seconds}s)`,
13072
+ source_ip: event.source_ip,
13073
+ username: event.details?.user || void 0,
13074
+ raw_metadata: {
13075
+ rule_version: rule.version,
13076
+ tags: rule.tags,
13077
+ category: rule.category,
13078
+ remediation: rule.remediation
13079
+ }
13080
+ });
13081
+ }
13082
+ }
13083
+ matchesCondition(event, match) {
13084
+ const fieldValue = this.getFieldValue(event, match.field);
13085
+ if (fieldValue === void 0) return false;
13086
+ const strValue = String(fieldValue);
13087
+ let result = false;
13088
+ switch (match.operator) {
13089
+ case "contains":
13090
+ result = strValue.toLowerCase().includes(String(match.value).toLowerCase());
13091
+ break;
13092
+ case "regex":
13093
+ try {
13094
+ result = new RegExp(String(match.value), "i").test(strValue);
13095
+ } catch {
13096
+ result = false;
13097
+ }
13098
+ break;
13099
+ case "equals":
13100
+ result = strValue === String(match.value);
13101
+ break;
13102
+ case "starts_with":
13103
+ result = strValue.startsWith(String(match.value));
13104
+ break;
13105
+ case "ends_with":
13106
+ result = strValue.endsWith(String(match.value));
13107
+ break;
13108
+ }
13109
+ if (result && match.and) {
13110
+ result = match.and.every((m) => this.matchesCondition(event, m));
13111
+ }
13112
+ if (!result && match.or) {
13113
+ result = match.or.some((m) => this.matchesCondition(event, m));
13114
+ }
13115
+ return result;
13116
+ }
13117
+ getFieldValue(event, field) {
13118
+ switch (field) {
13119
+ case "message":
13120
+ return event.message;
13121
+ case "severity":
13122
+ return event.severity;
13123
+ case "module":
13124
+ return event.module;
13125
+ case "category":
13126
+ return event.category;
13127
+ case "source_ip":
13128
+ return event.source_ip;
13129
+ default:
13130
+ return event.details?.[field];
13131
+ }
13132
+ }
13133
+ // Periodic cleanup of stale windows
13134
+ cleanup() {
13135
+ const now = Date.now();
13136
+ for (const [key, window] of this.windows.entries()) {
13137
+ if (window.events.length === 0 && now - window.lastAlert > 36e5) {
13138
+ this.windows.delete(key);
13139
+ }
13140
+ }
13141
+ }
13142
+ };
13143
+
13144
+ // src/daemon/rules/loader.ts
13145
+ var import_node_fs19 = require("fs");
13146
+ var import_node_path11 = require("path");
13147
+
13148
+ // src/daemon/rules/default-rules.ts
13149
+ var DEFAULT_RULES = [
13150
+ {
13151
+ id: "ssh-brute-force",
13152
+ title: "SSH Brute Force Detected",
13153
+ description: "Multiple failed SSH login attempts from the same source",
13154
+ version: "1.0.0",
13155
+ category: "auth",
13156
+ severity: "high",
13157
+ source_types: ["ssh-guard", "auth"],
13158
+ match: {
13159
+ field: "message",
13160
+ operator: "regex",
13161
+ value: "failed ssh login|invalid ssh user"
13162
+ },
13163
+ threshold: 5,
13164
+ window_seconds: 300,
13165
+ cooldown_seconds: 600,
13166
+ tags: ["ssh", "brute-force", "credential-stuffing"],
13167
+ remediation: {
13168
+ action: "block",
13169
+ ttl_seconds: 3600,
13170
+ description: "Block source IP for 1 hour"
13171
+ },
13172
+ enabled: true
13173
+ },
13174
+ {
13175
+ id: "ssh-success-after-failures",
13176
+ title: "SSH Login After Failed Attempts",
13177
+ description: "Successful SSH login from an IP that had recent failures",
13178
+ version: "1.0.0",
13179
+ category: "auth",
13180
+ severity: "critical",
13181
+ source_types: ["ssh-guard", "auth"],
13182
+ match: {
13183
+ field: "message",
13184
+ operator: "contains",
13185
+ value: "SSH login accepted"
13186
+ },
13187
+ threshold: 1,
13188
+ window_seconds: 60,
13189
+ cooldown_seconds: 300,
13190
+ tags: ["ssh", "compromise-indicator"],
13191
+ enabled: true
13192
+ },
13193
+ {
13194
+ id: "ssh-root-login",
13195
+ title: "Root SSH Login Attempt",
13196
+ description: "Direct root login via SSH detected",
13197
+ version: "1.0.0",
13198
+ category: "auth",
13199
+ severity: "high",
13200
+ source_types: ["ssh-guard", "auth"],
13201
+ match: {
13202
+ field: "message",
13203
+ operator: "regex",
13204
+ value: "(failed|accepted).*\\broot\\b"
13205
+ },
13206
+ threshold: 1,
13207
+ window_seconds: 60,
13208
+ cooldown_seconds: 300,
13209
+ tags: ["ssh", "root-access"],
13210
+ remediation: {
13211
+ action: "block",
13212
+ ttl_seconds: 7200,
13213
+ description: "Block source IP attempting root login"
13214
+ },
13215
+ enabled: true
13216
+ },
13217
+ {
13218
+ id: "ssh-user-enumeration",
13219
+ title: "SSH User Enumeration",
13220
+ description: "Multiple SSH attempts with different usernames from same source",
13221
+ version: "1.0.0",
13222
+ category: "auth",
13223
+ severity: "high",
13224
+ source_types: ["ssh-guard", "auth"],
13225
+ match: {
13226
+ field: "message",
13227
+ operator: "contains",
13228
+ value: "Invalid SSH user"
13229
+ },
13230
+ threshold: 3,
13231
+ window_seconds: 120,
13232
+ cooldown_seconds: 600,
13233
+ tags: ["ssh", "enumeration", "reconnaissance"],
13234
+ remediation: {
13235
+ action: "block",
13236
+ ttl_seconds: 3600,
13237
+ description: "Block source IP performing user enumeration"
13238
+ },
13239
+ enabled: true
13240
+ },
13241
+ {
13242
+ id: "sudo-abuse",
13243
+ title: "Sudo Authentication Failure",
13244
+ description: "Repeated sudo authentication failures",
13245
+ version: "1.0.0",
13246
+ category: "auth",
13247
+ severity: "high",
13248
+ source_types: ["user-journal", "system"],
13249
+ match: {
13250
+ field: "message",
13251
+ operator: "regex",
13252
+ value: "sudo.*authentication failure|sudo.*incorrect password|sudo.*FAILED"
13253
+ },
13254
+ threshold: 3,
13255
+ window_seconds: 300,
13256
+ cooldown_seconds: 600,
13257
+ tags: ["sudo", "privilege-escalation"],
13258
+ enabled: true
13259
+ },
13260
+ {
13261
+ id: "web-sqli-attack",
13262
+ title: "SQL Injection Attack Detected",
13263
+ description: "HTTP request with SQL injection patterns",
13264
+ version: "1.0.0",
13265
+ category: "web",
13266
+ severity: "critical",
13267
+ source_types: ["log-watcher", "web"],
13268
+ match: {
13269
+ field: "message",
13270
+ operator: "contains",
13271
+ value: "Attack detected [SQLI]"
13272
+ },
13273
+ threshold: 1,
13274
+ window_seconds: 60,
13275
+ cooldown_seconds: 300,
13276
+ tags: ["web", "sqli", "injection"],
13277
+ remediation: {
13278
+ action: "block",
13279
+ ttl_seconds: 3600,
13280
+ description: "Block source IP performing SQL injection"
13281
+ },
13282
+ enabled: true
13283
+ },
13284
+ {
13285
+ id: "web-path-traversal",
13286
+ title: "Path Traversal Attack Detected",
13287
+ description: "HTTP request with path traversal patterns",
13288
+ version: "1.0.0",
13289
+ category: "web",
13290
+ severity: "critical",
13291
+ source_types: ["log-watcher", "web"],
13292
+ match: {
13293
+ field: "message",
13294
+ operator: "contains",
13295
+ value: "Attack detected [PATH_TRAVERSAL]"
13296
+ },
13297
+ threshold: 1,
13298
+ window_seconds: 60,
13299
+ cooldown_seconds: 300,
13300
+ tags: ["web", "path-traversal", "lfi"],
13301
+ remediation: {
13302
+ action: "block",
13303
+ ttl_seconds: 3600,
13304
+ description: "Block source IP performing path traversal"
13305
+ },
13306
+ enabled: true
13307
+ },
13308
+ {
13309
+ id: "web-xss-attack",
13310
+ title: "XSS Attack Detected",
13311
+ description: "HTTP request with cross-site scripting patterns",
13312
+ version: "1.0.0",
13313
+ category: "web",
13314
+ severity: "high",
13315
+ source_types: ["log-watcher", "web"],
13316
+ match: {
13317
+ field: "message",
13318
+ operator: "regex",
13319
+ value: "Attack detected \\[XSS\\]"
13320
+ },
13321
+ threshold: 1,
13322
+ window_seconds: 60,
13323
+ cooldown_seconds: 300,
13324
+ tags: ["web", "xss", "injection"],
13325
+ remediation: {
13326
+ action: "block",
13327
+ ttl_seconds: 3600,
13328
+ description: "Block source IP performing XSS attack"
13329
+ },
13330
+ enabled: true
13331
+ },
13332
+ {
13333
+ id: "web-scanner-detection",
13334
+ title: "Web Vulnerability Scanner Detected",
13335
+ description: "High volume of 4xx errors suggesting automated scanning",
13336
+ version: "1.0.0",
13337
+ category: "web",
13338
+ severity: "medium",
13339
+ source_types: ["log-watcher", "web"],
13340
+ match: {
13341
+ field: "message",
13342
+ operator: "regex",
13343
+ value: "Client error 4\\d{2}:"
13344
+ },
13345
+ threshold: 20,
13346
+ window_seconds: 60,
13347
+ cooldown_seconds: 600,
13348
+ tags: ["web", "scanner", "reconnaissance"],
13349
+ remediation: {
13350
+ action: "block",
13351
+ ttl_seconds: 1800,
13352
+ description: "Block automated scanner"
13353
+ },
13354
+ enabled: true
13355
+ },
13356
+ {
13357
+ id: "port-scan-indicator",
13358
+ title: "Port Scan Indicators",
13359
+ description: "Connection attempts to many ports from a single source",
13360
+ version: "1.0.0",
13361
+ category: "network",
13362
+ severity: "medium",
13363
+ source_types: ["network-monitor", "network"],
13364
+ match: {
13365
+ field: "message",
13366
+ operator: "contains",
13367
+ value: "port scan"
13368
+ },
13369
+ threshold: 1,
13370
+ window_seconds: 60,
13371
+ cooldown_seconds: 300,
13372
+ tags: ["network", "port-scan", "reconnaissance"],
13373
+ remediation: {
13374
+ action: "block",
13375
+ ttl_seconds: 3600,
13376
+ description: "Block port scanner"
13377
+ },
13378
+ enabled: true
13379
+ },
13380
+ {
13381
+ id: "system-critical-error",
13382
+ title: "Critical System Error",
13383
+ description: "Critical or emergency level system log message",
13384
+ version: "1.0.0",
13385
+ category: "system",
13386
+ severity: "critical",
13387
+ source_types: ["user-journal", "system"],
13388
+ match: {
13389
+ field: "severity",
13390
+ operator: "equals",
13391
+ value: "critical"
13392
+ },
13393
+ threshold: 1,
13394
+ window_seconds: 60,
13395
+ cooldown_seconds: 300,
13396
+ tags: ["system", "critical"],
13397
+ enabled: true
13398
+ },
13399
+ {
13400
+ id: "exploit-probe-pattern",
13401
+ title: "Exploit Probe Pattern",
13402
+ description: "HTTP requests matching common exploit probe patterns",
13403
+ version: "1.0.0",
13404
+ category: "web",
13405
+ severity: "high",
13406
+ source_types: ["log-watcher", "web"],
13407
+ match: {
13408
+ field: "message",
13409
+ operator: "regex",
13410
+ value: "Attack detected \\[(CMD_INJECTION|RCE|SSRF|XXE)\\]"
13411
+ },
13412
+ threshold: 1,
13413
+ window_seconds: 60,
13414
+ cooldown_seconds: 300,
13415
+ tags: ["web", "exploit", "probe"],
13416
+ remediation: {
13417
+ action: "block",
13418
+ ttl_seconds: 7200,
13419
+ description: "Block source IP performing exploit probes"
13420
+ },
13421
+ enabled: true
13422
+ }
13423
+ ];
13424
+
13425
+ // src/daemon/rules/loader.ts
13426
+ var RULES_DIR = "/etc/threatcrush/rules.d";
13427
+ function loadAllRules(customDir) {
13428
+ const rules = [...DEFAULT_RULES];
13429
+ const dir = customDir || RULES_DIR;
13430
+ if ((0, import_node_fs19.existsSync)(dir)) {
13431
+ const files = (0, import_node_fs19.readdirSync)(dir).filter((f) => f.endsWith(".json"));
13432
+ for (const file of files) {
13433
+ try {
13434
+ const raw = (0, import_node_fs19.readFileSync)((0, import_node_path11.join)(dir, file), "utf-8");
13435
+ const parsed = JSON.parse(raw);
13436
+ const customRules = Array.isArray(parsed) ? parsed : [parsed];
13437
+ for (const rule of customRules) {
13438
+ if (!rule.id || !rule.title || !rule.match) {
13439
+ console.warn(`[rules] skipping invalid rule in ${file}: missing required fields`);
13440
+ continue;
13441
+ }
13442
+ const existingIdx = rules.findIndex((r) => r.id === rule.id);
13443
+ if (existingIdx >= 0) {
13444
+ rules[existingIdx] = { ...rules[existingIdx], ...rule };
13445
+ } else {
13446
+ rules.push(rule);
13447
+ }
13448
+ }
13449
+ } catch (err) {
13450
+ console.warn(`[rules] failed to load ${file}: ${err.message}`);
13451
+ }
13452
+ }
13453
+ }
13454
+ return rules;
13455
+ }
13456
+
13457
+ // src/daemon/firewall/adapters.ts
13458
+ var import_node_child_process6 = require("child_process");
13459
+ var NftablesAdapter = class {
13460
+ name = "nftables";
13461
+ table = "threatcrush";
13462
+ set = "blocklist";
13463
+ isAvailable() {
13464
+ const result = (0, import_node_child_process6.spawnSync)("nft", ["--version"], { stdio: "pipe" });
13465
+ return result.status === 0;
13466
+ }
13467
+ ensureSetup() {
13468
+ try {
13469
+ (0, import_node_child_process6.execSync)(`nft list table inet ${this.table} 2>/dev/null`, { stdio: "pipe" });
13470
+ } catch {
13471
+ (0, import_node_child_process6.execSync)(`nft add table inet ${this.table}`);
13472
+ (0, import_node_child_process6.execSync)(`nft add set inet ${this.table} ${this.set} '{ type ipv4_addr; flags timeout; }'`);
13473
+ (0, import_node_child_process6.execSync)(`nft add chain inet ${this.table} input '{ type filter hook input priority -1; policy accept; }'`);
13474
+ (0, import_node_child_process6.execSync)(`nft add rule inet ${this.table} input ip saddr @${this.set} drop`);
13475
+ }
13476
+ }
13477
+ async block(ip) {
13478
+ this.ensureSetup();
13479
+ (0, import_node_child_process6.execSync)(`nft add element inet ${this.table} ${this.set} '{ ${ip} }'`);
13480
+ }
13481
+ async unblock(ip) {
13482
+ try {
13483
+ (0, import_node_child_process6.execSync)(`nft delete element inet ${this.table} ${this.set} '{ ${ip} }'`);
13484
+ } catch {
13485
+ }
13486
+ }
13487
+ async isBlocked(ip) {
13488
+ try {
13489
+ const output = (0, import_node_child_process6.execSync)(`nft list set inet ${this.table} ${this.set}`, { encoding: "utf-8" });
13490
+ return output.includes(ip);
13491
+ } catch {
13492
+ return false;
13493
+ }
13494
+ }
13495
+ async listBlocked() {
13496
+ try {
13497
+ const output = (0, import_node_child_process6.execSync)(`nft list set inet ${this.table} ${this.set}`, { encoding: "utf-8" });
13498
+ const match = output.match(/elements\s*=\s*\{([^}]*)\}/);
13499
+ if (!match) return [];
13500
+ return match[1].split(",").map((s) => s.trim().split(/\s/)[0]).filter(Boolean);
13501
+ } catch {
13502
+ return [];
13503
+ }
13504
+ }
13505
+ };
13506
+ var IptablesAdapter = class {
13507
+ name = "iptables";
13508
+ chain = "THREATCRUSH";
13509
+ isAvailable() {
13510
+ const result = (0, import_node_child_process6.spawnSync)("iptables", ["--version"], { stdio: "pipe" });
13511
+ return result.status === 0;
13512
+ }
13513
+ ensureChain() {
13514
+ try {
13515
+ (0, import_node_child_process6.execSync)(`iptables -n -L ${this.chain} 2>/dev/null`, { stdio: "pipe" });
13516
+ } catch {
13517
+ (0, import_node_child_process6.execSync)(`iptables -N ${this.chain}`);
13518
+ (0, import_node_child_process6.execSync)(`iptables -I INPUT 1 -j ${this.chain}`);
13519
+ }
13520
+ }
13521
+ async block(ip) {
13522
+ this.ensureChain();
13523
+ if (await this.isBlocked(ip)) return;
13524
+ (0, import_node_child_process6.execSync)(`iptables -A ${this.chain} -s ${ip} -j DROP`);
13525
+ }
13526
+ async unblock(ip) {
13527
+ try {
13528
+ (0, import_node_child_process6.execSync)(`iptables -D ${this.chain} -s ${ip} -j DROP`);
13529
+ } catch {
13530
+ }
13531
+ }
13532
+ async isBlocked(ip) {
13533
+ try {
13534
+ const output = (0, import_node_child_process6.execSync)(`iptables -n -L ${this.chain}`, { encoding: "utf-8" });
13535
+ return output.includes(ip);
13536
+ } catch {
13537
+ return false;
13538
+ }
13539
+ }
13540
+ async listBlocked() {
13541
+ try {
13542
+ const output = (0, import_node_child_process6.execSync)(`iptables -n -L ${this.chain}`, { encoding: "utf-8" });
13543
+ const ips = [];
13544
+ for (const line of output.split("\n")) {
13545
+ const match = line.match(/DROP\s+all\s+--\s+(\d+\.\d+\.\d+\.\d+)/);
13546
+ if (match) ips.push(match[1]);
13547
+ }
13548
+ return ips;
13549
+ } catch {
13550
+ return [];
13551
+ }
13552
+ }
13553
+ };
13554
+ var DryRunAdapter = class {
13555
+ name = "dry-run";
13556
+ blocked = /* @__PURE__ */ new Set();
13557
+ isAvailable() {
13558
+ return true;
13559
+ }
13560
+ async block(ip) {
13561
+ this.blocked.add(ip);
13562
+ }
13563
+ async unblock(ip) {
13564
+ this.blocked.delete(ip);
13565
+ }
13566
+ async isBlocked(ip) {
13567
+ return this.blocked.has(ip);
13568
+ }
13569
+ async listBlocked() {
13570
+ return [...this.blocked];
13571
+ }
13572
+ };
13573
+ function detectFirewallAdapter() {
13574
+ const nft = new NftablesAdapter();
13575
+ if (nft.isAvailable()) return nft;
13576
+ const ipt = new IptablesAdapter();
13577
+ if (ipt.isAvailable()) return ipt;
13578
+ return new DryRunAdapter();
13579
+ }
13580
+
13581
+ // src/daemon/firewall/remediation.ts
13582
+ var import_node_fs20 = require("fs");
13583
+ init_state();
13584
+ init_paths();
13585
+ var DEFAULT_CONFIG2 = {
13586
+ enabled: true,
13587
+ dry_run: true,
13588
+ default_ttl_seconds: 3600,
13589
+ min_severity: "high",
13590
+ allowlist: ["127.0.0.1", "::1"]
13591
+ };
13592
+ var SEVERITY_RANK4 = {
13593
+ info: 0,
13594
+ low: 1,
13595
+ medium: 2,
13596
+ high: 3,
13597
+ critical: 4
13598
+ };
13599
+ var RemediationManager = class {
13600
+ constructor(adapter, bus2, config) {
13601
+ this.adapter = adapter;
13602
+ this.bus = bus2;
13603
+ this.config = { ...DEFAULT_CONFIG2, ...config };
13604
+ this.loadState();
13605
+ this.startExpiryWorker();
13606
+ }
13607
+ adapter;
13608
+ bus;
13609
+ config;
13610
+ blocklist = [];
13611
+ expiryTimer = null;
13612
+ async handleDetection(event) {
13613
+ if (!this.config.enabled) return;
13614
+ const eventRank = SEVERITY_RANK4[event.severity] ?? 0;
13615
+ const minRank = SEVERITY_RANK4[this.config.min_severity] ?? 3;
13616
+ if (eventRank < minRank) return;
13617
+ const ip = event.source_ip;
13618
+ if (!ip) return;
13619
+ if (this.isAllowlisted(ip)) return;
13620
+ if (this.blocklist.some((b) => b.ip === ip)) return;
13621
+ const ruleRemediation = event.details?.remediation;
13622
+ const ttl = ruleRemediation?.ttl_seconds || this.config.default_ttl_seconds;
13623
+ const ruleId = event.details?.rule_id;
13624
+ await this.blockIp(ip, event.message, ruleId, ttl);
13625
+ }
13626
+ async blockIp(ip, reason, ruleId, ttlSeconds) {
13627
+ if (this.isAllowlisted(ip)) return false;
13628
+ const entry = {
13629
+ ip,
13630
+ reason,
13631
+ rule_id: ruleId,
13632
+ blocked_at: Date.now(),
13633
+ expires_at: ttlSeconds ? Date.now() + ttlSeconds * 1e3 : void 0,
13634
+ dry_run: this.config.dry_run
13635
+ };
13636
+ if (!this.config.dry_run) {
13637
+ try {
13638
+ await this.adapter.block(ip);
13639
+ } catch (err) {
13640
+ this.logLine(`[firewall] EACCES or error blocking ${ip}: ${err.message}`);
13641
+ this.bus.publish({
13642
+ timestamp: /* @__PURE__ */ new Date(),
13643
+ module: "firewall-rules",
13644
+ category: "system",
13645
+ severity: "medium",
13646
+ message: `Failed to block ${ip}: ${err.message}. Ensure daemon has CAP_NET_ADMIN.`
13647
+ });
13648
+ return false;
13649
+ }
13650
+ }
13651
+ this.blocklist.push(entry);
13652
+ this.saveState();
13653
+ const mode = this.config.dry_run ? "[DRY-RUN] " : "";
13654
+ const expiryMsg = ttlSeconds ? ` (expires in ${ttlSeconds}s)` : " (permanent)";
13655
+ this.logLine(`[firewall] ${mode}Blocked ${ip}: ${reason}${expiryMsg}`);
13656
+ this.bus.publish({
13657
+ timestamp: /* @__PURE__ */ new Date(),
13658
+ module: "firewall-rules",
13659
+ category: "system",
13660
+ severity: "info",
13661
+ message: `${mode}Blocked ${ip}: ${reason}${expiryMsg}`,
13662
+ source_ip: ip,
13663
+ details: { action: "block", rule_id: ruleId, dry_run: this.config.dry_run, ttl_seconds: ttlSeconds }
13664
+ });
13665
+ return true;
13666
+ }
13667
+ async unblockIp(ip) {
13668
+ const idx = this.blocklist.findIndex((b) => b.ip === ip);
13669
+ if (idx < 0) return false;
13670
+ const entry = this.blocklist[idx];
13671
+ if (!entry.dry_run) {
13672
+ try {
13673
+ await this.adapter.unblock(ip);
13674
+ } catch (err) {
13675
+ this.logLine(`[firewall] Error unblocking ${ip}: ${err.message}`);
13676
+ return false;
13677
+ }
13678
+ }
13679
+ this.blocklist.splice(idx, 1);
13680
+ this.saveState();
13681
+ this.logLine(`[firewall] Unblocked ${ip}`);
13682
+ this.bus.publish({
13683
+ timestamp: /* @__PURE__ */ new Date(),
13684
+ module: "firewall-rules",
13685
+ category: "system",
13686
+ severity: "info",
13687
+ message: `Unblocked ${ip}`,
13688
+ source_ip: ip,
13689
+ details: { action: "unblock" }
13690
+ });
13691
+ return true;
13692
+ }
13693
+ isAllowlisted(ip) {
13694
+ return this.config.allowlist.includes(ip);
13695
+ }
13696
+ addToAllowlist(ip) {
13697
+ if (!this.config.allowlist.includes(ip)) {
13698
+ this.config.allowlist.push(ip);
13699
+ }
13700
+ }
13701
+ removeFromAllowlist(ip) {
13702
+ this.config.allowlist = this.config.allowlist.filter((a) => a !== ip);
13703
+ }
13704
+ getBlocklist() {
13705
+ return [...this.blocklist];
13706
+ }
13707
+ getAllowlist() {
13708
+ return [...this.config.allowlist];
13709
+ }
13710
+ stop() {
13711
+ if (this.expiryTimer) clearInterval(this.expiryTimer);
13712
+ this.expiryTimer = null;
13713
+ }
13714
+ startExpiryWorker() {
13715
+ this.expiryTimer = setInterval(() => void this.processExpiries(), 3e4);
13716
+ }
13717
+ async processExpiries() {
13718
+ const now = Date.now();
13719
+ const expired = this.blocklist.filter((b) => b.expires_at && b.expires_at <= now);
13720
+ for (const entry of expired) {
13721
+ await this.unblockIp(entry.ip);
13722
+ }
13723
+ }
13724
+ loadState() {
13725
+ try {
13726
+ const saved = getModuleState("firewall-rules", "blocklist");
13727
+ if (Array.isArray(saved)) this.blocklist = saved;
13728
+ } catch {
13729
+ }
13730
+ }
13731
+ saveState() {
12313
13732
  try {
12314
- await fetch(
12315
- `${API_URL6}/api/orgs/${orgId}/properties/${claimed.property_id}/runs/${claimed.id}`,
12316
- {
12317
- method: "PATCH",
12318
- headers: authHeaders(),
12319
- body: JSON.stringify({
12320
- status: result.error ? "failed" : "succeeded",
12321
- findings_count: result.findings.length,
12322
- severity_summary: result.severity_summary,
12323
- summary: result.summary,
12324
- findings: result.findings,
12325
- error: result.error,
12326
- source: "daemon",
12327
- worker_id: workerId()
12328
- })
12329
- }
12330
- );
13733
+ setModuleState("firewall-rules", "blocklist", this.blocklist);
13734
+ } catch {
13735
+ }
13736
+ }
13737
+ logLine(line) {
13738
+ try {
13739
+ (0, import_node_fs20.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
13740
+ `);
12331
13741
  } catch {
12332
- } finally {
12333
- this.bus.announceModule("runs-worker", "idle");
12334
13742
  }
12335
13743
  }
12336
13744
  };
@@ -12387,7 +13795,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
12387
13795
  // src/daemon/index.ts
12388
13796
  function readVersion() {
12389
13797
  try {
12390
- const pkg = JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path11.join)(__dirname, "..", "package.json"), "utf-8"));
13798
+ const pkg = JSON.parse((0, import_node_fs21.readFileSync)((0, import_node_path12.join)(__dirname, "..", "package.json"), "utf-8"));
12391
13799
  return pkg.version || "0.0.0";
12392
13800
  } catch {
12393
13801
  return "0.0.0";
@@ -12395,7 +13803,7 @@ function readVersion() {
12395
13803
  }
12396
13804
  function logLine(line) {
12397
13805
  try {
12398
- (0, import_node_fs17.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
13806
+ (0, import_node_fs21.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
12399
13807
  `);
12400
13808
  } catch {
12401
13809
  }
@@ -12423,12 +13831,42 @@ async function runDaemon() {
12423
13831
  } catch (err) {
12424
13832
  logLine(`[daemon] state db unavailable: ${err.message}`);
12425
13833
  }
12426
- const config = loadConfig((0, import_node_fs17.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
13834
+ const config = loadConfig((0, import_node_fs21.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
12427
13835
  bus.on("event", (event) => {
12428
13836
  logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
12429
13837
  });
12430
13838
  const moduleHost = new ModuleHost(bus);
12431
13839
  await moduleHost.start();
13840
+ const ruleEngine = new RuleEngine((detection) => {
13841
+ const event = {
13842
+ timestamp: /* @__PURE__ */ new Date(),
13843
+ module: "rule-engine",
13844
+ category: detection.raw_metadata?.category || "system",
13845
+ severity: detection.severity,
13846
+ message: `[DETECTION] ${detection.title}`,
13847
+ source_ip: detection.source_ip,
13848
+ details: {
13849
+ rule_id: detection.rule_id,
13850
+ username: detection.username,
13851
+ ...detection.raw_metadata
13852
+ }
13853
+ };
13854
+ bus.publish(event);
13855
+ });
13856
+ ruleEngine.loadRules(loadAllRules());
13857
+ bus.on("event", (event) => {
13858
+ if (event.module !== "rule-engine") ruleEngine.evaluate(event);
13859
+ });
13860
+ logLine(`[daemon] rule engine loaded ${ruleEngine.getRules().length} rules`);
13861
+ setInterval(() => ruleEngine.cleanup(), 3e5);
13862
+ const firewallAdapter = detectFirewallAdapter();
13863
+ const remediation = new RemediationManager(firewallAdapter, bus, config.remediation);
13864
+ bus.on("event", (event) => {
13865
+ if (event.module !== "firewall-rules") {
13866
+ void remediation.handleDetection(event);
13867
+ }
13868
+ });
13869
+ logLine(`[daemon] firewall remediation active (backend=${firewallAdapter.name}, dry_run=${config.remediation?.dry_run ?? true})`);
12432
13870
  new AlertDispatcher(bus, config);
12433
13871
  const runsWorker = new RunsWorker(bus);
12434
13872
  try {
@@ -12441,6 +13879,10 @@ async function runDaemon() {
12441
13879
  logLine(`[daemon] ipc listening on ${PATHS.socket}`);
12442
13880
  const shutdown = async (signal) => {
12443
13881
  logLine(`[daemon] received ${signal}, shutting down`);
13882
+ try {
13883
+ remediation.stop();
13884
+ } catch {
13885
+ }
12444
13886
  try {
12445
13887
  runsWorker.stop();
12446
13888
  } catch {
@@ -12481,7 +13923,7 @@ async function runDaemon() {
12481
13923
  init_paths();
12482
13924
  init_pidfile();
12483
13925
  init_ipc_client();
12484
- var DAEMON_ENTRY = (0, import_node_path12.join)(__dirname, "daemon.js");
13926
+ var DAEMON_ENTRY = (0, import_node_path13.join)(__dirname, "daemon.js");
12485
13927
  async function daemonForeground() {
12486
13928
  await runDaemon();
12487
13929
  }
@@ -12492,14 +13934,29 @@ async function daemonStart() {
12492
13934
  return;
12493
13935
  }
12494
13936
  ensureRuntimeDirs();
12495
- if (!(0, import_node_fs18.existsSync)(DAEMON_ENTRY)) {
13937
+ if (!(0, import_node_fs22.existsSync)(DAEMON_ENTRY)) {
12496
13938
  console.log(source_default.red(` Daemon entry not found at ${DAEMON_ENTRY}.`));
12497
13939
  console.log(source_default.dim(" Reinstall with `threatcrush update` or `pnpm run build` in the cli package."));
12498
13940
  return;
12499
13941
  }
12500
- const out = (0, import_node_fs19.openSync)(PATHS.logFile, "a");
12501
- const err = (0, import_node_fs19.openSync)(PATHS.logFile, "a");
12502
- const child = (0, import_node_child_process5.spawn)(process.execPath, [DAEMON_ENTRY], {
13942
+ let out;
13943
+ let err;
13944
+ try {
13945
+ out = (0, import_node_fs23.openSync)(PATHS.logFile, "a");
13946
+ err = (0, import_node_fs23.openSync)(PATHS.logFile, "a");
13947
+ } catch (e) {
13948
+ const code = e.code;
13949
+ console.log(source_default.red(` \u2717 Cannot write daemon log at ${PATHS.logFile} (${code ?? "error"}).`));
13950
+ if (code === "EACCES") {
13951
+ console.log(
13952
+ source_default.dim(
13953
+ PATHS.mode === "system" ? " Run as root (sudo) to use system paths, or run without sudo to use ~/.threatcrush." : ` Fix permissions on ${PATHS.logDir} (it should be owned by your user).`
13954
+ )
13955
+ );
13956
+ }
13957
+ return;
13958
+ }
13959
+ const child = (0, import_node_child_process7.spawn)(process.execPath, [DAEMON_ENTRY], {
12503
13960
  detached: true,
12504
13961
  stdio: ["ignore", out, err],
12505
13962
  env: { ...process.env, THREATCRUSH_DAEMON: "1" }
@@ -12564,27 +14021,27 @@ async function daemonStop() {
12564
14021
  }
12565
14022
 
12566
14023
  // src/commands/service.ts
12567
- var import_node_child_process6 = require("child_process");
12568
- var import_node_fs20 = require("fs");
12569
- var import_node_path13 = require("path");
14024
+ var import_node_child_process8 = require("child_process");
14025
+ var import_node_fs24 = require("fs");
14026
+ var import_node_path14 = require("path");
12570
14027
  var UNIT_PATH = "/etc/systemd/system/threatcrushd.service";
12571
14028
  function resolveTemplate() {
12572
- const templatePath = (0, import_node_path13.join)(__dirname, "systemd", "threatcrushd.service");
12573
- if (!(0, import_node_fs20.existsSync)(templatePath)) {
14029
+ const templatePath = (0, import_node_path14.join)(__dirname, "systemd", "threatcrushd.service");
14030
+ if (!(0, import_node_fs24.existsSync)(templatePath)) {
12574
14031
  throw new Error(`systemd unit template not found at ${templatePath}`);
12575
14032
  }
12576
- return (0, import_node_fs20.readFileSync)(templatePath, "utf-8");
14033
+ return (0, import_node_fs24.readFileSync)(templatePath, "utf-8");
12577
14034
  }
12578
14035
  function resolveBinPath() {
12579
14036
  const arg = process.argv[1];
12580
- if (arg && (0, import_node_fs20.existsSync)(arg)) return arg;
14037
+ if (arg && (0, import_node_fs24.existsSync)(arg)) return arg;
12581
14038
  try {
12582
- return (0, import_node_child_process6.execSync)("command -v threatcrush", { encoding: "utf-8" }).trim();
14039
+ return (0, import_node_child_process8.execSync)("command -v threatcrush", { encoding: "utf-8" }).trim();
12583
14040
  } catch {
12584
14041
  return "threatcrush";
12585
14042
  }
12586
14043
  }
12587
- function isRoot() {
14044
+ function isRoot2() {
12588
14045
  return typeof process.getuid === "function" && process.getuid() === 0;
12589
14046
  }
12590
14047
  async function installServiceCommand() {
@@ -12593,16 +14050,17 @@ async function installServiceCommand() {
12593
14050
  console.log(source_default.yellow(" systemd install is only supported on Linux."));
12594
14051
  return;
12595
14052
  }
12596
- if (!isRoot()) {
14053
+ if (!isRoot2()) {
12597
14054
  console.log(source_default.red(" Must run as root (try `sudo threatcrush install-service`)."));
12598
14055
  return;
12599
14056
  }
12600
14057
  const unit = resolveTemplate().replace("{{BIN_PATH}}", resolveBinPath());
12601
- (0, import_node_fs20.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
14058
+ (0, import_node_fs24.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
12602
14059
  console.log(source_default.green(` \u2713 Installed unit file: ${UNIT_PATH}`));
14060
+ ensureSystemDirs();
12603
14061
  try {
12604
- (0, import_node_child_process6.execSync)("systemctl daemon-reload", { stdio: "inherit" });
12605
- (0, import_node_child_process6.execSync)("systemctl enable threatcrushd.service", { stdio: "inherit" });
14062
+ (0, import_node_child_process8.execSync)("systemctl daemon-reload", { stdio: "inherit" });
14063
+ (0, import_node_child_process8.execSync)("systemctl enable threatcrushd.service", { stdio: "inherit" });
12606
14064
  console.log(source_default.green(" \u2713 Service enabled on boot."));
12607
14065
  console.log(source_default.dim(" Start now with: systemctl start threatcrushd"));
12608
14066
  console.log(source_default.dim(" View logs with: journalctl -u threatcrushd -f"));
@@ -12610,33 +14068,62 @@ async function installServiceCommand() {
12610
14068
  console.log(source_default.yellow(` ! systemctl error: ${err.message}`));
12611
14069
  }
12612
14070
  }
14071
+ function ensureSystemDirs() {
14072
+ const dirs = [
14073
+ { path: "/etc/threatcrush" },
14074
+ { path: "/etc/threatcrush/modules", sticky: true },
14075
+ { path: "/etc/threatcrush/threatcrushd.conf.d" },
14076
+ { path: "/var/log/threatcrush" },
14077
+ { path: "/var/lib/threatcrush" },
14078
+ { path: "/var/run/threatcrush" }
14079
+ ];
14080
+ let admGid = null;
14081
+ try {
14082
+ admGid = (0, import_node_fs24.statSync)("/var/log/auth.log").gid;
14083
+ } catch {
14084
+ }
14085
+ for (const { path, sticky } of dirs) {
14086
+ try {
14087
+ (0, import_node_fs24.mkdirSync)(path, { recursive: true });
14088
+ } catch {
14089
+ }
14090
+ if (admGid !== null) {
14091
+ try {
14092
+ (0, import_node_fs24.chmodSync)(path, sticky ? 1533 : 509);
14093
+ (0, import_node_child_process8.execSync)(`chgrp adm ${path}`, { stdio: "ignore" });
14094
+ } catch {
14095
+ }
14096
+ }
14097
+ }
14098
+ console.log(source_default.green(" \u2713 Runtime dirs prepared (group `adm` may install modules / edit config without sudo)."));
14099
+ }
12613
14100
  async function uninstallServiceCommand() {
12614
14101
  banner();
12615
14102
  if (process.platform !== "linux") {
12616
14103
  console.log(source_default.yellow(" systemd uninstall is only supported on Linux."));
12617
14104
  return;
12618
14105
  }
12619
- if (!isRoot()) {
14106
+ if (!isRoot2()) {
12620
14107
  console.log(source_default.red(" Must run as root (try `sudo threatcrush uninstall-service`)."));
12621
14108
  return;
12622
14109
  }
12623
14110
  try {
12624
- (0, import_node_child_process6.execSync)("systemctl stop threatcrushd.service", { stdio: "inherit" });
14111
+ (0, import_node_child_process8.execSync)("systemctl stop threatcrushd.service", { stdio: "inherit" });
12625
14112
  } catch {
12626
14113
  }
12627
14114
  try {
12628
- (0, import_node_child_process6.execSync)("systemctl disable threatcrushd.service", { stdio: "inherit" });
14115
+ (0, import_node_child_process8.execSync)("systemctl disable threatcrushd.service", { stdio: "inherit" });
12629
14116
  } catch {
12630
14117
  }
12631
14118
  try {
12632
- if ((0, import_node_fs20.existsSync)(UNIT_PATH)) {
12633
- (0, import_node_child_process6.execSync)(`rm -f ${UNIT_PATH}`);
14119
+ if ((0, import_node_fs24.existsSync)(UNIT_PATH)) {
14120
+ (0, import_node_child_process8.execSync)(`rm -f ${UNIT_PATH}`);
12634
14121
  console.log(source_default.green(` \u2713 Removed unit file: ${UNIT_PATH}`));
12635
14122
  }
12636
14123
  } catch {
12637
14124
  }
12638
14125
  try {
12639
- (0, import_node_child_process6.execSync)("systemctl daemon-reload", { stdio: "inherit" });
14126
+ (0, import_node_child_process8.execSync)("systemctl daemon-reload", { stdio: "inherit" });
12640
14127
  } catch {
12641
14128
  }
12642
14129
  console.log(source_default.green(" \u2713 threatcrushd service removed."));
@@ -12728,13 +14215,13 @@ function welcomeCommand() {
12728
14215
  }
12729
14216
 
12730
14217
  // src/commands/properties.ts
12731
- var import_node_fs21 = require("fs");
12732
- var import_node_path14 = require("path");
12733
- var import_node_readline5 = __toESM(require("readline"));
14218
+ var import_node_fs25 = require("fs");
14219
+ var import_node_path15 = require("path");
14220
+ var import_node_readline6 = __toESM(require("readline"));
12734
14221
  var API_URL7 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
12735
14222
  var KINDS = ["url", "api", "domain", "ip", "repo"];
12736
14223
  function prompt2(question) {
12737
- const rl = import_node_readline5.default.createInterface({ input: process.stdin, output: process.stdout });
14224
+ const rl = import_node_readline6.default.createInterface({ input: process.stdin, output: process.stdout });
12738
14225
  return new Promise((resolve3) => rl.question(question, (answer) => {
12739
14226
  rl.close();
12740
14227
  resolve3(answer.trim());
@@ -13058,8 +14545,8 @@ async function propertiesRunsCommand(opts) {
13058
14545
  }
13059
14546
  }
13060
14547
  function parseImportFile(path) {
13061
- const ext = (0, import_node_path14.extname)(path).toLowerCase();
13062
- const raw = (0, import_node_fs21.readFileSync)(path, "utf-8");
14548
+ const ext = (0, import_node_path15.extname)(path).toLowerCase();
14549
+ const raw = (0, import_node_fs25.readFileSync)(path, "utf-8");
13063
14550
  if (ext === ".json") {
13064
14551
  const parsed = JSON.parse(raw);
13065
14552
  if (!Array.isArray(parsed)) throw new Error("JSON must be an array of objects");
@@ -13173,11 +14660,556 @@ async function propertiesImportCommand(filePath, opts) {
13173
14660
  `);
13174
14661
  }
13175
14662
 
14663
+ // src/commands/rules.ts
14664
+ async function rulesListCommand() {
14665
+ banner();
14666
+ const rules = loadAllRules();
14667
+ console.log(source_default.green.bold(" Detection Rules"));
14668
+ console.log(source_default.gray(" " + "\u2500".repeat(70)));
14669
+ console.log(
14670
+ source_default.gray(" ") + source_default.white.bold("ID".padEnd(28)) + source_default.white.bold("Severity".padEnd(12)) + source_default.white.bold("Category".padEnd(12)) + source_default.white.bold("Threshold".padEnd(12)) + source_default.white.bold("Title")
14671
+ );
14672
+ console.log(source_default.gray(" " + "\u2500".repeat(70)));
14673
+ for (const rule of rules) {
14674
+ const sevColor = rule.severity === "critical" ? source_default.red : rule.severity === "high" ? source_default.red : rule.severity === "medium" ? source_default.yellow : source_default.green;
14675
+ console.log(
14676
+ source_default.gray(" ") + source_default.white(rule.id.padEnd(28)) + sevColor(rule.severity.padEnd(12)) + source_default.gray(rule.category.padEnd(12)) + source_default.white(String(rule.threshold).padEnd(12)) + source_default.gray(rule.title)
14677
+ );
14678
+ }
14679
+ console.log();
14680
+ console.log(source_default.gray(` ${rules.length} rule(s) loaded`));
14681
+ console.log(source_default.gray(` Custom rules: /etc/threatcrush/rules.d/*.json`));
14682
+ console.log();
14683
+ }
14684
+ async function rulesShowCommand(ruleId) {
14685
+ banner();
14686
+ const rules = loadAllRules();
14687
+ const rule = rules.find((r) => r.id === ruleId);
14688
+ if (!rule) {
14689
+ console.log(source_default.red(` Rule not found: ${ruleId}`));
14690
+ console.log(source_default.gray(" Run `threatcrush rules list` to see available rules.\n"));
14691
+ return;
14692
+ }
14693
+ console.log(source_default.green.bold(` Rule: ${rule.id}`));
14694
+ console.log(source_default.gray(" " + "\u2500".repeat(60)));
14695
+ console.log(` Title: ${source_default.white(rule.title)}`);
14696
+ console.log(` Description: ${source_default.gray(rule.description)}`);
14697
+ console.log(` Version: ${source_default.gray(rule.version)}`);
14698
+ console.log(` Category: ${source_default.gray(rule.category)}`);
14699
+ console.log(` Severity: ${source_default.yellow(rule.severity)}`);
14700
+ console.log(` Source types: ${source_default.gray(rule.source_types.join(", "))}`);
14701
+ console.log(` Threshold: ${source_default.white(String(rule.threshold))} events in ${source_default.white(String(rule.window_seconds))}s`);
14702
+ console.log(` Cooldown: ${source_default.gray(String(rule.cooldown_seconds))}s`);
14703
+ console.log(` Tags: ${source_default.gray(rule.tags.join(", "))}`);
14704
+ console.log(` Enabled: ${rule.enabled ? source_default.green("yes") : source_default.red("no")}`);
14705
+ if (rule.remediation) {
14706
+ console.log(` Remediation: ${source_default.gray(rule.remediation.description || rule.remediation.action || "none")}`);
14707
+ if (rule.remediation.ttl_seconds) {
14708
+ console.log(` Block TTL: ${source_default.gray(String(rule.remediation.ttl_seconds))}s`);
14709
+ }
14710
+ }
14711
+ console.log();
14712
+ console.log(source_default.gray(" Match condition:"));
14713
+ console.log(source_default.gray(` ${rule.match.field} ${rule.match.operator} "${rule.match.value}"`));
14714
+ console.log();
14715
+ }
14716
+ async function rulesCommand(opts) {
14717
+ const action = opts.action || "list";
14718
+ switch (action) {
14719
+ case "list":
14720
+ case "ls":
14721
+ await rulesListCommand();
14722
+ break;
14723
+ case "show":
14724
+ case "info":
14725
+ if (!opts.id) {
14726
+ console.log(source_default.red(" Rule ID required. Usage: threatcrush rules show <rule-id>\n"));
14727
+ return;
14728
+ }
14729
+ await rulesShowCommand(opts.id);
14730
+ break;
14731
+ default:
14732
+ console.log(source_default.yellow(` Unknown action: ${action}`));
14733
+ console.log(source_default.gray(" Available: list, show\n"));
14734
+ break;
14735
+ }
14736
+ }
14737
+
14738
+ // src/commands/harden.ts
14739
+ var import_node_fs26 = require("fs");
14740
+ var import_node_child_process9 = require("child_process");
14741
+ function tryExec(cmd) {
14742
+ try {
14743
+ return (0, import_node_child_process9.execSync)(cmd, { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"] });
14744
+ } catch {
14745
+ return null;
14746
+ }
14747
+ }
14748
+ function tryRead(path) {
14749
+ try {
14750
+ return (0, import_node_fs26.readFileSync)(path, "utf-8");
14751
+ } catch {
14752
+ return null;
14753
+ }
14754
+ }
14755
+ function checkSshPasswordAuth() {
14756
+ const config = tryRead("/etc/ssh/sshd_config");
14757
+ if (!config) {
14758
+ return {
14759
+ key: "ssh-password-auth",
14760
+ status: "warn",
14761
+ severity: "medium",
14762
+ title: "SSH Password Auth",
14763
+ explanation: "Could not read /etc/ssh/sshd_config to check password authentication setting.",
14764
+ recommendation: "Ensure PasswordAuthentication is set to no in sshd_config."
14765
+ };
14766
+ }
14767
+ const match = config.match(/^\s*PasswordAuthentication\s+(yes|no)/mi);
14768
+ if (!match || match[1] === "yes") {
14769
+ return {
14770
+ key: "ssh-password-auth",
14771
+ status: "fail",
14772
+ severity: "high",
14773
+ title: "SSH Password Authentication Enabled",
14774
+ explanation: "Password authentication is enabled for SSH, making it vulnerable to brute-force attacks.",
14775
+ recommendation: 'Set "PasswordAuthentication no" in /etc/ssh/sshd_config and restart sshd. Use key-based auth instead.'
14776
+ };
14777
+ }
14778
+ return {
14779
+ key: "ssh-password-auth",
14780
+ status: "pass",
14781
+ severity: "high",
14782
+ title: "SSH Password Authentication Disabled",
14783
+ explanation: "Password authentication is disabled for SSH. Key-based auth is enforced."
14784
+ };
14785
+ }
14786
+ function checkSshRootLogin() {
14787
+ const config = tryRead("/etc/ssh/sshd_config");
14788
+ if (!config) {
14789
+ return {
14790
+ key: "ssh-root-login",
14791
+ status: "warn",
14792
+ severity: "high",
14793
+ title: "SSH Root Login",
14794
+ explanation: "Could not read sshd_config.",
14795
+ recommendation: 'Set "PermitRootLogin no" in /etc/ssh/sshd_config.'
14796
+ };
14797
+ }
14798
+ const match = config.match(/^\s*PermitRootLogin\s+(\S+)/mi);
14799
+ if (!match || match[1] === "yes") {
14800
+ return {
14801
+ key: "ssh-root-login",
14802
+ status: "fail",
14803
+ severity: "high",
14804
+ title: "Root SSH Login Enabled",
14805
+ explanation: "Direct root login via SSH is permitted. Attackers frequently target root.",
14806
+ recommendation: 'Set "PermitRootLogin no" or "PermitRootLogin prohibit-password" in /etc/ssh/sshd_config.'
14807
+ };
14808
+ }
14809
+ return {
14810
+ key: "ssh-root-login",
14811
+ status: "pass",
14812
+ severity: "high",
14813
+ title: "Root SSH Login Restricted",
14814
+ explanation: `PermitRootLogin is set to "${match[1]}".`
14815
+ };
14816
+ }
14817
+ function checkSshWeakConfig() {
14818
+ const config = tryRead("/etc/ssh/sshd_config");
14819
+ if (!config) {
14820
+ return {
14821
+ key: "ssh-weak-config",
14822
+ status: "warn",
14823
+ severity: "medium",
14824
+ title: "SSH Configuration",
14825
+ explanation: "Could not read sshd_config."
14826
+ };
14827
+ }
14828
+ const issues = [];
14829
+ if (!/^\s*Protocol\s+2/mi.test(config) && !/^\s*#\s*Protocol/mi.test(config)) {
14830
+ if (/^\s*Protocol\s+1/mi.test(config)) issues.push("Protocol 1 is enabled");
14831
+ }
14832
+ if (/^\s*X11Forwarding\s+yes/mi.test(config)) issues.push("X11 forwarding is enabled");
14833
+ const maxAuth = config.match(/^\s*MaxAuthTries\s+(\d+)/mi);
14834
+ if (maxAuth && parseInt(maxAuth[1]) > 6) issues.push(`MaxAuthTries is high (${maxAuth[1]})`);
14835
+ if (issues.length > 0) {
14836
+ return {
14837
+ key: "ssh-weak-config",
14838
+ status: "warn",
14839
+ severity: "medium",
14840
+ title: "SSH Configuration Weaknesses",
14841
+ explanation: `Found: ${issues.join("; ")}.`,
14842
+ recommendation: "Review and harden sshd_config. Disable unused features."
14843
+ };
14844
+ }
14845
+ return {
14846
+ key: "ssh-weak-config",
14847
+ status: "pass",
14848
+ severity: "medium",
14849
+ title: "SSH Configuration",
14850
+ explanation: "No obvious SSH config weaknesses found."
14851
+ };
14852
+ }
14853
+ function checkAutoUpdates() {
14854
+ const unattended = (0, import_node_fs26.existsSync)("/etc/apt/apt.conf.d/20auto-upgrades") || (0, import_node_fs26.existsSync)("/etc/apt/apt.conf.d/50unattended-upgrades");
14855
+ const dnfAuto = (0, import_node_fs26.existsSync)("/etc/dnf/automatic.conf");
14856
+ if (unattended || dnfAuto) {
14857
+ return {
14858
+ key: "auto-updates",
14859
+ status: "pass",
14860
+ severity: "high",
14861
+ title: "Automatic Security Updates",
14862
+ explanation: "Automatic security updates appear to be configured."
14863
+ };
14864
+ }
14865
+ return {
14866
+ key: "auto-updates",
14867
+ status: "fail",
14868
+ severity: "high",
14869
+ title: "No Automatic Security Updates",
14870
+ explanation: "No automatic security update mechanism detected.",
14871
+ recommendation: "Install and enable unattended-upgrades (Debian/Ubuntu) or dnf-automatic (RHEL/Fedora)."
14872
+ };
14873
+ }
14874
+ function checkFirewallActive() {
14875
+ const ufw = tryExec("ufw status");
14876
+ if (ufw && ufw.includes("active")) {
14877
+ return {
14878
+ key: "firewall-active",
14879
+ status: "pass",
14880
+ severity: "high",
14881
+ title: "Firewall Active (UFW)",
14882
+ explanation: "UFW firewall is active."
14883
+ };
14884
+ }
14885
+ const nft = tryExec("nft list tables");
14886
+ if (nft && nft.trim().length > 0) {
14887
+ return {
14888
+ key: "firewall-active",
14889
+ status: "pass",
14890
+ severity: "high",
14891
+ title: "Firewall Active (nftables)",
14892
+ explanation: "nftables has active tables."
14893
+ };
14894
+ }
14895
+ const ipt = tryExec("iptables -L -n");
14896
+ if (ipt) {
14897
+ const rules = ipt.split("\n").filter((l) => l.trim() && !l.startsWith("Chain") && !l.startsWith("target"));
14898
+ if (rules.length > 0) {
14899
+ return {
14900
+ key: "firewall-active",
14901
+ status: "pass",
14902
+ severity: "high",
14903
+ title: "Firewall Active (iptables)",
14904
+ explanation: `iptables has ${rules.length} rules.`
14905
+ };
14906
+ }
14907
+ }
14908
+ return {
14909
+ key: "firewall-active",
14910
+ status: "fail",
14911
+ severity: "high",
14912
+ title: "No Firewall Detected",
14913
+ explanation: "No active firewall (UFW, nftables, or iptables) detected.",
14914
+ recommendation: "Enable a firewall: `ufw enable` or configure nftables/iptables."
14915
+ };
14916
+ }
14917
+ function checkExposedPorts() {
14918
+ const ss = tryExec("ss -tlnp");
14919
+ if (!ss) {
14920
+ return {
14921
+ key: "exposed-ports",
14922
+ status: "warn",
14923
+ severity: "medium",
14924
+ title: "Exposed Ports",
14925
+ explanation: "Could not check listening ports."
14926
+ };
14927
+ }
14928
+ const riskyPorts = ["3306", "5432", "6379", "27017", "9200", "11211", "2375"];
14929
+ const exposed = [];
14930
+ for (const line of ss.split("\n")) {
14931
+ if (!line.includes("LISTEN")) continue;
14932
+ if (line.includes("0.0.0.0:") || line.includes(":::")) {
14933
+ for (const port of riskyPorts) {
14934
+ if (line.includes(`:${port} `) || line.includes(`:${port} `)) {
14935
+ exposed.push(port);
14936
+ }
14937
+ }
14938
+ }
14939
+ }
14940
+ if (exposed.length > 0) {
14941
+ const portNames = {
14942
+ "3306": "MySQL",
14943
+ "5432": "PostgreSQL",
14944
+ "6379": "Redis",
14945
+ "27017": "MongoDB",
14946
+ "9200": "Elasticsearch",
14947
+ "11211": "Memcached",
14948
+ "2375": "Docker"
14949
+ };
14950
+ const desc = exposed.map((p) => `${portNames[p] || p} (:${p})`).join(", ");
14951
+ return {
14952
+ key: "exposed-ports",
14953
+ status: "fail",
14954
+ severity: "high",
14955
+ title: "Risky Ports Exposed",
14956
+ explanation: `Services exposed on all interfaces: ${desc}.`,
14957
+ recommendation: "Bind database/cache services to 127.0.0.1 only, or restrict with firewall rules."
14958
+ };
14959
+ }
14960
+ return {
14961
+ key: "exposed-ports",
14962
+ status: "pass",
14963
+ severity: "high",
14964
+ title: "No Risky Ports Exposed",
14965
+ explanation: "No common database/cache ports are listening on all interfaces."
14966
+ };
14967
+ }
14968
+ function checkFail2ban() {
14969
+ const checkKey = "fail2ban-present";
14970
+ const sev = "medium";
14971
+ const f2bStatus = tryExec("fail2ban-client status");
14972
+ if (f2bStatus && f2bStatus.includes("Number of jail")) {
14973
+ return {
14974
+ key: checkKey,
14975
+ status: "pass",
14976
+ severity: sev,
14977
+ title: "fail2ban Active",
14978
+ explanation: "fail2ban is installed and running."
14979
+ };
14980
+ }
14981
+ if ((0, import_node_fs26.existsSync)("/etc/fail2ban/fail2ban.conf")) {
14982
+ return {
14983
+ key: checkKey,
14984
+ status: "warn",
14985
+ severity: sev,
14986
+ title: "fail2ban Installed but Not Running",
14987
+ explanation: "fail2ban is installed but does not appear to be running.",
14988
+ recommendation: "Start and enable fail2ban: `systemctl enable --now fail2ban`."
14989
+ };
14990
+ }
14991
+ return {
14992
+ key: checkKey,
14993
+ status: "warn",
14994
+ severity: sev,
14995
+ title: "fail2ban Not Installed",
14996
+ explanation: "fail2ban is not installed. ThreatCrush provides similar protection, but fail2ban adds defense in depth.",
14997
+ recommendation: "Consider installing fail2ban: `apt install fail2ban` or `dnf install fail2ban`."
14998
+ };
14999
+ }
15000
+ function checkWorldWritableDirs() {
15001
+ const sensitive = ["/etc", "/usr", "/var/log", "/boot"];
15002
+ const worldWritable = [];
15003
+ for (const dir of sensitive) {
15004
+ const result = tryExec(`find ${dir} -maxdepth 2 -type d -perm -0002 -not -path '*/tmp*' 2>/dev/null | head -5`);
15005
+ if (result && result.trim()) {
15006
+ worldWritable.push(...result.trim().split("\n"));
15007
+ }
15008
+ }
15009
+ if (worldWritable.length > 0) {
15010
+ return {
15011
+ key: "world-writable-dirs",
15012
+ status: "fail",
15013
+ severity: "medium",
15014
+ title: "World-Writable Directories Found",
15015
+ explanation: `Found ${worldWritable.length} world-writable directories in sensitive locations: ${worldWritable.slice(0, 3).join(", ")}${worldWritable.length > 3 ? "..." : ""}`,
15016
+ recommendation: "Remove world-writable permission: `chmod o-w <dir>`."
15017
+ };
15018
+ }
15019
+ return {
15020
+ key: "world-writable-dirs",
15021
+ status: "pass",
15022
+ severity: "medium",
15023
+ title: "No World-Writable Directories",
15024
+ explanation: "No world-writable directories found in sensitive locations."
15025
+ };
15026
+ }
15027
+ function checkRiskyServices() {
15028
+ const risky = ["telnet", "rsh", "rlogin", "rexec", "tftp"];
15029
+ const found = [];
15030
+ for (const svc of risky) {
15031
+ const result = tryExec(`systemctl is-active ${svc}.socket ${svc}.service 2>/dev/null`);
15032
+ if (result && result.trim() === "active") {
15033
+ found.push(svc);
15034
+ }
15035
+ }
15036
+ if (found.length > 0) {
15037
+ return {
15038
+ key: "risky-services",
15039
+ status: "fail",
15040
+ severity: "critical",
15041
+ title: "Risky Services Running",
15042
+ explanation: `Insecure services are active: ${found.join(", ")}.`,
15043
+ recommendation: `Disable and remove insecure services: \`systemctl disable --now ${found.join(" ")}\`.`
15044
+ };
15045
+ }
15046
+ return {
15047
+ key: "risky-services",
15048
+ status: "pass",
15049
+ severity: "critical",
15050
+ title: "No Risky Services",
15051
+ explanation: "No known insecure services (telnet, rsh, etc.) are running."
15052
+ };
15053
+ }
15054
+ function runAllChecks() {
15055
+ return [
15056
+ checkSshPasswordAuth(),
15057
+ checkSshRootLogin(),
15058
+ checkSshWeakConfig(),
15059
+ checkAutoUpdates(),
15060
+ checkFirewallActive(),
15061
+ checkExposedPorts(),
15062
+ checkFail2ban(),
15063
+ checkWorldWritableDirs(),
15064
+ checkRiskyServices()
15065
+ ];
15066
+ }
15067
+ function computeScore(results) {
15068
+ if (results.length === 0) return 100;
15069
+ const weights = { info: 0, low: 1, medium: 2, high: 3, critical: 4 };
15070
+ let maxScore = 0;
15071
+ let deductions = 0;
15072
+ for (const r of results) {
15073
+ const w = weights[r.severity] || 1;
15074
+ maxScore += w;
15075
+ if (r.status === "fail") deductions += w;
15076
+ else if (r.status === "warn") deductions += w * 0.5;
15077
+ }
15078
+ if (maxScore === 0) return 100;
15079
+ return Math.max(0, Math.round((maxScore - deductions) / maxScore * 100));
15080
+ }
15081
+ async function hardenCommand(opts) {
15082
+ if (!opts.json) {
15083
+ banner();
15084
+ logger.info("Running hardening scan...\n");
15085
+ }
15086
+ const spinner = opts.json ? null : ora({ text: "Scanning system configuration...", color: "green" }).start();
15087
+ const results = runAllChecks();
15088
+ const score = computeScore(results);
15089
+ if (spinner) spinner.succeed("Hardening scan complete\n");
15090
+ if (opts.json) {
15091
+ console.log(JSON.stringify({ score, findings: results }, null, 2));
15092
+ return;
15093
+ }
15094
+ const scoreColor = score >= 80 ? source_default.green : score >= 60 ? source_default.yellow : source_default.red;
15095
+ console.log(` ${source_default.white.bold("Hardening Score:")} ${scoreColor.bold(String(score) + "/100")}`);
15096
+ console.log(source_default.gray(" " + "\u2500".repeat(60)));
15097
+ console.log();
15098
+ const fails = results.filter((r) => r.status === "fail");
15099
+ const warns = results.filter((r) => r.status === "warn");
15100
+ const passes = results.filter((r) => r.status === "pass");
15101
+ if (fails.length > 0) {
15102
+ console.log(source_default.red.bold(" FAIL"));
15103
+ for (const r of fails) {
15104
+ console.log(` ${source_default.red("\u2717")} ${source_default.white.bold(r.title)}`);
15105
+ console.log(` ${source_default.gray(r.explanation)}`);
15106
+ if (r.recommendation) console.log(` ${source_default.yellow("Fix:")} ${r.recommendation}`);
15107
+ console.log();
15108
+ }
15109
+ }
15110
+ if (warns.length > 0) {
15111
+ console.log(source_default.yellow.bold(" WARNING"));
15112
+ for (const r of warns) {
15113
+ console.log(` ${source_default.yellow("!")} ${source_default.white.bold(r.title)}`);
15114
+ console.log(` ${source_default.gray(r.explanation)}`);
15115
+ if (r.recommendation) console.log(` ${source_default.yellow("Fix:")} ${r.recommendation}`);
15116
+ console.log();
15117
+ }
15118
+ }
15119
+ if (passes.length > 0) {
15120
+ console.log(source_default.green.bold(" PASS"));
15121
+ for (const r of passes) {
15122
+ console.log(` ${source_default.green("\u2713")} ${source_default.white(r.title)}`);
15123
+ }
15124
+ console.log();
15125
+ }
15126
+ console.log(source_default.gray(" " + "\u2500".repeat(60)));
15127
+ console.log(` ${source_default.white.bold(`${results.length} checks:`)} ${source_default.green(`${passes.length} pass`)} ${source_default.yellow(`${warns.length} warn`)} ${source_default.red(`${fails.length} fail`)}`);
15128
+ console.log();
15129
+ }
15130
+
15131
+ // src/commands/firewall.ts
15132
+ async function blockCommand(ip, opts) {
15133
+ banner();
15134
+ console.log(source_default.green.bold(" Firewall Block"));
15135
+ console.log(source_default.gray(" " + "\u2500".repeat(50)));
15136
+ let ttlSeconds;
15137
+ if (opts.ttl) {
15138
+ const match = opts.ttl.match(/^(\d+)(s|m|h|d)?$/);
15139
+ if (match) {
15140
+ const value = parseInt(match[1]);
15141
+ const unit = match[2] || "s";
15142
+ const multipliers = { s: 1, m: 60, h: 3600, d: 86400 };
15143
+ ttlSeconds = value * (multipliers[unit] || 1);
15144
+ }
15145
+ }
15146
+ console.log(` Blocking ${source_default.red(ip)}${ttlSeconds ? ` for ${opts.ttl}` : " permanently"}...`);
15147
+ console.log(source_default.gray(" Note: Requires running daemon with CAP_NET_ADMIN"));
15148
+ console.log(source_default.gray(" Configure in /etc/threatcrush/threatcrushd.conf under [remediation]"));
15149
+ console.log();
15150
+ }
15151
+ async function unblockCommand(ip) {
15152
+ banner();
15153
+ console.log(source_default.green.bold(" Firewall Unblock"));
15154
+ console.log(source_default.gray(" " + "\u2500".repeat(50)));
15155
+ console.log(` Unblocking ${source_default.white(ip)}...`);
15156
+ console.log(source_default.gray(" Note: Requires running daemon"));
15157
+ console.log();
15158
+ }
15159
+ async function blocklistCommand() {
15160
+ banner();
15161
+ console.log(source_default.green.bold(" Active Blocklist"));
15162
+ console.log(source_default.gray(" " + "\u2500".repeat(60)));
15163
+ console.log(source_default.gray(" Connect to running daemon for live blocklist data."));
15164
+ console.log(source_default.gray(" Configure via /etc/threatcrush/threatcrushd.conf [remediation]"));
15165
+ console.log();
15166
+ console.log(source_default.gray(" Remediation config options:"));
15167
+ console.log(source_default.gray(" enabled = true"));
15168
+ console.log(source_default.gray(" dry_run = true # Log only, no actual blocks"));
15169
+ console.log(source_default.gray(' default_ttl = "1h" # Default block duration'));
15170
+ console.log(source_default.gray(' min_severity = "high" # Minimum severity to auto-block'));
15171
+ console.log();
15172
+ }
15173
+ async function allowlistCommand(opts) {
15174
+ banner();
15175
+ const action = opts.action || "list";
15176
+ console.log(source_default.green.bold(" Allowlist"));
15177
+ console.log(source_default.gray(" " + "\u2500".repeat(50)));
15178
+ switch (action) {
15179
+ case "list":
15180
+ case "ls":
15181
+ console.log(source_default.gray(" Allowlisted IPs/CIDRs (from config):"));
15182
+ console.log(source_default.gray(" 127.0.0.1"));
15183
+ console.log(source_default.gray(" ::1"));
15184
+ console.log();
15185
+ console.log(source_default.gray(" Add more via /etc/threatcrush/threatcrushd.conf [remediation] allowlist"));
15186
+ break;
15187
+ case "add":
15188
+ if (!opts.value) {
15189
+ console.log(source_default.red(" IP/CIDR required. Usage: threatcrush allowlist add <ip>"));
15190
+ break;
15191
+ }
15192
+ console.log(source_default.green(` Added ${opts.value} to allowlist`));
15193
+ break;
15194
+ case "remove":
15195
+ if (!opts.value) {
15196
+ console.log(source_default.red(" IP/CIDR required. Usage: threatcrush allowlist remove <ip>"));
15197
+ break;
15198
+ }
15199
+ console.log(source_default.green(` Removed ${opts.value} from allowlist`));
15200
+ break;
15201
+ default:
15202
+ console.log(source_default.yellow(` Unknown action: ${action}`));
15203
+ console.log(source_default.gray(" Available: list, add, remove"));
15204
+ }
15205
+ console.log();
15206
+ }
15207
+
13176
15208
  // src/index.ts
13177
15209
  init_paths();
13178
15210
  var PKG_VERSION = "0.1.8";
13179
15211
  try {
13180
- const pkg = JSON.parse((0, import_node_fs22.readFileSync)((0, import_node_path15.join)(__dirname, "..", "package.json"), "utf-8"));
15212
+ const pkg = JSON.parse((0, import_node_fs27.readFileSync)((0, import_node_path16.join)(__dirname, "..", "package.json"), "utf-8"));
13181
15213
  PKG_VERSION = pkg.version;
13182
15214
  } catch {
13183
15215
  }
@@ -13193,25 +15225,25 @@ ${source_default.dim(" C R U S H")}
13193
15225
  var API_URL8 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
13194
15226
  var PKG_NAME = "@profullstack/threatcrush";
13195
15227
  var DESKTOP_PKG_NAME = "@profullstack/threatcrush-desktop";
13196
- var INSTALL_CONFIG_PATH = (0, import_node_path15.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
15228
+ var INSTALL_CONFIG_PATH = (0, import_node_path16.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
13197
15229
  function detectPackageManager() {
13198
15230
  try {
13199
- const npmGlobal = (0, import_node_child_process7.execSync)("npm ls -g --depth=0 --json 2>/dev/null", { encoding: "utf-8" });
15231
+ const npmGlobal = (0, import_node_child_process10.execSync)("npm ls -g --depth=0 --json 2>/dev/null", { encoding: "utf-8" });
13200
15232
  if (npmGlobal.includes(PKG_NAME)) return "npm";
13201
15233
  } catch {
13202
15234
  }
13203
15235
  try {
13204
- (0, import_node_child_process7.execSync)("pnpm --version", { stdio: "pipe" });
15236
+ (0, import_node_child_process10.execSync)("pnpm --version", { stdio: "pipe" });
13205
15237
  return "pnpm";
13206
15238
  } catch {
13207
15239
  }
13208
15240
  try {
13209
- (0, import_node_child_process7.execSync)("yarn --version", { stdio: "pipe" });
15241
+ (0, import_node_child_process10.execSync)("yarn --version", { stdio: "pipe" });
13210
15242
  return "yarn";
13211
15243
  } catch {
13212
15244
  }
13213
15245
  try {
13214
- (0, import_node_child_process7.execSync)("bun --version", { stdio: "pipe" });
15246
+ (0, import_node_child_process10.execSync)("bun --version", { stdio: "pipe" });
13215
15247
  return "bun";
13216
15248
  } catch {
13217
15249
  }
@@ -13219,7 +15251,7 @@ function detectPackageManager() {
13219
15251
  }
13220
15252
  function readInstallConfig() {
13221
15253
  try {
13222
- return JSON.parse((0, import_node_fs22.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
15254
+ return JSON.parse((0, import_node_fs27.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
13223
15255
  } catch {
13224
15256
  return {};
13225
15257
  }
@@ -13257,7 +15289,7 @@ function packageLooksInstalled(pm, pkgName) {
13257
15289
  yarn: `yarn global list --pattern ${pkgName}`,
13258
15290
  bun: `bun pm ls -g`
13259
15291
  };
13260
- const output = (0, import_node_child_process7.execSync)(listCommands[pm] || listCommands.npm, {
15292
+ const output = (0, import_node_child_process10.execSync)(listCommands[pm] || listCommands.npm, {
13261
15293
  encoding: "utf-8",
13262
15294
  stdio: ["pipe", "pipe", "pipe"]
13263
15295
  });
@@ -13331,6 +15363,24 @@ program2.command("pentest").description("Penetration test URLs and APIs").argume
13331
15363
  program2.command("status").description("Show daemon status and loaded modules").action(async () => {
13332
15364
  await statusCommand();
13333
15365
  });
15366
+ program2.command("rules").description("Manage detection rules").argument("[action]", "list | show", "list").argument("[id]", "Rule ID (for show)").action(async (action, id) => {
15367
+ await rulesCommand({ action, id });
15368
+ });
15369
+ program2.command("harden").description("Run hardening security scan").option("--json", "Output results as JSON").action(async (opts) => {
15370
+ await hardenCommand(opts);
15371
+ });
15372
+ program2.command("block").description("Block an IP address via the firewall").argument("<ip>", "IP address to block").option("--ttl <duration>", "Block duration (e.g. 1h, 30m, 1d)").action(async (ip, opts) => {
15373
+ await blockCommand(ip, opts);
15374
+ });
15375
+ program2.command("unblock").description("Unblock an IP address").argument("<ip>", "IP address to unblock").action(async (ip) => {
15376
+ await unblockCommand(ip);
15377
+ });
15378
+ program2.command("blocklist").description("Show active firewall blocklist").action(async () => {
15379
+ await blocklistCommand();
15380
+ });
15381
+ program2.command("allowlist").description("Manage IP allowlist").argument("[action]", "list | add | remove", "list").argument("[value]", "IP/CIDR to add or remove").action(async (action, value) => {
15382
+ await allowlistCommand({ action, value });
15383
+ });
13334
15384
  program2.command("start").description("Start the ThreatCrush daemon in the background").action(async () => {
13335
15385
  console.log(LOGO2);
13336
15386
  await daemonStart();
@@ -13351,7 +15401,7 @@ program2.command("uninstall-service").description("Remove the threatcrushd syste
13351
15401
  program2.command("logs").description("Tail daemon logs").action(async () => {
13352
15402
  console.log(LOGO2);
13353
15403
  const logPath = PATHS.logFile;
13354
- if (!(0, import_node_fs22.existsSync)(logPath)) {
15404
+ if (!(0, import_node_fs27.existsSync)(logPath)) {
13355
15405
  console.log(source_default.yellow(` No log file found at ${logPath}`));
13356
15406
  console.log(source_default.dim(" Start the daemon first with `threatcrush start`\n"));
13357
15407
  return;
@@ -13359,7 +15409,7 @@ program2.command("logs").description("Tail daemon logs").action(async () => {
13359
15409
  console.log(source_default.green(` Tailing ${logPath}...
13360
15410
  `));
13361
15411
  console.log(source_default.gray(" Press Ctrl+C to stop\n"));
13362
- (0, import_node_child_process7.execSync)(`tail -f ${logPath}`, { stdio: "inherit" });
15412
+ (0, import_node_child_process10.execSync)(`tail -f ${logPath}`, { stdio: "inherit" });
13363
15413
  });
13364
15414
  program2.command("activate").description("Activate your license key").action(async () => {
13365
15415
  console.log(LOGO2);
@@ -13409,7 +15459,7 @@ program2.command("update").description("Update ThreatCrush CLI and installed bun
13409
15459
  for (const cmd of commands2) {
13410
15460
  console.log(source_default.green(` \u2192 ${cmd}
13411
15461
  `));
13412
- (0, import_node_child_process7.execSync)(cmd, { stdio: "inherit" });
15462
+ (0, import_node_child_process10.execSync)(cmd, { stdio: "inherit" });
13413
15463
  }
13414
15464
  console.log(source_default.green("\n \u2713 Modules updated successfully!\n"));
13415
15465
  } catch (err) {
@@ -13435,7 +15485,7 @@ program2.command("update").description("Update ThreatCrush CLI and installed bun
13435
15485
  for (const cmd of commands) {
13436
15486
  console.log(source_default.green(` \u2192 ${cmd}
13437
15487
  `));
13438
- (0, import_node_child_process7.execSync)(cmd, { stdio: "inherit" });
15488
+ (0, import_node_child_process10.execSync)(cmd, { stdio: "inherit" });
13439
15489
  }
13440
15490
  console.log(source_default.green("\n \u2713 ThreatCrush updated successfully!\n"));
13441
15491
  if (installMode === "desktop") {
@@ -13479,7 +15529,7 @@ program2.command("remove").description("Uninstall ThreatCrush and the installed
13479
15529
  for (const cmd of commands) {
13480
15530
  console.log(source_default.green(` \u2192 ${cmd}
13481
15531
  `));
13482
- (0, import_node_child_process7.execSync)(cmd, { stdio: "inherit" });
15532
+ (0, import_node_child_process10.execSync)(cmd, { stdio: "inherit" });
13483
15533
  }
13484
15534
  console.log(source_default.green("\n \u2713 ThreatCrush has been uninstalled.\n"));
13485
15535
  console.log(source_default.dim(" We're sorry to see you go! \u{1F44B}\n"));
@@ -13556,10 +15606,10 @@ storeCmd.command("search <query>").description("Search for modules in the store"
13556
15606
  });
13557
15607
  storeCmd.command("publish <url>").description("Publish a module from a git URL or web URL").action(async (url) => {
13558
15608
  console.log(LOGO2);
13559
- const configPath = (0, import_node_path15.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
15609
+ const configPath = (0, import_node_path16.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
13560
15610
  let email = "";
13561
15611
  try {
13562
- const config = JSON.parse((0, import_node_fs22.readFileSync)(configPath, "utf-8"));
15612
+ const config = JSON.parse((0, import_node_fs27.readFileSync)(configPath, "utf-8"));
13563
15613
  email = config.email || "";
13564
15614
  } catch {
13565
15615
  }
@@ -13576,9 +15626,9 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
13576
15626
  return;
13577
15627
  }
13578
15628
  try {
13579
- const dir = (0, import_node_path15.join)((0, import_node_os8.homedir)(), ".threatcrush");
13580
- if (!(0, import_node_fs22.existsSync)(dir)) (0, import_node_fs22.mkdirSync)(dir, { recursive: true });
13581
- (0, import_node_fs22.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
15629
+ const dir = (0, import_node_path16.join)((0, import_node_os8.homedir)(), ".threatcrush");
15630
+ if (!(0, import_node_fs27.existsSync)(dir)) (0, import_node_fs27.mkdirSync)(dir, { recursive: true });
15631
+ (0, import_node_fs27.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
13582
15632
  console.log(source_default.dim(` Saved email to ${configPath}`));
13583
15633
  } catch {
13584
15634
  }