@profullstack/threatcrush 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.js +380 -82
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +324 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -2134,12 +2134,21 @@ bus.setMaxListeners(50);
|
|
|
2134
2134
|
// src/core/state.ts
|
|
2135
2135
|
var import_better_sqlite3 = __toESM(require("better-sqlite3"));
|
|
2136
2136
|
var db = null;
|
|
2137
|
+
var dbUnavailable = false;
|
|
2137
2138
|
function initStateDB(dbPath = "/var/lib/threatcrush/state.db") {
|
|
2138
2139
|
if (db) return db;
|
|
2140
|
+
if (dbUnavailable) {
|
|
2141
|
+
throw new Error("state db unavailable (previous init failed)");
|
|
2142
|
+
}
|
|
2139
2143
|
try {
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2144
|
+
try {
|
|
2145
|
+
db = new import_better_sqlite3.default(dbPath);
|
|
2146
|
+
} catch {
|
|
2147
|
+
db = new import_better_sqlite3.default(":memory:");
|
|
2148
|
+
}
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
dbUnavailable = true;
|
|
2151
|
+
throw err;
|
|
2143
2152
|
}
|
|
2144
2153
|
db.pragma("journal_mode = WAL");
|
|
2145
2154
|
db.exec(`
|
|
@@ -2175,7 +2184,8 @@ function initStateDB(dbPath = "/var/lib/threatcrush/state.db") {
|
|
|
2175
2184
|
return db;
|
|
2176
2185
|
}
|
|
2177
2186
|
function insertEvent(event) {
|
|
2178
|
-
const database =
|
|
2187
|
+
const database = tryDb();
|
|
2188
|
+
if (!database) return -1;
|
|
2179
2189
|
const stmt = database.prepare(`
|
|
2180
2190
|
INSERT INTO events (timestamp, module, category, severity, message, source_ip, details)
|
|
2181
2191
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
@@ -2191,22 +2201,34 @@ function insertEvent(event) {
|
|
|
2191
2201
|
);
|
|
2192
2202
|
return result.lastInsertRowid;
|
|
2193
2203
|
}
|
|
2204
|
+
function tryDb() {
|
|
2205
|
+
if (db) return db;
|
|
2206
|
+
if (dbUnavailable) return null;
|
|
2207
|
+
try {
|
|
2208
|
+
return initStateDB();
|
|
2209
|
+
} catch {
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
}
|
|
2194
2213
|
function getRecentEvents(limit = 50) {
|
|
2195
|
-
const database =
|
|
2214
|
+
const database = tryDb();
|
|
2215
|
+
if (!database) return [];
|
|
2196
2216
|
const rows = database.prepare(`
|
|
2197
2217
|
SELECT * FROM events ORDER BY timestamp DESC LIMIT ?
|
|
2198
2218
|
`).all(limit);
|
|
2199
2219
|
return rows.map(rowToEvent);
|
|
2200
2220
|
}
|
|
2201
2221
|
function getEventCount(since) {
|
|
2202
|
-
const database =
|
|
2222
|
+
const database = tryDb();
|
|
2223
|
+
if (!database) return 0;
|
|
2203
2224
|
if (since) {
|
|
2204
2225
|
return database.prepare(`SELECT COUNT(*) as count FROM events WHERE timestamp >= ?`).get(since.toISOString()).count;
|
|
2205
2226
|
}
|
|
2206
2227
|
return database.prepare(`SELECT COUNT(*) as count FROM events`).get().count;
|
|
2207
2228
|
}
|
|
2208
2229
|
function getThreatCount(since) {
|
|
2209
|
-
const database =
|
|
2230
|
+
const database = tryDb();
|
|
2231
|
+
if (!database) return 0;
|
|
2210
2232
|
const severities = "('medium','high','critical')";
|
|
2211
2233
|
if (since) {
|
|
2212
2234
|
return database.prepare(
|
|
@@ -2218,13 +2240,30 @@ function getThreatCount(since) {
|
|
|
2218
2240
|
).get().count;
|
|
2219
2241
|
}
|
|
2220
2242
|
function getTopSources(limit = 10) {
|
|
2221
|
-
const database =
|
|
2243
|
+
const database = tryDb();
|
|
2244
|
+
if (!database) return [];
|
|
2222
2245
|
return database.prepare(`
|
|
2223
2246
|
SELECT source_ip as ip, COUNT(*) as count FROM events
|
|
2224
2247
|
WHERE source_ip IS NOT NULL
|
|
2225
2248
|
GROUP BY source_ip ORDER BY count DESC LIMIT ?
|
|
2226
2249
|
`).all(limit);
|
|
2227
2250
|
}
|
|
2251
|
+
function getModuleState(module2, key) {
|
|
2252
|
+
const database = db || initStateDB();
|
|
2253
|
+
const row = database.prepare(`SELECT value FROM module_state WHERE module = ? AND key = ?`).get(module2, key);
|
|
2254
|
+
if (!row) return void 0;
|
|
2255
|
+
try {
|
|
2256
|
+
return JSON.parse(row.value);
|
|
2257
|
+
} catch {
|
|
2258
|
+
return row.value;
|
|
2259
|
+
}
|
|
2260
|
+
}
|
|
2261
|
+
function setModuleState(module2, key, value) {
|
|
2262
|
+
const database = db || initStateDB();
|
|
2263
|
+
database.prepare(`
|
|
2264
|
+
INSERT OR REPLACE INTO module_state (module, key, value) VALUES (?, ?, ?)
|
|
2265
|
+
`).run(module2, key, JSON.stringify(value));
|
|
2266
|
+
}
|
|
2228
2267
|
function rowToEvent(row) {
|
|
2229
2268
|
return {
|
|
2230
2269
|
id: row.id,
|
|
@@ -2413,9 +2452,10 @@ var IpcServer = class {
|
|
|
2413
2452
|
};
|
|
2414
2453
|
|
|
2415
2454
|
// src/daemon/module-host.ts
|
|
2416
|
-
var
|
|
2417
|
-
var
|
|
2418
|
-
var
|
|
2455
|
+
var import_node_fs6 = require("fs");
|
|
2456
|
+
var import_node_path3 = require("path");
|
|
2457
|
+
var import_node_url = require("url");
|
|
2458
|
+
var import_toml2 = __toESM(require_toml());
|
|
2419
2459
|
|
|
2420
2460
|
// src/daemon/watchers/log-watcher.ts
|
|
2421
2461
|
var import_node_fs4 = require("fs");
|
|
@@ -2674,6 +2714,182 @@ var LogWatcher = class {
|
|
|
2674
2714
|
}
|
|
2675
2715
|
};
|
|
2676
2716
|
|
|
2717
|
+
// src/daemon/watchers/journal-watcher.ts
|
|
2718
|
+
var import_node_child_process = require("child_process");
|
|
2719
|
+
var JournalWatcher = class _JournalWatcher {
|
|
2720
|
+
constructor(bus2) {
|
|
2721
|
+
this.bus = bus2;
|
|
2722
|
+
}
|
|
2723
|
+
bus;
|
|
2724
|
+
proc = null;
|
|
2725
|
+
buffer = "";
|
|
2726
|
+
moduleName = "user-journal";
|
|
2727
|
+
active = false;
|
|
2728
|
+
static isAvailable() {
|
|
2729
|
+
const probe = (0, import_node_child_process.spawnSync)("journalctl", ["--user", "-n", "0", "--no-pager"], {
|
|
2730
|
+
stdio: ["ignore", "ignore", "ignore"]
|
|
2731
|
+
});
|
|
2732
|
+
return probe.status === 0;
|
|
2733
|
+
}
|
|
2734
|
+
start() {
|
|
2735
|
+
if (!_JournalWatcher.isAvailable()) return false;
|
|
2736
|
+
const child = (0, import_node_child_process.spawn)(
|
|
2737
|
+
"journalctl",
|
|
2738
|
+
["--user", "-o", "json", "-f", "--since", "now"],
|
|
2739
|
+
{ stdio: ["ignore", "pipe", "pipe"] }
|
|
2740
|
+
);
|
|
2741
|
+
if (!child.stdout) return false;
|
|
2742
|
+
child.stdout.setEncoding("utf-8");
|
|
2743
|
+
child.stdout.on("data", (chunk) => this.onData(chunk));
|
|
2744
|
+
child.on("exit", () => {
|
|
2745
|
+
this.proc = null;
|
|
2746
|
+
this.active = false;
|
|
2747
|
+
});
|
|
2748
|
+
this.proc = child;
|
|
2749
|
+
this.active = true;
|
|
2750
|
+
return true;
|
|
2751
|
+
}
|
|
2752
|
+
stop() {
|
|
2753
|
+
if (this.proc) {
|
|
2754
|
+
try {
|
|
2755
|
+
this.proc.kill("SIGTERM");
|
|
2756
|
+
} catch {
|
|
2757
|
+
}
|
|
2758
|
+
this.proc = null;
|
|
2759
|
+
}
|
|
2760
|
+
this.active = false;
|
|
2761
|
+
}
|
|
2762
|
+
isActive() {
|
|
2763
|
+
return this.active;
|
|
2764
|
+
}
|
|
2765
|
+
moduleNameValue() {
|
|
2766
|
+
return this.moduleName;
|
|
2767
|
+
}
|
|
2768
|
+
onData(chunk) {
|
|
2769
|
+
this.buffer += chunk;
|
|
2770
|
+
let idx;
|
|
2771
|
+
while ((idx = this.buffer.indexOf("\n")) >= 0) {
|
|
2772
|
+
const line = this.buffer.slice(0, idx);
|
|
2773
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
2774
|
+
if (!line.trim()) continue;
|
|
2775
|
+
this.handleLine(line);
|
|
2776
|
+
}
|
|
2777
|
+
}
|
|
2778
|
+
handleLine(line) {
|
|
2779
|
+
let entry;
|
|
2780
|
+
try {
|
|
2781
|
+
entry = JSON.parse(line);
|
|
2782
|
+
} catch {
|
|
2783
|
+
return;
|
|
2784
|
+
}
|
|
2785
|
+
const message = entry.MESSAGE;
|
|
2786
|
+
if (!message) return;
|
|
2787
|
+
const priority = parseInt(entry.PRIORITY ?? "6", 10);
|
|
2788
|
+
const severity = priorityToSeverity(priority);
|
|
2789
|
+
const ident = entry.SYSLOG_IDENTIFIER || entry._COMM || "journal";
|
|
2790
|
+
const bumpedSeverity = bumpForIdent(ident, message, severity);
|
|
2791
|
+
const event = {
|
|
2792
|
+
timestamp: realtimeToDate(entry.__REALTIME_TIMESTAMP) || /* @__PURE__ */ new Date(),
|
|
2793
|
+
module: this.moduleName,
|
|
2794
|
+
category: "system",
|
|
2795
|
+
severity: bumpedSeverity,
|
|
2796
|
+
message: `[${ident}] ${message}`.slice(0, 500)
|
|
2797
|
+
};
|
|
2798
|
+
try {
|
|
2799
|
+
insertEvent(event);
|
|
2800
|
+
} catch {
|
|
2801
|
+
}
|
|
2802
|
+
this.bus.publish(event);
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
function priorityToSeverity(priority) {
|
|
2806
|
+
if (priority <= 2) return "critical";
|
|
2807
|
+
if (priority === 3) return "high";
|
|
2808
|
+
if (priority === 4) return "medium";
|
|
2809
|
+
if (priority === 5) return "low";
|
|
2810
|
+
return "info";
|
|
2811
|
+
}
|
|
2812
|
+
function bumpForIdent(ident, message, base) {
|
|
2813
|
+
if (/sudo/i.test(ident) && /authentication failure|incorrect password|FAILED/i.test(message)) {
|
|
2814
|
+
return "high";
|
|
2815
|
+
}
|
|
2816
|
+
if (/sshd/i.test(ident) && /failed|invalid user|break-in/i.test(message)) {
|
|
2817
|
+
return "high";
|
|
2818
|
+
}
|
|
2819
|
+
return base;
|
|
2820
|
+
}
|
|
2821
|
+
function realtimeToDate(rt) {
|
|
2822
|
+
if (!rt) return null;
|
|
2823
|
+
const us = parseInt(rt, 10);
|
|
2824
|
+
if (!Number.isFinite(us)) return null;
|
|
2825
|
+
return new Date(Math.floor(us / 1e3));
|
|
2826
|
+
}
|
|
2827
|
+
|
|
2828
|
+
// src/core/config.ts
|
|
2829
|
+
var import_node_fs5 = require("fs");
|
|
2830
|
+
var import_node_path2 = require("path");
|
|
2831
|
+
var import_toml = __toESM(require_toml());
|
|
2832
|
+
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
2833
|
+
var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
|
|
2834
|
+
var DEFAULT_CONFIG = {
|
|
2835
|
+
daemon: {
|
|
2836
|
+
pid_file: "/var/run/threatcrush/threatcrushd.pid",
|
|
2837
|
+
log_level: "info",
|
|
2838
|
+
log_file: "/var/log/threatcrush/threatcrushd.log",
|
|
2839
|
+
state_db: "/var/lib/threatcrush/state.db"
|
|
2840
|
+
},
|
|
2841
|
+
api: {
|
|
2842
|
+
enabled: true,
|
|
2843
|
+
bind: "127.0.0.1:9393",
|
|
2844
|
+
tls: false
|
|
2845
|
+
},
|
|
2846
|
+
alerts: {},
|
|
2847
|
+
modules: {
|
|
2848
|
+
auto_update: true,
|
|
2849
|
+
update_interval: "24h",
|
|
2850
|
+
module_dir: "/etc/threatcrush/modules",
|
|
2851
|
+
config_dir: DEFAULT_CONFDIR
|
|
2852
|
+
}
|
|
2853
|
+
};
|
|
2854
|
+
function loadConfig(configPath) {
|
|
2855
|
+
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
2856
|
+
if (!(0, import_node_fs5.existsSync)(path)) {
|
|
2857
|
+
return { ...DEFAULT_CONFIG };
|
|
2858
|
+
}
|
|
2859
|
+
try {
|
|
2860
|
+
const raw = (0, import_node_fs5.readFileSync)(path, "utf-8");
|
|
2861
|
+
const parsed = import_toml.default.parse(raw);
|
|
2862
|
+
return {
|
|
2863
|
+
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
2864
|
+
api: { ...DEFAULT_CONFIG.api, ...parsed.api },
|
|
2865
|
+
alerts: parsed.alerts || {},
|
|
2866
|
+
modules: { ...DEFAULT_CONFIG.modules, ...parsed.modules },
|
|
2867
|
+
license: parsed.license
|
|
2868
|
+
};
|
|
2869
|
+
} catch {
|
|
2870
|
+
return { ...DEFAULT_CONFIG };
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
function loadModuleConfigs(confDir) {
|
|
2874
|
+
const dir = confDir || DEFAULT_CONFDIR;
|
|
2875
|
+
const configs = /* @__PURE__ */ new Map();
|
|
2876
|
+
if (!(0, import_node_fs5.existsSync)(dir)) {
|
|
2877
|
+
return configs;
|
|
2878
|
+
}
|
|
2879
|
+
const files = (0, import_node_fs5.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
|
|
2880
|
+
for (const file of files) {
|
|
2881
|
+
try {
|
|
2882
|
+
const raw = (0, import_node_fs5.readFileSync)((0, import_node_path2.join)(dir, file), "utf-8");
|
|
2883
|
+
const parsed = import_toml.default.parse(raw);
|
|
2884
|
+
for (const [name, config] of Object.entries(parsed)) {
|
|
2885
|
+
configs.set(name, config);
|
|
2886
|
+
}
|
|
2887
|
+
} catch {
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
return configs;
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2677
2893
|
// src/daemon/module-host.ts
|
|
2678
2894
|
var ModuleHost = class {
|
|
2679
2895
|
constructor(bus2) {
|
|
@@ -2681,14 +2897,23 @@ var ModuleHost = class {
|
|
|
2681
2897
|
bus2.on("event", (event) => {
|
|
2682
2898
|
const mod = this.modules.get(event.module);
|
|
2683
2899
|
if (mod) mod.events++;
|
|
2900
|
+
for (const hosted of this.modules.values()) {
|
|
2901
|
+
if (hosted.status !== "running" || !hosted.instance?.onEvent) continue;
|
|
2902
|
+
void hosted.instance.onEvent(event).catch((err) => {
|
|
2903
|
+
hosted.status = "error";
|
|
2904
|
+
hosted.detail = `onEvent failed: ${String(err.message || err)}`;
|
|
2905
|
+
this.bus.announceModule(hosted.name, "error", hosted.detail);
|
|
2906
|
+
});
|
|
2907
|
+
}
|
|
2684
2908
|
});
|
|
2685
2909
|
}
|
|
2686
2910
|
bus;
|
|
2687
2911
|
modules = /* @__PURE__ */ new Map();
|
|
2688
2912
|
logWatcher = null;
|
|
2913
|
+
journalWatcher = null;
|
|
2689
2914
|
async start() {
|
|
2690
2915
|
this.registerBuiltins();
|
|
2691
|
-
this.
|
|
2916
|
+
await this.discoverAndStartInstalled();
|
|
2692
2917
|
this.logWatcher = new LogWatcher(this.bus);
|
|
2693
2918
|
const watched = this.logWatcher.start();
|
|
2694
2919
|
for (const modName of this.logWatcher.activeModules()) {
|
|
@@ -2699,10 +2924,30 @@ var ModuleHost = class {
|
|
|
2699
2924
|
this.bus.announceModule(modName, "running", mod.detail);
|
|
2700
2925
|
}
|
|
2701
2926
|
}
|
|
2927
|
+
this.journalWatcher = new JournalWatcher(this.bus);
|
|
2928
|
+
if (this.journalWatcher.start()) {
|
|
2929
|
+
const mod = this.modules.get("user-journal");
|
|
2930
|
+
if (mod) {
|
|
2931
|
+
mod.status = "running";
|
|
2932
|
+
mod.detail = "tailing journalctl --user";
|
|
2933
|
+
this.bus.announceModule("user-journal", "running", mod.detail);
|
|
2934
|
+
}
|
|
2935
|
+
}
|
|
2702
2936
|
}
|
|
2703
2937
|
async stop() {
|
|
2704
2938
|
this.logWatcher?.stop();
|
|
2939
|
+
this.journalWatcher?.stop();
|
|
2705
2940
|
for (const mod of this.modules.values()) {
|
|
2941
|
+
try {
|
|
2942
|
+
if (mod.instance && mod.status === "running") {
|
|
2943
|
+
await mod.instance.stop();
|
|
2944
|
+
}
|
|
2945
|
+
} catch (err) {
|
|
2946
|
+
mod.status = "error";
|
|
2947
|
+
mod.detail = `stop failed: ${String(err.message || err)}`;
|
|
2948
|
+
this.bus.announceModule(mod.name, "error", mod.detail);
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2706
2951
|
mod.status = "loaded";
|
|
2707
2952
|
this.bus.announceModule(mod.name, "stopped");
|
|
2708
2953
|
}
|
|
@@ -2718,30 +2963,129 @@ var ModuleHost = class {
|
|
|
2718
2963
|
registerBuiltins() {
|
|
2719
2964
|
const builtins = [
|
|
2720
2965
|
{ name: "log-watcher", version: "0.1.0", source: "builtin", status: "loaded", events: 0 },
|
|
2721
|
-
{ name: "ssh-guard", version: "0.1.0", source: "builtin", status: "loaded", events: 0 }
|
|
2966
|
+
{ 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 }
|
|
2722
2968
|
];
|
|
2723
2969
|
for (const m of builtins) this.modules.set(m.name, m);
|
|
2724
2970
|
}
|
|
2725
|
-
|
|
2726
|
-
if (!(0,
|
|
2727
|
-
const
|
|
2971
|
+
async discoverAndStartInstalled() {
|
|
2972
|
+
if (!(0, import_node_fs6.existsSync)(PATHS.moduleDir)) return;
|
|
2973
|
+
const configs = loadModuleConfigs(PATHS.confD);
|
|
2974
|
+
const entries = (0, import_node_fs6.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
|
|
2728
2975
|
for (const entry of entries) {
|
|
2729
2976
|
if (!entry.isDirectory()) continue;
|
|
2730
|
-
const manifestPath = (0,
|
|
2731
|
-
if (!(0,
|
|
2977
|
+
const manifestPath = (0, import_node_path3.join)(PATHS.moduleDir, entry.name, "mod.toml");
|
|
2978
|
+
if (!(0, import_node_fs6.existsSync)(manifestPath)) continue;
|
|
2732
2979
|
try {
|
|
2733
|
-
const manifest =
|
|
2980
|
+
const manifest = import_toml2.default.parse((0, import_node_fs6.readFileSync)(manifestPath, "utf-8"));
|
|
2734
2981
|
const name = manifest.module?.name || entry.name;
|
|
2735
|
-
|
|
2982
|
+
const defaults = manifest.module?.config?.defaults || {};
|
|
2983
|
+
const config = {
|
|
2984
|
+
enabled: true,
|
|
2985
|
+
...defaults,
|
|
2986
|
+
...configs.get(name) || {}
|
|
2987
|
+
};
|
|
2988
|
+
const hosted = {
|
|
2736
2989
|
name,
|
|
2737
2990
|
version: manifest.module?.version || "0.0.0",
|
|
2738
2991
|
source: "installed",
|
|
2739
|
-
status: "loaded",
|
|
2740
|
-
events: 0
|
|
2992
|
+
status: config.enabled === false ? "disabled" : "loaded",
|
|
2993
|
+
events: 0,
|
|
2994
|
+
path: (0, import_node_path3.join)(PATHS.moduleDir, entry.name),
|
|
2995
|
+
config
|
|
2996
|
+
};
|
|
2997
|
+
this.modules.set(name, hosted);
|
|
2998
|
+
if (config.enabled === false) continue;
|
|
2999
|
+
await this.startInstalled(hosted);
|
|
3000
|
+
} catch (err) {
|
|
3001
|
+
const name = entry.name;
|
|
3002
|
+
this.modules.set(name, {
|
|
3003
|
+
name,
|
|
3004
|
+
version: "0.0.0",
|
|
3005
|
+
source: "installed",
|
|
3006
|
+
status: "error",
|
|
3007
|
+
events: 0,
|
|
3008
|
+
detail: `manifest load failed: ${String(err.message || err)}`,
|
|
3009
|
+
path: (0, import_node_path3.join)(PATHS.moduleDir, entry.name)
|
|
2741
3010
|
});
|
|
3011
|
+
}
|
|
3012
|
+
}
|
|
3013
|
+
}
|
|
3014
|
+
async startInstalled(hosted) {
|
|
3015
|
+
const entrypoint = this.installedEntrypoint(hosted.path);
|
|
3016
|
+
if (!entrypoint) {
|
|
3017
|
+
hosted.status = "loaded";
|
|
3018
|
+
hosted.detail = "no built entrypoint found; run npm install && npm run build in the module directory";
|
|
3019
|
+
return;
|
|
3020
|
+
}
|
|
3021
|
+
try {
|
|
3022
|
+
const imported = await import((0, import_node_url.pathToFileURL)(entrypoint).href);
|
|
3023
|
+
const exported = imported.default || imported.module || imported;
|
|
3024
|
+
const instance = typeof exported === "function" ? new exported() : exported;
|
|
3025
|
+
if (!this.isThreatCrushModule(instance)) {
|
|
3026
|
+
throw new Error("entrypoint does not export a ThreatCrush module");
|
|
3027
|
+
}
|
|
3028
|
+
hosted.instance = instance;
|
|
3029
|
+
await instance.init(this.contextFor(hosted));
|
|
3030
|
+
await instance.start();
|
|
3031
|
+
hosted.status = "running";
|
|
3032
|
+
hosted.detail = `started from ${entrypoint}`;
|
|
3033
|
+
this.bus.announceModule(hosted.name, "running", hosted.detail);
|
|
3034
|
+
} catch (err) {
|
|
3035
|
+
hosted.status = "error";
|
|
3036
|
+
hosted.detail = String(err.message || err);
|
|
3037
|
+
this.bus.announceModule(hosted.name, "error", hosted.detail);
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
installedEntrypoint(modulePath) {
|
|
3041
|
+
const packageJson = (0, import_node_path3.join)(modulePath, "package.json");
|
|
3042
|
+
const candidates = [];
|
|
3043
|
+
if ((0, import_node_fs6.existsSync)(packageJson)) {
|
|
3044
|
+
try {
|
|
3045
|
+
const pkg = JSON.parse((0, import_node_fs6.readFileSync)(packageJson, "utf-8"));
|
|
3046
|
+
if (pkg.main) candidates.push((0, import_node_path3.join)(modulePath, pkg.main));
|
|
2742
3047
|
} catch {
|
|
2743
3048
|
}
|
|
2744
3049
|
}
|
|
3050
|
+
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, import_node_fs6.existsSync)(candidate)) || null;
|
|
3052
|
+
}
|
|
3053
|
+
isThreatCrushModule(value) {
|
|
3054
|
+
return Boolean(
|
|
3055
|
+
value && typeof value === "object" && typeof value.init === "function" && typeof value.start === "function" && typeof value.stop === "function"
|
|
3056
|
+
);
|
|
3057
|
+
}
|
|
3058
|
+
contextFor(hosted) {
|
|
3059
|
+
return {
|
|
3060
|
+
config: hosted.config || { enabled: true },
|
|
3061
|
+
logger: this.loggerFor(hosted.name),
|
|
3062
|
+
emit: (event) => this.bus.publish(event),
|
|
3063
|
+
subscribe: (eventType, handler) => {
|
|
3064
|
+
this.bus.on("event", (event) => {
|
|
3065
|
+
if (event.category === eventType || event.module === eventType) handler(event);
|
|
3066
|
+
});
|
|
3067
|
+
},
|
|
3068
|
+
alert: (alert) => {
|
|
3069
|
+
this.bus.emit("alert", alert.event || {
|
|
3070
|
+
timestamp: /* @__PURE__ */ new Date(),
|
|
3071
|
+
module: hosted.name,
|
|
3072
|
+
category: "system",
|
|
3073
|
+
severity: alert.severity,
|
|
3074
|
+
message: alert.title,
|
|
3075
|
+
details: alert.body ? { body: alert.body } : void 0
|
|
3076
|
+
});
|
|
3077
|
+
},
|
|
3078
|
+
getState: (key) => getModuleState(hosted.name, key),
|
|
3079
|
+
setState: (key, value) => setModuleState(hosted.name, key, value)
|
|
3080
|
+
};
|
|
3081
|
+
}
|
|
3082
|
+
loggerFor(moduleName) {
|
|
3083
|
+
return {
|
|
3084
|
+
debug: (msg, ...args) => console.debug(`[${moduleName}] ${msg}`, ...args),
|
|
3085
|
+
info: (msg, ...args) => console.info(`[${moduleName}] ${msg}`, ...args),
|
|
3086
|
+
warn: (msg, ...args) => console.warn(`[${moduleName}] ${msg}`, ...args),
|
|
3087
|
+
error: (msg, ...args) => console.error(`[${moduleName}] ${msg}`, ...args)
|
|
3088
|
+
};
|
|
2745
3089
|
}
|
|
2746
3090
|
};
|
|
2747
3091
|
|
|
@@ -2863,14 +3207,14 @@ function slackChannel(webhookUrl) {
|
|
|
2863
3207
|
}
|
|
2864
3208
|
|
|
2865
3209
|
// src/core/cli-config.ts
|
|
2866
|
-
var
|
|
2867
|
-
var
|
|
3210
|
+
var import_node_fs7 = require("fs");
|
|
3211
|
+
var import_node_path4 = require("path");
|
|
2868
3212
|
var import_node_os2 = require("os");
|
|
2869
|
-
var CLI_CONFIG_DIR = (0,
|
|
2870
|
-
var CLI_CONFIG_PATH = (0,
|
|
3213
|
+
var CLI_CONFIG_DIR = (0, import_node_path4.join)((0, import_node_os2.homedir)(), ".threatcrush");
|
|
3214
|
+
var CLI_CONFIG_PATH = (0, import_node_path4.join)(CLI_CONFIG_DIR, "config.json");
|
|
2871
3215
|
function readCliConfig() {
|
|
2872
3216
|
try {
|
|
2873
|
-
return JSON.parse((0,
|
|
3217
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
|
|
2874
3218
|
} catch {
|
|
2875
3219
|
return {};
|
|
2876
3220
|
}
|
|
@@ -2889,8 +3233,8 @@ function authHeaders() {
|
|
|
2889
3233
|
}
|
|
2890
3234
|
|
|
2891
3235
|
// src/commands/scan.ts
|
|
2892
|
-
var
|
|
2893
|
-
var
|
|
3236
|
+
var import_node_fs8 = require("fs");
|
|
3237
|
+
var import_node_path5 = require("path");
|
|
2894
3238
|
|
|
2895
3239
|
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
2896
3240
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -3518,12 +3862,12 @@ async function runScan(targetPath) {
|
|
|
3518
3862
|
function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
3519
3863
|
let entries;
|
|
3520
3864
|
try {
|
|
3521
|
-
entries = (0,
|
|
3865
|
+
entries = (0, import_node_fs8.readdirSync)(currentPath, { withFileTypes: true });
|
|
3522
3866
|
} catch {
|
|
3523
3867
|
return;
|
|
3524
3868
|
}
|
|
3525
3869
|
for (const entry of entries) {
|
|
3526
|
-
const fullPath = (0,
|
|
3870
|
+
const fullPath = (0, import_node_path5.join)(currentPath, entry.name);
|
|
3527
3871
|
if (entry.isDirectory()) {
|
|
3528
3872
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
3529
3873
|
scanDirectory(basePath, fullPath, findings, onFile);
|
|
@@ -3533,7 +3877,7 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
|
3533
3877
|
for (const mc of MISCONFIG_FILES) {
|
|
3534
3878
|
if (entry.name === mc.pattern || entry.name.endsWith(mc.pattern)) {
|
|
3535
3879
|
findings.push({
|
|
3536
|
-
file: (0,
|
|
3880
|
+
file: (0, import_node_path5.relative)(basePath, fullPath),
|
|
3537
3881
|
line: 0,
|
|
3538
3882
|
type: "Sensitive File",
|
|
3539
3883
|
severity: "high",
|
|
@@ -3542,10 +3886,10 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
|
3542
3886
|
});
|
|
3543
3887
|
}
|
|
3544
3888
|
}
|
|
3545
|
-
const ext = (0,
|
|
3889
|
+
const ext = (0, import_node_path5.extname)(entry.name).toLowerCase();
|
|
3546
3890
|
if (!SCAN_EXTENSIONS.has(ext) && !entry.name.startsWith(".env")) continue;
|
|
3547
3891
|
try {
|
|
3548
|
-
const stat = (0,
|
|
3892
|
+
const stat = (0, import_node_fs8.statSync)(fullPath);
|
|
3549
3893
|
if (stat.size > 1024 * 1024) continue;
|
|
3550
3894
|
} catch {
|
|
3551
3895
|
continue;
|
|
@@ -3553,7 +3897,7 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
|
3553
3897
|
onFile();
|
|
3554
3898
|
let content;
|
|
3555
3899
|
try {
|
|
3556
|
-
content = (0,
|
|
3900
|
+
content = (0, import_node_fs8.readFileSync)(fullPath, "utf-8");
|
|
3557
3901
|
} catch {
|
|
3558
3902
|
continue;
|
|
3559
3903
|
}
|
|
@@ -3565,7 +3909,7 @@ function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
|
3565
3909
|
pattern.pattern.lastIndex = 0;
|
|
3566
3910
|
if (pattern.pattern.test(line)) {
|
|
3567
3911
|
findings.push({
|
|
3568
|
-
file: (0,
|
|
3912
|
+
file: (0, import_node_path5.relative)(basePath, fullPath),
|
|
3569
3913
|
line: i + 1,
|
|
3570
3914
|
type: pattern.name,
|
|
3571
3915
|
severity: pattern.severity,
|
|
@@ -3843,52 +4187,6 @@ var RunsWorker = class {
|
|
|
3843
4187
|
}
|
|
3844
4188
|
};
|
|
3845
4189
|
|
|
3846
|
-
// src/core/config.ts
|
|
3847
|
-
var import_node_fs8 = require("fs");
|
|
3848
|
-
var import_node_path5 = require("path");
|
|
3849
|
-
var import_toml2 = __toESM(require_toml());
|
|
3850
|
-
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
3851
|
-
var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
|
|
3852
|
-
var DEFAULT_CONFIG = {
|
|
3853
|
-
daemon: {
|
|
3854
|
-
pid_file: "/var/run/threatcrush/threatcrushd.pid",
|
|
3855
|
-
log_level: "info",
|
|
3856
|
-
log_file: "/var/log/threatcrush/threatcrushd.log",
|
|
3857
|
-
state_db: "/var/lib/threatcrush/state.db"
|
|
3858
|
-
},
|
|
3859
|
-
api: {
|
|
3860
|
-
enabled: true,
|
|
3861
|
-
bind: "127.0.0.1:9393",
|
|
3862
|
-
tls: false
|
|
3863
|
-
},
|
|
3864
|
-
alerts: {},
|
|
3865
|
-
modules: {
|
|
3866
|
-
auto_update: true,
|
|
3867
|
-
update_interval: "24h",
|
|
3868
|
-
module_dir: "/etc/threatcrush/modules",
|
|
3869
|
-
config_dir: DEFAULT_CONFDIR
|
|
3870
|
-
}
|
|
3871
|
-
};
|
|
3872
|
-
function loadConfig(configPath) {
|
|
3873
|
-
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
3874
|
-
if (!(0, import_node_fs8.existsSync)(path)) {
|
|
3875
|
-
return { ...DEFAULT_CONFIG };
|
|
3876
|
-
}
|
|
3877
|
-
try {
|
|
3878
|
-
const raw = (0, import_node_fs8.readFileSync)(path, "utf-8");
|
|
3879
|
-
const parsed = import_toml2.default.parse(raw);
|
|
3880
|
-
return {
|
|
3881
|
-
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
3882
|
-
api: { ...DEFAULT_CONFIG.api, ...parsed.api },
|
|
3883
|
-
alerts: parsed.alerts || {},
|
|
3884
|
-
modules: { ...DEFAULT_CONFIG.modules, ...parsed.modules },
|
|
3885
|
-
license: parsed.license
|
|
3886
|
-
};
|
|
3887
|
-
} catch {
|
|
3888
|
-
return { ...DEFAULT_CONFIG };
|
|
3889
|
-
}
|
|
3890
|
-
}
|
|
3891
|
-
|
|
3892
4190
|
// src/core/telemetry.ts
|
|
3893
4191
|
var ready = false;
|
|
3894
4192
|
var sentry = null;
|