@profullstack/threatcrush 0.2.2 → 0.3.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 +1434 -222
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1971 -473
- package/dist/index.js.map +1 -1
- package/package.json +6 -3
package/dist/index.js
CHANGED
|
@@ -1074,7 +1074,7 @@ var require_suggestSimilar = __commonJS({
|
|
|
1074
1074
|
"../../node_modules/.pnpm/commander@13.1.0/node_modules/commander/lib/suggestSimilar.js"(exports2) {
|
|
1075
1075
|
"use strict";
|
|
1076
1076
|
var maxDistance = 3;
|
|
1077
|
-
function
|
|
1077
|
+
function editDistance2(a, b) {
|
|
1078
1078
|
if (Math.abs(a.length - b.length) > maxDistance)
|
|
1079
1079
|
return Math.max(a.length, b.length);
|
|
1080
1080
|
const d = [];
|
|
@@ -1120,7 +1120,7 @@ var require_suggestSimilar = __commonJS({
|
|
|
1120
1120
|
const minSimilarity = 0.4;
|
|
1121
1121
|
candidates.forEach((candidate) => {
|
|
1122
1122
|
if (candidate.length <= 1) return;
|
|
1123
|
-
const distance =
|
|
1123
|
+
const distance = editDistance2(word, candidate);
|
|
1124
1124
|
const length = Math.max(word.length, candidate.length);
|
|
1125
1125
|
const similarity = (length - distance) / length;
|
|
1126
1126
|
if (similarity > minSimilarity) {
|
|
@@ -3917,7 +3917,7 @@ var init_ipc_client = __esm({
|
|
|
3917
3917
|
if (!(0, import_node_fs2.existsSync)(this.socketPath)) {
|
|
3918
3918
|
throw new Error(`threatcrushd socket not found at ${this.socketPath}`);
|
|
3919
3919
|
}
|
|
3920
|
-
return new Promise((
|
|
3920
|
+
return new Promise((resolve5, reject) => {
|
|
3921
3921
|
const sock = (0, import_node_net.createConnection)(this.socketPath);
|
|
3922
3922
|
const timer = setTimeout(() => {
|
|
3923
3923
|
sock.destroy();
|
|
@@ -3930,7 +3930,7 @@ var init_ipc_client = __esm({
|
|
|
3930
3930
|
sock.on("data", (chunk) => this.onData(chunk.toString()));
|
|
3931
3931
|
sock.on("close", () => this.onClose());
|
|
3932
3932
|
sock.on("error", () => this.onClose());
|
|
3933
|
-
|
|
3933
|
+
resolve5();
|
|
3934
3934
|
});
|
|
3935
3935
|
sock.once("error", (err) => {
|
|
3936
3936
|
clearTimeout(timer);
|
|
@@ -3946,8 +3946,8 @@ var init_ipc_client = __esm({
|
|
|
3946
3946
|
if (!this.socket) throw new Error("not connected");
|
|
3947
3947
|
const id = this.nextId++;
|
|
3948
3948
|
const frame = params ? { id, method, params } : { id, method };
|
|
3949
|
-
return new Promise((
|
|
3950
|
-
this.pending.set(id, { resolve:
|
|
3949
|
+
return new Promise((resolve5, reject) => {
|
|
3950
|
+
this.pending.set(id, { resolve: resolve5, reject });
|
|
3951
3951
|
this.socket.write(JSON.stringify(frame) + "\n", (err) => {
|
|
3952
3952
|
if (err) {
|
|
3953
3953
|
this.pending.delete(id);
|
|
@@ -7363,20 +7363,20 @@ var require_parse_async = __commonJS({
|
|
|
7363
7363
|
const index = 0;
|
|
7364
7364
|
const blocksize = opts.blocksize || 40960;
|
|
7365
7365
|
const parser = new TOMLParser();
|
|
7366
|
-
return new Promise((
|
|
7367
|
-
setImmediate(parseAsyncNext, index, blocksize,
|
|
7366
|
+
return new Promise((resolve5, reject) => {
|
|
7367
|
+
setImmediate(parseAsyncNext, index, blocksize, resolve5, reject);
|
|
7368
7368
|
});
|
|
7369
|
-
function parseAsyncNext(index2, blocksize2,
|
|
7369
|
+
function parseAsyncNext(index2, blocksize2, resolve5, reject) {
|
|
7370
7370
|
if (index2 >= str.length) {
|
|
7371
7371
|
try {
|
|
7372
|
-
return
|
|
7372
|
+
return resolve5(parser.finish());
|
|
7373
7373
|
} catch (err) {
|
|
7374
7374
|
return reject(prettyError(err, str));
|
|
7375
7375
|
}
|
|
7376
7376
|
}
|
|
7377
7377
|
try {
|
|
7378
7378
|
parser.parse(str.slice(index2, index2 + blocksize2));
|
|
7379
|
-
setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2,
|
|
7379
|
+
setImmediate(parseAsyncNext, index2 + blocksize2, blocksize2, resolve5, reject);
|
|
7380
7380
|
} catch (err) {
|
|
7381
7381
|
reject(prettyError(err, str));
|
|
7382
7382
|
}
|
|
@@ -7402,7 +7402,7 @@ var require_parse_stream = __commonJS({
|
|
|
7402
7402
|
function parseReadable(stm) {
|
|
7403
7403
|
const parser = new TOMLParser();
|
|
7404
7404
|
stm.setEncoding("utf8");
|
|
7405
|
-
return new Promise((
|
|
7405
|
+
return new Promise((resolve5, reject) => {
|
|
7406
7406
|
let readable;
|
|
7407
7407
|
let ended = false;
|
|
7408
7408
|
let errored = false;
|
|
@@ -7410,7 +7410,7 @@ var require_parse_stream = __commonJS({
|
|
|
7410
7410
|
ended = true;
|
|
7411
7411
|
if (readable) return;
|
|
7412
7412
|
try {
|
|
7413
|
-
|
|
7413
|
+
resolve5(parser.finish());
|
|
7414
7414
|
} catch (err) {
|
|
7415
7415
|
reject(err);
|
|
7416
7416
|
}
|
|
@@ -8260,8 +8260,8 @@ var source_default = chalk;
|
|
|
8260
8260
|
// src/index.ts
|
|
8261
8261
|
var import_readline = __toESM(require("readline"));
|
|
8262
8262
|
var import_node_child_process10 = require("child_process");
|
|
8263
|
-
var
|
|
8264
|
-
var
|
|
8263
|
+
var import_node_fs29 = require("fs");
|
|
8264
|
+
var import_node_path19 = require("path");
|
|
8265
8265
|
var import_node_os8 = require("os");
|
|
8266
8266
|
|
|
8267
8267
|
// src/commands/monitor.ts
|
|
@@ -8323,6 +8323,7 @@ var NGINX_REGEX = /^(\S+) \S+ \S+ \[([^\]]+)\] "(\S+) (\S+) \S+" (\d{3}) (\d+) "
|
|
|
8323
8323
|
var AUTH_REGEX = /^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(\S+?)(?:\[\d+\])?:\s+(.*)/;
|
|
8324
8324
|
var SYSLOG_REGEX = /^(\w+\s+\d+\s+[\d:]+)\s+\S+\s+(\S+?)(?:\[\d+\])?:\s+(.*)/;
|
|
8325
8325
|
var IP_REGEX = /(?:from|FROM)\s+(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})/;
|
|
8326
|
+
var INVALID_USER_REGEX = /(?:for\s+invalid\s+user)\s+(\S+?)(?:\s+from|\s*$)/;
|
|
8326
8327
|
var USER_REGEX = /(?:for|user)\s+(\S+?)(?:\s+from|\s*$)/;
|
|
8327
8328
|
var ATTACK_PATTERNS = {
|
|
8328
8329
|
sqli: [
|
|
@@ -8376,7 +8377,7 @@ function parseAuthLog(line) {
|
|
|
8376
8377
|
const match = line.match(AUTH_REGEX);
|
|
8377
8378
|
if (!match) return null;
|
|
8378
8379
|
const ipMatch = match[3].match(IP_REGEX);
|
|
8379
|
-
const userMatch = match[3].match(USER_REGEX);
|
|
8380
|
+
const userMatch = match[3].match(INVALID_USER_REGEX) || match[3].match(USER_REGEX);
|
|
8380
8381
|
return {
|
|
8381
8382
|
timestamp: parseSyslogTimestamp(match[1]),
|
|
8382
8383
|
raw: line,
|
|
@@ -8404,10 +8405,25 @@ function parseSyslog(line) {
|
|
|
8404
8405
|
};
|
|
8405
8406
|
}
|
|
8406
8407
|
function detectAttackPattern(path) {
|
|
8408
|
+
const candidates = /* @__PURE__ */ new Set([path]);
|
|
8409
|
+
let current = path;
|
|
8410
|
+
for (let i = 0; i < 2; i++) {
|
|
8411
|
+
let decoded = null;
|
|
8412
|
+
try {
|
|
8413
|
+
decoded = decodeURIComponent(current);
|
|
8414
|
+
} catch {
|
|
8415
|
+
decoded = null;
|
|
8416
|
+
}
|
|
8417
|
+
if (decoded === null || decoded === current) break;
|
|
8418
|
+
candidates.add(decoded);
|
|
8419
|
+
current = decoded;
|
|
8420
|
+
}
|
|
8407
8421
|
for (const [type, patterns] of Object.entries(ATTACK_PATTERNS)) {
|
|
8408
8422
|
for (const pattern of patterns) {
|
|
8409
|
-
|
|
8410
|
-
|
|
8423
|
+
for (const candidate of candidates) {
|
|
8424
|
+
if (pattern.test(candidate)) {
|
|
8425
|
+
return type;
|
|
8426
|
+
}
|
|
8411
8427
|
}
|
|
8412
8428
|
}
|
|
8413
8429
|
}
|
|
@@ -8421,20 +8437,19 @@ function autoDetectParser(line) {
|
|
|
8421
8437
|
return parseSyslog(line);
|
|
8422
8438
|
}
|
|
8423
8439
|
function parseNginxTimestamp(s) {
|
|
8424
|
-
|
|
8425
|
-
|
|
8426
|
-
|
|
8427
|
-
} catch {
|
|
8428
|
-
return /* @__PURE__ */ new Date();
|
|
8429
|
-
}
|
|
8440
|
+
const cleaned = s.replace(/(\d{2})\/(\w{3})\/(\d{4}):/, "$2 $1, $3 ");
|
|
8441
|
+
const d = new Date(cleaned);
|
|
8442
|
+
return Number.isNaN(d.getTime()) ? /* @__PURE__ */ new Date() : d;
|
|
8430
8443
|
}
|
|
8431
8444
|
function parseSyslogTimestamp(s) {
|
|
8432
|
-
|
|
8433
|
-
|
|
8434
|
-
|
|
8435
|
-
|
|
8436
|
-
|
|
8445
|
+
const now = /* @__PURE__ */ new Date();
|
|
8446
|
+
let d = /* @__PURE__ */ new Date(`${s} ${now.getFullYear()}`);
|
|
8447
|
+
if (Number.isNaN(d.getTime())) return now;
|
|
8448
|
+
if (d.getTime() > now.getTime()) {
|
|
8449
|
+
const prev = /* @__PURE__ */ new Date(`${s} ${now.getFullYear() - 1}`);
|
|
8450
|
+
if (!Number.isNaN(prev.getTime())) d = prev;
|
|
8437
8451
|
}
|
|
8452
|
+
return d;
|
|
8438
8453
|
}
|
|
8439
8454
|
|
|
8440
8455
|
// src/commands/monitor.ts
|
|
@@ -8624,8 +8639,8 @@ async function runDemoMode() {
|
|
|
8624
8639
|
}
|
|
8625
8640
|
|
|
8626
8641
|
// src/commands/scan.ts
|
|
8627
|
-
var
|
|
8628
|
-
var
|
|
8642
|
+
var import_node_fs7 = require("fs");
|
|
8643
|
+
var import_node_path5 = require("path");
|
|
8629
8644
|
|
|
8630
8645
|
// ../../node_modules/.pnpm/ora@8.2.0/node_modules/ora/index.js
|
|
8631
8646
|
var import_node_process7 = __toESM(require("process"), 1);
|
|
@@ -9565,58 +9580,1113 @@ function workerId() {
|
|
|
9565
9580
|
return `${import_node_os3.default.hostname()}/${process.pid}`;
|
|
9566
9581
|
}
|
|
9567
9582
|
|
|
9568
|
-
// src/
|
|
9569
|
-
var
|
|
9570
|
-
|
|
9571
|
-
|
|
9572
|
-
{
|
|
9573
|
-
{
|
|
9574
|
-
{
|
|
9575
|
-
{
|
|
9576
|
-
{
|
|
9577
|
-
|
|
9578
|
-
|
|
9579
|
-
|
|
9580
|
-
|
|
9581
|
-
|
|
9583
|
+
// src/scan/dependencies.ts
|
|
9584
|
+
var import_node_fs5 = require("fs");
|
|
9585
|
+
var import_node_path2 = require("path");
|
|
9586
|
+
var LOCKFILES = [
|
|
9587
|
+
{ file: "package-lock.json", ecosystem: "npm" },
|
|
9588
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
9589
|
+
{ file: "yarn.lock", ecosystem: "npm" },
|
|
9590
|
+
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
9591
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
9592
|
+
];
|
|
9593
|
+
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
9594
|
+
async function scanDependencies(targetPath) {
|
|
9595
|
+
const findings = [];
|
|
9596
|
+
for (const { file, ecosystem } of LOCKFILES) {
|
|
9597
|
+
const lockPath = (0, import_node_path2.join)(targetPath, file);
|
|
9598
|
+
if (!(0, import_node_fs5.existsSync)(lockPath)) continue;
|
|
9599
|
+
let deps;
|
|
9600
|
+
try {
|
|
9601
|
+
deps = parseDependencies(lockPath, file);
|
|
9602
|
+
} catch {
|
|
9603
|
+
continue;
|
|
9604
|
+
}
|
|
9605
|
+
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
9606
|
+
let vulns;
|
|
9607
|
+
try {
|
|
9608
|
+
vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
9609
|
+
} catch {
|
|
9610
|
+
continue;
|
|
9611
|
+
}
|
|
9612
|
+
for (const vuln of vulns) {
|
|
9613
|
+
const cvss = vuln.severity?.find((entry) => entry.type === "CVSS_V3")?.score;
|
|
9614
|
+
findings.push({
|
|
9615
|
+
ruleId: "dependency-known-vulnerability",
|
|
9616
|
+
title: "Dependency CVE",
|
|
9617
|
+
file,
|
|
9618
|
+
line: 1,
|
|
9619
|
+
severity: severityFromCvss(cvss),
|
|
9620
|
+
confidence: "evidence",
|
|
9621
|
+
message: `${dep.name}@${dep.version}: ${vuln.summary ?? vuln.id}`,
|
|
9622
|
+
consequence: "A published advisory exists for the exact version resolved in this lockfile.",
|
|
9623
|
+
excerpt: `${vuln.id}${cvss ? ` (CVSS: ${cvss})` : ""}`,
|
|
9624
|
+
category: "dependency"
|
|
9625
|
+
});
|
|
9626
|
+
}
|
|
9627
|
+
}
|
|
9628
|
+
}
|
|
9629
|
+
return findings;
|
|
9630
|
+
}
|
|
9631
|
+
function severityFromCvss(score) {
|
|
9632
|
+
if (!score) return "medium";
|
|
9633
|
+
const value = Number.parseFloat(score);
|
|
9634
|
+
if (Number.isNaN(value)) return "medium";
|
|
9635
|
+
if (value >= 9) return "critical";
|
|
9636
|
+
if (value >= 7) return "high";
|
|
9637
|
+
if (value >= 4) return "medium";
|
|
9638
|
+
return "low";
|
|
9639
|
+
}
|
|
9640
|
+
function parseDependencies(lockPath, filename) {
|
|
9641
|
+
const deps = [];
|
|
9642
|
+
if (filename === "package-lock.json") {
|
|
9643
|
+
const lock = JSON.parse((0, import_node_fs5.readFileSync)(lockPath, "utf-8"));
|
|
9644
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
9645
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
9646
|
+
const name = key.replace(/^node_modules\//, "");
|
|
9647
|
+
const version = value?.version;
|
|
9648
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
9649
|
+
}
|
|
9650
|
+
return deps;
|
|
9651
|
+
}
|
|
9652
|
+
if (filename === "requirements.txt") {
|
|
9653
|
+
for (const line of (0, import_node_fs5.readFileSync)(lockPath, "utf-8").split("\n")) {
|
|
9654
|
+
const match = /^([a-zA-Z0-9_.-]+)==([0-9.]+)/.exec(line);
|
|
9655
|
+
if (match?.[1] && match[2]) deps.push({ name: match[1], version: match[2] });
|
|
9656
|
+
}
|
|
9657
|
+
}
|
|
9658
|
+
return deps;
|
|
9659
|
+
}
|
|
9660
|
+
function isValidPackageName(name) {
|
|
9661
|
+
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
9662
|
+
}
|
|
9663
|
+
function isValidVersion(version) {
|
|
9664
|
+
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
9665
|
+
}
|
|
9666
|
+
async function queryOsv(name, version, ecosystem) {
|
|
9667
|
+
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
9668
|
+
try {
|
|
9669
|
+
const response = await fetch("https://api.osv.dev/v1/query", {
|
|
9670
|
+
method: "POST",
|
|
9671
|
+
headers: { "Content-Type": "application/json" },
|
|
9672
|
+
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
9673
|
+
signal: AbortSignal.timeout(5e3)
|
|
9674
|
+
});
|
|
9675
|
+
if (!response.ok) return [];
|
|
9676
|
+
const data = await response.json();
|
|
9677
|
+
return data.vulns ?? [];
|
|
9678
|
+
} catch {
|
|
9679
|
+
return [];
|
|
9680
|
+
}
|
|
9681
|
+
}
|
|
9682
|
+
|
|
9683
|
+
// src/scan/engine.ts
|
|
9684
|
+
var import_node_fs6 = require("fs");
|
|
9685
|
+
var import_node_path3 = require("path");
|
|
9686
|
+
|
|
9687
|
+
// src/scan/types.ts
|
|
9688
|
+
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
9689
|
+
function severityRank(severity) {
|
|
9690
|
+
const index = SEVERITY_ORDER.indexOf(severity);
|
|
9691
|
+
return index === -1 ? 0 : index;
|
|
9692
|
+
}
|
|
9693
|
+
function severityFor(declared, confidence) {
|
|
9694
|
+
if (confidence !== "pattern") return declared;
|
|
9695
|
+
return severityRank(declared) > severityRank("medium") ? "medium" : declared;
|
|
9696
|
+
}
|
|
9697
|
+
|
|
9698
|
+
// src/scan/code-rules.ts
|
|
9699
|
+
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\b|\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetInputStream\s*\(/;
|
|
9700
|
+
var UNTRUSTED_PY = /\brequest\b|\bparams\b|\bflask\b|\bsys\.argv\b|\bos\.environ\b\s*\[/;
|
|
9701
|
+
var UNTRUSTED_RB = /\bparams\s*\[|\brequest\b|\bcookies\s*\[/;
|
|
9702
|
+
var UNTRUSTED_GO = /\br\s*\.\s*(?:URL|Form|Body|Header|PostForm)\b|\bFormValue\s*\(|\bQuery\s*\(\s*\)\s*\.\s*Get\s*\(|\bmux\.Vars\s*\(/;
|
|
9703
|
+
var UNTRUSTED_JAVA = /\bgetParameter\s*\(|\bgetQueryString\s*\(|\bgetHeader\s*\(|\bgetInputStream\s*\(|\bgetCookies\s*\(|\b@RequestParam\b|\b@PathVariable\b/;
|
|
9704
|
+
function untrustedPatternFor(language) {
|
|
9705
|
+
switch (language) {
|
|
9706
|
+
case "python":
|
|
9707
|
+
return UNTRUSTED_PY;
|
|
9708
|
+
case "ruby":
|
|
9709
|
+
return UNTRUSTED_RB;
|
|
9710
|
+
case "go":
|
|
9711
|
+
return UNTRUSTED_GO;
|
|
9712
|
+
case "java":
|
|
9713
|
+
return UNTRUSTED_JAVA;
|
|
9714
|
+
default:
|
|
9715
|
+
return UNTRUSTED_JS;
|
|
9716
|
+
}
|
|
9717
|
+
}
|
|
9718
|
+
var GENERIC_GUARD = /\ballow(?:ed|list|_list|ed_hosts)?\b|\bwhitelist\b|\bescape(?:Html|Html4|Xml|Sql)?\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;
|
|
9719
|
+
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
9720
|
+
var CODE_SINK = /\bglobalThis\s*\[|\bconstructor\b|\beval\b|\bFunction\b|\brun\s*\(|\bvm\s*\.\s*run/;
|
|
9721
|
+
var EXFIL_SINK = /\bconsole\s*\.\s*(?:log|debug|info|warn|error)\s*\(|\bfetch\s*\(|\baxios\b|\brequest\s*\(|\.\s*send\s*\(/;
|
|
9722
|
+
var SQL_KEYWORDS = "SELECT|INSERT\\s+INTO|INSERT|UPDATE|DELETE\\s+FROM|DELETE|DROP|UNION\\s+SELECT";
|
|
9723
|
+
var SQL_IN_DOUBLE = `"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*"`;
|
|
9724
|
+
var SQL_IN_SINGLE = `'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*'`;
|
|
9725
|
+
var SQL_STRING = `(?:${SQL_IN_DOUBLE}|${SQL_IN_SINGLE})`;
|
|
9726
|
+
var CODE_RULES = [
|
|
9727
|
+
// ── Injection: SQL ───────────────────────────────────────────────────────
|
|
9728
|
+
{
|
|
9729
|
+
id: "sql-string-concatenation",
|
|
9730
|
+
title: "SQL assembled by concatenation or interpolation",
|
|
9731
|
+
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.",
|
|
9732
|
+
cwe: "CWE-89",
|
|
9733
|
+
severity: "critical",
|
|
9734
|
+
// The tail is what distinguishes assembly from parameterisation. A bound
|
|
9735
|
+
// query leaves a comma after the closing quote (`"… = $1", [id]`) and
|
|
9736
|
+
// matches none of these.
|
|
9737
|
+
pattern: new RegExp(
|
|
9738
|
+
`${SQL_STRING}\\s*\\+|${SQL_STRING}\\s*%\\s*[\\w(]|${SQL_STRING}\\s*\\.\\s*format\\s*\\(|\\+\\s*(?:"[^"\\n]*|'[^'\\n]*)(?:WHERE|ORDER\\s+BY|VALUES|SET)\\b`,
|
|
9739
|
+
"i"
|
|
9740
|
+
)
|
|
9741
|
+
},
|
|
9742
|
+
{
|
|
9743
|
+
id: "sql-template-interpolation",
|
|
9744
|
+
title: "SQL built from a template literal or f-string",
|
|
9745
|
+
consequence: "Template interpolation is string concatenation with nicer syntax \u2014 it binds nothing and escapes nothing.",
|
|
9746
|
+
cwe: "CWE-89",
|
|
9747
|
+
severity: "critical",
|
|
9748
|
+
pattern: new RegExp(
|
|
9749
|
+
`\`[^\`\\n]*(?:${SQL_KEYWORDS})\\b[^\`\\n]*\\$\\{|\\bf"[^"\\n]*(?:${SQL_KEYWORDS})\\b[^"\\n]*\\{|\\bf'[^'\\n]*(?:${SQL_KEYWORDS})\\b[^'\\n]*\\{`,
|
|
9750
|
+
"i"
|
|
9751
|
+
)
|
|
9752
|
+
},
|
|
9753
|
+
{
|
|
9754
|
+
id: "sql-format-call",
|
|
9755
|
+
title: "SQL text produced by a format helper",
|
|
9756
|
+
consequence: "`Sprintf`/`String.format` substitute without quoting; the resulting string is concatenated SQL by another name.",
|
|
9757
|
+
cwe: "CWE-89",
|
|
9758
|
+
severity: "critical",
|
|
9759
|
+
languages: ["go", "java"],
|
|
9760
|
+
pattern: new RegExp(
|
|
9761
|
+
`\\b(?:fmt\\.Sprintf|String\\.format)\\s*\\(\\s*"[^"\\n]*(?:${SQL_KEYWORDS})\\b`,
|
|
9762
|
+
"i"
|
|
9763
|
+
)
|
|
9764
|
+
},
|
|
9765
|
+
{
|
|
9766
|
+
id: "rb-sql-interpolation",
|
|
9767
|
+
title: "ActiveRecord query built by string interpolation",
|
|
9768
|
+
consequence: '`where("\u2026 #{value}")` interpolates before the adapter sees it, so no binding ever happens.',
|
|
9769
|
+
cwe: "CWE-89",
|
|
9770
|
+
severity: "critical",
|
|
9771
|
+
languages: ["ruby"],
|
|
9772
|
+
pattern: /\b(?:where|find_by_sql|execute|select_all|select_values|order|group|pluck)\s*[( ]\s*(?:"[^"\n]*|'[^'\n]*)#\{/
|
|
9773
|
+
},
|
|
9774
|
+
// ── Injection: OS command ────────────────────────────────────────────────
|
|
9775
|
+
{
|
|
9776
|
+
id: "js-shell-exec-interpolation",
|
|
9777
|
+
title: "shell execution with an interpolated string",
|
|
9778
|
+
consequence: "A `;` or `$(\u2026)` in the interpolated value runs as the server user.",
|
|
9779
|
+
cwe: "CWE-78",
|
|
9780
|
+
severity: "critical",
|
|
9781
|
+
languages: ["javascript", "typescript"],
|
|
9782
|
+
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/
|
|
9783
|
+
},
|
|
9784
|
+
{
|
|
9785
|
+
id: "py-shell-command-string",
|
|
9786
|
+
title: "shell command built from a string",
|
|
9787
|
+
consequence: "`os.system` and `shell=True` hand the string to `/bin/sh`, which happily interprets metacharacters.",
|
|
9788
|
+
cwe: "CWE-78",
|
|
9789
|
+
severity: "critical",
|
|
9790
|
+
languages: ["python"],
|
|
9791
|
+
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/
|
|
9792
|
+
},
|
|
9793
|
+
{
|
|
9794
|
+
id: "go-shell-exec-command",
|
|
9795
|
+
title: "exec.Command invoking a shell",
|
|
9796
|
+
consequence: "Passing `sh -c` re-introduces the shell that `exec.Command`\u2019s argv interface exists to avoid.",
|
|
9797
|
+
cwe: "CWE-78",
|
|
9798
|
+
severity: "critical",
|
|
9799
|
+
languages: ["go"],
|
|
9800
|
+
pattern: /\bexec\.Command(?:Context)?\s*\(\s*(?:ctx\s*,\s*)?"(?:\/bin\/)?(?:sh|bash|zsh|cmd|powershell)"\s*,\s*"(?:-c|\/c)"/
|
|
9801
|
+
},
|
|
9802
|
+
{
|
|
9803
|
+
id: "rb-backtick-interpolation",
|
|
9804
|
+
title: "backtick command with interpolation",
|
|
9805
|
+
consequence: "Ruby backticks are a shell invocation; `#{}` inside one is command injection.",
|
|
9806
|
+
cwe: "CWE-78",
|
|
9807
|
+
severity: "critical",
|
|
9808
|
+
languages: ["ruby"],
|
|
9809
|
+
pattern: /`[^`\n]*#\{|\bsystem\s*\(\s*["'][^"'\n]*#\{|%x\[[^\]]*#\{/
|
|
9810
|
+
},
|
|
9811
|
+
// ── Injection: dynamic code ──────────────────────────────────────────────
|
|
9812
|
+
{
|
|
9813
|
+
id: "js-dynamic-code-execution",
|
|
9814
|
+
title: "dynamic code execution",
|
|
9815
|
+
consequence: "Any string reaching this call executes as code with the process\u2019 privileges.",
|
|
9816
|
+
cwe: "CWE-95",
|
|
9817
|
+
severity: "critical",
|
|
9818
|
+
languages: ["javascript", "typescript"],
|
|
9819
|
+
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)/
|
|
9820
|
+
},
|
|
9821
|
+
{
|
|
9822
|
+
id: "js-indirect-code-sink",
|
|
9823
|
+
title: "code sink reached indirectly",
|
|
9824
|
+
consequence: "Resolving `eval`/`Function` through `globalThis[\u2026]` or `.constructor` hides the sink from literal matching. Legitimate code has no reason to.",
|
|
9825
|
+
cwe: "CWE-506",
|
|
9826
|
+
severity: "high",
|
|
9827
|
+
languages: ["javascript", "typescript"],
|
|
9828
|
+
pattern: /\bglobalThis\s*\[\s*[a-zA-Z_$][\w$]*\s*\]|\(\s*function\s*\(\s*\)\s*\{\s*\}\s*\)\s*\.\s*constructor/
|
|
9829
|
+
},
|
|
9830
|
+
{
|
|
9831
|
+
id: "js-encoded-payload-execution",
|
|
9832
|
+
title: "encoded blob decoded next to a code sink",
|
|
9833
|
+
consequence: "A base64 literal that is decoded and executed is the standard shape of a planted backdoor; the encoding exists to defeat review.",
|
|
9834
|
+
cwe: "CWE-506",
|
|
9835
|
+
severity: "critical",
|
|
9836
|
+
languages: ["javascript", "typescript"],
|
|
9837
|
+
pattern: /\bBuffer\.from\s*\(\s*[\w.$]+\s*,\s*['"]base64['"]\s*\)|\batob\s*\(\s*[\w.$]+\s*\)/,
|
|
9838
|
+
requires: CODE_SINK,
|
|
9839
|
+
guardBack: 6,
|
|
9840
|
+
guardForward: 3
|
|
9841
|
+
},
|
|
9842
|
+
{
|
|
9843
|
+
id: "py-dynamic-code-execution",
|
|
9844
|
+
title: "dynamic code execution",
|
|
9845
|
+
consequence: "Any string reaching this call executes as Python with the process\u2019 privileges.",
|
|
9846
|
+
cwe: "CWE-95",
|
|
9847
|
+
severity: "critical",
|
|
9848
|
+
languages: ["python"],
|
|
9849
|
+
pattern: /\b(?:eval|exec)\s*\(\s*(?!['"]\s*\))[a-zA-Z_(f'"]/,
|
|
9850
|
+
needsContext: true
|
|
9851
|
+
},
|
|
9852
|
+
{
|
|
9853
|
+
id: "rb-dynamic-dispatch",
|
|
9854
|
+
title: "dynamic code execution or unrestricted #send",
|
|
9855
|
+
consequence: "`eval` runs arbitrary Ruby; unrestricted `#send` lets the caller invoke any method on the receiver, including private ones.",
|
|
9856
|
+
cwe: "CWE-95",
|
|
9857
|
+
severity: "critical",
|
|
9858
|
+
languages: ["ruby"],
|
|
9859
|
+
pattern: /\beval\s*\(|\binstance_eval\s*\(|\bclass_eval\s*\(|\.\s*send\s*\(\s*(?:params|request|args)\b/
|
|
9860
|
+
},
|
|
9861
|
+
// ── Cross-site scripting ─────────────────────────────────────────────────
|
|
9862
|
+
{
|
|
9863
|
+
id: "js-unescaped-html-sink",
|
|
9864
|
+
title: "unescaped HTML rendering",
|
|
9865
|
+
consequence: "A script tag in the value executes in the victim\u2019s session \u2014 stored or reflected XSS.",
|
|
9866
|
+
cwe: "CWE-79",
|
|
9867
|
+
severity: "high",
|
|
9868
|
+
languages: ["javascript", "typescript"],
|
|
9869
|
+
pattern: /\bdangerouslySetInnerHTML\s*=|\.\s*(?:innerHTML|outerHTML)\s*=\s*(?!\s*['"`]\s*['"`]\s*;?\s*$)|\bdocument\s*\.\s*write(?:ln)?\s*\(|\.\s*insertAdjacentHTML\s*\(/
|
|
9870
|
+
},
|
|
9871
|
+
{
|
|
9872
|
+
id: "java-html-writer-concatenation",
|
|
9873
|
+
title: "HTML written to the response by concatenation",
|
|
9874
|
+
consequence: "The servlet writer performs no encoding; a value concatenated into markup is rendered as markup.",
|
|
9875
|
+
cwe: "CWE-79",
|
|
9876
|
+
severity: "high",
|
|
9877
|
+
languages: ["java"],
|
|
9878
|
+
pattern: /\b(?:println|print|write)\s*\(\s*"[^"\n]*<[^"\n]*"\s*\+/
|
|
9879
|
+
},
|
|
9880
|
+
{
|
|
9881
|
+
id: "rb-unescaped-output",
|
|
9882
|
+
title: "Rails output escaping bypassed",
|
|
9883
|
+
consequence: "`html_safe` and `raw` tell Rails the string is already safe. If it came from a parameter, it is not.",
|
|
9884
|
+
cwe: "CWE-79",
|
|
9885
|
+
severity: "high",
|
|
9886
|
+
languages: ["ruby"],
|
|
9887
|
+
pattern: /\.\s*html_safe\b|\braw\s*\(\s*(?:params|request|@)|\blink_to\s+[^,\n]+,\s*params\s*\[/
|
|
9888
|
+
},
|
|
9889
|
+
{
|
|
9890
|
+
id: "py-template-autoescape-off",
|
|
9891
|
+
title: "template rendering with escaping disabled",
|
|
9892
|
+
consequence: "With autoescape off \u2014 or a `|safe` filter \u2014 every interpolated value is rendered as markup.",
|
|
9893
|
+
cwe: "CWE-79",
|
|
9894
|
+
severity: "high",
|
|
9895
|
+
languages: ["python"],
|
|
9896
|
+
pattern: /\bEnvironment\s*\([^)]*\bautoescape\s*=\s*False|\|\s*safe\b|\bMarkup\s*\(\s*(?!['"])/
|
|
9897
|
+
},
|
|
9898
|
+
{
|
|
9899
|
+
id: "py-template-from-input",
|
|
9900
|
+
title: "template compiled from a non-literal source",
|
|
9901
|
+
consequence: "Server-side template injection. In Jinja2 the sandbox is escapable, so this escalates from XSS to remote code execution.",
|
|
9902
|
+
cwe: "CWE-1336",
|
|
9903
|
+
severity: "critical",
|
|
9904
|
+
languages: ["python"],
|
|
9905
|
+
pattern: /\bTemplate\s*\(\s*(?!['"])[a-zA-Z_]/,
|
|
9906
|
+
needsContext: true
|
|
9907
|
+
},
|
|
9908
|
+
// ── Server-side request forgery ──────────────────────────────────────────
|
|
9909
|
+
{
|
|
9910
|
+
id: "js-ssrf-outbound-request",
|
|
9911
|
+
title: "outbound request to a non-constant URL",
|
|
9912
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
9913
|
+
cwe: "CWE-918",
|
|
9914
|
+
severity: "high",
|
|
9915
|
+
languages: ["javascript", "typescript"],
|
|
9916
|
+
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*[,)]/,
|
|
9917
|
+
needsContext: true
|
|
9918
|
+
},
|
|
9919
|
+
{
|
|
9920
|
+
id: "py-ssrf-outbound-request",
|
|
9921
|
+
title: "outbound request to a non-constant URL",
|
|
9922
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
9923
|
+
cwe: "CWE-918",
|
|
9924
|
+
severity: "high",
|
|
9925
|
+
languages: ["python"],
|
|
9926
|
+
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*[,)]/,
|
|
9927
|
+
needsContext: true
|
|
9928
|
+
},
|
|
9929
|
+
{
|
|
9930
|
+
id: "go-ssrf-outbound-request",
|
|
9931
|
+
title: "outbound request to a non-constant URL",
|
|
9932
|
+
consequence: "An attacker who controls the URL reaches the cloud metadata endpoint, localhost, and every service on the private network.",
|
|
9933
|
+
cwe: "CWE-918",
|
|
9934
|
+
severity: "high",
|
|
9935
|
+
languages: ["go"],
|
|
9936
|
+
pattern: /\bhttp\.(?:Get|Post|Head)\s*\(\s*(?:[a-zA-Z_]\w*\s*[,)]|"[^"]*"\s*\+)/,
|
|
9937
|
+
needsContext: true
|
|
9938
|
+
},
|
|
9939
|
+
// ── Open redirect ────────────────────────────────────────────────────────
|
|
9940
|
+
{
|
|
9941
|
+
id: "js-open-redirect",
|
|
9942
|
+
title: "redirect to a non-constant destination",
|
|
9943
|
+
consequence: "Your domain becomes the credible first hop of a phishing chain; the victim sees your hostname in the link they clicked.",
|
|
9944
|
+
cwe: "CWE-601",
|
|
9945
|
+
severity: "medium",
|
|
9946
|
+
languages: ["javascript", "typescript"],
|
|
9947
|
+
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*\))/,
|
|
9948
|
+
needsContext: true
|
|
9949
|
+
},
|
|
9950
|
+
// ── Deserialisation ──────────────────────────────────────────────────────
|
|
9951
|
+
{
|
|
9952
|
+
id: "py-unsafe-deserialization",
|
|
9953
|
+
title: "deserialisation of untrusted data",
|
|
9954
|
+
consequence: "`pickle` and `yaml.load` instantiate arbitrary types during parsing \u2014 a crafted payload is remote code execution, not a parse error.",
|
|
9955
|
+
cwe: "CWE-502",
|
|
9956
|
+
severity: "critical",
|
|
9957
|
+
languages: ["python"],
|
|
9958
|
+
pattern: /\bpickle\.loads?\s*\(|\bcPickle\.loads?\s*\(|\bmarshal\.loads\s*\(|\byaml\.load\s*\(|\bjsonpickle\.decode\s*\(/
|
|
9959
|
+
},
|
|
9960
|
+
{
|
|
9961
|
+
id: "java-unsafe-deserialization",
|
|
9962
|
+
title: "Java deserialisation without a class filter",
|
|
9963
|
+
consequence: "A gadget chain in the classpath turns `readObject()` on attacker bytes into remote code execution.",
|
|
9964
|
+
cwe: "CWE-502",
|
|
9965
|
+
severity: "critical",
|
|
9966
|
+
languages: ["java"],
|
|
9967
|
+
pattern: /\breadObject\s*\(\s*\)|\bnew\s+ObjectInputStream\s*\(/,
|
|
9968
|
+
guardBack: 8,
|
|
9969
|
+
// The stream is constructed, *then* filtered. Without a forward window the
|
|
9970
|
+
// guarded case matches on its constructor line and reports a correct
|
|
9971
|
+
// implementation as a finding.
|
|
9972
|
+
guardForward: 6
|
|
9973
|
+
},
|
|
9974
|
+
{
|
|
9975
|
+
id: "js-unsafe-yaml-load",
|
|
9976
|
+
title: "YAML parsed with type resolution enabled",
|
|
9977
|
+
consequence: "A crafted document can instantiate arbitrary types during parsing.",
|
|
9978
|
+
cwe: "CWE-502",
|
|
9979
|
+
severity: "high",
|
|
9980
|
+
languages: ["javascript", "typescript"],
|
|
9981
|
+
pattern: /\byaml\s*\.\s*load\s*\((?![^)]*safe)|\bloadAll\s*\([^)]*unsafe/i
|
|
9982
|
+
},
|
|
9983
|
+
// ── XML external entities ────────────────────────────────────────────────
|
|
9984
|
+
{
|
|
9985
|
+
id: "java-xxe-parser-defaults",
|
|
9986
|
+
title: "XML parser left on its insecure defaults",
|
|
9987
|
+
consequence: "External entity expansion reads local files and makes outbound requests on the parser\u2019s behalf \u2014 file disclosure and SSRF from a document.",
|
|
9988
|
+
cwe: "CWE-611",
|
|
9989
|
+
severity: "high",
|
|
9990
|
+
languages: ["java"],
|
|
9991
|
+
pattern: /\b(?:DocumentBuilderFactory|SAXParserFactory|XMLInputFactory|TransformerFactory|SchemaFactory)\s*\.\s*newInstance\s*\(\s*\)/,
|
|
9992
|
+
guard: XXE_GUARD,
|
|
9993
|
+
guardBack: 4,
|
|
9994
|
+
guardForward: 8
|
|
9995
|
+
},
|
|
9996
|
+
{
|
|
9997
|
+
id: "java-xxe-parse-call",
|
|
9998
|
+
title: "XML parsed by a builder that was never hardened",
|
|
9999
|
+
consequence: "The expansion happens at `parse()`. Flagging only the factory misses the line where the document is actually read.",
|
|
10000
|
+
cwe: "CWE-611",
|
|
10001
|
+
severity: "high",
|
|
10002
|
+
languages: ["java"],
|
|
10003
|
+
// Receiver-qualified so `LocalDate.parse(s)` and friends stay out of it.
|
|
10004
|
+
pattern: /\b\w*(?:[Bb]uilder|[Pp]arser|[Rr]eader)\s*\.\s*parse\s*\(/,
|
|
10005
|
+
guard: XXE_GUARD,
|
|
10006
|
+
guardBack: 6,
|
|
10007
|
+
guardForward: 4
|
|
10008
|
+
},
|
|
10009
|
+
// ── Path traversal ───────────────────────────────────────────────────────
|
|
10010
|
+
{
|
|
10011
|
+
id: "py-path-traversal",
|
|
10012
|
+
title: "file opened at a path built from input",
|
|
10013
|
+
consequence: "A `../` sequence \u2014 or an absolute path \u2014 reads or writes outside the intended directory.",
|
|
10014
|
+
cwe: "CWE-22",
|
|
10015
|
+
severity: "high",
|
|
10016
|
+
languages: ["python"],
|
|
10017
|
+
pattern: /\bopen\s*\(\s*(?:os\.path\.join\s*\(|[a-zA-Z_]\w*\s*\+|f['"])/,
|
|
10018
|
+
needsContext: true
|
|
10019
|
+
},
|
|
10020
|
+
{
|
|
10021
|
+
id: "js-path-traversal",
|
|
10022
|
+
title: "file path built from a variable",
|
|
10023
|
+
consequence: "A `../` sequence in the value reads or writes outside the intended directory.",
|
|
10024
|
+
cwe: "CWE-22",
|
|
10025
|
+
severity: "medium",
|
|
10026
|
+
languages: ["javascript", "typescript"],
|
|
10027
|
+
pattern: /\b(?:readFile|readFileSync|writeFile|writeFileSync|createReadStream|createWriteStream|unlink|unlinkSync|sendFile)\s*\(\s*(?:`[^`]*\$\{|[a-zA-Z_$][\w$]*\s*\+|path\.join\s*\([^)]*(?:req|request)\b)/,
|
|
10028
|
+
needsContext: true
|
|
10029
|
+
},
|
|
10030
|
+
// ── Cryptography, tokens, randomness ─────────────────────────────────────
|
|
10031
|
+
{
|
|
10032
|
+
id: "js-jwt-decode-without-verify",
|
|
10033
|
+
title: "JWT decoded without verifying the signature",
|
|
10034
|
+
consequence: "`decode` parses the claims and checks nothing. Anyone can mint a token with any `sub` and any `role`.",
|
|
10035
|
+
cwe: "CWE-347",
|
|
10036
|
+
severity: "critical",
|
|
10037
|
+
languages: ["javascript", "typescript"],
|
|
10038
|
+
pattern: /\bjwt\s*\.\s*decode\s*\(|\bjsonwebtoken\s*\.\s*decode\s*\(|\bdecodeJwt\s*\(/
|
|
10039
|
+
},
|
|
10040
|
+
{
|
|
10041
|
+
id: "tls-verification-disabled",
|
|
10042
|
+
title: "TLS certificate verification disabled",
|
|
10043
|
+
consequence: "Every connection made this way is trivially interceptable; the encryption is decorative.",
|
|
10044
|
+
cwe: "CWE-295",
|
|
10045
|
+
severity: "high",
|
|
10046
|
+
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/
|
|
10047
|
+
},
|
|
10048
|
+
{
|
|
10049
|
+
id: "weak-hash-on-credential",
|
|
10050
|
+
title: "broken hash used on a credential",
|
|
10051
|
+
consequence: "MD5 and SHA-1 are fast and collision-prone; hashed passwords are recoverable.",
|
|
10052
|
+
cwe: "CWE-327",
|
|
10053
|
+
severity: "high",
|
|
10054
|
+
pattern: /(?:createHash|hashlib|MessageDigest\.getInstance|Digest::)\s*[.(]?\s*['"]?(?:md5|MD5|sha1|SHA-?1)['"]?\s*\)?[\s\S]{0,80}(?:password|passwd|secret|token|credential)/i
|
|
10055
|
+
},
|
|
10056
|
+
{
|
|
10057
|
+
id: "insecure-randomness-for-secret",
|
|
10058
|
+
title: "predictable randomness used for a security value",
|
|
10059
|
+
consequence: "`Math.random`/`random.random` are predictable; tokens, session ids and reset codes built from them are guessable.",
|
|
10060
|
+
cwe: "CWE-338",
|
|
10061
|
+
severity: "high",
|
|
10062
|
+
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
|
|
10063
|
+
},
|
|
10064
|
+
{
|
|
10065
|
+
id: "redos-nested-quantifier",
|
|
10066
|
+
title: "regex with nested unbounded quantifiers",
|
|
10067
|
+
consequence: "Catastrophic backtracking: a crafted input of a few dozen characters pins a CPU core for minutes.",
|
|
10068
|
+
cwe: "CWE-1333",
|
|
10069
|
+
severity: "medium",
|
|
10070
|
+
pattern: /\([^)\n]*[+*]\s*\)\s*[+*]|\([^)\n]*\{\d+,\}\s*\)\s*[+*{]/
|
|
10071
|
+
},
|
|
10072
|
+
// ── Temporary files ──────────────────────────────────────────────────────
|
|
10073
|
+
{
|
|
10074
|
+
id: "insecure-temp-file",
|
|
10075
|
+
title: "predictable temporary file path",
|
|
10076
|
+
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.",
|
|
10077
|
+
cwe: "CWE-377",
|
|
10078
|
+
severity: "medium",
|
|
10079
|
+
// A hardcoded path under /tmp is the finding whether or not it is
|
|
10080
|
+
// formatted: `"/tmp/application.log.tmp"` is worse than the PID-based one,
|
|
10081
|
+
// because every process on the host can predict it exactly.
|
|
10082
|
+
pattern: /\btempfile\.mktemp\s*\(|\bos\.tmpnam\s*\(|['"]\/tmp\/[^'"\n]+['"]|['"]\/tmp\/[^'"\n]*\{|\bFile\.createTempFile\s*\(/
|
|
10083
|
+
},
|
|
10084
|
+
// ── Information exposure ─────────────────────────────────────────────────
|
|
10085
|
+
{
|
|
10086
|
+
id: "py-stack-trace-returned",
|
|
10087
|
+
title: "stack trace returned to the caller",
|
|
10088
|
+
consequence: "Tracebacks leak absolute paths, dependency versions and source fragments \u2014 the reconnaissance an attacker would otherwise have to guess at.",
|
|
10089
|
+
cwe: "CWE-209",
|
|
10090
|
+
severity: "medium",
|
|
10091
|
+
languages: ["python"],
|
|
10092
|
+
pattern: /\breturn\b[^\n]*\btraceback\.(?:format_exc|format_exception|print_exc)\s*\(|\breturn\b[^\n]*\bstr\s*\(\s*e\s*\)/
|
|
10093
|
+
},
|
|
10094
|
+
{
|
|
10095
|
+
id: "js-environment-exfiltration",
|
|
10096
|
+
title: "process environment serialised into a payload",
|
|
10097
|
+
consequence: "The environment is where every secret lives. Serialising it whole into a request body is credential exfiltration regardless of the endpoint.",
|
|
10098
|
+
cwe: "CWE-532",
|
|
10099
|
+
severity: "critical",
|
|
10100
|
+
languages: ["javascript", "typescript"],
|
|
10101
|
+
pattern: /\bJSON\.stringify\s*\(\s*\{?[^)]*\bprocess\.env\b(?!\s*\.)/,
|
|
10102
|
+
guard: false
|
|
10103
|
+
},
|
|
10104
|
+
{
|
|
10105
|
+
id: "js-credential-logged",
|
|
10106
|
+
title: "credential read from the environment into a log sink",
|
|
10107
|
+
consequence: "CI retains job logs, and on public forks it publishes them. A token printed once is a token leaked permanently.",
|
|
10108
|
+
cwe: "CWE-532",
|
|
10109
|
+
severity: "high",
|
|
10110
|
+
languages: ["javascript", "typescript"],
|
|
10111
|
+
pattern: /\b(?:token|apiKey|api_key|secret|password|credential|auth)\w*\s*:\s*process\.env\.\w+/i,
|
|
10112
|
+
requires: EXFIL_SINK,
|
|
10113
|
+
guard: false,
|
|
10114
|
+
guardBack: 4,
|
|
10115
|
+
guardForward: 1
|
|
10116
|
+
},
|
|
10117
|
+
// ── Prototype pollution ──────────────────────────────────────────────────
|
|
10118
|
+
{
|
|
10119
|
+
id: "js-prototype-pollution",
|
|
10120
|
+
title: "write to a prototype-reachable key",
|
|
10121
|
+
consequence: "An attacker-supplied `__proto__` key changes behaviour for every object in the process, including ones it never touched.",
|
|
10122
|
+
cwe: "CWE-1321",
|
|
10123
|
+
severity: "high",
|
|
10124
|
+
languages: ["javascript", "typescript"],
|
|
10125
|
+
pattern: /\[\s*['"]__proto__['"]\s*\]|\bObject\s*\.\s*assign\s*\(\s*[\w.$]*\.prototype\b|\.\s*__proto__\s*=/
|
|
10126
|
+
}
|
|
10127
|
+
];
|
|
10128
|
+
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
10129
|
+
var DEFINITION_PREFIX = /^\s*(?:(?:export|public|private|protected|static|final|async|abstract)\s+)*(?:def|function|func|class|module|interface|struct|impl|fn|sub)\b/;
|
|
10130
|
+
function isComment(line) {
|
|
10131
|
+
return COMMENT_PREFIX.test(line);
|
|
10132
|
+
}
|
|
10133
|
+
function proseLines(lines) {
|
|
10134
|
+
const inside = /* @__PURE__ */ new Set();
|
|
10135
|
+
let delimiter = null;
|
|
10136
|
+
lines.forEach((line, index) => {
|
|
10137
|
+
if (delimiter) {
|
|
10138
|
+
inside.add(index);
|
|
10139
|
+
if (line.includes(delimiter)) delimiter = null;
|
|
10140
|
+
return;
|
|
10141
|
+
}
|
|
10142
|
+
for (const candidate of ['"""', "'''"]) {
|
|
10143
|
+
const start = line.indexOf(candidate);
|
|
10144
|
+
if (start === -1) continue;
|
|
10145
|
+
if (line.indexOf(candidate, start + candidate.length) !== -1) return;
|
|
10146
|
+
delimiter = candidate;
|
|
10147
|
+
inside.add(index);
|
|
10148
|
+
return;
|
|
10149
|
+
}
|
|
10150
|
+
});
|
|
10151
|
+
return inside;
|
|
10152
|
+
}
|
|
10153
|
+
function skippable(line, index, prose) {
|
|
10154
|
+
return isComment(line) || DEFINITION_PREFIX.test(line) || (prose?.has(index) ?? false);
|
|
10155
|
+
}
|
|
10156
|
+
function windowText(lines, index, back, forward, prose) {
|
|
10157
|
+
const from = Math.max(0, index - back);
|
|
10158
|
+
const to = Math.min(lines.length - 1, index + forward);
|
|
10159
|
+
const collected = [];
|
|
10160
|
+
for (let i = from; i <= to; i += 1) {
|
|
10161
|
+
const line = lines[i] ?? "";
|
|
10162
|
+
if (i !== index && skippable(line, i, prose)) continue;
|
|
10163
|
+
collected.push(line);
|
|
10164
|
+
}
|
|
10165
|
+
return collected.join("\n");
|
|
10166
|
+
}
|
|
10167
|
+
function evaluateRule(rule, ctx) {
|
|
10168
|
+
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
10169
|
+
const line = ctx.lines[ctx.index] ?? "";
|
|
10170
|
+
if (isComment(line) || ctx.prose?.has(ctx.index)) return null;
|
|
10171
|
+
if (!rule.pattern.test(line)) return null;
|
|
10172
|
+
const back = rule.guardBack ?? 8;
|
|
10173
|
+
const forward = rule.guardForward ?? 0;
|
|
10174
|
+
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
10175
|
+
if (rule.requires && !rule.requires.test(context)) return null;
|
|
10176
|
+
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
10177
|
+
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
10178
|
+
const untrusted = untrustedPatternFor(ctx.language);
|
|
10179
|
+
const contextual = untrusted.test(line) || untrusted.test(context);
|
|
10180
|
+
if (rule.needsContext && !contextual) return null;
|
|
10181
|
+
const confidence = contextual ? "contextual" : "pattern";
|
|
10182
|
+
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
10183
|
+
}
|
|
10184
|
+
|
|
10185
|
+
// src/scan/manifest-rules.ts
|
|
10186
|
+
var POPULAR_NPM = [
|
|
10187
|
+
"react",
|
|
10188
|
+
"react-dom",
|
|
10189
|
+
"lodash",
|
|
10190
|
+
"express",
|
|
10191
|
+
"axios",
|
|
10192
|
+
"chalk",
|
|
10193
|
+
"commander",
|
|
10194
|
+
"debug",
|
|
10195
|
+
"moment",
|
|
10196
|
+
"dayjs",
|
|
10197
|
+
"uuid",
|
|
10198
|
+
"dotenv",
|
|
10199
|
+
"typescript",
|
|
10200
|
+
"webpack",
|
|
10201
|
+
"vite",
|
|
10202
|
+
"rollup",
|
|
10203
|
+
"eslint",
|
|
10204
|
+
"prettier",
|
|
10205
|
+
"jest",
|
|
10206
|
+
"vitest",
|
|
10207
|
+
"mocha",
|
|
10208
|
+
"chai",
|
|
10209
|
+
"sinon",
|
|
10210
|
+
"request",
|
|
10211
|
+
"node-fetch",
|
|
10212
|
+
"cross-env",
|
|
10213
|
+
"rimraf",
|
|
10214
|
+
"glob",
|
|
10215
|
+
"minimist",
|
|
10216
|
+
"yargs",
|
|
10217
|
+
"inquirer",
|
|
10218
|
+
"colors",
|
|
10219
|
+
"ora",
|
|
10220
|
+
"semver",
|
|
10221
|
+
"ws",
|
|
10222
|
+
"socket.io",
|
|
10223
|
+
"mongoose",
|
|
10224
|
+
"sequelize",
|
|
10225
|
+
"knex",
|
|
10226
|
+
"pg",
|
|
10227
|
+
"mysql",
|
|
10228
|
+
"mysql2",
|
|
10229
|
+
"redis",
|
|
10230
|
+
"ioredis",
|
|
10231
|
+
"jsonwebtoken",
|
|
10232
|
+
"bcrypt",
|
|
10233
|
+
"passport",
|
|
10234
|
+
"cors",
|
|
10235
|
+
"helmet",
|
|
10236
|
+
"morgan",
|
|
10237
|
+
"body-parser",
|
|
10238
|
+
"multer",
|
|
10239
|
+
"nodemailer",
|
|
10240
|
+
"puppeteer",
|
|
10241
|
+
"playwright",
|
|
10242
|
+
"cheerio",
|
|
10243
|
+
"sharp",
|
|
10244
|
+
"canvas",
|
|
10245
|
+
"esbuild",
|
|
10246
|
+
"babel",
|
|
10247
|
+
"postcss",
|
|
10248
|
+
"tailwindcss",
|
|
10249
|
+
"next",
|
|
10250
|
+
"nuxt",
|
|
10251
|
+
"vue",
|
|
10252
|
+
"svelte",
|
|
10253
|
+
"angular",
|
|
10254
|
+
"rxjs",
|
|
10255
|
+
"zod"
|
|
10256
|
+
];
|
|
10257
|
+
var POPULAR_PYPI = [
|
|
10258
|
+
"requests",
|
|
10259
|
+
"urllib3",
|
|
10260
|
+
"numpy",
|
|
10261
|
+
"pandas",
|
|
10262
|
+
"scipy",
|
|
10263
|
+
"flask",
|
|
10264
|
+
"django",
|
|
10265
|
+
"fastapi",
|
|
10266
|
+
"sqlalchemy",
|
|
10267
|
+
"pydantic",
|
|
10268
|
+
"click",
|
|
10269
|
+
"jinja2",
|
|
10270
|
+
"pyyaml",
|
|
10271
|
+
"boto3",
|
|
10272
|
+
"botocore",
|
|
10273
|
+
"setuptools",
|
|
10274
|
+
"wheel",
|
|
10275
|
+
"pip",
|
|
10276
|
+
"six",
|
|
10277
|
+
"certifi",
|
|
10278
|
+
"idna",
|
|
10279
|
+
"chardet",
|
|
10280
|
+
"attrs",
|
|
10281
|
+
"python-dateutil",
|
|
10282
|
+
"pytz",
|
|
10283
|
+
"pytest",
|
|
10284
|
+
"tox",
|
|
10285
|
+
"black",
|
|
10286
|
+
"flake8",
|
|
10287
|
+
"mypy",
|
|
10288
|
+
"isort",
|
|
10289
|
+
"beautifulsoup4",
|
|
10290
|
+
"lxml",
|
|
10291
|
+
"pillow",
|
|
10292
|
+
"matplotlib",
|
|
10293
|
+
"seaborn",
|
|
10294
|
+
"scikit-learn",
|
|
10295
|
+
"tensorflow",
|
|
10296
|
+
"torch",
|
|
10297
|
+
"transformers",
|
|
10298
|
+
"openai",
|
|
10299
|
+
"anthropic",
|
|
10300
|
+
"httpx",
|
|
10301
|
+
"aiohttp",
|
|
10302
|
+
"celery",
|
|
10303
|
+
"redis",
|
|
10304
|
+
"psycopg2",
|
|
10305
|
+
"pymongo",
|
|
10306
|
+
"cryptography",
|
|
10307
|
+
"paramiko",
|
|
10308
|
+
"colorama"
|
|
10309
|
+
];
|
|
10310
|
+
var INTERNAL_MARKER = /(?:^|[-_/@])(?:internal|private|corp|intranet|inhouse|confidential)(?:$|[-_/])/i;
|
|
10311
|
+
function normalizeName(name) {
|
|
10312
|
+
return name.toLowerCase().replace(/^@/, "").replace(/[-_.\s]/g, "");
|
|
10313
|
+
}
|
|
10314
|
+
function editDistance(a, b, cap = 3) {
|
|
10315
|
+
if (a === b) return 0;
|
|
10316
|
+
if (Math.abs(a.length - b.length) > cap) return cap + 1;
|
|
10317
|
+
const rows = [];
|
|
10318
|
+
for (let i = 0; i <= a.length; i += 1) {
|
|
10319
|
+
rows.push(new Array(b.length + 1).fill(0));
|
|
10320
|
+
rows[i][0] = i;
|
|
10321
|
+
}
|
|
10322
|
+
for (let j = 0; j <= b.length; j += 1) rows[0][j] = j;
|
|
10323
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
10324
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
10325
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
10326
|
+
let best = Math.min(
|
|
10327
|
+
rows[i - 1][j] + 1,
|
|
10328
|
+
rows[i][j - 1] + 1,
|
|
10329
|
+
rows[i - 1][j - 1] + cost
|
|
10330
|
+
);
|
|
10331
|
+
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
|
|
10332
|
+
best = Math.min(best, rows[i - 2][j - 2] + 1);
|
|
10333
|
+
}
|
|
10334
|
+
rows[i][j] = best;
|
|
10335
|
+
}
|
|
10336
|
+
}
|
|
10337
|
+
return rows[a.length][b.length];
|
|
10338
|
+
}
|
|
10339
|
+
function detectTyposquat(name, ecosystem) {
|
|
10340
|
+
const popular = ecosystem === "npm" ? POPULAR_NPM : POPULAR_PYPI;
|
|
10341
|
+
const lower = name.toLowerCase().replace(/^@[^/]+\//, "");
|
|
10342
|
+
if (popular.includes(lower)) return null;
|
|
10343
|
+
if (lower.length < 4) return null;
|
|
10344
|
+
const normalized = normalizeName(lower);
|
|
10345
|
+
for (const candidate of popular) {
|
|
10346
|
+
const candidateNormalized = normalizeName(candidate);
|
|
10347
|
+
if (normalized === candidateNormalized) return { impersonates: candidate, kind: "separator" };
|
|
10348
|
+
if (editDistance(normalized, candidateNormalized, 1) === 1) {
|
|
10349
|
+
return { impersonates: candidate, kind: "edit" };
|
|
10350
|
+
}
|
|
10351
|
+
}
|
|
10352
|
+
return null;
|
|
10353
|
+
}
|
|
10354
|
+
var LIFECYCLE_SCRIPTS = ["preinstall", "install", "postinstall", "prepare", "prepublish"];
|
|
10355
|
+
function scanPackageJson(text) {
|
|
10356
|
+
const findings = [];
|
|
10357
|
+
const lines = text.split("\n");
|
|
10358
|
+
let parsed;
|
|
10359
|
+
try {
|
|
10360
|
+
parsed = JSON.parse(text);
|
|
10361
|
+
} catch {
|
|
10362
|
+
return findings;
|
|
10363
|
+
}
|
|
10364
|
+
const lineOf = (needle) => {
|
|
10365
|
+
const index = lines.findIndex((line) => line.includes(`"${needle}"`));
|
|
10366
|
+
return index === -1 ? 1 : index + 1;
|
|
10367
|
+
};
|
|
10368
|
+
const depBuckets = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
10369
|
+
for (const bucket of depBuckets) {
|
|
10370
|
+
const deps = parsed[bucket];
|
|
10371
|
+
if (!deps || typeof deps !== "object") continue;
|
|
10372
|
+
for (const name of Object.keys(deps)) {
|
|
10373
|
+
const line = lineOf(name);
|
|
10374
|
+
if (INTERNAL_MARKER.test(name)) {
|
|
10375
|
+
findings.push({
|
|
10376
|
+
ruleId: "manifest-dependency-confusion",
|
|
10377
|
+
title: "internal-looking package resolved from a public registry",
|
|
10378
|
+
line,
|
|
10379
|
+
severity: "critical",
|
|
10380
|
+
cwe: "CWE-1357",
|
|
10381
|
+
message: `"${name}" names itself as internal but carries no registry pin`,
|
|
10382
|
+
consequence: "Whoever registers this name publicly first wins the resolution, and their code runs in your build.",
|
|
10383
|
+
excerpt: (lines[line - 1] ?? "").trim()
|
|
10384
|
+
});
|
|
10385
|
+
continue;
|
|
10386
|
+
}
|
|
10387
|
+
const squat = detectTyposquat(name, "npm");
|
|
10388
|
+
if (squat) {
|
|
10389
|
+
findings.push({
|
|
10390
|
+
ruleId: "manifest-typosquat",
|
|
10391
|
+
title: "dependency name close to a popular package",
|
|
10392
|
+
line,
|
|
10393
|
+
severity: "high",
|
|
10394
|
+
cwe: "CWE-1357",
|
|
10395
|
+
message: squat.kind === "separator" ? `"${name}" differs from "${squat.impersonates}" only in punctuation` : `"${name}" is one edit from "${squat.impersonates}"`,
|
|
10396
|
+
consequence: "A reviewer scanning a diff reads the name they expected. The package that installs is the one that was written.",
|
|
10397
|
+
excerpt: (lines[line - 1] ?? "").trim()
|
|
10398
|
+
});
|
|
10399
|
+
}
|
|
10400
|
+
}
|
|
10401
|
+
}
|
|
10402
|
+
const scripts = parsed.scripts;
|
|
10403
|
+
if (scripts && typeof scripts === "object") {
|
|
10404
|
+
for (const [name, body] of Object.entries(scripts)) {
|
|
10405
|
+
if (!LIFECYCLE_SCRIPTS.includes(name)) continue;
|
|
10406
|
+
findings.push({
|
|
10407
|
+
ruleId: "manifest-install-lifecycle-script",
|
|
10408
|
+
title: "install-time lifecycle script",
|
|
10409
|
+
line: lineOf(name),
|
|
10410
|
+
severity: "medium",
|
|
10411
|
+
cwe: "CWE-506",
|
|
10412
|
+
message: `"${name}" runs automatically on install: ${String(body).slice(0, 120)}`,
|
|
10413
|
+
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.",
|
|
10414
|
+
excerpt: (lines[lineOf(name) - 1] ?? "").trim()
|
|
10415
|
+
});
|
|
10416
|
+
}
|
|
10417
|
+
}
|
|
10418
|
+
return findings;
|
|
10419
|
+
}
|
|
10420
|
+
function scanRequirementsTxt(text) {
|
|
10421
|
+
const findings = [];
|
|
10422
|
+
const lines = text.split("\n");
|
|
10423
|
+
lines.forEach((raw, index) => {
|
|
10424
|
+
const line = raw.trim();
|
|
10425
|
+
if (!line || line.startsWith("#") || line.startsWith("-")) return;
|
|
10426
|
+
const match = /^([A-Za-z0-9_.-]+)\s*(?:[=<>!~]=|@|$)/.exec(line);
|
|
10427
|
+
const name = match?.[1];
|
|
10428
|
+
if (!name) return;
|
|
10429
|
+
if (INTERNAL_MARKER.test(name)) {
|
|
10430
|
+
findings.push({
|
|
10431
|
+
ruleId: "manifest-dependency-confusion",
|
|
10432
|
+
title: "internal-looking package resolved from a public index",
|
|
10433
|
+
line: index + 1,
|
|
10434
|
+
severity: "critical",
|
|
10435
|
+
cwe: "CWE-1357",
|
|
10436
|
+
message: `"${name}" names itself as internal but carries no index pin`,
|
|
10437
|
+
consequence: "pip resolves the highest version across every configured index, so a public package of the same name shadows the private one.",
|
|
10438
|
+
excerpt: line
|
|
10439
|
+
});
|
|
10440
|
+
return;
|
|
10441
|
+
}
|
|
10442
|
+
const squat = detectTyposquat(name, "pypi");
|
|
10443
|
+
if (squat) {
|
|
10444
|
+
findings.push({
|
|
10445
|
+
ruleId: "manifest-typosquat",
|
|
10446
|
+
title: "dependency name close to a popular package",
|
|
10447
|
+
line: index + 1,
|
|
10448
|
+
severity: "high",
|
|
10449
|
+
cwe: "CWE-1357",
|
|
10450
|
+
message: squat.kind === "separator" ? `"${name}" differs from "${squat.impersonates}" only in punctuation` : `"${name}" is one edit from "${squat.impersonates}"`,
|
|
10451
|
+
consequence: "A reviewer scanning a diff reads the name they expected. The package that installs is the one that was written.",
|
|
10452
|
+
excerpt: line
|
|
10453
|
+
});
|
|
10454
|
+
}
|
|
10455
|
+
});
|
|
10456
|
+
return findings;
|
|
10457
|
+
}
|
|
10458
|
+
|
|
10459
|
+
// src/scan/secret-rules.ts
|
|
10460
|
+
var SECRET_RULES = [
|
|
10461
|
+
{
|
|
10462
|
+
id: "secret-aws-access-key",
|
|
10463
|
+
name: "AWS Access Key",
|
|
10464
|
+
pattern: /\b(?:AKIA|ASIA|ABIA|ACCA)[0-9A-Z]{16}\b/,
|
|
10465
|
+
severity: "critical",
|
|
10466
|
+
cwe: "CWE-798",
|
|
10467
|
+
consequence: "Paired with a secret key, grants the API access of whatever IAM principal issued it."
|
|
10468
|
+
},
|
|
10469
|
+
{
|
|
10470
|
+
id: "secret-aws-secret-key",
|
|
10471
|
+
name: "AWS Secret Access Key",
|
|
10472
|
+
pattern: /(?:aws_secret_access_key|AWS_SECRET(?:_ACCESS_KEY)?)\s*[=:]\s*['"]?([A-Za-z0-9/+=]{40})['"]?/i,
|
|
10473
|
+
severity: "critical",
|
|
10474
|
+
cwe: "CWE-798",
|
|
10475
|
+
consequence: "The other half of an AWS credential pair; on its own it is still the hard half to guess."
|
|
10476
|
+
},
|
|
10477
|
+
{
|
|
10478
|
+
id: "secret-github-token",
|
|
10479
|
+
name: "GitHub Token",
|
|
10480
|
+
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/,
|
|
10481
|
+
severity: "critical",
|
|
10482
|
+
cwe: "CWE-798",
|
|
10483
|
+
consequence: "Repository read or write as the issuing account, including the ability to push workflow changes."
|
|
10484
|
+
},
|
|
10485
|
+
{
|
|
10486
|
+
id: "secret-npm-token",
|
|
10487
|
+
name: "npm Token",
|
|
10488
|
+
pattern: /\bnpm_[A-Za-z0-9]{36}\b/,
|
|
10489
|
+
severity: "critical",
|
|
10490
|
+
cwe: "CWE-798",
|
|
10491
|
+
consequence: "Publish rights to every package the account owns \u2014 a supply-chain compromise in one command."
|
|
10492
|
+
},
|
|
10493
|
+
{
|
|
10494
|
+
id: "secret-private-key",
|
|
10495
|
+
name: "Private Key",
|
|
10496
|
+
pattern: /-----BEGIN (?:RSA |EC |DSA |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/,
|
|
10497
|
+
severity: "critical",
|
|
10498
|
+
cwe: "CWE-798",
|
|
10499
|
+
consequence: "Key material, committed. Rotation is the only remediation."
|
|
10500
|
+
},
|
|
10501
|
+
{
|
|
10502
|
+
id: "secret-slack-token",
|
|
10503
|
+
name: "Slack Token",
|
|
10504
|
+
pattern: /\bxox[bpoasr]-[A-Za-z0-9-]{10,}/,
|
|
10505
|
+
severity: "critical",
|
|
10506
|
+
cwe: "CWE-798",
|
|
10507
|
+
consequence: "Read and post access to the workspace as the installing app."
|
|
10508
|
+
},
|
|
10509
|
+
{
|
|
10510
|
+
id: "secret-slack-webhook",
|
|
10511
|
+
name: "Slack Webhook URL",
|
|
10512
|
+
pattern: /https:\/\/hooks\.slack\.com\/services\/[A-Za-z0-9_+\/-]{6,}/,
|
|
10513
|
+
severity: "high",
|
|
10514
|
+
cwe: "CWE-798",
|
|
10515
|
+
consequence: "The URL *is* the credential \u2014 anyone holding it can post to that channel."
|
|
10516
|
+
},
|
|
10517
|
+
{
|
|
10518
|
+
id: "secret-stripe-key",
|
|
10519
|
+
name: "Stripe Key",
|
|
10520
|
+
pattern: /\b(?:sk_live_|rk_live_|sk_test_|rk_test_)[A-Za-z0-9]{20,}\b/,
|
|
10521
|
+
severity: "critical",
|
|
10522
|
+
cwe: "CWE-798",
|
|
10523
|
+
consequence: "Charge, refund and customer-data access against the account."
|
|
10524
|
+
},
|
|
10525
|
+
{
|
|
10526
|
+
id: "secret-sendgrid-key",
|
|
10527
|
+
name: "SendGrid API Key",
|
|
10528
|
+
pattern: /\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/,
|
|
10529
|
+
severity: "critical",
|
|
10530
|
+
cwe: "CWE-798",
|
|
10531
|
+
consequence: "Send mail as the domain \u2014 the credential behind most convincing phishing from a real sender."
|
|
10532
|
+
},
|
|
10533
|
+
{
|
|
10534
|
+
id: "secret-google-api-key",
|
|
10535
|
+
name: "Google API Key",
|
|
10536
|
+
pattern: /\bAIza[0-9A-Za-z_-]{35}\b/,
|
|
10537
|
+
severity: "high",
|
|
10538
|
+
cwe: "CWE-798",
|
|
10539
|
+
consequence: "Quota theft at minimum; API access to whatever the key was scoped to at worst."
|
|
10540
|
+
},
|
|
10541
|
+
{
|
|
10542
|
+
id: "secret-openai-key",
|
|
10543
|
+
name: "OpenAI API Key",
|
|
10544
|
+
pattern: /\bsk-(?:proj-)?[A-Za-z0-9_-]{32,}\b/,
|
|
10545
|
+
severity: "critical",
|
|
10546
|
+
cwe: "CWE-798",
|
|
10547
|
+
consequence: "Billed inference against the owner\u2019s account, with no per-key spend limit by default."
|
|
10548
|
+
},
|
|
10549
|
+
{
|
|
10550
|
+
id: "secret-anthropic-key",
|
|
10551
|
+
name: "Anthropic API Key",
|
|
10552
|
+
pattern: /\bsk-ant-(?:api\d{2}-)?[A-Za-z0-9_-]{32,}\b/,
|
|
10553
|
+
severity: "critical",
|
|
10554
|
+
cwe: "CWE-798",
|
|
10555
|
+
consequence: "Billed inference against the owner\u2019s account."
|
|
10556
|
+
},
|
|
10557
|
+
{
|
|
10558
|
+
id: "secret-database-url",
|
|
10559
|
+
name: "Database URL with credentials",
|
|
10560
|
+
// Requires a credential segment before the `@` — `postgres://localhost/db`
|
|
10561
|
+
// is a hostname, not a secret.
|
|
10562
|
+
pattern: /\b(?:postgres(?:ql)?|mysql|mongodb(?:\+srv)?|redis|amqp|clickhouse):\/\/[^\s'"@\/]*:[^\s'"@\/]*@[^\s'"]+/i,
|
|
10563
|
+
severity: "high",
|
|
10564
|
+
cwe: "CWE-798",
|
|
10565
|
+
consequence: "Direct database access, usually bypassing every application-level authorisation check."
|
|
10566
|
+
},
|
|
10567
|
+
{
|
|
10568
|
+
id: "secret-jwt",
|
|
10569
|
+
name: "JSON Web Token",
|
|
10570
|
+
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
10571
|
+
severity: "medium",
|
|
10572
|
+
cwe: "CWE-798",
|
|
10573
|
+
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
|
|
10574
|
+
},
|
|
10575
|
+
{
|
|
10576
|
+
id: "secret-generic-api-key",
|
|
10577
|
+
name: "Generic API Key",
|
|
10578
|
+
// Quoted assignment only. An unquoted value in a `.env` is covered by the
|
|
10579
|
+
// vendor-prefixed rules above; matching it here is what starts flagging
|
|
10580
|
+
// ARNs and parameter-store paths.
|
|
10581
|
+
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
10582
|
+
severity: "high",
|
|
10583
|
+
cwe: "CWE-798",
|
|
10584
|
+
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
|
|
10585
|
+
},
|
|
10586
|
+
{
|
|
10587
|
+
id: "secret-generic-credential",
|
|
10588
|
+
name: "Hardcoded Credential",
|
|
10589
|
+
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
10590
|
+
severity: "high",
|
|
10591
|
+
cwe: "CWE-798",
|
|
10592
|
+
consequence: "A password in source is a password in every clone, fork and CI cache of that source."
|
|
10593
|
+
},
|
|
10594
|
+
{
|
|
10595
|
+
id: "secret-hex-token",
|
|
10596
|
+
name: "High-entropy Hex Token",
|
|
10597
|
+
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
10598
|
+
severity: "medium",
|
|
10599
|
+
cwe: "CWE-798",
|
|
10600
|
+
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
|
|
10601
|
+
}
|
|
9582
10602
|
];
|
|
9583
|
-
var
|
|
9584
|
-
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
10603
|
+
var KNOWN_PLACEHOLDERS = [
|
|
10604
|
+
// Deliberately NOT here: AWS's published documentation key/secret pair
|
|
10605
|
+
// (`AKIAIOSFODNN7EXAMPLE`, `wJalrXUtnFEMI/…`). GitHub allow-lists them, and
|
|
10606
|
+
// the argument for following suit is that they authenticate nothing. The
|
|
10607
|
+
// argument against is stronger: they appear in a repository because someone
|
|
10608
|
+
// pasted a credentials template and left it there, and the remediation —
|
|
10609
|
+
// move this to the secret manager — is identical to the one for a live key.
|
|
10610
|
+
// Exempting them means the scanner goes quiet on the file most likely to
|
|
10611
|
+
// acquire a real key next.
|
|
10612
|
+
/\bEXAMPLE_?KEY\b/i,
|
|
10613
|
+
/\bYOUR_[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)\b/,
|
|
10614
|
+
/\b(?:xxx+|X{4,}|\*{4,}|<[a-z-]+>)\b/,
|
|
10615
|
+
/\bchangeme\b/i
|
|
9592
10616
|
];
|
|
10617
|
+
function isKnownPlaceholder(text) {
|
|
10618
|
+
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
10619
|
+
}
|
|
10620
|
+
function redactSecret(line) {
|
|
10621
|
+
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
10622
|
+
if (match.length <= 12) return match;
|
|
10623
|
+
return `${match.slice(0, 3)}${"*".repeat(Math.min(16, match.length - 3))}`;
|
|
10624
|
+
});
|
|
10625
|
+
}
|
|
10626
|
+
var SENSITIVE_FILES = [
|
|
10627
|
+
{ pattern: ".env", message: "Environment file committed \u2014 the usual home of every runtime credential", severity: "high" },
|
|
10628
|
+
{ pattern: ".env.local", message: "Local environment file committed", severity: "high" },
|
|
10629
|
+
{ pattern: ".env.production", message: "Production environment file committed", severity: "critical" },
|
|
10630
|
+
{ pattern: "id_rsa", message: "Private SSH key committed", severity: "critical" },
|
|
10631
|
+
{ pattern: "id_ed25519", message: "Private SSH key committed", severity: "critical" },
|
|
10632
|
+
{ pattern: "id_ecdsa", message: "Private SSH key committed", severity: "critical" },
|
|
10633
|
+
{ pattern: ".pem", message: "PEM certificate or key file committed", severity: "high" },
|
|
10634
|
+
{ pattern: ".p12", message: "PKCS#12 keystore committed", severity: "high" },
|
|
10635
|
+
{ pattern: ".pfx", message: "PKCS#12 keystore committed", severity: "high" },
|
|
10636
|
+
{ pattern: ".keystore", message: "Java keystore committed", severity: "high" }
|
|
10637
|
+
// Deliberately not `.npmrc`. Its presence is normal; only an `_authToken`
|
|
10638
|
+
// line in it is a credential, and that is a content match, not a filename
|
|
10639
|
+
// match. Reporting the file itself trades a real finding for a chore.
|
|
10640
|
+
];
|
|
10641
|
+
|
|
10642
|
+
// src/scan/engine.ts
|
|
9593
10643
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
9594
10644
|
"node_modules",
|
|
9595
10645
|
".git",
|
|
9596
10646
|
".next",
|
|
10647
|
+
".nuxt",
|
|
9597
10648
|
"dist",
|
|
9598
10649
|
"build",
|
|
10650
|
+
"out",
|
|
9599
10651
|
"__pycache__",
|
|
9600
10652
|
".venv",
|
|
10653
|
+
"venv",
|
|
9601
10654
|
"vendor",
|
|
9602
10655
|
".terraform",
|
|
9603
10656
|
"coverage",
|
|
9604
|
-
".cache"
|
|
10657
|
+
".cache",
|
|
10658
|
+
".pnpm-store",
|
|
10659
|
+
"target",
|
|
10660
|
+
".gradle",
|
|
10661
|
+
".idea",
|
|
10662
|
+
".vscode",
|
|
10663
|
+
"bower_components",
|
|
10664
|
+
".svelte-kit"
|
|
9605
10665
|
]);
|
|
9606
10666
|
var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
9607
10667
|
".ts",
|
|
9608
10668
|
".js",
|
|
9609
10669
|
".tsx",
|
|
9610
10670
|
".jsx",
|
|
10671
|
+
".mjs",
|
|
10672
|
+
".cjs",
|
|
10673
|
+
".mts",
|
|
10674
|
+
".cts",
|
|
9611
10675
|
".py",
|
|
9612
10676
|
".rb",
|
|
9613
10677
|
".go",
|
|
9614
10678
|
".java",
|
|
10679
|
+
".kt",
|
|
10680
|
+
".scala",
|
|
9615
10681
|
".php",
|
|
9616
10682
|
".rs",
|
|
9617
10683
|
".c",
|
|
10684
|
+
".cc",
|
|
9618
10685
|
".cpp",
|
|
9619
10686
|
".h",
|
|
10687
|
+
".hpp",
|
|
10688
|
+
".cs",
|
|
10689
|
+
".swift",
|
|
9620
10690
|
".yml",
|
|
9621
10691
|
".yaml",
|
|
9622
10692
|
".json",
|
|
@@ -9627,283 +10697,622 @@ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
9627
10697
|
".env",
|
|
9628
10698
|
".sh",
|
|
9629
10699
|
".bash",
|
|
10700
|
+
".zsh",
|
|
9630
10701
|
".tf",
|
|
9631
10702
|
".hcl",
|
|
9632
10703
|
".xml",
|
|
9633
10704
|
".properties",
|
|
9634
|
-
".gradle"
|
|
10705
|
+
".gradle",
|
|
10706
|
+
".txt",
|
|
10707
|
+
".md",
|
|
10708
|
+
".sql",
|
|
10709
|
+
".erb",
|
|
10710
|
+
".ejs",
|
|
10711
|
+
".vue",
|
|
10712
|
+
".svelte"
|
|
9635
10713
|
]);
|
|
9636
|
-
|
|
10714
|
+
var LANGUAGE_BY_EXTENSION = {
|
|
10715
|
+
".js": "javascript",
|
|
10716
|
+
".jsx": "javascript",
|
|
10717
|
+
".mjs": "javascript",
|
|
10718
|
+
".cjs": "javascript",
|
|
10719
|
+
".ts": "typescript",
|
|
10720
|
+
".tsx": "typescript",
|
|
10721
|
+
".mts": "typescript",
|
|
10722
|
+
".cts": "typescript",
|
|
10723
|
+
".vue": "javascript",
|
|
10724
|
+
".svelte": "javascript",
|
|
10725
|
+
".ejs": "javascript",
|
|
10726
|
+
".py": "python",
|
|
10727
|
+
".rb": "ruby",
|
|
10728
|
+
".erb": "ruby",
|
|
10729
|
+
".go": "go",
|
|
10730
|
+
".java": "java",
|
|
10731
|
+
".kt": "java",
|
|
10732
|
+
".scala": "java",
|
|
10733
|
+
".php": "php",
|
|
10734
|
+
".sh": "shell",
|
|
10735
|
+
".bash": "shell",
|
|
10736
|
+
".zsh": "shell",
|
|
10737
|
+
".yml": "config",
|
|
10738
|
+
".yaml": "config",
|
|
10739
|
+
".json": "config",
|
|
10740
|
+
".toml": "config",
|
|
10741
|
+
".ini": "config",
|
|
10742
|
+
".cfg": "config",
|
|
10743
|
+
".conf": "config",
|
|
10744
|
+
".env": "config",
|
|
10745
|
+
".tf": "config",
|
|
10746
|
+
".hcl": "config",
|
|
10747
|
+
".properties": "config"
|
|
10748
|
+
};
|
|
10749
|
+
function languageOf(filename) {
|
|
10750
|
+
if (filename.startsWith(".env") || filename.endsWith(".env")) return "config";
|
|
10751
|
+
return LANGUAGE_BY_EXTENSION[(0, import_node_path3.extname)(filename).toLowerCase()] ?? "other";
|
|
10752
|
+
}
|
|
10753
|
+
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
10754
|
+
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
10755
|
+
function collectSuppressions(lines) {
|
|
10756
|
+
const byLine = /* @__PURE__ */ new Map();
|
|
10757
|
+
let count = 0;
|
|
10758
|
+
const add = (index, ruleId) => {
|
|
10759
|
+
const existing = byLine.get(index) ?? /* @__PURE__ */ new Set();
|
|
10760
|
+
existing.add(ruleId ?? "*");
|
|
10761
|
+
byLine.set(index, existing);
|
|
10762
|
+
count += 1;
|
|
10763
|
+
};
|
|
10764
|
+
lines.forEach((line, index) => {
|
|
10765
|
+
const next = SUPPRESS_NEXT.exec(line);
|
|
10766
|
+
if (next) add(index + 1, next[1]);
|
|
10767
|
+
const same = SUPPRESS_LINE.exec(line);
|
|
10768
|
+
if (same && !next) add(index, same[1]);
|
|
10769
|
+
});
|
|
10770
|
+
return { byLine, count };
|
|
10771
|
+
}
|
|
10772
|
+
function isSuppressed(suppressions, index, ruleId) {
|
|
10773
|
+
const rules = suppressions.byLine.get(index);
|
|
10774
|
+
if (!rules) return false;
|
|
10775
|
+
return rules.has("*") || rules.has(ruleId);
|
|
10776
|
+
}
|
|
10777
|
+
function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
9637
10778
|
const findings = [];
|
|
9638
|
-
|
|
9639
|
-
|
|
10779
|
+
const lines = text.split("\n");
|
|
10780
|
+
const suppressions = collectSuppressions(lines);
|
|
10781
|
+
lines.forEach((line, index) => {
|
|
10782
|
+
for (const rule of SECRET_RULES) {
|
|
10783
|
+
const match = rule.pattern.exec(line);
|
|
10784
|
+
if (!match) continue;
|
|
10785
|
+
if (isKnownPlaceholder(match[0])) continue;
|
|
10786
|
+
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
10787
|
+
findings.push({
|
|
10788
|
+
ruleId: rule.id,
|
|
10789
|
+
title: rule.name,
|
|
10790
|
+
file: relativePath,
|
|
10791
|
+
line: index + 1,
|
|
10792
|
+
severity: rule.severity,
|
|
10793
|
+
// A matched credential format is the finding, not a proxy for one.
|
|
10794
|
+
confidence: "evidence",
|
|
10795
|
+
message: `Possible ${rule.name} detected`,
|
|
10796
|
+
consequence: rule.consequence,
|
|
10797
|
+
cwe: rule.cwe,
|
|
10798
|
+
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
10799
|
+
sensitive: true,
|
|
10800
|
+
category: "secret"
|
|
10801
|
+
});
|
|
10802
|
+
}
|
|
10803
|
+
});
|
|
10804
|
+
const prose = proseLines(lines);
|
|
10805
|
+
lines.forEach((_line, index) => {
|
|
10806
|
+
for (const rule of CODE_RULES) {
|
|
10807
|
+
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
10808
|
+
const match = evaluateRule(rule, { lines, index, language, prose });
|
|
10809
|
+
if (!match) continue;
|
|
10810
|
+
findings.push({
|
|
10811
|
+
ruleId: rule.id,
|
|
10812
|
+
title: rule.title,
|
|
10813
|
+
file: relativePath,
|
|
10814
|
+
line: index + 1,
|
|
10815
|
+
severity: match.severity,
|
|
10816
|
+
confidence: match.confidence,
|
|
10817
|
+
message: `${rule.title} (${rule.cwe})`,
|
|
10818
|
+
consequence: rule.consequence,
|
|
10819
|
+
cwe: rule.cwe,
|
|
10820
|
+
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
10821
|
+
category: "code"
|
|
10822
|
+
});
|
|
10823
|
+
}
|
|
10824
|
+
});
|
|
10825
|
+
return findings;
|
|
10826
|
+
}
|
|
10827
|
+
function scanManifest(relativePath, filename, text) {
|
|
10828
|
+
const manifestFindings = filename === "package.json" ? scanPackageJson(text) : filename === "requirements.txt" ? scanRequirementsTxt(text) : [];
|
|
10829
|
+
return manifestFindings.map((finding) => ({
|
|
10830
|
+
ruleId: finding.ruleId,
|
|
10831
|
+
title: finding.title,
|
|
10832
|
+
file: relativePath,
|
|
10833
|
+
line: finding.line,
|
|
10834
|
+
severity: finding.severity,
|
|
10835
|
+
confidence: "evidence",
|
|
10836
|
+
message: finding.message,
|
|
10837
|
+
consequence: finding.consequence,
|
|
10838
|
+
cwe: finding.cwe,
|
|
10839
|
+
excerpt: finding.excerpt.slice(0, 200),
|
|
10840
|
+
category: "manifest"
|
|
10841
|
+
}));
|
|
10842
|
+
}
|
|
10843
|
+
function scanPath(targetPath, options = {}) {
|
|
10844
|
+
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
10845
|
+
const allowed = options.categories ? new Set(options.categories) : null;
|
|
10846
|
+
const findings = [];
|
|
10847
|
+
const unreadable = [];
|
|
10848
|
+
let filesScanned = 0;
|
|
10849
|
+
let suppressed = 0;
|
|
10850
|
+
const rootIsDirectory = (() => {
|
|
10851
|
+
try {
|
|
10852
|
+
return (0, import_node_fs6.statSync)(targetPath).isDirectory();
|
|
10853
|
+
} catch {
|
|
10854
|
+
return true;
|
|
10855
|
+
}
|
|
10856
|
+
})();
|
|
10857
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path3.dirname)(targetPath);
|
|
10858
|
+
const scanFile = (fullPath, filename) => {
|
|
10859
|
+
const relativePath = toRelative(walkRoot, fullPath);
|
|
10860
|
+
const extension = (0, import_node_path3.extname)(filename).toLowerCase();
|
|
10861
|
+
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
10862
|
+
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
10863
|
+
if (!scannable && !isManifest) {
|
|
10864
|
+
recordSensitiveFile(filename, relativePath, findings, []);
|
|
10865
|
+
return;
|
|
10866
|
+
}
|
|
10867
|
+
let text;
|
|
10868
|
+
let handle;
|
|
10869
|
+
try {
|
|
10870
|
+
handle = (0, import_node_fs6.openSync)(fullPath, "r");
|
|
10871
|
+
} catch {
|
|
10872
|
+
unreadable.push(relativePath);
|
|
10873
|
+
return;
|
|
10874
|
+
}
|
|
10875
|
+
try {
|
|
10876
|
+
if ((0, import_node_fs6.fstatSync)(handle).size > maxFileBytes) return;
|
|
10877
|
+
text = (0, import_node_fs6.readFileSync)(handle, "utf-8");
|
|
10878
|
+
} catch {
|
|
10879
|
+
unreadable.push(relativePath);
|
|
10880
|
+
return;
|
|
10881
|
+
} finally {
|
|
10882
|
+
try {
|
|
10883
|
+
(0, import_node_fs6.closeSync)(handle);
|
|
10884
|
+
} catch {
|
|
10885
|
+
}
|
|
10886
|
+
}
|
|
10887
|
+
filesScanned += 1;
|
|
10888
|
+
options.onFile?.(relativePath);
|
|
10889
|
+
suppressed += collectSuppressions(text.split("\n")).count;
|
|
10890
|
+
const fileFindings = [
|
|
10891
|
+
...scanText(relativePath, text, languageOf(filename)),
|
|
10892
|
+
...isManifest ? scanManifest(relativePath, filename, text) : []
|
|
10893
|
+
];
|
|
10894
|
+
findings.push(...fileFindings);
|
|
10895
|
+
recordSensitiveFile(filename, relativePath, findings, fileFindings);
|
|
10896
|
+
};
|
|
10897
|
+
const walk = (currentPath) => {
|
|
10898
|
+
let entries;
|
|
10899
|
+
try {
|
|
10900
|
+
entries = (0, import_node_fs6.readdirSync)(currentPath, { withFileTypes: true });
|
|
10901
|
+
} catch {
|
|
10902
|
+
unreadable.push(toRelative(walkRoot, currentPath));
|
|
10903
|
+
return;
|
|
10904
|
+
}
|
|
10905
|
+
for (const entry of entries) {
|
|
10906
|
+
const fullPath = (0, import_node_path3.join)(currentPath, entry.name);
|
|
10907
|
+
if (entry.isDirectory()) {
|
|
10908
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
10909
|
+
walk(fullPath);
|
|
10910
|
+
continue;
|
|
10911
|
+
}
|
|
10912
|
+
if (!entry.isFile()) continue;
|
|
10913
|
+
scanFile(fullPath, entry.name);
|
|
10914
|
+
}
|
|
10915
|
+
};
|
|
10916
|
+
if (rootIsDirectory) {
|
|
10917
|
+
walk(targetPath);
|
|
10918
|
+
} else {
|
|
10919
|
+
scanFile(targetPath, (0, import_node_path3.basename)(targetPath));
|
|
10920
|
+
}
|
|
10921
|
+
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
10922
|
+
filtered.sort(
|
|
10923
|
+
(a, b) => severityRank(b.severity) - severityRank(a.severity) || a.file.localeCompare(b.file) || a.line - b.line
|
|
10924
|
+
);
|
|
10925
|
+
return { findings: filtered, filesScanned, unreadable, suppressed, root: walkRoot };
|
|
10926
|
+
}
|
|
10927
|
+
function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
10928
|
+
if (fileFindings.length > 0) return;
|
|
10929
|
+
for (const sensitive of SENSITIVE_FILES) {
|
|
10930
|
+
const matches = filename === sensitive.pattern || filename.endsWith(sensitive.pattern);
|
|
10931
|
+
if (!matches) continue;
|
|
10932
|
+
sink.push({
|
|
10933
|
+
ruleId: "sensitive-file-committed",
|
|
10934
|
+
title: "Sensitive file",
|
|
10935
|
+
file: relativePath,
|
|
10936
|
+
line: 1,
|
|
10937
|
+
severity: sensitive.severity,
|
|
10938
|
+
confidence: "evidence",
|
|
10939
|
+
message: sensitive.message,
|
|
10940
|
+
consequence: "Anything in this file is in every clone, fork and CI cache of the repository.",
|
|
10941
|
+
cwe: "CWE-538",
|
|
10942
|
+
excerpt: "",
|
|
10943
|
+
sensitive: true,
|
|
10944
|
+
category: "file"
|
|
9640
10945
|
});
|
|
9641
|
-
|
|
9642
|
-
|
|
9643
|
-
|
|
9644
|
-
|
|
9645
|
-
|
|
9646
|
-
|
|
9647
|
-
|
|
9648
|
-
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
10946
|
+
return;
|
|
10947
|
+
}
|
|
10948
|
+
}
|
|
10949
|
+
function toRelative(base, target) {
|
|
10950
|
+
const rel = (0, import_node_path3.relative)(base, target);
|
|
10951
|
+
return (rel === "" ? target : rel).split(import_node_path3.sep).join("/");
|
|
10952
|
+
}
|
|
10953
|
+
function meetsFailThreshold(findings, threshold) {
|
|
10954
|
+
if (threshold.length === 0) return false;
|
|
10955
|
+
const floor = Math.min(...threshold.map(severityRank));
|
|
10956
|
+
return findings.some((finding) => severityRank(finding.severity) >= floor);
|
|
10957
|
+
}
|
|
10958
|
+
|
|
10959
|
+
// src/scan/sarif.ts
|
|
10960
|
+
var import_node_path4 = require("path");
|
|
10961
|
+
var SARIF_VERSION = "2.1.0";
|
|
10962
|
+
var SARIF_SCHEMA = "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json";
|
|
10963
|
+
function sarifLevel(severity) {
|
|
10964
|
+
switch (severity) {
|
|
10965
|
+
case "critical":
|
|
10966
|
+
case "high":
|
|
10967
|
+
return "error";
|
|
10968
|
+
case "medium":
|
|
10969
|
+
return "warning";
|
|
10970
|
+
case "low":
|
|
10971
|
+
return "note";
|
|
10972
|
+
default:
|
|
10973
|
+
return "none";
|
|
10974
|
+
}
|
|
10975
|
+
}
|
|
10976
|
+
function securitySeverity(severity) {
|
|
10977
|
+
switch (severity) {
|
|
10978
|
+
case "critical":
|
|
10979
|
+
return "9.0";
|
|
10980
|
+
case "high":
|
|
10981
|
+
return "7.0";
|
|
10982
|
+
case "medium":
|
|
10983
|
+
return "5.0";
|
|
10984
|
+
case "low":
|
|
10985
|
+
return "3.0";
|
|
10986
|
+
default:
|
|
10987
|
+
return "1.0";
|
|
10988
|
+
}
|
|
10989
|
+
}
|
|
10990
|
+
function toArtifactUri(filePath, base, prefix = "", root = base) {
|
|
10991
|
+
const absolute = (0, import_node_path4.isAbsolute)(filePath) ? filePath : (0, import_node_path4.resolve)(root, filePath);
|
|
10992
|
+
const relativePath = (0, import_node_path4.relative)(base, absolute);
|
|
10993
|
+
const escapedOut = relativePath.startsWith("..") || relativePath === "";
|
|
10994
|
+
const chosen = escapedOut ? absolute : relativePath;
|
|
10995
|
+
const posix = chosen.split(import_node_path4.sep).join("/").replace(/^\.\//, "");
|
|
10996
|
+
if (!prefix || escapedOut) return posix;
|
|
10997
|
+
const trimmed = prefix.replace(/^\/+|\/+$/g, "");
|
|
10998
|
+
return trimmed ? `${trimmed}/${posix}` : posix;
|
|
10999
|
+
}
|
|
11000
|
+
function buildSarif(findings, options) {
|
|
11001
|
+
const rules = /* @__PURE__ */ new Map();
|
|
11002
|
+
for (const finding of findings) {
|
|
11003
|
+
if (rules.has(finding.ruleId)) continue;
|
|
11004
|
+
const tags = ["security"];
|
|
11005
|
+
if (finding.cwe) tags.push(`external/cwe/${finding.cwe.toLowerCase()}`);
|
|
11006
|
+
tags.push(`threatcrush/${finding.category}`);
|
|
11007
|
+
const description = finding.consequence ? `${finding.title}. ${finding.consequence}` : finding.title;
|
|
11008
|
+
rules.set(finding.ruleId, {
|
|
11009
|
+
id: finding.ruleId,
|
|
11010
|
+
name: finding.ruleId,
|
|
11011
|
+
shortDescription: { text: finding.title },
|
|
11012
|
+
fullDescription: { text: description },
|
|
11013
|
+
help: {
|
|
11014
|
+
text: description,
|
|
11015
|
+
markdown: finding.consequence ? `**${finding.title}**
|
|
11016
|
+
|
|
11017
|
+
${finding.consequence}` : `**${finding.title}**`
|
|
11018
|
+
},
|
|
11019
|
+
defaultConfiguration: { level: sarifLevel(finding.severity) },
|
|
11020
|
+
properties: {
|
|
11021
|
+
tags,
|
|
11022
|
+
"security-severity": securitySeverity(finding.severity),
|
|
11023
|
+
// SARIF's vocabulary for how much the rule is claiming. It lines up
|
|
11024
|
+
// with the confidence model: a bare construct match is `medium`, a
|
|
11025
|
+
// match with visible untrusted input is `high`.
|
|
11026
|
+
precision: finding.confidence === "pattern" ? "medium" : "high"
|
|
11027
|
+
}
|
|
11028
|
+
});
|
|
11029
|
+
}
|
|
11030
|
+
const base = options.base ?? process.cwd();
|
|
11031
|
+
const root = options.root ?? base;
|
|
11032
|
+
const results = findings.map((finding) => ({
|
|
11033
|
+
ruleId: finding.ruleId,
|
|
11034
|
+
level: sarifLevel(finding.severity),
|
|
11035
|
+
message: { text: finding.message },
|
|
11036
|
+
locations: [
|
|
11037
|
+
{
|
|
11038
|
+
physicalLocation: {
|
|
11039
|
+
artifactLocation: {
|
|
11040
|
+
uri: toArtifactUri(finding.file, base, options.pathPrefix, root),
|
|
11041
|
+
uriBaseId: "%SRCROOT%"
|
|
11042
|
+
},
|
|
11043
|
+
region: {
|
|
11044
|
+
// Clamped, never 0. A whole-file finding has no line; SARIF has no
|
|
11045
|
+
// way to say that, and 0 fails validation outright.
|
|
11046
|
+
startLine: Math.max(1, finding.line),
|
|
11047
|
+
snippet: { text: finding.excerpt }
|
|
11048
|
+
}
|
|
11049
|
+
}
|
|
11050
|
+
}
|
|
11051
|
+
],
|
|
11052
|
+
partialFingerprints: {
|
|
11053
|
+
primaryLocationLineHash: `${finding.ruleId}:${finding.file}:${Math.max(1, finding.line)}`
|
|
11054
|
+
},
|
|
11055
|
+
properties: {
|
|
11056
|
+
severity: finding.severity,
|
|
11057
|
+
confidence: finding.confidence,
|
|
11058
|
+
category: finding.category,
|
|
11059
|
+
...finding.cwe ? { cwe: finding.cwe } : {}
|
|
11060
|
+
}
|
|
11061
|
+
}));
|
|
11062
|
+
return {
|
|
11063
|
+
$schema: SARIF_SCHEMA,
|
|
11064
|
+
version: SARIF_VERSION,
|
|
11065
|
+
runs: [
|
|
11066
|
+
{
|
|
11067
|
+
tool: {
|
|
11068
|
+
driver: {
|
|
11069
|
+
name: "ThreatCrush",
|
|
11070
|
+
version: options.toolVersion,
|
|
11071
|
+
informationUri: "https://threatcrush.com",
|
|
11072
|
+
rules: [...rules.values()]
|
|
11073
|
+
}
|
|
11074
|
+
},
|
|
11075
|
+
results,
|
|
11076
|
+
columnKind: "utf16CodeUnits"
|
|
11077
|
+
}
|
|
11078
|
+
]
|
|
11079
|
+
};
|
|
11080
|
+
}
|
|
11081
|
+
|
|
11082
|
+
// src/commands/scan.ts
|
|
11083
|
+
function readVersion() {
|
|
11084
|
+
for (const candidate of [
|
|
11085
|
+
(0, import_node_path5.join)(__dirname, "..", "package.json"),
|
|
11086
|
+
(0, import_node_path5.join)(__dirname, "..", "..", "package.json")
|
|
11087
|
+
]) {
|
|
11088
|
+
try {
|
|
11089
|
+
return JSON.parse((0, import_node_fs7.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
11090
|
+
} catch {
|
|
11091
|
+
}
|
|
11092
|
+
}
|
|
11093
|
+
return "0.0.0";
|
|
11094
|
+
}
|
|
11095
|
+
var PKG_VERSION = readVersion();
|
|
11096
|
+
function parseFailOn(raw) {
|
|
11097
|
+
if (!raw) return [];
|
|
11098
|
+
const requested = raw.split(",").map((part) => part.trim().toLowerCase()).filter(Boolean);
|
|
11099
|
+
const unknown = requested.filter((name) => !SEVERITY_ORDER.includes(name));
|
|
11100
|
+
if (unknown.length > 0) {
|
|
11101
|
+
throw new Error(
|
|
11102
|
+
`unknown severity in --fail-on: ${unknown.join(", ")} (expected ${SEVERITY_ORDER.join(", ")})`
|
|
11103
|
+
);
|
|
9652
11104
|
}
|
|
9653
|
-
|
|
9654
|
-
|
|
9655
|
-
|
|
9656
|
-
|
|
9657
|
-
|
|
9658
|
-
|
|
11105
|
+
return requested;
|
|
11106
|
+
}
|
|
11107
|
+
function toRunResult(targetPath, findings, filesScanned) {
|
|
11108
|
+
const structured = findings.map((finding) => ({
|
|
11109
|
+
type: finding.title,
|
|
11110
|
+
severity: finding.severity,
|
|
11111
|
+
message: finding.message,
|
|
11112
|
+
location: `${finding.file}:${finding.line}`,
|
|
11113
|
+
details: {
|
|
11114
|
+
file: finding.file,
|
|
11115
|
+
line: finding.line,
|
|
11116
|
+
snippet: finding.excerpt,
|
|
11117
|
+
ruleId: finding.ruleId,
|
|
11118
|
+
confidence: finding.confidence,
|
|
11119
|
+
...finding.cwe ? { cwe: finding.cwe } : {}
|
|
11120
|
+
}
|
|
9659
11121
|
}));
|
|
9660
|
-
const
|
|
11122
|
+
const counts = summarize(structured);
|
|
9661
11123
|
return {
|
|
9662
11124
|
type: "scan",
|
|
9663
11125
|
target: targetPath,
|
|
9664
11126
|
findings: structured,
|
|
9665
|
-
severity_summary:
|
|
9666
|
-
summary: findings.length === 0 ?
|
|
11127
|
+
severity_summary: counts,
|
|
11128
|
+
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`
|
|
9667
11129
|
};
|
|
9668
11130
|
}
|
|
9669
|
-
|
|
9670
|
-
|
|
9671
|
-
|
|
11131
|
+
function failedResult(targetPath, message) {
|
|
11132
|
+
return {
|
|
11133
|
+
type: "scan",
|
|
11134
|
+
target: targetPath,
|
|
11135
|
+
findings: [],
|
|
11136
|
+
severity_summary: { critical: 0, high: 0, medium: 0, low: 0, info: 0 },
|
|
11137
|
+
summary: `Scan failed: ${message}`,
|
|
11138
|
+
error: message
|
|
11139
|
+
};
|
|
11140
|
+
}
|
|
11141
|
+
async function runScan(targetPath) {
|
|
11142
|
+
try {
|
|
11143
|
+
const report = scanPath(targetPath);
|
|
11144
|
+
const findings = [...report.findings, ...await scanDependencies(targetPath)];
|
|
11145
|
+
return toRunResult(targetPath, findings, report.filesScanned);
|
|
11146
|
+
} catch (err) {
|
|
11147
|
+
return failedResult(targetPath, err.message);
|
|
11148
|
+
}
|
|
11149
|
+
}
|
|
11150
|
+
async function scanCommand(targetPath, options = {}) {
|
|
11151
|
+
const format = options.format ?? "text";
|
|
11152
|
+
const machineReadable = format !== "text";
|
|
11153
|
+
const say = machineReadable ? (line) => process.stderr.write(`${line}
|
|
11154
|
+
`) : (line) => process.stdout.write(`${line}
|
|
9672
11155
|
`);
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
11156
|
+
if (!(0, import_node_fs7.existsSync)(targetPath)) {
|
|
11157
|
+
say(source_default.red(`Scan target does not exist: ${targetPath}`));
|
|
11158
|
+
process.exitCode = 2;
|
|
11159
|
+
return failedResult(targetPath, `no such path: ${targetPath}`);
|
|
11160
|
+
}
|
|
11161
|
+
if (!machineReadable) {
|
|
11162
|
+
banner();
|
|
11163
|
+
logger.info(`Scanning ${source_default.white(targetPath)} for security issues...
|
|
11164
|
+
`);
|
|
11165
|
+
}
|
|
11166
|
+
const spinner = machineReadable ? null : ora({ text: "Scanning files...", color: "green" }).start();
|
|
11167
|
+
let outcome;
|
|
9676
11168
|
try {
|
|
9677
|
-
|
|
9678
|
-
|
|
9679
|
-
|
|
11169
|
+
let seen = 0;
|
|
11170
|
+
const report = scanPath(targetPath, {
|
|
11171
|
+
onFile: () => {
|
|
11172
|
+
seen += 1;
|
|
11173
|
+
if (spinner) spinner.text = `Scanning files... (${seen} files)`;
|
|
11174
|
+
}
|
|
9680
11175
|
});
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
11176
|
+
if (options.dependencies) {
|
|
11177
|
+
if (spinner) spinner.text = "Querying OSV.dev for dependency advisories...";
|
|
11178
|
+
report.findings.push(...await scanDependencies(targetPath));
|
|
11179
|
+
}
|
|
11180
|
+
outcome = {
|
|
11181
|
+
result: toRunResult(targetPath, report.findings, report.filesScanned),
|
|
11182
|
+
findings: report.findings,
|
|
11183
|
+
filesScanned: report.filesScanned,
|
|
11184
|
+
unreadable: report.unreadable,
|
|
11185
|
+
suppressed: report.suppressed,
|
|
11186
|
+
root: report.root
|
|
9690
11187
|
};
|
|
11188
|
+
} catch (err) {
|
|
11189
|
+
spinner?.fail(`Scan failed: ${err.message}`);
|
|
11190
|
+
process.exitCode = 2;
|
|
11191
|
+
return failedResult(targetPath, err.message);
|
|
9691
11192
|
}
|
|
9692
|
-
spinner
|
|
11193
|
+
spinner?.succeed(`Scanned ${outcome.filesScanned} files
|
|
9693
11194
|
`);
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
9699
|
-
|
|
9700
|
-
|
|
9701
|
-
|
|
11195
|
+
if (outcome.unreadable.length > 0) {
|
|
11196
|
+
say(
|
|
11197
|
+
source_default.yellow(
|
|
11198
|
+
` ! ${outcome.unreadable.length} path(s) could not be read and were NOT scanned`
|
|
11199
|
+
)
|
|
11200
|
+
);
|
|
11201
|
+
if (options.verbose) {
|
|
11202
|
+
for (const path of outcome.unreadable) say(source_default.gray(` ${path}`));
|
|
11203
|
+
}
|
|
11204
|
+
}
|
|
11205
|
+
if (outcome.suppressed > 0) {
|
|
11206
|
+
say(
|
|
11207
|
+
source_default.gray(
|
|
11208
|
+
` \xB7 ${outcome.suppressed} finding(s) suppressed by inline threatcrush-disable comments`
|
|
11209
|
+
)
|
|
11210
|
+
);
|
|
11211
|
+
}
|
|
11212
|
+
if (machineReadable) {
|
|
11213
|
+
emitMachineReadable(format, outcome, targetPath, options, say);
|
|
11214
|
+
} else {
|
|
11215
|
+
printHuman(outcome);
|
|
11216
|
+
}
|
|
11217
|
+
const failOn = options.failOn ?? [];
|
|
11218
|
+
if (meetsFailThreshold(outcome.findings, failOn)) {
|
|
11219
|
+
say(
|
|
11220
|
+
source_default.red(
|
|
11221
|
+
`
|
|
11222
|
+
\u2717 findings at or above ${[...failOn].join("/")} \u2014 failing as requested by --fail-on`
|
|
11223
|
+
)
|
|
11224
|
+
);
|
|
11225
|
+
process.exitCode = 1;
|
|
11226
|
+
}
|
|
11227
|
+
return outcome.result;
|
|
11228
|
+
}
|
|
11229
|
+
function emitMachineReadable(format, outcome, targetPath, options, say) {
|
|
11230
|
+
const payload = format === "sarif" ? buildSarif(outcome.findings, {
|
|
11231
|
+
toolVersion: PKG_VERSION,
|
|
11232
|
+
pathPrefix: options.pathPrefix,
|
|
11233
|
+
// Relative to the working directory, NOT the scan root. `threatcrush
|
|
11234
|
+
// scan vulns` from a repo root must emit `vulns/secrets/x.env`, not
|
|
11235
|
+
// `secrets/x.env` — the second form matches nothing in the
|
|
11236
|
+
// consumer's view of the repository, so every finding lands
|
|
11237
|
+
// "outside" whatever it scoped to and a working scan reads as 0%.
|
|
11238
|
+
// This is the single most expensive mistake in the whole pipeline
|
|
11239
|
+
// and it fails silently. `--path-prefix` covers the remaining case:
|
|
11240
|
+
// a scan run from inside the subdirectory it is scanning.
|
|
11241
|
+
base: process.cwd(),
|
|
11242
|
+
root: (0, import_node_path5.resolve)(outcome.root)
|
|
11243
|
+
}) : {
|
|
11244
|
+
tool: "threatcrush",
|
|
11245
|
+
version: PKG_VERSION,
|
|
11246
|
+
target: targetPath,
|
|
11247
|
+
filesScanned: outcome.filesScanned,
|
|
11248
|
+
unreadable: outcome.unreadable,
|
|
11249
|
+
suppressed: outcome.suppressed,
|
|
11250
|
+
summary: outcome.result.severity_summary,
|
|
11251
|
+
findings: outcome.findings
|
|
11252
|
+
};
|
|
11253
|
+
const serialized = `${JSON.stringify(payload, null, 2)}
|
|
11254
|
+
`;
|
|
11255
|
+
if (options.output) {
|
|
11256
|
+
(0, import_node_fs7.mkdirSync)((0, import_node_path5.dirname)((0, import_node_path5.resolve)(options.output)), { recursive: true });
|
|
11257
|
+
(0, import_node_fs7.writeFileSync)(options.output, serialized, "utf-8");
|
|
11258
|
+
say(
|
|
11259
|
+
source_default.gray(
|
|
11260
|
+
` ${format.toUpperCase()} written to ${options.output} (${outcome.findings.length} finding(s))`
|
|
11261
|
+
)
|
|
11262
|
+
);
|
|
11263
|
+
return;
|
|
11264
|
+
}
|
|
11265
|
+
process.stdout.write(serialized);
|
|
11266
|
+
}
|
|
11267
|
+
function printHuman(outcome) {
|
|
11268
|
+
const { findings, filesScanned } = outcome;
|
|
9702
11269
|
if (findings.length === 0) {
|
|
9703
11270
|
console.log(source_default.green.bold(" \u2713 No security issues found!"));
|
|
9704
11271
|
console.log();
|
|
9705
|
-
return
|
|
9706
|
-
type: "scan",
|
|
9707
|
-
target: targetPath,
|
|
9708
|
-
findings: [],
|
|
9709
|
-
severity_summary: sevCounts,
|
|
9710
|
-
summary: `No issues found across ${filesScanned} files`
|
|
9711
|
-
};
|
|
11272
|
+
return;
|
|
9712
11273
|
}
|
|
9713
|
-
const
|
|
9714
|
-
const high = findings.filter((f) => f.severity === "high");
|
|
9715
|
-
const medium = findings.filter((f) => f.severity === "medium");
|
|
9716
|
-
const low = findings.filter((f) => f.severity === "low");
|
|
11274
|
+
const counts = outcome.result.severity_summary;
|
|
9717
11275
|
console.log(source_default.white.bold(" Scan Results"));
|
|
9718
11276
|
console.log(source_default.gray(" " + "\u2500".repeat(70)));
|
|
9719
11277
|
console.log(
|
|
9720
|
-
` ${source_default.red.bold(critical
|
|
11278
|
+
` ${source_default.red.bold(counts.critical + " critical")} ${source_default.red(counts.high + " high")} ${source_default.yellow(counts.medium + " medium")} ${source_default.gray(counts.low + " low")}`
|
|
9721
11279
|
);
|
|
9722
11280
|
console.log(source_default.gray(" " + "\u2500".repeat(70)));
|
|
9723
11281
|
console.log();
|
|
9724
|
-
const
|
|
9725
|
-
|
|
9726
|
-
const
|
|
9727
|
-
console.log(` ${
|
|
9728
|
-
console.log(
|
|
11282
|
+
for (const finding of findings) {
|
|
11283
|
+
const label = finding.severity.toUpperCase();
|
|
11284
|
+
const badge = finding.severity === "critical" ? source_default.bgRed.white.bold(` ${label} `) : finding.severity === "high" ? source_default.red(`[${label}]`) : finding.severity === "medium" ? source_default.yellow(`[${label}]`) : source_default.gray(`[${label}]`);
|
|
11285
|
+
console.log(` ${badge} ${source_default.white.bold(finding.title)}`);
|
|
11286
|
+
console.log(
|
|
11287
|
+
` ${source_default.gray("File:")} ${source_default.cyan(finding.file)}:${source_default.yellow(String(finding.line))}`
|
|
11288
|
+
);
|
|
9729
11289
|
console.log(` ${source_default.gray("Info:")} ${finding.message}`);
|
|
9730
|
-
if (finding.
|
|
9731
|
-
|
|
9732
|
-
|
|
9733
|
-
|
|
9734
|
-
);
|
|
9735
|
-
console.log(` ${source_default.gray("Code:")} ${redacted.trim()}`);
|
|
11290
|
+
if (finding.consequence) {
|
|
11291
|
+
console.log(` ${source_default.gray("Risk:")} ${source_default.dim(finding.consequence)}`);
|
|
11292
|
+
}
|
|
11293
|
+
if (finding.excerpt) {
|
|
11294
|
+
console.log(` ${source_default.gray("Code:")} ${finding.excerpt}`);
|
|
9736
11295
|
}
|
|
11296
|
+
console.log(
|
|
11297
|
+
` ${source_default.gray("Rule:")} ${source_default.dim(finding.ruleId)}` + (finding.cwe ? source_default.dim(` \xB7 ${finding.cwe}`) : "") + source_default.dim(` \xB7 confidence: ${finding.confidence}`)
|
|
11298
|
+
);
|
|
9737
11299
|
console.log();
|
|
9738
11300
|
}
|
|
9739
11301
|
console.log(source_default.gray(" " + "\u2500".repeat(70)));
|
|
9740
|
-
console.log(
|
|
11302
|
+
console.log(
|
|
11303
|
+
` ${source_default.white.bold(`${findings.length} issue(s) found`)} across ${filesScanned} files`
|
|
11304
|
+
);
|
|
9741
11305
|
console.log();
|
|
9742
|
-
return {
|
|
9743
|
-
type: "scan",
|
|
9744
|
-
target: targetPath,
|
|
9745
|
-
findings: structured,
|
|
9746
|
-
severity_summary: sevCounts,
|
|
9747
|
-
summary: `${findings.length} issue(s): ${sevCounts.critical}C ${sevCounts.high}H ${sevCounts.medium}M ${sevCounts.low}L`
|
|
9748
|
-
};
|
|
9749
|
-
}
|
|
9750
|
-
function scanDirectory(basePath, currentPath, findings, onFile) {
|
|
9751
|
-
let entries;
|
|
9752
|
-
try {
|
|
9753
|
-
entries = (0, import_node_fs5.readdirSync)(currentPath, { withFileTypes: true });
|
|
9754
|
-
} catch {
|
|
9755
|
-
return;
|
|
9756
|
-
}
|
|
9757
|
-
for (const entry of entries) {
|
|
9758
|
-
const fullPath = (0, import_node_path2.join)(currentPath, entry.name);
|
|
9759
|
-
if (entry.isDirectory()) {
|
|
9760
|
-
if (SKIP_DIRS.has(entry.name)) continue;
|
|
9761
|
-
scanDirectory(basePath, fullPath, findings, onFile);
|
|
9762
|
-
continue;
|
|
9763
|
-
}
|
|
9764
|
-
if (!entry.isFile()) continue;
|
|
9765
|
-
for (const mc of MISCONFIG_FILES) {
|
|
9766
|
-
if (entry.name === mc.pattern || entry.name.endsWith(mc.pattern)) {
|
|
9767
|
-
findings.push({
|
|
9768
|
-
file: (0, import_node_path2.relative)(basePath, fullPath),
|
|
9769
|
-
line: 0,
|
|
9770
|
-
type: "Sensitive File",
|
|
9771
|
-
severity: "high",
|
|
9772
|
-
message: mc.message,
|
|
9773
|
-
snippet: ""
|
|
9774
|
-
});
|
|
9775
|
-
}
|
|
9776
|
-
}
|
|
9777
|
-
const ext = (0, import_node_path2.extname)(entry.name).toLowerCase();
|
|
9778
|
-
if (!SCAN_EXTENSIONS.has(ext) && !entry.name.startsWith(".env")) continue;
|
|
9779
|
-
try {
|
|
9780
|
-
const stat = (0, import_node_fs5.statSync)(fullPath);
|
|
9781
|
-
if (stat.size > 1024 * 1024) continue;
|
|
9782
|
-
} catch {
|
|
9783
|
-
continue;
|
|
9784
|
-
}
|
|
9785
|
-
onFile();
|
|
9786
|
-
let content;
|
|
9787
|
-
try {
|
|
9788
|
-
content = (0, import_node_fs5.readFileSync)(fullPath, "utf-8");
|
|
9789
|
-
} catch {
|
|
9790
|
-
continue;
|
|
9791
|
-
}
|
|
9792
|
-
const lines = content.split("\n");
|
|
9793
|
-
for (let i = 0; i < lines.length; i++) {
|
|
9794
|
-
const line = lines[i];
|
|
9795
|
-
if (line.trim().startsWith("//") && !line.includes("password") && !line.includes("secret")) continue;
|
|
9796
|
-
for (const pattern of SECRET_PATTERNS) {
|
|
9797
|
-
pattern.pattern.lastIndex = 0;
|
|
9798
|
-
if (pattern.pattern.test(line)) {
|
|
9799
|
-
findings.push({
|
|
9800
|
-
file: (0, import_node_path2.relative)(basePath, fullPath),
|
|
9801
|
-
line: i + 1,
|
|
9802
|
-
type: pattern.name,
|
|
9803
|
-
severity: pattern.severity,
|
|
9804
|
-
message: `Possible ${pattern.name} detected`,
|
|
9805
|
-
snippet: line.length > 120 ? line.slice(0, 120) + "..." : line
|
|
9806
|
-
});
|
|
9807
|
-
}
|
|
9808
|
-
}
|
|
9809
|
-
}
|
|
9810
|
-
}
|
|
9811
|
-
}
|
|
9812
|
-
async function scanDependencies(targetPath) {
|
|
9813
|
-
const findings = [];
|
|
9814
|
-
const lockfiles = [
|
|
9815
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
9816
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
9817
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
9818
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
9819
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
9820
|
-
];
|
|
9821
|
-
for (const { file, ecosystem } of lockfiles) {
|
|
9822
|
-
const lockPath = (0, import_node_path2.join)(targetPath, file);
|
|
9823
|
-
if (!(0, import_node_fs5.existsSync)(lockPath)) continue;
|
|
9824
|
-
try {
|
|
9825
|
-
const deps = parseDependencies(lockPath, file, ecosystem);
|
|
9826
|
-
for (const dep of deps.slice(0, 50)) {
|
|
9827
|
-
try {
|
|
9828
|
-
const vulns = await queryOsv(dep.name, dep.version, ecosystem);
|
|
9829
|
-
for (const vuln of vulns) {
|
|
9830
|
-
const cvssScore = vuln.severity?.find((s) => s.type === "CVSS_V3")?.score;
|
|
9831
|
-
const severity = cvssScore ? parseFloat(cvssScore) >= 9 ? "critical" : parseFloat(cvssScore) >= 7 ? "high" : parseFloat(cvssScore) >= 4 ? "medium" : "low" : "medium";
|
|
9832
|
-
findings.push({
|
|
9833
|
-
file,
|
|
9834
|
-
line: 0,
|
|
9835
|
-
type: "Dependency CVE",
|
|
9836
|
-
severity,
|
|
9837
|
-
message: `${dep.name}@${dep.version}: ${vuln.summary || vuln.id}`,
|
|
9838
|
-
snippet: `${vuln.id}${cvssScore ? ` (CVSS: ${cvssScore})` : ""}`
|
|
9839
|
-
});
|
|
9840
|
-
}
|
|
9841
|
-
} catch {
|
|
9842
|
-
}
|
|
9843
|
-
}
|
|
9844
|
-
} catch {
|
|
9845
|
-
}
|
|
9846
|
-
}
|
|
9847
|
-
return findings;
|
|
9848
|
-
}
|
|
9849
|
-
function parseDependencies(lockPath, filename, ecosystem) {
|
|
9850
|
-
const deps = [];
|
|
9851
|
-
if (filename === "package-lock.json") {
|
|
9852
|
-
try {
|
|
9853
|
-
const lock = JSON.parse((0, import_node_fs5.readFileSync)(lockPath, "utf-8"));
|
|
9854
|
-
const packages = lock.packages || lock.dependencies || {};
|
|
9855
|
-
for (const [key, value] of Object.entries(packages)) {
|
|
9856
|
-
const name = key.replace(/^node_modules\//, "");
|
|
9857
|
-
const version = value.version;
|
|
9858
|
-
if (name && version && !name.startsWith(".")) {
|
|
9859
|
-
deps.push({ name, version });
|
|
9860
|
-
}
|
|
9861
|
-
}
|
|
9862
|
-
} catch {
|
|
9863
|
-
}
|
|
9864
|
-
} else if (filename === "requirements.txt") {
|
|
9865
|
-
try {
|
|
9866
|
-
const content = (0, import_node_fs5.readFileSync)(lockPath, "utf-8");
|
|
9867
|
-
for (const line of content.split("\n")) {
|
|
9868
|
-
const match = line.match(/^([a-zA-Z0-9_.-]+)==([0-9.]+)/);
|
|
9869
|
-
if (match) deps.push({ name: match[1], version: match[2] });
|
|
9870
|
-
}
|
|
9871
|
-
} catch {
|
|
9872
|
-
}
|
|
9873
|
-
}
|
|
9874
|
-
return deps;
|
|
9875
|
-
}
|
|
9876
|
-
function isValidPackageName(name) {
|
|
9877
|
-
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
9878
|
-
}
|
|
9879
|
-
function isValidVersion(version) {
|
|
9880
|
-
return /^[0-9a-zA-Z._\-+]{1,50}$/.test(version);
|
|
9881
|
-
}
|
|
9882
|
-
async function queryOsv(name, version, ecosystem) {
|
|
9883
|
-
if (!isValidPackageName(name) || !isValidVersion(version)) return [];
|
|
9884
|
-
try {
|
|
9885
|
-
const res = await fetch("https://api.osv.dev/v1/query", {
|
|
9886
|
-
method: "POST",
|
|
9887
|
-
headers: { "Content-Type": "application/json" },
|
|
9888
|
-
body: JSON.stringify({ package: { name, ecosystem }, version }),
|
|
9889
|
-
signal: AbortSignal.timeout(5e3)
|
|
9890
|
-
});
|
|
9891
|
-
if (!res.ok) return [];
|
|
9892
|
-
const data = await res.json();
|
|
9893
|
-
return data.vulns || [];
|
|
9894
|
-
} catch {
|
|
9895
|
-
return [];
|
|
9896
|
-
}
|
|
9897
11306
|
}
|
|
9898
11307
|
|
|
9899
11308
|
// src/commands/init.ts
|
|
9900
|
-
var
|
|
11309
|
+
var import_node_fs10 = require("fs");
|
|
9901
11310
|
var import_node_child_process = require("child_process");
|
|
9902
11311
|
var import_node_readline3 = __toESM(require("readline"));
|
|
9903
11312
|
|
|
9904
11313
|
// src/core/config.ts
|
|
9905
|
-
var
|
|
9906
|
-
var
|
|
11314
|
+
var import_node_fs8 = require("fs");
|
|
11315
|
+
var import_node_path6 = require("path");
|
|
9907
11316
|
var import_toml = __toESM(require_toml());
|
|
9908
11317
|
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
9909
11318
|
var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
|
|
@@ -9929,11 +11338,11 @@ var DEFAULT_CONFIG = {
|
|
|
9929
11338
|
};
|
|
9930
11339
|
function loadConfig(configPath) {
|
|
9931
11340
|
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
9932
|
-
if (!(0,
|
|
11341
|
+
if (!(0, import_node_fs8.existsSync)(path)) {
|
|
9933
11342
|
return { ...DEFAULT_CONFIG };
|
|
9934
11343
|
}
|
|
9935
11344
|
try {
|
|
9936
|
-
const raw = (0,
|
|
11345
|
+
const raw = (0, import_node_fs8.readFileSync)(path, "utf-8");
|
|
9937
11346
|
const parsed = import_toml.default.parse(raw);
|
|
9938
11347
|
return {
|
|
9939
11348
|
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
@@ -9949,13 +11358,13 @@ function loadConfig(configPath) {
|
|
|
9949
11358
|
function loadModuleConfigs(confDir) {
|
|
9950
11359
|
const dir = confDir || DEFAULT_CONFDIR;
|
|
9951
11360
|
const configs = /* @__PURE__ */ new Map();
|
|
9952
|
-
if (!(0,
|
|
11361
|
+
if (!(0, import_node_fs8.existsSync)(dir)) {
|
|
9953
11362
|
return configs;
|
|
9954
11363
|
}
|
|
9955
|
-
const files = (0,
|
|
11364
|
+
const files = (0, import_node_fs8.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
|
|
9956
11365
|
for (const file of files) {
|
|
9957
11366
|
try {
|
|
9958
|
-
const raw = (0,
|
|
11367
|
+
const raw = (0, import_node_fs8.readFileSync)((0, import_node_path6.join)(dir, file), "utf-8");
|
|
9959
11368
|
const parsed = import_toml.default.parse(raw);
|
|
9960
11369
|
for (const [name, config] of Object.entries(parsed)) {
|
|
9961
11370
|
configs.set(name, config);
|
|
@@ -9984,23 +11393,23 @@ function generateModuleConfig(moduleName, defaults = {}) {
|
|
|
9984
11393
|
}
|
|
9985
11394
|
|
|
9986
11395
|
// src/core/cli-config.ts
|
|
9987
|
-
var
|
|
9988
|
-
var
|
|
11396
|
+
var import_node_fs9 = require("fs");
|
|
11397
|
+
var import_node_path7 = require("path");
|
|
9989
11398
|
var import_node_os4 = require("os");
|
|
9990
|
-
var CLI_CONFIG_DIR = (0,
|
|
9991
|
-
var CLI_CONFIG_PATH = (0,
|
|
11399
|
+
var CLI_CONFIG_DIR = (0, import_node_path7.join)((0, import_node_os4.homedir)(), ".threatcrush");
|
|
11400
|
+
var CLI_CONFIG_PATH = (0, import_node_path7.join)(CLI_CONFIG_DIR, "config.json");
|
|
9992
11401
|
function readCliConfig() {
|
|
9993
11402
|
try {
|
|
9994
|
-
return JSON.parse((0,
|
|
11403
|
+
return JSON.parse((0, import_node_fs9.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
|
|
9995
11404
|
} catch {
|
|
9996
11405
|
return {};
|
|
9997
11406
|
}
|
|
9998
11407
|
}
|
|
9999
11408
|
function writeCliConfig(config) {
|
|
10000
|
-
if (!(0,
|
|
10001
|
-
(0,
|
|
11409
|
+
if (!(0, import_node_fs9.existsSync)(CLI_CONFIG_DIR)) (0, import_node_fs9.mkdirSync)(CLI_CONFIG_DIR, { recursive: true });
|
|
11410
|
+
(0, import_node_fs9.writeFileSync)(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
10002
11411
|
try {
|
|
10003
|
-
(0,
|
|
11412
|
+
(0, import_node_fs9.chmodSync)(CLI_CONFIG_PATH, 384);
|
|
10004
11413
|
} catch {
|
|
10005
11414
|
}
|
|
10006
11415
|
}
|
|
@@ -10038,13 +11447,13 @@ var import_node_stream = require("stream");
|
|
|
10038
11447
|
var API_URL = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
10039
11448
|
function prompt(question) {
|
|
10040
11449
|
const rl = import_node_readline2.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
10041
|
-
return new Promise((
|
|
11450
|
+
return new Promise((resolve5) => rl.question(question, (answer) => {
|
|
10042
11451
|
rl.close();
|
|
10043
|
-
|
|
11452
|
+
resolve5(answer.trim());
|
|
10044
11453
|
}));
|
|
10045
11454
|
}
|
|
10046
11455
|
function promptPassword(question) {
|
|
10047
|
-
return new Promise((
|
|
11456
|
+
return new Promise((resolve5) => {
|
|
10048
11457
|
const muted = new import_node_stream.Writable({
|
|
10049
11458
|
write(_chunk, _enc, cb) {
|
|
10050
11459
|
cb();
|
|
@@ -10055,7 +11464,7 @@ function promptPassword(question) {
|
|
|
10055
11464
|
rl.question("", (answer) => {
|
|
10056
11465
|
rl.close();
|
|
10057
11466
|
process.stdout.write("\n");
|
|
10058
|
-
|
|
11467
|
+
resolve5(answer);
|
|
10059
11468
|
});
|
|
10060
11469
|
});
|
|
10061
11470
|
}
|
|
@@ -10245,16 +11654,16 @@ function binaryExists(name) {
|
|
|
10245
11654
|
}
|
|
10246
11655
|
}
|
|
10247
11656
|
function findLogPath(paths) {
|
|
10248
|
-
return paths.find((p) => (0,
|
|
11657
|
+
return paths.find((p) => (0, import_node_fs10.existsSync)(p));
|
|
10249
11658
|
}
|
|
10250
11659
|
async function promptYesNo(question, fallback2) {
|
|
10251
11660
|
const rl = import_node_readline3.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
10252
11661
|
const hint = fallback2 ? "(Y/n)" : "(y/N)";
|
|
10253
|
-
return new Promise((
|
|
11662
|
+
return new Promise((resolve5) => rl.question(`${question} ${hint}: `, (answer) => {
|
|
10254
11663
|
rl.close();
|
|
10255
11664
|
const trimmed = answer.trim().toLowerCase();
|
|
10256
|
-
if (!trimmed) return
|
|
10257
|
-
|
|
11665
|
+
if (!trimmed) return resolve5(fallback2);
|
|
11666
|
+
resolve5(trimmed === "y" || trimmed === "yes");
|
|
10258
11667
|
}));
|
|
10259
11668
|
}
|
|
10260
11669
|
async function ensureLoggedIn() {
|
|
@@ -10361,11 +11770,11 @@ async function initCommand() {
|
|
|
10361
11770
|
}
|
|
10362
11771
|
} else {
|
|
10363
11772
|
const spinner2 = ora({ text: "Writing configuration files...", color: "green" }).start();
|
|
10364
|
-
(0,
|
|
10365
|
-
(0,
|
|
10366
|
-
(0,
|
|
11773
|
+
(0, import_node_fs10.mkdirSync)(confDDir, { recursive: true });
|
|
11774
|
+
(0, import_node_fs10.mkdirSync)("/var/log/threatcrush", { recursive: true });
|
|
11775
|
+
(0, import_node_fs10.mkdirSync)("/var/lib/threatcrush", { recursive: true });
|
|
10367
11776
|
const mainConfig = generateDefaultConfig(detected.map((d) => d.name));
|
|
10368
|
-
(0,
|
|
11777
|
+
(0, import_node_fs10.writeFileSync)(`${configDir}/threatcrushd.conf`, mainConfig);
|
|
10369
11778
|
for (const svc of detected) {
|
|
10370
11779
|
const svcDef = SERVICES_TO_DETECT.find((s) => s.name === svc.name);
|
|
10371
11780
|
if (!svcDef) continue;
|
|
@@ -10374,7 +11783,7 @@ async function initCommand() {
|
|
|
10374
11783
|
...svcDef.moduleConfig,
|
|
10375
11784
|
log_path: svc.logPath || svcDef.logPaths[0]
|
|
10376
11785
|
});
|
|
10377
|
-
(0,
|
|
11786
|
+
(0, import_node_fs10.writeFileSync)(`${confDDir}/${modName}.conf`, modConfig);
|
|
10378
11787
|
}
|
|
10379
11788
|
spinner2.succeed("Configuration written successfully");
|
|
10380
11789
|
console.log();
|
|
@@ -10392,8 +11801,8 @@ async function initCommand() {
|
|
|
10392
11801
|
}
|
|
10393
11802
|
function checkWriteAccess(dir) {
|
|
10394
11803
|
try {
|
|
10395
|
-
if (!(0,
|
|
10396
|
-
(0,
|
|
11804
|
+
if (!(0, import_node_fs10.existsSync)(dir)) {
|
|
11805
|
+
(0, import_node_fs10.mkdirSync)(dir, { recursive: true });
|
|
10397
11806
|
}
|
|
10398
11807
|
return true;
|
|
10399
11808
|
} catch {
|
|
@@ -10402,8 +11811,8 @@ function checkWriteAccess(dir) {
|
|
|
10402
11811
|
}
|
|
10403
11812
|
|
|
10404
11813
|
// src/core/module-loader.ts
|
|
10405
|
-
var
|
|
10406
|
-
var
|
|
11814
|
+
var import_node_fs11 = require("fs");
|
|
11815
|
+
var import_node_path8 = require("path");
|
|
10407
11816
|
var import_toml2 = __toESM(require_toml());
|
|
10408
11817
|
init_paths();
|
|
10409
11818
|
function discoverModules(moduleDir, confDir) {
|
|
@@ -10411,22 +11820,22 @@ function discoverModules(moduleDir, confDir) {
|
|
|
10411
11820
|
const configs = loadModuleConfigs(confDir || PATHS.confD);
|
|
10412
11821
|
const searchPaths = [
|
|
10413
11822
|
moduleDir || PATHS.moduleDir,
|
|
10414
|
-
(0,
|
|
11823
|
+
(0, import_node_path8.resolve)(process.cwd(), "modules")
|
|
10415
11824
|
];
|
|
10416
|
-
const builtinDir = (0,
|
|
10417
|
-
if ((0,
|
|
11825
|
+
const builtinDir = (0, import_node_path8.resolve)(__dirname || ".", "..", "modules");
|
|
11826
|
+
if ((0, import_node_fs11.existsSync)(builtinDir)) {
|
|
10418
11827
|
searchPaths.push(builtinDir);
|
|
10419
11828
|
}
|
|
10420
11829
|
for (const basePath of searchPaths) {
|
|
10421
|
-
if (!(0,
|
|
10422
|
-
const entries = (0,
|
|
11830
|
+
if (!(0, import_node_fs11.existsSync)(basePath)) continue;
|
|
11831
|
+
const entries = (0, import_node_fs11.readdirSync)(basePath, { withFileTypes: true });
|
|
10423
11832
|
for (const entry of entries) {
|
|
10424
11833
|
if (!entry.isDirectory()) continue;
|
|
10425
|
-
const modPath = (0,
|
|
10426
|
-
const manifestPath = (0,
|
|
10427
|
-
if (!(0,
|
|
11834
|
+
const modPath = (0, import_node_path8.join)(basePath, entry.name);
|
|
11835
|
+
const manifestPath = (0, import_node_path8.join)(modPath, "mod.toml");
|
|
11836
|
+
if (!(0, import_node_fs11.existsSync)(manifestPath)) continue;
|
|
10428
11837
|
try {
|
|
10429
|
-
const raw = (0,
|
|
11838
|
+
const raw = (0, import_node_fs11.readFileSync)(manifestPath, "utf-8");
|
|
10430
11839
|
const manifest = import_toml2.default.parse(raw);
|
|
10431
11840
|
const config = configs.get(manifest.module.name) || { enabled: true };
|
|
10432
11841
|
modules.push({
|
|
@@ -10545,8 +11954,8 @@ function formatUptime(seconds) {
|
|
|
10545
11954
|
|
|
10546
11955
|
// src/commands/modules.ts
|
|
10547
11956
|
var import_node_child_process2 = require("child_process");
|
|
10548
|
-
var
|
|
10549
|
-
var
|
|
11957
|
+
var import_node_fs12 = require("fs");
|
|
11958
|
+
var import_node_path9 = require("path");
|
|
10550
11959
|
var import_toml3 = __toESM(require_toml());
|
|
10551
11960
|
init_paths();
|
|
10552
11961
|
init_pidfile();
|
|
@@ -10555,13 +11964,32 @@ function modulesDir() {
|
|
|
10555
11964
|
ensureRuntimeDirs();
|
|
10556
11965
|
return PATHS.moduleDir;
|
|
10557
11966
|
}
|
|
11967
|
+
function safeModuleDirName(name, label = "module name") {
|
|
11968
|
+
if (!/^[A-Za-z0-9._@-]+$/.test(name) || name === "." || name === "..") {
|
|
11969
|
+
throw new Error(`Unsafe ${label}: ${name}`);
|
|
11970
|
+
}
|
|
11971
|
+
return name;
|
|
11972
|
+
}
|
|
11973
|
+
function moduleDestination(dir, name, label) {
|
|
11974
|
+
return (0, import_node_path9.join)(dir, safeModuleDirName(name, label));
|
|
11975
|
+
}
|
|
11976
|
+
function assertSafeTarballEntries(tarPath) {
|
|
11977
|
+
const listing = (0, import_node_child_process2.execFileSync)("tar", ["-tzf", tarPath], { encoding: "utf-8" });
|
|
11978
|
+
for (const entry of listing.split("\n").map((line) => line.trim()).filter(Boolean)) {
|
|
11979
|
+
const normalized = entry.replace(/\\/g, "/");
|
|
11980
|
+
const segments = normalized.split("/");
|
|
11981
|
+
if (normalized.startsWith("/") || segments.includes("..")) {
|
|
11982
|
+
throw new Error(`Unsafe tarball entry: ${entry}`);
|
|
11983
|
+
}
|
|
11984
|
+
}
|
|
11985
|
+
}
|
|
10558
11986
|
function validateManifest(modPath) {
|
|
10559
|
-
const manifestPath = (0,
|
|
10560
|
-
if (!(0,
|
|
11987
|
+
const manifestPath = (0, import_node_path9.join)(modPath, "mod.toml");
|
|
11988
|
+
if (!(0, import_node_fs12.existsSync)(manifestPath)) {
|
|
10561
11989
|
return { ok: false, error: `mod.toml not found at ${manifestPath}` };
|
|
10562
11990
|
}
|
|
10563
11991
|
try {
|
|
10564
|
-
const raw = (0,
|
|
11992
|
+
const raw = (0, import_node_fs12.readFileSync)(manifestPath, "utf-8");
|
|
10565
11993
|
const parsed = import_toml3.default.parse(raw);
|
|
10566
11994
|
const name = parsed.module?.name;
|
|
10567
11995
|
const version = parsed.module?.version;
|
|
@@ -10615,8 +12043,8 @@ async function modulesInstallCommand(source) {
|
|
|
10615
12043
|
console.log();
|
|
10616
12044
|
const dir = modulesDir();
|
|
10617
12045
|
if (source.startsWith("./") || source.startsWith("/") || source.startsWith("~")) {
|
|
10618
|
-
const absPath = (0,
|
|
10619
|
-
if (!(0,
|
|
12046
|
+
const absPath = (0, import_node_path9.resolve)(source.startsWith("~") ? source.replace("~", process.env.HOME || "") : source);
|
|
12047
|
+
if (!(0, import_node_fs12.existsSync)(absPath)) {
|
|
10620
12048
|
console.log(source_default.red(` \u2717 Path not found: ${absPath}
|
|
10621
12049
|
`));
|
|
10622
12050
|
return;
|
|
@@ -10627,8 +12055,15 @@ async function modulesInstallCommand(source) {
|
|
|
10627
12055
|
`));
|
|
10628
12056
|
return;
|
|
10629
12057
|
}
|
|
10630
|
-
|
|
10631
|
-
|
|
12058
|
+
let dest2;
|
|
12059
|
+
try {
|
|
12060
|
+
dest2 = moduleDestination(dir, check.name);
|
|
12061
|
+
} catch (err) {
|
|
12062
|
+
console.log(source_default.red(` x ${err.message}
|
|
12063
|
+
`));
|
|
12064
|
+
return;
|
|
12065
|
+
}
|
|
12066
|
+
if ((0, import_node_fs12.existsSync)(dest2)) {
|
|
10632
12067
|
console.log(source_default.yellow(` ! ${check.name} is already installed at ${dest2}`));
|
|
10633
12068
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${check.name}`)} first.
|
|
10634
12069
|
`));
|
|
@@ -10636,7 +12071,7 @@ async function modulesInstallCommand(source) {
|
|
|
10636
12071
|
}
|
|
10637
12072
|
const spinner2 = ora({ text: `Copying module files...`, color: "green" }).start();
|
|
10638
12073
|
try {
|
|
10639
|
-
(0,
|
|
12074
|
+
(0, import_node_fs12.cpSync)(absPath, dest2, { recursive: true, dereference: true });
|
|
10640
12075
|
spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
|
|
10641
12076
|
} catch (err) {
|
|
10642
12077
|
spinner2.fail(`Copy failed: ${err.message}`);
|
|
@@ -10648,9 +12083,17 @@ async function modulesInstallCommand(source) {
|
|
|
10648
12083
|
}
|
|
10649
12084
|
if (source.startsWith("github:") || source.startsWith("https://") || source.startsWith("git@") || source.endsWith(".git")) {
|
|
10650
12085
|
const gitUrl = source.startsWith("github:") ? `https://github.com/${source.slice("github:".length)}.git` : source;
|
|
10651
|
-
|
|
10652
|
-
|
|
10653
|
-
|
|
12086
|
+
let name;
|
|
12087
|
+
let dest2;
|
|
12088
|
+
try {
|
|
12089
|
+
name = safeModuleDirName((0, import_node_path9.basename)(gitUrl.replace(/\.git$/, "")), "repository name");
|
|
12090
|
+
dest2 = moduleDestination(dir, name);
|
|
12091
|
+
} catch (err) {
|
|
12092
|
+
console.log(source_default.red(` x ${err.message}
|
|
12093
|
+
`));
|
|
12094
|
+
return;
|
|
12095
|
+
}
|
|
12096
|
+
if ((0, import_node_fs12.existsSync)(dest2)) {
|
|
10654
12097
|
console.log(source_default.yellow(` ! ${name} is already installed at ${dest2}`));
|
|
10655
12098
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${name}`)} first.
|
|
10656
12099
|
`));
|
|
@@ -10658,7 +12101,7 @@ async function modulesInstallCommand(source) {
|
|
|
10658
12101
|
}
|
|
10659
12102
|
const spinner2 = ora({ text: `Cloning ${gitUrl}...`, color: "green" }).start();
|
|
10660
12103
|
try {
|
|
10661
|
-
(0, import_node_child_process2.
|
|
12104
|
+
(0, import_node_child_process2.execFileSync)("git", ["clone", "--depth", "1", "--", gitUrl, dest2], { stdio: "pipe" });
|
|
10662
12105
|
} catch (err) {
|
|
10663
12106
|
spinner2.fail(`Clone failed: ${err.message}`);
|
|
10664
12107
|
return;
|
|
@@ -10667,7 +12110,7 @@ async function modulesInstallCommand(source) {
|
|
|
10667
12110
|
if (!check.ok) {
|
|
10668
12111
|
spinner2.fail(check.error);
|
|
10669
12112
|
try {
|
|
10670
|
-
(0,
|
|
12113
|
+
(0, import_node_fs12.rmSync)(dest2, { recursive: true, force: true });
|
|
10671
12114
|
} catch {
|
|
10672
12115
|
}
|
|
10673
12116
|
return;
|
|
@@ -10704,8 +12147,15 @@ async function modulesInstallCommand(source) {
|
|
|
10704
12147
|
}
|
|
10705
12148
|
spinner.succeed(`Found ${mod.name} v${mod.version}`);
|
|
10706
12149
|
const install = mod.install;
|
|
10707
|
-
|
|
10708
|
-
|
|
12150
|
+
let dest;
|
|
12151
|
+
try {
|
|
12152
|
+
dest = moduleDestination(dir, mod.slug, "module slug");
|
|
12153
|
+
} catch (err) {
|
|
12154
|
+
console.log(source_default.red(` x ${err.message}
|
|
12155
|
+
`));
|
|
12156
|
+
return;
|
|
12157
|
+
}
|
|
12158
|
+
if ((0, import_node_fs12.existsSync)(dest)) {
|
|
10709
12159
|
console.log(source_default.yellow(` ! ${mod.slug} is already installed at ${dest}
|
|
10710
12160
|
`));
|
|
10711
12161
|
return;
|
|
@@ -10713,7 +12163,7 @@ async function modulesInstallCommand(source) {
|
|
|
10713
12163
|
if (install.npm_package) {
|
|
10714
12164
|
const npmSpinner = ora({ text: `Installing ${install.npm_package}...`, color: "green" }).start();
|
|
10715
12165
|
try {
|
|
10716
|
-
(0, import_node_child_process2.
|
|
12166
|
+
(0, import_node_child_process2.execFileSync)("npm", ["install", "-g", "--", install.npm_package], { stdio: "pipe" });
|
|
10717
12167
|
npmSpinner.succeed(`Package ${install.npm_package} installed globally`);
|
|
10718
12168
|
} catch (err) {
|
|
10719
12169
|
npmSpinner.fail(`npm install failed: ${err.message}`);
|
|
@@ -10722,7 +12172,7 @@ async function modulesInstallCommand(source) {
|
|
|
10722
12172
|
} else if (install.git_url) {
|
|
10723
12173
|
const cloneSpinner = ora({ text: "Cloning module repository...", color: "green" }).start();
|
|
10724
12174
|
try {
|
|
10725
|
-
(0, import_node_child_process2.
|
|
12175
|
+
(0, import_node_child_process2.execFileSync)("git", ["clone", "--depth", "1", "--", install.git_url, dest], { stdio: "pipe" });
|
|
10726
12176
|
} catch (err) {
|
|
10727
12177
|
cloneSpinner.fail(`Clone failed: ${err.message}`);
|
|
10728
12178
|
return;
|
|
@@ -10731,7 +12181,7 @@ async function modulesInstallCommand(source) {
|
|
|
10731
12181
|
if (!check.ok) {
|
|
10732
12182
|
cloneSpinner.fail(check.error);
|
|
10733
12183
|
try {
|
|
10734
|
-
(0,
|
|
12184
|
+
(0, import_node_fs12.rmSync)(dest, { recursive: true, force: true });
|
|
10735
12185
|
} catch {
|
|
10736
12186
|
}
|
|
10737
12187
|
return;
|
|
@@ -10745,10 +12195,11 @@ async function modulesInstallCommand(source) {
|
|
|
10745
12195
|
dlSpinner.fail(`HTTP ${res.status}`);
|
|
10746
12196
|
return;
|
|
10747
12197
|
}
|
|
10748
|
-
const tar = (0,
|
|
10749
|
-
(0,
|
|
10750
|
-
(
|
|
10751
|
-
(0,
|
|
12198
|
+
const tar = (0, import_node_path9.join)(dir, `${mod.slug}.tar.gz`);
|
|
12199
|
+
(0, import_node_fs12.writeFileSync)(tar, Buffer.from(await res.arrayBuffer()));
|
|
12200
|
+
assertSafeTarballEntries(tar);
|
|
12201
|
+
(0, import_node_child_process2.execFileSync)("tar", ["-xzf", tar, "-C", dir], { stdio: "pipe" });
|
|
12202
|
+
(0, import_node_fs12.rmSync)(tar, { force: true });
|
|
10752
12203
|
const check = validateManifest(dest);
|
|
10753
12204
|
if (!check.ok) {
|
|
10754
12205
|
dlSpinner.fail(check.error);
|
|
@@ -10771,8 +12222,15 @@ async function modulesRemoveCommand(name) {
|
|
|
10771
12222
|
console.log(source_default.green.bold(" Module Removal"));
|
|
10772
12223
|
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
10773
12224
|
const dir = modulesDir();
|
|
10774
|
-
|
|
10775
|
-
|
|
12225
|
+
let target;
|
|
12226
|
+
try {
|
|
12227
|
+
target = moduleDestination(dir, name);
|
|
12228
|
+
} catch (err) {
|
|
12229
|
+
console.log(source_default.red(` x ${err.message}
|
|
12230
|
+
`));
|
|
12231
|
+
return;
|
|
12232
|
+
}
|
|
12233
|
+
if (!(0, import_node_fs12.existsSync)(target)) {
|
|
10776
12234
|
console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
|
|
10777
12235
|
`));
|
|
10778
12236
|
return;
|
|
@@ -10782,7 +12240,7 @@ async function modulesRemoveCommand(name) {
|
|
|
10782
12240
|
console.log(source_default.yellow(` ! Directory name "${name}" does not match manifest name "${check.name}"`));
|
|
10783
12241
|
}
|
|
10784
12242
|
try {
|
|
10785
|
-
(0,
|
|
12243
|
+
(0, import_node_fs12.rmSync)(target, { recursive: true, force: true });
|
|
10786
12244
|
console.log(source_default.green(` \u2713 Removed ${name} from ${dir}
|
|
10787
12245
|
`));
|
|
10788
12246
|
} catch (err) {
|
|
@@ -11155,21 +12613,21 @@ async function pentestCommand(targetUrl) {
|
|
|
11155
12613
|
|
|
11156
12614
|
// src/commands/orgs.ts
|
|
11157
12615
|
var import_node_os5 = require("os");
|
|
11158
|
-
var
|
|
11159
|
-
var
|
|
12616
|
+
var import_node_fs13 = require("fs");
|
|
12617
|
+
var import_node_path10 = require("path");
|
|
11160
12618
|
var API_URL3 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
11161
|
-
var CONFIG_PATH = (0,
|
|
12619
|
+
var CONFIG_PATH = (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".threatcrush", "config.json");
|
|
11162
12620
|
function readConfig() {
|
|
11163
12621
|
try {
|
|
11164
|
-
return JSON.parse((0,
|
|
12622
|
+
return JSON.parse((0, import_node_fs13.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
11165
12623
|
} catch {
|
|
11166
12624
|
return {};
|
|
11167
12625
|
}
|
|
11168
12626
|
}
|
|
11169
12627
|
function writeConfig(config) {
|
|
11170
|
-
const dir = (0,
|
|
11171
|
-
if (!(0,
|
|
11172
|
-
(0,
|
|
12628
|
+
const dir = (0, import_node_path10.join)((0, import_node_os5.homedir)(), ".threatcrush");
|
|
12629
|
+
if (!(0, import_node_fs13.existsSync)(dir)) (0, import_node_fs13.mkdirSync)(dir, { recursive: true });
|
|
12630
|
+
(0, import_node_fs13.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
11173
12631
|
}
|
|
11174
12632
|
function getAuthHeaders() {
|
|
11175
12633
|
const config = readConfig();
|
|
@@ -11305,13 +12763,13 @@ async function useOrganization(slug) {
|
|
|
11305
12763
|
|
|
11306
12764
|
// src/commands/servers.ts
|
|
11307
12765
|
var import_node_os6 = require("os");
|
|
11308
|
-
var
|
|
11309
|
-
var
|
|
12766
|
+
var import_node_fs14 = require("fs");
|
|
12767
|
+
var import_node_path11 = require("path");
|
|
11310
12768
|
var API_URL4 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
11311
|
-
var CONFIG_PATH2 = (0,
|
|
12769
|
+
var CONFIG_PATH2 = (0, import_node_path11.join)((0, import_node_os6.homedir)(), ".threatcrush", "config.json");
|
|
11312
12770
|
function readConfig2() {
|
|
11313
12771
|
try {
|
|
11314
|
-
return JSON.parse((0,
|
|
12772
|
+
return JSON.parse((0, import_node_fs14.readFileSync)(CONFIG_PATH2, "utf-8"));
|
|
11315
12773
|
} catch {
|
|
11316
12774
|
return {};
|
|
11317
12775
|
}
|
|
@@ -11414,14 +12872,14 @@ function timeAgo(dateStr) {
|
|
|
11414
12872
|
|
|
11415
12873
|
// src/commands/connect.ts
|
|
11416
12874
|
var import_node_os7 = require("os");
|
|
11417
|
-
var
|
|
11418
|
-
var
|
|
12875
|
+
var import_node_fs15 = require("fs");
|
|
12876
|
+
var import_node_path12 = require("path");
|
|
11419
12877
|
var import_node_child_process3 = require("child_process");
|
|
11420
12878
|
var API_URL5 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
11421
|
-
var CONFIG_PATH3 = (0,
|
|
12879
|
+
var CONFIG_PATH3 = (0, import_node_path12.join)((0, import_node_os7.homedir)(), ".threatcrush", "config.json");
|
|
11422
12880
|
function readConfig3() {
|
|
11423
12881
|
try {
|
|
11424
|
-
return JSON.parse((0,
|
|
12882
|
+
return JSON.parse((0, import_node_fs15.readFileSync)(CONFIG_PATH3, "utf-8"));
|
|
11425
12883
|
} catch {
|
|
11426
12884
|
return {};
|
|
11427
12885
|
}
|
|
@@ -11576,19 +13034,19 @@ async function sshConnect(options) {
|
|
|
11576
13034
|
|
|
11577
13035
|
// src/commands/daemon.ts
|
|
11578
13036
|
var import_node_child_process7 = require("child_process");
|
|
11579
|
-
var
|
|
11580
|
-
var
|
|
11581
|
-
var
|
|
13037
|
+
var import_node_fs24 = require("fs");
|
|
13038
|
+
var import_node_path16 = require("path");
|
|
13039
|
+
var import_node_fs25 = require("fs");
|
|
11582
13040
|
|
|
11583
13041
|
// src/daemon/index.ts
|
|
11584
|
-
var
|
|
11585
|
-
var
|
|
13042
|
+
var import_node_fs23 = require("fs");
|
|
13043
|
+
var import_node_path15 = require("path");
|
|
11586
13044
|
init_paths();
|
|
11587
13045
|
init_pidfile();
|
|
11588
13046
|
|
|
11589
13047
|
// src/daemon/ipc-server.ts
|
|
11590
13048
|
var import_node_net2 = require("net");
|
|
11591
|
-
var
|
|
13049
|
+
var import_node_fs16 = require("fs");
|
|
11592
13050
|
init_paths();
|
|
11593
13051
|
|
|
11594
13052
|
// src/daemon/event-bus.ts
|
|
@@ -11635,13 +13093,13 @@ var IpcServer = class {
|
|
|
11635
13093
|
startedAt = /* @__PURE__ */ new Date();
|
|
11636
13094
|
counters = { events: 0, threats: 0, alerts: 0 };
|
|
11637
13095
|
async start() {
|
|
11638
|
-
if ((0,
|
|
13096
|
+
if ((0, import_node_fs16.existsSync)(PATHS.socket)) {
|
|
11639
13097
|
try {
|
|
11640
|
-
(0,
|
|
13098
|
+
(0, import_node_fs16.unlinkSync)(PATHS.socket);
|
|
11641
13099
|
} catch {
|
|
11642
13100
|
}
|
|
11643
13101
|
}
|
|
11644
|
-
return new Promise((
|
|
13102
|
+
return new Promise((resolve5, reject) => {
|
|
11645
13103
|
this.server = (0, import_node_net2.createServer)((sock) => this.handleClient(sock));
|
|
11646
13104
|
this.server.on("error", reject);
|
|
11647
13105
|
this.server.listen(PATHS.socket, () => {
|
|
@@ -11658,7 +13116,7 @@ var IpcServer = class {
|
|
|
11658
13116
|
} catch {
|
|
11659
13117
|
}
|
|
11660
13118
|
}
|
|
11661
|
-
|
|
13119
|
+
resolve5();
|
|
11662
13120
|
});
|
|
11663
13121
|
});
|
|
11664
13122
|
}
|
|
@@ -11670,20 +13128,20 @@ var IpcServer = class {
|
|
|
11670
13128
|
}
|
|
11671
13129
|
}
|
|
11672
13130
|
this.clients.clear();
|
|
11673
|
-
return new Promise((
|
|
13131
|
+
return new Promise((resolve5) => {
|
|
11674
13132
|
if (!this.server) {
|
|
11675
13133
|
try {
|
|
11676
|
-
if ((0,
|
|
13134
|
+
if ((0, import_node_fs16.existsSync)(PATHS.socket)) (0, import_node_fs16.unlinkSync)(PATHS.socket);
|
|
11677
13135
|
} catch {
|
|
11678
13136
|
}
|
|
11679
|
-
return
|
|
13137
|
+
return resolve5();
|
|
11680
13138
|
}
|
|
11681
13139
|
this.server.close(() => {
|
|
11682
13140
|
try {
|
|
11683
|
-
if ((0,
|
|
13141
|
+
if ((0, import_node_fs16.existsSync)(PATHS.socket)) (0, import_node_fs16.unlinkSync)(PATHS.socket);
|
|
11684
13142
|
} catch {
|
|
11685
13143
|
}
|
|
11686
|
-
|
|
13144
|
+
resolve5();
|
|
11687
13145
|
});
|
|
11688
13146
|
});
|
|
11689
13147
|
}
|
|
@@ -11786,14 +13244,14 @@ var IpcServer = class {
|
|
|
11786
13244
|
};
|
|
11787
13245
|
|
|
11788
13246
|
// src/daemon/module-host.ts
|
|
11789
|
-
var
|
|
11790
|
-
var
|
|
13247
|
+
var import_node_fs20 = require("fs");
|
|
13248
|
+
var import_node_path13 = require("path");
|
|
11791
13249
|
var import_node_url = require("url");
|
|
11792
13250
|
var import_toml4 = __toESM(require_toml());
|
|
11793
13251
|
init_paths();
|
|
11794
13252
|
|
|
11795
13253
|
// src/daemon/watchers/log-watcher.ts
|
|
11796
|
-
var
|
|
13254
|
+
var import_node_fs17 = require("fs");
|
|
11797
13255
|
var import_node_readline4 = require("readline");
|
|
11798
13256
|
init_state();
|
|
11799
13257
|
var DEFAULT_SOURCES = [
|
|
@@ -11815,9 +13273,9 @@ var LogWatcher = class {
|
|
|
11815
13273
|
start() {
|
|
11816
13274
|
const started = [];
|
|
11817
13275
|
for (const src of this.sources) {
|
|
11818
|
-
if (!(0,
|
|
13276
|
+
if (!(0, import_node_fs17.existsSync)(src.path)) continue;
|
|
11819
13277
|
try {
|
|
11820
|
-
(0,
|
|
13278
|
+
(0, import_node_fs17.accessSync)(src.path, import_node_fs17.constants.R_OK);
|
|
11821
13279
|
} catch {
|
|
11822
13280
|
continue;
|
|
11823
13281
|
}
|
|
@@ -11837,7 +13295,7 @@ var LogWatcher = class {
|
|
|
11837
13295
|
}
|
|
11838
13296
|
tail(src) {
|
|
11839
13297
|
try {
|
|
11840
|
-
this.positions.set(src.path, (0,
|
|
13298
|
+
this.positions.set(src.path, (0, import_node_fs17.statSync)(src.path).size);
|
|
11841
13299
|
} catch {
|
|
11842
13300
|
this.positions.set(src.path, 0);
|
|
11843
13301
|
}
|
|
@@ -11848,7 +13306,7 @@ var LogWatcher = class {
|
|
|
11848
13306
|
poll(src) {
|
|
11849
13307
|
let stat;
|
|
11850
13308
|
try {
|
|
11851
|
-
stat = (0,
|
|
13309
|
+
stat = (0, import_node_fs17.statSync)(src.path);
|
|
11852
13310
|
} catch {
|
|
11853
13311
|
return;
|
|
11854
13312
|
}
|
|
@@ -11858,7 +13316,7 @@ var LogWatcher = class {
|
|
|
11858
13316
|
return;
|
|
11859
13317
|
}
|
|
11860
13318
|
if (stat.size === prev) return;
|
|
11861
|
-
const stream = (0,
|
|
13319
|
+
const stream = (0, import_node_fs17.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
|
|
11862
13320
|
stream.on("error", () => this.positions.set(src.path, stat.size));
|
|
11863
13321
|
const rl = (0, import_node_readline4.createInterface)({ input: stream });
|
|
11864
13322
|
rl.on("error", () => {
|
|
@@ -12051,7 +13509,7 @@ function realtimeToDate(rt) {
|
|
|
12051
13509
|
|
|
12052
13510
|
// src/modules/network-monitor/index.ts
|
|
12053
13511
|
var import_node_child_process5 = require("child_process");
|
|
12054
|
-
var
|
|
13512
|
+
var import_node_fs18 = require("fs");
|
|
12055
13513
|
init_state();
|
|
12056
13514
|
var NetworkMonitor = class {
|
|
12057
13515
|
constructor(bus2) {
|
|
@@ -12090,7 +13548,7 @@ var NetworkMonitor = class {
|
|
|
12090
13548
|
hasConntrackOrSs() {
|
|
12091
13549
|
const ss = (0, import_node_child_process5.spawnSync)("ss", ["--version"], { stdio: "pipe" });
|
|
12092
13550
|
if (ss.status === 0) return true;
|
|
12093
|
-
return (0,
|
|
13551
|
+
return (0, import_node_fs18.existsSync)("/proc/net/tcp");
|
|
12094
13552
|
}
|
|
12095
13553
|
poll() {
|
|
12096
13554
|
try {
|
|
@@ -12229,7 +13687,7 @@ var NetworkMonitor = class {
|
|
|
12229
13687
|
};
|
|
12230
13688
|
|
|
12231
13689
|
// src/modules/dns-monitor/index.ts
|
|
12232
|
-
var
|
|
13690
|
+
var import_node_fs19 = require("fs");
|
|
12233
13691
|
var import_node_readline5 = require("readline");
|
|
12234
13692
|
init_state();
|
|
12235
13693
|
var DNS_LOG_SOURCES = [
|
|
@@ -12264,9 +13722,9 @@ var DnsMonitor = class {
|
|
|
12264
13722
|
entropyThreshold = 3.5;
|
|
12265
13723
|
start() {
|
|
12266
13724
|
const sources = DNS_LOG_SOURCES.filter((p) => {
|
|
12267
|
-
if (!(0,
|
|
13725
|
+
if (!(0, import_node_fs19.existsSync)(p)) return false;
|
|
12268
13726
|
try {
|
|
12269
|
-
(0,
|
|
13727
|
+
(0, import_node_fs19.accessSync)(p, import_node_fs19.constants.R_OK);
|
|
12270
13728
|
return true;
|
|
12271
13729
|
} catch {
|
|
12272
13730
|
return false;
|
|
@@ -12290,7 +13748,7 @@ var DnsMonitor = class {
|
|
|
12290
13748
|
}
|
|
12291
13749
|
tailLog(path) {
|
|
12292
13750
|
try {
|
|
12293
|
-
this.positions.set(path, (0,
|
|
13751
|
+
this.positions.set(path, (0, import_node_fs19.statSync)(path).size);
|
|
12294
13752
|
} catch {
|
|
12295
13753
|
this.positions.set(path, 0);
|
|
12296
13754
|
}
|
|
@@ -12300,7 +13758,7 @@ var DnsMonitor = class {
|
|
|
12300
13758
|
pollLog(path) {
|
|
12301
13759
|
let stat;
|
|
12302
13760
|
try {
|
|
12303
|
-
stat = (0,
|
|
13761
|
+
stat = (0, import_node_fs19.statSync)(path);
|
|
12304
13762
|
} catch {
|
|
12305
13763
|
return;
|
|
12306
13764
|
}
|
|
@@ -12310,7 +13768,7 @@ var DnsMonitor = class {
|
|
|
12310
13768
|
return;
|
|
12311
13769
|
}
|
|
12312
13770
|
if (stat.size === prev) return;
|
|
12313
|
-
const stream = (0,
|
|
13771
|
+
const stream = (0, import_node_fs19.createReadStream)(path, { start: prev, encoding: "utf-8" });
|
|
12314
13772
|
stream.on("error", () => this.positions.set(path, stat.size));
|
|
12315
13773
|
const rl = (0, import_node_readline5.createInterface)({ input: stream });
|
|
12316
13774
|
rl.on("line", (line) => this.parseDnsLine(line));
|
|
@@ -12542,15 +14000,15 @@ var ModuleHost = class {
|
|
|
12542
14000
|
for (const m of builtins) this.modules.set(m.name, m);
|
|
12543
14001
|
}
|
|
12544
14002
|
async discoverAndStartInstalled() {
|
|
12545
|
-
if (!(0,
|
|
14003
|
+
if (!(0, import_node_fs20.existsSync)(PATHS.moduleDir)) return;
|
|
12546
14004
|
const configs = loadModuleConfigs(PATHS.confD);
|
|
12547
|
-
const entries = (0,
|
|
14005
|
+
const entries = (0, import_node_fs20.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
|
|
12548
14006
|
for (const entry of entries) {
|
|
12549
14007
|
if (!entry.isDirectory()) continue;
|
|
12550
|
-
const manifestPath = (0,
|
|
12551
|
-
if (!(0,
|
|
14008
|
+
const manifestPath = (0, import_node_path13.join)(PATHS.moduleDir, entry.name, "mod.toml");
|
|
14009
|
+
if (!(0, import_node_fs20.existsSync)(manifestPath)) continue;
|
|
12552
14010
|
try {
|
|
12553
|
-
const manifest = import_toml4.default.parse((0,
|
|
14011
|
+
const manifest = import_toml4.default.parse((0, import_node_fs20.readFileSync)(manifestPath, "utf-8"));
|
|
12554
14012
|
const name = manifest.module?.name || entry.name;
|
|
12555
14013
|
const defaults = manifest.module?.config?.defaults || {};
|
|
12556
14014
|
const config = {
|
|
@@ -12564,7 +14022,7 @@ var ModuleHost = class {
|
|
|
12564
14022
|
source: "installed",
|
|
12565
14023
|
status: config.enabled === false ? "disabled" : "loaded",
|
|
12566
14024
|
events: 0,
|
|
12567
|
-
path: (0,
|
|
14025
|
+
path: (0, import_node_path13.join)(PATHS.moduleDir, entry.name),
|
|
12568
14026
|
config
|
|
12569
14027
|
};
|
|
12570
14028
|
this.modules.set(name, hosted);
|
|
@@ -12579,7 +14037,7 @@ var ModuleHost = class {
|
|
|
12579
14037
|
status: "error",
|
|
12580
14038
|
events: 0,
|
|
12581
14039
|
detail: `manifest load failed: ${String(err.message || err)}`,
|
|
12582
|
-
path: (0,
|
|
14040
|
+
path: (0, import_node_path13.join)(PATHS.moduleDir, entry.name)
|
|
12583
14041
|
});
|
|
12584
14042
|
}
|
|
12585
14043
|
}
|
|
@@ -12611,17 +14069,17 @@ var ModuleHost = class {
|
|
|
12611
14069
|
}
|
|
12612
14070
|
}
|
|
12613
14071
|
installedEntrypoint(modulePath) {
|
|
12614
|
-
const packageJson = (0,
|
|
14072
|
+
const packageJson = (0, import_node_path13.join)(modulePath, "package.json");
|
|
12615
14073
|
const candidates = [];
|
|
12616
|
-
if ((0,
|
|
14074
|
+
if ((0, import_node_fs20.existsSync)(packageJson)) {
|
|
12617
14075
|
try {
|
|
12618
|
-
const pkg = JSON.parse((0,
|
|
12619
|
-
if (pkg.main) candidates.push((0,
|
|
14076
|
+
const pkg = JSON.parse((0, import_node_fs20.readFileSync)(packageJson, "utf-8"));
|
|
14077
|
+
if (pkg.main) candidates.push((0, import_node_path13.join)(modulePath, pkg.main));
|
|
12620
14078
|
} catch {
|
|
12621
14079
|
}
|
|
12622
14080
|
}
|
|
12623
|
-
candidates.push((0,
|
|
12624
|
-
return candidates.find((candidate) => (0,
|
|
14081
|
+
candidates.push((0, import_node_path13.join)(modulePath, "dist", "index.js"), (0, import_node_path13.join)(modulePath, "index.js"));
|
|
14082
|
+
return candidates.find((candidate) => (0, import_node_fs20.existsSync)(candidate)) || null;
|
|
12625
14083
|
}
|
|
12626
14084
|
isThreatCrushModule(value) {
|
|
12627
14085
|
return Boolean(
|
|
@@ -13142,8 +14600,8 @@ var RuleEngine = class {
|
|
|
13142
14600
|
};
|
|
13143
14601
|
|
|
13144
14602
|
// src/daemon/rules/loader.ts
|
|
13145
|
-
var
|
|
13146
|
-
var
|
|
14603
|
+
var import_node_fs21 = require("fs");
|
|
14604
|
+
var import_node_path14 = require("path");
|
|
13147
14605
|
|
|
13148
14606
|
// src/daemon/rules/default-rules.ts
|
|
13149
14607
|
var DEFAULT_RULES = [
|
|
@@ -13427,11 +14885,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
13427
14885
|
function loadAllRules(customDir) {
|
|
13428
14886
|
const rules = [...DEFAULT_RULES];
|
|
13429
14887
|
const dir = customDir || RULES_DIR;
|
|
13430
|
-
if ((0,
|
|
13431
|
-
const files = (0,
|
|
14888
|
+
if ((0, import_node_fs21.existsSync)(dir)) {
|
|
14889
|
+
const files = (0, import_node_fs21.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
13432
14890
|
for (const file of files) {
|
|
13433
14891
|
try {
|
|
13434
|
-
const raw = (0,
|
|
14892
|
+
const raw = (0, import_node_fs21.readFileSync)((0, import_node_path14.join)(dir, file), "utf-8");
|
|
13435
14893
|
const parsed = JSON.parse(raw);
|
|
13436
14894
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
13437
14895
|
for (const rule of customRules) {
|
|
@@ -13456,6 +14914,12 @@ function loadAllRules(customDir) {
|
|
|
13456
14914
|
|
|
13457
14915
|
// src/daemon/firewall/adapters.ts
|
|
13458
14916
|
var import_node_child_process6 = require("child_process");
|
|
14917
|
+
var import_node_net3 = require("net");
|
|
14918
|
+
function assertValidFirewallIp(ip) {
|
|
14919
|
+
if ((0, import_node_net3.isIP)(ip) !== 4) {
|
|
14920
|
+
throw new Error(`Invalid IPv4 address: ${ip}`);
|
|
14921
|
+
}
|
|
14922
|
+
}
|
|
13459
14923
|
var NftablesAdapter = class {
|
|
13460
14924
|
name = "nftables";
|
|
13461
14925
|
table = "threatcrush";
|
|
@@ -13475,16 +14939,19 @@ var NftablesAdapter = class {
|
|
|
13475
14939
|
}
|
|
13476
14940
|
}
|
|
13477
14941
|
async block(ip) {
|
|
14942
|
+
assertValidFirewallIp(ip);
|
|
13478
14943
|
this.ensureSetup();
|
|
13479
14944
|
(0, import_node_child_process6.execSync)(`nft add element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
13480
14945
|
}
|
|
13481
14946
|
async unblock(ip) {
|
|
14947
|
+
assertValidFirewallIp(ip);
|
|
13482
14948
|
try {
|
|
13483
14949
|
(0, import_node_child_process6.execSync)(`nft delete element inet ${this.table} ${this.set} '{ ${ip} }'`);
|
|
13484
14950
|
} catch {
|
|
13485
14951
|
}
|
|
13486
14952
|
}
|
|
13487
14953
|
async isBlocked(ip) {
|
|
14954
|
+
assertValidFirewallIp(ip);
|
|
13488
14955
|
try {
|
|
13489
14956
|
const output = (0, import_node_child_process6.execSync)(`nft list set inet ${this.table} ${this.set}`, { encoding: "utf-8" });
|
|
13490
14957
|
return output.includes(ip);
|
|
@@ -13519,17 +14986,20 @@ var IptablesAdapter = class {
|
|
|
13519
14986
|
}
|
|
13520
14987
|
}
|
|
13521
14988
|
async block(ip) {
|
|
14989
|
+
assertValidFirewallIp(ip);
|
|
13522
14990
|
this.ensureChain();
|
|
13523
14991
|
if (await this.isBlocked(ip)) return;
|
|
13524
14992
|
(0, import_node_child_process6.execSync)(`iptables -A ${this.chain} -s ${ip} -j DROP`);
|
|
13525
14993
|
}
|
|
13526
14994
|
async unblock(ip) {
|
|
14995
|
+
assertValidFirewallIp(ip);
|
|
13527
14996
|
try {
|
|
13528
14997
|
(0, import_node_child_process6.execSync)(`iptables -D ${this.chain} -s ${ip} -j DROP`);
|
|
13529
14998
|
} catch {
|
|
13530
14999
|
}
|
|
13531
15000
|
}
|
|
13532
15001
|
async isBlocked(ip) {
|
|
15002
|
+
assertValidFirewallIp(ip);
|
|
13533
15003
|
try {
|
|
13534
15004
|
const output = (0, import_node_child_process6.execSync)(`iptables -n -L ${this.chain}`, { encoding: "utf-8" });
|
|
13535
15005
|
return output.includes(ip);
|
|
@@ -13558,12 +15028,15 @@ var DryRunAdapter = class {
|
|
|
13558
15028
|
return true;
|
|
13559
15029
|
}
|
|
13560
15030
|
async block(ip) {
|
|
15031
|
+
assertValidFirewallIp(ip);
|
|
13561
15032
|
this.blocked.add(ip);
|
|
13562
15033
|
}
|
|
13563
15034
|
async unblock(ip) {
|
|
15035
|
+
assertValidFirewallIp(ip);
|
|
13564
15036
|
this.blocked.delete(ip);
|
|
13565
15037
|
}
|
|
13566
15038
|
async isBlocked(ip) {
|
|
15039
|
+
assertValidFirewallIp(ip);
|
|
13567
15040
|
return this.blocked.has(ip);
|
|
13568
15041
|
}
|
|
13569
15042
|
async listBlocked() {
|
|
@@ -13579,7 +15052,7 @@ function detectFirewallAdapter() {
|
|
|
13579
15052
|
}
|
|
13580
15053
|
|
|
13581
15054
|
// src/daemon/firewall/remediation.ts
|
|
13582
|
-
var
|
|
15055
|
+
var import_node_fs22 = require("fs");
|
|
13583
15056
|
init_state();
|
|
13584
15057
|
init_paths();
|
|
13585
15058
|
var DEFAULT_CONFIG2 = {
|
|
@@ -13736,7 +15209,7 @@ var RemediationManager = class {
|
|
|
13736
15209
|
}
|
|
13737
15210
|
logLine(line) {
|
|
13738
15211
|
try {
|
|
13739
|
-
(0,
|
|
15212
|
+
(0, import_node_fs22.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
13740
15213
|
`);
|
|
13741
15214
|
} catch {
|
|
13742
15215
|
}
|
|
@@ -13793,9 +15266,9 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
13793
15266
|
}
|
|
13794
15267
|
|
|
13795
15268
|
// src/daemon/index.ts
|
|
13796
|
-
function
|
|
15269
|
+
function readVersion2() {
|
|
13797
15270
|
try {
|
|
13798
|
-
const pkg = JSON.parse((0,
|
|
15271
|
+
const pkg = JSON.parse((0, import_node_fs23.readFileSync)((0, import_node_path15.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
13799
15272
|
return pkg.version || "0.0.0";
|
|
13800
15273
|
} catch {
|
|
13801
15274
|
return "0.0.0";
|
|
@@ -13803,7 +15276,7 @@ function readVersion() {
|
|
|
13803
15276
|
}
|
|
13804
15277
|
function logLine(line) {
|
|
13805
15278
|
try {
|
|
13806
|
-
(0,
|
|
15279
|
+
(0, import_node_fs23.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
13807
15280
|
`);
|
|
13808
15281
|
} catch {
|
|
13809
15282
|
}
|
|
@@ -13824,14 +15297,14 @@ async function runDaemon() {
|
|
|
13824
15297
|
logLine(`[daemon] unhandledRejection: ${String(reason)}`);
|
|
13825
15298
|
captureException(reason);
|
|
13826
15299
|
});
|
|
13827
|
-
const version =
|
|
15300
|
+
const version = readVersion2();
|
|
13828
15301
|
logLine(`[daemon] starting threatcrushd v${version} mode=${PATHS.mode}`);
|
|
13829
15302
|
try {
|
|
13830
15303
|
initStateDB(PATHS.stateDb);
|
|
13831
15304
|
} catch (err) {
|
|
13832
15305
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
13833
15306
|
}
|
|
13834
|
-
const config = loadConfig((0,
|
|
15307
|
+
const config = loadConfig((0, import_node_fs23.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
13835
15308
|
bus.on("event", (event) => {
|
|
13836
15309
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
13837
15310
|
});
|
|
@@ -13923,7 +15396,7 @@ async function runDaemon() {
|
|
|
13923
15396
|
init_paths();
|
|
13924
15397
|
init_pidfile();
|
|
13925
15398
|
init_ipc_client();
|
|
13926
|
-
var DAEMON_ENTRY = (0,
|
|
15399
|
+
var DAEMON_ENTRY = (0, import_node_path16.join)(__dirname, "daemon.js");
|
|
13927
15400
|
async function daemonForeground() {
|
|
13928
15401
|
await runDaemon();
|
|
13929
15402
|
}
|
|
@@ -13934,7 +15407,7 @@ async function daemonStart() {
|
|
|
13934
15407
|
return;
|
|
13935
15408
|
}
|
|
13936
15409
|
ensureRuntimeDirs();
|
|
13937
|
-
if (!(0,
|
|
15410
|
+
if (!(0, import_node_fs24.existsSync)(DAEMON_ENTRY)) {
|
|
13938
15411
|
console.log(source_default.red(` Daemon entry not found at ${DAEMON_ENTRY}.`));
|
|
13939
15412
|
console.log(source_default.dim(" Reinstall with `threatcrush update` or `pnpm run build` in the cli package."));
|
|
13940
15413
|
return;
|
|
@@ -13942,8 +15415,8 @@ async function daemonStart() {
|
|
|
13942
15415
|
let out;
|
|
13943
15416
|
let err;
|
|
13944
15417
|
try {
|
|
13945
|
-
out = (0,
|
|
13946
|
-
err = (0,
|
|
15418
|
+
out = (0, import_node_fs25.openSync)(PATHS.logFile, "a");
|
|
15419
|
+
err = (0, import_node_fs25.openSync)(PATHS.logFile, "a");
|
|
13947
15420
|
} catch (e) {
|
|
13948
15421
|
const code = e.code;
|
|
13949
15422
|
console.log(source_default.red(` \u2717 Cannot write daemon log at ${PATHS.logFile} (${code ?? "error"}).`));
|
|
@@ -14022,19 +15495,19 @@ async function daemonStop() {
|
|
|
14022
15495
|
|
|
14023
15496
|
// src/commands/service.ts
|
|
14024
15497
|
var import_node_child_process8 = require("child_process");
|
|
14025
|
-
var
|
|
14026
|
-
var
|
|
15498
|
+
var import_node_fs26 = require("fs");
|
|
15499
|
+
var import_node_path17 = require("path");
|
|
14027
15500
|
var UNIT_PATH = "/etc/systemd/system/threatcrushd.service";
|
|
14028
15501
|
function resolveTemplate() {
|
|
14029
|
-
const templatePath = (0,
|
|
14030
|
-
if (!(0,
|
|
15502
|
+
const templatePath = (0, import_node_path17.join)(__dirname, "systemd", "threatcrushd.service");
|
|
15503
|
+
if (!(0, import_node_fs26.existsSync)(templatePath)) {
|
|
14031
15504
|
throw new Error(`systemd unit template not found at ${templatePath}`);
|
|
14032
15505
|
}
|
|
14033
|
-
return (0,
|
|
15506
|
+
return (0, import_node_fs26.readFileSync)(templatePath, "utf-8");
|
|
14034
15507
|
}
|
|
14035
15508
|
function resolveBinPath() {
|
|
14036
15509
|
const arg = process.argv[1];
|
|
14037
|
-
if (arg && (0,
|
|
15510
|
+
if (arg && (0, import_node_fs26.existsSync)(arg)) return arg;
|
|
14038
15511
|
try {
|
|
14039
15512
|
return (0, import_node_child_process8.execSync)("command -v threatcrush", { encoding: "utf-8" }).trim();
|
|
14040
15513
|
} catch {
|
|
@@ -14055,7 +15528,7 @@ async function installServiceCommand() {
|
|
|
14055
15528
|
return;
|
|
14056
15529
|
}
|
|
14057
15530
|
const unit = resolveTemplate().replace("{{BIN_PATH}}", resolveBinPath());
|
|
14058
|
-
(0,
|
|
15531
|
+
(0, import_node_fs26.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
|
|
14059
15532
|
console.log(source_default.green(` \u2713 Installed unit file: ${UNIT_PATH}`));
|
|
14060
15533
|
ensureSystemDirs();
|
|
14061
15534
|
try {
|
|
@@ -14079,17 +15552,17 @@ function ensureSystemDirs() {
|
|
|
14079
15552
|
];
|
|
14080
15553
|
let admGid = null;
|
|
14081
15554
|
try {
|
|
14082
|
-
admGid = (0,
|
|
15555
|
+
admGid = (0, import_node_fs26.statSync)("/var/log/auth.log").gid;
|
|
14083
15556
|
} catch {
|
|
14084
15557
|
}
|
|
14085
15558
|
for (const { path, sticky } of dirs) {
|
|
14086
15559
|
try {
|
|
14087
|
-
(0,
|
|
15560
|
+
(0, import_node_fs26.mkdirSync)(path, { recursive: true });
|
|
14088
15561
|
} catch {
|
|
14089
15562
|
}
|
|
14090
15563
|
if (admGid !== null) {
|
|
14091
15564
|
try {
|
|
14092
|
-
(0,
|
|
15565
|
+
(0, import_node_fs26.chmodSync)(path, sticky ? 1533 : 509);
|
|
14093
15566
|
(0, import_node_child_process8.execSync)(`chgrp adm ${path}`, { stdio: "ignore" });
|
|
14094
15567
|
} catch {
|
|
14095
15568
|
}
|
|
@@ -14116,7 +15589,7 @@ async function uninstallServiceCommand() {
|
|
|
14116
15589
|
} catch {
|
|
14117
15590
|
}
|
|
14118
15591
|
try {
|
|
14119
|
-
if ((0,
|
|
15592
|
+
if ((0, import_node_fs26.existsSync)(UNIT_PATH)) {
|
|
14120
15593
|
(0, import_node_child_process8.execSync)(`rm -f ${UNIT_PATH}`);
|
|
14121
15594
|
console.log(source_default.green(` \u2713 Removed unit file: ${UNIT_PATH}`));
|
|
14122
15595
|
}
|
|
@@ -14215,16 +15688,16 @@ function welcomeCommand() {
|
|
|
14215
15688
|
}
|
|
14216
15689
|
|
|
14217
15690
|
// src/commands/properties.ts
|
|
14218
|
-
var
|
|
14219
|
-
var
|
|
15691
|
+
var import_node_fs27 = require("fs");
|
|
15692
|
+
var import_node_path18 = require("path");
|
|
14220
15693
|
var import_node_readline6 = __toESM(require("readline"));
|
|
14221
15694
|
var API_URL7 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
14222
15695
|
var KINDS = ["url", "api", "domain", "ip", "repo"];
|
|
14223
15696
|
function prompt2(question) {
|
|
14224
15697
|
const rl = import_node_readline6.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
14225
|
-
return new Promise((
|
|
15698
|
+
return new Promise((resolve5) => rl.question(question, (answer) => {
|
|
14226
15699
|
rl.close();
|
|
14227
|
-
|
|
15700
|
+
resolve5(answer.trim());
|
|
14228
15701
|
}));
|
|
14229
15702
|
}
|
|
14230
15703
|
async function requireAuth() {
|
|
@@ -14545,8 +16018,8 @@ async function propertiesRunsCommand(opts) {
|
|
|
14545
16018
|
}
|
|
14546
16019
|
}
|
|
14547
16020
|
function parseImportFile(path) {
|
|
14548
|
-
const ext = (0,
|
|
14549
|
-
const raw = (0,
|
|
16021
|
+
const ext = (0, import_node_path18.extname)(path).toLowerCase();
|
|
16022
|
+
const raw = (0, import_node_fs27.readFileSync)(path, "utf-8");
|
|
14550
16023
|
if (ext === ".json") {
|
|
14551
16024
|
const parsed = JSON.parse(raw);
|
|
14552
16025
|
if (!Array.isArray(parsed)) throw new Error("JSON must be an array of objects");
|
|
@@ -14736,7 +16209,7 @@ async function rulesCommand(opts) {
|
|
|
14736
16209
|
}
|
|
14737
16210
|
|
|
14738
16211
|
// src/commands/harden.ts
|
|
14739
|
-
var
|
|
16212
|
+
var import_node_fs28 = require("fs");
|
|
14740
16213
|
var import_node_child_process9 = require("child_process");
|
|
14741
16214
|
function tryExec(cmd) {
|
|
14742
16215
|
try {
|
|
@@ -14747,7 +16220,7 @@ function tryExec(cmd) {
|
|
|
14747
16220
|
}
|
|
14748
16221
|
function tryRead(path) {
|
|
14749
16222
|
try {
|
|
14750
|
-
return (0,
|
|
16223
|
+
return (0, import_node_fs28.readFileSync)(path, "utf-8");
|
|
14751
16224
|
} catch {
|
|
14752
16225
|
return null;
|
|
14753
16226
|
}
|
|
@@ -14851,8 +16324,8 @@ function checkSshWeakConfig() {
|
|
|
14851
16324
|
};
|
|
14852
16325
|
}
|
|
14853
16326
|
function checkAutoUpdates() {
|
|
14854
|
-
const unattended = (0,
|
|
14855
|
-
const dnfAuto = (0,
|
|
16327
|
+
const unattended = (0, import_node_fs28.existsSync)("/etc/apt/apt.conf.d/20auto-upgrades") || (0, import_node_fs28.existsSync)("/etc/apt/apt.conf.d/50unattended-upgrades");
|
|
16328
|
+
const dnfAuto = (0, import_node_fs28.existsSync)("/etc/dnf/automatic.conf");
|
|
14856
16329
|
if (unattended || dnfAuto) {
|
|
14857
16330
|
return {
|
|
14858
16331
|
key: "auto-updates",
|
|
@@ -14978,7 +16451,7 @@ function checkFail2ban() {
|
|
|
14978
16451
|
explanation: "fail2ban is installed and running."
|
|
14979
16452
|
};
|
|
14980
16453
|
}
|
|
14981
|
-
if ((0,
|
|
16454
|
+
if ((0, import_node_fs28.existsSync)("/etc/fail2ban/fail2ban.conf")) {
|
|
14982
16455
|
return {
|
|
14983
16456
|
key: checkKey,
|
|
14984
16457
|
status: "warn",
|
|
@@ -15207,10 +16680,10 @@ async function allowlistCommand(opts) {
|
|
|
15207
16680
|
|
|
15208
16681
|
// src/index.ts
|
|
15209
16682
|
init_paths();
|
|
15210
|
-
var
|
|
16683
|
+
var PKG_VERSION2 = "0.1.8";
|
|
15211
16684
|
try {
|
|
15212
|
-
const pkg = JSON.parse((0,
|
|
15213
|
-
|
|
16685
|
+
const pkg = JSON.parse((0, import_node_fs29.readFileSync)((0, import_node_path19.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
16686
|
+
PKG_VERSION2 = pkg.version;
|
|
15214
16687
|
} catch {
|
|
15215
16688
|
}
|
|
15216
16689
|
var LOGO2 = `
|
|
@@ -15225,7 +16698,7 @@ ${source_default.dim(" C R U S H")}
|
|
|
15225
16698
|
var API_URL8 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
15226
16699
|
var PKG_NAME = "@profullstack/threatcrush";
|
|
15227
16700
|
var DESKTOP_PKG_NAME = "@profullstack/threatcrush-desktop";
|
|
15228
|
-
var INSTALL_CONFIG_PATH = (0,
|
|
16701
|
+
var INSTALL_CONFIG_PATH = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
|
|
15229
16702
|
function detectPackageManager() {
|
|
15230
16703
|
try {
|
|
15231
16704
|
const npmGlobal = (0, import_node_child_process10.execSync)("npm ls -g --depth=0 --json 2>/dev/null", { encoding: "utf-8" });
|
|
@@ -15251,7 +16724,7 @@ function detectPackageManager() {
|
|
|
15251
16724
|
}
|
|
15252
16725
|
function readInstallConfig() {
|
|
15253
16726
|
try {
|
|
15254
|
-
return JSON.parse((0,
|
|
16727
|
+
return JSON.parse((0, import_node_fs29.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
|
|
15255
16728
|
} catch {
|
|
15256
16729
|
return {};
|
|
15257
16730
|
}
|
|
@@ -15318,7 +16791,7 @@ ${source_default.dim("Examples:")}
|
|
|
15318
16791
|
${source_default.green("$")} threatcrush modules install ${source_default.dim("# Install a module")}
|
|
15319
16792
|
${source_default.green("$")} threatcrush update ${source_default.dim("# Update to latest")}
|
|
15320
16793
|
${source_default.green("$")} threatcrush remove ${source_default.dim("# Uninstall completely")}`
|
|
15321
|
-
).version(
|
|
16794
|
+
).version(PKG_VERSION2, "-v, --version", "Show version number").helpOption("-h, --help", "Show this help").addHelpText("after", `
|
|
15322
16795
|
${source_default.dim("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500")}
|
|
15323
16796
|
${source_default.dim("Modules:")}
|
|
15324
16797
|
ThreatCrush uses pluggable security modules. Core modules included:
|
|
@@ -15354,8 +16827,33 @@ program2.command("logout").description("Clear stored threatcrush.com credentials
|
|
|
15354
16827
|
program2.command("whoami").description("Show the currently logged-in threatcrush.com account").action(async () => {
|
|
15355
16828
|
await whoamiCommand();
|
|
15356
16829
|
});
|
|
15357
|
-
program2.command("scan").description("Scan codebase for vulnerabilities and secrets").argument("[path]", "Path to scan", ".").
|
|
15358
|
-
|
|
16830
|
+
program2.command("scan").description("Scan codebase for vulnerabilities and secrets").argument("[path]", "Path to scan", ".").option("-f, --format <format>", "output format: text, json, or sarif", "text").option("-o, --output <file>", "write json/sarif output to a file instead of stdout").option(
|
|
16831
|
+
"--fail-on <severities>",
|
|
16832
|
+
"exit 1 when a finding at or above any of these exists (comma-separated: critical,high,medium,low,info)"
|
|
16833
|
+
).option(
|
|
16834
|
+
"--path-prefix <prefix>",
|
|
16835
|
+
"prepend this to SARIF file URIs \u2014 use when the scan root is not the repository root"
|
|
16836
|
+
).option("--deps", "also query OSV.dev for advisories against lockfile versions (network)").option("-v, --verbose", "list the paths that could not be read").action(async (targetPath, opts) => {
|
|
16837
|
+
const format = (opts.format ?? "text").toLowerCase();
|
|
16838
|
+
if (!["text", "json", "sarif"].includes(format)) {
|
|
16839
|
+
console.error(source_default.red(`Unknown --format "${opts.format}" (expected text, json, or sarif)`));
|
|
16840
|
+
process.exit(2);
|
|
16841
|
+
}
|
|
16842
|
+
let failOn;
|
|
16843
|
+
try {
|
|
16844
|
+
failOn = parseFailOn(opts.failOn);
|
|
16845
|
+
} catch (err) {
|
|
16846
|
+
console.error(source_default.red(err.message));
|
|
16847
|
+
process.exit(2);
|
|
16848
|
+
}
|
|
16849
|
+
await scanCommand(targetPath, {
|
|
16850
|
+
format,
|
|
16851
|
+
output: opts.output,
|
|
16852
|
+
failOn,
|
|
16853
|
+
pathPrefix: opts.pathPrefix,
|
|
16854
|
+
dependencies: opts.deps,
|
|
16855
|
+
verbose: opts.verbose
|
|
16856
|
+
});
|
|
15359
16857
|
});
|
|
15360
16858
|
program2.command("pentest").description("Penetration test URLs and APIs").argument("<url>", "Target URL to pentest").action(async (url) => {
|
|
15361
16859
|
await pentestCommand(url);
|
|
@@ -15401,7 +16899,7 @@ program2.command("uninstall-service").description("Remove the threatcrushd syste
|
|
|
15401
16899
|
program2.command("logs").description("Tail daemon logs").action(async () => {
|
|
15402
16900
|
console.log(LOGO2);
|
|
15403
16901
|
const logPath = PATHS.logFile;
|
|
15404
|
-
if (!(0,
|
|
16902
|
+
if (!(0, import_node_fs29.existsSync)(logPath)) {
|
|
15405
16903
|
console.log(source_default.yellow(` No log file found at ${logPath}`));
|
|
15406
16904
|
console.log(source_default.dim(" Start the daemon first with `threatcrush start`\n"));
|
|
15407
16905
|
return;
|
|
@@ -15414,10 +16912,10 @@ program2.command("logs").description("Tail daemon logs").action(async () => {
|
|
|
15414
16912
|
program2.command("activate").description("Activate your license key").action(async () => {
|
|
15415
16913
|
console.log(LOGO2);
|
|
15416
16914
|
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15417
|
-
const key = await new Promise((
|
|
16915
|
+
const key = await new Promise((resolve5) => {
|
|
15418
16916
|
rl.question(source_default.green(" Enter your ThreatCrush license key: "), (answer) => {
|
|
15419
16917
|
rl.close();
|
|
15420
|
-
|
|
16918
|
+
resolve5(answer.trim());
|
|
15421
16919
|
});
|
|
15422
16920
|
});
|
|
15423
16921
|
if (!key) {
|
|
@@ -15504,10 +17002,10 @@ program2.command("update").description("Update ThreatCrush CLI and installed bun
|
|
|
15504
17002
|
program2.command("remove").description("Uninstall ThreatCrush and the installed bundle").alias("uninstall").action(async () => {
|
|
15505
17003
|
console.log(LOGO2);
|
|
15506
17004
|
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15507
|
-
const confirm = await new Promise((
|
|
17005
|
+
const confirm = await new Promise((resolve5) => {
|
|
15508
17006
|
rl.question(source_default.yellow(" Are you sure you want to uninstall ThreatCrush? (y/N): "), (answer) => {
|
|
15509
17007
|
rl.close();
|
|
15510
|
-
|
|
17008
|
+
resolve5(answer.trim().toLowerCase());
|
|
15511
17009
|
});
|
|
15512
17010
|
});
|
|
15513
17011
|
if (confirm !== "y" && confirm !== "yes") {
|
|
@@ -15606,19 +17104,19 @@ storeCmd.command("search <query>").description("Search for modules in the store"
|
|
|
15606
17104
|
});
|
|
15607
17105
|
storeCmd.command("publish <url>").description("Publish a module from a git URL or web URL").action(async (url) => {
|
|
15608
17106
|
console.log(LOGO2);
|
|
15609
|
-
const configPath = (0,
|
|
17107
|
+
const configPath = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
|
|
15610
17108
|
let email = "";
|
|
15611
17109
|
try {
|
|
15612
|
-
const config = JSON.parse((0,
|
|
17110
|
+
const config = JSON.parse((0, import_node_fs29.readFileSync)(configPath, "utf-8"));
|
|
15613
17111
|
email = config.email || "";
|
|
15614
17112
|
} catch {
|
|
15615
17113
|
}
|
|
15616
17114
|
if (!email) {
|
|
15617
17115
|
const rl2 = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15618
|
-
email = await new Promise((
|
|
17116
|
+
email = await new Promise((resolve5) => {
|
|
15619
17117
|
rl2.question(source_default.green(" Enter your email: "), (answer) => {
|
|
15620
17118
|
rl2.close();
|
|
15621
|
-
|
|
17119
|
+
resolve5(answer.trim());
|
|
15622
17120
|
});
|
|
15623
17121
|
});
|
|
15624
17122
|
if (!email || !email.includes("@")) {
|
|
@@ -15626,9 +17124,9 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
|
|
|
15626
17124
|
return;
|
|
15627
17125
|
}
|
|
15628
17126
|
try {
|
|
15629
|
-
const dir = (0,
|
|
15630
|
-
if (!(0,
|
|
15631
|
-
(0,
|
|
17127
|
+
const dir = (0, import_node_path19.join)((0, import_node_os8.homedir)(), ".threatcrush");
|
|
17128
|
+
if (!(0, import_node_fs29.existsSync)(dir)) (0, import_node_fs29.mkdirSync)(dir, { recursive: true });
|
|
17129
|
+
(0, import_node_fs29.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
|
|
15632
17130
|
console.log(source_default.dim(` Saved email to ${configPath}`));
|
|
15633
17131
|
} catch {
|
|
15634
17132
|
}
|
|
@@ -15672,10 +17170,10 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
|
|
|
15672
17170
|
}
|
|
15673
17171
|
console.log();
|
|
15674
17172
|
const rl = import_readline.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
15675
|
-
const confirm = await new Promise((
|
|
17173
|
+
const confirm = await new Promise((resolve5) => {
|
|
15676
17174
|
rl.question(source_default.yellow(" Publish this module? (y/N): "), (answer) => {
|
|
15677
17175
|
rl.close();
|
|
15678
|
-
|
|
17176
|
+
resolve5(answer.trim().toLowerCase());
|
|
15679
17177
|
});
|
|
15680
17178
|
});
|
|
15681
17179
|
if (confirm !== "y" && confirm !== "yes") {
|