@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/daemon.js +1449 -48
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +2248 -198
- package/dist/index.js.map +1 -1
- package/dist/systemd/threatcrushd.service +10 -3
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -2022,27 +2022,18 @@ var require_toml = __commonJS({
|
|
|
2022
2022
|
});
|
|
2023
2023
|
|
|
2024
2024
|
// src/daemon/index.ts
|
|
2025
|
-
var
|
|
2026
|
-
var
|
|
2025
|
+
var import_node_fs13 = require("fs");
|
|
2026
|
+
var import_node_path7 = require("path");
|
|
2027
2027
|
|
|
2028
2028
|
// src/daemon/paths.ts
|
|
2029
2029
|
var import_node_fs = require("fs");
|
|
2030
2030
|
var import_node_os = require("os");
|
|
2031
2031
|
var import_node_path = require("path");
|
|
2032
|
-
function
|
|
2033
|
-
|
|
2034
|
-
if (process.getuid && process.getuid() === 0) return true;
|
|
2035
|
-
try {
|
|
2036
|
-
if (!(0, import_node_fs.existsSync)("/etc/threatcrush")) return false;
|
|
2037
|
-
(0, import_node_fs.mkdirSync)("/etc/threatcrush/.probe", { recursive: true });
|
|
2038
|
-
return true;
|
|
2039
|
-
} catch {
|
|
2040
|
-
return false;
|
|
2041
|
-
}
|
|
2032
|
+
function isRoot() {
|
|
2033
|
+
return process.platform === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
|
|
2042
2034
|
}
|
|
2043
|
-
var systemMode = canWriteSystemPaths();
|
|
2044
2035
|
var userBase = (0, import_node_path.join)((0, import_node_os.homedir)(), ".threatcrush");
|
|
2045
|
-
var
|
|
2036
|
+
var SYSTEM_PATHS = {
|
|
2046
2037
|
mode: "system",
|
|
2047
2038
|
configDir: "/etc/threatcrush",
|
|
2048
2039
|
configFile: "/etc/threatcrush/threatcrushd.conf",
|
|
@@ -2055,7 +2046,8 @@ var PATHS = systemMode ? {
|
|
|
2055
2046
|
runDir: "/var/run/threatcrush",
|
|
2056
2047
|
pidFile: "/var/run/threatcrush/threatcrushd.pid",
|
|
2057
2048
|
socket: "/var/run/threatcrush/threatcrushd.sock"
|
|
2058
|
-
}
|
|
2049
|
+
};
|
|
2050
|
+
var USER_PATHS = {
|
|
2059
2051
|
mode: "user",
|
|
2060
2052
|
configDir: userBase,
|
|
2061
2053
|
configFile: (0, import_node_path.join)(userBase, "threatcrushd.conf"),
|
|
@@ -2069,6 +2061,7 @@ var PATHS = systemMode ? {
|
|
|
2069
2061
|
pidFile: (0, import_node_path.join)(userBase, "run", "threatcrushd.pid"),
|
|
2070
2062
|
socket: (0, import_node_path.join)(userBase, "run", "threatcrushd.sock")
|
|
2071
2063
|
};
|
|
2064
|
+
var PATHS = isRoot() ? SYSTEM_PATHS : USER_PATHS;
|
|
2072
2065
|
function ensureRuntimeDirs() {
|
|
2073
2066
|
for (const dir of [PATHS.configDir, PATHS.confD, PATHS.moduleDir, PATHS.logDir, PATHS.stateDir, PATHS.runDir]) {
|
|
2074
2067
|
try {
|
|
@@ -2100,8 +2093,9 @@ function isProcessAlive(pid) {
|
|
|
2100
2093
|
try {
|
|
2101
2094
|
process.kill(pid, 0);
|
|
2102
2095
|
return true;
|
|
2103
|
-
} catch {
|
|
2104
|
-
|
|
2096
|
+
} catch (err) {
|
|
2097
|
+
const code = err.code;
|
|
2098
|
+
return code === "EPERM";
|
|
2105
2099
|
}
|
|
2106
2100
|
}
|
|
2107
2101
|
function findRunningDaemon() {
|
|
@@ -2320,10 +2314,19 @@ var IpcServer = class {
|
|
|
2320
2314
|
this.server = (0, import_node_net.createServer)((sock) => this.handleClient(sock));
|
|
2321
2315
|
this.server.on("error", reject);
|
|
2322
2316
|
this.server.listen(PATHS.socket, () => {
|
|
2317
|
+
const nodeFs = require("fs");
|
|
2323
2318
|
try {
|
|
2324
|
-
|
|
2319
|
+
nodeFs.chmodSync(PATHS.socket, 432);
|
|
2325
2320
|
} catch {
|
|
2326
2321
|
}
|
|
2322
|
+
const isRoot2 = process.platform === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
|
|
2323
|
+
if (isRoot2) {
|
|
2324
|
+
try {
|
|
2325
|
+
const { gid } = nodeFs.statSync("/var/log/auth.log");
|
|
2326
|
+
nodeFs.chownSync(PATHS.socket, 0, gid);
|
|
2327
|
+
} catch {
|
|
2328
|
+
}
|
|
2329
|
+
}
|
|
2327
2330
|
resolve();
|
|
2328
2331
|
});
|
|
2329
2332
|
});
|
|
@@ -2452,7 +2455,7 @@ var IpcServer = class {
|
|
|
2452
2455
|
};
|
|
2453
2456
|
|
|
2454
2457
|
// src/daemon/module-host.ts
|
|
2455
|
-
var
|
|
2458
|
+
var import_node_fs8 = require("fs");
|
|
2456
2459
|
var import_node_path3 = require("path");
|
|
2457
2460
|
var import_node_url = require("url");
|
|
2458
2461
|
var import_toml2 = __toESM(require_toml());
|
|
@@ -2725,8 +2728,16 @@ var JournalWatcher = class _JournalWatcher {
|
|
|
2725
2728
|
buffer = "";
|
|
2726
2729
|
moduleName = "user-journal";
|
|
2727
2730
|
active = false;
|
|
2731
|
+
// When the daemon runs as root (system mode), tail the SYSTEM journal so
|
|
2732
|
+
// we pick up sshd / sudo / kernel / UFW events. Falling back to --user
|
|
2733
|
+
// would give us root's mostly-empty per-user journal. Otherwise we use
|
|
2734
|
+
// --user so the daemon can run unprivileged on a workstation.
|
|
2735
|
+
static scopeArgs() {
|
|
2736
|
+
const isRoot2 = process.platform === "linux" && typeof process.getuid === "function" && process.getuid() === 0;
|
|
2737
|
+
return isRoot2 ? [] : ["--user"];
|
|
2738
|
+
}
|
|
2728
2739
|
static isAvailable() {
|
|
2729
|
-
const probe = (0, import_node_child_process.spawnSync)("journalctl", [
|
|
2740
|
+
const probe = (0, import_node_child_process.spawnSync)("journalctl", [...this.scopeArgs(), "-n", "0", "--no-pager"], {
|
|
2730
2741
|
stdio: ["ignore", "ignore", "ignore"]
|
|
2731
2742
|
});
|
|
2732
2743
|
return probe.status === 0;
|
|
@@ -2735,7 +2746,7 @@ var JournalWatcher = class _JournalWatcher {
|
|
|
2735
2746
|
if (!_JournalWatcher.isAvailable()) return false;
|
|
2736
2747
|
const child = (0, import_node_child_process.spawn)(
|
|
2737
2748
|
"journalctl",
|
|
2738
|
-
[
|
|
2749
|
+
[..._JournalWatcher.scopeArgs(), "-o", "json", "-f", "--since", "now"],
|
|
2739
2750
|
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
2740
2751
|
);
|
|
2741
2752
|
if (!child.stdout) return false;
|
|
@@ -2825,8 +2836,395 @@ function realtimeToDate(rt) {
|
|
|
2825
2836
|
return new Date(Math.floor(us / 1e3));
|
|
2826
2837
|
}
|
|
2827
2838
|
|
|
2828
|
-
// src/
|
|
2839
|
+
// src/modules/network-monitor/index.ts
|
|
2840
|
+
var import_node_child_process2 = require("child_process");
|
|
2829
2841
|
var import_node_fs5 = require("fs");
|
|
2842
|
+
var NetworkMonitor = class {
|
|
2843
|
+
constructor(bus2) {
|
|
2844
|
+
this.bus = bus2;
|
|
2845
|
+
}
|
|
2846
|
+
bus;
|
|
2847
|
+
active = false;
|
|
2848
|
+
pollTimer = null;
|
|
2849
|
+
scanTrackers = /* @__PURE__ */ new Map();
|
|
2850
|
+
halfOpenTrackers = /* @__PURE__ */ new Map();
|
|
2851
|
+
lastConnections = /* @__PURE__ */ new Set();
|
|
2852
|
+
// Config
|
|
2853
|
+
pollIntervalMs = 5e3;
|
|
2854
|
+
portScanThreshold = 10;
|
|
2855
|
+
// unique ports in window
|
|
2856
|
+
portScanWindowMs = 3e4;
|
|
2857
|
+
synFloodThreshold = 50;
|
|
2858
|
+
// half-open connections
|
|
2859
|
+
synFloodWindowMs = 1e4;
|
|
2860
|
+
start() {
|
|
2861
|
+
if (!this.hasConntrackOrSs()) {
|
|
2862
|
+
return false;
|
|
2863
|
+
}
|
|
2864
|
+
this.active = true;
|
|
2865
|
+
this.pollTimer = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
2866
|
+
return true;
|
|
2867
|
+
}
|
|
2868
|
+
stop() {
|
|
2869
|
+
if (this.pollTimer) clearInterval(this.pollTimer);
|
|
2870
|
+
this.pollTimer = null;
|
|
2871
|
+
this.active = false;
|
|
2872
|
+
}
|
|
2873
|
+
isActive() {
|
|
2874
|
+
return this.active;
|
|
2875
|
+
}
|
|
2876
|
+
hasConntrackOrSs() {
|
|
2877
|
+
const ss = (0, import_node_child_process2.spawnSync)("ss", ["--version"], { stdio: "pipe" });
|
|
2878
|
+
if (ss.status === 0) return true;
|
|
2879
|
+
return (0, import_node_fs5.existsSync)("/proc/net/tcp");
|
|
2880
|
+
}
|
|
2881
|
+
poll() {
|
|
2882
|
+
try {
|
|
2883
|
+
const connections = this.getConnections();
|
|
2884
|
+
this.analyzePortScans(connections);
|
|
2885
|
+
this.analyzeSynFlood(connections);
|
|
2886
|
+
this.cleanupTrackers();
|
|
2887
|
+
} catch {
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
getConnections() {
|
|
2891
|
+
const records = [];
|
|
2892
|
+
const now = Date.now();
|
|
2893
|
+
try {
|
|
2894
|
+
const ct = (0, import_node_child_process2.spawnSync)("conntrack", ["-L", "-p", "tcp", "-o", "extended"], {
|
|
2895
|
+
encoding: "utf-8",
|
|
2896
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2897
|
+
timeout: 3e3
|
|
2898
|
+
});
|
|
2899
|
+
if (ct.status === 0 && ct.stdout) {
|
|
2900
|
+
for (const line of ct.stdout.split("\n")) {
|
|
2901
|
+
const srcMatch = line.match(/src=(\d+\.\d+\.\d+\.\d+)/);
|
|
2902
|
+
const dportMatch = line.match(/dport=(\d+)/);
|
|
2903
|
+
if (srcMatch && dportMatch) {
|
|
2904
|
+
records.push({ source_ip: srcMatch[1], dest_port: parseInt(dportMatch[1]), timestamp: now });
|
|
2905
|
+
}
|
|
2906
|
+
}
|
|
2907
|
+
if (records.length > 0) return records;
|
|
2908
|
+
}
|
|
2909
|
+
} catch {
|
|
2910
|
+
}
|
|
2911
|
+
try {
|
|
2912
|
+
const ss = (0, import_node_child_process2.spawnSync)("ss", ["-tnp", "-H"], {
|
|
2913
|
+
encoding: "utf-8",
|
|
2914
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2915
|
+
timeout: 3e3
|
|
2916
|
+
});
|
|
2917
|
+
if (ss.status === 0 && ss.stdout) {
|
|
2918
|
+
for (const line of ss.stdout.split("\n")) {
|
|
2919
|
+
const parts = line.trim().split(/\s+/);
|
|
2920
|
+
if (parts.length < 5) continue;
|
|
2921
|
+
const peerParts = parts[4].split(":");
|
|
2922
|
+
const localParts = parts[3].split(":");
|
|
2923
|
+
if (peerParts.length >= 2 && localParts.length >= 2) {
|
|
2924
|
+
const sourceIp = peerParts.slice(0, -1).join(":");
|
|
2925
|
+
const destPort = parseInt(localParts[localParts.length - 1]);
|
|
2926
|
+
if (sourceIp && !isNaN(destPort) && !this.isLocalIp(sourceIp)) {
|
|
2927
|
+
records.push({ source_ip: sourceIp, dest_port: destPort, timestamp: now });
|
|
2928
|
+
}
|
|
2929
|
+
}
|
|
2930
|
+
}
|
|
2931
|
+
}
|
|
2932
|
+
} catch {
|
|
2933
|
+
}
|
|
2934
|
+
return records;
|
|
2935
|
+
}
|
|
2936
|
+
analyzePortScans(connections) {
|
|
2937
|
+
const now = Date.now();
|
|
2938
|
+
for (const conn of connections) {
|
|
2939
|
+
const key = conn.source_ip;
|
|
2940
|
+
let tracker = this.scanTrackers.get(key);
|
|
2941
|
+
if (!tracker) {
|
|
2942
|
+
tracker = { ports: /* @__PURE__ */ new Set(), firstSeen: now, lastSeen: now, count: 0 };
|
|
2943
|
+
this.scanTrackers.set(key, tracker);
|
|
2944
|
+
}
|
|
2945
|
+
tracker.ports.add(conn.dest_port);
|
|
2946
|
+
tracker.lastSeen = now;
|
|
2947
|
+
tracker.count++;
|
|
2948
|
+
if (tracker.ports.size >= this.portScanThreshold && now - tracker.firstSeen <= this.portScanWindowMs) {
|
|
2949
|
+
this.emitEvent(
|
|
2950
|
+
"high",
|
|
2951
|
+
`Port scan detected: ${conn.source_ip} probed ${tracker.ports.size} ports in ${Math.round((now - tracker.firstSeen) / 1e3)}s`,
|
|
2952
|
+
conn.source_ip,
|
|
2953
|
+
{ ports_scanned: tracker.ports.size, window_seconds: Math.round((now - tracker.firstSeen) / 1e3) }
|
|
2954
|
+
);
|
|
2955
|
+
this.scanTrackers.delete(key);
|
|
2956
|
+
}
|
|
2957
|
+
}
|
|
2958
|
+
}
|
|
2959
|
+
analyzeSynFlood(connections) {
|
|
2960
|
+
try {
|
|
2961
|
+
const ss = (0, import_node_child_process2.spawnSync)("ss", ["-tn", "state", "syn-recv", "-H"], {
|
|
2962
|
+
encoding: "utf-8",
|
|
2963
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
2964
|
+
timeout: 3e3
|
|
2965
|
+
});
|
|
2966
|
+
if (ss.status !== 0 || !ss.stdout) return;
|
|
2967
|
+
const perSource = /* @__PURE__ */ new Map();
|
|
2968
|
+
for (const line of ss.stdout.split("\n")) {
|
|
2969
|
+
const parts = line.trim().split(/\s+/);
|
|
2970
|
+
if (parts.length < 5) continue;
|
|
2971
|
+
const peer = parts[4].split(":");
|
|
2972
|
+
const ip = peer.slice(0, -1).join(":");
|
|
2973
|
+
if (ip) perSource.set(ip, (perSource.get(ip) || 0) + 1);
|
|
2974
|
+
}
|
|
2975
|
+
for (const [ip, count] of perSource) {
|
|
2976
|
+
if (count >= this.synFloodThreshold) {
|
|
2977
|
+
this.emitEvent(
|
|
2978
|
+
"critical",
|
|
2979
|
+
`SYN flood indicators: ${count} half-open connections from ${ip}`,
|
|
2980
|
+
ip,
|
|
2981
|
+
{ half_open_count: count }
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
}
|
|
2985
|
+
} catch {
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
emitEvent(severity, message, sourceIp, details) {
|
|
2989
|
+
const event = {
|
|
2990
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
2991
|
+
module: "network-monitor",
|
|
2992
|
+
category: "network",
|
|
2993
|
+
severity,
|
|
2994
|
+
message,
|
|
2995
|
+
source_ip: sourceIp,
|
|
2996
|
+
details
|
|
2997
|
+
};
|
|
2998
|
+
try {
|
|
2999
|
+
insertEvent(event);
|
|
3000
|
+
} catch {
|
|
3001
|
+
}
|
|
3002
|
+
this.bus.publish(event);
|
|
3003
|
+
}
|
|
3004
|
+
cleanupTrackers() {
|
|
3005
|
+
const now = Date.now();
|
|
3006
|
+
for (const [key, tracker] of this.scanTrackers) {
|
|
3007
|
+
if (now - tracker.lastSeen > this.portScanWindowMs * 2) {
|
|
3008
|
+
this.scanTrackers.delete(key);
|
|
3009
|
+
}
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
isLocalIp(ip) {
|
|
3013
|
+
return ip === "127.0.0.1" || ip === "::1" || ip === "0.0.0.0" || ip.startsWith("::ffff:127.");
|
|
3014
|
+
}
|
|
3015
|
+
};
|
|
3016
|
+
|
|
3017
|
+
// src/modules/dns-monitor/index.ts
|
|
3018
|
+
var import_node_fs6 = require("fs");
|
|
3019
|
+
var import_node_readline2 = require("readline");
|
|
3020
|
+
var DNS_LOG_SOURCES = [
|
|
3021
|
+
"/var/log/syslog",
|
|
3022
|
+
// systemd-resolved logs here
|
|
3023
|
+
"/var/log/dnsmasq.log",
|
|
3024
|
+
// dnsmasq
|
|
3025
|
+
"/var/log/named/queries.log",
|
|
3026
|
+
// bind9
|
|
3027
|
+
"/var/log/pihole.log"
|
|
3028
|
+
// Pi-hole
|
|
3029
|
+
];
|
|
3030
|
+
var DnsMonitor = class {
|
|
3031
|
+
// Shannon entropy threshold for DGA
|
|
3032
|
+
constructor(bus2) {
|
|
3033
|
+
this.bus = bus2;
|
|
3034
|
+
}
|
|
3035
|
+
bus;
|
|
3036
|
+
active = false;
|
|
3037
|
+
timers = /* @__PURE__ */ new Map();
|
|
3038
|
+
positions = /* @__PURE__ */ new Map();
|
|
3039
|
+
// Tracking windows
|
|
3040
|
+
txtQueryCounts = /* @__PURE__ */ new Map();
|
|
3041
|
+
domainBuffer = [];
|
|
3042
|
+
// Config
|
|
3043
|
+
txtRateThreshold = 20;
|
|
3044
|
+
// TXT queries per source per window
|
|
3045
|
+
txtWindowMs = 6e4;
|
|
3046
|
+
dgaBurstThreshold = 15;
|
|
3047
|
+
// unique high-entropy domains per window
|
|
3048
|
+
dgaWindowMs = 6e4;
|
|
3049
|
+
entropyThreshold = 3.5;
|
|
3050
|
+
start() {
|
|
3051
|
+
const sources = DNS_LOG_SOURCES.filter((p) => {
|
|
3052
|
+
if (!(0, import_node_fs6.existsSync)(p)) return false;
|
|
3053
|
+
try {
|
|
3054
|
+
(0, import_node_fs6.accessSync)(p, import_node_fs6.constants.R_OK);
|
|
3055
|
+
return true;
|
|
3056
|
+
} catch {
|
|
3057
|
+
return false;
|
|
3058
|
+
}
|
|
3059
|
+
});
|
|
3060
|
+
if (sources.length === 0) return false;
|
|
3061
|
+
this.active = true;
|
|
3062
|
+
for (const src of sources) {
|
|
3063
|
+
this.tailLog(src);
|
|
3064
|
+
}
|
|
3065
|
+
setInterval(() => this.analyzeBuffer(), 1e4);
|
|
3066
|
+
return true;
|
|
3067
|
+
}
|
|
3068
|
+
stop() {
|
|
3069
|
+
for (const t of this.timers.values()) clearInterval(t);
|
|
3070
|
+
this.timers.clear();
|
|
3071
|
+
this.active = false;
|
|
3072
|
+
}
|
|
3073
|
+
isActive() {
|
|
3074
|
+
return this.active;
|
|
3075
|
+
}
|
|
3076
|
+
tailLog(path) {
|
|
3077
|
+
try {
|
|
3078
|
+
this.positions.set(path, (0, import_node_fs6.statSync)(path).size);
|
|
3079
|
+
} catch {
|
|
3080
|
+
this.positions.set(path, 0);
|
|
3081
|
+
}
|
|
3082
|
+
const timer = setInterval(() => this.pollLog(path), 2e3);
|
|
3083
|
+
this.timers.set(path, timer);
|
|
3084
|
+
}
|
|
3085
|
+
pollLog(path) {
|
|
3086
|
+
let stat;
|
|
3087
|
+
try {
|
|
3088
|
+
stat = (0, import_node_fs6.statSync)(path);
|
|
3089
|
+
} catch {
|
|
3090
|
+
return;
|
|
3091
|
+
}
|
|
3092
|
+
const prev = this.positions.get(path) ?? 0;
|
|
3093
|
+
if (stat.size < prev) {
|
|
3094
|
+
this.positions.set(path, 0);
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
if (stat.size === prev) return;
|
|
3098
|
+
const stream = (0, import_node_fs6.createReadStream)(path, { start: prev, encoding: "utf-8" });
|
|
3099
|
+
stream.on("error", () => this.positions.set(path, stat.size));
|
|
3100
|
+
const rl = (0, import_node_readline2.createInterface)({ input: stream });
|
|
3101
|
+
rl.on("line", (line) => this.parseDnsLine(line));
|
|
3102
|
+
rl.on("close", () => this.positions.set(path, stat.size));
|
|
3103
|
+
}
|
|
3104
|
+
parseDnsLine(line) {
|
|
3105
|
+
const resolvedMatch = line.match(/query\[(\w+)\]\s+(\S+)\s+from\s+(\S+)/i);
|
|
3106
|
+
if (resolvedMatch) {
|
|
3107
|
+
this.domainBuffer.push({
|
|
3108
|
+
type: resolvedMatch[1],
|
|
3109
|
+
domain: resolvedMatch[2],
|
|
3110
|
+
source_ip: resolvedMatch[3],
|
|
3111
|
+
timestamp: Date.now()
|
|
3112
|
+
});
|
|
3113
|
+
return;
|
|
3114
|
+
}
|
|
3115
|
+
const dnsmasqMatch = line.match(/query\[(\w+)\]\s+(\S+)\s+from\s+(\S+)/i);
|
|
3116
|
+
if (dnsmasqMatch) {
|
|
3117
|
+
this.domainBuffer.push({
|
|
3118
|
+
type: dnsmasqMatch[1],
|
|
3119
|
+
domain: dnsmasqMatch[2],
|
|
3120
|
+
source_ip: dnsmasqMatch[3],
|
|
3121
|
+
timestamp: Date.now()
|
|
3122
|
+
});
|
|
3123
|
+
return;
|
|
3124
|
+
}
|
|
3125
|
+
const genericMatch = line.match(/(?:query|lookup|resolve)[:\s]+(\S+)/i);
|
|
3126
|
+
if (genericMatch) {
|
|
3127
|
+
const typeMatch = line.match(/type[:\s]+(\w+)/i);
|
|
3128
|
+
this.domainBuffer.push({
|
|
3129
|
+
type: typeMatch?.[1] || "A",
|
|
3130
|
+
domain: genericMatch[1],
|
|
3131
|
+
timestamp: Date.now()
|
|
3132
|
+
});
|
|
3133
|
+
}
|
|
3134
|
+
}
|
|
3135
|
+
analyzeBuffer() {
|
|
3136
|
+
const now = Date.now();
|
|
3137
|
+
const cutoff = now - this.txtWindowMs;
|
|
3138
|
+
this.domainBuffer = this.domainBuffer.filter((q) => q.timestamp > cutoff);
|
|
3139
|
+
this.detectTunneling();
|
|
3140
|
+
this.detectDga();
|
|
3141
|
+
}
|
|
3142
|
+
detectTunneling() {
|
|
3143
|
+
const txtBySource = /* @__PURE__ */ new Map();
|
|
3144
|
+
const longLabelDomains = [];
|
|
3145
|
+
for (const q of this.domainBuffer) {
|
|
3146
|
+
if (q.type === "TXT") {
|
|
3147
|
+
const key = q.source_ip || "unknown";
|
|
3148
|
+
txtBySource.set(key, (txtBySource.get(key) || 0) + 1);
|
|
3149
|
+
}
|
|
3150
|
+
const labels = q.domain.split(".");
|
|
3151
|
+
const maxLabel = Math.max(...labels.map((l) => l.length));
|
|
3152
|
+
if (maxLabel > 50) {
|
|
3153
|
+
longLabelDomains.push(q.domain);
|
|
3154
|
+
}
|
|
3155
|
+
}
|
|
3156
|
+
for (const [source, count] of txtBySource) {
|
|
3157
|
+
if (count >= this.txtRateThreshold) {
|
|
3158
|
+
this.emitEvent(
|
|
3159
|
+
"high",
|
|
3160
|
+
`DNS tunneling indicators: ${count} TXT queries from ${source} in ${this.txtWindowMs / 1e3}s`,
|
|
3161
|
+
source !== "unknown" ? source : void 0,
|
|
3162
|
+
{ txt_query_count: count, type: "tunneling" }
|
|
3163
|
+
);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
if (longLabelDomains.length >= 5) {
|
|
3167
|
+
this.emitEvent(
|
|
3168
|
+
"high",
|
|
3169
|
+
`DNS tunneling: ${longLabelDomains.length} queries with abnormally long labels detected`,
|
|
3170
|
+
void 0,
|
|
3171
|
+
{ domains: longLabelDomains.slice(0, 5), type: "tunneling-labels" }
|
|
3172
|
+
);
|
|
3173
|
+
}
|
|
3174
|
+
}
|
|
3175
|
+
detectDga() {
|
|
3176
|
+
const highEntropyDomains = [];
|
|
3177
|
+
for (const q of this.domainBuffer) {
|
|
3178
|
+
const domain = q.domain.toLowerCase();
|
|
3179
|
+
const parts = domain.split(".");
|
|
3180
|
+
if (parts.length < 2) continue;
|
|
3181
|
+
const sld = parts[parts.length - 2];
|
|
3182
|
+
if (sld.length >= 8 && this.shannonEntropy(sld) >= this.entropyThreshold) {
|
|
3183
|
+
highEntropyDomains.push(domain);
|
|
3184
|
+
}
|
|
3185
|
+
}
|
|
3186
|
+
const unique = [...new Set(highEntropyDomains)];
|
|
3187
|
+
if (unique.length >= this.dgaBurstThreshold) {
|
|
3188
|
+
this.emitEvent(
|
|
3189
|
+
"critical",
|
|
3190
|
+
`DGA-like domain burst: ${unique.length} unique high-entropy domains detected`,
|
|
3191
|
+
void 0,
|
|
3192
|
+
{ sample_domains: unique.slice(0, 10), type: "dga", unique_count: unique.length }
|
|
3193
|
+
);
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
shannonEntropy(str) {
|
|
3197
|
+
const freq = /* @__PURE__ */ new Map();
|
|
3198
|
+
for (const ch of str) {
|
|
3199
|
+
freq.set(ch, (freq.get(ch) || 0) + 1);
|
|
3200
|
+
}
|
|
3201
|
+
let entropy = 0;
|
|
3202
|
+
for (const count of freq.values()) {
|
|
3203
|
+
const p = count / str.length;
|
|
3204
|
+
if (p > 0) entropy -= p * Math.log2(p);
|
|
3205
|
+
}
|
|
3206
|
+
return entropy;
|
|
3207
|
+
}
|
|
3208
|
+
emitEvent(severity, message, sourceIp, details) {
|
|
3209
|
+
const event = {
|
|
3210
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3211
|
+
module: "dns-monitor",
|
|
3212
|
+
category: "network",
|
|
3213
|
+
severity,
|
|
3214
|
+
message,
|
|
3215
|
+
source_ip: sourceIp,
|
|
3216
|
+
details
|
|
3217
|
+
};
|
|
3218
|
+
try {
|
|
3219
|
+
insertEvent(event);
|
|
3220
|
+
} catch {
|
|
3221
|
+
}
|
|
3222
|
+
this.bus.publish(event);
|
|
3223
|
+
}
|
|
3224
|
+
};
|
|
3225
|
+
|
|
3226
|
+
// src/core/config.ts
|
|
3227
|
+
var import_node_fs7 = require("fs");
|
|
2830
3228
|
var import_node_path2 = require("path");
|
|
2831
3229
|
var import_toml = __toESM(require_toml());
|
|
2832
3230
|
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
@@ -2853,11 +3251,11 @@ var DEFAULT_CONFIG = {
|
|
|
2853
3251
|
};
|
|
2854
3252
|
function loadConfig(configPath) {
|
|
2855
3253
|
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
2856
|
-
if (!(0,
|
|
3254
|
+
if (!(0, import_node_fs7.existsSync)(path)) {
|
|
2857
3255
|
return { ...DEFAULT_CONFIG };
|
|
2858
3256
|
}
|
|
2859
3257
|
try {
|
|
2860
|
-
const raw = (0,
|
|
3258
|
+
const raw = (0, import_node_fs7.readFileSync)(path, "utf-8");
|
|
2861
3259
|
const parsed = import_toml.default.parse(raw);
|
|
2862
3260
|
return {
|
|
2863
3261
|
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
@@ -2873,13 +3271,13 @@ function loadConfig(configPath) {
|
|
|
2873
3271
|
function loadModuleConfigs(confDir) {
|
|
2874
3272
|
const dir = confDir || DEFAULT_CONFDIR;
|
|
2875
3273
|
const configs = /* @__PURE__ */ new Map();
|
|
2876
|
-
if (!(0,
|
|
3274
|
+
if (!(0, import_node_fs7.existsSync)(dir)) {
|
|
2877
3275
|
return configs;
|
|
2878
3276
|
}
|
|
2879
|
-
const files = (0,
|
|
3277
|
+
const files = (0, import_node_fs7.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
|
|
2880
3278
|
for (const file of files) {
|
|
2881
3279
|
try {
|
|
2882
|
-
const raw = (0,
|
|
3280
|
+
const raw = (0, import_node_fs7.readFileSync)((0, import_node_path2.join)(dir, file), "utf-8");
|
|
2883
3281
|
const parsed = import_toml.default.parse(raw);
|
|
2884
3282
|
for (const [name, config] of Object.entries(parsed)) {
|
|
2885
3283
|
configs.set(name, config);
|
|
@@ -2911,6 +3309,8 @@ var ModuleHost = class {
|
|
|
2911
3309
|
modules = /* @__PURE__ */ new Map();
|
|
2912
3310
|
logWatcher = null;
|
|
2913
3311
|
journalWatcher = null;
|
|
3312
|
+
networkMonitor = null;
|
|
3313
|
+
dnsMonitor = null;
|
|
2914
3314
|
async start() {
|
|
2915
3315
|
this.registerBuiltins();
|
|
2916
3316
|
await this.discoverAndStartInstalled();
|
|
@@ -2929,14 +3329,34 @@ var ModuleHost = class {
|
|
|
2929
3329
|
const mod = this.modules.get("user-journal");
|
|
2930
3330
|
if (mod) {
|
|
2931
3331
|
mod.status = "running";
|
|
2932
|
-
mod.detail =
|
|
3332
|
+
mod.detail = `tailing ${JournalWatcher.scopeArgs().includes("--user") ? "user journal" : "system journal"}`;
|
|
2933
3333
|
this.bus.announceModule("user-journal", "running", mod.detail);
|
|
2934
3334
|
}
|
|
2935
3335
|
}
|
|
3336
|
+
this.networkMonitor = new NetworkMonitor(this.bus);
|
|
3337
|
+
if (this.networkMonitor.start()) {
|
|
3338
|
+
const nmod = this.modules.get("network-monitor");
|
|
3339
|
+
if (nmod) {
|
|
3340
|
+
nmod.status = "running";
|
|
3341
|
+
nmod.detail = "monitoring connections via conntrack/ss";
|
|
3342
|
+
this.bus.announceModule("network-monitor", "running", nmod.detail);
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
this.dnsMonitor = new DnsMonitor(this.bus);
|
|
3346
|
+
if (this.dnsMonitor.start()) {
|
|
3347
|
+
const dmod = this.modules.get("dns-monitor");
|
|
3348
|
+
if (dmod) {
|
|
3349
|
+
dmod.status = "running";
|
|
3350
|
+
dmod.detail = "monitoring DNS queries";
|
|
3351
|
+
this.bus.announceModule("dns-monitor", "running", dmod.detail);
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
2936
3354
|
}
|
|
2937
3355
|
async stop() {
|
|
2938
3356
|
this.logWatcher?.stop();
|
|
2939
3357
|
this.journalWatcher?.stop();
|
|
3358
|
+
this.networkMonitor?.stop();
|
|
3359
|
+
this.dnsMonitor?.stop();
|
|
2940
3360
|
for (const mod of this.modules.values()) {
|
|
2941
3361
|
try {
|
|
2942
3362
|
if (mod.instance && mod.status === "running") {
|
|
@@ -2964,20 +3384,22 @@ var ModuleHost = class {
|
|
|
2964
3384
|
const builtins = [
|
|
2965
3385
|
{ name: "log-watcher", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
|
|
2966
3386
|
{ name: "ssh-guard", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
|
|
2967
|
-
{ name: "user-journal", version: "0.1.0", source: "builtin", status: "loaded", events: 0 }
|
|
3387
|
+
{ name: "user-journal", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
|
|
3388
|
+
{ name: "network-monitor", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
|
|
3389
|
+
{ name: "dns-monitor", version: "0.1.0", source: "builtin", status: "loaded", events: 0 }
|
|
2968
3390
|
];
|
|
2969
3391
|
for (const m of builtins) this.modules.set(m.name, m);
|
|
2970
3392
|
}
|
|
2971
3393
|
async discoverAndStartInstalled() {
|
|
2972
|
-
if (!(0,
|
|
3394
|
+
if (!(0, import_node_fs8.existsSync)(PATHS.moduleDir)) return;
|
|
2973
3395
|
const configs = loadModuleConfigs(PATHS.confD);
|
|
2974
|
-
const entries = (0,
|
|
3396
|
+
const entries = (0, import_node_fs8.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
|
|
2975
3397
|
for (const entry of entries) {
|
|
2976
3398
|
if (!entry.isDirectory()) continue;
|
|
2977
3399
|
const manifestPath = (0, import_node_path3.join)(PATHS.moduleDir, entry.name, "mod.toml");
|
|
2978
|
-
if (!(0,
|
|
3400
|
+
if (!(0, import_node_fs8.existsSync)(manifestPath)) continue;
|
|
2979
3401
|
try {
|
|
2980
|
-
const manifest = import_toml2.default.parse((0,
|
|
3402
|
+
const manifest = import_toml2.default.parse((0, import_node_fs8.readFileSync)(manifestPath, "utf-8"));
|
|
2981
3403
|
const name = manifest.module?.name || entry.name;
|
|
2982
3404
|
const defaults = manifest.module?.config?.defaults || {};
|
|
2983
3405
|
const config = {
|
|
@@ -3040,15 +3462,15 @@ var ModuleHost = class {
|
|
|
3040
3462
|
installedEntrypoint(modulePath) {
|
|
3041
3463
|
const packageJson = (0, import_node_path3.join)(modulePath, "package.json");
|
|
3042
3464
|
const candidates = [];
|
|
3043
|
-
if ((0,
|
|
3465
|
+
if ((0, import_node_fs8.existsSync)(packageJson)) {
|
|
3044
3466
|
try {
|
|
3045
|
-
const pkg = JSON.parse((0,
|
|
3467
|
+
const pkg = JSON.parse((0, import_node_fs8.readFileSync)(packageJson, "utf-8"));
|
|
3046
3468
|
if (pkg.main) candidates.push((0, import_node_path3.join)(modulePath, pkg.main));
|
|
3047
3469
|
} catch {
|
|
3048
3470
|
}
|
|
3049
3471
|
}
|
|
3050
3472
|
candidates.push((0, import_node_path3.join)(modulePath, "dist", "index.js"), (0, import_node_path3.join)(modulePath, "index.js"));
|
|
3051
|
-
return candidates.find((candidate) => (0,
|
|
3473
|
+
return candidates.find((candidate) => (0, import_node_fs8.existsSync)(candidate)) || null;
|
|
3052
3474
|
}
|
|
3053
3475
|
isThreatCrushModule(value) {
|
|
3054
3476
|
return Boolean(
|
|
@@ -3152,6 +3574,98 @@ function smtpChannel(config) {
|
|
|
3152
3574
|
};
|
|
3153
3575
|
}
|
|
3154
3576
|
|
|
3577
|
+
// src/daemon/alerts/discord.ts
|
|
3578
|
+
var SEVERITY_RANK2 = {
|
|
3579
|
+
info: 0,
|
|
3580
|
+
low: 1,
|
|
3581
|
+
medium: 2,
|
|
3582
|
+
high: 3,
|
|
3583
|
+
critical: 4
|
|
3584
|
+
};
|
|
3585
|
+
var SEVERITY_COLORS = {
|
|
3586
|
+
info: 3066993,
|
|
3587
|
+
// green
|
|
3588
|
+
low: 3447003,
|
|
3589
|
+
// blue
|
|
3590
|
+
medium: 15965202,
|
|
3591
|
+
// orange
|
|
3592
|
+
high: 15158332,
|
|
3593
|
+
// red
|
|
3594
|
+
critical: 10181046
|
|
3595
|
+
// purple
|
|
3596
|
+
};
|
|
3597
|
+
function discordChannel(config) {
|
|
3598
|
+
return async (event) => {
|
|
3599
|
+
if (config.min_severity) {
|
|
3600
|
+
const eventRank = SEVERITY_RANK2[event.severity] ?? 0;
|
|
3601
|
+
const minRank = SEVERITY_RANK2[config.min_severity] ?? 0;
|
|
3602
|
+
if (eventRank < minRank) return;
|
|
3603
|
+
}
|
|
3604
|
+
const embed = {
|
|
3605
|
+
title: `${event.severity === "critical" ? "\u{1F6A8}" : "\u26A0\uFE0F"} [${event.severity.toUpperCase()}] ${event.module}`,
|
|
3606
|
+
description: event.message,
|
|
3607
|
+
color: SEVERITY_COLORS[event.severity] ?? 16777215,
|
|
3608
|
+
fields: [
|
|
3609
|
+
...event.source_ip ? [{ name: "Source IP", value: `\`${event.source_ip}\``, inline: true }] : [],
|
|
3610
|
+
{ name: "Category", value: event.category, inline: true },
|
|
3611
|
+
{ name: "Time", value: event.timestamp.toISOString(), inline: true }
|
|
3612
|
+
],
|
|
3613
|
+
footer: { text: "ThreatCrush Security Alert" }
|
|
3614
|
+
};
|
|
3615
|
+
await fetch(config.webhook_url, {
|
|
3616
|
+
method: "POST",
|
|
3617
|
+
headers: { "Content-Type": "application/json" },
|
|
3618
|
+
body: JSON.stringify({ embeds: [embed] })
|
|
3619
|
+
});
|
|
3620
|
+
};
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
// src/daemon/alerts/pagerduty.ts
|
|
3624
|
+
var SEVERITY_RANK3 = {
|
|
3625
|
+
info: 0,
|
|
3626
|
+
low: 1,
|
|
3627
|
+
medium: 2,
|
|
3628
|
+
high: 3,
|
|
3629
|
+
critical: 4
|
|
3630
|
+
};
|
|
3631
|
+
var PD_SEVERITY = {
|
|
3632
|
+
info: "info",
|
|
3633
|
+
low: "info",
|
|
3634
|
+
medium: "warning",
|
|
3635
|
+
high: "error",
|
|
3636
|
+
critical: "critical"
|
|
3637
|
+
};
|
|
3638
|
+
function pagerdutyChannel(config) {
|
|
3639
|
+
return async (event) => {
|
|
3640
|
+
if (config.min_severity) {
|
|
3641
|
+
const eventRank = SEVERITY_RANK3[event.severity] ?? 0;
|
|
3642
|
+
const minRank = SEVERITY_RANK3[config.min_severity] ?? 0;
|
|
3643
|
+
if (eventRank < minRank) return;
|
|
3644
|
+
}
|
|
3645
|
+
const payload = {
|
|
3646
|
+
routing_key: config.routing_key,
|
|
3647
|
+
event_action: "trigger",
|
|
3648
|
+
payload: {
|
|
3649
|
+
summary: `[${event.severity.toUpperCase()}] ${event.module}: ${event.message}`,
|
|
3650
|
+
source: "threatcrush",
|
|
3651
|
+
severity: PD_SEVERITY[event.severity] || "warning",
|
|
3652
|
+
timestamp: event.timestamp.toISOString(),
|
|
3653
|
+
custom_details: {
|
|
3654
|
+
module: event.module,
|
|
3655
|
+
category: event.category,
|
|
3656
|
+
source_ip: event.source_ip,
|
|
3657
|
+
details: event.details
|
|
3658
|
+
}
|
|
3659
|
+
}
|
|
3660
|
+
};
|
|
3661
|
+
await fetch("https://events.pagerduty.com/v2/enqueue", {
|
|
3662
|
+
method: "POST",
|
|
3663
|
+
headers: { "Content-Type": "application/json" },
|
|
3664
|
+
body: JSON.stringify(payload)
|
|
3665
|
+
});
|
|
3666
|
+
};
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3155
3669
|
// src/daemon/alerts/index.ts
|
|
3156
3670
|
var AlertDispatcher = class {
|
|
3157
3671
|
constructor(bus2, config) {
|
|
@@ -3165,6 +3679,7 @@ var AlertDispatcher = class {
|
|
|
3165
3679
|
bus;
|
|
3166
3680
|
config;
|
|
3167
3681
|
channels = [];
|
|
3682
|
+
rateLimits = /* @__PURE__ */ new Map();
|
|
3168
3683
|
bindChannels() {
|
|
3169
3684
|
const alerts = this.config.alerts || {};
|
|
3170
3685
|
for (const [name, raw] of Object.entries(alerts)) {
|
|
@@ -3179,11 +3694,31 @@ var AlertDispatcher = class {
|
|
|
3179
3694
|
if (name === "email" && typeof cfg.host === "string" && typeof cfg.from === "string") {
|
|
3180
3695
|
this.channels.push(smtpChannel(cfg));
|
|
3181
3696
|
}
|
|
3697
|
+
if (name === "discord" && typeof cfg.webhook_url === "string") {
|
|
3698
|
+
this.channels.push(discordChannel(cfg));
|
|
3699
|
+
}
|
|
3700
|
+
if (name === "pagerduty" && typeof cfg.routing_key === "string") {
|
|
3701
|
+
this.channels.push(pagerdutyChannel(cfg));
|
|
3702
|
+
}
|
|
3182
3703
|
}
|
|
3183
3704
|
}
|
|
3705
|
+
checkRateLimit(channelIdx, maxPerHour = 60) {
|
|
3706
|
+
const key = String(channelIdx);
|
|
3707
|
+
const now = Date.now();
|
|
3708
|
+
const hour = 36e5;
|
|
3709
|
+
let timestamps = this.rateLimits.get(key) || [];
|
|
3710
|
+
timestamps = timestamps.filter((t) => t > now - hour);
|
|
3711
|
+
if (timestamps.length >= maxPerHour) return false;
|
|
3712
|
+
timestamps.push(now);
|
|
3713
|
+
this.rateLimits.set(key, timestamps);
|
|
3714
|
+
return true;
|
|
3715
|
+
}
|
|
3184
3716
|
async dispatch(event) {
|
|
3185
|
-
await Promise.all(this.channels.map((ch
|
|
3186
|
-
|
|
3717
|
+
await Promise.all(this.channels.map((ch, idx) => {
|
|
3718
|
+
if (!this.checkRateLimit(idx)) return Promise.resolve();
|
|
3719
|
+
return ch(event).catch(() => {
|
|
3720
|
+
});
|
|
3721
|
+
}));
|
|
3187
3722
|
}
|
|
3188
3723
|
};
|
|
3189
3724
|
function webhookChannel(url, secret) {
|
|
@@ -3207,14 +3742,14 @@ function slackChannel(webhookUrl) {
|
|
|
3207
3742
|
}
|
|
3208
3743
|
|
|
3209
3744
|
// src/core/cli-config.ts
|
|
3210
|
-
var
|
|
3745
|
+
var import_node_fs9 = require("fs");
|
|
3211
3746
|
var import_node_path4 = require("path");
|
|
3212
3747
|
var import_node_os2 = require("os");
|
|
3213
3748
|
var CLI_CONFIG_DIR = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".threatcrush");
|
|
3214
3749
|
var CLI_CONFIG_PATH = (0, import_node_path4.join)(CLI_CONFIG_DIR, "config.json");
|
|
3215
3750
|
function readCliConfig() {
|
|
3216
3751
|
try {
|
|
3217
|
-
return JSON.parse((0,
|
|
3752
|
+
return JSON.parse((0, import_node_fs9.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
|
|
3218
3753
|
} catch {
|
|
3219
3754
|
return {};
|
|
3220
3755
|
}
|
|
@@ -3233,7 +3768,7 @@ function authHeaders() {
|
|
|
3233
3768
|
}
|
|
3234
3769
|
|
|
3235
3770
|
// src/commands/scan.ts
|
|
3236
|
-
var
|
|
3771
|
+
var import_node_fs10 = require("fs");
|
|
3237
3772
|
var import_node_path5 = require("path");
|
|
3238
3773
|
|
|
3239
3774
|
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
@@ -3732,7 +4267,7 @@ var chalkStderr = createChalk({ level: stderrColor ? stderrColor.level : 0 });
|
|
|
3732
4267
|
var source_default = chalk;
|
|
3733
4268
|
|
|
3734
4269
|
// src/core/logger.ts
|
|
3735
|
-
var
|
|
4270
|
+
var SEVERITY_COLORS2 = {
|
|
3736
4271
|
info: source_default.green,
|
|
3737
4272
|
low: source_default.cyan,
|
|
3738
4273
|
medium: source_default.yellow,
|
|
@@ -3833,6 +4368,8 @@ async function runScan(targetPath) {
|
|
|
3833
4368
|
try {
|
|
3834
4369
|
scanDirectory(targetPath, targetPath, findings, () => {
|
|
3835
4370
|
});
|
|
4371
|
+
const depFindings = await scanDependencies(targetPath);
|
|
4372
|
+
findings.push(...depFindings);
|
|
3836
4373
|
} catch (err) {
|
|
3837
4374
|
return {
|
|
3838
4375
|
type: "scan",
|
|
@@ -3862,7 +4399,7 @@ async function runScan(targetPath) {
|
|
|
3862
4399
|
function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
3863
4400
|
let entries;
|
|
3864
4401
|
try {
|
|
3865
|
-
entries = (0,
|
|
4402
|
+
entries = (0, import_node_fs10.readdirSync)(currentPath, { withFileTypes: true });
|
|
3866
4403
|
} catch {
|
|
3867
4404
|
return;
|
|
3868
4405
|
}
|
|
@@ -3889,7 +4426,7 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
|
3889
4426
|
const ext = (0, import_node_path5.extname)(entry.name).toLowerCase();
|
|
3890
4427
|
if (!SCAN_EXTENSIONS.has(ext) && !entry.name.startsWith(".env")) continue;
|
|
3891
4428
|
try {
|
|
3892
|
-
const stat = (0,
|
|
4429
|
+
const stat = (0, import_node_fs10.statSync)(fullPath);
|
|
3893
4430
|
if (stat.size > 1024 * 1024) continue;
|
|
3894
4431
|
} catch {
|
|
3895
4432
|
continue;
|
|
@@ -3897,7 +4434,7 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
|
3897
4434
|
onFile();
|
|
3898
4435
|
let content;
|
|
3899
4436
|
try {
|
|
3900
|
-
content = (0,
|
|
4437
|
+
content = (0, import_node_fs10.readFileSync)(fullPath, "utf-8");
|
|
3901
4438
|
} catch {
|
|
3902
4439
|
continue;
|
|
3903
4440
|
}
|
|
@@ -3921,6 +4458,92 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
|
3921
4458
|
}
|
|
3922
4459
|
}
|
|
3923
4460
|
}
|
|
4461
|
+
async function scanDependencies(targetPath) {
|
|
4462
|
+
const findings = [];
|
|
4463
|
+
const lockfiles = [
|
|
4464
|
+
{ file: "package-lock.json", ecosystem: "npm" },
|
|
4465
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
4466
|
+
{ file: "yarn.lock", ecosystem: "npm" },
|
|
4467
|
+
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
4468
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
4469
|
+
];
|
|
4470
|
+
for (const { file, ecosystem } of lockfiles) {
|
|
4471
|
+
const lockPath = (0, import_node_path5.join)(targetPath, file);
|
|
4472
|
+
if (!(0, import_node_fs10.existsSync)(lockPath)) continue;
|
|
4473
|
+
try {
|
|
4474
|
+
const deps = parseDependencies(lockPath, file, ecosystem);
|
|
4475
|
+
for (const dep of deps.slice(0, 50)) {
|
|
4476
|
+
try {
|
|
4477
|
+
const vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
4478
|
+
for (const vuln of vulns) {
|
|
4479
|
+
const cvssScore = vuln.severity?.find((s) => s.type === "CVSS_V3")?.score;
|
|
4480
|
+
const severity = cvssScore ? parseFloat(cvssScore) >= 9 ? "critical" : parseFloat(cvssScore) >= 7 ? "high" : parseFloat(cvssScore) >= 4 ? "medium" : "low" : "medium";
|
|
4481
|
+
findings.push({
|
|
4482
|
+
file,
|
|
4483
|
+
line: 0,
|
|
4484
|
+
type: "Dependency CVE",
|
|
4485
|
+
severity,
|
|
4486
|
+
message: `${dep.name}@${dep.version}: ${vuln.summary || vuln.id}`,
|
|
4487
|
+
snippet: `${vuln.id}${cvssScore ? ` (CVSS: ${cvssScore})` : ""}`
|
|
4488
|
+
});
|
|
4489
|
+
}
|
|
4490
|
+
} catch {
|
|
4491
|
+
}
|
|
4492
|
+
}
|
|
4493
|
+
} catch {
|
|
4494
|
+
}
|
|
4495
|
+
}
|
|
4496
|
+
return findings;
|
|
4497
|
+
}
|
|
4498
|
+
function parseDependencies(lockPath, filename, ecosystem) {
|
|
4499
|
+
const deps = [];
|
|
4500
|
+
if (filename === "package-lock.json") {
|
|
4501
|
+
try {
|
|
4502
|
+
const lock = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf-8"));
|
|
4503
|
+
const packages = lock.packages || lock.dependencies || {};
|
|
4504
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
4505
|
+
const name = key.replace(/^node_modules\//, "");
|
|
4506
|
+
const version = value.version;
|
|
4507
|
+
if (name && version && !name.startsWith(".")) {
|
|
4508
|
+
deps.push({ name, version });
|
|
4509
|
+
}
|
|
4510
|
+
}
|
|
4511
|
+
} catch {
|
|
4512
|
+
}
|
|
4513
|
+
} else if (filename === "requirements.txt") {
|
|
4514
|
+
try {
|
|
4515
|
+
const content = (0, import_node_fs10.readFileSync)(lockPath, "utf-8");
|
|
4516
|
+
for (const line of content.split("\n")) {
|
|
4517
|
+
const match = line.match(/^([a-zA-Z0-9_.-]+)==([0-9.]+)/);
|
|
4518
|
+
if (match) deps.push({ name: match[1], version: match[2] });
|
|
4519
|
+
}
|
|
4520
|
+
} catch {
|
|
4521
|
+
}
|
|
4522
|
+
}
|
|
4523
|
+
return deps;
|
|
4524
|
+
}
|
|
4525
|
+
function isValidPackageName(name) {
|
|
4526
|
+
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
4527
|
+
}
|
|
4528
|
+
function isValidVersion(version) {
|
|
4529
|
+
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
4530
|
+
}
|
|
4531
|
+
async function queryOsv(name, version, ecosystem) {
|
|
4532
|
+
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
4533
|
+
try {
|
|
4534
|
+
const res = await fetch("https://api.osv.dev/v1/query", {
|
|
4535
|
+
method: "POST",
|
|
4536
|
+
headers: { "Content-Type": "application/json" },
|
|
4537
|
+
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
4538
|
+
signal: AbortSignal.timeout(5e3)
|
|
4539
|
+
});
|
|
4540
|
+
if (!res.ok) return [];
|
|
4541
|
+
const data = await res.json();
|
|
4542
|
+
return data.vulns || [];
|
|
4543
|
+
} catch {
|
|
4544
|
+
return [];
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
3924
4547
|
|
|
3925
4548
|
// src/commands/pentest.ts
|
|
3926
4549
|
var PENTEST_CHECKS = [
|
|
@@ -3965,6 +4588,39 @@ var PENTEST_CHECKS = [
|
|
|
3965
4588
|
test: (html) => /stack trace|traceback|exception|at\s+\w+\.\w+\(/i.test(html),
|
|
3966
4589
|
severity: "medium",
|
|
3967
4590
|
message: "Error page reveals internal information"
|
|
4591
|
+
},
|
|
4592
|
+
// PRD 07: Additional checks
|
|
4593
|
+
{
|
|
4594
|
+
name: "CORS Misconfiguration",
|
|
4595
|
+
test: (_url, _body, headers) => {
|
|
4596
|
+
const acao = headers["access-control-allow-origin"];
|
|
4597
|
+
return acao === "*" || acao === "null";
|
|
4598
|
+
},
|
|
4599
|
+
severity: "medium",
|
|
4600
|
+
message: "CORS allows any origin (Access-Control-Allow-Origin: *)"
|
|
4601
|
+
},
|
|
4602
|
+
{
|
|
4603
|
+
name: "Cookie Security",
|
|
4604
|
+
test: (_url, _body, headers) => {
|
|
4605
|
+
const setCookie = headers["set-cookie"] || "";
|
|
4606
|
+
return setCookie.length > 0 && (!setCookie.includes("HttpOnly") || !setCookie.includes("Secure"));
|
|
4607
|
+
},
|
|
4608
|
+
severity: "medium",
|
|
4609
|
+
message: "Cookies missing HttpOnly or Secure flags"
|
|
4610
|
+
},
|
|
4611
|
+
{
|
|
4612
|
+
name: "Content Security Policy",
|
|
4613
|
+
test: (_url, _body, headers) => {
|
|
4614
|
+
return !headers["content-security-policy"];
|
|
4615
|
+
},
|
|
4616
|
+
severity: "low",
|
|
4617
|
+
message: "No Content-Security-Policy header set"
|
|
4618
|
+
},
|
|
4619
|
+
{
|
|
4620
|
+
name: "Sensitive Path Exposure",
|
|
4621
|
+
test: (html) => /\.env|wp-admin|phpinfo|\.git\/config|server-status/i.test(html),
|
|
4622
|
+
severity: "high",
|
|
4623
|
+
message: "Response references sensitive paths or admin endpoints"
|
|
3968
4624
|
}
|
|
3969
4625
|
];
|
|
3970
4626
|
async function runPentest(rawUrl) {
|
|
@@ -4187,6 +4843,717 @@ var RunsWorker = class {
|
|
|
4187
4843
|
}
|
|
4188
4844
|
};
|
|
4189
4845
|
|
|
4846
|
+
// src/daemon/rules/engine.ts
|
|
4847
|
+
var RuleEngine = class {
|
|
4848
|
+
constructor(onDetection) {
|
|
4849
|
+
this.onDetection = onDetection;
|
|
4850
|
+
}
|
|
4851
|
+
onDetection;
|
|
4852
|
+
rules = [];
|
|
4853
|
+
windows = /* @__PURE__ */ new Map();
|
|
4854
|
+
loadRules(rules) {
|
|
4855
|
+
this.rules = rules.filter((r) => r.enabled !== false);
|
|
4856
|
+
}
|
|
4857
|
+
getRules() {
|
|
4858
|
+
return [...this.rules];
|
|
4859
|
+
}
|
|
4860
|
+
evaluate(event) {
|
|
4861
|
+
const now = Date.now();
|
|
4862
|
+
for (const rule of this.rules) {
|
|
4863
|
+
if (rule.source_types.length > 0 && !rule.source_types.includes(event.module) && !rule.source_types.includes(event.category)) {
|
|
4864
|
+
continue;
|
|
4865
|
+
}
|
|
4866
|
+
if (!this.matchesCondition(event, rule.match)) continue;
|
|
4867
|
+
const windowKey = `${rule.id}:${event.source_ip || "global"}`;
|
|
4868
|
+
let window = this.windows.get(windowKey);
|
|
4869
|
+
if (!window) {
|
|
4870
|
+
window = { events: [], lastAlert: 0 };
|
|
4871
|
+
this.windows.set(windowKey, window);
|
|
4872
|
+
}
|
|
4873
|
+
window.events.push({ timestamp: now, event });
|
|
4874
|
+
const cutoff = now - rule.window_seconds * 1e3;
|
|
4875
|
+
window.events = window.events.filter((e) => e.timestamp >= cutoff);
|
|
4876
|
+
if (window.events.length < rule.threshold) continue;
|
|
4877
|
+
if (window.lastAlert > 0 && now - window.lastAlert < rule.cooldown_seconds * 1e3) continue;
|
|
4878
|
+
window.lastAlert = now;
|
|
4879
|
+
window.events = [];
|
|
4880
|
+
this.onDetection({
|
|
4881
|
+
rule_id: rule.id,
|
|
4882
|
+
severity: rule.severity,
|
|
4883
|
+
title: rule.title,
|
|
4884
|
+
description: `${rule.description} (${rule.threshold} events in ${rule.window_seconds}s)`,
|
|
4885
|
+
source_ip: event.source_ip,
|
|
4886
|
+
username: event.details?.user || void 0,
|
|
4887
|
+
raw_metadata: {
|
|
4888
|
+
rule_version: rule.version,
|
|
4889
|
+
tags: rule.tags,
|
|
4890
|
+
category: rule.category,
|
|
4891
|
+
remediation: rule.remediation
|
|
4892
|
+
}
|
|
4893
|
+
});
|
|
4894
|
+
}
|
|
4895
|
+
}
|
|
4896
|
+
matchesCondition(event, match) {
|
|
4897
|
+
const fieldValue = this.getFieldValue(event, match.field);
|
|
4898
|
+
if (fieldValue === void 0) return false;
|
|
4899
|
+
const strValue = String(fieldValue);
|
|
4900
|
+
let result = false;
|
|
4901
|
+
switch (match.operator) {
|
|
4902
|
+
case "contains":
|
|
4903
|
+
result = strValue.toLowerCase().includes(String(match.value).toLowerCase());
|
|
4904
|
+
break;
|
|
4905
|
+
case "regex":
|
|
4906
|
+
try {
|
|
4907
|
+
result = new RegExp(String(match.value), "i").test(strValue);
|
|
4908
|
+
} catch {
|
|
4909
|
+
result = false;
|
|
4910
|
+
}
|
|
4911
|
+
break;
|
|
4912
|
+
case "equals":
|
|
4913
|
+
result = strValue === String(match.value);
|
|
4914
|
+
break;
|
|
4915
|
+
case "starts_with":
|
|
4916
|
+
result = strValue.startsWith(String(match.value));
|
|
4917
|
+
break;
|
|
4918
|
+
case "ends_with":
|
|
4919
|
+
result = strValue.endsWith(String(match.value));
|
|
4920
|
+
break;
|
|
4921
|
+
}
|
|
4922
|
+
if (result && match.and) {
|
|
4923
|
+
result = match.and.every((m) => this.matchesCondition(event, m));
|
|
4924
|
+
}
|
|
4925
|
+
if (!result && match.or) {
|
|
4926
|
+
result = match.or.some((m) => this.matchesCondition(event, m));
|
|
4927
|
+
}
|
|
4928
|
+
return result;
|
|
4929
|
+
}
|
|
4930
|
+
getFieldValue(event, field) {
|
|
4931
|
+
switch (field) {
|
|
4932
|
+
case "message":
|
|
4933
|
+
return event.message;
|
|
4934
|
+
case "severity":
|
|
4935
|
+
return event.severity;
|
|
4936
|
+
case "module":
|
|
4937
|
+
return event.module;
|
|
4938
|
+
case "category":
|
|
4939
|
+
return event.category;
|
|
4940
|
+
case "source_ip":
|
|
4941
|
+
return event.source_ip;
|
|
4942
|
+
default:
|
|
4943
|
+
return event.details?.[field];
|
|
4944
|
+
}
|
|
4945
|
+
}
|
|
4946
|
+
// Periodic cleanup of stale windows
|
|
4947
|
+
cleanup() {
|
|
4948
|
+
const now = Date.now();
|
|
4949
|
+
for (const [key, window] of this.windows.entries()) {
|
|
4950
|
+
if (window.events.length === 0 && now - window.lastAlert > 36e5) {
|
|
4951
|
+
this.windows.delete(key);
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
}
|
|
4955
|
+
};
|
|
4956
|
+
|
|
4957
|
+
// src/daemon/rules/loader.ts
|
|
4958
|
+
var import_node_fs11 = require("fs");
|
|
4959
|
+
var import_node_path6 = require("path");
|
|
4960
|
+
|
|
4961
|
+
// src/daemon/rules/default-rules.ts
|
|
4962
|
+
var DEFAULT_RULES = [
|
|
4963
|
+
{
|
|
4964
|
+
id: "ssh-brute-force",
|
|
4965
|
+
title: "SSH Brute Force Detected",
|
|
4966
|
+
description: "Multiple failed SSH login attempts from the same source",
|
|
4967
|
+
version: "1.0.0",
|
|
4968
|
+
category: "auth",
|
|
4969
|
+
severity: "high",
|
|
4970
|
+
source_types: ["ssh-guard", "auth"],
|
|
4971
|
+
match: {
|
|
4972
|
+
field: "message",
|
|
4973
|
+
operator: "regex",
|
|
4974
|
+
value: "failed ssh login|invalid ssh user"
|
|
4975
|
+
},
|
|
4976
|
+
threshold: 5,
|
|
4977
|
+
window_seconds: 300,
|
|
4978
|
+
cooldown_seconds: 600,
|
|
4979
|
+
tags: ["ssh", "brute-force", "credential-stuffing"],
|
|
4980
|
+
remediation: {
|
|
4981
|
+
action: "block",
|
|
4982
|
+
ttl_seconds: 3600,
|
|
4983
|
+
description: "Block source IP for 1 hour"
|
|
4984
|
+
},
|
|
4985
|
+
enabled: true
|
|
4986
|
+
},
|
|
4987
|
+
{
|
|
4988
|
+
id: "ssh-success-after-failures",
|
|
4989
|
+
title: "SSH Login After Failed Attempts",
|
|
4990
|
+
description: "Successful SSH login from an IP that had recent failures",
|
|
4991
|
+
version: "1.0.0",
|
|
4992
|
+
category: "auth",
|
|
4993
|
+
severity: "critical",
|
|
4994
|
+
source_types: ["ssh-guard", "auth"],
|
|
4995
|
+
match: {
|
|
4996
|
+
field: "message",
|
|
4997
|
+
operator: "contains",
|
|
4998
|
+
value: "SSH login accepted"
|
|
4999
|
+
},
|
|
5000
|
+
threshold: 1,
|
|
5001
|
+
window_seconds: 60,
|
|
5002
|
+
cooldown_seconds: 300,
|
|
5003
|
+
tags: ["ssh", "compromise-indicator"],
|
|
5004
|
+
enabled: true
|
|
5005
|
+
},
|
|
5006
|
+
{
|
|
5007
|
+
id: "ssh-root-login",
|
|
5008
|
+
title: "Root SSH Login Attempt",
|
|
5009
|
+
description: "Direct root login via SSH detected",
|
|
5010
|
+
version: "1.0.0",
|
|
5011
|
+
category: "auth",
|
|
5012
|
+
severity: "high",
|
|
5013
|
+
source_types: ["ssh-guard", "auth"],
|
|
5014
|
+
match: {
|
|
5015
|
+
field: "message",
|
|
5016
|
+
operator: "regex",
|
|
5017
|
+
value: "(failed|accepted).*\\broot\\b"
|
|
5018
|
+
},
|
|
5019
|
+
threshold: 1,
|
|
5020
|
+
window_seconds: 60,
|
|
5021
|
+
cooldown_seconds: 300,
|
|
5022
|
+
tags: ["ssh", "root-access"],
|
|
5023
|
+
remediation: {
|
|
5024
|
+
action: "block",
|
|
5025
|
+
ttl_seconds: 7200,
|
|
5026
|
+
description: "Block source IP attempting root login"
|
|
5027
|
+
},
|
|
5028
|
+
enabled: true
|
|
5029
|
+
},
|
|
5030
|
+
{
|
|
5031
|
+
id: "ssh-user-enumeration",
|
|
5032
|
+
title: "SSH User Enumeration",
|
|
5033
|
+
description: "Multiple SSH attempts with different usernames from same source",
|
|
5034
|
+
version: "1.0.0",
|
|
5035
|
+
category: "auth",
|
|
5036
|
+
severity: "high",
|
|
5037
|
+
source_types: ["ssh-guard", "auth"],
|
|
5038
|
+
match: {
|
|
5039
|
+
field: "message",
|
|
5040
|
+
operator: "contains",
|
|
5041
|
+
value: "Invalid SSH user"
|
|
5042
|
+
},
|
|
5043
|
+
threshold: 3,
|
|
5044
|
+
window_seconds: 120,
|
|
5045
|
+
cooldown_seconds: 600,
|
|
5046
|
+
tags: ["ssh", "enumeration", "reconnaissance"],
|
|
5047
|
+
remediation: {
|
|
5048
|
+
action: "block",
|
|
5049
|
+
ttl_seconds: 3600,
|
|
5050
|
+
description: "Block source IP performing user enumeration"
|
|
5051
|
+
},
|
|
5052
|
+
enabled: true
|
|
5053
|
+
},
|
|
5054
|
+
{
|
|
5055
|
+
id: "sudo-abuse",
|
|
5056
|
+
title: "Sudo Authentication Failure",
|
|
5057
|
+
description: "Repeated sudo authentication failures",
|
|
5058
|
+
version: "1.0.0",
|
|
5059
|
+
category: "auth",
|
|
5060
|
+
severity: "high",
|
|
5061
|
+
source_types: ["user-journal", "system"],
|
|
5062
|
+
match: {
|
|
5063
|
+
field: "message",
|
|
5064
|
+
operator: "regex",
|
|
5065
|
+
value: "sudo.*authentication failure|sudo.*incorrect password|sudo.*FAILED"
|
|
5066
|
+
},
|
|
5067
|
+
threshold: 3,
|
|
5068
|
+
window_seconds: 300,
|
|
5069
|
+
cooldown_seconds: 600,
|
|
5070
|
+
tags: ["sudo", "privilege-escalation"],
|
|
5071
|
+
enabled: true
|
|
5072
|
+
},
|
|
5073
|
+
{
|
|
5074
|
+
id: "web-sqli-attack",
|
|
5075
|
+
title: "SQL Injection Attack Detected",
|
|
5076
|
+
description: "HTTP request with SQL injection patterns",
|
|
5077
|
+
version: "1.0.0",
|
|
5078
|
+
category: "web",
|
|
5079
|
+
severity: "critical",
|
|
5080
|
+
source_types: ["log-watcher", "web"],
|
|
5081
|
+
match: {
|
|
5082
|
+
field: "message",
|
|
5083
|
+
operator: "contains",
|
|
5084
|
+
value: "Attack detected [SQLI]"
|
|
5085
|
+
},
|
|
5086
|
+
threshold: 1,
|
|
5087
|
+
window_seconds: 60,
|
|
5088
|
+
cooldown_seconds: 300,
|
|
5089
|
+
tags: ["web", "sqli", "injection"],
|
|
5090
|
+
remediation: {
|
|
5091
|
+
action: "block",
|
|
5092
|
+
ttl_seconds: 3600,
|
|
5093
|
+
description: "Block source IP performing SQL injection"
|
|
5094
|
+
},
|
|
5095
|
+
enabled: true
|
|
5096
|
+
},
|
|
5097
|
+
{
|
|
5098
|
+
id: "web-path-traversal",
|
|
5099
|
+
title: "Path Traversal Attack Detected",
|
|
5100
|
+
description: "HTTP request with path traversal patterns",
|
|
5101
|
+
version: "1.0.0",
|
|
5102
|
+
category: "web",
|
|
5103
|
+
severity: "critical",
|
|
5104
|
+
source_types: ["log-watcher", "web"],
|
|
5105
|
+
match: {
|
|
5106
|
+
field: "message",
|
|
5107
|
+
operator: "contains",
|
|
5108
|
+
value: "Attack detected [PATH_TRAVERSAL]"
|
|
5109
|
+
},
|
|
5110
|
+
threshold: 1,
|
|
5111
|
+
window_seconds: 60,
|
|
5112
|
+
cooldown_seconds: 300,
|
|
5113
|
+
tags: ["web", "path-traversal", "lfi"],
|
|
5114
|
+
remediation: {
|
|
5115
|
+
action: "block",
|
|
5116
|
+
ttl_seconds: 3600,
|
|
5117
|
+
description: "Block source IP performing path traversal"
|
|
5118
|
+
},
|
|
5119
|
+
enabled: true
|
|
5120
|
+
},
|
|
5121
|
+
{
|
|
5122
|
+
id: "web-xss-attack",
|
|
5123
|
+
title: "XSS Attack Detected",
|
|
5124
|
+
description: "HTTP request with cross-site scripting patterns",
|
|
5125
|
+
version: "1.0.0",
|
|
5126
|
+
category: "web",
|
|
5127
|
+
severity: "high",
|
|
5128
|
+
source_types: ["log-watcher", "web"],
|
|
5129
|
+
match: {
|
|
5130
|
+
field: "message",
|
|
5131
|
+
operator: "regex",
|
|
5132
|
+
value: "Attack detected \\[XSS\\]"
|
|
5133
|
+
},
|
|
5134
|
+
threshold: 1,
|
|
5135
|
+
window_seconds: 60,
|
|
5136
|
+
cooldown_seconds: 300,
|
|
5137
|
+
tags: ["web", "xss", "injection"],
|
|
5138
|
+
remediation: {
|
|
5139
|
+
action: "block",
|
|
5140
|
+
ttl_seconds: 3600,
|
|
5141
|
+
description: "Block source IP performing XSS attack"
|
|
5142
|
+
},
|
|
5143
|
+
enabled: true
|
|
5144
|
+
},
|
|
5145
|
+
{
|
|
5146
|
+
id: "web-scanner-detection",
|
|
5147
|
+
title: "Web Vulnerability Scanner Detected",
|
|
5148
|
+
description: "High volume of 4xx errors suggesting automated scanning",
|
|
5149
|
+
version: "1.0.0",
|
|
5150
|
+
category: "web",
|
|
5151
|
+
severity: "medium",
|
|
5152
|
+
source_types: ["log-watcher", "web"],
|
|
5153
|
+
match: {
|
|
5154
|
+
field: "message",
|
|
5155
|
+
operator: "regex",
|
|
5156
|
+
value: "Client error 4\\d{2}:"
|
|
5157
|
+
},
|
|
5158
|
+
threshold: 20,
|
|
5159
|
+
window_seconds: 60,
|
|
5160
|
+
cooldown_seconds: 600,
|
|
5161
|
+
tags: ["web", "scanner", "reconnaissance"],
|
|
5162
|
+
remediation: {
|
|
5163
|
+
action: "block",
|
|
5164
|
+
ttl_seconds: 1800,
|
|
5165
|
+
description: "Block automated scanner"
|
|
5166
|
+
},
|
|
5167
|
+
enabled: true
|
|
5168
|
+
},
|
|
5169
|
+
{
|
|
5170
|
+
id: "port-scan-indicator",
|
|
5171
|
+
title: "Port Scan Indicators",
|
|
5172
|
+
description: "Connection attempts to many ports from a single source",
|
|
5173
|
+
version: "1.0.0",
|
|
5174
|
+
category: "network",
|
|
5175
|
+
severity: "medium",
|
|
5176
|
+
source_types: ["network-monitor", "network"],
|
|
5177
|
+
match: {
|
|
5178
|
+
field: "message",
|
|
5179
|
+
operator: "contains",
|
|
5180
|
+
value: "port scan"
|
|
5181
|
+
},
|
|
5182
|
+
threshold: 1,
|
|
5183
|
+
window_seconds: 60,
|
|
5184
|
+
cooldown_seconds: 300,
|
|
5185
|
+
tags: ["network", "port-scan", "reconnaissance"],
|
|
5186
|
+
remediation: {
|
|
5187
|
+
action: "block",
|
|
5188
|
+
ttl_seconds: 3600,
|
|
5189
|
+
description: "Block port scanner"
|
|
5190
|
+
},
|
|
5191
|
+
enabled: true
|
|
5192
|
+
},
|
|
5193
|
+
{
|
|
5194
|
+
id: "system-critical-error",
|
|
5195
|
+
title: "Critical System Error",
|
|
5196
|
+
description: "Critical or emergency level system log message",
|
|
5197
|
+
version: "1.0.0",
|
|
5198
|
+
category: "system",
|
|
5199
|
+
severity: "critical",
|
|
5200
|
+
source_types: ["user-journal", "system"],
|
|
5201
|
+
match: {
|
|
5202
|
+
field: "severity",
|
|
5203
|
+
operator: "equals",
|
|
5204
|
+
value: "critical"
|
|
5205
|
+
},
|
|
5206
|
+
threshold: 1,
|
|
5207
|
+
window_seconds: 60,
|
|
5208
|
+
cooldown_seconds: 300,
|
|
5209
|
+
tags: ["system", "critical"],
|
|
5210
|
+
enabled: true
|
|
5211
|
+
},
|
|
5212
|
+
{
|
|
5213
|
+
id: "exploit-probe-pattern",
|
|
5214
|
+
title: "Exploit Probe Pattern",
|
|
5215
|
+
description: "HTTP requests matching common exploit probe patterns",
|
|
5216
|
+
version: "1.0.0",
|
|
5217
|
+
category: "web",
|
|
5218
|
+
severity: "high",
|
|
5219
|
+
source_types: ["log-watcher", "web"],
|
|
5220
|
+
match: {
|
|
5221
|
+
field: "message",
|
|
5222
|
+
operator: "regex",
|
|
5223
|
+
value: "Attack detected \\[(CMD_INJECTION|RCE|SSRF|XXE)\\]"
|
|
5224
|
+
},
|
|
5225
|
+
threshold: 1,
|
|
5226
|
+
window_seconds: 60,
|
|
5227
|
+
cooldown_seconds: 300,
|
|
5228
|
+
tags: ["web", "exploit", "probe"],
|
|
5229
|
+
remediation: {
|
|
5230
|
+
action: "block",
|
|
5231
|
+
ttl_seconds: 7200,
|
|
5232
|
+
description: "Block source IP performing exploit probes"
|
|
5233
|
+
},
|
|
5234
|
+
enabled: true
|
|
5235
|
+
}
|
|
5236
|
+
];
|
|
5237
|
+
|
|
5238
|
+
// src/daemon/rules/loader.ts
|
|
5239
|
+
var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
5240
|
+
function loadAllRules(customDir) {
|
|
5241
|
+
const rules = [...DEFAULT_RULES];
|
|
5242
|
+
const dir = customDir || RULES_DIR;
|
|
5243
|
+
if ((0, import_node_fs11.existsSync)(dir)) {
|
|
5244
|
+
const files = (0, import_node_fs11.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
5245
|
+
for (const file of files) {
|
|
5246
|
+
try {
|
|
5247
|
+
const raw = (0, import_node_fs11.readFileSync)((0, import_node_path6.join)(dir, file), "utf-8");
|
|
5248
|
+
const parsed = JSON.parse(raw);
|
|
5249
|
+
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
5250
|
+
for (const rule of customRules) {
|
|
5251
|
+
if (!rule.id || !rule.title || !rule.match) {
|
|
5252
|
+
console.warn(`[rules] skipping invalid rule in ${file}: missing required fields`);
|
|
5253
|
+
continue;
|
|
5254
|
+
}
|
|
5255
|
+
const existingIdx = rules.findIndex((r) => r.id === rule.id);
|
|
5256
|
+
if (existingIdx >= 0) {
|
|
5257
|
+
rules[existingIdx] = { ...rules[existingIdx], ...rule };
|
|
5258
|
+
} else {
|
|
5259
|
+
rules.push(rule);
|
|
5260
|
+
}
|
|
5261
|
+
}
|
|
5262
|
+
} catch (err) {
|
|
5263
|
+
console.warn(`[rules] failed to load ${file}: ${err.message}`);
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
}
|
|
5267
|
+
return rules;
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// src/daemon/firewall/adapters.ts
|
|
5271
|
+
var import_node_child_process3 = require("child_process");
|
|
5272
|
+
var NftablesAdapter = class {
|
|
5273
|
+
name = "nftables";
|
|
5274
|
+
table = "threatcrush";
|
|
5275
|
+
set = "blocklist";
|
|
5276
|
+
isAvailable() {
|
|
5277
|
+
const result = (0, import_node_child_process3.spawnSync)("nft", ["--version"], { stdio: "pipe" });
|
|
5278
|
+
return result.status === 0;
|
|
5279
|
+
}
|
|
5280
|
+
ensureSetup() {
|
|
5281
|
+
try {
|
|
5282
|
+
(0, import_node_child_process3.execSync)(`nft list table inet ${this.table} 2>/dev/null`, { stdio: "pipe" });
|
|
5283
|
+
} catch {
|
|
5284
|
+
(0, import_node_child_process3.execSync)(`nft add table inet ${this.table}`);
|
|
5285
|
+
(0, import_node_child_process3.execSync)(`nft add set inet ${this.table} ${this.set} '{ type ipv4_addr; flags timeout; }'`);
|
|
5286
|
+
(0, import_node_child_process3.execSync)(`nft add chain inet ${this.table} input '{ type filter hook input priority -1; policy accept; }'`);
|
|
5287
|
+
(0, import_node_child_process3.execSync)(`nft add rule inet ${this.table} input ip saddr @${this.set} drop`);
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
async block(ip) {
|
|
5291
|
+
this.ensureSetup();
|
|
5292
|
+
(0, import_node_child_process3.execSync)(`nft add element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
5293
|
+
}
|
|
5294
|
+
async unblock(ip) {
|
|
5295
|
+
try {
|
|
5296
|
+
(0, import_node_child_process3.execSync)(`nft delete element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
5297
|
+
} catch {
|
|
5298
|
+
}
|
|
5299
|
+
}
|
|
5300
|
+
async isBlocked(ip) {
|
|
5301
|
+
try {
|
|
5302
|
+
const output = (0, import_node_child_process3.execSync)(`nft list set inet ${this.table} ${this.set}`, { encoding: "utf-8" });
|
|
5303
|
+
return output.includes(ip);
|
|
5304
|
+
} catch {
|
|
5305
|
+
return false;
|
|
5306
|
+
}
|
|
5307
|
+
}
|
|
5308
|
+
async listBlocked() {
|
|
5309
|
+
try {
|
|
5310
|
+
const output = (0, import_node_child_process3.execSync)(`nft list set inet ${this.table} ${this.set}`, { encoding: "utf-8" });
|
|
5311
|
+
const match = output.match(/elements\s*=\s*\{([^}]*)\}/);
|
|
5312
|
+
if (!match) return [];
|
|
5313
|
+
return match[1].split(",").map((s) => s.trim().split(/\s/)[0]).filter(Boolean);
|
|
5314
|
+
} catch {
|
|
5315
|
+
return [];
|
|
5316
|
+
}
|
|
5317
|
+
}
|
|
5318
|
+
};
|
|
5319
|
+
var IptablesAdapter = class {
|
|
5320
|
+
name = "iptables";
|
|
5321
|
+
chain = "THREATCRUSH";
|
|
5322
|
+
isAvailable() {
|
|
5323
|
+
const result = (0, import_node_child_process3.spawnSync)("iptables", ["--version"], { stdio: "pipe" });
|
|
5324
|
+
return result.status === 0;
|
|
5325
|
+
}
|
|
5326
|
+
ensureChain() {
|
|
5327
|
+
try {
|
|
5328
|
+
(0, import_node_child_process3.execSync)(`iptables -n -L ${this.chain} 2>/dev/null`, { stdio: "pipe" });
|
|
5329
|
+
} catch {
|
|
5330
|
+
(0, import_node_child_process3.execSync)(`iptables -N ${this.chain}`);
|
|
5331
|
+
(0, import_node_child_process3.execSync)(`iptables -I INPUT 1 -j ${this.chain}`);
|
|
5332
|
+
}
|
|
5333
|
+
}
|
|
5334
|
+
async block(ip) {
|
|
5335
|
+
this.ensureChain();
|
|
5336
|
+
if (await this.isBlocked(ip)) return;
|
|
5337
|
+
(0, import_node_child_process3.execSync)(`iptables -A ${this.chain} -s ${ip} -j DROP`);
|
|
5338
|
+
}
|
|
5339
|
+
async unblock(ip) {
|
|
5340
|
+
try {
|
|
5341
|
+
(0, import_node_child_process3.execSync)(`iptables -D ${this.chain} -s ${ip} -j DROP`);
|
|
5342
|
+
} catch {
|
|
5343
|
+
}
|
|
5344
|
+
}
|
|
5345
|
+
async isBlocked(ip) {
|
|
5346
|
+
try {
|
|
5347
|
+
const output = (0, import_node_child_process3.execSync)(`iptables -n -L ${this.chain}`, { encoding: "utf-8" });
|
|
5348
|
+
return output.includes(ip);
|
|
5349
|
+
} catch {
|
|
5350
|
+
return false;
|
|
5351
|
+
}
|
|
5352
|
+
}
|
|
5353
|
+
async listBlocked() {
|
|
5354
|
+
try {
|
|
5355
|
+
const output = (0, import_node_child_process3.execSync)(`iptables -n -L ${this.chain}`, { encoding: "utf-8" });
|
|
5356
|
+
const ips = [];
|
|
5357
|
+
for (const line of output.split("\n")) {
|
|
5358
|
+
const match = line.match(/DROP\s+all\s+--\s+(\d+\.\d+\.\d+\.\d+)/);
|
|
5359
|
+
if (match) ips.push(match[1]);
|
|
5360
|
+
}
|
|
5361
|
+
return ips;
|
|
5362
|
+
} catch {
|
|
5363
|
+
return [];
|
|
5364
|
+
}
|
|
5365
|
+
}
|
|
5366
|
+
};
|
|
5367
|
+
var DryRunAdapter = class {
|
|
5368
|
+
name = "dry-run";
|
|
5369
|
+
blocked = /* @__PURE__ */ new Set();
|
|
5370
|
+
isAvailable() {
|
|
5371
|
+
return true;
|
|
5372
|
+
}
|
|
5373
|
+
async block(ip) {
|
|
5374
|
+
this.blocked.add(ip);
|
|
5375
|
+
}
|
|
5376
|
+
async unblock(ip) {
|
|
5377
|
+
this.blocked.delete(ip);
|
|
5378
|
+
}
|
|
5379
|
+
async isBlocked(ip) {
|
|
5380
|
+
return this.blocked.has(ip);
|
|
5381
|
+
}
|
|
5382
|
+
async listBlocked() {
|
|
5383
|
+
return [...this.blocked];
|
|
5384
|
+
}
|
|
5385
|
+
};
|
|
5386
|
+
function detectFirewallAdapter() {
|
|
5387
|
+
const nft = new NftablesAdapter();
|
|
5388
|
+
if (nft.isAvailable()) return nft;
|
|
5389
|
+
const ipt = new IptablesAdapter();
|
|
5390
|
+
if (ipt.isAvailable()) return ipt;
|
|
5391
|
+
return new DryRunAdapter();
|
|
5392
|
+
}
|
|
5393
|
+
|
|
5394
|
+
// src/daemon/firewall/remediation.ts
|
|
5395
|
+
var import_node_fs12 = require("fs");
|
|
5396
|
+
var DEFAULT_CONFIG2 = {
|
|
5397
|
+
enabled: true,
|
|
5398
|
+
dry_run: true,
|
|
5399
|
+
default_ttl_seconds: 3600,
|
|
5400
|
+
min_severity: "high",
|
|
5401
|
+
allowlist: ["127.0.0.1", "::1"]
|
|
5402
|
+
};
|
|
5403
|
+
var SEVERITY_RANK4 = {
|
|
5404
|
+
info: 0,
|
|
5405
|
+
low: 1,
|
|
5406
|
+
medium: 2,
|
|
5407
|
+
high: 3,
|
|
5408
|
+
critical: 4
|
|
5409
|
+
};
|
|
5410
|
+
var RemediationManager = class {
|
|
5411
|
+
constructor(adapter, bus2, config) {
|
|
5412
|
+
this.adapter = adapter;
|
|
5413
|
+
this.bus = bus2;
|
|
5414
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
5415
|
+
this.loadState();
|
|
5416
|
+
this.startExpiryWorker();
|
|
5417
|
+
}
|
|
5418
|
+
adapter;
|
|
5419
|
+
bus;
|
|
5420
|
+
config;
|
|
5421
|
+
blocklist = [];
|
|
5422
|
+
expiryTimer = null;
|
|
5423
|
+
async handleDetection(event) {
|
|
5424
|
+
if (!this.config.enabled) return;
|
|
5425
|
+
const eventRank = SEVERITY_RANK4[event.severity] ?? 0;
|
|
5426
|
+
const minRank = SEVERITY_RANK4[this.config.min_severity] ?? 3;
|
|
5427
|
+
if (eventRank < minRank) return;
|
|
5428
|
+
const ip = event.source_ip;
|
|
5429
|
+
if (!ip) return;
|
|
5430
|
+
if (this.isAllowlisted(ip)) return;
|
|
5431
|
+
if (this.blocklist.some((b) => b.ip === ip)) return;
|
|
5432
|
+
const ruleRemediation = event.details?.remediation;
|
|
5433
|
+
const ttl = ruleRemediation?.ttl_seconds || this.config.default_ttl_seconds;
|
|
5434
|
+
const ruleId = event.details?.rule_id;
|
|
5435
|
+
await this.blockIp(ip, event.message, ruleId, ttl);
|
|
5436
|
+
}
|
|
5437
|
+
async blockIp(ip, reason, ruleId, ttlSeconds) {
|
|
5438
|
+
if (this.isAllowlisted(ip)) return false;
|
|
5439
|
+
const entry = {
|
|
5440
|
+
ip,
|
|
5441
|
+
reason,
|
|
5442
|
+
rule_id: ruleId,
|
|
5443
|
+
blocked_at: Date.now(),
|
|
5444
|
+
expires_at: ttlSeconds ? Date.now() + ttlSeconds * 1e3 : void 0,
|
|
5445
|
+
dry_run: this.config.dry_run
|
|
5446
|
+
};
|
|
5447
|
+
if (!this.config.dry_run) {
|
|
5448
|
+
try {
|
|
5449
|
+
await this.adapter.block(ip);
|
|
5450
|
+
} catch (err) {
|
|
5451
|
+
this.logLine(`[firewall] EACCES or error blocking ${ip}: ${err.message}`);
|
|
5452
|
+
this.bus.publish({
|
|
5453
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5454
|
+
module: "firewall-rules",
|
|
5455
|
+
category: "system",
|
|
5456
|
+
severity: "medium",
|
|
5457
|
+
message: `Failed to block ${ip}: ${err.message}. Ensure daemon has CAP_NET_ADMIN.`
|
|
5458
|
+
});
|
|
5459
|
+
return false;
|
|
5460
|
+
}
|
|
5461
|
+
}
|
|
5462
|
+
this.blocklist.push(entry);
|
|
5463
|
+
this.saveState();
|
|
5464
|
+
const mode = this.config.dry_run ? "[DRY-RUN] " : "";
|
|
5465
|
+
const expiryMsg = ttlSeconds ? ` (expires in ${ttlSeconds}s)` : " (permanent)";
|
|
5466
|
+
this.logLine(`[firewall] ${mode}Blocked ${ip}: ${reason}${expiryMsg}`);
|
|
5467
|
+
this.bus.publish({
|
|
5468
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5469
|
+
module: "firewall-rules",
|
|
5470
|
+
category: "system",
|
|
5471
|
+
severity: "info",
|
|
5472
|
+
message: `${mode}Blocked ${ip}: ${reason}${expiryMsg}`,
|
|
5473
|
+
source_ip: ip,
|
|
5474
|
+
details: { action: "block", rule_id: ruleId, dry_run: this.config.dry_run, ttl_seconds: ttlSeconds }
|
|
5475
|
+
});
|
|
5476
|
+
return true;
|
|
5477
|
+
}
|
|
5478
|
+
async unblockIp(ip) {
|
|
5479
|
+
const idx = this.blocklist.findIndex((b) => b.ip === ip);
|
|
5480
|
+
if (idx < 0) return false;
|
|
5481
|
+
const entry = this.blocklist[idx];
|
|
5482
|
+
if (!entry.dry_run) {
|
|
5483
|
+
try {
|
|
5484
|
+
await this.adapter.unblock(ip);
|
|
5485
|
+
} catch (err) {
|
|
5486
|
+
this.logLine(`[firewall] Error unblocking ${ip}: ${err.message}`);
|
|
5487
|
+
return false;
|
|
5488
|
+
}
|
|
5489
|
+
}
|
|
5490
|
+
this.blocklist.splice(idx, 1);
|
|
5491
|
+
this.saveState();
|
|
5492
|
+
this.logLine(`[firewall] Unblocked ${ip}`);
|
|
5493
|
+
this.bus.publish({
|
|
5494
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5495
|
+
module: "firewall-rules",
|
|
5496
|
+
category: "system",
|
|
5497
|
+
severity: "info",
|
|
5498
|
+
message: `Unblocked ${ip}`,
|
|
5499
|
+
source_ip: ip,
|
|
5500
|
+
details: { action: "unblock" }
|
|
5501
|
+
});
|
|
5502
|
+
return true;
|
|
5503
|
+
}
|
|
5504
|
+
isAllowlisted(ip) {
|
|
5505
|
+
return this.config.allowlist.includes(ip);
|
|
5506
|
+
}
|
|
5507
|
+
addToAllowlist(ip) {
|
|
5508
|
+
if (!this.config.allowlist.includes(ip)) {
|
|
5509
|
+
this.config.allowlist.push(ip);
|
|
5510
|
+
}
|
|
5511
|
+
}
|
|
5512
|
+
removeFromAllowlist(ip) {
|
|
5513
|
+
this.config.allowlist = this.config.allowlist.filter((a) => a !== ip);
|
|
5514
|
+
}
|
|
5515
|
+
getBlocklist() {
|
|
5516
|
+
return [...this.blocklist];
|
|
5517
|
+
}
|
|
5518
|
+
getAllowlist() {
|
|
5519
|
+
return [...this.config.allowlist];
|
|
5520
|
+
}
|
|
5521
|
+
stop() {
|
|
5522
|
+
if (this.expiryTimer) clearInterval(this.expiryTimer);
|
|
5523
|
+
this.expiryTimer = null;
|
|
5524
|
+
}
|
|
5525
|
+
startExpiryWorker() {
|
|
5526
|
+
this.expiryTimer = setInterval(() => void this.processExpiries(), 3e4);
|
|
5527
|
+
}
|
|
5528
|
+
async processExpiries() {
|
|
5529
|
+
const now = Date.now();
|
|
5530
|
+
const expired = this.blocklist.filter((b) => b.expires_at && b.expires_at <= now);
|
|
5531
|
+
for (const entry of expired) {
|
|
5532
|
+
await this.unblockIp(entry.ip);
|
|
5533
|
+
}
|
|
5534
|
+
}
|
|
5535
|
+
loadState() {
|
|
5536
|
+
try {
|
|
5537
|
+
const saved = getModuleState("firewall-rules", "blocklist");
|
|
5538
|
+
if (Array.isArray(saved)) this.blocklist = saved;
|
|
5539
|
+
} catch {
|
|
5540
|
+
}
|
|
5541
|
+
}
|
|
5542
|
+
saveState() {
|
|
5543
|
+
try {
|
|
5544
|
+
setModuleState("firewall-rules", "blocklist", this.blocklist);
|
|
5545
|
+
} catch {
|
|
5546
|
+
}
|
|
5547
|
+
}
|
|
5548
|
+
logLine(line) {
|
|
5549
|
+
try {
|
|
5550
|
+
(0, import_node_fs12.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
5551
|
+
`);
|
|
5552
|
+
} catch {
|
|
5553
|
+
}
|
|
5554
|
+
}
|
|
5555
|
+
};
|
|
5556
|
+
|
|
4190
5557
|
// src/core/telemetry.ts
|
|
4191
5558
|
var ready = false;
|
|
4192
5559
|
var sentry = null;
|
|
@@ -4236,7 +5603,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
4236
5603
|
// src/daemon/index.ts
|
|
4237
5604
|
function readVersion() {
|
|
4238
5605
|
try {
|
|
4239
|
-
const pkg = JSON.parse((0,
|
|
5606
|
+
const pkg = JSON.parse((0, import_node_fs13.readFileSync)((0, import_node_path7.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
4240
5607
|
return pkg.version || "0.0.0";
|
|
4241
5608
|
} catch {
|
|
4242
5609
|
return "0.0.0";
|
|
@@ -4244,7 +5611,7 @@ function readVersion() {
|
|
|
4244
5611
|
}
|
|
4245
5612
|
function logLine(line) {
|
|
4246
5613
|
try {
|
|
4247
|
-
(0,
|
|
5614
|
+
(0, import_node_fs13.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
4248
5615
|
`);
|
|
4249
5616
|
} catch {
|
|
4250
5617
|
}
|
|
@@ -4272,12 +5639,42 @@ async function runDaemon() {
|
|
|
4272
5639
|
} catch (err) {
|
|
4273
5640
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
4274
5641
|
}
|
|
4275
|
-
const config = loadConfig((0,
|
|
5642
|
+
const config = loadConfig((0, import_node_fs13.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
4276
5643
|
bus.on("event", (event) => {
|
|
4277
5644
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
4278
5645
|
});
|
|
4279
5646
|
const moduleHost = new ModuleHost(bus);
|
|
4280
5647
|
await moduleHost.start();
|
|
5648
|
+
const ruleEngine = new RuleEngine((detection) => {
|
|
5649
|
+
const event = {
|
|
5650
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
5651
|
+
module: "rule-engine",
|
|
5652
|
+
category: detection.raw_metadata?.category || "system",
|
|
5653
|
+
severity: detection.severity,
|
|
5654
|
+
message: `[DETECTION] ${detection.title}`,
|
|
5655
|
+
source_ip: detection.source_ip,
|
|
5656
|
+
details: {
|
|
5657
|
+
rule_id: detection.rule_id,
|
|
5658
|
+
username: detection.username,
|
|
5659
|
+
...detection.raw_metadata
|
|
5660
|
+
}
|
|
5661
|
+
};
|
|
5662
|
+
bus.publish(event);
|
|
5663
|
+
});
|
|
5664
|
+
ruleEngine.loadRules(loadAllRules());
|
|
5665
|
+
bus.on("event", (event) => {
|
|
5666
|
+
if (event.module !== "rule-engine") ruleEngine.evaluate(event);
|
|
5667
|
+
});
|
|
5668
|
+
logLine(`[daemon] rule engine loaded ${ruleEngine.getRules().length} rules`);
|
|
5669
|
+
setInterval(() => ruleEngine.cleanup(), 3e5);
|
|
5670
|
+
const firewallAdapter = detectFirewallAdapter();
|
|
5671
|
+
const remediation = new RemediationManager(firewallAdapter, bus, config.remediation);
|
|
5672
|
+
bus.on("event", (event) => {
|
|
5673
|
+
if (event.module !== "firewall-rules") {
|
|
5674
|
+
void remediation.handleDetection(event);
|
|
5675
|
+
}
|
|
5676
|
+
});
|
|
5677
|
+
logLine(`[daemon] firewall remediation active (backend=${firewallAdapter.name}, dry_run=${config.remediation?.dry_run ?? true})`);
|
|
4281
5678
|
new AlertDispatcher(bus, config);
|
|
4282
5679
|
const runsWorker = new RunsWorker(bus);
|
|
4283
5680
|
try {
|
|
@@ -4290,6 +5687,10 @@ async function runDaemon() {
|
|
|
4290
5687
|
logLine(`[daemon] ipc listening on ${PATHS.socket}`);
|
|
4291
5688
|
const shutdown = async (signal) => {
|
|
4292
5689
|
logLine(`[daemon] received ${signal}, shutting down`);
|
|
5690
|
+
try {
|
|
5691
|
+
remediation.stop();
|
|
5692
|
+
} catch {
|
|
5693
|
+
}
|
|
4293
5694
|
try {
|
|
4294
5695
|
runsWorker.stop();
|
|
4295
5696
|
} catch {
|