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