@profullstack/threatcrush 0.11.1 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.js +536 -113
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +819 -248
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/daemon.js
CHANGED
|
@@ -2022,8 +2022,8 @@ var require_toml = __commonJS({
|
|
|
2022
2022
|
});
|
|
2023
2023
|
|
|
2024
2024
|
// src/daemon/index.ts
|
|
2025
|
-
var
|
|
2026
|
-
var
|
|
2025
|
+
var import_node_fs17 = require("fs");
|
|
2026
|
+
var import_node_path12 = require("path");
|
|
2027
2027
|
|
|
2028
2028
|
// src/daemon/paths.ts
|
|
2029
2029
|
var import_node_fs = require("fs");
|
|
@@ -2107,7 +2107,28 @@ function findRunningDaemon() {
|
|
|
2107
2107
|
|
|
2108
2108
|
// src/daemon/ipc-server.ts
|
|
2109
2109
|
var import_node_net = require("net");
|
|
2110
|
+
var import_node_fs4 = require("fs");
|
|
2111
|
+
|
|
2112
|
+
// src/daemon/control-token.ts
|
|
2113
|
+
var import_node_crypto = require("crypto");
|
|
2110
2114
|
var import_node_fs3 = require("fs");
|
|
2115
|
+
var import_node_path2 = require("path");
|
|
2116
|
+
var CONTROL_TOKEN_FILE = (0, import_node_path2.join)(PATHS.runDir, "control.token");
|
|
2117
|
+
function issueControlToken() {
|
|
2118
|
+
const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
|
|
2119
|
+
(0, import_node_fs3.writeFileSync)(CONTROL_TOKEN_FILE, token, { mode: 384 });
|
|
2120
|
+
try {
|
|
2121
|
+
(0, import_node_fs3.chmodSync)(CONTROL_TOKEN_FILE, 384);
|
|
2122
|
+
} catch {
|
|
2123
|
+
}
|
|
2124
|
+
return token;
|
|
2125
|
+
}
|
|
2126
|
+
function tokensMatch(expected, provided) {
|
|
2127
|
+
if (typeof provided !== "string" || !provided) return false;
|
|
2128
|
+
const a = Buffer.from(expected);
|
|
2129
|
+
const b = Buffer.from(provided);
|
|
2130
|
+
return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
2131
|
+
}
|
|
2111
2132
|
|
|
2112
2133
|
// src/daemon/event-bus.ts
|
|
2113
2134
|
var import_node_events = require("events");
|
|
@@ -2303,10 +2324,12 @@ var IpcServer = class {
|
|
|
2303
2324
|
nextClientId = 1;
|
|
2304
2325
|
startedAt = /* @__PURE__ */ new Date();
|
|
2305
2326
|
counters = { events: 0, threats: 0, alerts: 0 };
|
|
2327
|
+
controlToken = "";
|
|
2306
2328
|
async start() {
|
|
2307
|
-
|
|
2329
|
+
this.controlToken = issueControlToken();
|
|
2330
|
+
if ((0, import_node_fs4.existsSync)(PATHS.socket)) {
|
|
2308
2331
|
try {
|
|
2309
|
-
(0,
|
|
2332
|
+
(0, import_node_fs4.unlinkSync)(PATHS.socket);
|
|
2310
2333
|
} catch {
|
|
2311
2334
|
}
|
|
2312
2335
|
}
|
|
@@ -2342,14 +2365,14 @@ var IpcServer = class {
|
|
|
2342
2365
|
return new Promise((resolve3) => {
|
|
2343
2366
|
if (!this.server) {
|
|
2344
2367
|
try {
|
|
2345
|
-
if ((0,
|
|
2368
|
+
if ((0, import_node_fs4.existsSync)(PATHS.socket)) (0, import_node_fs4.unlinkSync)(PATHS.socket);
|
|
2346
2369
|
} catch {
|
|
2347
2370
|
}
|
|
2348
2371
|
return resolve3();
|
|
2349
2372
|
}
|
|
2350
2373
|
this.server.close(() => {
|
|
2351
2374
|
try {
|
|
2352
|
-
if ((0,
|
|
2375
|
+
if ((0, import_node_fs4.existsSync)(PATHS.socket)) (0, import_node_fs4.unlinkSync)(PATHS.socket);
|
|
2353
2376
|
} catch {
|
|
2354
2377
|
}
|
|
2355
2378
|
resolve3();
|
|
@@ -2435,6 +2458,13 @@ var IpcServer = class {
|
|
|
2435
2458
|
for (const ch of req.params.channels) client.subscriptions.add(ch);
|
|
2436
2459
|
return this.send(client, { id: req.id, ok: true, result: { subscribed: [...client.subscriptions] } });
|
|
2437
2460
|
case "shutdown":
|
|
2461
|
+
if (!tokensMatch(this.controlToken, req.params?.token)) {
|
|
2462
|
+
return this.send(client, {
|
|
2463
|
+
id: req.id,
|
|
2464
|
+
ok: false,
|
|
2465
|
+
error: "shutdown requires the daemon control token (run as root, or use systemctl)"
|
|
2466
|
+
});
|
|
2467
|
+
}
|
|
2438
2468
|
this.send(client, { id: req.id, ok: true, result: "shutting down" });
|
|
2439
2469
|
setTimeout(() => process.emit("SIGTERM"), 50);
|
|
2440
2470
|
return;
|
|
@@ -2455,13 +2485,134 @@ var IpcServer = class {
|
|
|
2455
2485
|
};
|
|
2456
2486
|
|
|
2457
2487
|
// src/daemon/module-host.ts
|
|
2458
|
-
var
|
|
2459
|
-
var
|
|
2488
|
+
var import_node_fs10 = require("fs");
|
|
2489
|
+
var import_node_path5 = require("path");
|
|
2460
2490
|
var import_node_url = require("url");
|
|
2461
2491
|
var import_toml2 = __toESM(require_toml());
|
|
2462
2492
|
|
|
2493
|
+
// src/daemon/module-trust.ts
|
|
2494
|
+
var import_node_crypto2 = require("crypto");
|
|
2495
|
+
var import_node_fs5 = require("fs");
|
|
2496
|
+
var import_node_path3 = require("path");
|
|
2497
|
+
var TRUST_FILE = (0, import_node_path3.join)(PATHS.configDir, "trusted-modules.json");
|
|
2498
|
+
var PUBLISHER_KEYS_FILE = (0, import_node_path3.join)(PATHS.configDir, "publisher-keys.json");
|
|
2499
|
+
var DIGEST_EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git"]);
|
|
2500
|
+
function collectFiles(root, dir = root, out = []) {
|
|
2501
|
+
for (const entry of (0, import_node_fs5.readdirSync)(dir, { withFileTypes: true }).sort(
|
|
2502
|
+
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
2503
|
+
)) {
|
|
2504
|
+
const full = (0, import_node_path3.join)(dir, entry.name);
|
|
2505
|
+
if (entry.isSymbolicLink()) {
|
|
2506
|
+
out.push({ path: full, isSymlink: true });
|
|
2507
|
+
} else if (entry.isDirectory()) {
|
|
2508
|
+
if (DIGEST_EXCLUDED_DIRS.has(entry.name)) continue;
|
|
2509
|
+
collectFiles(root, full, out);
|
|
2510
|
+
} else if (entry.isFile()) {
|
|
2511
|
+
out.push({ path: full, isSymlink: false });
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
return out;
|
|
2515
|
+
}
|
|
2516
|
+
function computeModuleDigest(modulePath) {
|
|
2517
|
+
const hash = (0, import_node_crypto2.createHash)("sha256");
|
|
2518
|
+
for (const entry of collectFiles(modulePath)) {
|
|
2519
|
+
const relPath = (0, import_node_path3.relative)(modulePath, entry.path).split(import_node_path3.sep).join("/");
|
|
2520
|
+
hash.update(relPath);
|
|
2521
|
+
hash.update("\0");
|
|
2522
|
+
if (entry.isSymlink) {
|
|
2523
|
+
hash.update("symlink:");
|
|
2524
|
+
hash.update((0, import_node_fs5.readlinkSync)(entry.path));
|
|
2525
|
+
} else {
|
|
2526
|
+
hash.update("file:");
|
|
2527
|
+
hash.update((0, import_node_fs5.readFileSync)(entry.path));
|
|
2528
|
+
}
|
|
2529
|
+
hash.update("\0");
|
|
2530
|
+
}
|
|
2531
|
+
return hash.digest("hex");
|
|
2532
|
+
}
|
|
2533
|
+
function readTrustFile() {
|
|
2534
|
+
if (!(0, import_node_fs5.existsSync)(TRUST_FILE)) return { version: 1, modules: {} };
|
|
2535
|
+
try {
|
|
2536
|
+
const parsed = JSON.parse((0, import_node_fs5.readFileSync)(TRUST_FILE, "utf-8"));
|
|
2537
|
+
if (parsed.version !== 1 || typeof parsed.modules !== "object" || !parsed.modules) {
|
|
2538
|
+
return { version: 1, modules: {} };
|
|
2539
|
+
}
|
|
2540
|
+
return { version: 1, modules: parsed.modules };
|
|
2541
|
+
} catch {
|
|
2542
|
+
return { version: 1, modules: {} };
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
function readPublisherKeys() {
|
|
2546
|
+
if (!(0, import_node_fs5.existsSync)(PUBLISHER_KEYS_FILE)) return {};
|
|
2547
|
+
try {
|
|
2548
|
+
const parsed = JSON.parse((0, import_node_fs5.readFileSync)(PUBLISHER_KEYS_FILE, "utf-8"));
|
|
2549
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
2550
|
+
} catch {
|
|
2551
|
+
return {};
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
function verifyModuleSignature(modulePath, digest) {
|
|
2555
|
+
const keys = readPublisherKeys();
|
|
2556
|
+
const pinnedKeyIds = Object.keys(keys);
|
|
2557
|
+
const sigPath = (0, import_node_path3.join)(modulePath, "mod.sig");
|
|
2558
|
+
if (pinnedKeyIds.length === 0) {
|
|
2559
|
+
return { ok: true };
|
|
2560
|
+
}
|
|
2561
|
+
if (!(0, import_node_fs5.existsSync)(sigPath)) {
|
|
2562
|
+
return { ok: false, reason: "publisher keys are pinned but the module ships no mod.sig" };
|
|
2563
|
+
}
|
|
2564
|
+
let parsed;
|
|
2565
|
+
try {
|
|
2566
|
+
parsed = JSON.parse((0, import_node_fs5.readFileSync)(sigPath, "utf-8"));
|
|
2567
|
+
} catch {
|
|
2568
|
+
return { ok: false, reason: "mod.sig is not valid JSON" };
|
|
2569
|
+
}
|
|
2570
|
+
if (!parsed.keyId || !parsed.signature) {
|
|
2571
|
+
return { ok: false, reason: "mod.sig is missing keyId or signature" };
|
|
2572
|
+
}
|
|
2573
|
+
const publicKey = keys[parsed.keyId];
|
|
2574
|
+
if (!publicKey) {
|
|
2575
|
+
return { ok: false, reason: `mod.sig references unpinned key "${parsed.keyId}"` };
|
|
2576
|
+
}
|
|
2577
|
+
try {
|
|
2578
|
+
const valid = (0, import_node_crypto2.verify)(
|
|
2579
|
+
null,
|
|
2580
|
+
Buffer.from(digest, "hex"),
|
|
2581
|
+
publicKey,
|
|
2582
|
+
Buffer.from(parsed.signature, "base64")
|
|
2583
|
+
);
|
|
2584
|
+
return valid ? { ok: true } : { ok: false, reason: "mod.sig signature does not match" };
|
|
2585
|
+
} catch (err) {
|
|
2586
|
+
return { ok: false, reason: `signature check failed: ${String(err.message || err)}` };
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
function verifyModuleTrust(name, modulePath) {
|
|
2590
|
+
let digest;
|
|
2591
|
+
try {
|
|
2592
|
+
digest = computeModuleDigest(modulePath);
|
|
2593
|
+
} catch (err) {
|
|
2594
|
+
return { ok: false, reason: `could not hash module: ${String(err.message || err)}` };
|
|
2595
|
+
}
|
|
2596
|
+
const signature = verifyModuleSignature(modulePath, digest);
|
|
2597
|
+
if (!signature.ok) return signature;
|
|
2598
|
+
const record = readTrustFile().modules[name];
|
|
2599
|
+
if (!record) {
|
|
2600
|
+
return {
|
|
2601
|
+
ok: false,
|
|
2602
|
+
reason: `not trusted \u2014 review it, then run: threatcrush modules trust ${name}`
|
|
2603
|
+
};
|
|
2604
|
+
}
|
|
2605
|
+
if (record.digest !== digest) {
|
|
2606
|
+
return {
|
|
2607
|
+
ok: false,
|
|
2608
|
+
reason: `contents changed since it was trusted on ${record.trustedAt} \u2014 re-review it, then run: threatcrush modules trust ${name}`
|
|
2609
|
+
};
|
|
2610
|
+
}
|
|
2611
|
+
return { ok: true };
|
|
2612
|
+
}
|
|
2613
|
+
|
|
2463
2614
|
// src/daemon/watchers/log-watcher.ts
|
|
2464
|
-
var
|
|
2615
|
+
var import_node_fs6 = require("fs");
|
|
2465
2616
|
var import_node_readline = require("readline");
|
|
2466
2617
|
|
|
2467
2618
|
// src/core/log-parser.ts
|
|
@@ -2618,9 +2769,9 @@ var LogWatcher = class {
|
|
|
2618
2769
|
start() {
|
|
2619
2770
|
const started = [];
|
|
2620
2771
|
for (const src of this.sources) {
|
|
2621
|
-
if (!(0,
|
|
2772
|
+
if (!(0, import_node_fs6.existsSync)(src.path)) continue;
|
|
2622
2773
|
try {
|
|
2623
|
-
(0,
|
|
2774
|
+
(0, import_node_fs6.accessSync)(src.path, import_node_fs6.constants.R_OK);
|
|
2624
2775
|
} catch {
|
|
2625
2776
|
continue;
|
|
2626
2777
|
}
|
|
@@ -2640,7 +2791,7 @@ var LogWatcher = class {
|
|
|
2640
2791
|
}
|
|
2641
2792
|
tail(src) {
|
|
2642
2793
|
try {
|
|
2643
|
-
this.positions.set(src.path, (0,
|
|
2794
|
+
this.positions.set(src.path, (0, import_node_fs6.statSync)(src.path).size);
|
|
2644
2795
|
} catch {
|
|
2645
2796
|
this.positions.set(src.path, 0);
|
|
2646
2797
|
}
|
|
@@ -2651,7 +2802,7 @@ var LogWatcher = class {
|
|
|
2651
2802
|
poll(src) {
|
|
2652
2803
|
let stat;
|
|
2653
2804
|
try {
|
|
2654
|
-
stat = (0,
|
|
2805
|
+
stat = (0, import_node_fs6.statSync)(src.path);
|
|
2655
2806
|
} catch {
|
|
2656
2807
|
return;
|
|
2657
2808
|
}
|
|
@@ -2661,7 +2812,7 @@ var LogWatcher = class {
|
|
|
2661
2812
|
return;
|
|
2662
2813
|
}
|
|
2663
2814
|
if (stat.size === prev) return;
|
|
2664
|
-
const stream = (0,
|
|
2815
|
+
const stream = (0, import_node_fs6.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
|
|
2665
2816
|
stream.on("error", () => this.positions.set(src.path, stat.size));
|
|
2666
2817
|
const rl = (0, import_node_readline.createInterface)({ input: stream });
|
|
2667
2818
|
rl.on("error", () => {
|
|
@@ -2853,7 +3004,7 @@ function realtimeToDate(rt) {
|
|
|
2853
3004
|
|
|
2854
3005
|
// src/modules/network-monitor/index.ts
|
|
2855
3006
|
var import_node_child_process2 = require("child_process");
|
|
2856
|
-
var
|
|
3007
|
+
var import_node_fs7 = require("fs");
|
|
2857
3008
|
var NetworkMonitor = class {
|
|
2858
3009
|
constructor(bus2) {
|
|
2859
3010
|
this.bus = bus2;
|
|
@@ -2891,7 +3042,7 @@ var NetworkMonitor = class {
|
|
|
2891
3042
|
hasConntrackOrSs() {
|
|
2892
3043
|
const ss = (0, import_node_child_process2.spawnSync)("ss", ["--version"], { stdio: "pipe" });
|
|
2893
3044
|
if (ss.status === 0) return true;
|
|
2894
|
-
return (0,
|
|
3045
|
+
return (0, import_node_fs7.existsSync)("/proc/net/tcp");
|
|
2895
3046
|
}
|
|
2896
3047
|
poll() {
|
|
2897
3048
|
try {
|
|
@@ -3030,7 +3181,7 @@ var NetworkMonitor = class {
|
|
|
3030
3181
|
};
|
|
3031
3182
|
|
|
3032
3183
|
// src/modules/dns-monitor/index.ts
|
|
3033
|
-
var
|
|
3184
|
+
var import_node_fs8 = require("fs");
|
|
3034
3185
|
var import_node_readline2 = require("readline");
|
|
3035
3186
|
var DNS_LOG_SOURCES = [
|
|
3036
3187
|
"/var/log/syslog",
|
|
@@ -3064,9 +3215,9 @@ var DnsMonitor = class {
|
|
|
3064
3215
|
entropyThreshold = 3.5;
|
|
3065
3216
|
start() {
|
|
3066
3217
|
const sources = DNS_LOG_SOURCES.filter((p) => {
|
|
3067
|
-
if (!(0,
|
|
3218
|
+
if (!(0, import_node_fs8.existsSync)(p)) return false;
|
|
3068
3219
|
try {
|
|
3069
|
-
(0,
|
|
3220
|
+
(0, import_node_fs8.accessSync)(p, import_node_fs8.constants.R_OK);
|
|
3070
3221
|
return true;
|
|
3071
3222
|
} catch {
|
|
3072
3223
|
return false;
|
|
@@ -3090,7 +3241,7 @@ var DnsMonitor = class {
|
|
|
3090
3241
|
}
|
|
3091
3242
|
tailLog(path) {
|
|
3092
3243
|
try {
|
|
3093
|
-
this.positions.set(path, (0,
|
|
3244
|
+
this.positions.set(path, (0, import_node_fs8.statSync)(path).size);
|
|
3094
3245
|
} catch {
|
|
3095
3246
|
this.positions.set(path, 0);
|
|
3096
3247
|
}
|
|
@@ -3100,7 +3251,7 @@ var DnsMonitor = class {
|
|
|
3100
3251
|
pollLog(path) {
|
|
3101
3252
|
let stat;
|
|
3102
3253
|
try {
|
|
3103
|
-
stat = (0,
|
|
3254
|
+
stat = (0, import_node_fs8.statSync)(path);
|
|
3104
3255
|
} catch {
|
|
3105
3256
|
return;
|
|
3106
3257
|
}
|
|
@@ -3110,7 +3261,7 @@ var DnsMonitor = class {
|
|
|
3110
3261
|
return;
|
|
3111
3262
|
}
|
|
3112
3263
|
if (stat.size === prev) return;
|
|
3113
|
-
const stream = (0,
|
|
3264
|
+
const stream = (0, import_node_fs8.createReadStream)(path, { start: prev, encoding: "utf-8" });
|
|
3114
3265
|
stream.on("error", () => this.positions.set(path, stat.size));
|
|
3115
3266
|
const rl = (0, import_node_readline2.createInterface)({ input: stream });
|
|
3116
3267
|
rl.on("line", (line) => this.parseDnsLine(line));
|
|
@@ -3239,8 +3390,8 @@ var DnsMonitor = class {
|
|
|
3239
3390
|
};
|
|
3240
3391
|
|
|
3241
3392
|
// src/core/config.ts
|
|
3242
|
-
var
|
|
3243
|
-
var
|
|
3393
|
+
var import_node_fs9 = require("fs");
|
|
3394
|
+
var import_node_path4 = require("path");
|
|
3244
3395
|
var import_toml = __toESM(require_toml());
|
|
3245
3396
|
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
3246
3397
|
var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
|
|
@@ -3266,11 +3417,11 @@ var DEFAULT_CONFIG = {
|
|
|
3266
3417
|
};
|
|
3267
3418
|
function loadConfig(configPath) {
|
|
3268
3419
|
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
3269
|
-
if (!(0,
|
|
3420
|
+
if (!(0, import_node_fs9.existsSync)(path)) {
|
|
3270
3421
|
return { ...DEFAULT_CONFIG };
|
|
3271
3422
|
}
|
|
3272
3423
|
try {
|
|
3273
|
-
const raw = (0,
|
|
3424
|
+
const raw = (0, import_node_fs9.readFileSync)(path, "utf-8");
|
|
3274
3425
|
const parsed = import_toml.default.parse(raw);
|
|
3275
3426
|
return {
|
|
3276
3427
|
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
@@ -3286,13 +3437,13 @@ function loadConfig(configPath) {
|
|
|
3286
3437
|
function loadModuleConfigs(confDir) {
|
|
3287
3438
|
const dir = confDir || DEFAULT_CONFDIR;
|
|
3288
3439
|
const configs = /* @__PURE__ */ new Map();
|
|
3289
|
-
if (!(0,
|
|
3440
|
+
if (!(0, import_node_fs9.existsSync)(dir)) {
|
|
3290
3441
|
return configs;
|
|
3291
3442
|
}
|
|
3292
|
-
const files = (0,
|
|
3443
|
+
const files = (0, import_node_fs9.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
|
|
3293
3444
|
for (const file of files) {
|
|
3294
3445
|
try {
|
|
3295
|
-
const raw = (0,
|
|
3446
|
+
const raw = (0, import_node_fs9.readFileSync)((0, import_node_path4.join)(dir, file), "utf-8");
|
|
3296
3447
|
const parsed = import_toml.default.parse(raw);
|
|
3297
3448
|
for (const [name, config] of Object.entries(parsed)) {
|
|
3298
3449
|
configs.set(name, config);
|
|
@@ -3406,15 +3557,15 @@ var ModuleHost = class {
|
|
|
3406
3557
|
for (const m of builtins) this.modules.set(m.name, m);
|
|
3407
3558
|
}
|
|
3408
3559
|
async discoverAndStartInstalled() {
|
|
3409
|
-
if (!(0,
|
|
3560
|
+
if (!(0, import_node_fs10.existsSync)(PATHS.moduleDir)) return;
|
|
3410
3561
|
const configs = loadModuleConfigs(PATHS.confD);
|
|
3411
|
-
const entries = (0,
|
|
3562
|
+
const entries = (0, import_node_fs10.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
|
|
3412
3563
|
for (const entry of entries) {
|
|
3413
3564
|
if (!entry.isDirectory()) continue;
|
|
3414
|
-
const manifestPath = (0,
|
|
3415
|
-
if (!(0,
|
|
3565
|
+
const manifestPath = (0, import_node_path5.join)(PATHS.moduleDir, entry.name, "mod.toml");
|
|
3566
|
+
if (!(0, import_node_fs10.existsSync)(manifestPath)) continue;
|
|
3416
3567
|
try {
|
|
3417
|
-
const manifest = import_toml2.default.parse((0,
|
|
3568
|
+
const manifest = import_toml2.default.parse((0, import_node_fs10.readFileSync)(manifestPath, "utf-8"));
|
|
3418
3569
|
const name = manifest.module?.name || entry.name;
|
|
3419
3570
|
const defaults = manifest.module?.config?.defaults || {};
|
|
3420
3571
|
const config = {
|
|
@@ -3428,7 +3579,7 @@ var ModuleHost = class {
|
|
|
3428
3579
|
source: "installed",
|
|
3429
3580
|
status: config.enabled === false ? "disabled" : "loaded",
|
|
3430
3581
|
events: 0,
|
|
3431
|
-
path: (0,
|
|
3582
|
+
path: (0, import_node_path5.join)(PATHS.moduleDir, entry.name),
|
|
3432
3583
|
config
|
|
3433
3584
|
};
|
|
3434
3585
|
this.modules.set(name, hosted);
|
|
@@ -3443,7 +3594,7 @@ var ModuleHost = class {
|
|
|
3443
3594
|
status: "error",
|
|
3444
3595
|
events: 0,
|
|
3445
3596
|
detail: `manifest load failed: ${String(err.message || err)}`,
|
|
3446
|
-
path: (0,
|
|
3597
|
+
path: (0, import_node_path5.join)(PATHS.moduleDir, entry.name)
|
|
3447
3598
|
});
|
|
3448
3599
|
}
|
|
3449
3600
|
}
|
|
@@ -3455,6 +3606,13 @@ var ModuleHost = class {
|
|
|
3455
3606
|
hosted.detail = "no built entrypoint found; run npm install && npm run build in the module directory";
|
|
3456
3607
|
return;
|
|
3457
3608
|
}
|
|
3609
|
+
const trust = verifyModuleTrust(hosted.name, hosted.path);
|
|
3610
|
+
if (!trust.ok) {
|
|
3611
|
+
hosted.status = "error";
|
|
3612
|
+
hosted.detail = `refusing to load: ${trust.reason}`;
|
|
3613
|
+
this.bus.announceModule(hosted.name, "error", hosted.detail);
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3458
3616
|
try {
|
|
3459
3617
|
const imported = await import((0, import_node_url.pathToFileURL)(entrypoint).href);
|
|
3460
3618
|
const exported = imported.default || imported.module || imported;
|
|
@@ -3475,17 +3633,17 @@ var ModuleHost = class {
|
|
|
3475
3633
|
}
|
|
3476
3634
|
}
|
|
3477
3635
|
installedEntrypoint(modulePath) {
|
|
3478
|
-
const packageJson = (0,
|
|
3636
|
+
const packageJson = (0, import_node_path5.join)(modulePath, "package.json");
|
|
3479
3637
|
const candidates = [];
|
|
3480
|
-
if ((0,
|
|
3638
|
+
if ((0, import_node_fs10.existsSync)(packageJson)) {
|
|
3481
3639
|
try {
|
|
3482
|
-
const pkg = JSON.parse((0,
|
|
3483
|
-
if (pkg.main) candidates.push((0,
|
|
3640
|
+
const pkg = JSON.parse((0, import_node_fs10.readFileSync)(packageJson, "utf-8"));
|
|
3641
|
+
if (pkg.main) candidates.push((0, import_node_path5.join)(modulePath, pkg.main));
|
|
3484
3642
|
} catch {
|
|
3485
3643
|
}
|
|
3486
3644
|
}
|
|
3487
|
-
candidates.push((0,
|
|
3488
|
-
return candidates.find((candidate) => (0,
|
|
3645
|
+
candidates.push((0, import_node_path5.join)(modulePath, "dist", "index.js"), (0, import_node_path5.join)(modulePath, "index.js"));
|
|
3646
|
+
return candidates.find((candidate) => (0, import_node_fs10.existsSync)(candidate)) || null;
|
|
3489
3647
|
}
|
|
3490
3648
|
isThreatCrushModule(value) {
|
|
3491
3649
|
return Boolean(
|
|
@@ -3757,14 +3915,14 @@ function slackChannel(webhookUrl) {
|
|
|
3757
3915
|
}
|
|
3758
3916
|
|
|
3759
3917
|
// src/core/cli-config.ts
|
|
3760
|
-
var
|
|
3761
|
-
var
|
|
3918
|
+
var import_node_fs11 = require("fs");
|
|
3919
|
+
var import_node_path6 = require("path");
|
|
3762
3920
|
var import_node_os2 = require("os");
|
|
3763
|
-
var CLI_CONFIG_DIR = (0,
|
|
3764
|
-
var CLI_CONFIG_PATH = (0,
|
|
3921
|
+
var CLI_CONFIG_DIR = (0, import_node_path6.join)((0, import_node_os2.homedir)(), ".threatcrush");
|
|
3922
|
+
var CLI_CONFIG_PATH = (0, import_node_path6.join)(CLI_CONFIG_DIR, "config.json");
|
|
3765
3923
|
function readCliConfig() {
|
|
3766
3924
|
try {
|
|
3767
|
-
return JSON.parse((0,
|
|
3925
|
+
return JSON.parse((0, import_node_fs11.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
|
|
3768
3926
|
} catch {
|
|
3769
3927
|
return {};
|
|
3770
3928
|
}
|
|
@@ -3783,8 +3941,8 @@ function authHeaders() {
|
|
|
3783
3941
|
}
|
|
3784
3942
|
|
|
3785
3943
|
// src/commands/scan.ts
|
|
3786
|
-
var
|
|
3787
|
-
var
|
|
3944
|
+
var import_node_fs14 = require("fs");
|
|
3945
|
+
var import_node_path10 = require("path");
|
|
3788
3946
|
|
|
3789
3947
|
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
3790
3948
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -4527,7 +4685,17 @@ var NODE_RULES = [
|
|
|
4527
4685
|
// arrangement of nearby lines that makes an echoed origin safe — except a
|
|
4528
4686
|
// membership test on the value, which is what the guard looks for.
|
|
4529
4687
|
inherent: true,
|
|
4530
|
-
guard: ORIGIN_ALLOWLIST_GUARD
|
|
4688
|
+
guard: ORIGIN_ALLOWLIST_GUARD,
|
|
4689
|
+
/**
|
|
4690
|
+
* Logging the origin is not reflecting it.
|
|
4691
|
+
*
|
|
4692
|
+
* `origin: request.headers.origin` is the CORS mistake *and* the ordinary
|
|
4693
|
+
* way to record who called — the two are character-for-character
|
|
4694
|
+
* identical, and only what encloses them differs. A response header goes
|
|
4695
|
+
* out to the browser; a log line goes to stdout, where it grants nobody
|
|
4696
|
+
* anything.
|
|
4697
|
+
*/
|
|
4698
|
+
enclosingCallGuard: /(?:^|\.)(?:log|debug|info|warn|error|trace|verbose|fatal)$/
|
|
4531
4699
|
},
|
|
4532
4700
|
// ── Server-side request forgery ──────────────────────────────────────────
|
|
4533
4701
|
{
|
|
@@ -4776,7 +4944,8 @@ var CODE_RULES = [
|
|
|
4776
4944
|
cwe: "CWE-78",
|
|
4777
4945
|
severity: "critical",
|
|
4778
4946
|
languages: ["javascript", "typescript"],
|
|
4779
|
-
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)
|
|
4947
|
+
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/,
|
|
4948
|
+
constantInterpolationGuard: true
|
|
4780
4949
|
},
|
|
4781
4950
|
{
|
|
4782
4951
|
id: "py-shell-command-string",
|
|
@@ -5675,7 +5844,26 @@ var CODE_RULES = [
|
|
|
5675
5844
|
cwe: "CWE-346",
|
|
5676
5845
|
severity: "high",
|
|
5677
5846
|
languages: ["javascript", "typescript"],
|
|
5678
|
-
pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b
|
|
5847
|
+
pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b/,
|
|
5848
|
+
/**
|
|
5849
|
+
* Parsing the request's own URL is not building a link from the Host
|
|
5850
|
+
* header, even though it is spelled with one.
|
|
5851
|
+
*
|
|
5852
|
+
* const url = new URL(request.url, `http://${request.headers.host}`);
|
|
5853
|
+
* const token = url.searchParams.get('token');
|
|
5854
|
+
*
|
|
5855
|
+
* `request.url` on a Node server is a path — `/ws?token=…` — and `new URL`
|
|
5856
|
+
* refuses a relative input without a base. The base exists to satisfy the
|
|
5857
|
+
* parser and is thrown away; only the path and query are ever read. Every
|
|
5858
|
+
* Node HTTP handler that wants a query parameter is written this way, so
|
|
5859
|
+
* the rule fired on the framework idiom rather than on the defect.
|
|
5860
|
+
*
|
|
5861
|
+
* Narrow on purpose: the first argument must be `req.url` itself. The
|
|
5862
|
+
* dangerous shape passes a *path* the application chose —
|
|
5863
|
+
* `new URL('/reset?t=…', `https://${req.headers.host}`)` — and that is what
|
|
5864
|
+
* produces an attacker-controlled link. It does not match this guard.
|
|
5865
|
+
*/
|
|
5866
|
+
lineGuard: /\bnew\s+URL\s*\(\s*(?:req|request|ctx)(?:uest)?\s*\.\s*url\b\s*,/
|
|
5679
5867
|
},
|
|
5680
5868
|
// A recursive-merge prototype-pollution rule (`target[key] = source[key]`
|
|
5681
5869
|
// with no `__proto__` guard) was built and dropped. The bare copy-by-key is
|
|
@@ -5743,6 +5931,55 @@ function fileTextOf(lines) {
|
|
|
5743
5931
|
function withoutSingleQuoted(text) {
|
|
5744
5932
|
return text.replace(/'[^'\n]*'/g, "''");
|
|
5745
5933
|
}
|
|
5934
|
+
function calleeEndingAt(text, open) {
|
|
5935
|
+
let end = open;
|
|
5936
|
+
while (end > 0 && (text[end - 1] === " " || text[end - 1] === " ")) end -= 1;
|
|
5937
|
+
let start = end;
|
|
5938
|
+
while (start > 0 && /[\w$.]/.test(text[start - 1])) start -= 1;
|
|
5939
|
+
return text.slice(start, end);
|
|
5940
|
+
}
|
|
5941
|
+
function enclosingCallees(lines, index, back) {
|
|
5942
|
+
const before = lines.slice(Math.max(0, index - back), index).join("\n");
|
|
5943
|
+
const stack = [];
|
|
5944
|
+
let quote = null;
|
|
5945
|
+
for (let i = 0; i < before.length; i += 1) {
|
|
5946
|
+
const ch = before[i];
|
|
5947
|
+
if (quote) {
|
|
5948
|
+
if (ch === "\\") i += 1;
|
|
5949
|
+
else if (ch === quote) quote = null;
|
|
5950
|
+
continue;
|
|
5951
|
+
}
|
|
5952
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
5953
|
+
else if (ch === "(") stack.push(calleeEndingAt(before, i));
|
|
5954
|
+
else if (ch === ")") stack.pop();
|
|
5955
|
+
}
|
|
5956
|
+
return stack;
|
|
5957
|
+
}
|
|
5958
|
+
function interpolations(line) {
|
|
5959
|
+
const found = [];
|
|
5960
|
+
for (let i = 0; ; ) {
|
|
5961
|
+
const start = line.indexOf("${", i);
|
|
5962
|
+
if (start === -1) break;
|
|
5963
|
+
const end = line.indexOf("}", start + 2);
|
|
5964
|
+
if (end === -1) return null;
|
|
5965
|
+
found.push(line.slice(start + 2, end).trim());
|
|
5966
|
+
i = end + 1;
|
|
5967
|
+
}
|
|
5968
|
+
return found.length > 0 ? found : null;
|
|
5969
|
+
}
|
|
5970
|
+
function isConstantString(name, fileText) {
|
|
5971
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5972
|
+
return new RegExp(
|
|
5973
|
+
`\\bconst\\s+${escaped}\\s*(?::[^=\\n]+)?=\\s*(?:'[^'\\n]*'|"[^"\\n]*"|\`[^\`$\\n]*\`)`
|
|
5974
|
+
).test(fileText);
|
|
5975
|
+
}
|
|
5976
|
+
function interpolationsAreConstant(line, fileText) {
|
|
5977
|
+
const found = interpolations(line);
|
|
5978
|
+
if (!found) return false;
|
|
5979
|
+
return found.every(
|
|
5980
|
+
(expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
|
|
5981
|
+
);
|
|
5982
|
+
}
|
|
5746
5983
|
function evaluateRule(rule, ctx) {
|
|
5747
5984
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
5748
5985
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -5754,6 +5991,13 @@ function evaluateRule(rule, ctx) {
|
|
|
5754
5991
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
5755
5992
|
if (rule.requires && !rule.requires.test(context)) return null;
|
|
5756
5993
|
if (rule.lineGuard?.test(line)) return null;
|
|
5994
|
+
if (rule.enclosingCallGuard) {
|
|
5995
|
+
const callees = enclosingCallees(ctx.lines, ctx.index, back);
|
|
5996
|
+
if (callees.some((callee) => rule.enclosingCallGuard.test(callee))) return null;
|
|
5997
|
+
}
|
|
5998
|
+
if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
|
|
5999
|
+
return null;
|
|
6000
|
+
}
|
|
5757
6001
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
5758
6002
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
5759
6003
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -6336,7 +6580,8 @@ var SECRET_RULES = [
|
|
|
6336
6580
|
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
6337
6581
|
severity: "medium",
|
|
6338
6582
|
cwe: "CWE-798",
|
|
6339
|
-
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
|
|
6583
|
+
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived.",
|
|
6584
|
+
keywordShaped: true
|
|
6340
6585
|
},
|
|
6341
6586
|
{
|
|
6342
6587
|
id: "secret-generic-api-key",
|
|
@@ -6347,7 +6592,8 @@ var SECRET_RULES = [
|
|
|
6347
6592
|
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
6348
6593
|
severity: "high",
|
|
6349
6594
|
cwe: "CWE-798",
|
|
6350
|
-
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
|
|
6595
|
+
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid.",
|
|
6596
|
+
keywordShaped: true
|
|
6351
6597
|
},
|
|
6352
6598
|
{
|
|
6353
6599
|
id: "secret-generic-credential",
|
|
@@ -6355,7 +6601,8 @@ var SECRET_RULES = [
|
|
|
6355
6601
|
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
6356
6602
|
severity: "high",
|
|
6357
6603
|
cwe: "CWE-798",
|
|
6358
|
-
consequence: "A password in source is a password in every clone, fork and CI cache of that source."
|
|
6604
|
+
consequence: "A password in source is a password in every clone, fork and CI cache of that source.",
|
|
6605
|
+
keywordShaped: true
|
|
6359
6606
|
},
|
|
6360
6607
|
{
|
|
6361
6608
|
id: "secret-hex-token",
|
|
@@ -6363,7 +6610,8 @@ var SECRET_RULES = [
|
|
|
6363
6610
|
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
6364
6611
|
severity: "medium",
|
|
6365
6612
|
cwe: "CWE-798",
|
|
6366
|
-
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
|
|
6613
|
+
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign.",
|
|
6614
|
+
keywordShaped: true
|
|
6367
6615
|
}
|
|
6368
6616
|
];
|
|
6369
6617
|
var KNOWN_PLACEHOLDERS = [
|
|
@@ -6394,6 +6642,54 @@ var KNOWN_PLACEHOLDERS = [
|
|
|
6394
6642
|
function isKnownPlaceholder(text) {
|
|
6395
6643
|
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
6396
6644
|
}
|
|
6645
|
+
function isVariableReference(value) {
|
|
6646
|
+
const trimmed = value.trim();
|
|
6647
|
+
const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
|
|
6648
|
+
if (braced) {
|
|
6649
|
+
const fallback = braced[2];
|
|
6650
|
+
if (fallback === void 0 || fallback.trim() === "") return true;
|
|
6651
|
+
return /^\$\{?[A-Za-z_][\w.]*\}?$/.test(fallback.trim());
|
|
6652
|
+
}
|
|
6653
|
+
return /^\$[A-Za-z_]\w*$/.test(trimmed) || // $VAR
|
|
6654
|
+
/^\$\([\s\S]*\)$/.test(trimmed) || // $(command substitution)
|
|
6655
|
+
/^%[A-Za-z_]\w*%$/.test(trimmed) || // %VAR% on Windows
|
|
6656
|
+
/^\{\{[\s\S]*\}\}$/.test(trimmed) || // {{ template }}
|
|
6657
|
+
/^#\{[\s\S]*\}$/.test(trimmed) || // #{ruby}
|
|
6658
|
+
/^<%=?[\s\S]*%>$/.test(trimmed);
|
|
6659
|
+
}
|
|
6660
|
+
var FIXTURE_STEMS = [
|
|
6661
|
+
"test",
|
|
6662
|
+
"mock",
|
|
6663
|
+
"fake",
|
|
6664
|
+
"dummy",
|
|
6665
|
+
"stub",
|
|
6666
|
+
"sample",
|
|
6667
|
+
"example",
|
|
6668
|
+
"placeholder",
|
|
6669
|
+
"fixture",
|
|
6670
|
+
"invalid",
|
|
6671
|
+
"expired",
|
|
6672
|
+
"forged",
|
|
6673
|
+
"bogus",
|
|
6674
|
+
"notreal",
|
|
6675
|
+
"nonexistent",
|
|
6676
|
+
"changeme",
|
|
6677
|
+
"foobar",
|
|
6678
|
+
"lorem"
|
|
6679
|
+
];
|
|
6680
|
+
var KEY_NOISE = /* @__PURE__ */ new Set(["const", "this", "return", "await", "async", "expect", "value"]);
|
|
6681
|
+
function words(text) {
|
|
6682
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z]+/g, " ").toLowerCase().split(" ").filter(Boolean);
|
|
6683
|
+
}
|
|
6684
|
+
function isTestFixtureValue(line, value) {
|
|
6685
|
+
const valueWords = words(value);
|
|
6686
|
+
if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
|
|
6687
|
+
if (value.length > 48) return false;
|
|
6688
|
+
const valueAt = line.lastIndexOf(value);
|
|
6689
|
+
const key = valueAt === -1 ? line : line.slice(0, valueAt);
|
|
6690
|
+
const flattened = valueWords.join("");
|
|
6691
|
+
return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
|
|
6692
|
+
}
|
|
6397
6693
|
function redactSecret(line) {
|
|
6398
6694
|
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
6399
6695
|
if (match.length <= 12) return match;
|
|
@@ -6612,6 +6908,9 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6612
6908
|
if (!match) continue;
|
|
6613
6909
|
if (isKnownPlaceholder(match[0])) continue;
|
|
6614
6910
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
6911
|
+
const value = match[1] ?? match[0];
|
|
6912
|
+
if (isVariableReference(value)) continue;
|
|
6913
|
+
if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
|
|
6615
6914
|
const marked = foreignCredentialMark(line);
|
|
6616
6915
|
findings.push({
|
|
6617
6916
|
ruleId: rule.id,
|
|
@@ -6690,8 +6989,8 @@ function scanManifest(relativePath, filename, text) {
|
|
|
6690
6989
|
}
|
|
6691
6990
|
|
|
6692
6991
|
// ../../packages/scan/src/node/walk.ts
|
|
6693
|
-
var
|
|
6694
|
-
var
|
|
6992
|
+
var import_node_fs12 = require("fs");
|
|
6993
|
+
var import_node_path7 = require("path");
|
|
6695
6994
|
function compileExcludes(patterns) {
|
|
6696
6995
|
const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
|
|
6697
6996
|
if (matchers.length === 0) return () => false;
|
|
@@ -6762,7 +7061,7 @@ function matchPrefix(parts, segs) {
|
|
|
6762
7061
|
}
|
|
6763
7062
|
function readIgnoreFile(root) {
|
|
6764
7063
|
try {
|
|
6765
|
-
return (0,
|
|
7064
|
+
return (0, import_node_fs12.readFileSync)((0, import_node_path7.join)(root, ".threatcrushignore"), "utf-8").split("\n");
|
|
6766
7065
|
} catch {
|
|
6767
7066
|
return [];
|
|
6768
7067
|
}
|
|
@@ -6778,16 +7077,16 @@ function scanPath(targetPath, options = {}) {
|
|
|
6778
7077
|
let excluded = 0;
|
|
6779
7078
|
const rootIsDirectory = (() => {
|
|
6780
7079
|
try {
|
|
6781
|
-
return (0,
|
|
7080
|
+
return (0, import_node_fs12.statSync)(targetPath).isDirectory();
|
|
6782
7081
|
} catch {
|
|
6783
7082
|
return true;
|
|
6784
7083
|
}
|
|
6785
7084
|
})();
|
|
6786
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
7085
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path7.dirname)(targetPath);
|
|
6787
7086
|
const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
|
|
6788
7087
|
const scanFile = (fullPath, filename) => {
|
|
6789
7088
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
6790
|
-
const extension = (0,
|
|
7089
|
+
const extension = (0, import_node_path7.extname)(filename).toLowerCase();
|
|
6791
7090
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
6792
7091
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
6793
7092
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -6799,26 +7098,26 @@ function scanPath(targetPath, options = {}) {
|
|
|
6799
7098
|
let handle;
|
|
6800
7099
|
let declared = null;
|
|
6801
7100
|
try {
|
|
6802
|
-
handle = (0,
|
|
7101
|
+
handle = (0, import_node_fs12.openSync)(fullPath, "r");
|
|
6803
7102
|
} catch {
|
|
6804
7103
|
unreadable.push(relativePath);
|
|
6805
7104
|
return;
|
|
6806
7105
|
}
|
|
6807
7106
|
try {
|
|
6808
|
-
if ((0,
|
|
7107
|
+
if ((0, import_node_fs12.fstatSync)(handle).size > maxFileBytes) return;
|
|
6809
7108
|
if (mayDeclareInterpreter) {
|
|
6810
7109
|
const prefix = Buffer.alloc(128);
|
|
6811
|
-
const read = (0,
|
|
7110
|
+
const read = (0, import_node_fs12.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
6812
7111
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
6813
7112
|
if (!declared) return;
|
|
6814
7113
|
}
|
|
6815
|
-
text = (0,
|
|
7114
|
+
text = (0, import_node_fs12.readFileSync)(handle, "utf-8");
|
|
6816
7115
|
} catch {
|
|
6817
7116
|
unreadable.push(relativePath);
|
|
6818
7117
|
return;
|
|
6819
7118
|
} finally {
|
|
6820
7119
|
try {
|
|
6821
|
-
(0,
|
|
7120
|
+
(0, import_node_fs12.closeSync)(handle);
|
|
6822
7121
|
} catch {
|
|
6823
7122
|
}
|
|
6824
7123
|
}
|
|
@@ -6836,13 +7135,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
6836
7135
|
const walk = (currentPath) => {
|
|
6837
7136
|
let entries;
|
|
6838
7137
|
try {
|
|
6839
|
-
entries = (0,
|
|
7138
|
+
entries = (0, import_node_fs12.readdirSync)(currentPath, { withFileTypes: true });
|
|
6840
7139
|
} catch {
|
|
6841
7140
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
6842
7141
|
return;
|
|
6843
7142
|
}
|
|
6844
7143
|
for (const entry of entries) {
|
|
6845
|
-
const fullPath = (0,
|
|
7144
|
+
const fullPath = (0, import_node_path7.join)(currentPath, entry.name);
|
|
6846
7145
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
6847
7146
|
if (entry.isDirectory()) {
|
|
6848
7147
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
@@ -6866,7 +7165,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
6866
7165
|
} else if (isExcluded(toRelative(walkRoot, targetPath))) {
|
|
6867
7166
|
excluded += 1;
|
|
6868
7167
|
} else {
|
|
6869
|
-
scanFile(targetPath, (0,
|
|
7168
|
+
scanFile(targetPath, (0, import_node_path7.basename)(targetPath));
|
|
6870
7169
|
}
|
|
6871
7170
|
if (options.missingControls) findings.push(...controls.findings());
|
|
6872
7171
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
@@ -6898,32 +7197,37 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
6898
7197
|
}
|
|
6899
7198
|
}
|
|
6900
7199
|
function toRelative(base, target) {
|
|
6901
|
-
const rel = (0,
|
|
6902
|
-
return (rel === "" ? target : rel).split(
|
|
7200
|
+
const rel = (0, import_node_path7.relative)(base, target);
|
|
7201
|
+
return (rel === "" ? target : rel).split(import_node_path7.sep).join("/");
|
|
6903
7202
|
}
|
|
6904
7203
|
|
|
6905
7204
|
// ../../packages/scan/src/node/dependencies.ts
|
|
6906
|
-
var
|
|
6907
|
-
var
|
|
6908
|
-
var LOCKFILES = [
|
|
6909
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
6910
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
6911
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
6912
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
6913
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
6914
|
-
];
|
|
7205
|
+
var import_node_fs13 = require("fs");
|
|
7206
|
+
var import_node_path8 = require("path");
|
|
6915
7207
|
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
6916
7208
|
async function scanDependencies(targetPath) {
|
|
6917
7209
|
const findings = [];
|
|
6918
|
-
for (const { file, ecosystem } of LOCKFILES) {
|
|
6919
|
-
const lockPath = (0,
|
|
6920
|
-
if (!(0,
|
|
7210
|
+
for (const { file, ecosystem, parse } of LOCKFILES) {
|
|
7211
|
+
const lockPath = (0, import_node_path8.join)(targetPath, file);
|
|
7212
|
+
if (!(0, import_node_fs13.existsSync)(lockPath)) continue;
|
|
6921
7213
|
let deps;
|
|
6922
7214
|
try {
|
|
6923
|
-
deps =
|
|
7215
|
+
deps = dedupe(parse((0, import_node_fs13.readFileSync)(lockPath, "utf-8")));
|
|
6924
7216
|
} catch {
|
|
6925
7217
|
continue;
|
|
6926
7218
|
}
|
|
7219
|
+
if (deps.length === 0) {
|
|
7220
|
+
findings.push(incompleteFinding(file, "No dependencies could be read from this lockfile."));
|
|
7221
|
+
continue;
|
|
7222
|
+
}
|
|
7223
|
+
if (deps.length > MAX_DEPS_PER_LOCKFILE) {
|
|
7224
|
+
findings.push(
|
|
7225
|
+
incompleteFinding(
|
|
7226
|
+
file,
|
|
7227
|
+
`Only the first ${MAX_DEPS_PER_LOCKFILE} of ${deps.length} locked packages were checked against OSV.`
|
|
7228
|
+
)
|
|
7229
|
+
);
|
|
7230
|
+
}
|
|
6927
7231
|
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
6928
7232
|
let vulns;
|
|
6929
7233
|
try {
|
|
@@ -6950,6 +7254,20 @@ async function scanDependencies(targetPath) {
|
|
|
6950
7254
|
}
|
|
6951
7255
|
return findings;
|
|
6952
7256
|
}
|
|
7257
|
+
function incompleteFinding(file, message) {
|
|
7258
|
+
return {
|
|
7259
|
+
ruleId: "dependency-scan-incomplete",
|
|
7260
|
+
title: "Dependency scan incomplete",
|
|
7261
|
+
file,
|
|
7262
|
+
line: 1,
|
|
7263
|
+
severity: "low",
|
|
7264
|
+
confidence: "evidence",
|
|
7265
|
+
message,
|
|
7266
|
+
consequence: "Advisories affecting the unchecked packages would not appear in this report.",
|
|
7267
|
+
excerpt: file,
|
|
7268
|
+
category: "dependency"
|
|
7269
|
+
};
|
|
7270
|
+
}
|
|
6953
7271
|
function severityFromCvss(score) {
|
|
6954
7272
|
if (!score) return "medium";
|
|
6955
7273
|
const value = Number.parseFloat(score);
|
|
@@ -6959,26 +7277,131 @@ function severityFromCvss(score) {
|
|
|
6959
7277
|
if (value >= 4) return "medium";
|
|
6960
7278
|
return "low";
|
|
6961
7279
|
}
|
|
6962
|
-
function
|
|
7280
|
+
function dedupe(deps) {
|
|
7281
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7282
|
+
const unique = [];
|
|
7283
|
+
for (const dep of deps) {
|
|
7284
|
+
const key = `${dep.name}@${dep.version}`;
|
|
7285
|
+
if (seen.has(key)) continue;
|
|
7286
|
+
seen.add(key);
|
|
7287
|
+
unique.push(dep);
|
|
7288
|
+
}
|
|
7289
|
+
return unique;
|
|
7290
|
+
}
|
|
7291
|
+
function splitNameVersion(spec) {
|
|
7292
|
+
const at = spec.lastIndexOf("@");
|
|
7293
|
+
if (at <= 0) return null;
|
|
7294
|
+
const name = spec.slice(0, at);
|
|
7295
|
+
const version = spec.slice(at + 1);
|
|
7296
|
+
if (!name || !version) return null;
|
|
7297
|
+
return { name, version };
|
|
7298
|
+
}
|
|
7299
|
+
function exactVersion(raw) {
|
|
7300
|
+
const version = raw.trim().replace(/^[=v]+/, "");
|
|
7301
|
+
return /^[0-9][0-9a-zA-Z.+-]*$/.test(version) ? version : null;
|
|
7302
|
+
}
|
|
7303
|
+
function parsePackageLock(content) {
|
|
7304
|
+
const lock = JSON.parse(content);
|
|
7305
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
7306
|
+
const deps = [];
|
|
7307
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
7308
|
+
const name = key.replace(/^.*node_modules\//, "");
|
|
7309
|
+
const version = value?.version;
|
|
7310
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
7311
|
+
}
|
|
7312
|
+
return deps;
|
|
7313
|
+
}
|
|
7314
|
+
function parsePnpmLock(content) {
|
|
6963
7315
|
const deps = [];
|
|
6964
|
-
|
|
6965
|
-
|
|
6966
|
-
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
7316
|
+
let inPackages = false;
|
|
7317
|
+
for (const line of content.split("\n")) {
|
|
7318
|
+
if (/^[a-zA-Z]/.test(line)) {
|
|
7319
|
+
inPackages = line.startsWith("packages:");
|
|
7320
|
+
continue;
|
|
7321
|
+
}
|
|
7322
|
+
if (!inPackages) continue;
|
|
7323
|
+
const match = /^ {2}(?! )(.+):\s*$/.exec(line);
|
|
7324
|
+
if (!match?.[1]) continue;
|
|
7325
|
+
let key = match[1].trim().replace(/^['"]|['"]$/g, "");
|
|
7326
|
+
key = key.replace(/^\//, "");
|
|
7327
|
+
key = key.replace(/\(.*$/, "");
|
|
7328
|
+
let name;
|
|
7329
|
+
let rawVersion;
|
|
7330
|
+
const slashed = /^(@?[^@]+)\/([0-9][^/]*)$/.exec(key);
|
|
7331
|
+
if (slashed?.[1] && slashed[2]) {
|
|
7332
|
+
name = slashed[1];
|
|
7333
|
+
rawVersion = slashed[2];
|
|
7334
|
+
} else {
|
|
7335
|
+
const dep = splitNameVersion(key);
|
|
7336
|
+
if (!dep) continue;
|
|
7337
|
+
name = dep.name;
|
|
7338
|
+
rawVersion = dep.version;
|
|
6971
7339
|
}
|
|
6972
|
-
|
|
7340
|
+
const version = exactVersion(rawVersion.replace(/_.*$/, ""));
|
|
7341
|
+
if (version) deps.push({ name, version });
|
|
6973
7342
|
}
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
7343
|
+
return deps;
|
|
7344
|
+
}
|
|
7345
|
+
function parseYarnLock(content) {
|
|
7346
|
+
const deps = [];
|
|
7347
|
+
let pendingName = null;
|
|
7348
|
+
for (const line of content.split("\n")) {
|
|
7349
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
7350
|
+
if (!/^\s/.test(line)) {
|
|
7351
|
+
pendingName = null;
|
|
7352
|
+
const header = line.replace(/:\s*$/, "");
|
|
7353
|
+
const first = header.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
|
|
7354
|
+
if (!first) continue;
|
|
7355
|
+
if (!first.includes("@") || first === "__metadata") continue;
|
|
7356
|
+
if (/@(?:workspace|file|link|portal|exec|patch):/.test(first)) continue;
|
|
7357
|
+
const dep = splitNameVersion(first.replace(/@npm:/, "@"));
|
|
7358
|
+
if (dep) pendingName = dep.name;
|
|
7359
|
+
continue;
|
|
6978
7360
|
}
|
|
7361
|
+
if (!pendingName) continue;
|
|
7362
|
+
const version = /^\s+version:?\s+["']?([^"'\s]+)["']?\s*$/.exec(line);
|
|
7363
|
+
if (!version?.[1]) continue;
|
|
7364
|
+
const exact = exactVersion(version[1]);
|
|
7365
|
+
if (exact) deps.push({ name: pendingName, version: exact });
|
|
7366
|
+
pendingName = null;
|
|
6979
7367
|
}
|
|
6980
7368
|
return deps;
|
|
6981
7369
|
}
|
|
7370
|
+
function parsePipfileLock(content) {
|
|
7371
|
+
const lock = JSON.parse(content);
|
|
7372
|
+
const deps = [];
|
|
7373
|
+
for (const section of ["default", "develop"]) {
|
|
7374
|
+
const packages = lock[section];
|
|
7375
|
+
if (!packages || typeof packages !== "object") continue;
|
|
7376
|
+
for (const [name, value] of Object.entries(packages)) {
|
|
7377
|
+
const version = exactVersion(String(value?.version ?? "").replace(/^==/, ""));
|
|
7378
|
+
if (name && version) deps.push({ name, version });
|
|
7379
|
+
}
|
|
7380
|
+
}
|
|
7381
|
+
return deps;
|
|
7382
|
+
}
|
|
7383
|
+
function parseRequirementsTxt(content) {
|
|
7384
|
+
const deps = [];
|
|
7385
|
+
for (const raw of content.split("\n")) {
|
|
7386
|
+
const line = raw.split("#")[0]?.split(";")[0]?.trim();
|
|
7387
|
+
if (!line || line.startsWith("-")) continue;
|
|
7388
|
+
const match = /^([a-zA-Z0-9._-]+)\s*(?:\[[^\]]*\])?\s*==\s*([^\s,]+)/.exec(line);
|
|
7389
|
+
if (!match?.[1] || !match[2]) continue;
|
|
7390
|
+
const version = exactVersion(match[2]);
|
|
7391
|
+
if (version) deps.push({ name: match[1], version });
|
|
7392
|
+
}
|
|
7393
|
+
return deps;
|
|
7394
|
+
}
|
|
7395
|
+
var LOCKFILES = [
|
|
7396
|
+
{ file: "package-lock.json", ecosystem: "npm", parse: parsePackageLock },
|
|
7397
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm", parse: parsePnpmLock },
|
|
7398
|
+
{ file: "yarn.lock", ecosystem: "npm", parse: parseYarnLock },
|
|
7399
|
+
{ file: "requirements.txt", ecosystem: "PyPI", parse: parseRequirementsTxt },
|
|
7400
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI", parse: parsePipfileLock }
|
|
7401
|
+
];
|
|
7402
|
+
var LOCKFILE_PARSERS = Object.fromEntries(
|
|
7403
|
+
LOCKFILES.map((entry) => [entry.file, entry.parse])
|
|
7404
|
+
);
|
|
6982
7405
|
function isValidPackageName(name) {
|
|
6983
7406
|
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
6984
7407
|
}
|
|
@@ -7003,17 +7426,17 @@ async function queryOsv(name, version, ecosystem) {
|
|
|
7003
7426
|
}
|
|
7004
7427
|
|
|
7005
7428
|
// ../../packages/scan/src/node/sarif.ts
|
|
7006
|
-
var
|
|
7007
|
-
var
|
|
7429
|
+
var import_node_crypto3 = require("crypto");
|
|
7430
|
+
var import_node_path9 = require("path");
|
|
7008
7431
|
|
|
7009
7432
|
// src/commands/scan.ts
|
|
7010
7433
|
function readVersion() {
|
|
7011
7434
|
for (const candidate of [
|
|
7012
|
-
(0,
|
|
7013
|
-
(0,
|
|
7435
|
+
(0, import_node_path10.join)(__dirname, "..", "package.json"),
|
|
7436
|
+
(0, import_node_path10.join)(__dirname, "..", "..", "package.json")
|
|
7014
7437
|
]) {
|
|
7015
7438
|
try {
|
|
7016
|
-
return JSON.parse((0,
|
|
7439
|
+
return JSON.parse((0, import_node_fs14.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
7017
7440
|
} catch {
|
|
7018
7441
|
}
|
|
7019
7442
|
}
|
|
@@ -7474,8 +7897,8 @@ var RuleEngine = class {
|
|
|
7474
7897
|
};
|
|
7475
7898
|
|
|
7476
7899
|
// src/daemon/rules/loader.ts
|
|
7477
|
-
var
|
|
7478
|
-
var
|
|
7900
|
+
var import_node_fs15 = require("fs");
|
|
7901
|
+
var import_node_path11 = require("path");
|
|
7479
7902
|
|
|
7480
7903
|
// src/daemon/rules/default-rules.ts
|
|
7481
7904
|
var DEFAULT_RULES = [
|
|
@@ -7759,11 +8182,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
7759
8182
|
function loadAllRules(customDir) {
|
|
7760
8183
|
const rules = [...DEFAULT_RULES];
|
|
7761
8184
|
const dir = customDir || RULES_DIR;
|
|
7762
|
-
if ((0,
|
|
7763
|
-
const files = (0,
|
|
8185
|
+
if ((0, import_node_fs15.existsSync)(dir)) {
|
|
8186
|
+
const files = (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
7764
8187
|
for (const file of files) {
|
|
7765
8188
|
try {
|
|
7766
|
-
const raw = (0,
|
|
8189
|
+
const raw = (0, import_node_fs15.readFileSync)((0, import_node_path11.join)(dir, file), "utf-8");
|
|
7767
8190
|
const parsed = JSON.parse(raw);
|
|
7768
8191
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
7769
8192
|
for (const rule of customRules) {
|
|
@@ -7926,7 +8349,7 @@ function detectFirewallAdapter() {
|
|
|
7926
8349
|
}
|
|
7927
8350
|
|
|
7928
8351
|
// src/daemon/firewall/remediation.ts
|
|
7929
|
-
var
|
|
8352
|
+
var import_node_fs16 = require("fs");
|
|
7930
8353
|
var DEFAULT_CONFIG2 = {
|
|
7931
8354
|
enabled: true,
|
|
7932
8355
|
dry_run: true,
|
|
@@ -8081,7 +8504,7 @@ var RemediationManager = class {
|
|
|
8081
8504
|
}
|
|
8082
8505
|
logLine(line) {
|
|
8083
8506
|
try {
|
|
8084
|
-
(0,
|
|
8507
|
+
(0, import_node_fs16.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
8085
8508
|
`);
|
|
8086
8509
|
} catch {
|
|
8087
8510
|
}
|
|
@@ -8137,7 +8560,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
8137
8560
|
// src/daemon/index.ts
|
|
8138
8561
|
function readVersion2() {
|
|
8139
8562
|
try {
|
|
8140
|
-
const pkg = JSON.parse((0,
|
|
8563
|
+
const pkg = JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path12.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
8141
8564
|
return pkg.version || "0.0.0";
|
|
8142
8565
|
} catch {
|
|
8143
8566
|
return "0.0.0";
|
|
@@ -8145,7 +8568,7 @@ function readVersion2() {
|
|
|
8145
8568
|
}
|
|
8146
8569
|
function logLine(line) {
|
|
8147
8570
|
try {
|
|
8148
|
-
(0,
|
|
8571
|
+
(0, import_node_fs17.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
8149
8572
|
`);
|
|
8150
8573
|
} catch {
|
|
8151
8574
|
}
|
|
@@ -8173,7 +8596,7 @@ async function runDaemon() {
|
|
|
8173
8596
|
} catch (err) {
|
|
8174
8597
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
8175
8598
|
}
|
|
8176
|
-
const config = loadConfig((0,
|
|
8599
|
+
const config = loadConfig((0, import_node_fs17.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
8177
8600
|
bus.on("event", (event) => {
|
|
8178
8601
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
8179
8602
|
});
|