@profullstack/threatcrush 0.11.0 → 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 +1139 -119
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1427 -254
- 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;
|
|
@@ -4310,6 +4468,357 @@ function workerId() {
|
|
|
4310
4468
|
return `${import_node_os4.default.hostname()}/${process.pid}`;
|
|
4311
4469
|
}
|
|
4312
4470
|
|
|
4471
|
+
// ../../packages/scan/src/node-rules.ts
|
|
4472
|
+
var PATH_CONTAINMENT_GUARD = /\bstartsWith\s*\(|\brelative\s*\(|\bnormalize\s*\(|\brealpath\b|\bisInside\b|\bwithin\s*\(|\bsanitiz\w*\b/i;
|
|
4473
|
+
var ARCHIVE_LIBRARY = /\b(?:adm-zip|unzipper|yauzl|node-stream-zip|extract-zip|decompress|tar-stream|tar-fs)\b|require\s*\(\s*['"]tar['"]|from\s+['"]tar['"]/;
|
|
4474
|
+
var HEADLESS_BROWSER = /\b(?:puppeteer|playwright|phantom|phantomjs|wkhtmltopdf|wkhtmltoimage|chrome-aws-lambda|html-pdf)\b/;
|
|
4475
|
+
var ORIGIN_ALLOWLIST_GUARD = /\b(?:includes|indexOf|has|test|find|some)\s*\(/;
|
|
4476
|
+
var FUNCTION_DESERIALIZER = /\b(?:node-serialize|serialize-to-js|funcster|cryo)\b/;
|
|
4477
|
+
var CONCATENATED_STRING = String.raw`(?:"[^"\n]*"|'[^'\n]*')\s*\+`;
|
|
4478
|
+
var INTERPOLATED_TEMPLATE = "`[^`\\n]*\\$\\{";
|
|
4479
|
+
var NODE_RULES = [
|
|
4480
|
+
// ── Code execution ───────────────────────────────────────────────────────
|
|
4481
|
+
{
|
|
4482
|
+
id: "js-vm-untrusted-execution",
|
|
4483
|
+
title: "script compiled and run by the `vm` module",
|
|
4484
|
+
consequence: "Node\u2019s `vm` is not a security boundary \u2014 it isolates globals, not the process. Code reaching it can walk back out through any object it is handed and runs with the server\u2019s full privileges.",
|
|
4485
|
+
cwe: "CWE-94",
|
|
4486
|
+
severity: "critical",
|
|
4487
|
+
languages: ["javascript", "typescript"],
|
|
4488
|
+
pattern: /\bvm\s*\.\s*(?:runInNewContext|runInThisContext|runInContext|compileFunction)\s*\(|\bnew\s+vm\s*\.\s*Script\s*\(/,
|
|
4489
|
+
// A `vm` call whose source is a build-time constant is a plugin loader, not
|
|
4490
|
+
// a vulnerability. The class only becomes real once the script text can be
|
|
4491
|
+
// influenced, so require that evidence before reporting at full severity.
|
|
4492
|
+
needsContext: true
|
|
4493
|
+
},
|
|
4494
|
+
{
|
|
4495
|
+
id: "js-vm2-sandbox",
|
|
4496
|
+
title: "`vm2` used as a sandbox",
|
|
4497
|
+
consequence: "vm2 was discontinued after a series of escapes that its maintainer judged unfixable by design. Any code it runs should be assumed to run on the host.",
|
|
4498
|
+
cwe: "CWE-1104",
|
|
4499
|
+
severity: "high",
|
|
4500
|
+
languages: ["javascript", "typescript"],
|
|
4501
|
+
pattern: /\bnew\s+(?:NodeVM|VMScript)\s*\(|\bfrom\s+['"]vm2['"]|require\s*\(\s*['"]vm2['"]\s*\)/,
|
|
4502
|
+
// Nothing on the surrounding lines changes the answer: the package itself
|
|
4503
|
+
// is the finding, the same way a broken cipher is.
|
|
4504
|
+
inherent: true
|
|
4505
|
+
},
|
|
4506
|
+
{
|
|
4507
|
+
id: "js-function-deserialization",
|
|
4508
|
+
title: "deserialiser that reconstructs functions",
|
|
4509
|
+
consequence: "These formats encode functions alongside data and invoke them on load, so parsing an attacker\u2019s payload is executing it. No amount of validation after the parse call helps \u2014 the code has already run.",
|
|
4510
|
+
cwe: "CWE-502",
|
|
4511
|
+
severity: "critical",
|
|
4512
|
+
languages: ["javascript", "typescript"],
|
|
4513
|
+
pattern: /\b(?:unserialize|deepDeserialize|deserialize)\s*\(/,
|
|
4514
|
+
// Without this the rule fires on every project that happens to own a
|
|
4515
|
+
// function called `deserialize`, which is most of them. The import is what
|
|
4516
|
+
// makes the call the dangerous one.
|
|
4517
|
+
fileRequires: FUNCTION_DESERIALIZER,
|
|
4518
|
+
// The parse *is* the execution, so nearby input cannot make it worse and
|
|
4519
|
+
// its absence cannot make it safe. Nobody round-trips a constant through
|
|
4520
|
+
// these libraries.
|
|
4521
|
+
inherent: true
|
|
4522
|
+
},
|
|
4523
|
+
{
|
|
4524
|
+
id: "js-template-injection",
|
|
4525
|
+
title: "template compiled from a non-constant source",
|
|
4526
|
+
consequence: "Template languages are programming languages. A user-supplied template body is remote code execution, not cross-site scripting \u2014 the expression runs on the server before any output is escaped.",
|
|
4527
|
+
cwe: "CWE-1336",
|
|
4528
|
+
severity: "critical",
|
|
4529
|
+
languages: ["javascript", "typescript"],
|
|
4530
|
+
pattern: /\b(?:handlebars|Handlebars|hbs|ejs|pug|jade|nunjucks|eta|twig|dot|doT|liquid|mustache)\s*\.\s*(?:compile|compileFile|render|renderString)\s*\(\s*(?:`[^`\n]*\$\{|[a-zA-Z_$][\w$.]*\s*[,)]|[a-zA-Z_$][\w$.]*\s*\+)/,
|
|
4531
|
+
// Rendering a template held in a variable is the normal case — it was read
|
|
4532
|
+
// from a file at boot. Only the version where request data reaches the
|
|
4533
|
+
// template *body* is this class.
|
|
4534
|
+
needsContext: true
|
|
4535
|
+
},
|
|
4536
|
+
{
|
|
4537
|
+
id: "js-shelljs-command-execution",
|
|
4538
|
+
title: "shelljs command assembled from a string",
|
|
4539
|
+
consequence: "`shell.exec` runs its argument through a shell, so a `;` or backtick in an interpolated value runs as the server user.",
|
|
4540
|
+
cwe: "CWE-78",
|
|
4541
|
+
severity: "critical",
|
|
4542
|
+
languages: ["javascript", "typescript"],
|
|
4543
|
+
pattern: new RegExp(
|
|
4544
|
+
`\\b(?:shell|shelljs|sh)\\s*\\.\\s*exec\\s*\\(\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING}|[a-zA-Z_$][\\w$]*\\s*\\+)`
|
|
4545
|
+
),
|
|
4546
|
+
fileRequires: /\bshelljs\b/
|
|
4547
|
+
},
|
|
4548
|
+
// ── Injection ────────────────────────────────────────────────────────────
|
|
4549
|
+
{
|
|
4550
|
+
id: "js-nosql-where-expression",
|
|
4551
|
+
title: "MongoDB `$where` built by string assembly",
|
|
4552
|
+
consequence: "`$where` is evaluated as JavaScript by the database server, once per document. An interpolated value can rewrite the predicate to `true` or run a denial-of-service loop inside the database.",
|
|
4553
|
+
cwe: "CWE-943",
|
|
4554
|
+
severity: "critical",
|
|
4555
|
+
languages: ["javascript", "typescript"],
|
|
4556
|
+
pattern: new RegExp(
|
|
4557
|
+
`\\$where\\s*['"]?\\s*:\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING})|\\.\\s*\\$where\\s*=\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING})`
|
|
4558
|
+
)
|
|
4559
|
+
},
|
|
4560
|
+
{
|
|
4561
|
+
id: "js-xpath-injection",
|
|
4562
|
+
title: "XPath expression built by concatenation",
|
|
4563
|
+
consequence: "A quote in the interpolated value closes the predicate early, so the query selects nodes the caller was never meant to read \u2014 the XML equivalent of `OR 1=1`.",
|
|
4564
|
+
cwe: "CWE-643",
|
|
4565
|
+
severity: "high",
|
|
4566
|
+
languages: ["javascript", "typescript"],
|
|
4567
|
+
// An XPath expression is recognisable by its own syntax — a descendant
|
|
4568
|
+
// axis or an attribute predicate. Matching on the *call* name alone would
|
|
4569
|
+
// flag every `select(` in the ecosystem. The quoted forms are split per
|
|
4570
|
+
// quote character for the reason given at CONCATENATED_STRING: the
|
|
4571
|
+
// predicate that makes it XPath usually contains the other quote.
|
|
4572
|
+
pattern: new RegExp(
|
|
4573
|
+
`\\b(?:xpath|xpathSelect|select|selectNodes|selectSingleNode|evaluate|find)\\s*\\(\\s*(?:\`[^\`\\n]*(?:\\/\\/|\\[@)[^\`\\n]*\\$\\{|"[^"\\n]*(?:\\/\\/|\\[@)[^"\\n]*"\\s*\\+|'[^'\\n]*(?:\\/\\/|\\[@)[^'\\n]*'\\s*\\+)`
|
|
4574
|
+
),
|
|
4575
|
+
fileRequires: /\bxpath\b|\bxmldom\b|\blibxmljs\b|\bxpath\.js\b/
|
|
4576
|
+
},
|
|
4577
|
+
{
|
|
4578
|
+
id: "js-regex-from-input",
|
|
4579
|
+
title: "regular expression compiled from a variable",
|
|
4580
|
+
consequence: "A caller who controls the pattern controls the matcher: they can supply catastrophic backtracking to hang the event loop, or a permissive pattern that defeats whatever the regex was validating.",
|
|
4581
|
+
cwe: "CWE-1333",
|
|
4582
|
+
severity: "medium",
|
|
4583
|
+
languages: ["javascript", "typescript"],
|
|
4584
|
+
pattern: new RegExp(
|
|
4585
|
+
`\\bnew\\s+RegExp\\s*\\(\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING}|(?!['"\`/])[a-zA-Z_$][\\w$.]*\\s*[,)])`
|
|
4586
|
+
),
|
|
4587
|
+
needsContext: true
|
|
4588
|
+
},
|
|
4589
|
+
// ── XML ──────────────────────────────────────────────────────────────────
|
|
4590
|
+
{
|
|
4591
|
+
id: "js-xml-external-entities",
|
|
4592
|
+
title: "XML parser configured to resolve entities",
|
|
4593
|
+
consequence: "An entity declaration in the document body makes the parser fetch a local file or an internal URL and paste the result into the parsed output \u2014 file disclosure and server-side request forgery from a document upload.",
|
|
4594
|
+
cwe: "CWE-611",
|
|
4595
|
+
severity: "high",
|
|
4596
|
+
languages: ["javascript", "typescript"],
|
|
4597
|
+
// The option name is the finding. Every parser in the ecosystem defaults
|
|
4598
|
+
// these off, so an explicit `true` is a deliberate re-enable.
|
|
4599
|
+
pattern: /\b(?:noent|resolveEntities|expandEntities|externalEntities|resolveExternals)\s*:\s*(?:true|1)\b/,
|
|
4600
|
+
inherent: true
|
|
4601
|
+
},
|
|
4602
|
+
// ── Authentication and tokens ────────────────────────────────────────────
|
|
4603
|
+
{
|
|
4604
|
+
id: "js-jwt-none-algorithm",
|
|
4605
|
+
title: "JWT algorithm set to `none`",
|
|
4606
|
+
consequence: "The `none` algorithm means the signature is not checked. Anyone can mint a token with any claims \u2014 including another user\u2019s id or an admin role \u2014 by base64-encoding a header and a body.",
|
|
4607
|
+
cwe: "CWE-347",
|
|
4608
|
+
severity: "critical",
|
|
4609
|
+
languages: ["javascript", "typescript"],
|
|
4610
|
+
pattern: /\balgorithms?\s*:\s*\[?\s*['"]none['"]/i,
|
|
4611
|
+
inherent: true
|
|
4612
|
+
},
|
|
4613
|
+
// ── Cryptography ─────────────────────────────────────────────────────────
|
|
4614
|
+
{
|
|
4615
|
+
id: "js-broken-cipher-algorithm",
|
|
4616
|
+
title: "broken cipher selected",
|
|
4617
|
+
consequence: "DES, 3DES, RC2, RC4, Blowfish and IDEA are all breakable with commodity hardware or have practical plaintext-recovery attacks. Data encrypted with them should be treated as encoded, not encrypted.",
|
|
4618
|
+
cwe: "CWE-327",
|
|
4619
|
+
severity: "high",
|
|
4620
|
+
languages: ["javascript", "typescript"],
|
|
4621
|
+
pattern: /\bcreate(?:Cipher|Decipher)(?:iv)?\s*\(\s*['"](?:des|des3|des-ede\w*|3des|rc2|rc4|bf|blowfish|cast5?|idea|seed)\b/i,
|
|
4622
|
+
inherent: true
|
|
4623
|
+
},
|
|
4624
|
+
{
|
|
4625
|
+
id: "js-ecb-mode-cipher",
|
|
4626
|
+
title: "block cipher in ECB mode",
|
|
4627
|
+
consequence: "ECB encrypts every block independently, so identical plaintext blocks produce identical ciphertext. Structure in the data survives encryption and blocks can be reordered or replayed without detection.",
|
|
4628
|
+
cwe: "CWE-327",
|
|
4629
|
+
severity: "high",
|
|
4630
|
+
languages: ["javascript", "typescript"],
|
|
4631
|
+
pattern: /\bcreate(?:Cipher|Decipher)(?:iv)?\s*\(\s*['"][^'"\n]*-ecb\b/i,
|
|
4632
|
+
inherent: true
|
|
4633
|
+
},
|
|
4634
|
+
{
|
|
4635
|
+
id: "js-legacy-cipher-api",
|
|
4636
|
+
title: "deprecated `createCipher` used",
|
|
4637
|
+
consequence: "`createCipher` derives the key from a passphrase with a single unsalted MD5 pass and uses a fixed all-zero IV, so the same passphrase always produces the same keystream. It was removed in Node 22.",
|
|
4638
|
+
cwe: "CWE-327",
|
|
4639
|
+
severity: "high",
|
|
4640
|
+
languages: ["javascript", "typescript"],
|
|
4641
|
+
// `createCipheriv` is the correct API and shares the prefix, so the
|
|
4642
|
+
// negative look-ahead is what separates the finding from the fix.
|
|
4643
|
+
pattern: /\bcrypto\s*\.\s*create(?:Cipher|Decipher)\s*\(/,
|
|
4644
|
+
lineGuard: /create(?:Cipher|Decipher)iv\s*\(/,
|
|
4645
|
+
inherent: true
|
|
4646
|
+
},
|
|
4647
|
+
// ── Cross-site scripting ─────────────────────────────────────────────────
|
|
4648
|
+
{
|
|
4649
|
+
id: "js-template-autoescape-disabled",
|
|
4650
|
+
title: "template auto-escaping turned off",
|
|
4651
|
+
consequence: "Auto-escaping is the control that makes a template engine safe by default. Disabling it globally means every interpolation in every template becomes an injection point, including ones written later by someone who assumed the default.",
|
|
4652
|
+
cwe: "CWE-79",
|
|
4653
|
+
severity: "high",
|
|
4654
|
+
languages: ["javascript", "typescript"],
|
|
4655
|
+
pattern: /\bautoescape\s*:\s*false\b|\bescape\s*:\s*false\b|\bnoEscape\s*:\s*true\b/,
|
|
4656
|
+
inherent: true
|
|
4657
|
+
},
|
|
4658
|
+
{
|
|
4659
|
+
id: "js-serialize-javascript-unsafe",
|
|
4660
|
+
title: "`serialize-javascript` in unsafe mode",
|
|
4661
|
+
consequence: "The `unsafe` flag turns off escaping of HTML-significant characters in the output. Embedding the result in a `<script>` block lets a string value close the tag and start a new one.",
|
|
4662
|
+
cwe: "CWE-79",
|
|
4663
|
+
severity: "high",
|
|
4664
|
+
languages: ["javascript", "typescript"],
|
|
4665
|
+
pattern: /\bunsafe\s*:\s*true\b/,
|
|
4666
|
+
fileRequires: /\bserialize-javascript\b/,
|
|
4667
|
+
inherent: true
|
|
4668
|
+
},
|
|
4669
|
+
{
|
|
4670
|
+
id: "js-cors-origin-reflected",
|
|
4671
|
+
title: "CORS origin reflected from the request",
|
|
4672
|
+
consequence: "Echoing the caller\u2019s `Origin` header back as `Access-Control-Allow-Origin` allows every site, while looking like an allow-list. Combined with credentials, any page the victim visits can read their authenticated responses.",
|
|
4673
|
+
cwe: "CWE-942",
|
|
4674
|
+
severity: "high",
|
|
4675
|
+
languages: ["javascript", "typescript"],
|
|
4676
|
+
// Both spellings of the same mistake: writing the header directly, and
|
|
4677
|
+
// handing the request's origin to a CORS middleware's `origin` option.
|
|
4678
|
+
//
|
|
4679
|
+
// An earlier draft also matched a bare variable — `setHeader('…', origin)`
|
|
4680
|
+
// — which is wrong. A variable holding a *validated* origin is exactly
|
|
4681
|
+
// what the correct implementation looks like, and the rule cannot tell the
|
|
4682
|
+
// two apart. Only the visible read from the request qualifies.
|
|
4683
|
+
pattern: /\bAccess-Control-Allow-Origin['"]\s*,\s*(?:req|request|ctx)\s*\.|\borigin\s*:\s*(?:req|request|ctx)\s*\./,
|
|
4684
|
+
// Reflecting the header is the defect whatever surrounds it; there is no
|
|
4685
|
+
// arrangement of nearby lines that makes an echoed origin safe — except a
|
|
4686
|
+
// membership test on the value, which is what the guard looks for.
|
|
4687
|
+
inherent: true,
|
|
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)$/
|
|
4699
|
+
},
|
|
4700
|
+
// ── Server-side request forgery ──────────────────────────────────────────
|
|
4701
|
+
{
|
|
4702
|
+
id: "js-headless-browser-navigation",
|
|
4703
|
+
title: "headless browser sent to a non-constant URL",
|
|
4704
|
+
consequence: "The browser runs on the server, inside the private network. A controlled URL reaches the cloud metadata endpoint, localhost admin panels and internal services \u2014 and `file://` reads the disk.",
|
|
4705
|
+
cwe: "CWE-918",
|
|
4706
|
+
severity: "high",
|
|
4707
|
+
languages: ["javascript", "typescript"],
|
|
4708
|
+
pattern: /\.\s*(?:goto|setContent|navigate)\s*\(\s*(?:`[^`\n]*\$\{|(?!['"`])[a-zA-Z_$][\w$.]*\s*[,)])/,
|
|
4709
|
+
fileRequires: HEADLESS_BROWSER,
|
|
4710
|
+
needsContext: true
|
|
4711
|
+
},
|
|
4712
|
+
// ── Path handling ────────────────────────────────────────────────────────
|
|
4713
|
+
{
|
|
4714
|
+
id: "js-archive-entry-path",
|
|
4715
|
+
title: "archive entry written to a path built from its own name",
|
|
4716
|
+
consequence: "An entry named `../../etc/cron.d/x` escapes the extraction directory when its name is joined to the destination. Overwriting a file outside the target \u2014 a systemd unit, an SSH key, a deployed script \u2014 is code execution on the next run.",
|
|
4717
|
+
cwe: "CWE-22",
|
|
4718
|
+
severity: "high",
|
|
4719
|
+
languages: ["javascript", "typescript"],
|
|
4720
|
+
pattern: /\b(?:join|resolve)\s*\(\s*[^,\n)]+,\s*[a-zA-Z_$][\w$]*\s*\.\s*(?:entryName|fileName|filename|name|path)\b/,
|
|
4721
|
+
fileRequires: ARCHIVE_LIBRARY,
|
|
4722
|
+
guard: PATH_CONTAINMENT_GUARD,
|
|
4723
|
+
// The containment check comes *after* the join — you build the path, then
|
|
4724
|
+
// verify it stayed inside. A backwards-only window would miss every
|
|
4725
|
+
// correct implementation and report the safe code alongside the unsafe.
|
|
4726
|
+
guardForward: 3
|
|
4727
|
+
},
|
|
4728
|
+
// ── Electron ─────────────────────────────────────────────────────────────
|
|
4729
|
+
{
|
|
4730
|
+
id: "js-electron-node-integration",
|
|
4731
|
+
title: "Electron renderer given Node access",
|
|
4732
|
+
consequence: "With node integration on \u2014 or context isolation off \u2014 any script that reaches the page reaches `require`. A single XSS in rendered content becomes `child_process.exec` on the user\u2019s machine.",
|
|
4733
|
+
cwe: "CWE-1188",
|
|
4734
|
+
severity: "high",
|
|
4735
|
+
languages: ["javascript", "typescript"],
|
|
4736
|
+
pattern: /\bnodeIntegration(?:InWorker|InSubFrames)?\s*:\s*true\b|\bcontextIsolation\s*:\s*false\b|\benableRemoteModule\s*:\s*true\b|\bsandbox\s*:\s*false\b/,
|
|
4737
|
+
inherent: true
|
|
4738
|
+
},
|
|
4739
|
+
{
|
|
4740
|
+
id: "js-electron-web-security-disabled",
|
|
4741
|
+
title: "Electron web security disabled",
|
|
4742
|
+
consequence: "Turning off `webSecurity` drops the same-origin policy for the window, so remote content can read local files and every other origin the app has loaded.",
|
|
4743
|
+
cwe: "CWE-1173",
|
|
4744
|
+
severity: "high",
|
|
4745
|
+
languages: ["javascript", "typescript"],
|
|
4746
|
+
pattern: /\bwebSecurity\s*:\s*false\b|\ballowRunningInsecureContent\s*:\s*true\b|\bwebviewTag\s*:\s*true\b/,
|
|
4747
|
+
inherent: true
|
|
4748
|
+
},
|
|
4749
|
+
{
|
|
4750
|
+
id: "js-electron-open-external",
|
|
4751
|
+
title: "Electron `openExternal` with a non-constant URL",
|
|
4752
|
+
consequence: "`shell.openExternal` hands the string to the operating system\u2019s handler. A `file://` path executes a local binary and, on Windows, an SMB path executes a remote one.",
|
|
4753
|
+
cwe: "CWE-749",
|
|
4754
|
+
severity: "high",
|
|
4755
|
+
languages: ["javascript", "typescript"],
|
|
4756
|
+
pattern: /\bshell\s*\.\s*openExternal\s*\(\s*(?:`[^`\n]*\$\{|(?!['"`])[a-zA-Z_$][\w$.]*\s*[,)])/,
|
|
4757
|
+
needsContext: true
|
|
4758
|
+
},
|
|
4759
|
+
// ── Hardening and resource limits ────────────────────────────────────────
|
|
4760
|
+
{
|
|
4761
|
+
id: "js-helmet-protection-disabled",
|
|
4762
|
+
title: "security header explicitly disabled",
|
|
4763
|
+
consequence: "Each of these switches off a browser-side protection that was already on. The header stops being sent, so the defence it enables \u2014 framing, sniffing, referrer leakage, transport downgrade \u2014 is available to an attacker again.",
|
|
4764
|
+
cwe: "CWE-693",
|
|
4765
|
+
severity: "medium",
|
|
4766
|
+
languages: ["javascript", "typescript"],
|
|
4767
|
+
pattern: /\b(?:contentSecurityPolicy|frameguard|hsts|noSniff|xssFilter|hidePoweredBy|referrerPolicy|dnsPrefetchControl|ieNoOpen|permittedCrossDomainPolicies|crossOriginEmbedderPolicy|crossOriginOpenerPolicy|crossOriginResourcePolicy|originAgentCluster)\s*:\s*false\b/,
|
|
4768
|
+
inherent: true
|
|
4769
|
+
},
|
|
4770
|
+
{
|
|
4771
|
+
id: "js-buffer-bounds-check-disabled",
|
|
4772
|
+
title: "buffer bounds checking turned off",
|
|
4773
|
+
consequence: "With `noAssert` the read or write is not range-checked, so an offset past the end of the buffer returns adjacent heap memory or corrupts it instead of throwing.",
|
|
4774
|
+
cwe: "CWE-125",
|
|
4775
|
+
severity: "medium",
|
|
4776
|
+
languages: ["javascript", "typescript"],
|
|
4777
|
+
// The trailing `true` on a Buffer numeric accessor *is* `noAssert` — it is
|
|
4778
|
+
// the last positional parameter of every one of these methods. Anchoring
|
|
4779
|
+
// on the accessor name is what keeps this from matching `, true)` on any
|
|
4780
|
+
// call in the codebase.
|
|
4781
|
+
pattern: /\b(?:read|write)(?:U?Int(?:8|16|32)(?:[BL]E)?|U?Int[BL]E|Float[BL]E|Double[BL]E)\s*\([^)\n]*,\s*true\s*\)|\bnoAssert\s*:\s*true\b/,
|
|
4782
|
+
inherent: true
|
|
4783
|
+
},
|
|
4784
|
+
{
|
|
4785
|
+
id: "js-uninitialized-buffer",
|
|
4786
|
+
title: "buffer allocated without zeroing",
|
|
4787
|
+
consequence: "`allocUnsafe` and the old `new Buffer(size)` hand back whatever was previously in that heap memory \u2014 keys, session tokens, other users\u2019 request bodies. Anything not overwritten before the buffer is sent leaks it.",
|
|
4788
|
+
cwe: "CWE-908",
|
|
4789
|
+
severity: "medium",
|
|
4790
|
+
languages: ["javascript", "typescript"],
|
|
4791
|
+
pattern: /\bBuffer\s*\.\s*allocUnsafe(?:Slow)?\s*\(|\bnew\s+Buffer\s*\(\s*(?![`'"])[a-zA-Z_$0-9]/,
|
|
4792
|
+
inherent: true
|
|
4793
|
+
},
|
|
4794
|
+
{
|
|
4795
|
+
id: "js-oversized-request-body-limit",
|
|
4796
|
+
title: "request body limit raised to a very large value",
|
|
4797
|
+
consequence: "A body limit in the tens of megabytes lets a handful of concurrent requests exhaust memory, and the parse happens before any authentication check the route performs.",
|
|
4798
|
+
cwe: "CWE-400",
|
|
4799
|
+
severity: "medium",
|
|
4800
|
+
languages: ["javascript", "typescript"],
|
|
4801
|
+
// Two or more digits before `mb` — 50mb and up. A limit is a *good* thing;
|
|
4802
|
+
// only an ineffective one is the finding.
|
|
4803
|
+
pattern: /\blimit\s*:\s*['"]\s*(?:[5-9]\d|\d{3,})\s*mb\s*['"]|\blimit\s*:\s*['"]\s*\d+\s*gb\s*['"]/i,
|
|
4804
|
+
inherent: true
|
|
4805
|
+
},
|
|
4806
|
+
// ── Information disclosure ───────────────────────────────────────────────
|
|
4807
|
+
{
|
|
4808
|
+
id: "js-error-detail-returned",
|
|
4809
|
+
title: "error object or stack trace sent to the client",
|
|
4810
|
+
consequence: "A stack trace names absolute paths, package versions and internal function names, and framework errors often carry the failing query or connection string with them. It is a map of the server, handed out on request.",
|
|
4811
|
+
cwe: "CWE-209",
|
|
4812
|
+
severity: "medium",
|
|
4813
|
+
languages: ["javascript", "typescript"],
|
|
4814
|
+
// `res.status(500).send(…)` is the overwhelmingly common spelling, so the
|
|
4815
|
+
// optional `.status(…)` hop is not a nicety — without it the rule misses
|
|
4816
|
+
// nearly every real occurrence.
|
|
4817
|
+
pattern: /\bres\s*\.\s*(?:status\s*\([^)\n]*\)\s*\.\s*)?(?:send|json|end|write)\s*\(\s*(?:[a-zA-Z_$][\w$]*\s*\.\s*stack\b|(?:error|err|e)\s*[,)])|\.\s*(?:send|json)\s*\(\s*\{[^}\n]*\b(?:stack|err|error)\s*:\s*(?:error|err|e)\s*[,}]/,
|
|
4818
|
+
inherent: true
|
|
4819
|
+
}
|
|
4820
|
+
];
|
|
4821
|
+
|
|
4313
4822
|
// ../../packages/scan/src/types.ts
|
|
4314
4823
|
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
4315
4824
|
function severityRank(severity) {
|
|
@@ -4356,7 +4865,20 @@ var GENERIC_GUARD = (
|
|
|
4356
4865
|
// The identifier must END at the escaper (with at most a known output-context
|
|
4357
4866
|
// suffix). An earlier, looser form also matched `describe(`, which would have
|
|
4358
4867
|
// silenced findings across every test file in every repository.
|
|
4359
|
-
|
|
4868
|
+
// The `Access-Control-` lookbehind is not a nicety. This regex is
|
|
4869
|
+
// case-insensitive, and `Access-Control-Allow-Origin` contains the word
|
|
4870
|
+
// "Allow" followed by a non-word character — so the CORS header name matched
|
|
4871
|
+
// the allow-list heuristic. Because the guard is tested against an 8-line
|
|
4872
|
+
// *window*, one header line silently disabled every guardable rule near it:
|
|
4873
|
+
// in a four-line Express error handler, the header on one line suppressed
|
|
4874
|
+
// both the `none`-algorithm JWT finding and the returned stack trace below
|
|
4875
|
+
// it. The failure mode is the dangerous kind — not fewer findings, none, and
|
|
4876
|
+
// indistinguishable from clean code.
|
|
4877
|
+
//
|
|
4878
|
+
// Scoped to the header prefix rather than to a bare `allow(?!-)`, because
|
|
4879
|
+
// this guard also runs over `.yml` and `.conf` files, where `allow-list:`
|
|
4880
|
+
// and `allowed-hosts:` are ordinary keys that should still guard.
|
|
4881
|
+
/(?<!Access-Control-)\ballow(?:ed|list|_list|ed_hosts)?\b|\bwhitelist\b|\b\w{0,6}[Ee]sc(?:ape)?(?:[Hh]tml|HTML|[Xx]ml|XML|[Ss]ql|[Aa]ttr|[Jj]s|[Uu]ri|[Uu]rl)?\s*\(|\bhtml_escape\b|\bhtmlspecialchars\s*\(|\bsanitiz\w*\b|\bencoded\b|\brealpath\b|\bcommonpath\b|\bresolve\(\)\.startsWith\b|\bprocess\.env\b|\bos\.environ\b|\bgetenv\b|\bENV\s*\[|setObjectInputFilter|ObjectInputFilter/i
|
|
4360
4882
|
);
|
|
4361
4883
|
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
4362
4884
|
var XML_PARSING_FILE = /\b(?:javax\.xml|org\.xml\.sax|org\.w3c\.dom|org\.jdom2?|org\.dom4j|XmlPullParser|DocumentBuilderFactory|DocumentBuilder|SAXParserFactory|SAXParser|XMLInputFactory|XMLReaderFactory|XMLReader|SAXBuilder|SAXReader)\b/;
|
|
@@ -4422,7 +4944,8 @@ var CODE_RULES = [
|
|
|
4422
4944
|
cwe: "CWE-78",
|
|
4423
4945
|
severity: "critical",
|
|
4424
4946
|
languages: ["javascript", "typescript"],
|
|
4425
|
-
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
|
|
4426
4949
|
},
|
|
4427
4950
|
{
|
|
4428
4951
|
id: "py-shell-command-string",
|
|
@@ -4784,6 +5307,20 @@ var CODE_RULES = [
|
|
|
4784
5307
|
consequence: "Catastrophic backtracking: a crafted input of a few dozen characters pins a CPU core for minutes.",
|
|
4785
5308
|
cwe: "CWE-1333",
|
|
4786
5309
|
severity: "medium",
|
|
5310
|
+
// Scoped, because unscoped this rule reads C as if it were a regex.
|
|
5311
|
+
//
|
|
5312
|
+
// `(void *)*memptr64` — a cast to a pointer type, then a dereference — is
|
|
5313
|
+
// the single most ordinary line in a C file, and it matches the first
|
|
5314
|
+
// alternative exactly: `(`, some text, `*`, `)`, `*`. Every `*(char *)*p`
|
|
5315
|
+
// in a codebase became a ReDoS finding. Caught on inspektor-gadget, where
|
|
5316
|
+
// the rule's one and only hit across 1186 files was a `bpf_probe_read_user`
|
|
5317
|
+
// call in an eBPF C program.
|
|
5318
|
+
//
|
|
5319
|
+
// Scoping is the fix rather than a cleverer pattern: C has no regex
|
|
5320
|
+
// literals, so there is nothing here for the rule to find no matter how
|
|
5321
|
+
// the pattern is written. `other` also covers C++, Rust and Zig, which
|
|
5322
|
+
// share the cast-then-deref spelling.
|
|
5323
|
+
languages: ["javascript", "typescript", "python", "ruby", "go", "java", "php"],
|
|
4787
5324
|
pattern: /\([^)\n]*[+*]\s*\)\s*[+*]|\([^)\n]*\{\d+,\}\s*\)\s*[+*{]/
|
|
4788
5325
|
},
|
|
4789
5326
|
// ── Temporary files ──────────────────────────────────────────────────────
|
|
@@ -5307,8 +5844,27 @@ var CODE_RULES = [
|
|
|
5307
5844
|
cwe: "CWE-346",
|
|
5308
5845
|
severity: "high",
|
|
5309
5846
|
languages: ["javascript", "typescript"],
|
|
5310
|
-
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
|
|
5311
|
-
|
|
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*,/
|
|
5867
|
+
},
|
|
5312
5868
|
// A recursive-merge prototype-pollution rule (`target[key] = source[key]`
|
|
5313
5869
|
// with no `__proto__` guard) was built and dropped. The bare copy-by-key is
|
|
5314
5870
|
// the safe allow-listed shape (`updates[field] = body[field]` over an
|
|
@@ -5318,6 +5874,11 @@ var CODE_RULES = [
|
|
|
5318
5874
|
// answer. It flagged legitimate merges in Capacitor and in this repo's own
|
|
5319
5875
|
// web app. `js-prototype-pollution` still catches the explicit `__proto__`
|
|
5320
5876
|
// literal; the recursive-merge case is left to KNOWN_GAPS.
|
|
5877
|
+
// Node-ecosystem classes — vm2, Electron, JWT, archive extraction, headless
|
|
5878
|
+
// browsers — are kept in their own table because each has to know which
|
|
5879
|
+
// package it is looking at before it can claim anything. Folded in here so
|
|
5880
|
+
// there stays exactly one rule list for every consumer to iterate.
|
|
5881
|
+
...NODE_RULES
|
|
5321
5882
|
];
|
|
5322
5883
|
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
5323
5884
|
var DEFINITION_PREFIX = /^\s*(?:(?:export|public|private|protected|static|final|async|abstract)\s+)*(?:def|function|func|class|module|interface|struct|impl|fn|sub)\b/;
|
|
@@ -5370,6 +5931,55 @@ function fileTextOf(lines) {
|
|
|
5370
5931
|
function withoutSingleQuoted(text) {
|
|
5371
5932
|
return text.replace(/'[^'\n]*'/g, "''");
|
|
5372
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
|
+
}
|
|
5373
5983
|
function evaluateRule(rule, ctx) {
|
|
5374
5984
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
5375
5985
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -5381,6 +5991,13 @@ function evaluateRule(rule, ctx) {
|
|
|
5381
5991
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
5382
5992
|
if (rule.requires && !rule.requires.test(context)) return null;
|
|
5383
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
|
+
}
|
|
5384
6001
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
5385
6002
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
5386
6003
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -5392,6 +6009,188 @@ function evaluateRule(rule, ctx) {
|
|
|
5392
6009
|
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
5393
6010
|
}
|
|
5394
6011
|
|
|
6012
|
+
// ../../packages/scan/src/template-rules.ts
|
|
6013
|
+
var LAYOUT_SLOT = /\{\{\{\s*(?:body|content|outlet|children)\s*\}\}\}/;
|
|
6014
|
+
var XSS_CONSEQUENCE = "The value is written into the page as markup rather than as text, so a `<script>` or an `onerror=` attribute in it executes in the visitor\u2019s session \u2014 with their cookies, and their privileges.";
|
|
6015
|
+
var TEMPLATE_RULES = [
|
|
6016
|
+
{
|
|
6017
|
+
id: "tpl-handlebars-unescaped",
|
|
6018
|
+
title: "Handlebars/Mustache interpolation that skips escaping",
|
|
6019
|
+
consequence: XSS_CONSEQUENCE,
|
|
6020
|
+
cwe: "CWE-79",
|
|
6021
|
+
severity: "high",
|
|
6022
|
+
extensions: [".hbs", ".handlebars", ".hdbs", ".mustache", ".ms"],
|
|
6023
|
+
// Two spellings of the same opt-out: the triple-stache and the `&` prefix.
|
|
6024
|
+
pattern: /\{\{\{(?!\{)[^}]*\}\}\}|\{\{\s*&\s*[^}]+\}\}/,
|
|
6025
|
+
lineGuard: LAYOUT_SLOT
|
|
6026
|
+
},
|
|
6027
|
+
{
|
|
6028
|
+
id: "tpl-vue-v-html",
|
|
6029
|
+
title: "`v-html` binding",
|
|
6030
|
+
consequence: XSS_CONSEQUENCE,
|
|
6031
|
+
cwe: "CWE-79",
|
|
6032
|
+
severity: "high",
|
|
6033
|
+
extensions: [".vue", ".html", ".htm"],
|
|
6034
|
+
pattern: /\bv-html\s*=|\s:inner-html\.prop\s*=/
|
|
6035
|
+
},
|
|
6036
|
+
{
|
|
6037
|
+
id: "tpl-pug-unescaped",
|
|
6038
|
+
title: "Pug/Jade unescaped interpolation",
|
|
6039
|
+
consequence: XSS_CONSEQUENCE,
|
|
6040
|
+
cwe: "CWE-79",
|
|
6041
|
+
severity: "high",
|
|
6042
|
+
extensions: [".pug", ".jade"],
|
|
6043
|
+
// `!{…}` is unescaped interpolation anywhere on the line. `!=` is
|
|
6044
|
+
// unescaped buffered code, but only in the tag position — anchored to the
|
|
6045
|
+
// start of the line and preceded by nothing but tag, class and id
|
|
6046
|
+
// characters, so the `!==` inside `- if (a !== b)` cannot reach it.
|
|
6047
|
+
pattern: /!\{[^}\n]*\}|^\s*[\w.#%-]*!=(?!=)\s*\S/
|
|
6048
|
+
},
|
|
6049
|
+
{
|
|
6050
|
+
id: "tpl-ejs-raw-output",
|
|
6051
|
+
title: "EJS/ECT raw output tag",
|
|
6052
|
+
consequence: XSS_CONSEQUENCE,
|
|
6053
|
+
cwe: "CWE-79",
|
|
6054
|
+
severity: "high",
|
|
6055
|
+
extensions: [".ejs", ".ect"],
|
|
6056
|
+
// In EJS `<%= %>` escapes and `<%-` does not, which is the reverse of
|
|
6057
|
+
// Underscore's convention — see KNOWN_GAPS.
|
|
6058
|
+
pattern: /<%-(?!\s*(?:include|-))/,
|
|
6059
|
+
// `<%- include('partial') %>` splices another template, not user data.
|
|
6060
|
+
lineGuard: /<%-\s*(?:include|partial)\s*\(/
|
|
6061
|
+
},
|
|
6062
|
+
{
|
|
6063
|
+
id: "tpl-dust-escape-filter-off",
|
|
6064
|
+
title: "Dust reference with escaping suppressed",
|
|
6065
|
+
consequence: XSS_CONSEQUENCE,
|
|
6066
|
+
cwe: "CWE-79",
|
|
6067
|
+
severity: "high",
|
|
6068
|
+
extensions: [".dust", ".tl"],
|
|
6069
|
+
// The `|s` filter means "suppress the default HTML escape".
|
|
6070
|
+
pattern: /\{[^{}\n]+\|\s*s\s*\}/
|
|
6071
|
+
},
|
|
6072
|
+
{
|
|
6073
|
+
id: "tpl-jinja-safe-filter",
|
|
6074
|
+
title: "Nunjucks/Twig/Jinja `safe` filter or autoescape block off",
|
|
6075
|
+
consequence: XSS_CONSEQUENCE,
|
|
6076
|
+
cwe: "CWE-79",
|
|
6077
|
+
severity: "high",
|
|
6078
|
+
extensions: [".njk", ".nunjucks", ".twig", ".jinja", ".jinja2", ".j2"],
|
|
6079
|
+
pattern: /\|\s*(?:safe|raw)\s*(?:\}\}|\|)|\{%\s*autoescape\s+(?:false|off)\s*%\}/
|
|
6080
|
+
},
|
|
6081
|
+
{
|
|
6082
|
+
id: "tpl-haml-unescaped",
|
|
6083
|
+
title: "Haml unescaped output",
|
|
6084
|
+
consequence: XSS_CONSEQUENCE,
|
|
6085
|
+
cwe: "CWE-79",
|
|
6086
|
+
severity: "high",
|
|
6087
|
+
extensions: [".haml"],
|
|
6088
|
+
pattern: /^\s*[\w.#%-]*!=(?!=)\s*\S/
|
|
6089
|
+
}
|
|
6090
|
+
];
|
|
6091
|
+
var TEMPLATE_EXTENSIONS = new Set(
|
|
6092
|
+
TEMPLATE_RULES.flatMap((rule) => rule.extensions)
|
|
6093
|
+
);
|
|
6094
|
+
function evaluateTemplateRules(extension, lines) {
|
|
6095
|
+
const ext = extension.toLowerCase();
|
|
6096
|
+
const applicable = TEMPLATE_RULES.filter((rule) => rule.extensions.includes(ext));
|
|
6097
|
+
if (applicable.length === 0) return [];
|
|
6098
|
+
const matches = [];
|
|
6099
|
+
lines.forEach((line, index) => {
|
|
6100
|
+
for (const rule of applicable) {
|
|
6101
|
+
if (!rule.pattern.test(line)) continue;
|
|
6102
|
+
if (rule.lineGuard?.test(line)) continue;
|
|
6103
|
+
matches.push({
|
|
6104
|
+
rule,
|
|
6105
|
+
// A template cannot show that the value is untrusted, so the claim
|
|
6106
|
+
// never rises above `pattern` and the shared cap holds it at medium.
|
|
6107
|
+
severity: severityFor(rule.severity, "pattern"),
|
|
6108
|
+
line: index + 1
|
|
6109
|
+
});
|
|
6110
|
+
}
|
|
6111
|
+
});
|
|
6112
|
+
return matches;
|
|
6113
|
+
}
|
|
6114
|
+
|
|
6115
|
+
// ../../packages/scan/src/controls.ts
|
|
6116
|
+
var SERVER_FRAMEWORK = /\bexpress\s*\(\s*\)|\bnew\s+Koa\s*\(|\bfastify\s*\(|\brequire\s*\(\s*['"](?:express|koa|@hapi\/hapi|restify)['"]\s*\)|\bfrom\s+['"](?:express|koa|@hapi\/hapi|restify)['"]|\bhttp\s*\.\s*createServer\s*\(|\bNestFactory\s*\.\s*create\s*\(/;
|
|
6117
|
+
var SECURITY_CONTROLS = [
|
|
6118
|
+
{
|
|
6119
|
+
id: "control-security-headers-absent",
|
|
6120
|
+
title: "no security header middleware",
|
|
6121
|
+
consequence: "Without them the browser applies no framing protection, sniffs content types, sends full referrers cross-origin and never learns to require HTTPS \u2014 a set of defences that cost one line to enable.",
|
|
6122
|
+
cwe: "CWE-693",
|
|
6123
|
+
severity: "medium",
|
|
6124
|
+
evidence: /\bhelmet\b|\bkoa-helmet\b|\b@fastify\/helmet\b|\blusca\b|Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options/i
|
|
6125
|
+
},
|
|
6126
|
+
{
|
|
6127
|
+
id: "control-anti-csrf-absent",
|
|
6128
|
+
title: "no cross-site request forgery protection",
|
|
6129
|
+
consequence: "A cookie-authenticated endpoint with no token check can be driven by a form on any other site \u2014 the browser attaches the session automatically, so the victim only has to visit a page.",
|
|
6130
|
+
cwe: "CWE-352",
|
|
6131
|
+
severity: "medium",
|
|
6132
|
+
// SameSite counts. A cookie the browser refuses to send cross-site is not
|
|
6133
|
+
// reachable by the attack this control exists to stop, so a project that
|
|
6134
|
+
// chose that route instead of tokens has the control, not a gap.
|
|
6135
|
+
evidence: /\bcsurf\b|\bcsrf\b|\bxsrf\b|\blusca\b|\b@fastify\/csrf\b|\bdouble-csrf\b|\bsameSite\s*:\s*['"](?:strict|lax)['"]/i
|
|
6136
|
+
},
|
|
6137
|
+
{
|
|
6138
|
+
id: "control-rate-limiting-absent",
|
|
6139
|
+
title: "no request rate limiting",
|
|
6140
|
+
consequence: "Login and password-reset endpoints with no limiter can be tried at the speed of the network \u2014 credential stuffing, token brute force and enumeration all become a matter of waiting.",
|
|
6141
|
+
cwe: "CWE-770",
|
|
6142
|
+
severity: "medium",
|
|
6143
|
+
evidence: /\bexpress-rate-limit\b|\brateLimit\b|\brate-limiter\b|\bratelimit\b|\bexpress-slow-down\b|\bslowDown\b|\bbottleneck\b|\bthrottle\b/i
|
|
6144
|
+
},
|
|
6145
|
+
{
|
|
6146
|
+
id: "control-body-size-limit-absent",
|
|
6147
|
+
title: "no request body size limit",
|
|
6148
|
+
consequence: "The body is parsed into memory before any handler \u2014 and before any authentication check \u2014 so unbounded parsing lets a few concurrent requests exhaust the process.",
|
|
6149
|
+
cwe: "CWE-400",
|
|
6150
|
+
severity: "medium",
|
|
6151
|
+
evidence: /\blimit\s*:\s*['"]?\d+\s*(?:kb|mb|b)\b|\bbodyLimit\b|\bmaxRequestBodySize\b|\bclient_max_body_size\b/i
|
|
6152
|
+
}
|
|
6153
|
+
];
|
|
6154
|
+
var ControlAudit = class {
|
|
6155
|
+
seen = /* @__PURE__ */ new Set();
|
|
6156
|
+
/** Where the server is built. Anchors the findings somewhere meaningful. */
|
|
6157
|
+
serverFile = null;
|
|
6158
|
+
observe(relativePath, text) {
|
|
6159
|
+
if (this.serverFile === null && SERVER_FRAMEWORK.test(text)) {
|
|
6160
|
+
this.serverFile = relativePath;
|
|
6161
|
+
}
|
|
6162
|
+
for (const control of SECURITY_CONTROLS) {
|
|
6163
|
+
if (this.seen.has(control.id)) continue;
|
|
6164
|
+
if (control.evidence.test(text)) this.seen.add(control.id);
|
|
6165
|
+
}
|
|
6166
|
+
}
|
|
6167
|
+
/** Controls with no evidence anywhere. Empty when the tree serves no HTTP. */
|
|
6168
|
+
missing() {
|
|
6169
|
+
if (this.serverFile === null) return [];
|
|
6170
|
+
return SECURITY_CONTROLS.filter((control) => !this.seen.has(control.id));
|
|
6171
|
+
}
|
|
6172
|
+
findings() {
|
|
6173
|
+
const anchor = this.serverFile;
|
|
6174
|
+
if (anchor === null) return [];
|
|
6175
|
+
return this.missing().map((control) => ({
|
|
6176
|
+
ruleId: control.id,
|
|
6177
|
+
title: control.title,
|
|
6178
|
+
file: anchor,
|
|
6179
|
+
line: 1,
|
|
6180
|
+
// `pattern` is doing real work here: it caps the severity, and it says
|
|
6181
|
+
// in the report itself how much the scanner is claiming. An absence is
|
|
6182
|
+
// never `evidence`.
|
|
6183
|
+
severity: severityFor(control.severity, "pattern"),
|
|
6184
|
+
confidence: "pattern",
|
|
6185
|
+
message: `${control.title} \u2014 no evidence of one anywhere in the scanned tree (${control.cwe})`,
|
|
6186
|
+
consequence: control.consequence,
|
|
6187
|
+
cwe: control.cwe,
|
|
6188
|
+
excerpt: "",
|
|
6189
|
+
category: "code"
|
|
6190
|
+
}));
|
|
6191
|
+
}
|
|
6192
|
+
};
|
|
6193
|
+
|
|
5395
6194
|
// ../../packages/scan/src/manifest-rules.ts
|
|
5396
6195
|
var POPULAR_NPM = [
|
|
5397
6196
|
"react",
|
|
@@ -5781,7 +6580,8 @@ var SECRET_RULES = [
|
|
|
5781
6580
|
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
5782
6581
|
severity: "medium",
|
|
5783
6582
|
cwe: "CWE-798",
|
|
5784
|
-
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
|
|
5785
6585
|
},
|
|
5786
6586
|
{
|
|
5787
6587
|
id: "secret-generic-api-key",
|
|
@@ -5792,7 +6592,8 @@ var SECRET_RULES = [
|
|
|
5792
6592
|
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
5793
6593
|
severity: "high",
|
|
5794
6594
|
cwe: "CWE-798",
|
|
5795
|
-
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
|
|
5796
6597
|
},
|
|
5797
6598
|
{
|
|
5798
6599
|
id: "secret-generic-credential",
|
|
@@ -5800,7 +6601,8 @@ var SECRET_RULES = [
|
|
|
5800
6601
|
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
5801
6602
|
severity: "high",
|
|
5802
6603
|
cwe: "CWE-798",
|
|
5803
|
-
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
|
|
5804
6606
|
},
|
|
5805
6607
|
{
|
|
5806
6608
|
id: "secret-hex-token",
|
|
@@ -5808,7 +6610,8 @@ var SECRET_RULES = [
|
|
|
5808
6610
|
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
5809
6611
|
severity: "medium",
|
|
5810
6612
|
cwe: "CWE-798",
|
|
5811
|
-
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
|
|
5812
6615
|
}
|
|
5813
6616
|
];
|
|
5814
6617
|
var KNOWN_PLACEHOLDERS = [
|
|
@@ -5823,11 +6626,70 @@ var KNOWN_PLACEHOLDERS = [
|
|
|
5823
6626
|
/\bEXAMPLE_?KEY\b/i,
|
|
5824
6627
|
/\bYOUR_[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)\b/,
|
|
5825
6628
|
/\b(?:xxx+|X{4,}|\*{4,}|<[a-z-]+>)\b/,
|
|
5826
|
-
/\bchangeme\b/i
|
|
6629
|
+
/\bchangeme\b/i,
|
|
6630
|
+
// The metasyntactic pair in a connection string, `postgres://user:pass@host`.
|
|
6631
|
+
// This is not the AWS case above and the distinction is the whole reason it
|
|
6632
|
+
// is allowed: `AKIAIOSFODNN7EXAMPLE` is a real credential *format* carrying a
|
|
6633
|
+
// fake value, so it arrives by way of a pasted template. `user:pass` is the
|
|
6634
|
+
// English words sitting where a credential goes, which is how every database
|
|
6635
|
+
// driver writes its DSN in its own README, and nobody pastes that out of a
|
|
6636
|
+
// secret manager.
|
|
6637
|
+
//
|
|
6638
|
+
// Both halves have to be metasyntactic. `root:hunter2@` is not exempt, since
|
|
6639
|
+
// a real password beside a common username is the case this must not swallow.
|
|
6640
|
+
/:\/\/(?:user(?:name)?|admin|root|dbuser|myuser):(?:pass(?:word|wd)?|secret|dbpass|mypassword)@/i
|
|
5827
6641
|
];
|
|
5828
6642
|
function isKnownPlaceholder(text) {
|
|
5829
6643
|
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
5830
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
|
+
}
|
|
5831
6693
|
function redactSecret(line) {
|
|
5832
6694
|
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
5833
6695
|
if (match.length <= 12) return match;
|
|
@@ -5928,7 +6790,12 @@ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
5928
6790
|
".erb",
|
|
5929
6791
|
".ejs",
|
|
5930
6792
|
".vue",
|
|
5931
|
-
".svelte"
|
|
6793
|
+
".svelte",
|
|
6794
|
+
// Template files, so the engine in `template-rules.ts` has something to
|
|
6795
|
+
// read. Sourced from the rules themselves rather than repeated here: an
|
|
6796
|
+
// engine added there becomes scannable without a second edit that could be
|
|
6797
|
+
// forgotten, which is how a rule ends up quietly never firing.
|
|
6798
|
+
...TEMPLATE_EXTENSIONS
|
|
5932
6799
|
]);
|
|
5933
6800
|
var LANGUAGE_BY_EXTENSION = {
|
|
5934
6801
|
".js": "javascript",
|
|
@@ -6000,6 +6867,10 @@ function languageOfShebang(firstLine) {
|
|
|
6000
6867
|
}
|
|
6001
6868
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
6002
6869
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
6870
|
+
var FOREIGN_CREDENTIAL = /\b(?:nolint:[\w,]*gosec|nosec)\b[^\n]*\bG101\b|\bG101\b[^\n]*\b(?:nolint:[\w,]*gosec|nosec)\b/;
|
|
6871
|
+
function foreignCredentialMark(line) {
|
|
6872
|
+
return FOREIGN_CREDENTIAL.test(line);
|
|
6873
|
+
}
|
|
6003
6874
|
function collectSuppressions(lines) {
|
|
6004
6875
|
const byLine = /* @__PURE__ */ new Map();
|
|
6005
6876
|
let count = 0;
|
|
@@ -6037,16 +6908,21 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6037
6908
|
if (!match) continue;
|
|
6038
6909
|
if (isKnownPlaceholder(match[0])) continue;
|
|
6039
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;
|
|
6914
|
+
const marked = foreignCredentialMark(line);
|
|
6040
6915
|
findings.push({
|
|
6041
6916
|
ruleId: rule.id,
|
|
6042
6917
|
title: rule.name,
|
|
6043
6918
|
file: relativePath,
|
|
6044
6919
|
line: index + 1,
|
|
6045
|
-
// Reported but not blocking in tests — see isTestPath.
|
|
6046
|
-
|
|
6920
|
+
// Reported but not blocking in tests — see isTestPath. The same goes
|
|
6921
|
+
// for a line another linter's credential rule was already told about.
|
|
6922
|
+
severity: inTests || marked ? "low" : rule.severity,
|
|
6047
6923
|
// A matched credential format is the finding, not a proxy for one.
|
|
6048
6924
|
confidence: "evidence",
|
|
6049
|
-
message: inTests ? `Possible ${rule.name} detected in a test file \u2014 usually a fixture, still worth confirming it is not a live credential` : `Possible ${rule.name} detected`,
|
|
6925
|
+
message: inTests ? `Possible ${rule.name} detected in a test file \u2014 usually a fixture, still worth confirming it is not a live credential` : marked ? `Possible ${rule.name} detected on a line already marked as a false positive for another linter's credential rule` : `Possible ${rule.name} detected`,
|
|
6050
6926
|
consequence: rule.consequence,
|
|
6051
6927
|
cwe: rule.cwe,
|
|
6052
6928
|
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
@@ -6076,6 +6952,23 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
6076
6952
|
});
|
|
6077
6953
|
}
|
|
6078
6954
|
});
|
|
6955
|
+
for (const match of evaluateTemplateRules(extensionOf(relativePath), lines)) {
|
|
6956
|
+
const index = match.line - 1;
|
|
6957
|
+
if (isSuppressed(suppressions, index, match.rule.id)) continue;
|
|
6958
|
+
findings.push({
|
|
6959
|
+
ruleId: match.rule.id,
|
|
6960
|
+
title: match.rule.title,
|
|
6961
|
+
file: relativePath,
|
|
6962
|
+
line: match.line,
|
|
6963
|
+
severity: match.severity,
|
|
6964
|
+
confidence: "pattern",
|
|
6965
|
+
message: `${match.rule.title} (${match.rule.cwe})`,
|
|
6966
|
+
consequence: match.rule.consequence,
|
|
6967
|
+
cwe: match.rule.cwe,
|
|
6968
|
+
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
6969
|
+
category: "code"
|
|
6970
|
+
});
|
|
6971
|
+
}
|
|
6079
6972
|
return findings;
|
|
6080
6973
|
}
|
|
6081
6974
|
function scanManifest(relativePath, filename, text) {
|
|
@@ -6096,8 +6989,8 @@ function scanManifest(relativePath, filename, text) {
|
|
|
6096
6989
|
}
|
|
6097
6990
|
|
|
6098
6991
|
// ../../packages/scan/src/node/walk.ts
|
|
6099
|
-
var
|
|
6100
|
-
var
|
|
6992
|
+
var import_node_fs12 = require("fs");
|
|
6993
|
+
var import_node_path7 = require("path");
|
|
6101
6994
|
function compileExcludes(patterns) {
|
|
6102
6995
|
const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
|
|
6103
6996
|
if (matchers.length === 0) return () => false;
|
|
@@ -6168,7 +7061,7 @@ function matchPrefix(parts, segs) {
|
|
|
6168
7061
|
}
|
|
6169
7062
|
function readIgnoreFile(root) {
|
|
6170
7063
|
try {
|
|
6171
|
-
return (0,
|
|
7064
|
+
return (0, import_node_fs12.readFileSync)((0, import_node_path7.join)(root, ".threatcrushignore"), "utf-8").split("\n");
|
|
6172
7065
|
} catch {
|
|
6173
7066
|
return [];
|
|
6174
7067
|
}
|
|
@@ -6177,22 +7070,23 @@ function scanPath(targetPath, options = {}) {
|
|
|
6177
7070
|
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
6178
7071
|
const allowed = options.categories ? new Set(options.categories) : null;
|
|
6179
7072
|
const findings = [];
|
|
7073
|
+
const controls = new ControlAudit();
|
|
6180
7074
|
const unreadable = [];
|
|
6181
7075
|
let filesScanned = 0;
|
|
6182
7076
|
let suppressed = 0;
|
|
6183
7077
|
let excluded = 0;
|
|
6184
7078
|
const rootIsDirectory = (() => {
|
|
6185
7079
|
try {
|
|
6186
|
-
return (0,
|
|
7080
|
+
return (0, import_node_fs12.statSync)(targetPath).isDirectory();
|
|
6187
7081
|
} catch {
|
|
6188
7082
|
return true;
|
|
6189
7083
|
}
|
|
6190
7084
|
})();
|
|
6191
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
7085
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path7.dirname)(targetPath);
|
|
6192
7086
|
const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
|
|
6193
7087
|
const scanFile = (fullPath, filename) => {
|
|
6194
7088
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
6195
|
-
const extension = (0,
|
|
7089
|
+
const extension = (0, import_node_path7.extname)(filename).toLowerCase();
|
|
6196
7090
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
6197
7091
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
6198
7092
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -6204,32 +7098,33 @@ function scanPath(targetPath, options = {}) {
|
|
|
6204
7098
|
let handle;
|
|
6205
7099
|
let declared = null;
|
|
6206
7100
|
try {
|
|
6207
|
-
handle = (0,
|
|
7101
|
+
handle = (0, import_node_fs12.openSync)(fullPath, "r");
|
|
6208
7102
|
} catch {
|
|
6209
7103
|
unreadable.push(relativePath);
|
|
6210
7104
|
return;
|
|
6211
7105
|
}
|
|
6212
7106
|
try {
|
|
6213
|
-
if ((0,
|
|
7107
|
+
if ((0, import_node_fs12.fstatSync)(handle).size > maxFileBytes) return;
|
|
6214
7108
|
if (mayDeclareInterpreter) {
|
|
6215
7109
|
const prefix = Buffer.alloc(128);
|
|
6216
|
-
const read = (0,
|
|
7110
|
+
const read = (0, import_node_fs12.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
6217
7111
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
6218
7112
|
if (!declared) return;
|
|
6219
7113
|
}
|
|
6220
|
-
text = (0,
|
|
7114
|
+
text = (0, import_node_fs12.readFileSync)(handle, "utf-8");
|
|
6221
7115
|
} catch {
|
|
6222
7116
|
unreadable.push(relativePath);
|
|
6223
7117
|
return;
|
|
6224
7118
|
} finally {
|
|
6225
7119
|
try {
|
|
6226
|
-
(0,
|
|
7120
|
+
(0, import_node_fs12.closeSync)(handle);
|
|
6227
7121
|
} catch {
|
|
6228
7122
|
}
|
|
6229
7123
|
}
|
|
6230
7124
|
filesScanned += 1;
|
|
6231
7125
|
options.onFile?.(relativePath);
|
|
6232
7126
|
suppressed += collectSuppressions(text.split("\n")).count;
|
|
7127
|
+
controls.observe(relativePath, text);
|
|
6233
7128
|
const fileFindings = [
|
|
6234
7129
|
...scanText(relativePath, text, declared ?? languageOf(filename)),
|
|
6235
7130
|
...isManifest ? scanManifest(relativePath, filename, text) : []
|
|
@@ -6240,13 +7135,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
6240
7135
|
const walk = (currentPath) => {
|
|
6241
7136
|
let entries;
|
|
6242
7137
|
try {
|
|
6243
|
-
entries = (0,
|
|
7138
|
+
entries = (0, import_node_fs12.readdirSync)(currentPath, { withFileTypes: true });
|
|
6244
7139
|
} catch {
|
|
6245
7140
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
6246
7141
|
return;
|
|
6247
7142
|
}
|
|
6248
7143
|
for (const entry of entries) {
|
|
6249
|
-
const fullPath = (0,
|
|
7144
|
+
const fullPath = (0, import_node_path7.join)(currentPath, entry.name);
|
|
6250
7145
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
6251
7146
|
if (entry.isDirectory()) {
|
|
6252
7147
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
@@ -6270,8 +7165,9 @@ function scanPath(targetPath, options = {}) {
|
|
|
6270
7165
|
} else if (isExcluded(toRelative(walkRoot, targetPath))) {
|
|
6271
7166
|
excluded += 1;
|
|
6272
7167
|
} else {
|
|
6273
|
-
scanFile(targetPath, (0,
|
|
7168
|
+
scanFile(targetPath, (0, import_node_path7.basename)(targetPath));
|
|
6274
7169
|
}
|
|
7170
|
+
if (options.missingControls) findings.push(...controls.findings());
|
|
6275
7171
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
6276
7172
|
filtered.sort(
|
|
6277
7173
|
(a, b) => severityRank(b.severity) - severityRank(a.severity) || a.file.localeCompare(b.file) || a.line - b.line
|
|
@@ -6301,32 +7197,37 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
6301
7197
|
}
|
|
6302
7198
|
}
|
|
6303
7199
|
function toRelative(base, target) {
|
|
6304
|
-
const rel = (0,
|
|
6305
|
-
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("/");
|
|
6306
7202
|
}
|
|
6307
7203
|
|
|
6308
7204
|
// ../../packages/scan/src/node/dependencies.ts
|
|
6309
|
-
var
|
|
6310
|
-
var
|
|
6311
|
-
var LOCKFILES = [
|
|
6312
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
6313
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
6314
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
6315
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
6316
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
6317
|
-
];
|
|
7205
|
+
var import_node_fs13 = require("fs");
|
|
7206
|
+
var import_node_path8 = require("path");
|
|
6318
7207
|
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
6319
7208
|
async function scanDependencies(targetPath) {
|
|
6320
7209
|
const findings = [];
|
|
6321
|
-
for (const { file, ecosystem } of LOCKFILES) {
|
|
6322
|
-
const lockPath = (0,
|
|
6323
|
-
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;
|
|
6324
7213
|
let deps;
|
|
6325
7214
|
try {
|
|
6326
|
-
deps =
|
|
7215
|
+
deps = dedupe(parse((0, import_node_fs13.readFileSync)(lockPath, "utf-8")));
|
|
6327
7216
|
} catch {
|
|
6328
7217
|
continue;
|
|
6329
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
|
+
}
|
|
6330
7231
|
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
6331
7232
|
let vulns;
|
|
6332
7233
|
try {
|
|
@@ -6353,6 +7254,20 @@ async function scanDependencies(targetPath) {
|
|
|
6353
7254
|
}
|
|
6354
7255
|
return findings;
|
|
6355
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
|
+
}
|
|
6356
7271
|
function severityFromCvss(score) {
|
|
6357
7272
|
if (!score) return "medium";
|
|
6358
7273
|
const value = Number.parseFloat(score);
|
|
@@ -6362,26 +7277,131 @@ function severityFromCvss(score) {
|
|
|
6362
7277
|
if (value >= 4) return "medium";
|
|
6363
7278
|
return "low";
|
|
6364
7279
|
}
|
|
6365
|
-
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) {
|
|
6366
7315
|
const deps = [];
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
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;
|
|
6374
7339
|
}
|
|
6375
|
-
|
|
7340
|
+
const version = exactVersion(rawVersion.replace(/_.*$/, ""));
|
|
7341
|
+
if (version) deps.push({ name, version });
|
|
6376
7342
|
}
|
|
6377
|
-
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
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;
|
|
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;
|
|
7367
|
+
}
|
|
7368
|
+
return deps;
|
|
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 });
|
|
6381
7379
|
}
|
|
6382
7380
|
}
|
|
6383
7381
|
return deps;
|
|
6384
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
|
+
);
|
|
6385
7405
|
function isValidPackageName(name) {
|
|
6386
7406
|
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
6387
7407
|
}
|
|
@@ -6406,17 +7426,17 @@ async function queryOsv(name, version, ecosystem) {
|
|
|
6406
7426
|
}
|
|
6407
7427
|
|
|
6408
7428
|
// ../../packages/scan/src/node/sarif.ts
|
|
6409
|
-
var
|
|
6410
|
-
var
|
|
7429
|
+
var import_node_crypto3 = require("crypto");
|
|
7430
|
+
var import_node_path9 = require("path");
|
|
6411
7431
|
|
|
6412
7432
|
// src/commands/scan.ts
|
|
6413
7433
|
function readVersion() {
|
|
6414
7434
|
for (const candidate of [
|
|
6415
|
-
(0,
|
|
6416
|
-
(0,
|
|
7435
|
+
(0, import_node_path10.join)(__dirname, "..", "package.json"),
|
|
7436
|
+
(0, import_node_path10.join)(__dirname, "..", "..", "package.json")
|
|
6417
7437
|
]) {
|
|
6418
7438
|
try {
|
|
6419
|
-
return JSON.parse((0,
|
|
7439
|
+
return JSON.parse((0, import_node_fs14.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
6420
7440
|
} catch {
|
|
6421
7441
|
}
|
|
6422
7442
|
}
|
|
@@ -6877,8 +7897,8 @@ var RuleEngine = class {
|
|
|
6877
7897
|
};
|
|
6878
7898
|
|
|
6879
7899
|
// src/daemon/rules/loader.ts
|
|
6880
|
-
var
|
|
6881
|
-
var
|
|
7900
|
+
var import_node_fs15 = require("fs");
|
|
7901
|
+
var import_node_path11 = require("path");
|
|
6882
7902
|
|
|
6883
7903
|
// src/daemon/rules/default-rules.ts
|
|
6884
7904
|
var DEFAULT_RULES = [
|
|
@@ -7162,11 +8182,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
7162
8182
|
function loadAllRules(customDir) {
|
|
7163
8183
|
const rules = [...DEFAULT_RULES];
|
|
7164
8184
|
const dir = customDir || RULES_DIR;
|
|
7165
|
-
if ((0,
|
|
7166
|
-
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"));
|
|
7167
8187
|
for (const file of files) {
|
|
7168
8188
|
try {
|
|
7169
|
-
const raw = (0,
|
|
8189
|
+
const raw = (0, import_node_fs15.readFileSync)((0, import_node_path11.join)(dir, file), "utf-8");
|
|
7170
8190
|
const parsed = JSON.parse(raw);
|
|
7171
8191
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
7172
8192
|
for (const rule of customRules) {
|
|
@@ -7329,7 +8349,7 @@ function detectFirewallAdapter() {
|
|
|
7329
8349
|
}
|
|
7330
8350
|
|
|
7331
8351
|
// src/daemon/firewall/remediation.ts
|
|
7332
|
-
var
|
|
8352
|
+
var import_node_fs16 = require("fs");
|
|
7333
8353
|
var DEFAULT_CONFIG2 = {
|
|
7334
8354
|
enabled: true,
|
|
7335
8355
|
dry_run: true,
|
|
@@ -7484,7 +8504,7 @@ var RemediationManager = class {
|
|
|
7484
8504
|
}
|
|
7485
8505
|
logLine(line) {
|
|
7486
8506
|
try {
|
|
7487
|
-
(0,
|
|
8507
|
+
(0, import_node_fs16.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
7488
8508
|
`);
|
|
7489
8509
|
} catch {
|
|
7490
8510
|
}
|
|
@@ -7540,7 +8560,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
7540
8560
|
// src/daemon/index.ts
|
|
7541
8561
|
function readVersion2() {
|
|
7542
8562
|
try {
|
|
7543
|
-
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"));
|
|
7544
8564
|
return pkg.version || "0.0.0";
|
|
7545
8565
|
} catch {
|
|
7546
8566
|
return "0.0.0";
|
|
@@ -7548,7 +8568,7 @@ function readVersion2() {
|
|
|
7548
8568
|
}
|
|
7549
8569
|
function logLine(line) {
|
|
7550
8570
|
try {
|
|
7551
|
-
(0,
|
|
8571
|
+
(0, import_node_fs17.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
7552
8572
|
`);
|
|
7553
8573
|
} catch {
|
|
7554
8574
|
}
|
|
@@ -7576,7 +8596,7 @@ async function runDaemon() {
|
|
|
7576
8596
|
} catch (err) {
|
|
7577
8597
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
7578
8598
|
}
|
|
7579
|
-
const config = loadConfig((0,
|
|
8599
|
+
const config = loadConfig((0, import_node_fs17.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
7580
8600
|
bus.on("event", (event) => {
|
|
7581
8601
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
7582
8602
|
});
|