@profullstack/threatcrush 0.2.2 → 0.4.0
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/README.md +1 -0
- package/dist/daemon.js +1461 -222
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1998 -473
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
package/dist/daemon.js
CHANGED
|
@@ -1639,20 +1639,20 @@ var require_parse_async = __commonJS({
|
|
|
1639
1639
|
const index = 0;
|
|
1640
1640
|
const blocksize = opts.blocksize || 40960;
|
|
1641
1641
|
const parser = new TOMLParser();
|
|
1642
|
-
return new Promise((
|
|
1643
|
-
setImmediate(parseAsyncNext, index, blocksize,
|
|
1642
|
+
return new Promise((resolve3, reject) => {
|
|
1643
|
+
setImmediate(parseAsyncNext, index, blocksize, resolve3, reject);
|
|
1644
1644
|
});
|
|
1645
|
-
function parseAsyncNext(index2, blocksize2,
|
|
1645
|
+
function parseAsyncNext(index2, blocksize2, resolve3, reject) {
|
|
1646
1646
|
if (index2 >= str.length) {
|
|
1647
1647
|
try {
|
|
1648
|
-
return
|
|
1648
|
+
return resolve3(parser.finish());
|
|
1649
1649
|
} catch (err) {
|
|
1650
1650
|
return reject(prettyError(err, str));
|
|
1651
1651
|
}
|
|
1652
1652
|
}
|
|
1653
1653
|
try {
|
|
1654
1654
|
parser.parse(str.slice(index2, index2 + blocksize2));
|
|
1655
|
-
setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2,
|
|
1655
|
+
setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2, resolve3, reject);
|
|
1656
1656
|
} catch (err) {
|
|
1657
1657
|
reject(prettyError(err, str));
|
|
1658
1658
|
}
|
|
@@ -1678,7 +1678,7 @@ var require_parse_stream = __commonJS({
|
|
|
1678
1678
|
function parseReadable(stm) {
|
|
1679
1679
|
const parser = new TOMLParser();
|
|
1680
1680
|
stm.setEncoding("utf8");
|
|
1681
|
-
return new Promise((
|
|
1681
|
+
return new Promise((resolve3, reject) => {
|
|
1682
1682
|
let readable;
|
|
1683
1683
|
let ended = false;
|
|
1684
1684
|
let errored = false;
|
|
@@ -1686,7 +1686,7 @@ var require_parse_stream = __commonJS({
|
|
|
1686
1686
|
ended = true;
|
|
1687
1687
|
if (readable) return;
|
|
1688
1688
|
try {
|
|
1689
|
-
|
|
1689
|
+
resolve3(parser.finish());
|
|
1690
1690
|
} catch (err) {
|
|
1691
1691
|
reject(err);
|
|
1692
1692
|
}
|
|
@@ -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_fs15 = require("fs");
|
|
2026
|
+
var import_node_path10 = require("path");
|
|
2027
2027
|
|
|
2028
2028
|
// src/daemon/paths.ts
|
|
2029
2029
|
var import_node_fs = require("fs");
|
|
@@ -2310,7 +2310,7 @@ var IpcServer = class {
|
|
|
2310
2310
|
} catch {
|
|
2311
2311
|
}
|
|
2312
2312
|
}
|
|
2313
|
-
return new Promise((
|
|
2313
|
+
return new Promise((resolve3, reject) => {
|
|
2314
2314
|
this.server = (0, import_node_net.createServer)((sock) => this.handleClient(sock));
|
|
2315
2315
|
this.server.on("error", reject);
|
|
2316
2316
|
this.server.listen(PATHS.socket, () => {
|
|
@@ -2327,7 +2327,7 @@ var IpcServer = class {
|
|
|
2327
2327
|
} catch {
|
|
2328
2328
|
}
|
|
2329
2329
|
}
|
|
2330
|
-
|
|
2330
|
+
resolve3();
|
|
2331
2331
|
});
|
|
2332
2332
|
});
|
|
2333
2333
|
}
|
|
@@ -2339,20 +2339,20 @@ var IpcServer = class {
|
|
|
2339
2339
|
}
|
|
2340
2340
|
}
|
|
2341
2341
|
this.clients.clear();
|
|
2342
|
-
return new Promise((
|
|
2342
|
+
return new Promise((resolve3) => {
|
|
2343
2343
|
if (!this.server) {
|
|
2344
2344
|
try {
|
|
2345
2345
|
if ((0, import_node_fs3.existsSync)(PATHS.socket)) (0, import_node_fs3.unlinkSync)(PATHS.socket);
|
|
2346
2346
|
} catch {
|
|
2347
2347
|
}
|
|
2348
|
-
return
|
|
2348
|
+
return resolve3();
|
|
2349
2349
|
}
|
|
2350
2350
|
this.server.close(() => {
|
|
2351
2351
|
try {
|
|
2352
2352
|
if ((0, import_node_fs3.existsSync)(PATHS.socket)) (0, import_node_fs3.unlinkSync)(PATHS.socket);
|
|
2353
2353
|
} catch {
|
|
2354
2354
|
}
|
|
2355
|
-
|
|
2355
|
+
resolve3();
|
|
2356
2356
|
});
|
|
2357
2357
|
});
|
|
2358
2358
|
}
|
|
@@ -2469,6 +2469,7 @@ var NGINX_REGEX = /^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d{3}) (\d+) "
|
|
|
2469
2469
|
var AUTH_REGEX = /^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(\S+?)(?:\[\d+\])?:\s+(.*)/;
|
|
2470
2470
|
var SYSLOG_REGEX = /^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(\S+?)(?:\[\d+\])?:\s+(.*)/;
|
|
2471
2471
|
var IP_REGEX = /(?:from|FROM)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/;
|
|
2472
|
+
var INVALID_USER_REGEX = /(?:for\s+invalid\s+user)\s+(\S+?)(?:\s+from|\s*$)/;
|
|
2472
2473
|
var USER_REGEX = /(?:for|user)\s+(\S+?)(?:\s+from|\s*$)/;
|
|
2473
2474
|
var ATTACK_PATTERNS = {
|
|
2474
2475
|
sqli: [
|
|
@@ -2522,7 +2523,7 @@ function parseAuthLog(line) {
|
|
|
2522
2523
|
const match = line.match(AUTH_REGEX);
|
|
2523
2524
|
if (!match) return null;
|
|
2524
2525
|
const ipMatch = match[3].match(IP_REGEX);
|
|
2525
|
-
const userMatch = match[3].match(USER_REGEX);
|
|
2526
|
+
const userMatch = match[3].match(INVALID_USER_REGEX) || match[3].match(USER_REGEX);
|
|
2526
2527
|
return {
|
|
2527
2528
|
timestamp: parseSyslogTimestamp(match[1]),
|
|
2528
2529
|
raw: line,
|
|
@@ -2550,10 +2551,25 @@ function parseSyslog(line) {
|
|
|
2550
2551
|
};
|
|
2551
2552
|
}
|
|
2552
2553
|
function detectAttackPattern(path) {
|
|
2554
|
+
const candidates = /* @__PURE__ */ new Set([path]);
|
|
2555
|
+
let current = path;
|
|
2556
|
+
for (let i = 0; i < 2; i++) {
|
|
2557
|
+
let decoded = null;
|
|
2558
|
+
try {
|
|
2559
|
+
decoded = decodeURIComponent(current);
|
|
2560
|
+
} catch {
|
|
2561
|
+
decoded = null;
|
|
2562
|
+
}
|
|
2563
|
+
if (decoded === null || decoded === current) break;
|
|
2564
|
+
candidates.add(decoded);
|
|
2565
|
+
current = decoded;
|
|
2566
|
+
}
|
|
2553
2567
|
for (const [type, patterns] of Object.entries(ATTACK_PATTERNS)) {
|
|
2554
2568
|
for (const pattern of patterns) {
|
|
2555
|
-
|
|
2556
|
-
|
|
2569
|
+
for (const candidate of candidates) {
|
|
2570
|
+
if (pattern.test(candidate)) {
|
|
2571
|
+
return type;
|
|
2572
|
+
}
|
|
2557
2573
|
}
|
|
2558
2574
|
}
|
|
2559
2575
|
}
|
|
@@ -2567,20 +2583,19 @@ function autoDetectParser(line) {
|
|
|
2567
2583
|
return parseSyslog(line);
|
|
2568
2584
|
}
|
|
2569
2585
|
function parseNginxTimestamp(s) {
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
} catch {
|
|
2574
|
-
return /* @__PURE__ */ new Date();
|
|
2575
|
-
}
|
|
2586
|
+
const cleaned = s.replace(/(\d{2})\/(\w{3})\/(\d{4}):/, "$2 $1, $3 ");
|
|
2587
|
+
const d = new Date(cleaned);
|
|
2588
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
2576
2589
|
}
|
|
2577
2590
|
function parseSyslogTimestamp(s) {
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2583
|
-
|
|
2591
|
+
const now = /* @__PURE__ */ new Date();
|
|
2592
|
+
let d = /* @__PURE__ */ new Date(`${s} ${now.getFullYear()}`);
|
|
2593
|
+
if (Number.isNaN(d.getTime())) return now;
|
|
2594
|
+
if (d.getTime() > now.getTime()) {
|
|
2595
|
+
const prev = /* @__PURE__ */ new Date(`${s} ${now.getFullYear() - 1}`);
|
|
2596
|
+
if (!Number.isNaN(prev.getTime())) d = prev;
|
|
2597
|
+
}
|
|
2598
|
+
return d;
|
|
2584
2599
|
}
|
|
2585
2600
|
|
|
2586
2601
|
// src/daemon/watchers/log-watcher.ts
|
|
@@ -3768,8 +3783,8 @@ function authHeaders() {
|
|
|
3768
3783
|
}
|
|
3769
3784
|
|
|
3770
3785
|
// src/commands/scan.ts
|
|
3771
|
-
var
|
|
3772
|
-
var
|
|
3786
|
+
var import_node_fs12 = require("fs");
|
|
3787
|
+
var import_node_path8 = require("path");
|
|
3773
3788
|
|
|
3774
3789
|
// ../../node_modules/.pnpm/chalk@5.6.2/node_modules/chalk/source/vendor/ansi-styles/index.js
|
|
3775
3790
|
var ANSI_BACKGROUND_OFFSET = 10;
|
|
@@ -4295,58 +4310,1134 @@ function workerId() {
|
|
|
4295
4310
|
return `${import_node_os4.default.hostname()}/${process.pid}`;
|
|
4296
4311
|
}
|
|
4297
4312
|
|
|
4298
|
-
// src/
|
|
4299
|
-
var
|
|
4300
|
-
|
|
4301
|
-
|
|
4302
|
-
{
|
|
4303
|
-
{
|
|
4304
|
-
{
|
|
4305
|
-
{
|
|
4306
|
-
{
|
|
4307
|
-
{ name: "Slack Token", pattern: /xox[bpors]-[A-Za-z0-9-]{10,}/g, severity: "critical" },
|
|
4308
|
-
{ name: "Stripe Key", pattern: /(?:sk_live_|pk_live_|sk_test_|pk_test_)[A-Za-z0-9]{20,}/g, severity: "critical" },
|
|
4309
|
-
{ name: "Database URL", pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^\s'"]+/gi, severity: "high" },
|
|
4310
|
-
{ name: "Bearer Token", pattern: /Bearer\s+[A-Za-z0-9\-_\.]{20,}/g, severity: "medium" },
|
|
4311
|
-
{ name: "Hex Token (32+)", pattern: /(?:token|key|secret|auth)\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/gi, severity: "medium" }
|
|
4313
|
+
// src/scan/dependencies.ts
|
|
4314
|
+
var import_node_fs10 = require("fs");
|
|
4315
|
+
var import_node_path5 = require("path");
|
|
4316
|
+
var LOCKFILES = [
|
|
4317
|
+
{ file: "package-lock.json", ecosystem: "npm" },
|
|
4318
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
4319
|
+
{ file: "yarn.lock", ecosystem: "npm" },
|
|
4320
|
+
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
4321
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
4312
4322
|
];
|
|
4313
|
-
var
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4321
|
-
|
|
4323
|
+
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
4324
|
+
async function scanDependencies(targetPath) {
|
|
4325
|
+
const findings = [];
|
|
4326
|
+
for (const { file, ecosystem } of LOCKFILES) {
|
|
4327
|
+
const lockPath = (0, import_node_path5.join)(targetPath, file);
|
|
4328
|
+
if (!(0, import_node_fs10.existsSync)(lockPath)) continue;
|
|
4329
|
+
let deps;
|
|
4330
|
+
try {
|
|
4331
|
+
deps = parseDependencies(lockPath, file);
|
|
4332
|
+
} catch {
|
|
4333
|
+
continue;
|
|
4334
|
+
}
|
|
4335
|
+
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
4336
|
+
let vulns;
|
|
4337
|
+
try {
|
|
4338
|
+
vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
4339
|
+
} catch {
|
|
4340
|
+
continue;
|
|
4341
|
+
}
|
|
4342
|
+
for (const vuln of vulns) {
|
|
4343
|
+
const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
|
|
4344
|
+
findings.push({
|
|
4345
|
+
ruleId: "dependency-known-vulnerability",
|
|
4346
|
+
title: "Dependency CVE",
|
|
4347
|
+
file,
|
|
4348
|
+
line: 1,
|
|
4349
|
+
severity: severityFromCvss(cvss),
|
|
4350
|
+
confidence: "evidence",
|
|
4351
|
+
message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
|
|
4352
|
+
consequence: "A published advisory exists for the exact version resolved in this lockfile.",
|
|
4353
|
+
excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
|
|
4354
|
+
category: "dependency"
|
|
4355
|
+
});
|
|
4356
|
+
}
|
|
4357
|
+
}
|
|
4358
|
+
}
|
|
4359
|
+
return findings;
|
|
4360
|
+
}
|
|
4361
|
+
function severityFromCvss(score) {
|
|
4362
|
+
if (!score) return "medium";
|
|
4363
|
+
const value = Number.parseFloat(score);
|
|
4364
|
+
if (Number.isNaN(value)) return "medium";
|
|
4365
|
+
if (value >= 9) return "critical";
|
|
4366
|
+
if (value >= 7) return "high";
|
|
4367
|
+
if (value >= 4) return "medium";
|
|
4368
|
+
return "low";
|
|
4369
|
+
}
|
|
4370
|
+
function parseDependencies(lockPath, filename) {
|
|
4371
|
+
const deps = [];
|
|
4372
|
+
if (filename === "package-lock.json") {
|
|
4373
|
+
const lock = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf-8"));
|
|
4374
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
4375
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
4376
|
+
const name = key.replace(/^node_modules\//, "");
|
|
4377
|
+
const version = value?.version;
|
|
4378
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
4379
|
+
}
|
|
4380
|
+
return deps;
|
|
4381
|
+
}
|
|
4382
|
+
if (filename === "requirements.txt") {
|
|
4383
|
+
for (const line of (0, import_node_fs10.readFileSync)(lockPath, "utf-8").split("\n")) {
|
|
4384
|
+
const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
|
|
4385
|
+
if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
|
|
4386
|
+
}
|
|
4387
|
+
}
|
|
4388
|
+
return deps;
|
|
4389
|
+
}
|
|
4390
|
+
function isValidPackageName(name) {
|
|
4391
|
+
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
4392
|
+
}
|
|
4393
|
+
function isValidVersion(version) {
|
|
4394
|
+
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
4395
|
+
}
|
|
4396
|
+
async function queryOsv(name, version, ecosystem) {
|
|
4397
|
+
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
4398
|
+
try {
|
|
4399
|
+
const response = await fetch("https://api.osv.dev/v1/query", {
|
|
4400
|
+
method: "POST",
|
|
4401
|
+
headers: { "Content-Type": "application/json" },
|
|
4402
|
+
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
4403
|
+
signal: AbortSignal.timeout(5e3)
|
|
4404
|
+
});
|
|
4405
|
+
if (!response.ok) return [];
|
|
4406
|
+
const data = await response.json();
|
|
4407
|
+
return data.vulns ?? [];
|
|
4408
|
+
} catch {
|
|
4409
|
+
return [];
|
|
4410
|
+
}
|
|
4411
|
+
}
|
|
4412
|
+
|
|
4413
|
+
// src/scan/engine.ts
|
|
4414
|
+
var import_node_fs11 = require("fs");
|
|
4415
|
+
var import_node_path6 = require("path");
|
|
4416
|
+
|
|
4417
|
+
// src/scan/types.ts
|
|
4418
|
+
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
4419
|
+
function severityRank(severity) {
|
|
4420
|
+
const index = SEVERITY_ORDER.indexOf(severity);
|
|
4421
|
+
return index === -1 ? 0 : index;
|
|
4422
|
+
}
|
|
4423
|
+
function severityFor(declared, confidence) {
|
|
4424
|
+
if (confidence !== "pattern") return declared;
|
|
4425
|
+
return severityRank(declared) > severityRank("medium") ? "medium" : declared;
|
|
4426
|
+
}
|
|
4427
|
+
|
|
4428
|
+
// src/scan/code-rules.ts
|
|
4429
|
+
var UNTRUSTED_JS = /\b(?:req|request|ctx|context)\s*\.\s*(?:body|query|params|param|headers|cookies|url|files)\b|\bprocess\.argv\b|\bwindow\.location\b|\bdocument\.location\b|\blocation\.(?:search|hash|href)\b|\bsearchParams\s*\.\s*(?:get|getAll|has|entries|keys|values|forEach)\b|\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetInputStream\s*\(/;
|
|
4430
|
+
var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
|
|
4431
|
+
var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
|
|
4432
|
+
var UNTRUSTED_GO = /\br\s*\.\s*(?:URL|Form|Body|Header|PostForm)\b|\bFormValue\s*\(|\bQuery\s*\(\s*\)\s*\.\s*Get\s*\(|\bmux\.Vars\s*\(/;
|
|
4433
|
+
var UNTRUSTED_JAVA = /\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetHeader\s*\(|\bgetInputStream\s*\(|\bgetCookies\s*\(|\b@RequestParam\b|\b@PathVariable\b/;
|
|
4434
|
+
function untrustedPatternFor(language) {
|
|
4435
|
+
switch (language) {
|
|
4436
|
+
case "python":
|
|
4437
|
+
return UNTRUSTED_PY;
|
|
4438
|
+
case "ruby":
|
|
4439
|
+
return UNTRUSTED_RB;
|
|
4440
|
+
case "go":
|
|
4441
|
+
return UNTRUSTED_GO;
|
|
4442
|
+
case "java":
|
|
4443
|
+
return UNTRUSTED_JAVA;
|
|
4444
|
+
default:
|
|
4445
|
+
return UNTRUSTED_JS;
|
|
4446
|
+
}
|
|
4447
|
+
}
|
|
4448
|
+
var GENERIC_GUARD = (
|
|
4449
|
+
// `esc(`, `aEsc(`, `htmlEscape(`, `escapeHtml(` — the escaper is almost
|
|
4450
|
+
// never *named* `escapeHtml` in real code. It gets aliased to something
|
|
4451
|
+
// short because it is called on nearly every interpolation, so matching only
|
|
4452
|
+
// the long spellings reported the codebases that escape most rigorously.
|
|
4453
|
+
//
|
|
4454
|
+
// The identifier must END at the escaper (with at most a known output-context
|
|
4455
|
+
// suffix). An earlier, looser form also matched `describe(`, which would have
|
|
4456
|
+
// silenced findings across every test file in every repository.
|
|
4457
|
+
/\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
|
|
4458
|
+
);
|
|
4459
|
+
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
4460
|
+
var CODE_SINK = /\bglobalThis\s*\[|\bconstructor\b|\beval\b|\bFunction\b|\brun\s*\(|\bvm\s*\.\s*run/;
|
|
4461
|
+
var EXFIL_SINK = /\bconsole\s*\.\s*(?:log|debug|info|warn|error)\s*\(|\bfetch\s*\(|\baxios\b|\brequest\s*\(|\.\s*send\s*\(/;
|
|
4462
|
+
var SQL_KEYWORDS = "SELECT|INSERT\\s+INTO|INSERT|UPDATE|DELETE\\s+FROM|DELETE|DROP|UNION\\s+SELECT";
|
|
4463
|
+
var SQL_IN_DOUBLE = `"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*"`;
|
|
4464
|
+
var SQL_IN_SINGLE = `'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*'`;
|
|
4465
|
+
var SQL_STRING = `(?:${SQL_IN_DOUBLE}|${SQL_IN_SINGLE})`;
|
|
4466
|
+
var CODE_RULES = [
|
|
4467
|
+
// ── Injection: SQL ───────────────────────────────────────────────────────
|
|
4468
|
+
{
|
|
4469
|
+
id: "sql-string-concatenation",
|
|
4470
|
+
title: "SQL assembled by concatenation or interpolation",
|
|
4471
|
+
consequence: "A quote in the interpolated value changes the query\u2019s meaning \u2014 the query runs as the attacker wrote it, not as you wrote it.",
|
|
4472
|
+
cwe: "CWE-89",
|
|
4473
|
+
severity: "critical",
|
|
4474
|
+
// The tail is what distinguishes assembly from parameterisation. A bound
|
|
4475
|
+
// query leaves a comma after the closing quote (`"… = $1", [id]`) and
|
|
4476
|
+
// matches none of these.
|
|
4477
|
+
pattern: new RegExp(
|
|
4478
|
+
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"[^"\\n]*|'[^'\\n]*)(?:WHERE|ORDER\\s+BY|VALUES|SET)\\b`,
|
|
4479
|
+
"i"
|
|
4480
|
+
)
|
|
4481
|
+
},
|
|
4482
|
+
{
|
|
4483
|
+
id: "sql-template-interpolation",
|
|
4484
|
+
title: "SQL built from a template literal or f-string",
|
|
4485
|
+
consequence: "Template interpolation is string concatenation with nicer syntax \u2014 it binds nothing and escapes nothing.",
|
|
4486
|
+
cwe: "CWE-89",
|
|
4487
|
+
severity: "critical",
|
|
4488
|
+
pattern: new RegExp(
|
|
4489
|
+
`\`[^\`\\n]*(?:${SQL_KEYWORDS})\\b[^\`\\n]*\\$\\{|\\bf"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*\\{|\\bf'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*\\{`,
|
|
4490
|
+
"i"
|
|
4491
|
+
)
|
|
4492
|
+
},
|
|
4493
|
+
{
|
|
4494
|
+
id: "sql-format-call",
|
|
4495
|
+
title: "SQL text produced by a format helper",
|
|
4496
|
+
consequence: "`Sprintf`/`String.format` substitute without quoting; the resulting string is concatenated SQL by another name.",
|
|
4497
|
+
cwe: "CWE-89",
|
|
4498
|
+
severity: "critical",
|
|
4499
|
+
languages: ["go", "java"],
|
|
4500
|
+
pattern: new RegExp(
|
|
4501
|
+
`\\b(?:fmt\\.Sprintf|String\\.format)\\s*\\(\\s*"[^"\\n]*(?:${SQL_KEYWORDS})\\b`,
|
|
4502
|
+
"i"
|
|
4503
|
+
)
|
|
4504
|
+
},
|
|
4505
|
+
{
|
|
4506
|
+
id: "rb-sql-interpolation",
|
|
4507
|
+
title: "ActiveRecord query built by string interpolation",
|
|
4508
|
+
consequence: '`where("\u2026 #{value}")` interpolates before the adapter sees it, so no binding ever happens.',
|
|
4509
|
+
cwe: "CWE-89",
|
|
4510
|
+
severity: "critical",
|
|
4511
|
+
languages: ["ruby"],
|
|
4512
|
+
pattern: /\b(?:where|find_by_sql|execute|select_all|select_values|order|group|pluck)\s*[( ]\s*(?:"[^"\n]*|'[^'\n]*)#\{/
|
|
4513
|
+
},
|
|
4514
|
+
// ── Injection: OS command ────────────────────────────────────────────────
|
|
4515
|
+
{
|
|
4516
|
+
id: "js-shell-exec-interpolation",
|
|
4517
|
+
title: "shell execution with an interpolated string",
|
|
4518
|
+
consequence: "A `;` or `$(\u2026)` in the interpolated value runs as the server user.",
|
|
4519
|
+
cwe: "CWE-78",
|
|
4520
|
+
severity: "critical",
|
|
4521
|
+
languages: ["javascript", "typescript"],
|
|
4522
|
+
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/
|
|
4523
|
+
},
|
|
4524
|
+
{
|
|
4525
|
+
id: "py-shell-command-string",
|
|
4526
|
+
title: "shell command built from a string",
|
|
4527
|
+
consequence: "`os.system` and `shell=True` hand the string to `/bin/sh`, which happily interprets metacharacters.",
|
|
4528
|
+
cwe: "CWE-78",
|
|
4529
|
+
severity: "critical",
|
|
4530
|
+
languages: ["python"],
|
|
4531
|
+
pattern: /\bos\.(?:system|popen)\s*\(\s*(?:f?['"][^'"]*['"]\s*(?:\+|%|\.\s*format)|f['"]|[a-zA-Z_]\w*\s*[,)])|\bsubprocess\.(?:run|call|check_call|check_output|Popen)\s*\([^)]*\bshell\s*=\s*True/
|
|
4532
|
+
},
|
|
4533
|
+
{
|
|
4534
|
+
id: "go-shell-exec-command",
|
|
4535
|
+
title: "exec.Command invoking a shell",
|
|
4536
|
+
consequence: "Passing `sh -c` re-introduces the shell that `exec.Command`\u2019s argv interface exists to avoid.",
|
|
4537
|
+
cwe: "CWE-78",
|
|
4538
|
+
severity: "critical",
|
|
4539
|
+
languages: ["go"],
|
|
4540
|
+
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"/
|
|
4541
|
+
},
|
|
4542
|
+
{
|
|
4543
|
+
id: "rb-backtick-interpolation",
|
|
4544
|
+
title: "backtick command with interpolation",
|
|
4545
|
+
consequence: "Ruby backticks are a shell invocation; `#{}` inside one is command injection.",
|
|
4546
|
+
cwe: "CWE-78",
|
|
4547
|
+
severity: "critical",
|
|
4548
|
+
languages: ["ruby"],
|
|
4549
|
+
pattern: /`[^`\n]*#\{|\bsystem\s*\(\s*["'][^"'\n]*#\{|%x\[[^\]]*#\{/
|
|
4550
|
+
},
|
|
4551
|
+
// ── Injection: dynamic code ──────────────────────────────────────────────
|
|
4552
|
+
{
|
|
4553
|
+
id: "js-dynamic-code-execution",
|
|
4554
|
+
title: "dynamic code execution",
|
|
4555
|
+
consequence: "Any string reaching this call executes as code with the process\u2019 privileges.",
|
|
4556
|
+
cwe: "CWE-95",
|
|
4557
|
+
severity: "critical",
|
|
4558
|
+
languages: ["javascript", "typescript"],
|
|
4559
|
+
pattern: /\beval\s*\(|\bnew\s+Function\s*\(|\bvm\s*\.\s*run(?:InThisContext|InNewContext|InContext)\s*\(|\bset(?:Timeout|Interval)\s*\(\s*(?:['"`]|(?:req|request|ctx|params|query|body)\b)/
|
|
4560
|
+
},
|
|
4561
|
+
{
|
|
4562
|
+
id: "js-indirect-code-sink",
|
|
4563
|
+
title: "code sink reached indirectly",
|
|
4564
|
+
consequence: "Resolving `eval`/`Function` through `globalThis[\u2026]` or `.constructor` hides the sink from literal matching. Legitimate code has no reason to.",
|
|
4565
|
+
cwe: "CWE-506",
|
|
4566
|
+
severity: "high",
|
|
4567
|
+
languages: ["javascript", "typescript"],
|
|
4568
|
+
pattern: /\bglobalThis\s*\[\s*[a-zA-Z_$][\w$]*\s*\]|\(\s*function\s*\(\s*\)\s*\{\s*\}\s*\)\s*\.\s*constructor/
|
|
4569
|
+
},
|
|
4570
|
+
{
|
|
4571
|
+
id: "js-encoded-payload-execution",
|
|
4572
|
+
title: "encoded blob decoded next to a code sink",
|
|
4573
|
+
consequence: "A base64 literal that is decoded and executed is the standard shape of a planted backdoor; the encoding exists to defeat review.",
|
|
4574
|
+
cwe: "CWE-506",
|
|
4575
|
+
severity: "critical",
|
|
4576
|
+
languages: ["javascript", "typescript"],
|
|
4577
|
+
pattern: /\bBuffer\.from\s*\(\s*[\w.$]+\s*,\s*['"]base64['"]\s*\)|\batob\s*\(\s*[\w.$]+\s*\)/,
|
|
4578
|
+
requires: CODE_SINK,
|
|
4579
|
+
guardBack: 6,
|
|
4580
|
+
guardForward: 3
|
|
4581
|
+
},
|
|
4582
|
+
{
|
|
4583
|
+
id: "py-dynamic-code-execution",
|
|
4584
|
+
title: "dynamic code execution",
|
|
4585
|
+
consequence: "Any string reaching this call executes as Python with the process\u2019 privileges.",
|
|
4586
|
+
cwe: "CWE-95",
|
|
4587
|
+
severity: "critical",
|
|
4588
|
+
languages: ["python"],
|
|
4589
|
+
pattern: /\b(?:eval|exec)\s*\(\s*(?!['"]\s*\))[a-zA-Z_(f'"]/,
|
|
4590
|
+
needsContext: true
|
|
4591
|
+
},
|
|
4592
|
+
{
|
|
4593
|
+
id: "rb-dynamic-dispatch",
|
|
4594
|
+
title: "dynamic code execution or unrestricted #send",
|
|
4595
|
+
consequence: "`eval` runs arbitrary Ruby; unrestricted `#send` lets the caller invoke any method on the receiver, including private ones.",
|
|
4596
|
+
cwe: "CWE-95",
|
|
4597
|
+
severity: "critical",
|
|
4598
|
+
languages: ["ruby"],
|
|
4599
|
+
pattern: /\beval\s*\(|\binstance_eval\s*\(|\bclass_eval\s*\(|\.\s*send\s*\(\s*(?:params|request|args)\b/
|
|
4600
|
+
},
|
|
4601
|
+
// ── Cross-site scripting ─────────────────────────────────────────────────
|
|
4602
|
+
{
|
|
4603
|
+
id: "js-unescaped-html-sink",
|
|
4604
|
+
title: "unescaped HTML rendering",
|
|
4605
|
+
consequence: "A script tag in the value executes in the victim\u2019s session \u2014 stored or reflected XSS.",
|
|
4606
|
+
cwe: "CWE-79",
|
|
4607
|
+
severity: "high",
|
|
4608
|
+
languages: ["javascript", "typescript"],
|
|
4609
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/,
|
|
4610
|
+
/**
|
|
4611
|
+
* A whole-statement assignment of a string with no interpolation and no
|
|
4612
|
+
* concatenation carries no data, so it cannot carry attacker data. This
|
|
4613
|
+
* was the single largest source of noise: a codebase that builds its UI
|
|
4614
|
+
* with innerHTML reports every static heading and spinner as XSS, and a
|
|
4615
|
+
* rule that flags 40 safe lines to catch one real one gets switched off.
|
|
4616
|
+
*
|
|
4617
|
+
* Line-scoped on purpose — see `lineGuard`.
|
|
4618
|
+
*/
|
|
4619
|
+
lineGuard: /(?:innerHTML|outerHTML)\s*=\s*(?:'[^'\\]*'|"[^"\\]*"|`[^`$\\]*`)\s*;?\s*$/
|
|
4620
|
+
},
|
|
4621
|
+
{
|
|
4622
|
+
id: "java-html-writer-concatenation",
|
|
4623
|
+
title: "HTML written to the response by concatenation",
|
|
4624
|
+
consequence: "The servlet writer performs no encoding; a value concatenated into markup is rendered as markup.",
|
|
4625
|
+
cwe: "CWE-79",
|
|
4626
|
+
severity: "high",
|
|
4627
|
+
languages: ["java"],
|
|
4628
|
+
pattern: /\b(?:println|print|write)\s*\(\s*"[^"\n]*<[^"\n]*"\s*\+/
|
|
4629
|
+
},
|
|
4630
|
+
{
|
|
4631
|
+
id: "rb-unescaped-output",
|
|
4632
|
+
title: "Rails output escaping bypassed",
|
|
4633
|
+
consequence: "`html_safe` and `raw` tell Rails the string is already safe. If it came from a parameter, it is not.",
|
|
4634
|
+
cwe: "CWE-79",
|
|
4635
|
+
severity: "high",
|
|
4636
|
+
languages: ["ruby"],
|
|
4637
|
+
pattern: /\.\s*html_safe\b|\braw\s*\(\s*(?:params|request|@)|\blink_to\s+[^,\n]+,\s*params\s*\[/
|
|
4638
|
+
},
|
|
4639
|
+
{
|
|
4640
|
+
id: "py-template-autoescape-off",
|
|
4641
|
+
title: "template rendering with escaping disabled",
|
|
4642
|
+
consequence: "With autoescape off \u2014 or a `|safe` filter \u2014 every interpolated value is rendered as markup.",
|
|
4643
|
+
cwe: "CWE-79",
|
|
4644
|
+
severity: "high",
|
|
4645
|
+
languages: ["python"],
|
|
4646
|
+
pattern: /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False|\|\s*safe\b|\bMarkup\s*\(\s*(?!['"])/
|
|
4647
|
+
},
|
|
4648
|
+
{
|
|
4649
|
+
id: "py-template-from-input",
|
|
4650
|
+
title: "template compiled from a non-literal source",
|
|
4651
|
+
consequence: "Server-side template injection. In Jinja2 the sandbox is escapable, so this escalates from XSS to remote code execution.",
|
|
4652
|
+
cwe: "CWE-1336",
|
|
4653
|
+
severity: "critical",
|
|
4654
|
+
languages: ["python"],
|
|
4655
|
+
pattern: /\bTemplate\s*\(\s*(?!['"])[a-zA-Z_]/,
|
|
4656
|
+
needsContext: true
|
|
4657
|
+
},
|
|
4658
|
+
// ── Server-side request forgery ──────────────────────────────────────────
|
|
4659
|
+
{
|
|
4660
|
+
id: "js-ssrf-outbound-request",
|
|
4661
|
+
title: "outbound request to a non-constant URL",
|
|
4662
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
4663
|
+
cwe: "CWE-918",
|
|
4664
|
+
severity: "high",
|
|
4665
|
+
languages: ["javascript", "typescript"],
|
|
4666
|
+
pattern: /\bfetch\s*\(\s*[a-zA-Z_$][\w$]*\s*[,)]|\bhttps?\s*\.\s*(?:get|request)\s*\(\s*[a-zA-Z_$][\w$]*\s*[,)]|\baxios\s*\.\s*get\s*\(\s*[a-zA-Z_$][\w$]*\s*[,)]/,
|
|
4667
|
+
needsContext: true
|
|
4668
|
+
},
|
|
4669
|
+
{
|
|
4670
|
+
id: "py-ssrf-outbound-request",
|
|
4671
|
+
title: "outbound request to a non-constant URL",
|
|
4672
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
4673
|
+
cwe: "CWE-918",
|
|
4674
|
+
severity: "high",
|
|
4675
|
+
languages: ["python"],
|
|
4676
|
+
pattern: /\brequests\.(?:get|request|head)\s*\(\s*[a-zA-Z_]\w*\s*[,)]|\burlopen\s*\(\s*[a-zA-Z_]\w*\s*[,)]|\bhttpx\.get\s*\(\s*[a-zA-Z_]\w*\s*[,)]/,
|
|
4677
|
+
needsContext: true
|
|
4678
|
+
},
|
|
4679
|
+
{
|
|
4680
|
+
id: "go-ssrf-outbound-request",
|
|
4681
|
+
title: "outbound request to a non-constant URL",
|
|
4682
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
4683
|
+
cwe: "CWE-918",
|
|
4684
|
+
severity: "high",
|
|
4685
|
+
languages: ["go"],
|
|
4686
|
+
pattern: /\bhttp\.(?:Get|Post|Head)\s*\(\s*(?:[a-zA-Z_]\w*\s*[,)]|"[^"]*"\s*\+)/,
|
|
4687
|
+
needsContext: true
|
|
4688
|
+
},
|
|
4689
|
+
// ── Open redirect ────────────────────────────────────────────────────────
|
|
4690
|
+
{
|
|
4691
|
+
id: "js-open-redirect",
|
|
4692
|
+
title: "redirect to a non-constant destination",
|
|
4693
|
+
consequence: "Your domain becomes the credible first hop of a phishing chain; the victim sees your hostname in the link they clicked.",
|
|
4694
|
+
cwe: "CWE-601",
|
|
4695
|
+
severity: "medium",
|
|
4696
|
+
languages: ["javascript", "typescript"],
|
|
4697
|
+
pattern: /\b(?:res|response)\s*\.\s*redirect\s*\(\s*[a-zA-Z_$][\w$]*\s*\)|\bwindow\s*\.\s*location(?:\s*\.\s*(?:href|replace))?\s*(?:=\s*[a-zA-Z_$]|\(\s*[a-zA-Z_$][\w$]*\s*\))/,
|
|
4698
|
+
needsContext: true
|
|
4699
|
+
},
|
|
4700
|
+
// ── Deserialisation ──────────────────────────────────────────────────────
|
|
4701
|
+
{
|
|
4702
|
+
id: "py-unsafe-deserialization",
|
|
4703
|
+
title: "deserialisation of untrusted data",
|
|
4704
|
+
consequence: "`pickle` and `yaml.load` instantiate arbitrary types during parsing \u2014 a crafted payload is remote code execution, not a parse error.",
|
|
4705
|
+
cwe: "CWE-502",
|
|
4706
|
+
severity: "critical",
|
|
4707
|
+
languages: ["python"],
|
|
4708
|
+
pattern: /\bpickle\.loads?\s*\(|\bcPickle\.loads?\s*\(|\bmarshal\.loads\s*\(|\byaml\.load\s*\(|\bjsonpickle\.decode\s*\(/
|
|
4709
|
+
},
|
|
4710
|
+
{
|
|
4711
|
+
id: "java-unsafe-deserialization",
|
|
4712
|
+
title: "Java deserialisation without a class filter",
|
|
4713
|
+
consequence: "A gadget chain in the classpath turns `readObject()` on attacker bytes into remote code execution.",
|
|
4714
|
+
cwe: "CWE-502",
|
|
4715
|
+
severity: "critical",
|
|
4716
|
+
languages: ["java"],
|
|
4717
|
+
pattern: /\breadObject\s*\(\s*\)|\bnew\s+ObjectInputStream\s*\(/,
|
|
4718
|
+
guardBack: 8,
|
|
4719
|
+
// The stream is constructed, *then* filtered. Without a forward window the
|
|
4720
|
+
// guarded case matches on its constructor line and reports a correct
|
|
4721
|
+
// implementation as a finding.
|
|
4722
|
+
guardForward: 6
|
|
4723
|
+
},
|
|
4724
|
+
{
|
|
4725
|
+
id: "js-unsafe-yaml-load",
|
|
4726
|
+
title: "YAML parsed with type resolution enabled",
|
|
4727
|
+
consequence: "A crafted document can instantiate arbitrary types during parsing.",
|
|
4728
|
+
cwe: "CWE-502",
|
|
4729
|
+
severity: "high",
|
|
4730
|
+
languages: ["javascript", "typescript"],
|
|
4731
|
+
pattern: /\byaml\s*\.\s*load\s*\((?![^)]*safe)|\bloadAll\s*\([^)]*unsafe/i
|
|
4732
|
+
},
|
|
4733
|
+
// ── XML external entities ────────────────────────────────────────────────
|
|
4734
|
+
{
|
|
4735
|
+
id: "java-xxe-parser-defaults",
|
|
4736
|
+
title: "XML parser left on its insecure defaults",
|
|
4737
|
+
consequence: "External entity expansion reads local files and makes outbound requests on the parser\u2019s behalf \u2014 file disclosure and SSRF from a document.",
|
|
4738
|
+
cwe: "CWE-611",
|
|
4739
|
+
severity: "high",
|
|
4740
|
+
languages: ["java"],
|
|
4741
|
+
pattern: /\b(?:DocumentBuilderFactory|SAXParserFactory|XMLInputFactory|TransformerFactory|SchemaFactory)\s*\.\s*newInstance\s*\(\s*\)/,
|
|
4742
|
+
guard: XXE_GUARD,
|
|
4743
|
+
guardBack: 4,
|
|
4744
|
+
guardForward: 8
|
|
4745
|
+
},
|
|
4746
|
+
{
|
|
4747
|
+
id: "java-xxe-parse-call",
|
|
4748
|
+
title: "XML parsed by a builder that was never hardened",
|
|
4749
|
+
consequence: "The expansion happens at `parse()`. Flagging only the factory misses the line where the document is actually read.",
|
|
4750
|
+
cwe: "CWE-611",
|
|
4751
|
+
severity: "high",
|
|
4752
|
+
languages: ["java"],
|
|
4753
|
+
// Receiver-qualified so `LocalDate.parse(s)` and friends stay out of it.
|
|
4754
|
+
pattern: /\b\w*(?:[Bb]uilder|[Pp]arser|[Rr]eader)\s*\.\s*parse\s*\(/,
|
|
4755
|
+
guard: XXE_GUARD,
|
|
4756
|
+
guardBack: 6,
|
|
4757
|
+
guardForward: 4
|
|
4758
|
+
},
|
|
4759
|
+
// ── Path traversal ───────────────────────────────────────────────────────
|
|
4760
|
+
{
|
|
4761
|
+
id: "py-path-traversal",
|
|
4762
|
+
title: "file opened at a path built from input",
|
|
4763
|
+
consequence: "A `../` sequence \u2014 or an absolute path \u2014 reads or writes outside the intended directory.",
|
|
4764
|
+
cwe: "CWE-22",
|
|
4765
|
+
severity: "high",
|
|
4766
|
+
languages: ["python"],
|
|
4767
|
+
pattern: /\bopen\s*\(\s*(?:os\.path\.join\s*\(|[a-zA-Z_]\w*\s*\+|f['"])/,
|
|
4768
|
+
needsContext: true
|
|
4769
|
+
},
|
|
4770
|
+
{
|
|
4771
|
+
id: "js-path-traversal",
|
|
4772
|
+
title: "file path built from a variable",
|
|
4773
|
+
consequence: "A `../` sequence in the value reads or writes outside the intended directory.",
|
|
4774
|
+
cwe: "CWE-22",
|
|
4775
|
+
severity: "medium",
|
|
4776
|
+
languages: ["javascript", "typescript"],
|
|
4777
|
+
pattern: /\b(?:readFile|readFileSync|writeFile|writeFileSync|createReadStream|createWriteStream|unlink|unlinkSync|sendFile)\s*\(\s*(?:`[^`]*\$\{|[a-zA-Z_$][\w$]*\s*\+|path\.join\s*\([^)]*(?:req|request)\b)/,
|
|
4778
|
+
needsContext: true
|
|
4779
|
+
},
|
|
4780
|
+
// ── Cryptography, tokens, randomness ─────────────────────────────────────
|
|
4781
|
+
{
|
|
4782
|
+
id: "js-jwt-decode-without-verify",
|
|
4783
|
+
title: "JWT decoded without verifying the signature",
|
|
4784
|
+
consequence: "`decode` parses the claims and checks nothing. Anyone can mint a token with any `sub` and any `role`.",
|
|
4785
|
+
cwe: "CWE-347",
|
|
4786
|
+
severity: "critical",
|
|
4787
|
+
languages: ["javascript", "typescript"],
|
|
4788
|
+
pattern: /\bjwt\s*\.\s*decode\s*\(|\bjsonwebtoken\s*\.\s*decode\s*\(|\bdecodeJwt\s*\(/
|
|
4789
|
+
},
|
|
4790
|
+
{
|
|
4791
|
+
id: "tls-verification-disabled",
|
|
4792
|
+
title: "TLS certificate verification disabled",
|
|
4793
|
+
consequence: "Every connection made this way is trivially interceptable; the encryption is decorative.",
|
|
4794
|
+
cwe: "CWE-295",
|
|
4795
|
+
severity: "high",
|
|
4796
|
+
pattern: /rejectUnauthorized\s*:\s*false|NODE_TLS_REJECT_UNAUTHORIZED\s*[=:]\s*['"]?0|strictSSL\s*:\s*false|\bverify\s*=\s*False\b|InsecureSkipVerify\s*:\s*true/
|
|
4797
|
+
},
|
|
4798
|
+
{
|
|
4799
|
+
id: "weak-hash-on-credential",
|
|
4800
|
+
title: "broken hash used on a credential",
|
|
4801
|
+
consequence: "MD5 and SHA-1 are fast and collision-prone; hashed passwords are recoverable.",
|
|
4802
|
+
cwe: "CWE-327",
|
|
4803
|
+
severity: "high",
|
|
4804
|
+
pattern: /(?:createHash|hashlib|MessageDigest\.getInstance|Digest::)\s*[.(]?\s*['"]?(?:md5|MD5|sha1|SHA-?1)['"]?\s*\)?[\s\S]{0,80}(?:password|passwd|secret|token|credential)/i
|
|
4805
|
+
},
|
|
4806
|
+
{
|
|
4807
|
+
id: "insecure-randomness-for-secret",
|
|
4808
|
+
title: "predictable randomness used for a security value",
|
|
4809
|
+
consequence: "`Math.random`/`random.random` are predictable; tokens, session ids and reset codes built from them are guessable.",
|
|
4810
|
+
cwe: "CWE-338",
|
|
4811
|
+
severity: "high",
|
|
4812
|
+
pattern: /(?:token|secret|password|salt|nonce|session|otp|reset|apikey|api_key)[\w]*\s*[:=][^;\n]{0,60}(?:Math\s*\.\s*random\s*\(|\brandom\s*\.\s*(?:random|randint|choice)\s*\(|\brand\s*\()/i
|
|
4813
|
+
},
|
|
4814
|
+
{
|
|
4815
|
+
id: "redos-nested-quantifier",
|
|
4816
|
+
title: "regex with nested unbounded quantifiers",
|
|
4817
|
+
consequence: "Catastrophic backtracking: a crafted input of a few dozen characters pins a CPU core for minutes.",
|
|
4818
|
+
cwe: "CWE-1333",
|
|
4819
|
+
severity: "medium",
|
|
4820
|
+
pattern: /\([^)\n]*[+*]\s*\)\s*[+*]|\([^)\n]*\{\d+,\}\s*\)\s*[+*{]/
|
|
4821
|
+
},
|
|
4822
|
+
// ── Temporary files ──────────────────────────────────────────────────────
|
|
4823
|
+
{
|
|
4824
|
+
id: "insecure-temp-file",
|
|
4825
|
+
title: "predictable temporary file path",
|
|
4826
|
+
consequence: "A predictable name in a world-writable directory is a symlink attack: an attacker pre-creates the path and your process writes through it.",
|
|
4827
|
+
cwe: "CWE-377",
|
|
4828
|
+
severity: "medium",
|
|
4829
|
+
// A hardcoded path under /tmp is the finding whether or not it is
|
|
4830
|
+
// formatted: `"/tmp/application.log.tmp"` is worse than the PID-based one,
|
|
4831
|
+
// because every process on the host can predict it exactly.
|
|
4832
|
+
pattern: /\btempfile\.mktemp\s*\(|\bos\.tmpnam\s*\(|['"]\/tmp\/[^'"\n]+['"]|['"]\/tmp\/[^'"\n]*\{|\bFile\.createTempFile\s*\(/
|
|
4833
|
+
},
|
|
4834
|
+
// ── Information exposure ─────────────────────────────────────────────────
|
|
4835
|
+
{
|
|
4836
|
+
id: "py-stack-trace-returned",
|
|
4837
|
+
title: "stack trace returned to the caller",
|
|
4838
|
+
consequence: "Tracebacks leak absolute paths, dependency versions and source fragments \u2014 the reconnaissance an attacker would otherwise have to guess at.",
|
|
4839
|
+
cwe: "CWE-209",
|
|
4840
|
+
severity: "medium",
|
|
4841
|
+
languages: ["python"],
|
|
4842
|
+
pattern: /\breturn\b[^\n]*\btraceback\.(?:format_exc|format_exception|print_exc)\s*\(|\breturn\b[^\n]*\bstr\s*\(\s*e\s*\)/
|
|
4843
|
+
},
|
|
4844
|
+
{
|
|
4845
|
+
id: "js-environment-exfiltration",
|
|
4846
|
+
title: "process environment serialised into a payload",
|
|
4847
|
+
consequence: "The environment is where every secret lives. Serialising it whole into a request body is credential exfiltration regardless of the endpoint.",
|
|
4848
|
+
cwe: "CWE-532",
|
|
4849
|
+
severity: "critical",
|
|
4850
|
+
languages: ["javascript", "typescript"],
|
|
4851
|
+
pattern: /\bJSON\.stringify\s*\(\s*\{?[^)]*\bprocess\.env\b(?!\s*\.)/,
|
|
4852
|
+
guard: false
|
|
4853
|
+
},
|
|
4854
|
+
{
|
|
4855
|
+
id: "js-credential-logged",
|
|
4856
|
+
title: "credential read from the environment into a log sink",
|
|
4857
|
+
consequence: "CI retains job logs, and on public forks it publishes them. A token printed once is a token leaked permanently.",
|
|
4858
|
+
cwe: "CWE-532",
|
|
4859
|
+
severity: "high",
|
|
4860
|
+
languages: ["javascript", "typescript"],
|
|
4861
|
+
pattern: /\b(?:token|apiKey|api_key|secret|password|credential|auth)\w*\s*:\s*process\.env\.\w+/i,
|
|
4862
|
+
requires: EXFIL_SINK,
|
|
4863
|
+
guard: false,
|
|
4864
|
+
guardBack: 4,
|
|
4865
|
+
guardForward: 1
|
|
4866
|
+
},
|
|
4867
|
+
// ── Prototype pollution ──────────────────────────────────────────────────
|
|
4868
|
+
{
|
|
4869
|
+
id: "js-prototype-pollution",
|
|
4870
|
+
title: "write to a prototype-reachable key",
|
|
4871
|
+
consequence: "An attacker-supplied `__proto__` key changes behaviour for every object in the process, including ones it never touched.",
|
|
4872
|
+
cwe: "CWE-1321",
|
|
4873
|
+
severity: "high",
|
|
4874
|
+
languages: ["javascript", "typescript"],
|
|
4875
|
+
pattern: /\[\s*['"]__proto__['"]\s*\]|\bObject\s*\.\s*assign\s*\(\s*[\w.$]*\.prototype\b|\.\s*__proto__\s*=/
|
|
4876
|
+
}
|
|
4877
|
+
];
|
|
4878
|
+
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
4879
|
+
var DEFINITION_PREFIX = /^\s*(?:(?:export|public|private|protected|static|final|async|abstract)\s+)*(?:def|function|func|class|module|interface|struct|impl|fn|sub)\b/;
|
|
4880
|
+
function isComment(line) {
|
|
4881
|
+
return COMMENT_PREFIX.test(line);
|
|
4882
|
+
}
|
|
4883
|
+
function proseLines(lines) {
|
|
4884
|
+
const inside = /* @__PURE__ */ new Set();
|
|
4885
|
+
let delimiter = null;
|
|
4886
|
+
lines.forEach((line, index) => {
|
|
4887
|
+
if (delimiter) {
|
|
4888
|
+
inside.add(index);
|
|
4889
|
+
if (line.includes(delimiter)) delimiter = null;
|
|
4890
|
+
return;
|
|
4891
|
+
}
|
|
4892
|
+
for (const candidate of ['"""', "'''"]) {
|
|
4893
|
+
const start = line.indexOf(candidate);
|
|
4894
|
+
if (start === -1) continue;
|
|
4895
|
+
if (line.indexOf(candidate, start + candidate.length) !== -1) return;
|
|
4896
|
+
delimiter = candidate;
|
|
4897
|
+
inside.add(index);
|
|
4898
|
+
return;
|
|
4899
|
+
}
|
|
4900
|
+
});
|
|
4901
|
+
return inside;
|
|
4902
|
+
}
|
|
4903
|
+
function skippable(line, index, prose) {
|
|
4904
|
+
return isComment(line) || DEFINITION_PREFIX.test(line) || (prose?.has(index) ?? false);
|
|
4905
|
+
}
|
|
4906
|
+
function windowText(lines, index, back, forward, prose) {
|
|
4907
|
+
const from = Math.max(0, index - back);
|
|
4908
|
+
const to = Math.min(lines.length - 1, index + forward);
|
|
4909
|
+
const collected = [];
|
|
4910
|
+
for (let i = from; i <= to; i += 1) {
|
|
4911
|
+
const line = lines[i] ?? "";
|
|
4912
|
+
if (i !== index && skippable(line, i, prose)) continue;
|
|
4913
|
+
collected.push(line);
|
|
4914
|
+
}
|
|
4915
|
+
return collected.join("\n");
|
|
4916
|
+
}
|
|
4917
|
+
function evaluateRule(rule, ctx) {
|
|
4918
|
+
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
4919
|
+
const line = ctx.lines[ctx.index] ?? "";
|
|
4920
|
+
if (isComment(line) || ctx.prose?.has(ctx.index)) return null;
|
|
4921
|
+
if (!rule.pattern.test(line)) return null;
|
|
4922
|
+
const back = rule.guardBack ?? 8;
|
|
4923
|
+
const forward = rule.guardForward ?? 0;
|
|
4924
|
+
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
4925
|
+
if (rule.requires && !rule.requires.test(context)) return null;
|
|
4926
|
+
if (rule.lineGuard?.test(line)) return null;
|
|
4927
|
+
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
4928
|
+
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
4929
|
+
const untrusted = untrustedPatternFor(ctx.language);
|
|
4930
|
+
const contextual = untrusted.test(line) || untrusted.test(context);
|
|
4931
|
+
if (rule.needsContext && !contextual) return null;
|
|
4932
|
+
const confidence = contextual ? "contextual" : "pattern";
|
|
4933
|
+
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
4934
|
+
}
|
|
4935
|
+
|
|
4936
|
+
// src/scan/manifest-rules.ts
|
|
4937
|
+
var POPULAR_NPM = [
|
|
4938
|
+
"react",
|
|
4939
|
+
"react-dom",
|
|
4940
|
+
"lodash",
|
|
4941
|
+
"express",
|
|
4942
|
+
"axios",
|
|
4943
|
+
"chalk",
|
|
4944
|
+
"commander",
|
|
4945
|
+
"debug",
|
|
4946
|
+
"moment",
|
|
4947
|
+
"dayjs",
|
|
4948
|
+
"uuid",
|
|
4949
|
+
"dotenv",
|
|
4950
|
+
"typescript",
|
|
4951
|
+
"webpack",
|
|
4952
|
+
"vite",
|
|
4953
|
+
"rollup",
|
|
4954
|
+
"eslint",
|
|
4955
|
+
"prettier",
|
|
4956
|
+
"jest",
|
|
4957
|
+
"vitest",
|
|
4958
|
+
"mocha",
|
|
4959
|
+
"chai",
|
|
4960
|
+
"sinon",
|
|
4961
|
+
"request",
|
|
4962
|
+
"node-fetch",
|
|
4963
|
+
"cross-env",
|
|
4964
|
+
"rimraf",
|
|
4965
|
+
"glob",
|
|
4966
|
+
"minimist",
|
|
4967
|
+
"yargs",
|
|
4968
|
+
"inquirer",
|
|
4969
|
+
"colors",
|
|
4970
|
+
"ora",
|
|
4971
|
+
"semver",
|
|
4972
|
+
"ws",
|
|
4973
|
+
"socket.io",
|
|
4974
|
+
"mongoose",
|
|
4975
|
+
"sequelize",
|
|
4976
|
+
"knex",
|
|
4977
|
+
"pg",
|
|
4978
|
+
"mysql",
|
|
4979
|
+
"mysql2",
|
|
4980
|
+
"redis",
|
|
4981
|
+
"ioredis",
|
|
4982
|
+
"jsonwebtoken",
|
|
4983
|
+
"bcrypt",
|
|
4984
|
+
"passport",
|
|
4985
|
+
"cors",
|
|
4986
|
+
"helmet",
|
|
4987
|
+
"morgan",
|
|
4988
|
+
"body-parser",
|
|
4989
|
+
"multer",
|
|
4990
|
+
"nodemailer",
|
|
4991
|
+
"puppeteer",
|
|
4992
|
+
"playwright",
|
|
4993
|
+
"cheerio",
|
|
4994
|
+
"sharp",
|
|
4995
|
+
"canvas",
|
|
4996
|
+
"esbuild",
|
|
4997
|
+
"babel",
|
|
4998
|
+
"postcss",
|
|
4999
|
+
"tailwindcss",
|
|
5000
|
+
"next",
|
|
5001
|
+
"nuxt",
|
|
5002
|
+
"vue",
|
|
5003
|
+
"svelte",
|
|
5004
|
+
"angular",
|
|
5005
|
+
"rxjs",
|
|
5006
|
+
"zod"
|
|
5007
|
+
];
|
|
5008
|
+
var POPULAR_PYPI = [
|
|
5009
|
+
"requests",
|
|
5010
|
+
"urllib3",
|
|
5011
|
+
"numpy",
|
|
5012
|
+
"pandas",
|
|
5013
|
+
"scipy",
|
|
5014
|
+
"flask",
|
|
5015
|
+
"django",
|
|
5016
|
+
"fastapi",
|
|
5017
|
+
"sqlalchemy",
|
|
5018
|
+
"pydantic",
|
|
5019
|
+
"click",
|
|
5020
|
+
"jinja2",
|
|
5021
|
+
"pyyaml",
|
|
5022
|
+
"boto3",
|
|
5023
|
+
"botocore",
|
|
5024
|
+
"setuptools",
|
|
5025
|
+
"wheel",
|
|
5026
|
+
"pip",
|
|
5027
|
+
"six",
|
|
5028
|
+
"certifi",
|
|
5029
|
+
"idna",
|
|
5030
|
+
"chardet",
|
|
5031
|
+
"attrs",
|
|
5032
|
+
"python-dateutil",
|
|
5033
|
+
"pytz",
|
|
5034
|
+
"pytest",
|
|
5035
|
+
"tox",
|
|
5036
|
+
"black",
|
|
5037
|
+
"flake8",
|
|
5038
|
+
"mypy",
|
|
5039
|
+
"isort",
|
|
5040
|
+
"beautifulsoup4",
|
|
5041
|
+
"lxml",
|
|
5042
|
+
"pillow",
|
|
5043
|
+
"matplotlib",
|
|
5044
|
+
"seaborn",
|
|
5045
|
+
"scikit-learn",
|
|
5046
|
+
"tensorflow",
|
|
5047
|
+
"torch",
|
|
5048
|
+
"transformers",
|
|
5049
|
+
"openai",
|
|
5050
|
+
"anthropic",
|
|
5051
|
+
"httpx",
|
|
5052
|
+
"aiohttp",
|
|
5053
|
+
"celery",
|
|
5054
|
+
"redis",
|
|
5055
|
+
"psycopg2",
|
|
5056
|
+
"pymongo",
|
|
5057
|
+
"cryptography",
|
|
5058
|
+
"paramiko",
|
|
5059
|
+
"colorama"
|
|
4322
5060
|
];
|
|
5061
|
+
var INTERNAL_MARKER = /(?:^|[-_/@])(?:internal|private|corp|intranet|inhouse|confidential)(?:$|[-_/])/i;
|
|
5062
|
+
function normalizeName(name) {
|
|
5063
|
+
return name.toLowerCase().replace(/^@/, "").replace(/[-_.\s]/g, "");
|
|
5064
|
+
}
|
|
5065
|
+
function editDistance(a, b, cap = 3) {
|
|
5066
|
+
if (a === b) return 0;
|
|
5067
|
+
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
5068
|
+
const rows = [];
|
|
5069
|
+
for (let i = 0; i <= a.length; i += 1) {
|
|
5070
|
+
rows.push(new Array(b.length + 1).fill(0));
|
|
5071
|
+
rows[i][0] = i;
|
|
5072
|
+
}
|
|
5073
|
+
for (let j = 0; j <= b.length; j += 1) rows[0][j] = j;
|
|
5074
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
5075
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
5076
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
5077
|
+
let best = Math.min(
|
|
5078
|
+
rows[i - 1][j] + 1,
|
|
5079
|
+
rows[i][j - 1] + 1,
|
|
5080
|
+
rows[i - 1][j - 1] + cost
|
|
5081
|
+
);
|
|
5082
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
5083
|
+
best = Math.min(best, rows[i - 2][j - 2] + 1);
|
|
5084
|
+
}
|
|
5085
|
+
rows[i][j] = best;
|
|
5086
|
+
}
|
|
5087
|
+
}
|
|
5088
|
+
return rows[a.length][b.length];
|
|
5089
|
+
}
|
|
5090
|
+
function detectTyposquat(name, ecosystem) {
|
|
5091
|
+
const popular = ecosystem === "npm" ? POPULAR_NPM : POPULAR_PYPI;
|
|
5092
|
+
const lower = name.toLowerCase().replace(/^@[^/]+\//, "");
|
|
5093
|
+
if (popular.includes(lower)) return null;
|
|
5094
|
+
if (lower.length < 4) return null;
|
|
5095
|
+
const normalized = normalizeName(lower);
|
|
5096
|
+
for (const candidate of popular) {
|
|
5097
|
+
const candidateNormalized = normalizeName(candidate);
|
|
5098
|
+
if (normalized === candidateNormalized) return { impersonates: candidate, kind: "separator" };
|
|
5099
|
+
if (editDistance(normalized, candidateNormalized, 1) === 1) {
|
|
5100
|
+
return { impersonates: candidate, kind: "edit" };
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
return null;
|
|
5104
|
+
}
|
|
5105
|
+
var LIFECYCLE_SCRIPTS = ["preinstall", "install", "postinstall", "prepare", "prepublish"];
|
|
5106
|
+
function scanPackageJson(text) {
|
|
5107
|
+
const findings = [];
|
|
5108
|
+
const lines = text.split("\n");
|
|
5109
|
+
let parsed;
|
|
5110
|
+
try {
|
|
5111
|
+
parsed = JSON.parse(text);
|
|
5112
|
+
} catch {
|
|
5113
|
+
return findings;
|
|
5114
|
+
}
|
|
5115
|
+
const lineOf = (needle) => {
|
|
5116
|
+
const index = lines.findIndex((line) => line.includes(`"${needle}"`));
|
|
5117
|
+
return index === -1 ? 1 : index + 1;
|
|
5118
|
+
};
|
|
5119
|
+
const depBuckets = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
5120
|
+
for (const bucket of depBuckets) {
|
|
5121
|
+
const deps = parsed[bucket];
|
|
5122
|
+
if (!deps || typeof deps !== "object") continue;
|
|
5123
|
+
for (const name of Object.keys(deps)) {
|
|
5124
|
+
const line = lineOf(name);
|
|
5125
|
+
if (INTERNAL_MARKER.test(name)) {
|
|
5126
|
+
findings.push({
|
|
5127
|
+
ruleId: "manifest-dependency-confusion",
|
|
5128
|
+
title: "internal-looking package resolved from a public registry",
|
|
5129
|
+
line,
|
|
5130
|
+
severity: "critical",
|
|
5131
|
+
cwe: "CWE-1357",
|
|
5132
|
+
message: `"${name}" names itself as internal but carries no registry pin`,
|
|
5133
|
+
consequence: "Whoever registers this name publicly first wins the resolution, and their code runs in your build.",
|
|
5134
|
+
excerpt: (lines[line - 1] ?? "").trim()
|
|
5135
|
+
});
|
|
5136
|
+
continue;
|
|
5137
|
+
}
|
|
5138
|
+
const squat = detectTyposquat(name, "npm");
|
|
5139
|
+
if (squat) {
|
|
5140
|
+
findings.push({
|
|
5141
|
+
ruleId: "manifest-typosquat",
|
|
5142
|
+
title: "dependency name close to a popular package",
|
|
5143
|
+
line,
|
|
5144
|
+
severity: "high",
|
|
5145
|
+
cwe: "CWE-1357",
|
|
5146
|
+
message: squat.kind === "separator" ? `"${name}" differs from "${squat.impersonates}" only in punctuation` : `"${name}" is one edit from "${squat.impersonates}"`,
|
|
5147
|
+
consequence: "A reviewer scanning a diff reads the name they expected. The package that installs is the one that was written.",
|
|
5148
|
+
excerpt: (lines[line - 1] ?? "").trim()
|
|
5149
|
+
});
|
|
5150
|
+
}
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
const scripts = parsed.scripts;
|
|
5154
|
+
if (scripts && typeof scripts === "object") {
|
|
5155
|
+
for (const [name, body] of Object.entries(scripts)) {
|
|
5156
|
+
if (!LIFECYCLE_SCRIPTS.includes(name)) continue;
|
|
5157
|
+
findings.push({
|
|
5158
|
+
ruleId: "manifest-install-lifecycle-script",
|
|
5159
|
+
title: "install-time lifecycle script",
|
|
5160
|
+
line: lineOf(name),
|
|
5161
|
+
severity: "medium",
|
|
5162
|
+
cwe: "CWE-506",
|
|
5163
|
+
message: `"${name}" runs automatically on install: ${String(body).slice(0, 120)}`,
|
|
5164
|
+
consequence: "Lifecycle scripts run with the installing user\u2019s privileges and network access, before any code is reviewed. It is the execution vector every notable npm compromise has used.",
|
|
5165
|
+
excerpt: (lines[lineOf(name) - 1] ?? "").trim()
|
|
5166
|
+
});
|
|
5167
|
+
}
|
|
5168
|
+
}
|
|
5169
|
+
return findings;
|
|
5170
|
+
}
|
|
5171
|
+
function scanRequirementsTxt(text) {
|
|
5172
|
+
const findings = [];
|
|
5173
|
+
const lines = text.split("\n");
|
|
5174
|
+
lines.forEach((raw, index) => {
|
|
5175
|
+
const line = raw.trim();
|
|
5176
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) return;
|
|
5177
|
+
const match = /^([A-Za-z0-9_.-]+)\s*(?:[=<>!~]=|@|$)/.exec(line);
|
|
5178
|
+
const name = match?.[1];
|
|
5179
|
+
if (!name) return;
|
|
5180
|
+
if (INTERNAL_MARKER.test(name)) {
|
|
5181
|
+
findings.push({
|
|
5182
|
+
ruleId: "manifest-dependency-confusion",
|
|
5183
|
+
title: "internal-looking package resolved from a public index",
|
|
5184
|
+
line: index + 1,
|
|
5185
|
+
severity: "critical",
|
|
5186
|
+
cwe: "CWE-1357",
|
|
5187
|
+
message: `"${name}" names itself as internal but carries no index pin`,
|
|
5188
|
+
consequence: "pip resolves the highest version across every configured index, so a public package of the same name shadows the private one.",
|
|
5189
|
+
excerpt: line
|
|
5190
|
+
});
|
|
5191
|
+
return;
|
|
5192
|
+
}
|
|
5193
|
+
const squat = detectTyposquat(name, "pypi");
|
|
5194
|
+
if (squat) {
|
|
5195
|
+
findings.push({
|
|
5196
|
+
ruleId: "manifest-typosquat",
|
|
5197
|
+
title: "dependency name close to a popular package",
|
|
5198
|
+
line: index + 1,
|
|
5199
|
+
severity: "high",
|
|
5200
|
+
cwe: "CWE-1357",
|
|
5201
|
+
message: squat.kind === "separator" ? `"${name}" differs from "${squat.impersonates}" only in punctuation` : `"${name}" is one edit from "${squat.impersonates}"`,
|
|
5202
|
+
consequence: "A reviewer scanning a diff reads the name they expected. The package that installs is the one that was written.",
|
|
5203
|
+
excerpt: line
|
|
5204
|
+
});
|
|
5205
|
+
}
|
|
5206
|
+
});
|
|
5207
|
+
return findings;
|
|
5208
|
+
}
|
|
5209
|
+
|
|
5210
|
+
// src/scan/secret-rules.ts
|
|
5211
|
+
var SECRET_RULES = [
|
|
5212
|
+
{
|
|
5213
|
+
id: "secret-aws-access-key",
|
|
5214
|
+
name: "AWS Access Key",
|
|
5215
|
+
pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/,
|
|
5216
|
+
severity: "critical",
|
|
5217
|
+
cwe: "CWE-798",
|
|
5218
|
+
consequence: "Paired with a secret key, grants the API access of whatever IAM principal issued it."
|
|
5219
|
+
},
|
|
5220
|
+
{
|
|
5221
|
+
id: "secret-aws-secret-key",
|
|
5222
|
+
name: "AWS Secret Access Key",
|
|
5223
|
+
pattern: /(?:aws_secret_access_key|AWS_SECRET(?:_ACCESS_KEY)?)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/i,
|
|
5224
|
+
severity: "critical",
|
|
5225
|
+
cwe: "CWE-798",
|
|
5226
|
+
consequence: "The other half of an AWS credential pair; on its own it is still the hard half to guess."
|
|
5227
|
+
},
|
|
5228
|
+
{
|
|
5229
|
+
id: "secret-github-token",
|
|
5230
|
+
name: "GitHub Token",
|
|
5231
|
+
pattern: /\b(?:ghp_[A-Za-z0-9]{36}|gho_[A-Za-z0-9]{36}|ghu_[A-Za-z0-9]{36}|ghs_[A-Za-z0-9]{36}|ghr_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]{60,})\b/,
|
|
5232
|
+
severity: "critical",
|
|
5233
|
+
cwe: "CWE-798",
|
|
5234
|
+
consequence: "Repository read or write as the issuing account, including the ability to push workflow changes."
|
|
5235
|
+
},
|
|
5236
|
+
{
|
|
5237
|
+
id: "secret-npm-token",
|
|
5238
|
+
name: "npm Token",
|
|
5239
|
+
pattern: /\bnpm_[A-Za-z0-9]{36}\b/,
|
|
5240
|
+
severity: "critical",
|
|
5241
|
+
cwe: "CWE-798",
|
|
5242
|
+
consequence: "Publish rights to every package the account owns \u2014 a supply-chain compromise in one command."
|
|
5243
|
+
},
|
|
5244
|
+
{
|
|
5245
|
+
id: "secret-private-key",
|
|
5246
|
+
name: "Private Key",
|
|
5247
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/,
|
|
5248
|
+
severity: "critical",
|
|
5249
|
+
cwe: "CWE-798",
|
|
5250
|
+
consequence: "Key material, committed. Rotation is the only remediation."
|
|
5251
|
+
},
|
|
5252
|
+
{
|
|
5253
|
+
id: "secret-slack-token",
|
|
5254
|
+
name: "Slack Token",
|
|
5255
|
+
pattern: /\bxox[bpoasr]-[A-Za-z0-9-]{10,}/,
|
|
5256
|
+
severity: "critical",
|
|
5257
|
+
cwe: "CWE-798",
|
|
5258
|
+
consequence: "Read and post access to the workspace as the installing app."
|
|
5259
|
+
},
|
|
5260
|
+
{
|
|
5261
|
+
id: "secret-slack-webhook",
|
|
5262
|
+
name: "Slack Webhook URL",
|
|
5263
|
+
pattern: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_+\/-]{6,}/,
|
|
5264
|
+
severity: "high",
|
|
5265
|
+
cwe: "CWE-798",
|
|
5266
|
+
consequence: "The URL *is* the credential \u2014 anyone holding it can post to that channel."
|
|
5267
|
+
},
|
|
5268
|
+
{
|
|
5269
|
+
id: "secret-stripe-key",
|
|
5270
|
+
name: "Stripe Key",
|
|
5271
|
+
pattern: /\b(?:sk_live_|rk_live_|sk_test_|rk_test_)[A-Za-z0-9]{20,}\b/,
|
|
5272
|
+
severity: "critical",
|
|
5273
|
+
cwe: "CWE-798",
|
|
5274
|
+
consequence: "Charge, refund and customer-data access against the account."
|
|
5275
|
+
},
|
|
5276
|
+
{
|
|
5277
|
+
id: "secret-sendgrid-key",
|
|
5278
|
+
name: "SendGrid API Key",
|
|
5279
|
+
pattern: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/,
|
|
5280
|
+
severity: "critical",
|
|
5281
|
+
cwe: "CWE-798",
|
|
5282
|
+
consequence: "Send mail as the domain \u2014 the credential behind most convincing phishing from a real sender."
|
|
5283
|
+
},
|
|
5284
|
+
{
|
|
5285
|
+
id: "secret-google-api-key",
|
|
5286
|
+
name: "Google API Key",
|
|
5287
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
5288
|
+
severity: "high",
|
|
5289
|
+
cwe: "CWE-798",
|
|
5290
|
+
consequence: "Quota theft at minimum; API access to whatever the key was scoped to at worst."
|
|
5291
|
+
},
|
|
5292
|
+
{
|
|
5293
|
+
id: "secret-openai-key",
|
|
5294
|
+
name: "OpenAI API Key",
|
|
5295
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/,
|
|
5296
|
+
severity: "critical",
|
|
5297
|
+
cwe: "CWE-798",
|
|
5298
|
+
consequence: "Billed inference against the owner\u2019s account, with no per-key spend limit by default."
|
|
5299
|
+
},
|
|
5300
|
+
{
|
|
5301
|
+
id: "secret-anthropic-key",
|
|
5302
|
+
name: "Anthropic API Key",
|
|
5303
|
+
pattern: /\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{32,}\b/,
|
|
5304
|
+
severity: "critical",
|
|
5305
|
+
cwe: "CWE-798",
|
|
5306
|
+
consequence: "Billed inference against the owner\u2019s account."
|
|
5307
|
+
},
|
|
5308
|
+
{
|
|
5309
|
+
id: "secret-database-url",
|
|
5310
|
+
name: "Database URL with credentials",
|
|
5311
|
+
// Requires a credential segment before the `@` — `postgres://localhost/db`
|
|
5312
|
+
// is a hostname, not a secret.
|
|
5313
|
+
pattern: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|clickhouse):\/\/[^\s'"@\/]*:[^\s'"@\/]*@[^\s'"]+/i,
|
|
5314
|
+
severity: "high",
|
|
5315
|
+
cwe: "CWE-798",
|
|
5316
|
+
consequence: "Direct database access, usually bypassing every application-level authorisation check."
|
|
5317
|
+
},
|
|
5318
|
+
{
|
|
5319
|
+
id: "secret-jwt",
|
|
5320
|
+
name: "JSON Web Token",
|
|
5321
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
5322
|
+
severity: "medium",
|
|
5323
|
+
cwe: "CWE-798",
|
|
5324
|
+
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
|
|
5325
|
+
},
|
|
5326
|
+
{
|
|
5327
|
+
id: "secret-generic-api-key",
|
|
5328
|
+
name: "Generic API Key",
|
|
5329
|
+
// Quoted assignment only. An unquoted value in a `.env` is covered by the
|
|
5330
|
+
// vendor-prefixed rules above; matching it here is what starts flagging
|
|
5331
|
+
// ARNs and parameter-store paths.
|
|
5332
|
+
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
5333
|
+
severity: "high",
|
|
5334
|
+
cwe: "CWE-798",
|
|
5335
|
+
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
|
|
5336
|
+
},
|
|
5337
|
+
{
|
|
5338
|
+
id: "secret-generic-credential",
|
|
5339
|
+
name: "Hardcoded Credential",
|
|
5340
|
+
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
5341
|
+
severity: "high",
|
|
5342
|
+
cwe: "CWE-798",
|
|
5343
|
+
consequence: "A password in source is a password in every clone, fork and CI cache of that source."
|
|
5344
|
+
},
|
|
5345
|
+
{
|
|
5346
|
+
id: "secret-hex-token",
|
|
5347
|
+
name: "High-entropy Hex Token",
|
|
5348
|
+
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
5349
|
+
severity: "medium",
|
|
5350
|
+
cwe: "CWE-798",
|
|
5351
|
+
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
|
|
5352
|
+
}
|
|
5353
|
+
];
|
|
5354
|
+
var KNOWN_PLACEHOLDERS = [
|
|
5355
|
+
// Deliberately NOT here: AWS's published documentation key/secret pair
|
|
5356
|
+
// (`AKIAIOSFODNN7EXAMPLE`, `wJalrXUtnFEMI/…`). GitHub allow-lists them, and
|
|
5357
|
+
// the argument for following suit is that they authenticate nothing. The
|
|
5358
|
+
// argument against is stronger: they appear in a repository because someone
|
|
5359
|
+
// pasted a credentials template and left it there, and the remediation —
|
|
5360
|
+
// move this to the secret manager — is identical to the one for a live key.
|
|
5361
|
+
// Exempting them means the scanner goes quiet on the file most likely to
|
|
5362
|
+
// acquire a real key next.
|
|
5363
|
+
/\bEXAMPLE_?KEY\b/i,
|
|
5364
|
+
/\bYOUR_[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)\b/,
|
|
5365
|
+
/\b(?:xxx+|X{4,}|\*{4,}|<[a-z-]+>)\b/,
|
|
5366
|
+
/\bchangeme\b/i
|
|
5367
|
+
];
|
|
5368
|
+
function isKnownPlaceholder(text) {
|
|
5369
|
+
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
5370
|
+
}
|
|
5371
|
+
function redactSecret(line) {
|
|
5372
|
+
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
5373
|
+
if (match.length <= 12) return match;
|
|
5374
|
+
return `${match.slice(0, 3)}${"*".repeat(Math.min(16, match.length - 3))}`;
|
|
5375
|
+
});
|
|
5376
|
+
}
|
|
5377
|
+
var SENSITIVE_FILES = [
|
|
5378
|
+
{ pattern: ".env", message: "Environment file committed \u2014 the usual home of every runtime credential", severity: "high" },
|
|
5379
|
+
{ pattern: ".env.local", message: "Local environment file committed", severity: "high" },
|
|
5380
|
+
{ pattern: ".env.production", message: "Production environment file committed", severity: "critical" },
|
|
5381
|
+
{ pattern: "id_rsa", message: "Private SSH key committed", severity: "critical" },
|
|
5382
|
+
{ pattern: "id_ed25519", message: "Private SSH key committed", severity: "critical" },
|
|
5383
|
+
{ pattern: "id_ecdsa", message: "Private SSH key committed", severity: "critical" },
|
|
5384
|
+
{ pattern: ".pem", message: "PEM certificate or key file committed", severity: "high" },
|
|
5385
|
+
{ pattern: ".p12", message: "PKCS#12 keystore committed", severity: "high" },
|
|
5386
|
+
{ pattern: ".pfx", message: "PKCS#12 keystore committed", severity: "high" },
|
|
5387
|
+
{ pattern: ".keystore", message: "Java keystore committed", severity: "high" }
|
|
5388
|
+
// Deliberately not `.npmrc`. Its presence is normal; only an `_authToken`
|
|
5389
|
+
// line in it is a credential, and that is a content match, not a filename
|
|
5390
|
+
// match. Reporting the file itself trades a real finding for a chore.
|
|
5391
|
+
];
|
|
5392
|
+
|
|
5393
|
+
// src/scan/engine.ts
|
|
4323
5394
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
4324
5395
|
"node_modules",
|
|
4325
5396
|
".git",
|
|
4326
5397
|
".next",
|
|
5398
|
+
".nuxt",
|
|
4327
5399
|
"dist",
|
|
4328
5400
|
"build",
|
|
5401
|
+
"out",
|
|
4329
5402
|
"__pycache__",
|
|
4330
5403
|
".venv",
|
|
5404
|
+
"venv",
|
|
4331
5405
|
"vendor",
|
|
4332
5406
|
".terraform",
|
|
4333
5407
|
"coverage",
|
|
4334
|
-
".cache"
|
|
5408
|
+
".cache",
|
|
5409
|
+
".pnpm-store",
|
|
5410
|
+
"target",
|
|
5411
|
+
".gradle",
|
|
5412
|
+
".idea",
|
|
5413
|
+
".vscode",
|
|
5414
|
+
"bower_components",
|
|
5415
|
+
".svelte-kit"
|
|
4335
5416
|
]);
|
|
4336
5417
|
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
4337
5418
|
".ts",
|
|
4338
5419
|
".js",
|
|
4339
5420
|
".tsx",
|
|
4340
5421
|
".jsx",
|
|
5422
|
+
".mjs",
|
|
5423
|
+
".cjs",
|
|
5424
|
+
".mts",
|
|
5425
|
+
".cts",
|
|
4341
5426
|
".py",
|
|
4342
5427
|
".rb",
|
|
4343
5428
|
".go",
|
|
4344
5429
|
".java",
|
|
5430
|
+
".kt",
|
|
5431
|
+
".scala",
|
|
4345
5432
|
".php",
|
|
4346
5433
|
".rs",
|
|
4347
5434
|
".c",
|
|
5435
|
+
".cc",
|
|
4348
5436
|
".cpp",
|
|
4349
5437
|
".h",
|
|
5438
|
+
".hpp",
|
|
5439
|
+
".cs",
|
|
5440
|
+
".swift",
|
|
4350
5441
|
".yml",
|
|
4351
5442
|
".yaml",
|
|
4352
5443
|
".json",
|
|
@@ -4357,191 +5448,324 @@ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
4357
5448
|
".env",
|
|
4358
5449
|
".sh",
|
|
4359
5450
|
".bash",
|
|
5451
|
+
".zsh",
|
|
4360
5452
|
".tf",
|
|
4361
5453
|
".hcl",
|
|
4362
5454
|
".xml",
|
|
4363
5455
|
".properties",
|
|
4364
|
-
".gradle"
|
|
5456
|
+
".gradle",
|
|
5457
|
+
".txt",
|
|
5458
|
+
".md",
|
|
5459
|
+
".sql",
|
|
5460
|
+
".erb",
|
|
5461
|
+
".ejs",
|
|
5462
|
+
".vue",
|
|
5463
|
+
".svelte"
|
|
4365
5464
|
]);
|
|
4366
|
-
|
|
4367
|
-
|
|
4368
|
-
|
|
4369
|
-
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
4375
|
-
|
|
4376
|
-
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
|
|
4388
|
-
|
|
4389
|
-
|
|
4390
|
-
|
|
4391
|
-
|
|
4392
|
-
|
|
4393
|
-
|
|
4394
|
-
|
|
4395
|
-
|
|
4396
|
-
|
|
5465
|
+
var LANGUAGE_BY_EXTENSION = {
|
|
5466
|
+
".js": "javascript",
|
|
5467
|
+
".jsx": "javascript",
|
|
5468
|
+
".mjs": "javascript",
|
|
5469
|
+
".cjs": "javascript",
|
|
5470
|
+
".ts": "typescript",
|
|
5471
|
+
".tsx": "typescript",
|
|
5472
|
+
".mts": "typescript",
|
|
5473
|
+
".cts": "typescript",
|
|
5474
|
+
".vue": "javascript",
|
|
5475
|
+
".svelte": "javascript",
|
|
5476
|
+
".ejs": "javascript",
|
|
5477
|
+
".py": "python",
|
|
5478
|
+
".rb": "ruby",
|
|
5479
|
+
".erb": "ruby",
|
|
5480
|
+
".go": "go",
|
|
5481
|
+
".java": "java",
|
|
5482
|
+
".kt": "java",
|
|
5483
|
+
".scala": "java",
|
|
5484
|
+
".php": "php",
|
|
5485
|
+
".sh": "shell",
|
|
5486
|
+
".bash": "shell",
|
|
5487
|
+
".zsh": "shell",
|
|
5488
|
+
".yml": "config",
|
|
5489
|
+
".yaml": "config",
|
|
5490
|
+
".json": "config",
|
|
5491
|
+
".toml": "config",
|
|
5492
|
+
".ini": "config",
|
|
5493
|
+
".cfg": "config",
|
|
5494
|
+
".conf": "config",
|
|
5495
|
+
".env": "config",
|
|
5496
|
+
".tf": "config",
|
|
5497
|
+
".hcl": "config",
|
|
5498
|
+
".properties": "config"
|
|
5499
|
+
};
|
|
5500
|
+
function languageOf(filename) {
|
|
5501
|
+
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
5502
|
+
return LANGUAGE_BY_EXTENSION[(0, import_node_path6.extname)(filename).toLowerCase()] ?? "other";
|
|
5503
|
+
}
|
|
5504
|
+
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
5505
|
+
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
5506
|
+
function collectSuppressions(lines) {
|
|
5507
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
5508
|
+
let count = 0;
|
|
5509
|
+
const add = (index, ruleId) => {
|
|
5510
|
+
const existing = byLine.get(index) ?? /* @__PURE__ */ new Set();
|
|
5511
|
+
existing.add(ruleId ?? "*");
|
|
5512
|
+
byLine.set(index, existing);
|
|
5513
|
+
count += 1;
|
|
4397
5514
|
};
|
|
5515
|
+
lines.forEach((line, index) => {
|
|
5516
|
+
const next = SUPPRESS_NEXT.exec(line);
|
|
5517
|
+
if (next) add(index + 1, next[1]);
|
|
5518
|
+
const same = SUPPRESS_LINE.exec(line);
|
|
5519
|
+
if (same && !next) add(index, same[1]);
|
|
5520
|
+
});
|
|
5521
|
+
return { byLine, count };
|
|
4398
5522
|
}
|
|
4399
|
-
function
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
5523
|
+
function isSuppressed(suppressions, index, ruleId) {
|
|
5524
|
+
const rules = suppressions.byLine.get(index);
|
|
5525
|
+
if (!rules) return false;
|
|
5526
|
+
return rules.has("*") || rules.has(ruleId);
|
|
5527
|
+
}
|
|
5528
|
+
function isTestPath(relativePath) {
|
|
5529
|
+
const p = relativePath.replace(/\\/g, "/");
|
|
5530
|
+
return /(?:^|\/)(?:tests?|__tests__|__mocks__|spec|specs|fixtures?|mocks?|e2e|testdata)\//i.test(p) || /(?:^|\/)(?:test|conftest)_[^/]+$/i.test(p) || /[._-](?:test|spec)\.[a-z]+$/i.test(p) || /_test\.[a-z]+$/i.test(p);
|
|
5531
|
+
}
|
|
5532
|
+
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
5533
|
+
const findings = [];
|
|
5534
|
+
const lines = text.split("\n");
|
|
5535
|
+
const suppressions = collectSuppressions(lines);
|
|
5536
|
+
const inTests = isTestPath(relativePath);
|
|
5537
|
+
lines.forEach((line, index) => {
|
|
5538
|
+
for (const rule of SECRET_RULES) {
|
|
5539
|
+
const match = rule.pattern.exec(line);
|
|
5540
|
+
if (!match) continue;
|
|
5541
|
+
if (isKnownPlaceholder(match[0])) continue;
|
|
5542
|
+
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
5543
|
+
findings.push({
|
|
5544
|
+
ruleId: rule.id,
|
|
5545
|
+
title: rule.name,
|
|
5546
|
+
file: relativePath,
|
|
5547
|
+
line: index + 1,
|
|
5548
|
+
// Reported but not blocking in tests — see isTestPath.
|
|
5549
|
+
severity: inTests ? "low" : rule.severity,
|
|
5550
|
+
// A matched credential format is the finding, not a proxy for one.
|
|
5551
|
+
confidence: "evidence",
|
|
5552
|
+
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`,
|
|
5553
|
+
consequence: rule.consequence,
|
|
5554
|
+
cwe: rule.cwe,
|
|
5555
|
+
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
5556
|
+
sensitive: true,
|
|
5557
|
+
category: "secret"
|
|
5558
|
+
});
|
|
4412
5559
|
}
|
|
4413
|
-
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
4422
|
-
|
|
4423
|
-
|
|
4424
|
-
|
|
5560
|
+
});
|
|
5561
|
+
const prose = proseLines(lines);
|
|
5562
|
+
lines.forEach((_line, index) => {
|
|
5563
|
+
for (const rule of CODE_RULES) {
|
|
5564
|
+
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
5565
|
+
const match = evaluateRule(rule, { lines, index, language, prose });
|
|
5566
|
+
if (!match) continue;
|
|
5567
|
+
findings.push({
|
|
5568
|
+
ruleId: rule.id,
|
|
5569
|
+
title: rule.title,
|
|
5570
|
+
file: relativePath,
|
|
5571
|
+
line: index + 1,
|
|
5572
|
+
severity: match.severity,
|
|
5573
|
+
confidence: match.confidence,
|
|
5574
|
+
message: `${rule.title} (${rule.cwe})`,
|
|
5575
|
+
consequence: rule.consequence,
|
|
5576
|
+
cwe: rule.cwe,
|
|
5577
|
+
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
5578
|
+
category: "code"
|
|
5579
|
+
});
|
|
4425
5580
|
}
|
|
4426
|
-
|
|
4427
|
-
|
|
5581
|
+
});
|
|
5582
|
+
return findings;
|
|
5583
|
+
}
|
|
5584
|
+
function scanManifest(relativePath, filename, text) {
|
|
5585
|
+
const manifestFindings = filename === "package.json" ? scanPackageJson(text) : filename === "requirements.txt" ? scanRequirementsTxt(text) : [];
|
|
5586
|
+
return manifestFindings.map((finding) => ({
|
|
5587
|
+
ruleId: finding.ruleId,
|
|
5588
|
+
title: finding.title,
|
|
5589
|
+
file: relativePath,
|
|
5590
|
+
line: finding.line,
|
|
5591
|
+
severity: finding.severity,
|
|
5592
|
+
confidence: "evidence",
|
|
5593
|
+
message: finding.message,
|
|
5594
|
+
consequence: finding.consequence,
|
|
5595
|
+
cwe: finding.cwe,
|
|
5596
|
+
excerpt: finding.excerpt.slice(0, 200),
|
|
5597
|
+
category: "manifest"
|
|
5598
|
+
}));
|
|
5599
|
+
}
|
|
5600
|
+
function scanPath(targetPath, options = {}) {
|
|
5601
|
+
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
5602
|
+
const allowed = options.categories ? new Set(options.categories) : null;
|
|
5603
|
+
const findings = [];
|
|
5604
|
+
const unreadable = [];
|
|
5605
|
+
let filesScanned = 0;
|
|
5606
|
+
let suppressed = 0;
|
|
5607
|
+
const rootIsDirectory = (() => {
|
|
4428
5608
|
try {
|
|
4429
|
-
|
|
4430
|
-
if (stat.size > 1024 * 1024) continue;
|
|
5609
|
+
return (0, import_node_fs11.statSync)(targetPath).isDirectory();
|
|
4431
5610
|
} catch {
|
|
4432
|
-
|
|
5611
|
+
return true;
|
|
5612
|
+
}
|
|
5613
|
+
})();
|
|
5614
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path6.dirname)(targetPath);
|
|
5615
|
+
const scanFile = (fullPath, filename) => {
|
|
5616
|
+
const relativePath = toRelative(walkRoot, fullPath);
|
|
5617
|
+
const extension = (0, import_node_path6.extname)(filename).toLowerCase();
|
|
5618
|
+
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
5619
|
+
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
5620
|
+
if (!scannable && !isManifest) {
|
|
5621
|
+
recordSensitiveFile(filename, relativePath, findings, []);
|
|
5622
|
+
return;
|
|
4433
5623
|
}
|
|
4434
|
-
|
|
4435
|
-
let
|
|
5624
|
+
let text;
|
|
5625
|
+
let handle;
|
|
4436
5626
|
try {
|
|
4437
|
-
|
|
5627
|
+
handle = (0, import_node_fs11.openSync)(fullPath, "r");
|
|
4438
5628
|
} catch {
|
|
4439
|
-
|
|
5629
|
+
unreadable.push(relativePath);
|
|
5630
|
+
return;
|
|
4440
5631
|
}
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
type: pattern.name,
|
|
4452
|
-
severity: pattern.severity,
|
|
4453
|
-
message: `Possible ${pattern.name} detected`,
|
|
4454
|
-
snippet: line.length > 120 ? line.slice(0, 120) + "..." : line
|
|
4455
|
-
});
|
|
4456
|
-
}
|
|
5632
|
+
try {
|
|
5633
|
+
if ((0, import_node_fs11.fstatSync)(handle).size > maxFileBytes) return;
|
|
5634
|
+
text = (0, import_node_fs11.readFileSync)(handle, "utf-8");
|
|
5635
|
+
} catch {
|
|
5636
|
+
unreadable.push(relativePath);
|
|
5637
|
+
return;
|
|
5638
|
+
} finally {
|
|
5639
|
+
try {
|
|
5640
|
+
(0, import_node_fs11.closeSync)(handle);
|
|
5641
|
+
} catch {
|
|
4457
5642
|
}
|
|
4458
5643
|
}
|
|
4459
|
-
|
|
4460
|
-
|
|
4461
|
-
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
const lockPath = (0, import_node_path5.join)(targetPath, file);
|
|
4472
|
-
if (!(0, import_node_fs10.existsSync)(lockPath)) continue;
|
|
5644
|
+
filesScanned += 1;
|
|
5645
|
+
options.onFile?.(relativePath);
|
|
5646
|
+
suppressed += collectSuppressions(text.split("\n")).count;
|
|
5647
|
+
const fileFindings = [
|
|
5648
|
+
...scanText(relativePath, text, languageOf(filename)),
|
|
5649
|
+
...isManifest ? scanManifest(relativePath, filename, text) : []
|
|
5650
|
+
];
|
|
5651
|
+
findings.push(...fileFindings);
|
|
5652
|
+
recordSensitiveFile(filename, relativePath, findings, fileFindings);
|
|
5653
|
+
};
|
|
5654
|
+
const walk = (currentPath) => {
|
|
5655
|
+
let entries;
|
|
4473
5656
|
try {
|
|
4474
|
-
|
|
4475
|
-
for (const dep of deps.slice(0, 50)) {
|
|
4476
|
-
try {
|
|
4477
|
-
const vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
4478
|
-
for (const vuln of vulns) {
|
|
4479
|
-
const cvssScore = vuln.severity?.find((s) => s.type === "CVSS_V3")?.score;
|
|
4480
|
-
const severity = cvssScore ? parseFloat(cvssScore) >= 9 ? "critical" : parseFloat(cvssScore) >= 7 ? "high" : parseFloat(cvssScore) >= 4 ? "medium" : "low" : "medium";
|
|
4481
|
-
findings.push({
|
|
4482
|
-
file,
|
|
4483
|
-
line: 0,
|
|
4484
|
-
type: "Dependency CVE",
|
|
4485
|
-
severity,
|
|
4486
|
-
message: `${dep.name}@${dep.version}: ${vuln.summary || vuln.id}`,
|
|
4487
|
-
snippet: `${vuln.id}${cvssScore ? ` (CVSS: ${cvssScore})` : ""}`
|
|
4488
|
-
});
|
|
4489
|
-
}
|
|
4490
|
-
} catch {
|
|
4491
|
-
}
|
|
4492
|
-
}
|
|
5657
|
+
entries = (0, import_node_fs11.readdirSync)(currentPath, { withFileTypes: true });
|
|
4493
5658
|
} catch {
|
|
5659
|
+
unreadable.push(toRelative(walkRoot, currentPath));
|
|
5660
|
+
return;
|
|
4494
5661
|
}
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
try {
|
|
4502
|
-
const lock = JSON.parse((0, import_node_fs10.readFileSync)(lockPath, "utf-8"));
|
|
4503
|
-
const packages = lock.packages || lock.dependencies || {};
|
|
4504
|
-
for (const [key, value] of Object.entries(packages)) {
|
|
4505
|
-
const name = key.replace(/^node_modules\//, "");
|
|
4506
|
-
const version = value.version;
|
|
4507
|
-
if (name && version && !name.startsWith(".")) {
|
|
4508
|
-
deps.push({ name, version });
|
|
4509
|
-
}
|
|
5662
|
+
for (const entry of entries) {
|
|
5663
|
+
const fullPath = (0, import_node_path6.join)(currentPath, entry.name);
|
|
5664
|
+
if (entry.isDirectory()) {
|
|
5665
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
5666
|
+
walk(fullPath);
|
|
5667
|
+
continue;
|
|
4510
5668
|
}
|
|
4511
|
-
|
|
5669
|
+
if (!entry.isFile()) continue;
|
|
5670
|
+
scanFile(fullPath, entry.name);
|
|
4512
5671
|
}
|
|
4513
|
-
}
|
|
5672
|
+
};
|
|
5673
|
+
if (rootIsDirectory) {
|
|
5674
|
+
walk(targetPath);
|
|
5675
|
+
} else {
|
|
5676
|
+
scanFile(targetPath, (0, import_node_path6.basename)(targetPath));
|
|
5677
|
+
}
|
|
5678
|
+
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
5679
|
+
filtered.sort(
|
|
5680
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || a.file.localeCompare(b.file) || a.line - b.line
|
|
5681
|
+
);
|
|
5682
|
+
return { findings: filtered, filesScanned, unreadable, suppressed, root: walkRoot };
|
|
5683
|
+
}
|
|
5684
|
+
function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
5685
|
+
if (fileFindings.length > 0) return;
|
|
5686
|
+
for (const sensitive of SENSITIVE_FILES) {
|
|
5687
|
+
const matches = filename === sensitive.pattern || filename.endsWith(sensitive.pattern);
|
|
5688
|
+
if (!matches) continue;
|
|
5689
|
+
sink.push({
|
|
5690
|
+
ruleId: "sensitive-file-committed",
|
|
5691
|
+
title: "Sensitive file",
|
|
5692
|
+
file: relativePath,
|
|
5693
|
+
line: 1,
|
|
5694
|
+
severity: sensitive.severity,
|
|
5695
|
+
confidence: "evidence",
|
|
5696
|
+
message: sensitive.message,
|
|
5697
|
+
consequence: "Anything in this file is in every clone, fork and CI cache of the repository.",
|
|
5698
|
+
cwe: "CWE-538",
|
|
5699
|
+
excerpt: "",
|
|
5700
|
+
sensitive: true,
|
|
5701
|
+
category: "file"
|
|
5702
|
+
});
|
|
5703
|
+
return;
|
|
5704
|
+
}
|
|
5705
|
+
}
|
|
5706
|
+
function toRelative(base, target) {
|
|
5707
|
+
const rel = (0, import_node_path6.relative)(base, target);
|
|
5708
|
+
return (rel === "" ? target : rel).split(import_node_path6.sep).join("/");
|
|
5709
|
+
}
|
|
5710
|
+
|
|
5711
|
+
// src/scan/sarif.ts
|
|
5712
|
+
var import_node_path7 = require("path");
|
|
5713
|
+
|
|
5714
|
+
// src/commands/scan.ts
|
|
5715
|
+
function readVersion() {
|
|
5716
|
+
for (const candidate of [
|
|
5717
|
+
(0, import_node_path8.join)(__dirname, "..", "package.json"),
|
|
5718
|
+
(0, import_node_path8.join)(__dirname, "..", "..", "package.json")
|
|
5719
|
+
]) {
|
|
4514
5720
|
try {
|
|
4515
|
-
|
|
4516
|
-
for (const line of content.split("\n")) {
|
|
4517
|
-
const match = line.match(/^([a-zA-Z0-9_.-]+)==([0-9.]+)/);
|
|
4518
|
-
if (match) deps.push({ name: match[1], version: match[2] });
|
|
4519
|
-
}
|
|
5721
|
+
return JSON.parse((0, import_node_fs12.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
4520
5722
|
} catch {
|
|
4521
5723
|
}
|
|
4522
5724
|
}
|
|
4523
|
-
return
|
|
5725
|
+
return "0.0.0";
|
|
4524
5726
|
}
|
|
4525
|
-
|
|
4526
|
-
|
|
5727
|
+
var PKG_VERSION = readVersion();
|
|
5728
|
+
function toRunResult(targetPath, findings, filesScanned) {
|
|
5729
|
+
const structured = findings.map((finding) => ({
|
|
5730
|
+
type: finding.title,
|
|
5731
|
+
severity: finding.severity,
|
|
5732
|
+
message: finding.message,
|
|
5733
|
+
location: `${finding.file}:${finding.line}`,
|
|
5734
|
+
details: {
|
|
5735
|
+
file: finding.file,
|
|
5736
|
+
line: finding.line,
|
|
5737
|
+
snippet: finding.excerpt,
|
|
5738
|
+
ruleId: finding.ruleId,
|
|
5739
|
+
confidence: finding.confidence,
|
|
5740
|
+
...finding.cwe ? { cwe: finding.cwe } : {}
|
|
5741
|
+
}
|
|
5742
|
+
}));
|
|
5743
|
+
const counts = summarize(structured);
|
|
5744
|
+
return {
|
|
5745
|
+
type: "scan",
|
|
5746
|
+
target: targetPath,
|
|
5747
|
+
findings: structured,
|
|
5748
|
+
severity_summary: counts,
|
|
5749
|
+
summary: findings.length === 0 ? `No issues found across ${filesScanned} files` : `${findings.length} issue(s): ${counts.critical}C ${counts.high}H ${counts.medium}M ${counts.low}L`
|
|
5750
|
+
};
|
|
4527
5751
|
}
|
|
4528
|
-
function
|
|
4529
|
-
return
|
|
5752
|
+
function failedResult(targetPath, message) {
|
|
5753
|
+
return {
|
|
5754
|
+
type: "scan",
|
|
5755
|
+
target: targetPath,
|
|
5756
|
+
findings: [],
|
|
5757
|
+
severity_summary: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
5758
|
+
summary: `Scan failed: ${message}`,
|
|
5759
|
+
error: message
|
|
5760
|
+
};
|
|
4530
5761
|
}
|
|
4531
|
-
async function
|
|
4532
|
-
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
5762
|
+
async function runScan(targetPath) {
|
|
4533
5763
|
try {
|
|
4534
|
-
const
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
});
|
|
4540
|
-
if (!res.ok) return [];
|
|
4541
|
-
const data = await res.json();
|
|
4542
|
-
return data.vulns || [];
|
|
4543
|
-
} catch {
|
|
4544
|
-
return [];
|
|
5764
|
+
const report = scanPath(targetPath);
|
|
5765
|
+
const findings = [...report.findings, ...await scanDependencies(targetPath)];
|
|
5766
|
+
return toRunResult(targetPath, findings, report.filesScanned);
|
|
5767
|
+
} catch (err) {
|
|
5768
|
+
return failedResult(targetPath, err.message);
|
|
4545
5769
|
}
|
|
4546
5770
|
}
|
|
4547
5771
|
|
|
@@ -4955,8 +6179,8 @@ var RuleEngine = class {
|
|
|
4955
6179
|
};
|
|
4956
6180
|
|
|
4957
6181
|
// src/daemon/rules/loader.ts
|
|
4958
|
-
var
|
|
4959
|
-
var
|
|
6182
|
+
var import_node_fs13 = require("fs");
|
|
6183
|
+
var import_node_path9 = require("path");
|
|
4960
6184
|
|
|
4961
6185
|
// src/daemon/rules/default-rules.ts
|
|
4962
6186
|
var DEFAULT_RULES = [
|
|
@@ -5240,11 +6464,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
5240
6464
|
function loadAllRules(customDir) {
|
|
5241
6465
|
const rules = [...DEFAULT_RULES];
|
|
5242
6466
|
const dir = customDir || RULES_DIR;
|
|
5243
|
-
if ((0,
|
|
5244
|
-
const files = (0,
|
|
6467
|
+
if ((0, import_node_fs13.existsSync)(dir)) {
|
|
6468
|
+
const files = (0, import_node_fs13.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
5245
6469
|
for (const file of files) {
|
|
5246
6470
|
try {
|
|
5247
|
-
const raw = (0,
|
|
6471
|
+
const raw = (0, import_node_fs13.readFileSync)((0, import_node_path9.join)(dir, file), "utf-8");
|
|
5248
6472
|
const parsed = JSON.parse(raw);
|
|
5249
6473
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
5250
6474
|
for (const rule of customRules) {
|
|
@@ -5269,6 +6493,12 @@ function loadAllRules(customDir) {
|
|
|
5269
6493
|
|
|
5270
6494
|
// src/daemon/firewall/adapters.ts
|
|
5271
6495
|
var import_node_child_process3 = require("child_process");
|
|
6496
|
+
var import_node_net2 = require("net");
|
|
6497
|
+
function assertValidFirewallIp(ip) {
|
|
6498
|
+
if ((0, import_node_net2.isIP)(ip) !== 4) {
|
|
6499
|
+
throw new Error(`Invalid IPv4 address: ${ip}`);
|
|
6500
|
+
}
|
|
6501
|
+
}
|
|
5272
6502
|
var NftablesAdapter = class {
|
|
5273
6503
|
name = "nftables";
|
|
5274
6504
|
table = "threatcrush";
|
|
@@ -5288,16 +6518,19 @@ var NftablesAdapter = class {
|
|
|
5288
6518
|
}
|
|
5289
6519
|
}
|
|
5290
6520
|
async block(ip) {
|
|
6521
|
+
assertValidFirewallIp(ip);
|
|
5291
6522
|
this.ensureSetup();
|
|
5292
6523
|
(0, import_node_child_process3.execSync)(`nft add element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
5293
6524
|
}
|
|
5294
6525
|
async unblock(ip) {
|
|
6526
|
+
assertValidFirewallIp(ip);
|
|
5295
6527
|
try {
|
|
5296
6528
|
(0, import_node_child_process3.execSync)(`nft delete element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
5297
6529
|
} catch {
|
|
5298
6530
|
}
|
|
5299
6531
|
}
|
|
5300
6532
|
async isBlocked(ip) {
|
|
6533
|
+
assertValidFirewallIp(ip);
|
|
5301
6534
|
try {
|
|
5302
6535
|
const output = (0, import_node_child_process3.execSync)(`nft list set inet ${this.table} ${this.set}`, { encoding: "utf-8" });
|
|
5303
6536
|
return output.includes(ip);
|
|
@@ -5332,17 +6565,20 @@ var IptablesAdapter = class {
|
|
|
5332
6565
|
}
|
|
5333
6566
|
}
|
|
5334
6567
|
async block(ip) {
|
|
6568
|
+
assertValidFirewallIp(ip);
|
|
5335
6569
|
this.ensureChain();
|
|
5336
6570
|
if (await this.isBlocked(ip)) return;
|
|
5337
6571
|
(0, import_node_child_process3.execSync)(`iptables -A ${this.chain} -s ${ip} -j DROP`);
|
|
5338
6572
|
}
|
|
5339
6573
|
async unblock(ip) {
|
|
6574
|
+
assertValidFirewallIp(ip);
|
|
5340
6575
|
try {
|
|
5341
6576
|
(0, import_node_child_process3.execSync)(`iptables -D ${this.chain} -s ${ip} -j DROP`);
|
|
5342
6577
|
} catch {
|
|
5343
6578
|
}
|
|
5344
6579
|
}
|
|
5345
6580
|
async isBlocked(ip) {
|
|
6581
|
+
assertValidFirewallIp(ip);
|
|
5346
6582
|
try {
|
|
5347
6583
|
const output = (0, import_node_child_process3.execSync)(`iptables -n -L ${this.chain}`, { encoding: "utf-8" });
|
|
5348
6584
|
return output.includes(ip);
|
|
@@ -5371,12 +6607,15 @@ var DryRunAdapter = class {
|
|
|
5371
6607
|
return true;
|
|
5372
6608
|
}
|
|
5373
6609
|
async block(ip) {
|
|
6610
|
+
assertValidFirewallIp(ip);
|
|
5374
6611
|
this.blocked.add(ip);
|
|
5375
6612
|
}
|
|
5376
6613
|
async unblock(ip) {
|
|
6614
|
+
assertValidFirewallIp(ip);
|
|
5377
6615
|
this.blocked.delete(ip);
|
|
5378
6616
|
}
|
|
5379
6617
|
async isBlocked(ip) {
|
|
6618
|
+
assertValidFirewallIp(ip);
|
|
5380
6619
|
return this.blocked.has(ip);
|
|
5381
6620
|
}
|
|
5382
6621
|
async listBlocked() {
|
|
@@ -5392,7 +6631,7 @@ function detectFirewallAdapter() {
|
|
|
5392
6631
|
}
|
|
5393
6632
|
|
|
5394
6633
|
// src/daemon/firewall/remediation.ts
|
|
5395
|
-
var
|
|
6634
|
+
var import_node_fs14 = require("fs");
|
|
5396
6635
|
var DEFAULT_CONFIG2 = {
|
|
5397
6636
|
enabled: true,
|
|
5398
6637
|
dry_run: true,
|
|
@@ -5547,7 +6786,7 @@ var RemediationManager = class {
|
|
|
5547
6786
|
}
|
|
5548
6787
|
logLine(line) {
|
|
5549
6788
|
try {
|
|
5550
|
-
(0,
|
|
6789
|
+
(0, import_node_fs14.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
5551
6790
|
`);
|
|
5552
6791
|
} catch {
|
|
5553
6792
|
}
|
|
@@ -5601,9 +6840,9 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
5601
6840
|
}
|
|
5602
6841
|
|
|
5603
6842
|
// src/daemon/index.ts
|
|
5604
|
-
function
|
|
6843
|
+
function readVersion2() {
|
|
5605
6844
|
try {
|
|
5606
|
-
const pkg = JSON.parse((0,
|
|
6845
|
+
const pkg = JSON.parse((0, import_node_fs15.readFileSync)((0, import_node_path10.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
5607
6846
|
return pkg.version || "0.0.0";
|
|
5608
6847
|
} catch {
|
|
5609
6848
|
return "0.0.0";
|
|
@@ -5611,7 +6850,7 @@ function readVersion() {
|
|
|
5611
6850
|
}
|
|
5612
6851
|
function logLine(line) {
|
|
5613
6852
|
try {
|
|
5614
|
-
(0,
|
|
6853
|
+
(0, import_node_fs15.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
5615
6854
|
`);
|
|
5616
6855
|
} catch {
|
|
5617
6856
|
}
|
|
@@ -5632,14 +6871,14 @@ async function runDaemon() {
|
|
|
5632
6871
|
logLine(`[daemon] unhandledRejection: ${String(reason)}`);
|
|
5633
6872
|
captureException(reason);
|
|
5634
6873
|
});
|
|
5635
|
-
const version =
|
|
6874
|
+
const version = readVersion2();
|
|
5636
6875
|
logLine(`[daemon] starting threatcrushd v${version} mode=${PATHS.mode}`);
|
|
5637
6876
|
try {
|
|
5638
6877
|
initStateDB(PATHS.stateDb);
|
|
5639
6878
|
} catch (err) {
|
|
5640
6879
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
5641
6880
|
}
|
|
5642
|
-
const config = loadConfig((0,
|
|
6881
|
+
const config = loadConfig((0, import_node_fs15.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
5643
6882
|
bus.on("event", (event) => {
|
|
5644
6883
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
5645
6884
|
});
|