@profullstack/threatcrush 0.11.1 → 0.11.3
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 +727 -137
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1010 -272
- 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
|
{
|
|
@@ -4621,7 +4789,11 @@ var NODE_RULES = [
|
|
|
4621
4789
|
severity: "medium",
|
|
4622
4790
|
languages: ["javascript", "typescript"],
|
|
4623
4791
|
pattern: /\bBuffer\s*\.\s*allocUnsafe(?:Slow)?\s*\(|\bnew\s+Buffer\s*\(\s*(?![`'"])[a-zA-Z_$0-9]/,
|
|
4624
|
-
inherent: true
|
|
4792
|
+
inherent: true,
|
|
4793
|
+
// Filling the buffer yourself is the whole reason to call `allocUnsafe`,
|
|
4794
|
+
// so reporting every call reports correct code. What is left reported is
|
|
4795
|
+
// an allocation whose bytes are never written before it escapes.
|
|
4796
|
+
filledBeforeUseGuard: true
|
|
4625
4797
|
},
|
|
4626
4798
|
{
|
|
4627
4799
|
id: "js-oversized-request-body-limit",
|
|
@@ -4731,8 +4903,19 @@ var CODE_RULES = [
|
|
|
4731
4903
|
// The tail is what distinguishes assembly from parameterisation. A bound
|
|
4732
4904
|
// query leaves a comma after the closing quote (`"… = $1", [id]`) and
|
|
4733
4905
|
// matches none of these.
|
|
4906
|
+
//
|
|
4907
|
+
// The clause alternative anchors the keyword to the *start* of the
|
|
4908
|
+
// concatenated fragment, because that is where a clause being appended
|
|
4909
|
+
// actually sits: `sql + "WHERE id = " + id`, `sql + " ORDER BY " + col`.
|
|
4910
|
+
//
|
|
4911
|
+
// Allowing it anywhere in the fragment made the rule read English. `WHERE`
|
|
4912
|
+
// and `SET` are ordinary words, and without a leading `\b` they were not
|
|
4913
|
+
// even required to be whole ones — "any`where`" and "sub`set`" both
|
|
4914
|
+
// matched. A help string reading "lists them anywhere" was reported as
|
|
4915
|
+
// critical SQL injection, which is the kind of finding that teaches a team
|
|
4916
|
+
// the scanner is not worth reading.
|
|
4734
4917
|
pattern: new RegExp(
|
|
4735
|
-
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"
|
|
4918
|
+
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"|')\\s*\\b(?:WHERE|ORDER\\s+BY|VALUES|SET)\\b`,
|
|
4736
4919
|
"i"
|
|
4737
4920
|
)
|
|
4738
4921
|
},
|
|
@@ -4745,7 +4928,26 @@ var CODE_RULES = [
|
|
|
4745
4928
|
pattern: new RegExp(
|
|
4746
4929
|
`\`[^\`\\n]*(?:${SQL_KEYWORDS})\\b[^\`\\n]*\\$\\{|\\bf"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*\\{|\\bf'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*\\{`,
|
|
4747
4930
|
"i"
|
|
4748
|
-
)
|
|
4931
|
+
),
|
|
4932
|
+
/**
|
|
4933
|
+
* Interpolating a column list is not interpolating a value.
|
|
4934
|
+
*
|
|
4935
|
+
* const COLS = `tld, user_id, owner_email, price_usd`;
|
|
4936
|
+
* get(`SELECT ${COLS} FROM moshpit_tlds WHERE tld = ?`, [tld]);
|
|
4937
|
+
*
|
|
4938
|
+
* That query *is* parameterised: every value the caller supplies rides a
|
|
4939
|
+
* `?`, and the only thing spliced into the text is a constant written a
|
|
4940
|
+
* few lines up. Naming the column list once instead of repeating it in
|
|
4941
|
+
* fourteen queries is ordinary hygiene, and it is the shape this rule met
|
|
4942
|
+
* most often in practice — one repository produced eighteen findings this
|
|
4943
|
+
* way and not one of them could be injected into.
|
|
4944
|
+
*
|
|
4945
|
+
* The guard resolves the interpolations rather than trusting the shape, so
|
|
4946
|
+
* the moment a query mixes a constant with anything else —
|
|
4947
|
+
* `` `SELECT ${COLS} FROM t WHERE id = ${req.query.id}` `` — it is reported
|
|
4948
|
+
* again. `interpolationsAreConstant` requires *every* `${…}` to resolve.
|
|
4949
|
+
*/
|
|
4950
|
+
constantInterpolationGuard: true
|
|
4749
4951
|
},
|
|
4750
4952
|
{
|
|
4751
4953
|
id: "sql-format-call",
|
|
@@ -4776,7 +4978,8 @@ var CODE_RULES = [
|
|
|
4776
4978
|
cwe: "CWE-78",
|
|
4777
4979
|
severity: "critical",
|
|
4778
4980
|
languages: ["javascript", "typescript"],
|
|
4779
|
-
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)
|
|
4981
|
+
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/,
|
|
4982
|
+
constantInterpolationGuard: true
|
|
4780
4983
|
},
|
|
4781
4984
|
{
|
|
4782
4985
|
id: "py-shell-command-string",
|
|
@@ -4794,7 +4997,22 @@ var CODE_RULES = [
|
|
|
4794
4997
|
cwe: "CWE-78",
|
|
4795
4998
|
severity: "critical",
|
|
4796
4999
|
languages: ["go"],
|
|
4797
|
-
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"
|
|
5000
|
+
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"/,
|
|
5001
|
+
// A call whose whole argv is string literals cannot be injected into.
|
|
5002
|
+
// `exec.Command("cmd", "/c", "ver")` reads the Windows version; there is no
|
|
5003
|
+
// value in it for an attacker to reach, and the consequence above — that a
|
|
5004
|
+
// shell will interpret metacharacters — describes metacharacters nobody can
|
|
5005
|
+
// supply. gosec's G204 draws the line in the same place, and this was the
|
|
5006
|
+
// single finding a Go project got out of a whole scan before declining the
|
|
5007
|
+
// offer, which is an expensive way to report nothing.
|
|
5008
|
+
//
|
|
5009
|
+
// The guard has to end at the closing paren, so a literal followed by
|
|
5010
|
+
// anything else still reports: `"ls " + dir` leaves a `+` before the `)`,
|
|
5011
|
+
// `fmt.Sprintf(…)` leaves an identifier, and a bare variable leaves a name.
|
|
5012
|
+
// `(?:[^"\\]|\\.)*` rather than `[^"]*` so an escaped quote inside a
|
|
5013
|
+
// literal — `"echo \"hi\""` — does not end the literal early and drop the
|
|
5014
|
+
// guard on a line it should have covered.
|
|
5015
|
+
lineGuard: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"\s*(?:,\s*"(?:[^"\\]|\\.)*")*\s*,?\s*\)/
|
|
4798
5016
|
},
|
|
4799
5017
|
{
|
|
4800
5018
|
id: "rb-backtick-interpolation",
|
|
@@ -4863,17 +5081,43 @@ var CODE_RULES = [
|
|
|
4863
5081
|
cwe: "CWE-79",
|
|
4864
5082
|
severity: "high",
|
|
4865
5083
|
languages: ["javascript", "typescript"],
|
|
4866
|
-
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
|
|
4867
5084
|
/**
|
|
5085
|
+
* The assignment alternative carries its own exemption, as a lookahead, so
|
|
5086
|
+
* that it is decided per assignment rather than per line.
|
|
5087
|
+
*
|
|
4868
5088
|
* A whole-statement assignment of a string with no interpolation and no
|
|
4869
|
-
* concatenation carries no data, so it cannot carry attacker data. This
|
|
4870
|
-
*
|
|
4871
|
-
*
|
|
4872
|
-
*
|
|
5089
|
+
* concatenation carries no data, so it cannot carry attacker data. This was
|
|
5090
|
+
* the single largest source of noise: a codebase that builds its UI with
|
|
5091
|
+
* innerHTML reports every static heading and spinner as XSS, and a rule
|
|
5092
|
+
* that flags 40 safe lines to catch one real one gets switched off.
|
|
5093
|
+
*
|
|
5094
|
+
* Two things this has to get right, and a `lineGuard` could get neither:
|
|
5095
|
+
*
|
|
5096
|
+
* A statement ends at its semicolon, not at the newline. Anchoring to `$`
|
|
5097
|
+
* held the exemption for `el.innerHTML = '';` alone on a line and dropped
|
|
5098
|
+
* it the moment anything followed:
|
|
4873
5099
|
*
|
|
4874
|
-
*
|
|
5100
|
+
* function hide() { out.hidden = true; out.innerHTML = ''; rendered = null; }
|
|
5101
|
+
*
|
|
5102
|
+
* — the same assignment, clearing a node, reported as high-severity XSS
|
|
5103
|
+
* because two neighbours shared its line.
|
|
5104
|
+
*
|
|
5105
|
+
* And an exemption must not become a line-wide amnesty. A guard is tested
|
|
5106
|
+
* against the whole line, so one safe clear would exonerate a real sink
|
|
5107
|
+
* beside it:
|
|
5108
|
+
*
|
|
5109
|
+
* a.innerHTML = ''; b.innerHTML = userInput;
|
|
5110
|
+
*
|
|
5111
|
+
* As a lookahead the regex decides at each `=` it reaches, so the first
|
|
5112
|
+
* assignment is exempt and the second is still reported.
|
|
5113
|
+
*
|
|
5114
|
+
* The whitespace after `=` is matched *inside* the lookahead rather than
|
|
5115
|
+
* before it. Left outside, `\s*` backtracks to zero width, the lookahead
|
|
5116
|
+
* then starts on the space instead of the quote, fails to see a literal,
|
|
5117
|
+
* and the negative lookahead succeeds — reinstating every finding the
|
|
5118
|
+
* exemption was written to remove.
|
|
4875
5119
|
*/
|
|
4876
|
-
|
|
5120
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=(?!\s*(?:'[^'\\\n]*'|"[^"\\\n]*"|`[^`$\\\n]*`)\s*(?:;|$))|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/
|
|
4877
5121
|
},
|
|
4878
5122
|
{
|
|
4879
5123
|
id: "java-html-writer-concatenation",
|
|
@@ -5675,7 +5919,26 @@ var CODE_RULES = [
|
|
|
5675
5919
|
cwe: "CWE-346",
|
|
5676
5920
|
severity: "high",
|
|
5677
5921
|
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
|
|
5922
|
+
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/,
|
|
5923
|
+
/**
|
|
5924
|
+
* Parsing the request's own URL is not building a link from the Host
|
|
5925
|
+
* header, even though it is spelled with one.
|
|
5926
|
+
*
|
|
5927
|
+
* const url = new URL(request.url, `http://${request.headers.host}`);
|
|
5928
|
+
* const token = url.searchParams.get('token');
|
|
5929
|
+
*
|
|
5930
|
+
* `request.url` on a Node server is a path — `/ws?token=…` — and `new URL`
|
|
5931
|
+
* refuses a relative input without a base. The base exists to satisfy the
|
|
5932
|
+
* parser and is thrown away; only the path and query are ever read. Every
|
|
5933
|
+
* Node HTTP handler that wants a query parameter is written this way, so
|
|
5934
|
+
* the rule fired on the framework idiom rather than on the defect.
|
|
5935
|
+
*
|
|
5936
|
+
* Narrow on purpose: the first argument must be `req.url` itself. The
|
|
5937
|
+
* dangerous shape passes a *path* the application chose —
|
|
5938
|
+
* `new URL('/reset?t=…', `https://${req.headers.host}`)` — and that is what
|
|
5939
|
+
* produces an attacker-controlled link. It does not match this guard.
|
|
5940
|
+
*/
|
|
5941
|
+
lineGuard: /\bnew\s+URL\s*\(\s*(?:req|request|ctx)(?:uest)?\s*\.\s*url\b\s*,/
|
|
5679
5942
|
},
|
|
5680
5943
|
// A recursive-merge prototype-pollution rule (`target[key] = source[key]`
|
|
5681
5944
|
// with no `__proto__` guard) was built and dropped. The bare copy-by-key is
|
|
@@ -5743,6 +6006,94 @@ function fileTextOf(lines) {
|
|
|
5743
6006
|
function withoutSingleQuoted(text) {
|
|
5744
6007
|
return text.replace(/'[^'\n]*'/g, "''");
|
|
5745
6008
|
}
|
|
6009
|
+
function calleeEndingAt(text, open) {
|
|
6010
|
+
let end = open;
|
|
6011
|
+
while (end > 0 && (text[end - 1] === " " || text[end - 1] === " ")) end -= 1;
|
|
6012
|
+
let start = end;
|
|
6013
|
+
while (start > 0 && /[\w$.]/.test(text[start - 1])) start -= 1;
|
|
6014
|
+
return text.slice(start, end);
|
|
6015
|
+
}
|
|
6016
|
+
function enclosingCallees(lines, index, back) {
|
|
6017
|
+
const before = lines.slice(Math.max(0, index - back), index).join("\n");
|
|
6018
|
+
const stack = [];
|
|
6019
|
+
let quote = null;
|
|
6020
|
+
for (let i = 0; i < before.length; i += 1) {
|
|
6021
|
+
const ch = before[i];
|
|
6022
|
+
if (quote) {
|
|
6023
|
+
if (ch === "\\") i += 1;
|
|
6024
|
+
else if (ch === quote) quote = null;
|
|
6025
|
+
continue;
|
|
6026
|
+
}
|
|
6027
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
6028
|
+
else if (ch === "(") stack.push(calleeEndingAt(before, i));
|
|
6029
|
+
else if (ch === ")") stack.pop();
|
|
6030
|
+
}
|
|
6031
|
+
return stack;
|
|
6032
|
+
}
|
|
6033
|
+
function interpolations(line) {
|
|
6034
|
+
const found = [];
|
|
6035
|
+
for (let i = 0; ; ) {
|
|
6036
|
+
const start = line.indexOf("${", i);
|
|
6037
|
+
if (start === -1) break;
|
|
6038
|
+
const end = line.indexOf("}", start + 2);
|
|
6039
|
+
if (end === -1) return null;
|
|
6040
|
+
found.push(line.slice(start + 2, end).trim());
|
|
6041
|
+
i = end + 1;
|
|
6042
|
+
}
|
|
6043
|
+
return found.length > 0 ? found : null;
|
|
6044
|
+
}
|
|
6045
|
+
function isConstantString(name, fileText) {
|
|
6046
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
6047
|
+
return new RegExp(
|
|
6048
|
+
`\\bconst\\s+${escaped}\\s*(?::[^=\\n]+)?=\\s*(?:'[^'\\n]*'|"[^"\\n]*"|\`[^\`$\\n]*\`)`
|
|
6049
|
+
).test(fileText);
|
|
6050
|
+
}
|
|
6051
|
+
function interpolationsAreConstant(line, fileText) {
|
|
6052
|
+
const found = interpolations(line);
|
|
6053
|
+
if (!found) return false;
|
|
6054
|
+
return found.every(
|
|
6055
|
+
(expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
|
|
6056
|
+
);
|
|
6057
|
+
}
|
|
6058
|
+
var FILL_LOOKAHEAD = 16;
|
|
6059
|
+
var NAME = String.raw`(?<![\w$])([A-Za-z_$][\w$]*)`;
|
|
6060
|
+
var ALLOCATION_BINDING = new RegExp(
|
|
6061
|
+
`${NAME}\\s*=\\s*(?:new\\s+Buffer\\s*\\(|Buffer\\s*\\.\\s*allocUnsafe(?:Slow)?\\s*\\()`
|
|
6062
|
+
);
|
|
6063
|
+
function allocationBinding(line) {
|
|
6064
|
+
return ALLOCATION_BINDING.exec(line)?.[1] ?? null;
|
|
6065
|
+
}
|
|
6066
|
+
var WRITE_SHAPES = [
|
|
6067
|
+
// `src.copy(name, …)` — name is the destination.
|
|
6068
|
+
/\.\s*copy\s*\(\s*([A-Za-z_$][\w$]*)\s*[,)]/g,
|
|
6069
|
+
// `name.fill(…)`, `name.write*(…)`, `name.set(…)`.
|
|
6070
|
+
new RegExp(`${NAME}\\s*\\.\\s*(?:fill|set|write[A-Za-z0-9]*)\\s*\\(`, "g"),
|
|
6071
|
+
// `name[i] = …`, but not `name[i] === …`.
|
|
6072
|
+
new RegExp(`${NAME}\\s*\\[[^\\]\\n]*\\]\\s*=(?!=)`, "g")
|
|
6073
|
+
];
|
|
6074
|
+
function writesInto(text, name) {
|
|
6075
|
+
for (const shape of WRITE_SHAPES) {
|
|
6076
|
+
shape.lastIndex = 0;
|
|
6077
|
+
for (let found = shape.exec(text); found !== null; found = shape.exec(text)) {
|
|
6078
|
+
if (found[1] === name) return true;
|
|
6079
|
+
}
|
|
6080
|
+
}
|
|
6081
|
+
return false;
|
|
6082
|
+
}
|
|
6083
|
+
function bufferFilledBeforeUse(ctx) {
|
|
6084
|
+
const line = ctx.lines[ctx.index] ?? "";
|
|
6085
|
+
if (/\ballocUnsafe(?:Slow)?\s*\([^)\n]*\)\s*\.\s*fill\s*\(/.test(line)) return true;
|
|
6086
|
+
const name = allocationBinding(line);
|
|
6087
|
+
if (!name) return false;
|
|
6088
|
+
if (writesInto(line.slice(line.indexOf("=") + 1), name)) return true;
|
|
6089
|
+
const last = Math.min(ctx.lines.length - 1, ctx.index + FILL_LOOKAHEAD);
|
|
6090
|
+
for (let i = ctx.index + 1; i <= last; i += 1) {
|
|
6091
|
+
const next = ctx.lines[i] ?? "";
|
|
6092
|
+
if (skippable(next, i, ctx.prose)) continue;
|
|
6093
|
+
if (writesInto(next, name)) return true;
|
|
6094
|
+
}
|
|
6095
|
+
return false;
|
|
6096
|
+
}
|
|
5746
6097
|
function evaluateRule(rule, ctx) {
|
|
5747
6098
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
5748
6099
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -5754,6 +6105,14 @@ function evaluateRule(rule, ctx) {
|
|
|
5754
6105
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
5755
6106
|
if (rule.requires && !rule.requires.test(context)) return null;
|
|
5756
6107
|
if (rule.lineGuard?.test(line)) return null;
|
|
6108
|
+
if (rule.enclosingCallGuard) {
|
|
6109
|
+
const callees = enclosingCallees(ctx.lines, ctx.index, back);
|
|
6110
|
+
if (callees.some((callee) => rule.enclosingCallGuard.test(callee))) return null;
|
|
6111
|
+
}
|
|
6112
|
+
if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
|
|
6113
|
+
return null;
|
|
6114
|
+
}
|
|
6115
|
+
if (rule.filledBeforeUseGuard && bufferFilledBeforeUse(ctx)) return null;
|
|
5757
6116
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
5758
6117
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
5759
6118
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -6336,7 +6695,8 @@ var SECRET_RULES = [
|
|
|
6336
6695
|
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
6337
6696
|
severity: "medium",
|
|
6338
6697
|
cwe: "CWE-798",
|
|
6339
|
-
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
|
|
6698
|
+
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived.",
|
|
6699
|
+
keywordShaped: true
|
|
6340
6700
|
},
|
|
6341
6701
|
{
|
|
6342
6702
|
id: "secret-generic-api-key",
|
|
@@ -6347,7 +6707,8 @@ var SECRET_RULES = [
|
|
|
6347
6707
|
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
6348
6708
|
severity: "high",
|
|
6349
6709
|
cwe: "CWE-798",
|
|
6350
|
-
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
|
|
6710
|
+
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid.",
|
|
6711
|
+
keywordShaped: true
|
|
6351
6712
|
},
|
|
6352
6713
|
{
|
|
6353
6714
|
id: "secret-generic-credential",
|
|
@@ -6355,7 +6716,8 @@ var SECRET_RULES = [
|
|
|
6355
6716
|
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
6356
6717
|
severity: "high",
|
|
6357
6718
|
cwe: "CWE-798",
|
|
6358
|
-
consequence: "A password in source is a password in every clone, fork and CI cache of that source."
|
|
6719
|
+
consequence: "A password in source is a password in every clone, fork and CI cache of that source.",
|
|
6720
|
+
keywordShaped: true
|
|
6359
6721
|
},
|
|
6360
6722
|
{
|
|
6361
6723
|
id: "secret-hex-token",
|
|
@@ -6363,7 +6725,8 @@ var SECRET_RULES = [
|
|
|
6363
6725
|
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
6364
6726
|
severity: "medium",
|
|
6365
6727
|
cwe: "CWE-798",
|
|
6366
|
-
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
|
|
6728
|
+
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign.",
|
|
6729
|
+
keywordShaped: true
|
|
6367
6730
|
}
|
|
6368
6731
|
];
|
|
6369
6732
|
var KNOWN_PLACEHOLDERS = [
|
|
@@ -6394,6 +6757,62 @@ var KNOWN_PLACEHOLDERS = [
|
|
|
6394
6757
|
function isKnownPlaceholder(text) {
|
|
6395
6758
|
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
6396
6759
|
}
|
|
6760
|
+
function isPlaceholderAttribute(line, value) {
|
|
6761
|
+
const at = line.lastIndexOf(value);
|
|
6762
|
+
if (at === -1) return false;
|
|
6763
|
+
return /(?:^|\s)(?:aria-)?placeholder\s*=\s*[{("'`]*$/i.test(line.slice(0, at));
|
|
6764
|
+
}
|
|
6765
|
+
function isVariableReference(value) {
|
|
6766
|
+
const trimmed = value.trim();
|
|
6767
|
+
const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
|
|
6768
|
+
if (braced) {
|
|
6769
|
+
const fallback = braced[2];
|
|
6770
|
+
if (fallback === void 0 || fallback.trim() === "") return true;
|
|
6771
|
+
return /^\$\{?[A-Za-z_][\w.]*\}?$/.test(fallback.trim());
|
|
6772
|
+
}
|
|
6773
|
+
return /^\$[A-Za-z_]\w*$/.test(trimmed) || // $VAR
|
|
6774
|
+
/^\$\([\s\S]*\)$/.test(trimmed) || // $(command substitution)
|
|
6775
|
+
/^%[A-Za-z_]\w*%$/.test(trimmed) || // %VAR% on Windows
|
|
6776
|
+
/^\{\{[\s\S]*\}\}$/.test(trimmed) || // {{ template }}
|
|
6777
|
+
/^#\{[\s\S]*\}$/.test(trimmed) || // #{ruby}
|
|
6778
|
+
/^<%=?[\s\S]*%>$/.test(trimmed);
|
|
6779
|
+
}
|
|
6780
|
+
var FIXTURE_STEMS = [
|
|
6781
|
+
"test",
|
|
6782
|
+
"mock",
|
|
6783
|
+
"fake",
|
|
6784
|
+
"dummy",
|
|
6785
|
+
"stub",
|
|
6786
|
+
"sample",
|
|
6787
|
+
"example",
|
|
6788
|
+
"placeholder",
|
|
6789
|
+
"fixture",
|
|
6790
|
+
"invalid",
|
|
6791
|
+
"expired",
|
|
6792
|
+
"forged",
|
|
6793
|
+
"bogus",
|
|
6794
|
+
"notreal",
|
|
6795
|
+
"nonexistent",
|
|
6796
|
+
"changeme",
|
|
6797
|
+
"foobar",
|
|
6798
|
+
"lorem"
|
|
6799
|
+
];
|
|
6800
|
+
var KEY_NOISE = /* @__PURE__ */ new Set(["const", "this", "return", "await", "async", "expect", "value"]);
|
|
6801
|
+
function words(text) {
|
|
6802
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z]+/g, " ").toLowerCase().split(" ").filter(Boolean);
|
|
6803
|
+
}
|
|
6804
|
+
function isTestFixtureValue(line, value) {
|
|
6805
|
+
const valueWords = words(value);
|
|
6806
|
+
if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
|
|
6807
|
+
return describesItsOwnKey(line, value);
|
|
6808
|
+
}
|
|
6809
|
+
function describesItsOwnKey(line, value) {
|
|
6810
|
+
if (value.length > 48) return false;
|
|
6811
|
+
const valueAt = line.lastIndexOf(value);
|
|
6812
|
+
const key = valueAt === -1 ? line : line.slice(0, valueAt);
|
|
6813
|
+
const flattened = words(value).join("");
|
|
6814
|
+
return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
|
|
6815
|
+
}
|
|
6397
6816
|
function redactSecret(line) {
|
|
6398
6817
|
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
6399
6818
|
if (match.length <= 12) return match;
|
|
@@ -6571,9 +6990,9 @@ function languageOfShebang(firstLine) {
|
|
|
6571
6990
|
}
|
|
6572
6991
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
6573
6992
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
6574
|
-
var
|
|
6575
|
-
function
|
|
6576
|
-
return
|
|
6993
|
+
var FOREIGN_SECURITY = /\b(?:nolint:[\w,]*gosec|nosec)\b/;
|
|
6994
|
+
function foreignSecurityMark(line) {
|
|
6995
|
+
return FOREIGN_SECURITY.test(line);
|
|
6577
6996
|
}
|
|
6578
6997
|
function collectSuppressions(lines) {
|
|
6579
6998
|
const byLine = /* @__PURE__ */ new Map();
|
|
@@ -6599,31 +7018,68 @@ function isSuppressed(suppressions, index, ruleId) {
|
|
|
6599
7018
|
}
|
|
6600
7019
|
function isTestPath(relativePath) {
|
|
6601
7020
|
const p = relativePath.replace(/\\/g, "/");
|
|
6602
|
-
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
7021
|
+
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata|testutils?|harness)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
7022
|
+
}
|
|
7023
|
+
function isDocPath(relativePath) {
|
|
7024
|
+
const p = relativePath.replace(/\\/g, "/");
|
|
7025
|
+
return /(?:^|\/)(?:docs?|examples?|samples?)\//i.test(p) || /\.(?:md|mdx|markdown|rst|adoc)$/i.test(p);
|
|
7026
|
+
}
|
|
7027
|
+
var SOFTENING_ORDER = [
|
|
7028
|
+
"suppressed",
|
|
7029
|
+
"placeholder",
|
|
7030
|
+
"self-describing",
|
|
7031
|
+
"test",
|
|
7032
|
+
"docs"
|
|
7033
|
+
];
|
|
7034
|
+
function softening(reasons) {
|
|
7035
|
+
return SOFTENING_ORDER.find((reason) => reasons[reason]) ?? null;
|
|
6603
7036
|
}
|
|
7037
|
+
var SECRET_SOFTENING = {
|
|
7038
|
+
test: "in a test file \u2014 usually a fixture, still worth confirming it is not a live credential",
|
|
7039
|
+
docs: "in documentation \u2014 usually an illustrative example, still worth confirming it is not a live credential",
|
|
7040
|
+
suppressed: "on a line already marked as a false positive for another linter's security rule",
|
|
7041
|
+
placeholder: "in example text an empty input field shows, not in data",
|
|
7042
|
+
"self-describing": "in a value that repeats the name of the field holding it \u2014 usually a description of a credential rather than one"
|
|
7043
|
+
};
|
|
7044
|
+
var CODE_SOFTENING = {
|
|
7045
|
+
test: "in a test file, where the construct is ordinary",
|
|
7046
|
+
docs: "in documentation or example code, which nothing runs",
|
|
7047
|
+
suppressed: "on a line another linter's security suppression already covers",
|
|
7048
|
+
placeholder: "in example text rather than in data",
|
|
7049
|
+
"self-describing": "in a value that describes itself"
|
|
7050
|
+
};
|
|
6604
7051
|
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
6605
7052
|
const findings = [];
|
|
6606
7053
|
const lines = text.split("\n");
|
|
6607
7054
|
const suppressions = collectSuppressions(lines);
|
|
6608
7055
|
const inTests = isTestPath(relativePath);
|
|
7056
|
+
const inDocs = isDocPath(relativePath);
|
|
6609
7057
|
lines.forEach((line, index) => {
|
|
6610
7058
|
for (const rule of SECRET_RULES) {
|
|
6611
7059
|
const match = rule.pattern.exec(line);
|
|
6612
7060
|
if (!match) continue;
|
|
6613
7061
|
if (isKnownPlaceholder(match[0])) continue;
|
|
6614
7062
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
6615
|
-
const
|
|
7063
|
+
const value = match[1] ?? match[0];
|
|
7064
|
+
if (isVariableReference(value)) continue;
|
|
7065
|
+
if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
|
|
7066
|
+
const soft = softening({
|
|
7067
|
+
test: inTests,
|
|
7068
|
+
suppressed: foreignSecurityMark(line),
|
|
7069
|
+
placeholder: isPlaceholderAttribute(line, value),
|
|
7070
|
+
"self-describing": rule.keywordShaped === true && describesItsOwnKey(line, value)
|
|
7071
|
+
});
|
|
6616
7072
|
findings.push({
|
|
6617
7073
|
ruleId: rule.id,
|
|
6618
7074
|
title: rule.name,
|
|
6619
7075
|
file: relativePath,
|
|
6620
7076
|
line: index + 1,
|
|
6621
|
-
// Reported but not blocking
|
|
6622
|
-
//
|
|
6623
|
-
severity:
|
|
7077
|
+
// Reported but not blocking wherever context weakens the claim — see
|
|
7078
|
+
// `softening`. Never dropped: the count is the same either way.
|
|
7079
|
+
severity: soft ? "low" : rule.severity,
|
|
6624
7080
|
// A matched credential format is the finding, not a proxy for one.
|
|
6625
7081
|
confidence: "evidence",
|
|
6626
|
-
message:
|
|
7082
|
+
message: soft ? `Possible ${rule.name} detected ${SECRET_SOFTENING[soft]}` : `Possible ${rule.name} detected`,
|
|
6627
7083
|
consequence: rule.consequence,
|
|
6628
7084
|
cwe: rule.cwe,
|
|
6629
7085
|
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
@@ -6638,14 +7094,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6638
7094
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
6639
7095
|
const match = evaluateRule(rule, { lines, index, language, prose });
|
|
6640
7096
|
if (!match) continue;
|
|
7097
|
+
const soft = softening({
|
|
7098
|
+
test: inTests,
|
|
7099
|
+
docs: inDocs,
|
|
7100
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
7101
|
+
});
|
|
6641
7102
|
findings.push({
|
|
6642
7103
|
ruleId: rule.id,
|
|
6643
7104
|
title: rule.title,
|
|
6644
7105
|
file: relativePath,
|
|
6645
7106
|
line: index + 1,
|
|
6646
|
-
severity: match.severity,
|
|
7107
|
+
severity: soft ? "low" : match.severity,
|
|
6647
7108
|
confidence: match.confidence,
|
|
6648
|
-
message: `${rule.title} (${rule.cwe})`,
|
|
7109
|
+
message: soft ? `${rule.title} (${rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${rule.title} (${rule.cwe})`,
|
|
6649
7110
|
consequence: rule.consequence,
|
|
6650
7111
|
cwe: rule.cwe,
|
|
6651
7112
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
@@ -6656,14 +7117,19 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6656
7117
|
for (const match of evaluateTemplateRules(extensionOf(relativePath), lines)) {
|
|
6657
7118
|
const index = match.line - 1;
|
|
6658
7119
|
if (isSuppressed(suppressions, index, match.rule.id)) continue;
|
|
7120
|
+
const soft = softening({
|
|
7121
|
+
test: inTests,
|
|
7122
|
+
docs: inDocs,
|
|
7123
|
+
suppressed: foreignSecurityMark(lines[index] ?? "")
|
|
7124
|
+
});
|
|
6659
7125
|
findings.push({
|
|
6660
7126
|
ruleId: match.rule.id,
|
|
6661
7127
|
title: match.rule.title,
|
|
6662
7128
|
file: relativePath,
|
|
6663
7129
|
line: match.line,
|
|
6664
|
-
severity: match.severity,
|
|
7130
|
+
severity: soft ? "low" : match.severity,
|
|
6665
7131
|
confidence: "pattern",
|
|
6666
|
-
message: `${match.rule.title} (${match.rule.cwe})`,
|
|
7132
|
+
message: soft ? `${match.rule.title} (${match.rule.cwe}) \u2014 ${CODE_SOFTENING[soft]}` : `${match.rule.title} (${match.rule.cwe})`,
|
|
6667
7133
|
consequence: match.rule.consequence,
|
|
6668
7134
|
cwe: match.rule.cwe,
|
|
6669
7135
|
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
@@ -6690,8 +7156,8 @@ function scanManifest(relativePath, filename, text) {
|
|
|
6690
7156
|
}
|
|
6691
7157
|
|
|
6692
7158
|
// ../../packages/scan/src/node/walk.ts
|
|
6693
|
-
var
|
|
6694
|
-
var
|
|
7159
|
+
var import_node_fs12 = require("fs");
|
|
7160
|
+
var import_node_path7 = require("path");
|
|
6695
7161
|
function compileExcludes(patterns) {
|
|
6696
7162
|
const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
|
|
6697
7163
|
if (matchers.length === 0) return () => false;
|
|
@@ -6762,7 +7228,7 @@ function matchPrefix(parts, segs) {
|
|
|
6762
7228
|
}
|
|
6763
7229
|
function readIgnoreFile(root) {
|
|
6764
7230
|
try {
|
|
6765
|
-
return (0,
|
|
7231
|
+
return (0, import_node_fs12.readFileSync)((0, import_node_path7.join)(root, ".threatcrushignore"), "utf-8").split("\n");
|
|
6766
7232
|
} catch {
|
|
6767
7233
|
return [];
|
|
6768
7234
|
}
|
|
@@ -6778,16 +7244,16 @@ function scanPath(targetPath, options = {}) {
|
|
|
6778
7244
|
let excluded = 0;
|
|
6779
7245
|
const rootIsDirectory = (() => {
|
|
6780
7246
|
try {
|
|
6781
|
-
return (0,
|
|
7247
|
+
return (0, import_node_fs12.statSync)(targetPath).isDirectory();
|
|
6782
7248
|
} catch {
|
|
6783
7249
|
return true;
|
|
6784
7250
|
}
|
|
6785
7251
|
})();
|
|
6786
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
7252
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path7.dirname)(targetPath);
|
|
6787
7253
|
const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
|
|
6788
7254
|
const scanFile = (fullPath, filename) => {
|
|
6789
7255
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
6790
|
-
const extension = (0,
|
|
7256
|
+
const extension = (0, import_node_path7.extname)(filename).toLowerCase();
|
|
6791
7257
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
6792
7258
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
6793
7259
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -6799,26 +7265,26 @@ function scanPath(targetPath, options = {}) {
|
|
|
6799
7265
|
let handle;
|
|
6800
7266
|
let declared = null;
|
|
6801
7267
|
try {
|
|
6802
|
-
handle = (0,
|
|
7268
|
+
handle = (0, import_node_fs12.openSync)(fullPath, "r");
|
|
6803
7269
|
} catch {
|
|
6804
7270
|
unreadable.push(relativePath);
|
|
6805
7271
|
return;
|
|
6806
7272
|
}
|
|
6807
7273
|
try {
|
|
6808
|
-
if ((0,
|
|
7274
|
+
if ((0, import_node_fs12.fstatSync)(handle).size > maxFileBytes) return;
|
|
6809
7275
|
if (mayDeclareInterpreter) {
|
|
6810
7276
|
const prefix = Buffer.alloc(128);
|
|
6811
|
-
const read = (0,
|
|
7277
|
+
const read = (0, import_node_fs12.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
6812
7278
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
6813
7279
|
if (!declared) return;
|
|
6814
7280
|
}
|
|
6815
|
-
text = (0,
|
|
7281
|
+
text = (0, import_node_fs12.readFileSync)(handle, "utf-8");
|
|
6816
7282
|
} catch {
|
|
6817
7283
|
unreadable.push(relativePath);
|
|
6818
7284
|
return;
|
|
6819
7285
|
} finally {
|
|
6820
7286
|
try {
|
|
6821
|
-
(0,
|
|
7287
|
+
(0, import_node_fs12.closeSync)(handle);
|
|
6822
7288
|
} catch {
|
|
6823
7289
|
}
|
|
6824
7290
|
}
|
|
@@ -6836,13 +7302,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
6836
7302
|
const walk = (currentPath) => {
|
|
6837
7303
|
let entries;
|
|
6838
7304
|
try {
|
|
6839
|
-
entries = (0,
|
|
7305
|
+
entries = (0, import_node_fs12.readdirSync)(currentPath, { withFileTypes: true });
|
|
6840
7306
|
} catch {
|
|
6841
7307
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
6842
7308
|
return;
|
|
6843
7309
|
}
|
|
6844
7310
|
for (const entry of entries) {
|
|
6845
|
-
const fullPath = (0,
|
|
7311
|
+
const fullPath = (0, import_node_path7.join)(currentPath, entry.name);
|
|
6846
7312
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
6847
7313
|
if (entry.isDirectory()) {
|
|
6848
7314
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
@@ -6866,7 +7332,7 @@ function scanPath(targetPath, options = {}) {
|
|
|
6866
7332
|
} else if (isExcluded(toRelative(walkRoot, targetPath))) {
|
|
6867
7333
|
excluded += 1;
|
|
6868
7334
|
} else {
|
|
6869
|
-
scanFile(targetPath, (0,
|
|
7335
|
+
scanFile(targetPath, (0, import_node_path7.basename)(targetPath));
|
|
6870
7336
|
}
|
|
6871
7337
|
if (options.missingControls) findings.push(...controls.findings());
|
|
6872
7338
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
@@ -6898,32 +7364,37 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
6898
7364
|
}
|
|
6899
7365
|
}
|
|
6900
7366
|
function toRelative(base, target) {
|
|
6901
|
-
const rel = (0,
|
|
6902
|
-
return (rel === "" ? target : rel).split(
|
|
7367
|
+
const rel = (0, import_node_path7.relative)(base, target);
|
|
7368
|
+
return (rel === "" ? target : rel).split(import_node_path7.sep).join("/");
|
|
6903
7369
|
}
|
|
6904
7370
|
|
|
6905
7371
|
// ../../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
|
-
];
|
|
7372
|
+
var import_node_fs13 = require("fs");
|
|
7373
|
+
var import_node_path8 = require("path");
|
|
6915
7374
|
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
6916
7375
|
async function scanDependencies(targetPath) {
|
|
6917
7376
|
const findings = [];
|
|
6918
|
-
for (const { file, ecosystem } of LOCKFILES) {
|
|
6919
|
-
const lockPath = (0,
|
|
6920
|
-
if (!(0,
|
|
7377
|
+
for (const { file, ecosystem, parse } of LOCKFILES) {
|
|
7378
|
+
const lockPath = (0, import_node_path8.join)(targetPath, file);
|
|
7379
|
+
if (!(0, import_node_fs13.existsSync)(lockPath)) continue;
|
|
6921
7380
|
let deps;
|
|
6922
7381
|
try {
|
|
6923
|
-
deps =
|
|
7382
|
+
deps = dedupe(parse((0, import_node_fs13.readFileSync)(lockPath, "utf-8")));
|
|
6924
7383
|
} catch {
|
|
6925
7384
|
continue;
|
|
6926
7385
|
}
|
|
7386
|
+
if (deps.length === 0) {
|
|
7387
|
+
findings.push(incompleteFinding(file, "No dependencies could be read from this lockfile."));
|
|
7388
|
+
continue;
|
|
7389
|
+
}
|
|
7390
|
+
if (deps.length > MAX_DEPS_PER_LOCKFILE) {
|
|
7391
|
+
findings.push(
|
|
7392
|
+
incompleteFinding(
|
|
7393
|
+
file,
|
|
7394
|
+
`Only the first ${MAX_DEPS_PER_LOCKFILE} of ${deps.length} locked packages were checked against OSV.`
|
|
7395
|
+
)
|
|
7396
|
+
);
|
|
7397
|
+
}
|
|
6927
7398
|
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
6928
7399
|
let vulns;
|
|
6929
7400
|
try {
|
|
@@ -6950,6 +7421,20 @@ async function scanDependencies(targetPath) {
|
|
|
6950
7421
|
}
|
|
6951
7422
|
return findings;
|
|
6952
7423
|
}
|
|
7424
|
+
function incompleteFinding(file, message) {
|
|
7425
|
+
return {
|
|
7426
|
+
ruleId: "dependency-scan-incomplete",
|
|
7427
|
+
title: "Dependency scan incomplete",
|
|
7428
|
+
file,
|
|
7429
|
+
line: 1,
|
|
7430
|
+
severity: "low",
|
|
7431
|
+
confidence: "evidence",
|
|
7432
|
+
message,
|
|
7433
|
+
consequence: "Advisories affecting the unchecked packages would not appear in this report.",
|
|
7434
|
+
excerpt: file,
|
|
7435
|
+
category: "dependency"
|
|
7436
|
+
};
|
|
7437
|
+
}
|
|
6953
7438
|
function severityFromCvss(score) {
|
|
6954
7439
|
if (!score) return "medium";
|
|
6955
7440
|
const value = Number.parseFloat(score);
|
|
@@ -6959,26 +7444,131 @@ function severityFromCvss(score) {
|
|
|
6959
7444
|
if (value >= 4) return "medium";
|
|
6960
7445
|
return "low";
|
|
6961
7446
|
}
|
|
6962
|
-
function
|
|
7447
|
+
function dedupe(deps) {
|
|
7448
|
+
const seen = /* @__PURE__ */ new Set();
|
|
7449
|
+
const unique = [];
|
|
7450
|
+
for (const dep of deps) {
|
|
7451
|
+
const key = `${dep.name}@${dep.version}`;
|
|
7452
|
+
if (seen.has(key)) continue;
|
|
7453
|
+
seen.add(key);
|
|
7454
|
+
unique.push(dep);
|
|
7455
|
+
}
|
|
7456
|
+
return unique;
|
|
7457
|
+
}
|
|
7458
|
+
function splitNameVersion(spec) {
|
|
7459
|
+
const at = spec.lastIndexOf("@");
|
|
7460
|
+
if (at <= 0) return null;
|
|
7461
|
+
const name = spec.slice(0, at);
|
|
7462
|
+
const version = spec.slice(at + 1);
|
|
7463
|
+
if (!name || !version) return null;
|
|
7464
|
+
return { name, version };
|
|
7465
|
+
}
|
|
7466
|
+
function exactVersion(raw) {
|
|
7467
|
+
const version = raw.trim().replace(/^[=v]+/, "");
|
|
7468
|
+
return /^[0-9][0-9a-zA-Z.+-]*$/.test(version) ? version : null;
|
|
7469
|
+
}
|
|
7470
|
+
function parsePackageLock(content) {
|
|
7471
|
+
const lock = JSON.parse(content);
|
|
7472
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
6963
7473
|
const deps = [];
|
|
6964
|
-
|
|
6965
|
-
const
|
|
6966
|
-
const
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
6970
|
-
|
|
7474
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
7475
|
+
const name = key.replace(/^.*node_modules\//, "");
|
|
7476
|
+
const version = value?.version;
|
|
7477
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
7478
|
+
}
|
|
7479
|
+
return deps;
|
|
7480
|
+
}
|
|
7481
|
+
function parsePnpmLock(content) {
|
|
7482
|
+
const deps = [];
|
|
7483
|
+
let inPackages = false;
|
|
7484
|
+
for (const line of content.split("\n")) {
|
|
7485
|
+
if (/^[a-zA-Z]/.test(line)) {
|
|
7486
|
+
inPackages = line.startsWith("packages:");
|
|
7487
|
+
continue;
|
|
6971
7488
|
}
|
|
6972
|
-
|
|
7489
|
+
if (!inPackages) continue;
|
|
7490
|
+
const match = /^ {2}(?! )(.+):\s*$/.exec(line);
|
|
7491
|
+
if (!match?.[1]) continue;
|
|
7492
|
+
let key = match[1].trim().replace(/^['"]|['"]$/g, "");
|
|
7493
|
+
key = key.replace(/^\//, "");
|
|
7494
|
+
key = key.replace(/\(.*$/, "");
|
|
7495
|
+
let name;
|
|
7496
|
+
let rawVersion;
|
|
7497
|
+
const slashed = /^(@?[^@]+)\/([0-9][^/]*)$/.exec(key);
|
|
7498
|
+
if (slashed?.[1] && slashed[2]) {
|
|
7499
|
+
name = slashed[1];
|
|
7500
|
+
rawVersion = slashed[2];
|
|
7501
|
+
} else {
|
|
7502
|
+
const dep = splitNameVersion(key);
|
|
7503
|
+
if (!dep) continue;
|
|
7504
|
+
name = dep.name;
|
|
7505
|
+
rawVersion = dep.version;
|
|
7506
|
+
}
|
|
7507
|
+
const version = exactVersion(rawVersion.replace(/_.*$/, ""));
|
|
7508
|
+
if (version) deps.push({ name, version });
|
|
7509
|
+
}
|
|
7510
|
+
return deps;
|
|
7511
|
+
}
|
|
7512
|
+
function parseYarnLock(content) {
|
|
7513
|
+
const deps = [];
|
|
7514
|
+
let pendingName = null;
|
|
7515
|
+
for (const line of content.split("\n")) {
|
|
7516
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
7517
|
+
if (!/^\s/.test(line)) {
|
|
7518
|
+
pendingName = null;
|
|
7519
|
+
const header = line.replace(/:\s*$/, "");
|
|
7520
|
+
const first = header.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
|
|
7521
|
+
if (!first) continue;
|
|
7522
|
+
if (!first.includes("@") || first === "__metadata") continue;
|
|
7523
|
+
if (/@(?:workspace|file|link|portal|exec|patch):/.test(first)) continue;
|
|
7524
|
+
const dep = splitNameVersion(first.replace(/@npm:/, "@"));
|
|
7525
|
+
if (dep) pendingName = dep.name;
|
|
7526
|
+
continue;
|
|
7527
|
+
}
|
|
7528
|
+
if (!pendingName) continue;
|
|
7529
|
+
const version = /^\s+version:?\s+["']?([^"'\s]+)["']?\s*$/.exec(line);
|
|
7530
|
+
if (!version?.[1]) continue;
|
|
7531
|
+
const exact = exactVersion(version[1]);
|
|
7532
|
+
if (exact) deps.push({ name: pendingName, version: exact });
|
|
7533
|
+
pendingName = null;
|
|
6973
7534
|
}
|
|
6974
|
-
|
|
6975
|
-
|
|
6976
|
-
|
|
6977
|
-
|
|
7535
|
+
return deps;
|
|
7536
|
+
}
|
|
7537
|
+
function parsePipfileLock(content) {
|
|
7538
|
+
const lock = JSON.parse(content);
|
|
7539
|
+
const deps = [];
|
|
7540
|
+
for (const section of ["default", "develop"]) {
|
|
7541
|
+
const packages = lock[section];
|
|
7542
|
+
if (!packages || typeof packages !== "object") continue;
|
|
7543
|
+
for (const [name, value] of Object.entries(packages)) {
|
|
7544
|
+
const version = exactVersion(String(value?.version ?? "").replace(/^==/, ""));
|
|
7545
|
+
if (name && version) deps.push({ name, version });
|
|
6978
7546
|
}
|
|
6979
7547
|
}
|
|
6980
7548
|
return deps;
|
|
6981
7549
|
}
|
|
7550
|
+
function parseRequirementsTxt(content) {
|
|
7551
|
+
const deps = [];
|
|
7552
|
+
for (const raw of content.split("\n")) {
|
|
7553
|
+
const line = raw.split("#")[0]?.split(";")[0]?.trim();
|
|
7554
|
+
if (!line || line.startsWith("-")) continue;
|
|
7555
|
+
const match = /^([a-zA-Z0-9._-]+)\s*(?:\[[^\]]*\])?\s*==\s*([^\s,]+)/.exec(line);
|
|
7556
|
+
if (!match?.[1] || !match[2]) continue;
|
|
7557
|
+
const version = exactVersion(match[2]);
|
|
7558
|
+
if (version) deps.push({ name: match[1], version });
|
|
7559
|
+
}
|
|
7560
|
+
return deps;
|
|
7561
|
+
}
|
|
7562
|
+
var LOCKFILES = [
|
|
7563
|
+
{ file: "package-lock.json", ecosystem: "npm", parse: parsePackageLock },
|
|
7564
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm", parse: parsePnpmLock },
|
|
7565
|
+
{ file: "yarn.lock", ecosystem: "npm", parse: parseYarnLock },
|
|
7566
|
+
{ file: "requirements.txt", ecosystem: "PyPI", parse: parseRequirementsTxt },
|
|
7567
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI", parse: parsePipfileLock }
|
|
7568
|
+
];
|
|
7569
|
+
var LOCKFILE_PARSERS = Object.fromEntries(
|
|
7570
|
+
LOCKFILES.map((entry) => [entry.file, entry.parse])
|
|
7571
|
+
);
|
|
6982
7572
|
function isValidPackageName(name) {
|
|
6983
7573
|
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
6984
7574
|
}
|
|
@@ -7003,17 +7593,17 @@ async function queryOsv(name, version, ecosystem) {
|
|
|
7003
7593
|
}
|
|
7004
7594
|
|
|
7005
7595
|
// ../../packages/scan/src/node/sarif.ts
|
|
7006
|
-
var
|
|
7007
|
-
var
|
|
7596
|
+
var import_node_crypto3 = require("crypto");
|
|
7597
|
+
var import_node_path9 = require("path");
|
|
7008
7598
|
|
|
7009
7599
|
// src/commands/scan.ts
|
|
7010
7600
|
function readVersion() {
|
|
7011
7601
|
for (const candidate of [
|
|
7012
|
-
(0,
|
|
7013
|
-
(0,
|
|
7602
|
+
(0, import_node_path10.join)(__dirname, "..", "package.json"),
|
|
7603
|
+
(0, import_node_path10.join)(__dirname, "..", "..", "package.json")
|
|
7014
7604
|
]) {
|
|
7015
7605
|
try {
|
|
7016
|
-
return JSON.parse((0,
|
|
7606
|
+
return JSON.parse((0, import_node_fs14.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
7017
7607
|
} catch {
|
|
7018
7608
|
}
|
|
7019
7609
|
}
|
|
@@ -7474,8 +8064,8 @@ var RuleEngine = class {
|
|
|
7474
8064
|
};
|
|
7475
8065
|
|
|
7476
8066
|
// src/daemon/rules/loader.ts
|
|
7477
|
-
var
|
|
7478
|
-
var
|
|
8067
|
+
var import_node_fs15 = require("fs");
|
|
8068
|
+
var import_node_path11 = require("path");
|
|
7479
8069
|
|
|
7480
8070
|
// src/daemon/rules/default-rules.ts
|
|
7481
8071
|
var DEFAULT_RULES = [
|
|
@@ -7759,11 +8349,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
7759
8349
|
function loadAllRules(customDir) {
|
|
7760
8350
|
const rules = [...DEFAULT_RULES];
|
|
7761
8351
|
const dir = customDir || RULES_DIR;
|
|
7762
|
-
if ((0,
|
|
7763
|
-
const files = (0,
|
|
8352
|
+
if ((0, import_node_fs15.existsSync)(dir)) {
|
|
8353
|
+
const files = (0, import_node_fs15.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
7764
8354
|
for (const file of files) {
|
|
7765
8355
|
try {
|
|
7766
|
-
const raw = (0,
|
|
8356
|
+
const raw = (0, import_node_fs15.readFileSync)((0, import_node_path11.join)(dir, file), "utf-8");
|
|
7767
8357
|
const parsed = JSON.parse(raw);
|
|
7768
8358
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
7769
8359
|
for (const rule of customRules) {
|
|
@@ -7926,7 +8516,7 @@ function detectFirewallAdapter() {
|
|
|
7926
8516
|
}
|
|
7927
8517
|
|
|
7928
8518
|
// src/daemon/firewall/remediation.ts
|
|
7929
|
-
var
|
|
8519
|
+
var import_node_fs16 = require("fs");
|
|
7930
8520
|
var DEFAULT_CONFIG2 = {
|
|
7931
8521
|
enabled: true,
|
|
7932
8522
|
dry_run: true,
|
|
@@ -8081,7 +8671,7 @@ var RemediationManager = class {
|
|
|
8081
8671
|
}
|
|
8082
8672
|
logLine(line) {
|
|
8083
8673
|
try {
|
|
8084
|
-
(0,
|
|
8674
|
+
(0, import_node_fs16.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
8085
8675
|
`);
|
|
8086
8676
|
} catch {
|
|
8087
8677
|
}
|
|
@@ -8137,7 +8727,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
8137
8727
|
// src/daemon/index.ts
|
|
8138
8728
|
function readVersion2() {
|
|
8139
8729
|
try {
|
|
8140
|
-
const pkg = JSON.parse((0,
|
|
8730
|
+
const pkg = JSON.parse((0, import_node_fs17.readFileSync)((0, import_node_path12.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
8141
8731
|
return pkg.version || "0.0.0";
|
|
8142
8732
|
} catch {
|
|
8143
8733
|
return "0.0.0";
|
|
@@ -8145,7 +8735,7 @@ function readVersion2() {
|
|
|
8145
8735
|
}
|
|
8146
8736
|
function logLine(line) {
|
|
8147
8737
|
try {
|
|
8148
|
-
(0,
|
|
8738
|
+
(0, import_node_fs17.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
8149
8739
|
`);
|
|
8150
8740
|
} catch {
|
|
8151
8741
|
}
|
|
@@ -8173,7 +8763,7 @@ async function runDaemon() {
|
|
|
8173
8763
|
} catch (err) {
|
|
8174
8764
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
8175
8765
|
}
|
|
8176
|
-
const config = loadConfig((0,
|
|
8766
|
+
const config = loadConfig((0, import_node_fs17.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
8177
8767
|
bus.on("event", (event) => {
|
|
8178
8768
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
8179
8769
|
});
|