@profullstack/threatcrush 0.11.0 → 0.11.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/daemon.js +1139 -119
- package/dist/daemon.js.map +1 -1
- package/dist/index.js +1427 -254
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3891,14 +3891,52 @@ var init_paths = __esm({
|
|
|
3891
3891
|
}
|
|
3892
3892
|
});
|
|
3893
3893
|
|
|
3894
|
+
// src/daemon/control-token.ts
|
|
3895
|
+
function issueControlToken() {
|
|
3896
|
+
const token = (0, import_node_crypto.randomBytes)(32).toString("hex");
|
|
3897
|
+
(0, import_node_fs2.writeFileSync)(CONTROL_TOKEN_FILE, token, { mode: 384 });
|
|
3898
|
+
try {
|
|
3899
|
+
(0, import_node_fs2.chmodSync)(CONTROL_TOKEN_FILE, 384);
|
|
3900
|
+
} catch {
|
|
3901
|
+
}
|
|
3902
|
+
return token;
|
|
3903
|
+
}
|
|
3904
|
+
function readControlToken() {
|
|
3905
|
+
if (!(0, import_node_fs2.existsSync)(CONTROL_TOKEN_FILE)) return null;
|
|
3906
|
+
try {
|
|
3907
|
+
const token = (0, import_node_fs2.readFileSync)(CONTROL_TOKEN_FILE, "utf-8").trim();
|
|
3908
|
+
return token || null;
|
|
3909
|
+
} catch {
|
|
3910
|
+
return null;
|
|
3911
|
+
}
|
|
3912
|
+
}
|
|
3913
|
+
function tokensMatch(expected, provided) {
|
|
3914
|
+
if (typeof provided !== "string" || !provided) return false;
|
|
3915
|
+
const a = Buffer.from(expected);
|
|
3916
|
+
const b = Buffer.from(provided);
|
|
3917
|
+
return a.length === b.length && (0, import_node_crypto.timingSafeEqual)(a, b);
|
|
3918
|
+
}
|
|
3919
|
+
var import_node_crypto, import_node_fs2, import_node_path2, CONTROL_TOKEN_FILE;
|
|
3920
|
+
var init_control_token = __esm({
|
|
3921
|
+
"src/daemon/control-token.ts"() {
|
|
3922
|
+
"use strict";
|
|
3923
|
+
import_node_crypto = require("crypto");
|
|
3924
|
+
import_node_fs2 = require("fs");
|
|
3925
|
+
import_node_path2 = require("path");
|
|
3926
|
+
init_paths();
|
|
3927
|
+
CONTROL_TOKEN_FILE = (0, import_node_path2.join)(PATHS.runDir, "control.token");
|
|
3928
|
+
}
|
|
3929
|
+
});
|
|
3930
|
+
|
|
3894
3931
|
// src/core/ipc-client.ts
|
|
3895
|
-
var import_node_net,
|
|
3932
|
+
var import_node_net, import_node_fs3, IpcClient;
|
|
3896
3933
|
var init_ipc_client = __esm({
|
|
3897
3934
|
"src/core/ipc-client.ts"() {
|
|
3898
3935
|
"use strict";
|
|
3899
3936
|
import_node_net = require("net");
|
|
3900
|
-
|
|
3937
|
+
import_node_fs3 = require("fs");
|
|
3901
3938
|
init_paths();
|
|
3939
|
+
init_control_token();
|
|
3902
3940
|
IpcClient = class {
|
|
3903
3941
|
constructor(opts = {}) {
|
|
3904
3942
|
this.opts = opts;
|
|
@@ -3911,10 +3949,10 @@ var init_ipc_client = __esm({
|
|
|
3911
3949
|
pending = /* @__PURE__ */ new Map();
|
|
3912
3950
|
socketPath;
|
|
3913
3951
|
static isDaemonRunning(socketPath = resolveClientSocket()) {
|
|
3914
|
-
return (0,
|
|
3952
|
+
return (0, import_node_fs3.existsSync)(socketPath);
|
|
3915
3953
|
}
|
|
3916
3954
|
async connect(timeoutMs = 2e3) {
|
|
3917
|
-
if (!(0,
|
|
3955
|
+
if (!(0, import_node_fs3.existsSync)(this.socketPath)) {
|
|
3918
3956
|
throw new Error(`threatcrushd socket not found at ${this.socketPath}`);
|
|
3919
3957
|
}
|
|
3920
3958
|
return new Promise((resolve5, reject) => {
|
|
@@ -3975,7 +4013,7 @@ var init_ipc_client = __esm({
|
|
|
3975
4013
|
await this.request("subscribe", { channels });
|
|
3976
4014
|
}
|
|
3977
4015
|
async shutdown() {
|
|
3978
|
-
await this.request("shutdown");
|
|
4016
|
+
await this.request("shutdown", { token: readControlToken() });
|
|
3979
4017
|
}
|
|
3980
4018
|
onData(chunk) {
|
|
3981
4019
|
this.buffer += chunk;
|
|
@@ -4018,17 +4056,17 @@ var init_ipc_client = __esm({
|
|
|
4018
4056
|
// src/daemon/pidfile.ts
|
|
4019
4057
|
function writePidFile() {
|
|
4020
4058
|
ensureRuntimeDirs();
|
|
4021
|
-
(0,
|
|
4059
|
+
(0, import_node_fs4.writeFileSync)(PATHS.pidFile, String(process.pid), "utf-8");
|
|
4022
4060
|
}
|
|
4023
4061
|
function readPidFile() {
|
|
4024
|
-
if (!(0,
|
|
4025
|
-
const raw = (0,
|
|
4062
|
+
if (!(0, import_node_fs4.existsSync)(PATHS.pidFile)) return null;
|
|
4063
|
+
const raw = (0, import_node_fs4.readFileSync)(PATHS.pidFile, "utf-8").trim();
|
|
4026
4064
|
const pid = parseInt(raw, 10);
|
|
4027
4065
|
return Number.isFinite(pid) ? pid : null;
|
|
4028
4066
|
}
|
|
4029
4067
|
function removePidFile() {
|
|
4030
4068
|
try {
|
|
4031
|
-
if ((0,
|
|
4069
|
+
if ((0, import_node_fs4.existsSync)(PATHS.pidFile)) (0, import_node_fs4.unlinkSync)(PATHS.pidFile);
|
|
4032
4070
|
} catch {
|
|
4033
4071
|
}
|
|
4034
4072
|
}
|
|
@@ -4047,11 +4085,11 @@ function findRunningDaemon() {
|
|
|
4047
4085
|
if (pid) removePidFile();
|
|
4048
4086
|
return null;
|
|
4049
4087
|
}
|
|
4050
|
-
var
|
|
4088
|
+
var import_node_fs4;
|
|
4051
4089
|
var init_pidfile = __esm({
|
|
4052
4090
|
"src/daemon/pidfile.ts"() {
|
|
4053
4091
|
"use strict";
|
|
4054
|
-
|
|
4092
|
+
import_node_fs4 = require("fs");
|
|
4055
4093
|
init_paths();
|
|
4056
4094
|
}
|
|
4057
4095
|
});
|
|
@@ -8260,12 +8298,12 @@ var source_default = chalk;
|
|
|
8260
8298
|
// src/index.ts
|
|
8261
8299
|
var import_readline = __toESM(require("readline"));
|
|
8262
8300
|
var import_node_child_process10 = require("child_process");
|
|
8263
|
-
var
|
|
8264
|
-
var
|
|
8301
|
+
var import_node_fs31 = require("fs");
|
|
8302
|
+
var import_node_path21 = require("path");
|
|
8265
8303
|
var import_node_os8 = require("os");
|
|
8266
8304
|
|
|
8267
8305
|
// src/commands/monitor.ts
|
|
8268
|
-
var
|
|
8306
|
+
var import_node_fs5 = require("fs");
|
|
8269
8307
|
var import_node_readline = require("readline");
|
|
8270
8308
|
|
|
8271
8309
|
// src/core/logger.ts
|
|
@@ -8473,9 +8511,9 @@ async function monitorCommand(options) {
|
|
|
8473
8511
|
const unreadable = [];
|
|
8474
8512
|
for (const s of LOG_SOURCES) {
|
|
8475
8513
|
if (moduleFilter && !moduleFilter.includes(s.name)) continue;
|
|
8476
|
-
if (!(0,
|
|
8514
|
+
if (!(0, import_node_fs5.existsSync)(s.path)) continue;
|
|
8477
8515
|
try {
|
|
8478
|
-
(0,
|
|
8516
|
+
(0, import_node_fs5.accessSync)(s.path, import_node_fs5.constants.R_OK);
|
|
8479
8517
|
availableSources.push(s);
|
|
8480
8518
|
} catch {
|
|
8481
8519
|
unreadable.push(s);
|
|
@@ -8492,11 +8530,11 @@ async function monitorCommand(options) {
|
|
|
8492
8530
|
logger.warn("No readable log files found to monitor.");
|
|
8493
8531
|
logger.info("Available log paths checked:");
|
|
8494
8532
|
for (const s of LOG_SOURCES) {
|
|
8495
|
-
const exists = (0,
|
|
8533
|
+
const exists = (0, import_node_fs5.existsSync)(s.path);
|
|
8496
8534
|
let readable = false;
|
|
8497
8535
|
if (exists) {
|
|
8498
8536
|
try {
|
|
8499
|
-
(0,
|
|
8537
|
+
(0, import_node_fs5.accessSync)(s.path, import_node_fs5.constants.R_OK);
|
|
8500
8538
|
readable = true;
|
|
8501
8539
|
} catch {
|
|
8502
8540
|
readable = false;
|
|
@@ -8528,16 +8566,16 @@ async function monitorCommand(options) {
|
|
|
8528
8566
|
}
|
|
8529
8567
|
function tailLog(source) {
|
|
8530
8568
|
const { path, name, category } = source;
|
|
8531
|
-
const stat = (0,
|
|
8569
|
+
const stat = (0, import_node_fs5.statSync)(path);
|
|
8532
8570
|
let position = stat.size;
|
|
8533
8571
|
const checkForNewData = () => {
|
|
8534
8572
|
try {
|
|
8535
|
-
const currentStat = (0,
|
|
8573
|
+
const currentStat = (0, import_node_fs5.statSync)(path);
|
|
8536
8574
|
if (currentStat.size <= position) {
|
|
8537
8575
|
if (currentStat.size < position) position = 0;
|
|
8538
8576
|
return;
|
|
8539
8577
|
}
|
|
8540
|
-
const stream = (0,
|
|
8578
|
+
const stream = (0, import_node_fs5.createReadStream)(path, { start: position, encoding: "utf-8" });
|
|
8541
8579
|
stream.on("error", (err) => {
|
|
8542
8580
|
logger.warn(`stopped tailing ${path}: ${err.code || err.message}`);
|
|
8543
8581
|
position = currentStat.size;
|
|
@@ -8639,8 +8677,8 @@ async function runDemoMode() {
|
|
|
8639
8677
|
}
|
|
8640
8678
|
|
|
8641
8679
|
// src/commands/scan.ts
|
|
8642
|
-
var
|
|
8643
|
-
var
|
|
8680
|
+
var import_node_fs8 = require("fs");
|
|
8681
|
+
var import_node_path6 = require("path");
|
|
8644
8682
|
|
|
8645
8683
|
// ../../node_modules/.pnpm/ora@8.2.0/node_modules/ora/index.js
|
|
8646
8684
|
var import_node_process7 = __toESM(require("process"), 1);
|
|
@@ -9580,6 +9618,357 @@ function workerId() {
|
|
|
9580
9618
|
return `${import_node_os3.default.hostname()}/${process.pid}`;
|
|
9581
9619
|
}
|
|
9582
9620
|
|
|
9621
|
+
// ../../packages/scan/src/node-rules.ts
|
|
9622
|
+
var PATH_CONTAINMENT_GUARD = /\bstartsWith\s*\(|\brelative\s*\(|\bnormalize\s*\(|\brealpath\b|\bisInside\b|\bwithin\s*\(|\bsanitiz\w*\b/i;
|
|
9623
|
+
var ARCHIVE_LIBRARY = /\b(?:adm-zip|unzipper|yauzl|node-stream-zip|extract-zip|decompress|tar-stream|tar-fs)\b|require\s*\(\s*['"]tar['"]|from\s+['"]tar['"]/;
|
|
9624
|
+
var HEADLESS_BROWSER = /\b(?:puppeteer|playwright|phantom|phantomjs|wkhtmltopdf|wkhtmltoimage|chrome-aws-lambda|html-pdf)\b/;
|
|
9625
|
+
var ORIGIN_ALLOWLIST_GUARD = /\b(?:includes|indexOf|has|test|find|some)\s*\(/;
|
|
9626
|
+
var FUNCTION_DESERIALIZER = /\b(?:node-serialize|serialize-to-js|funcster|cryo)\b/;
|
|
9627
|
+
var CONCATENATED_STRING = String.raw`(?:"[^"\n]*"|'[^'\n]*')\s*\+`;
|
|
9628
|
+
var INTERPOLATED_TEMPLATE = "`[^`\\n]*\\$\\{";
|
|
9629
|
+
var NODE_RULES = [
|
|
9630
|
+
// ── Code execution ───────────────────────────────────────────────────────
|
|
9631
|
+
{
|
|
9632
|
+
id: "js-vm-untrusted-execution",
|
|
9633
|
+
title: "script compiled and run by the `vm` module",
|
|
9634
|
+
consequence: "Node\u2019s `vm` is not a security boundary \u2014 it isolates globals, not the process. Code reaching it can walk back out through any object it is handed and runs with the server\u2019s full privileges.",
|
|
9635
|
+
cwe: "CWE-94",
|
|
9636
|
+
severity: "critical",
|
|
9637
|
+
languages: ["javascript", "typescript"],
|
|
9638
|
+
pattern: /\bvm\s*\.\s*(?:runInNewContext|runInThisContext|runInContext|compileFunction)\s*\(|\bnew\s+vm\s*\.\s*Script\s*\(/,
|
|
9639
|
+
// A `vm` call whose source is a build-time constant is a plugin loader, not
|
|
9640
|
+
// a vulnerability. The class only becomes real once the script text can be
|
|
9641
|
+
// influenced, so require that evidence before reporting at full severity.
|
|
9642
|
+
needsContext: true
|
|
9643
|
+
},
|
|
9644
|
+
{
|
|
9645
|
+
id: "js-vm2-sandbox",
|
|
9646
|
+
title: "`vm2` used as a sandbox",
|
|
9647
|
+
consequence: "vm2 was discontinued after a series of escapes that its maintainer judged unfixable by design. Any code it runs should be assumed to run on the host.",
|
|
9648
|
+
cwe: "CWE-1104",
|
|
9649
|
+
severity: "high",
|
|
9650
|
+
languages: ["javascript", "typescript"],
|
|
9651
|
+
pattern: /\bnew\s+(?:NodeVM|VMScript)\s*\(|\bfrom\s+['"]vm2['"]|require\s*\(\s*['"]vm2['"]\s*\)/,
|
|
9652
|
+
// Nothing on the surrounding lines changes the answer: the package itself
|
|
9653
|
+
// is the finding, the same way a broken cipher is.
|
|
9654
|
+
inherent: true
|
|
9655
|
+
},
|
|
9656
|
+
{
|
|
9657
|
+
id: "js-function-deserialization",
|
|
9658
|
+
title: "deserialiser that reconstructs functions",
|
|
9659
|
+
consequence: "These formats encode functions alongside data and invoke them on load, so parsing an attacker\u2019s payload is executing it. No amount of validation after the parse call helps \u2014 the code has already run.",
|
|
9660
|
+
cwe: "CWE-502",
|
|
9661
|
+
severity: "critical",
|
|
9662
|
+
languages: ["javascript", "typescript"],
|
|
9663
|
+
pattern: /\b(?:unserialize|deepDeserialize|deserialize)\s*\(/,
|
|
9664
|
+
// Without this the rule fires on every project that happens to own a
|
|
9665
|
+
// function called `deserialize`, which is most of them. The import is what
|
|
9666
|
+
// makes the call the dangerous one.
|
|
9667
|
+
fileRequires: FUNCTION_DESERIALIZER,
|
|
9668
|
+
// The parse *is* the execution, so nearby input cannot make it worse and
|
|
9669
|
+
// its absence cannot make it safe. Nobody round-trips a constant through
|
|
9670
|
+
// these libraries.
|
|
9671
|
+
inherent: true
|
|
9672
|
+
},
|
|
9673
|
+
{
|
|
9674
|
+
id: "js-template-injection",
|
|
9675
|
+
title: "template compiled from a non-constant source",
|
|
9676
|
+
consequence: "Template languages are programming languages. A user-supplied template body is remote code execution, not cross-site scripting \u2014 the expression runs on the server before any output is escaped.",
|
|
9677
|
+
cwe: "CWE-1336",
|
|
9678
|
+
severity: "critical",
|
|
9679
|
+
languages: ["javascript", "typescript"],
|
|
9680
|
+
pattern: /\b(?:handlebars|Handlebars|hbs|ejs|pug|jade|nunjucks|eta|twig|dot|doT|liquid|mustache)\s*\.\s*(?:compile|compileFile|render|renderString)\s*\(\s*(?:`[^`\n]*\$\{|[a-zA-Z_$][\w$.]*\s*[,)]|[a-zA-Z_$][\w$.]*\s*\+)/,
|
|
9681
|
+
// Rendering a template held in a variable is the normal case — it was read
|
|
9682
|
+
// from a file at boot. Only the version where request data reaches the
|
|
9683
|
+
// template *body* is this class.
|
|
9684
|
+
needsContext: true
|
|
9685
|
+
},
|
|
9686
|
+
{
|
|
9687
|
+
id: "js-shelljs-command-execution",
|
|
9688
|
+
title: "shelljs command assembled from a string",
|
|
9689
|
+
consequence: "`shell.exec` runs its argument through a shell, so a `;` or backtick in an interpolated value runs as the server user.",
|
|
9690
|
+
cwe: "CWE-78",
|
|
9691
|
+
severity: "critical",
|
|
9692
|
+
languages: ["javascript", "typescript"],
|
|
9693
|
+
pattern: new RegExp(
|
|
9694
|
+
`\\b(?:shell|shelljs|sh)\\s*\\.\\s*exec\\s*\\(\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING}|[a-zA-Z_$][\\w$]*\\s*\\+)`
|
|
9695
|
+
),
|
|
9696
|
+
fileRequires: /\bshelljs\b/
|
|
9697
|
+
},
|
|
9698
|
+
// ── Injection ────────────────────────────────────────────────────────────
|
|
9699
|
+
{
|
|
9700
|
+
id: "js-nosql-where-expression",
|
|
9701
|
+
title: "MongoDB `$where` built by string assembly",
|
|
9702
|
+
consequence: "`$where` is evaluated as JavaScript by the database server, once per document. An interpolated value can rewrite the predicate to `true` or run a denial-of-service loop inside the database.",
|
|
9703
|
+
cwe: "CWE-943",
|
|
9704
|
+
severity: "critical",
|
|
9705
|
+
languages: ["javascript", "typescript"],
|
|
9706
|
+
pattern: new RegExp(
|
|
9707
|
+
`\\$where\\s*['"]?\\s*:\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING})|\\.\\s*\\$where\\s*=\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING})`
|
|
9708
|
+
)
|
|
9709
|
+
},
|
|
9710
|
+
{
|
|
9711
|
+
id: "js-xpath-injection",
|
|
9712
|
+
title: "XPath expression built by concatenation",
|
|
9713
|
+
consequence: "A quote in the interpolated value closes the predicate early, so the query selects nodes the caller was never meant to read \u2014 the XML equivalent of `OR 1=1`.",
|
|
9714
|
+
cwe: "CWE-643",
|
|
9715
|
+
severity: "high",
|
|
9716
|
+
languages: ["javascript", "typescript"],
|
|
9717
|
+
// An XPath expression is recognisable by its own syntax — a descendant
|
|
9718
|
+
// axis or an attribute predicate. Matching on the *call* name alone would
|
|
9719
|
+
// flag every `select(` in the ecosystem. The quoted forms are split per
|
|
9720
|
+
// quote character for the reason given at CONCATENATED_STRING: the
|
|
9721
|
+
// predicate that makes it XPath usually contains the other quote.
|
|
9722
|
+
pattern: new RegExp(
|
|
9723
|
+
`\\b(?:xpath|xpathSelect|select|selectNodes|selectSingleNode|evaluate|find)\\s*\\(\\s*(?:\`[^\`\\n]*(?:\\/\\/|\\[@)[^\`\\n]*\\$\\{|"[^"\\n]*(?:\\/\\/|\\[@)[^"\\n]*"\\s*\\+|'[^'\\n]*(?:\\/\\/|\\[@)[^'\\n]*'\\s*\\+)`
|
|
9724
|
+
),
|
|
9725
|
+
fileRequires: /\bxpath\b|\bxmldom\b|\blibxmljs\b|\bxpath\.js\b/
|
|
9726
|
+
},
|
|
9727
|
+
{
|
|
9728
|
+
id: "js-regex-from-input",
|
|
9729
|
+
title: "regular expression compiled from a variable",
|
|
9730
|
+
consequence: "A caller who controls the pattern controls the matcher: they can supply catastrophic backtracking to hang the event loop, or a permissive pattern that defeats whatever the regex was validating.",
|
|
9731
|
+
cwe: "CWE-1333",
|
|
9732
|
+
severity: "medium",
|
|
9733
|
+
languages: ["javascript", "typescript"],
|
|
9734
|
+
pattern: new RegExp(
|
|
9735
|
+
`\\bnew\\s+RegExp\\s*\\(\\s*(?:${INTERPOLATED_TEMPLATE}|${CONCATENATED_STRING}|(?!['"\`/])[a-zA-Z_$][\\w$.]*\\s*[,)])`
|
|
9736
|
+
),
|
|
9737
|
+
needsContext: true
|
|
9738
|
+
},
|
|
9739
|
+
// ── XML ──────────────────────────────────────────────────────────────────
|
|
9740
|
+
{
|
|
9741
|
+
id: "js-xml-external-entities",
|
|
9742
|
+
title: "XML parser configured to resolve entities",
|
|
9743
|
+
consequence: "An entity declaration in the document body makes the parser fetch a local file or an internal URL and paste the result into the parsed output \u2014 file disclosure and server-side request forgery from a document upload.",
|
|
9744
|
+
cwe: "CWE-611",
|
|
9745
|
+
severity: "high",
|
|
9746
|
+
languages: ["javascript", "typescript"],
|
|
9747
|
+
// The option name is the finding. Every parser in the ecosystem defaults
|
|
9748
|
+
// these off, so an explicit `true` is a deliberate re-enable.
|
|
9749
|
+
pattern: /\b(?:noent|resolveEntities|expandEntities|externalEntities|resolveExternals)\s*:\s*(?:true|1)\b/,
|
|
9750
|
+
inherent: true
|
|
9751
|
+
},
|
|
9752
|
+
// ── Authentication and tokens ────────────────────────────────────────────
|
|
9753
|
+
{
|
|
9754
|
+
id: "js-jwt-none-algorithm",
|
|
9755
|
+
title: "JWT algorithm set to `none`",
|
|
9756
|
+
consequence: "The `none` algorithm means the signature is not checked. Anyone can mint a token with any claims \u2014 including another user\u2019s id or an admin role \u2014 by base64-encoding a header and a body.",
|
|
9757
|
+
cwe: "CWE-347",
|
|
9758
|
+
severity: "critical",
|
|
9759
|
+
languages: ["javascript", "typescript"],
|
|
9760
|
+
pattern: /\balgorithms?\s*:\s*\[?\s*['"]none['"]/i,
|
|
9761
|
+
inherent: true
|
|
9762
|
+
},
|
|
9763
|
+
// ── Cryptography ─────────────────────────────────────────────────────────
|
|
9764
|
+
{
|
|
9765
|
+
id: "js-broken-cipher-algorithm",
|
|
9766
|
+
title: "broken cipher selected",
|
|
9767
|
+
consequence: "DES, 3DES, RC2, RC4, Blowfish and IDEA are all breakable with commodity hardware or have practical plaintext-recovery attacks. Data encrypted with them should be treated as encoded, not encrypted.",
|
|
9768
|
+
cwe: "CWE-327",
|
|
9769
|
+
severity: "high",
|
|
9770
|
+
languages: ["javascript", "typescript"],
|
|
9771
|
+
pattern: /\bcreate(?:Cipher|Decipher)(?:iv)?\s*\(\s*['"](?:des|des3|des-ede\w*|3des|rc2|rc4|bf|blowfish|cast5?|idea|seed)\b/i,
|
|
9772
|
+
inherent: true
|
|
9773
|
+
},
|
|
9774
|
+
{
|
|
9775
|
+
id: "js-ecb-mode-cipher",
|
|
9776
|
+
title: "block cipher in ECB mode",
|
|
9777
|
+
consequence: "ECB encrypts every block independently, so identical plaintext blocks produce identical ciphertext. Structure in the data survives encryption and blocks can be reordered or replayed without detection.",
|
|
9778
|
+
cwe: "CWE-327",
|
|
9779
|
+
severity: "high",
|
|
9780
|
+
languages: ["javascript", "typescript"],
|
|
9781
|
+
pattern: /\bcreate(?:Cipher|Decipher)(?:iv)?\s*\(\s*['"][^'"\n]*-ecb\b/i,
|
|
9782
|
+
inherent: true
|
|
9783
|
+
},
|
|
9784
|
+
{
|
|
9785
|
+
id: "js-legacy-cipher-api",
|
|
9786
|
+
title: "deprecated `createCipher` used",
|
|
9787
|
+
consequence: "`createCipher` derives the key from a passphrase with a single unsalted MD5 pass and uses a fixed all-zero IV, so the same passphrase always produces the same keystream. It was removed in Node 22.",
|
|
9788
|
+
cwe: "CWE-327",
|
|
9789
|
+
severity: "high",
|
|
9790
|
+
languages: ["javascript", "typescript"],
|
|
9791
|
+
// `createCipheriv` is the correct API and shares the prefix, so the
|
|
9792
|
+
// negative look-ahead is what separates the finding from the fix.
|
|
9793
|
+
pattern: /\bcrypto\s*\.\s*create(?:Cipher|Decipher)\s*\(/,
|
|
9794
|
+
lineGuard: /create(?:Cipher|Decipher)iv\s*\(/,
|
|
9795
|
+
inherent: true
|
|
9796
|
+
},
|
|
9797
|
+
// ── Cross-site scripting ─────────────────────────────────────────────────
|
|
9798
|
+
{
|
|
9799
|
+
id: "js-template-autoescape-disabled",
|
|
9800
|
+
title: "template auto-escaping turned off",
|
|
9801
|
+
consequence: "Auto-escaping is the control that makes a template engine safe by default. Disabling it globally means every interpolation in every template becomes an injection point, including ones written later by someone who assumed the default.",
|
|
9802
|
+
cwe: "CWE-79",
|
|
9803
|
+
severity: "high",
|
|
9804
|
+
languages: ["javascript", "typescript"],
|
|
9805
|
+
pattern: /\bautoescape\s*:\s*false\b|\bescape\s*:\s*false\b|\bnoEscape\s*:\s*true\b/,
|
|
9806
|
+
inherent: true
|
|
9807
|
+
},
|
|
9808
|
+
{
|
|
9809
|
+
id: "js-serialize-javascript-unsafe",
|
|
9810
|
+
title: "`serialize-javascript` in unsafe mode",
|
|
9811
|
+
consequence: "The `unsafe` flag turns off escaping of HTML-significant characters in the output. Embedding the result in a `<script>` block lets a string value close the tag and start a new one.",
|
|
9812
|
+
cwe: "CWE-79",
|
|
9813
|
+
severity: "high",
|
|
9814
|
+
languages: ["javascript", "typescript"],
|
|
9815
|
+
pattern: /\bunsafe\s*:\s*true\b/,
|
|
9816
|
+
fileRequires: /\bserialize-javascript\b/,
|
|
9817
|
+
inherent: true
|
|
9818
|
+
},
|
|
9819
|
+
{
|
|
9820
|
+
id: "js-cors-origin-reflected",
|
|
9821
|
+
title: "CORS origin reflected from the request",
|
|
9822
|
+
consequence: "Echoing the caller\u2019s `Origin` header back as `Access-Control-Allow-Origin` allows every site, while looking like an allow-list. Combined with credentials, any page the victim visits can read their authenticated responses.",
|
|
9823
|
+
cwe: "CWE-942",
|
|
9824
|
+
severity: "high",
|
|
9825
|
+
languages: ["javascript", "typescript"],
|
|
9826
|
+
// Both spellings of the same mistake: writing the header directly, and
|
|
9827
|
+
// handing the request's origin to a CORS middleware's `origin` option.
|
|
9828
|
+
//
|
|
9829
|
+
// An earlier draft also matched a bare variable — `setHeader('…', origin)`
|
|
9830
|
+
// — which is wrong. A variable holding a *validated* origin is exactly
|
|
9831
|
+
// what the correct implementation looks like, and the rule cannot tell the
|
|
9832
|
+
// two apart. Only the visible read from the request qualifies.
|
|
9833
|
+
pattern: /\bAccess-Control-Allow-Origin['"]\s*,\s*(?:req|request|ctx)\s*\.|\borigin\s*:\s*(?:req|request|ctx)\s*\./,
|
|
9834
|
+
// Reflecting the header is the defect whatever surrounds it; there is no
|
|
9835
|
+
// arrangement of nearby lines that makes an echoed origin safe — except a
|
|
9836
|
+
// membership test on the value, which is what the guard looks for.
|
|
9837
|
+
inherent: true,
|
|
9838
|
+
guard: ORIGIN_ALLOWLIST_GUARD,
|
|
9839
|
+
/**
|
|
9840
|
+
* Logging the origin is not reflecting it.
|
|
9841
|
+
*
|
|
9842
|
+
* `origin: request.headers.origin` is the CORS mistake *and* the ordinary
|
|
9843
|
+
* way to record who called — the two are character-for-character
|
|
9844
|
+
* identical, and only what encloses them differs. A response header goes
|
|
9845
|
+
* out to the browser; a log line goes to stdout, where it grants nobody
|
|
9846
|
+
* anything.
|
|
9847
|
+
*/
|
|
9848
|
+
enclosingCallGuard: /(?:^|\.)(?:log|debug|info|warn|error|trace|verbose|fatal)$/
|
|
9849
|
+
},
|
|
9850
|
+
// ── Server-side request forgery ──────────────────────────────────────────
|
|
9851
|
+
{
|
|
9852
|
+
id: "js-headless-browser-navigation",
|
|
9853
|
+
title: "headless browser sent to a non-constant URL",
|
|
9854
|
+
consequence: "The browser runs on the server, inside the private network. A controlled URL reaches the cloud metadata endpoint, localhost admin panels and internal services \u2014 and `file://` reads the disk.",
|
|
9855
|
+
cwe: "CWE-918",
|
|
9856
|
+
severity: "high",
|
|
9857
|
+
languages: ["javascript", "typescript"],
|
|
9858
|
+
pattern: /\.\s*(?:goto|setContent|navigate)\s*\(\s*(?:`[^`\n]*\$\{|(?!['"`])[a-zA-Z_$][\w$.]*\s*[,)])/,
|
|
9859
|
+
fileRequires: HEADLESS_BROWSER,
|
|
9860
|
+
needsContext: true
|
|
9861
|
+
},
|
|
9862
|
+
// ── Path handling ────────────────────────────────────────────────────────
|
|
9863
|
+
{
|
|
9864
|
+
id: "js-archive-entry-path",
|
|
9865
|
+
title: "archive entry written to a path built from its own name",
|
|
9866
|
+
consequence: "An entry named `../../etc/cron.d/x` escapes the extraction directory when its name is joined to the destination. Overwriting a file outside the target \u2014 a systemd unit, an SSH key, a deployed script \u2014 is code execution on the next run.",
|
|
9867
|
+
cwe: "CWE-22",
|
|
9868
|
+
severity: "high",
|
|
9869
|
+
languages: ["javascript", "typescript"],
|
|
9870
|
+
pattern: /\b(?:join|resolve)\s*\(\s*[^,\n)]+,\s*[a-zA-Z_$][\w$]*\s*\.\s*(?:entryName|fileName|filename|name|path)\b/,
|
|
9871
|
+
fileRequires: ARCHIVE_LIBRARY,
|
|
9872
|
+
guard: PATH_CONTAINMENT_GUARD,
|
|
9873
|
+
// The containment check comes *after* the join — you build the path, then
|
|
9874
|
+
// verify it stayed inside. A backwards-only window would miss every
|
|
9875
|
+
// correct implementation and report the safe code alongside the unsafe.
|
|
9876
|
+
guardForward: 3
|
|
9877
|
+
},
|
|
9878
|
+
// ── Electron ─────────────────────────────────────────────────────────────
|
|
9879
|
+
{
|
|
9880
|
+
id: "js-electron-node-integration",
|
|
9881
|
+
title: "Electron renderer given Node access",
|
|
9882
|
+
consequence: "With node integration on \u2014 or context isolation off \u2014 any script that reaches the page reaches `require`. A single XSS in rendered content becomes `child_process.exec` on the user\u2019s machine.",
|
|
9883
|
+
cwe: "CWE-1188",
|
|
9884
|
+
severity: "high",
|
|
9885
|
+
languages: ["javascript", "typescript"],
|
|
9886
|
+
pattern: /\bnodeIntegration(?:InWorker|InSubFrames)?\s*:\s*true\b|\bcontextIsolation\s*:\s*false\b|\benableRemoteModule\s*:\s*true\b|\bsandbox\s*:\s*false\b/,
|
|
9887
|
+
inherent: true
|
|
9888
|
+
},
|
|
9889
|
+
{
|
|
9890
|
+
id: "js-electron-web-security-disabled",
|
|
9891
|
+
title: "Electron web security disabled",
|
|
9892
|
+
consequence: "Turning off `webSecurity` drops the same-origin policy for the window, so remote content can read local files and every other origin the app has loaded.",
|
|
9893
|
+
cwe: "CWE-1173",
|
|
9894
|
+
severity: "high",
|
|
9895
|
+
languages: ["javascript", "typescript"],
|
|
9896
|
+
pattern: /\bwebSecurity\s*:\s*false\b|\ballowRunningInsecureContent\s*:\s*true\b|\bwebviewTag\s*:\s*true\b/,
|
|
9897
|
+
inherent: true
|
|
9898
|
+
},
|
|
9899
|
+
{
|
|
9900
|
+
id: "js-electron-open-external",
|
|
9901
|
+
title: "Electron `openExternal` with a non-constant URL",
|
|
9902
|
+
consequence: "`shell.openExternal` hands the string to the operating system\u2019s handler. A `file://` path executes a local binary and, on Windows, an SMB path executes a remote one.",
|
|
9903
|
+
cwe: "CWE-749",
|
|
9904
|
+
severity: "high",
|
|
9905
|
+
languages: ["javascript", "typescript"],
|
|
9906
|
+
pattern: /\bshell\s*\.\s*openExternal\s*\(\s*(?:`[^`\n]*\$\{|(?!['"`])[a-zA-Z_$][\w$.]*\s*[,)])/,
|
|
9907
|
+
needsContext: true
|
|
9908
|
+
},
|
|
9909
|
+
// ── Hardening and resource limits ────────────────────────────────────────
|
|
9910
|
+
{
|
|
9911
|
+
id: "js-helmet-protection-disabled",
|
|
9912
|
+
title: "security header explicitly disabled",
|
|
9913
|
+
consequence: "Each of these switches off a browser-side protection that was already on. The header stops being sent, so the defence it enables \u2014 framing, sniffing, referrer leakage, transport downgrade \u2014 is available to an attacker again.",
|
|
9914
|
+
cwe: "CWE-693",
|
|
9915
|
+
severity: "medium",
|
|
9916
|
+
languages: ["javascript", "typescript"],
|
|
9917
|
+
pattern: /\b(?:contentSecurityPolicy|frameguard|hsts|noSniff|xssFilter|hidePoweredBy|referrerPolicy|dnsPrefetchControl|ieNoOpen|permittedCrossDomainPolicies|crossOriginEmbedderPolicy|crossOriginOpenerPolicy|crossOriginResourcePolicy|originAgentCluster)\s*:\s*false\b/,
|
|
9918
|
+
inherent: true
|
|
9919
|
+
},
|
|
9920
|
+
{
|
|
9921
|
+
id: "js-buffer-bounds-check-disabled",
|
|
9922
|
+
title: "buffer bounds checking turned off",
|
|
9923
|
+
consequence: "With `noAssert` the read or write is not range-checked, so an offset past the end of the buffer returns adjacent heap memory or corrupts it instead of throwing.",
|
|
9924
|
+
cwe: "CWE-125",
|
|
9925
|
+
severity: "medium",
|
|
9926
|
+
languages: ["javascript", "typescript"],
|
|
9927
|
+
// The trailing `true` on a Buffer numeric accessor *is* `noAssert` — it is
|
|
9928
|
+
// the last positional parameter of every one of these methods. Anchoring
|
|
9929
|
+
// on the accessor name is what keeps this from matching `, true)` on any
|
|
9930
|
+
// call in the codebase.
|
|
9931
|
+
pattern: /\b(?:read|write)(?:U?Int(?:8|16|32)(?:[BL]E)?|U?Int[BL]E|Float[BL]E|Double[BL]E)\s*\([^)\n]*,\s*true\s*\)|\bnoAssert\s*:\s*true\b/,
|
|
9932
|
+
inherent: true
|
|
9933
|
+
},
|
|
9934
|
+
{
|
|
9935
|
+
id: "js-uninitialized-buffer",
|
|
9936
|
+
title: "buffer allocated without zeroing",
|
|
9937
|
+
consequence: "`allocUnsafe` and the old `new Buffer(size)` hand back whatever was previously in that heap memory \u2014 keys, session tokens, other users\u2019 request bodies. Anything not overwritten before the buffer is sent leaks it.",
|
|
9938
|
+
cwe: "CWE-908",
|
|
9939
|
+
severity: "medium",
|
|
9940
|
+
languages: ["javascript", "typescript"],
|
|
9941
|
+
pattern: /\bBuffer\s*\.\s*allocUnsafe(?:Slow)?\s*\(|\bnew\s+Buffer\s*\(\s*(?![`'"])[a-zA-Z_$0-9]/,
|
|
9942
|
+
inherent: true
|
|
9943
|
+
},
|
|
9944
|
+
{
|
|
9945
|
+
id: "js-oversized-request-body-limit",
|
|
9946
|
+
title: "request body limit raised to a very large value",
|
|
9947
|
+
consequence: "A body limit in the tens of megabytes lets a handful of concurrent requests exhaust memory, and the parse happens before any authentication check the route performs.",
|
|
9948
|
+
cwe: "CWE-400",
|
|
9949
|
+
severity: "medium",
|
|
9950
|
+
languages: ["javascript", "typescript"],
|
|
9951
|
+
// Two or more digits before `mb` — 50mb and up. A limit is a *good* thing;
|
|
9952
|
+
// only an ineffective one is the finding.
|
|
9953
|
+
pattern: /\blimit\s*:\s*['"]\s*(?:[5-9]\d|\d{3,})\s*mb\s*['"]|\blimit\s*:\s*['"]\s*\d+\s*gb\s*['"]/i,
|
|
9954
|
+
inherent: true
|
|
9955
|
+
},
|
|
9956
|
+
// ── Information disclosure ───────────────────────────────────────────────
|
|
9957
|
+
{
|
|
9958
|
+
id: "js-error-detail-returned",
|
|
9959
|
+
title: "error object or stack trace sent to the client",
|
|
9960
|
+
consequence: "A stack trace names absolute paths, package versions and internal function names, and framework errors often carry the failing query or connection string with them. It is a map of the server, handed out on request.",
|
|
9961
|
+
cwe: "CWE-209",
|
|
9962
|
+
severity: "medium",
|
|
9963
|
+
languages: ["javascript", "typescript"],
|
|
9964
|
+
// `res.status(500).send(…)` is the overwhelmingly common spelling, so the
|
|
9965
|
+
// optional `.status(…)` hop is not a nicety — without it the rule misses
|
|
9966
|
+
// nearly every real occurrence.
|
|
9967
|
+
pattern: /\bres\s*\.\s*(?:status\s*\([^)\n]*\)\s*\.\s*)?(?:send|json|end|write)\s*\(\s*(?:[a-zA-Z_$][\w$]*\s*\.\s*stack\b|(?:error|err|e)\s*[,)])|\.\s*(?:send|json)\s*\(\s*\{[^}\n]*\b(?:stack|err|error)\s*:\s*(?:error|err|e)\s*[,}]/,
|
|
9968
|
+
inherent: true
|
|
9969
|
+
}
|
|
9970
|
+
];
|
|
9971
|
+
|
|
9583
9972
|
// ../../packages/scan/src/types.ts
|
|
9584
9973
|
var SEVERITY_ORDER = ["info", "low", "medium", "high", "critical"];
|
|
9585
9974
|
function severityRank(severity) {
|
|
@@ -9626,7 +10015,20 @@ var GENERIC_GUARD = (
|
|
|
9626
10015
|
// The identifier must END at the escaper (with at most a known output-context
|
|
9627
10016
|
// suffix). An earlier, looser form also matched `describe(`, which would have
|
|
9628
10017
|
// silenced findings across every test file in every repository.
|
|
9629
|
-
|
|
10018
|
+
// The `Access-Control-` lookbehind is not a nicety. This regex is
|
|
10019
|
+
// case-insensitive, and `Access-Control-Allow-Origin` contains the word
|
|
10020
|
+
// "Allow" followed by a non-word character — so the CORS header name matched
|
|
10021
|
+
// the allow-list heuristic. Because the guard is tested against an 8-line
|
|
10022
|
+
// *window*, one header line silently disabled every guardable rule near it:
|
|
10023
|
+
// in a four-line Express error handler, the header on one line suppressed
|
|
10024
|
+
// both the `none`-algorithm JWT finding and the returned stack trace below
|
|
10025
|
+
// it. The failure mode is the dangerous kind — not fewer findings, none, and
|
|
10026
|
+
// indistinguishable from clean code.
|
|
10027
|
+
//
|
|
10028
|
+
// Scoped to the header prefix rather than to a bare `allow(?!-)`, because
|
|
10029
|
+
// this guard also runs over `.yml` and `.conf` files, where `allow-list:`
|
|
10030
|
+
// and `allowed-hosts:` are ordinary keys that should still guard.
|
|
10031
|
+
/(?<!Access-Control-)\ballow(?:ed|list|_list|ed_hosts)?\b|\bwhitelist\b|\b\w{0,6}[Ee]sc(?:ape)?(?:[Hh]tml|HTML|[Xx]ml|XML|[Ss]ql|[Aa]ttr|[Jj]s|[Uu]ri|[Uu]rl)?\s*\(|\bhtml_escape\b|\bhtmlspecialchars\s*\(|\bsanitiz\w*\b|\bencoded\b|\brealpath\b|\bcommonpath\b|\bresolve\(\)\.startsWith\b|\bprocess\.env\b|\bos\.environ\b|\bgetenv\b|\bENV\s*\[|setObjectInputFilter|ObjectInputFilter/i
|
|
9630
10032
|
);
|
|
9631
10033
|
var XXE_GUARD = /FEATURE_SECURE_PROCESSING|setExpandEntityReferences|disallow-doctype-decl|external-general-entities|external-parameter-entities|setXIncludeAware\s*\(\s*false/;
|
|
9632
10034
|
var XML_PARSING_FILE = /\b(?:javax\.xml|org\.xml\.sax|org\.w3c\.dom|org\.jdom2?|org\.dom4j|XmlPullParser|DocumentBuilderFactory|DocumentBuilder|SAXParserFactory|SAXParser|XMLInputFactory|XMLReaderFactory|XMLReader|SAXBuilder|SAXReader)\b/;
|
|
@@ -9692,7 +10094,8 @@ var CODE_RULES = [
|
|
|
9692
10094
|
cwe: "CWE-78",
|
|
9693
10095
|
severity: "critical",
|
|
9694
10096
|
languages: ["javascript", "typescript"],
|
|
9695
|
-
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)
|
|
10097
|
+
pattern: /\b(?:exec|execSync|spawn|spawnSync)\s*\(\s*(?:`[^`]*\$\{|['"][^'"]*['"]\s*\+|[a-zA-Z_$][\w$]*\s*\+)/,
|
|
10098
|
+
constantInterpolationGuard: true
|
|
9696
10099
|
},
|
|
9697
10100
|
{
|
|
9698
10101
|
id: "py-shell-command-string",
|
|
@@ -10054,6 +10457,20 @@ var CODE_RULES = [
|
|
|
10054
10457
|
consequence: "Catastrophic backtracking: a crafted input of a few dozen characters pins a CPU core for minutes.",
|
|
10055
10458
|
cwe: "CWE-1333",
|
|
10056
10459
|
severity: "medium",
|
|
10460
|
+
// Scoped, because unscoped this rule reads C as if it were a regex.
|
|
10461
|
+
//
|
|
10462
|
+
// `(void *)*memptr64` — a cast to a pointer type, then a dereference — is
|
|
10463
|
+
// the single most ordinary line in a C file, and it matches the first
|
|
10464
|
+
// alternative exactly: `(`, some text, `*`, `)`, `*`. Every `*(char *)*p`
|
|
10465
|
+
// in a codebase became a ReDoS finding. Caught on inspektor-gadget, where
|
|
10466
|
+
// the rule's one and only hit across 1186 files was a `bpf_probe_read_user`
|
|
10467
|
+
// call in an eBPF C program.
|
|
10468
|
+
//
|
|
10469
|
+
// Scoping is the fix rather than a cleverer pattern: C has no regex
|
|
10470
|
+
// literals, so there is nothing here for the rule to find no matter how
|
|
10471
|
+
// the pattern is written. `other` also covers C++, Rust and Zig, which
|
|
10472
|
+
// share the cast-then-deref spelling.
|
|
10473
|
+
languages: ["javascript", "typescript", "python", "ruby", "go", "java", "php"],
|
|
10057
10474
|
pattern: /\([^)\n]*[+*]\s*\)\s*[+*]|\([^)\n]*\{\d+,\}\s*\)\s*[+*{]/
|
|
10058
10475
|
},
|
|
10059
10476
|
// ── Temporary files ──────────────────────────────────────────────────────
|
|
@@ -10577,8 +10994,27 @@ var CODE_RULES = [
|
|
|
10577
10994
|
cwe: "CWE-346",
|
|
10578
10995
|
severity: "high",
|
|
10579
10996
|
languages: ["javascript", "typescript"],
|
|
10580
|
-
pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b
|
|
10581
|
-
|
|
10997
|
+
pattern: /\$\{[^}\n]*\breq(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname|host)\b|['"`]\s*\+\s*req(?:uest)?\s*\.\s*(?:headers\s*\.\s*host|hostname)\b/,
|
|
10998
|
+
/**
|
|
10999
|
+
* Parsing the request's own URL is not building a link from the Host
|
|
11000
|
+
* header, even though it is spelled with one.
|
|
11001
|
+
*
|
|
11002
|
+
* const url = new URL(request.url, `http://${request.headers.host}`);
|
|
11003
|
+
* const token = url.searchParams.get('token');
|
|
11004
|
+
*
|
|
11005
|
+
* `request.url` on a Node server is a path — `/ws?token=…` — and `new URL`
|
|
11006
|
+
* refuses a relative input without a base. The base exists to satisfy the
|
|
11007
|
+
* parser and is thrown away; only the path and query are ever read. Every
|
|
11008
|
+
* Node HTTP handler that wants a query parameter is written this way, so
|
|
11009
|
+
* the rule fired on the framework idiom rather than on the defect.
|
|
11010
|
+
*
|
|
11011
|
+
* Narrow on purpose: the first argument must be `req.url` itself. The
|
|
11012
|
+
* dangerous shape passes a *path* the application chose —
|
|
11013
|
+
* `new URL('/reset?t=…', `https://${req.headers.host}`)` — and that is what
|
|
11014
|
+
* produces an attacker-controlled link. It does not match this guard.
|
|
11015
|
+
*/
|
|
11016
|
+
lineGuard: /\bnew\s+URL\s*\(\s*(?:req|request|ctx)(?:uest)?\s*\.\s*url\b\s*,/
|
|
11017
|
+
},
|
|
10582
11018
|
// A recursive-merge prototype-pollution rule (`target[key] = source[key]`
|
|
10583
11019
|
// with no `__proto__` guard) was built and dropped. The bare copy-by-key is
|
|
10584
11020
|
// the safe allow-listed shape (`updates[field] = body[field]` over an
|
|
@@ -10588,6 +11024,11 @@ var CODE_RULES = [
|
|
|
10588
11024
|
// answer. It flagged legitimate merges in Capacitor and in this repo's own
|
|
10589
11025
|
// web app. `js-prototype-pollution` still catches the explicit `__proto__`
|
|
10590
11026
|
// literal; the recursive-merge case is left to KNOWN_GAPS.
|
|
11027
|
+
// Node-ecosystem classes — vm2, Electron, JWT, archive extraction, headless
|
|
11028
|
+
// browsers — are kept in their own table because each has to know which
|
|
11029
|
+
// package it is looking at before it can claim anything. Folded in here so
|
|
11030
|
+
// there stays exactly one rule list for every consumer to iterate.
|
|
11031
|
+
...NODE_RULES
|
|
10591
11032
|
];
|
|
10592
11033
|
var COMMENT_PREFIX = /^\s*(?:\/\/|\/\*|\*|#|--|<!--)/;
|
|
10593
11034
|
var DEFINITION_PREFIX = /^\s*(?:(?:export|public|private|protected|static|final|async|abstract)\s+)*(?:def|function|func|class|module|interface|struct|impl|fn|sub)\b/;
|
|
@@ -10640,6 +11081,55 @@ function fileTextOf(lines) {
|
|
|
10640
11081
|
function withoutSingleQuoted(text) {
|
|
10641
11082
|
return text.replace(/'[^'\n]*'/g, "''");
|
|
10642
11083
|
}
|
|
11084
|
+
function calleeEndingAt(text, open) {
|
|
11085
|
+
let end = open;
|
|
11086
|
+
while (end > 0 && (text[end - 1] === " " || text[end - 1] === " ")) end -= 1;
|
|
11087
|
+
let start = end;
|
|
11088
|
+
while (start > 0 && /[\w$.]/.test(text[start - 1])) start -= 1;
|
|
11089
|
+
return text.slice(start, end);
|
|
11090
|
+
}
|
|
11091
|
+
function enclosingCallees(lines, index, back) {
|
|
11092
|
+
const before = lines.slice(Math.max(0, index - back), index).join("\n");
|
|
11093
|
+
const stack = [];
|
|
11094
|
+
let quote = null;
|
|
11095
|
+
for (let i = 0; i < before.length; i += 1) {
|
|
11096
|
+
const ch = before[i];
|
|
11097
|
+
if (quote) {
|
|
11098
|
+
if (ch === "\\") i += 1;
|
|
11099
|
+
else if (ch === quote) quote = null;
|
|
11100
|
+
continue;
|
|
11101
|
+
}
|
|
11102
|
+
if (ch === '"' || ch === "'" || ch === "`") quote = ch;
|
|
11103
|
+
else if (ch === "(") stack.push(calleeEndingAt(before, i));
|
|
11104
|
+
else if (ch === ")") stack.pop();
|
|
11105
|
+
}
|
|
11106
|
+
return stack;
|
|
11107
|
+
}
|
|
11108
|
+
function interpolations(line) {
|
|
11109
|
+
const found = [];
|
|
11110
|
+
for (let i = 0; ; ) {
|
|
11111
|
+
const start = line.indexOf("${", i);
|
|
11112
|
+
if (start === -1) break;
|
|
11113
|
+
const end = line.indexOf("}", start + 2);
|
|
11114
|
+
if (end === -1) return null;
|
|
11115
|
+
found.push(line.slice(start + 2, end).trim());
|
|
11116
|
+
i = end + 1;
|
|
11117
|
+
}
|
|
11118
|
+
return found.length > 0 ? found : null;
|
|
11119
|
+
}
|
|
11120
|
+
function isConstantString(name, fileText) {
|
|
11121
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
11122
|
+
return new RegExp(
|
|
11123
|
+
`\\bconst\\s+${escaped}\\s*(?::[^=\\n]+)?=\\s*(?:'[^'\\n]*'|"[^"\\n]*"|\`[^\`$\\n]*\`)`
|
|
11124
|
+
).test(fileText);
|
|
11125
|
+
}
|
|
11126
|
+
function interpolationsAreConstant(line, fileText) {
|
|
11127
|
+
const found = interpolations(line);
|
|
11128
|
+
if (!found) return false;
|
|
11129
|
+
return found.every(
|
|
11130
|
+
(expr) => /^[A-Za-z_$][\w$]*$/.test(expr) && isConstantString(expr, fileText)
|
|
11131
|
+
);
|
|
11132
|
+
}
|
|
10643
11133
|
function evaluateRule(rule, ctx) {
|
|
10644
11134
|
if (rule.languages && !rule.languages.includes(ctx.language)) return null;
|
|
10645
11135
|
const line = ctx.lines[ctx.index] ?? "";
|
|
@@ -10651,6 +11141,13 @@ function evaluateRule(rule, ctx) {
|
|
|
10651
11141
|
const context = windowText(ctx.lines, ctx.index, back, forward, ctx.prose);
|
|
10652
11142
|
if (rule.requires && !rule.requires.test(context)) return null;
|
|
10653
11143
|
if (rule.lineGuard?.test(line)) return null;
|
|
11144
|
+
if (rule.enclosingCallGuard) {
|
|
11145
|
+
const callees = enclosingCallees(ctx.lines, ctx.index, back);
|
|
11146
|
+
if (callees.some((callee) => rule.enclosingCallGuard.test(callee))) return null;
|
|
11147
|
+
}
|
|
11148
|
+
if (rule.constantInterpolationGuard && interpolationsAreConstant(line, fileTextOf(ctx.lines))) {
|
|
11149
|
+
return null;
|
|
11150
|
+
}
|
|
10654
11151
|
const guard = rule.guard === void 0 ? GENERIC_GUARD : rule.guard;
|
|
10655
11152
|
if (guard && (guard.test(line) || guard.test(context))) return null;
|
|
10656
11153
|
const untrusted = untrustedPatternFor(ctx.language);
|
|
@@ -10662,6 +11159,188 @@ function evaluateRule(rule, ctx) {
|
|
|
10662
11159
|
return { rule, confidence, severity: severityFor(rule.severity, confidence) };
|
|
10663
11160
|
}
|
|
10664
11161
|
|
|
11162
|
+
// ../../packages/scan/src/template-rules.ts
|
|
11163
|
+
var LAYOUT_SLOT = /\{\{\{\s*(?:body|content|outlet|children)\s*\}\}\}/;
|
|
11164
|
+
var XSS_CONSEQUENCE = "The value is written into the page as markup rather than as text, so a `<script>` or an `onerror=` attribute in it executes in the visitor\u2019s session \u2014 with their cookies, and their privileges.";
|
|
11165
|
+
var TEMPLATE_RULES = [
|
|
11166
|
+
{
|
|
11167
|
+
id: "tpl-handlebars-unescaped",
|
|
11168
|
+
title: "Handlebars/Mustache interpolation that skips escaping",
|
|
11169
|
+
consequence: XSS_CONSEQUENCE,
|
|
11170
|
+
cwe: "CWE-79",
|
|
11171
|
+
severity: "high",
|
|
11172
|
+
extensions: [".hbs", ".handlebars", ".hdbs", ".mustache", ".ms"],
|
|
11173
|
+
// Two spellings of the same opt-out: the triple-stache and the `&` prefix.
|
|
11174
|
+
pattern: /\{\{\{(?!\{)[^}]*\}\}\}|\{\{\s*&\s*[^}]+\}\}/,
|
|
11175
|
+
lineGuard: LAYOUT_SLOT
|
|
11176
|
+
},
|
|
11177
|
+
{
|
|
11178
|
+
id: "tpl-vue-v-html",
|
|
11179
|
+
title: "`v-html` binding",
|
|
11180
|
+
consequence: XSS_CONSEQUENCE,
|
|
11181
|
+
cwe: "CWE-79",
|
|
11182
|
+
severity: "high",
|
|
11183
|
+
extensions: [".vue", ".html", ".htm"],
|
|
11184
|
+
pattern: /\bv-html\s*=|\s:inner-html\.prop\s*=/
|
|
11185
|
+
},
|
|
11186
|
+
{
|
|
11187
|
+
id: "tpl-pug-unescaped",
|
|
11188
|
+
title: "Pug/Jade unescaped interpolation",
|
|
11189
|
+
consequence: XSS_CONSEQUENCE,
|
|
11190
|
+
cwe: "CWE-79",
|
|
11191
|
+
severity: "high",
|
|
11192
|
+
extensions: [".pug", ".jade"],
|
|
11193
|
+
// `!{…}` is unescaped interpolation anywhere on the line. `!=` is
|
|
11194
|
+
// unescaped buffered code, but only in the tag position — anchored to the
|
|
11195
|
+
// start of the line and preceded by nothing but tag, class and id
|
|
11196
|
+
// characters, so the `!==` inside `- if (a !== b)` cannot reach it.
|
|
11197
|
+
pattern: /!\{[^}\n]*\}|^\s*[\w.#%-]*!=(?!=)\s*\S/
|
|
11198
|
+
},
|
|
11199
|
+
{
|
|
11200
|
+
id: "tpl-ejs-raw-output",
|
|
11201
|
+
title: "EJS/ECT raw output tag",
|
|
11202
|
+
consequence: XSS_CONSEQUENCE,
|
|
11203
|
+
cwe: "CWE-79",
|
|
11204
|
+
severity: "high",
|
|
11205
|
+
extensions: [".ejs", ".ect"],
|
|
11206
|
+
// In EJS `<%= %>` escapes and `<%-` does not, which is the reverse of
|
|
11207
|
+
// Underscore's convention — see KNOWN_GAPS.
|
|
11208
|
+
pattern: /<%-(?!\s*(?:include|-))/,
|
|
11209
|
+
// `<%- include('partial') %>` splices another template, not user data.
|
|
11210
|
+
lineGuard: /<%-\s*(?:include|partial)\s*\(/
|
|
11211
|
+
},
|
|
11212
|
+
{
|
|
11213
|
+
id: "tpl-dust-escape-filter-off",
|
|
11214
|
+
title: "Dust reference with escaping suppressed",
|
|
11215
|
+
consequence: XSS_CONSEQUENCE,
|
|
11216
|
+
cwe: "CWE-79",
|
|
11217
|
+
severity: "high",
|
|
11218
|
+
extensions: [".dust", ".tl"],
|
|
11219
|
+
// The `|s` filter means "suppress the default HTML escape".
|
|
11220
|
+
pattern: /\{[^{}\n]+\|\s*s\s*\}/
|
|
11221
|
+
},
|
|
11222
|
+
{
|
|
11223
|
+
id: "tpl-jinja-safe-filter",
|
|
11224
|
+
title: "Nunjucks/Twig/Jinja `safe` filter or autoescape block off",
|
|
11225
|
+
consequence: XSS_CONSEQUENCE,
|
|
11226
|
+
cwe: "CWE-79",
|
|
11227
|
+
severity: "high",
|
|
11228
|
+
extensions: [".njk", ".nunjucks", ".twig", ".jinja", ".jinja2", ".j2"],
|
|
11229
|
+
pattern: /\|\s*(?:safe|raw)\s*(?:\}\}|\|)|\{%\s*autoescape\s+(?:false|off)\s*%\}/
|
|
11230
|
+
},
|
|
11231
|
+
{
|
|
11232
|
+
id: "tpl-haml-unescaped",
|
|
11233
|
+
title: "Haml unescaped output",
|
|
11234
|
+
consequence: XSS_CONSEQUENCE,
|
|
11235
|
+
cwe: "CWE-79",
|
|
11236
|
+
severity: "high",
|
|
11237
|
+
extensions: [".haml"],
|
|
11238
|
+
pattern: /^\s*[\w.#%-]*!=(?!=)\s*\S/
|
|
11239
|
+
}
|
|
11240
|
+
];
|
|
11241
|
+
var TEMPLATE_EXTENSIONS = new Set(
|
|
11242
|
+
TEMPLATE_RULES.flatMap((rule) => rule.extensions)
|
|
11243
|
+
);
|
|
11244
|
+
function evaluateTemplateRules(extension, lines) {
|
|
11245
|
+
const ext = extension.toLowerCase();
|
|
11246
|
+
const applicable = TEMPLATE_RULES.filter((rule) => rule.extensions.includes(ext));
|
|
11247
|
+
if (applicable.length === 0) return [];
|
|
11248
|
+
const matches = [];
|
|
11249
|
+
lines.forEach((line, index) => {
|
|
11250
|
+
for (const rule of applicable) {
|
|
11251
|
+
if (!rule.pattern.test(line)) continue;
|
|
11252
|
+
if (rule.lineGuard?.test(line)) continue;
|
|
11253
|
+
matches.push({
|
|
11254
|
+
rule,
|
|
11255
|
+
// A template cannot show that the value is untrusted, so the claim
|
|
11256
|
+
// never rises above `pattern` and the shared cap holds it at medium.
|
|
11257
|
+
severity: severityFor(rule.severity, "pattern"),
|
|
11258
|
+
line: index + 1
|
|
11259
|
+
});
|
|
11260
|
+
}
|
|
11261
|
+
});
|
|
11262
|
+
return matches;
|
|
11263
|
+
}
|
|
11264
|
+
|
|
11265
|
+
// ../../packages/scan/src/controls.ts
|
|
11266
|
+
var SERVER_FRAMEWORK = /\bexpress\s*\(\s*\)|\bnew\s+Koa\s*\(|\bfastify\s*\(|\brequire\s*\(\s*['"](?:express|koa|@hapi\/hapi|restify)['"]\s*\)|\bfrom\s+['"](?:express|koa|@hapi\/hapi|restify)['"]|\bhttp\s*\.\s*createServer\s*\(|\bNestFactory\s*\.\s*create\s*\(/;
|
|
11267
|
+
var SECURITY_CONTROLS = [
|
|
11268
|
+
{
|
|
11269
|
+
id: "control-security-headers-absent",
|
|
11270
|
+
title: "no security header middleware",
|
|
11271
|
+
consequence: "Without them the browser applies no framing protection, sniffs content types, sends full referrers cross-origin and never learns to require HTTPS \u2014 a set of defences that cost one line to enable.",
|
|
11272
|
+
cwe: "CWE-693",
|
|
11273
|
+
severity: "medium",
|
|
11274
|
+
evidence: /\bhelmet\b|\bkoa-helmet\b|\b@fastify\/helmet\b|\blusca\b|Strict-Transport-Security|Content-Security-Policy|X-Frame-Options|X-Content-Type-Options/i
|
|
11275
|
+
},
|
|
11276
|
+
{
|
|
11277
|
+
id: "control-anti-csrf-absent",
|
|
11278
|
+
title: "no cross-site request forgery protection",
|
|
11279
|
+
consequence: "A cookie-authenticated endpoint with no token check can be driven by a form on any other site \u2014 the browser attaches the session automatically, so the victim only has to visit a page.",
|
|
11280
|
+
cwe: "CWE-352",
|
|
11281
|
+
severity: "medium",
|
|
11282
|
+
// SameSite counts. A cookie the browser refuses to send cross-site is not
|
|
11283
|
+
// reachable by the attack this control exists to stop, so a project that
|
|
11284
|
+
// chose that route instead of tokens has the control, not a gap.
|
|
11285
|
+
evidence: /\bcsurf\b|\bcsrf\b|\bxsrf\b|\blusca\b|\b@fastify\/csrf\b|\bdouble-csrf\b|\bsameSite\s*:\s*['"](?:strict|lax)['"]/i
|
|
11286
|
+
},
|
|
11287
|
+
{
|
|
11288
|
+
id: "control-rate-limiting-absent",
|
|
11289
|
+
title: "no request rate limiting",
|
|
11290
|
+
consequence: "Login and password-reset endpoints with no limiter can be tried at the speed of the network \u2014 credential stuffing, token brute force and enumeration all become a matter of waiting.",
|
|
11291
|
+
cwe: "CWE-770",
|
|
11292
|
+
severity: "medium",
|
|
11293
|
+
evidence: /\bexpress-rate-limit\b|\brateLimit\b|\brate-limiter\b|\bratelimit\b|\bexpress-slow-down\b|\bslowDown\b|\bbottleneck\b|\bthrottle\b/i
|
|
11294
|
+
},
|
|
11295
|
+
{
|
|
11296
|
+
id: "control-body-size-limit-absent",
|
|
11297
|
+
title: "no request body size limit",
|
|
11298
|
+
consequence: "The body is parsed into memory before any handler \u2014 and before any authentication check \u2014 so unbounded parsing lets a few concurrent requests exhaust the process.",
|
|
11299
|
+
cwe: "CWE-400",
|
|
11300
|
+
severity: "medium",
|
|
11301
|
+
evidence: /\blimit\s*:\s*['"]?\d+\s*(?:kb|mb|b)\b|\bbodyLimit\b|\bmaxRequestBodySize\b|\bclient_max_body_size\b/i
|
|
11302
|
+
}
|
|
11303
|
+
];
|
|
11304
|
+
var ControlAudit = class {
|
|
11305
|
+
seen = /* @__PURE__ */ new Set();
|
|
11306
|
+
/** Where the server is built. Anchors the findings somewhere meaningful. */
|
|
11307
|
+
serverFile = null;
|
|
11308
|
+
observe(relativePath, text) {
|
|
11309
|
+
if (this.serverFile === null && SERVER_FRAMEWORK.test(text)) {
|
|
11310
|
+
this.serverFile = relativePath;
|
|
11311
|
+
}
|
|
11312
|
+
for (const control of SECURITY_CONTROLS) {
|
|
11313
|
+
if (this.seen.has(control.id)) continue;
|
|
11314
|
+
if (control.evidence.test(text)) this.seen.add(control.id);
|
|
11315
|
+
}
|
|
11316
|
+
}
|
|
11317
|
+
/** Controls with no evidence anywhere. Empty when the tree serves no HTTP. */
|
|
11318
|
+
missing() {
|
|
11319
|
+
if (this.serverFile === null) return [];
|
|
11320
|
+
return SECURITY_CONTROLS.filter((control) => !this.seen.has(control.id));
|
|
11321
|
+
}
|
|
11322
|
+
findings() {
|
|
11323
|
+
const anchor = this.serverFile;
|
|
11324
|
+
if (anchor === null) return [];
|
|
11325
|
+
return this.missing().map((control) => ({
|
|
11326
|
+
ruleId: control.id,
|
|
11327
|
+
title: control.title,
|
|
11328
|
+
file: anchor,
|
|
11329
|
+
line: 1,
|
|
11330
|
+
// `pattern` is doing real work here: it caps the severity, and it says
|
|
11331
|
+
// in the report itself how much the scanner is claiming. An absence is
|
|
11332
|
+
// never `evidence`.
|
|
11333
|
+
severity: severityFor(control.severity, "pattern"),
|
|
11334
|
+
confidence: "pattern",
|
|
11335
|
+
message: `${control.title} \u2014 no evidence of one anywhere in the scanned tree (${control.cwe})`,
|
|
11336
|
+
consequence: control.consequence,
|
|
11337
|
+
cwe: control.cwe,
|
|
11338
|
+
excerpt: "",
|
|
11339
|
+
category: "code"
|
|
11340
|
+
}));
|
|
11341
|
+
}
|
|
11342
|
+
};
|
|
11343
|
+
|
|
10665
11344
|
// ../../packages/scan/src/manifest-rules.ts
|
|
10666
11345
|
var POPULAR_NPM = [
|
|
10667
11346
|
"react",
|
|
@@ -11051,7 +11730,8 @@ var SECRET_RULES = [
|
|
|
11051
11730
|
pattern: /\beyJ[A-Za-z0-9_-]{8,}\.eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_.+/=-]*/,
|
|
11052
11731
|
severity: "medium",
|
|
11053
11732
|
cwe: "CWE-798",
|
|
11054
|
-
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived."
|
|
11733
|
+
consequence: "Session or service identity until it expires \u2014 and committed tokens are usually long-lived.",
|
|
11734
|
+
keywordShaped: true
|
|
11055
11735
|
},
|
|
11056
11736
|
{
|
|
11057
11737
|
id: "secret-generic-api-key",
|
|
@@ -11062,7 +11742,8 @@ var SECRET_RULES = [
|
|
|
11062
11742
|
pattern: /(?:api[_-]?key|apikey|access[_-]?token|auth[_-]?token)\s*[=:]\s*['"]([A-Za-z0-9\-_.]{20,})['"]/i,
|
|
11063
11743
|
severity: "high",
|
|
11064
11744
|
cwe: "CWE-798",
|
|
11065
|
-
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid."
|
|
11745
|
+
consequence: "Whatever the third-party service lets the key do, for as long as it stays valid.",
|
|
11746
|
+
keywordShaped: true
|
|
11066
11747
|
},
|
|
11067
11748
|
{
|
|
11068
11749
|
id: "secret-generic-credential",
|
|
@@ -11070,7 +11751,8 @@ var SECRET_RULES = [
|
|
|
11070
11751
|
pattern: /(?:secret|password|passwd|pwd|token)\s*[=:]\s*['"]([^'"\s]{8,})['"]/i,
|
|
11071
11752
|
severity: "high",
|
|
11072
11753
|
cwe: "CWE-798",
|
|
11073
|
-
consequence: "A password in source is a password in every clone, fork and CI cache of that source."
|
|
11754
|
+
consequence: "A password in source is a password in every clone, fork and CI cache of that source.",
|
|
11755
|
+
keywordShaped: true
|
|
11074
11756
|
},
|
|
11075
11757
|
{
|
|
11076
11758
|
id: "secret-hex-token",
|
|
@@ -11078,7 +11760,8 @@ var SECRET_RULES = [
|
|
|
11078
11760
|
pattern: /(?:token|key|secret|auth|signing)\w*\s*[=:]\s*['"]?([0-9a-f]{32,})['"]?/i,
|
|
11079
11761
|
severity: "medium",
|
|
11080
11762
|
cwe: "CWE-798",
|
|
11081
|
-
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign."
|
|
11763
|
+
consequence: "Signing secrets and session keys are usually hex; a leaked one forges anything they sign.",
|
|
11764
|
+
keywordShaped: true
|
|
11082
11765
|
}
|
|
11083
11766
|
];
|
|
11084
11767
|
var KNOWN_PLACEHOLDERS = [
|
|
@@ -11093,11 +11776,70 @@ var KNOWN_PLACEHOLDERS = [
|
|
|
11093
11776
|
/\bEXAMPLE_?KEY\b/i,
|
|
11094
11777
|
/\bYOUR_[A-Z_]*(?:KEY|TOKEN|SECRET|PASSWORD)\b/,
|
|
11095
11778
|
/\b(?:xxx+|X{4,}|\*{4,}|<[a-z-]+>)\b/,
|
|
11096
|
-
/\bchangeme\b/i
|
|
11779
|
+
/\bchangeme\b/i,
|
|
11780
|
+
// The metasyntactic pair in a connection string, `postgres://user:pass@host`.
|
|
11781
|
+
// This is not the AWS case above and the distinction is the whole reason it
|
|
11782
|
+
// is allowed: `AKIAIOSFODNN7EXAMPLE` is a real credential *format* carrying a
|
|
11783
|
+
// fake value, so it arrives by way of a pasted template. `user:pass` is the
|
|
11784
|
+
// English words sitting where a credential goes, which is how every database
|
|
11785
|
+
// driver writes its DSN in its own README, and nobody pastes that out of a
|
|
11786
|
+
// secret manager.
|
|
11787
|
+
//
|
|
11788
|
+
// Both halves have to be metasyntactic. `root:hunter2@` is not exempt, since
|
|
11789
|
+
// a real password beside a common username is the case this must not swallow.
|
|
11790
|
+
/:\/\/(?:user(?:name)?|admin|root|dbuser|myuser):(?:pass(?:word|wd)?|secret|dbpass|mypassword)@/i
|
|
11097
11791
|
];
|
|
11098
11792
|
function isKnownPlaceholder(text) {
|
|
11099
11793
|
return KNOWN_PLACEHOLDERS.some((pattern) => pattern.test(text));
|
|
11100
11794
|
}
|
|
11795
|
+
function isVariableReference(value) {
|
|
11796
|
+
const trimmed = value.trim();
|
|
11797
|
+
const braced = /^\$\{\s*([A-Za-z_][\w.]*)\s*(?::?[-=+?]([\s\S]*))?\}$/.exec(trimmed);
|
|
11798
|
+
if (braced) {
|
|
11799
|
+
const fallback2 = braced[2];
|
|
11800
|
+
if (fallback2 === void 0 || fallback2.trim() === "") return true;
|
|
11801
|
+
return /^\$\{?[A-Za-z_][\w.]*\}?$/.test(fallback2.trim());
|
|
11802
|
+
}
|
|
11803
|
+
return /^\$[A-Za-z_]\w*$/.test(trimmed) || // $VAR
|
|
11804
|
+
/^\$\([\s\S]*\)$/.test(trimmed) || // $(command substitution)
|
|
11805
|
+
/^%[A-Za-z_]\w*%$/.test(trimmed) || // %VAR% on Windows
|
|
11806
|
+
/^\{\{[\s\S]*\}\}$/.test(trimmed) || // {{ template }}
|
|
11807
|
+
/^#\{[\s\S]*\}$/.test(trimmed) || // #{ruby}
|
|
11808
|
+
/^<%=?[\s\S]*%>$/.test(trimmed);
|
|
11809
|
+
}
|
|
11810
|
+
var FIXTURE_STEMS = [
|
|
11811
|
+
"test",
|
|
11812
|
+
"mock",
|
|
11813
|
+
"fake",
|
|
11814
|
+
"dummy",
|
|
11815
|
+
"stub",
|
|
11816
|
+
"sample",
|
|
11817
|
+
"example",
|
|
11818
|
+
"placeholder",
|
|
11819
|
+
"fixture",
|
|
11820
|
+
"invalid",
|
|
11821
|
+
"expired",
|
|
11822
|
+
"forged",
|
|
11823
|
+
"bogus",
|
|
11824
|
+
"notreal",
|
|
11825
|
+
"nonexistent",
|
|
11826
|
+
"changeme",
|
|
11827
|
+
"foobar",
|
|
11828
|
+
"lorem"
|
|
11829
|
+
];
|
|
11830
|
+
var KEY_NOISE = /* @__PURE__ */ new Set(["const", "this", "return", "await", "async", "expect", "value"]);
|
|
11831
|
+
function words(text) {
|
|
11832
|
+
return text.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[^A-Za-z]+/g, " ").toLowerCase().split(" ").filter(Boolean);
|
|
11833
|
+
}
|
|
11834
|
+
function isTestFixtureValue(line, value) {
|
|
11835
|
+
const valueWords = words(value);
|
|
11836
|
+
if (valueWords.some((word) => FIXTURE_STEMS.some((stem) => word.startsWith(stem)))) return true;
|
|
11837
|
+
if (value.length > 48) return false;
|
|
11838
|
+
const valueAt = line.lastIndexOf(value);
|
|
11839
|
+
const key = valueAt === -1 ? line : line.slice(0, valueAt);
|
|
11840
|
+
const flattened = valueWords.join("");
|
|
11841
|
+
return words(key).filter((word) => word.length >= 4 && !KEY_NOISE.has(word)).some((word) => flattened.includes(word));
|
|
11842
|
+
}
|
|
11101
11843
|
function redactSecret(line) {
|
|
11102
11844
|
return line.replace(/([A-Za-z0-9+/=_.-]{12,})/g, (match) => {
|
|
11103
11845
|
if (match.length <= 12) return match;
|
|
@@ -11198,7 +11940,12 @@ var SCAN_EXTENSIONS = /* @__PURE__ */ new Set([
|
|
|
11198
11940
|
".erb",
|
|
11199
11941
|
".ejs",
|
|
11200
11942
|
".vue",
|
|
11201
|
-
".svelte"
|
|
11943
|
+
".svelte",
|
|
11944
|
+
// Template files, so the engine in `template-rules.ts` has something to
|
|
11945
|
+
// read. Sourced from the rules themselves rather than repeated here: an
|
|
11946
|
+
// engine added there becomes scannable without a second edit that could be
|
|
11947
|
+
// forgotten, which is how a rule ends up quietly never firing.
|
|
11948
|
+
...TEMPLATE_EXTENSIONS
|
|
11202
11949
|
]);
|
|
11203
11950
|
var LANGUAGE_BY_EXTENSION = {
|
|
11204
11951
|
".js": "javascript",
|
|
@@ -11270,6 +12017,10 @@ function languageOfShebang(firstLine) {
|
|
|
11270
12017
|
}
|
|
11271
12018
|
var SUPPRESS_NEXT = /threatcrush-disable-next-line(?:\s+([\w-]+))?/;
|
|
11272
12019
|
var SUPPRESS_LINE = /threatcrush-disable-line(?:\s+([\w-]+))?/;
|
|
12020
|
+
var FOREIGN_CREDENTIAL = /\b(?:nolint:[\w,]*gosec|nosec)\b[^\n]*\bG101\b|\bG101\b[^\n]*\b(?:nolint:[\w,]*gosec|nosec)\b/;
|
|
12021
|
+
function foreignCredentialMark(line) {
|
|
12022
|
+
return FOREIGN_CREDENTIAL.test(line);
|
|
12023
|
+
}
|
|
11273
12024
|
function collectSuppressions(lines) {
|
|
11274
12025
|
const byLine = /* @__PURE__ */ new Map();
|
|
11275
12026
|
let count = 0;
|
|
@@ -11307,16 +12058,21 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
11307
12058
|
if (!match) continue;
|
|
11308
12059
|
if (isKnownPlaceholder(match[0])) continue;
|
|
11309
12060
|
if (isSuppressed(suppressions, index, rule.id)) continue;
|
|
12061
|
+
const value = match[1] ?? match[0];
|
|
12062
|
+
if (isVariableReference(value)) continue;
|
|
12063
|
+
if (inTests && rule.keywordShaped && isTestFixtureValue(line, value)) continue;
|
|
12064
|
+
const marked = foreignCredentialMark(line);
|
|
11310
12065
|
findings.push({
|
|
11311
12066
|
ruleId: rule.id,
|
|
11312
12067
|
title: rule.name,
|
|
11313
12068
|
file: relativePath,
|
|
11314
12069
|
line: index + 1,
|
|
11315
|
-
// Reported but not blocking in tests — see isTestPath.
|
|
11316
|
-
|
|
12070
|
+
// Reported but not blocking in tests — see isTestPath. The same goes
|
|
12071
|
+
// for a line another linter's credential rule was already told about.
|
|
12072
|
+
severity: inTests || marked ? "low" : rule.severity,
|
|
11317
12073
|
// A matched credential format is the finding, not a proxy for one.
|
|
11318
12074
|
confidence: "evidence",
|
|
11319
|
-
message: inTests ? `Possible ${rule.name} detected in a test file \u2014 usually a fixture, still worth confirming it is not a live credential` : `Possible ${rule.name} detected`,
|
|
12075
|
+
message: inTests ? `Possible ${rule.name} detected in a test file \u2014 usually a fixture, still worth confirming it is not a live credential` : marked ? `Possible ${rule.name} detected on a line already marked as a false positive for another linter's credential rule` : `Possible ${rule.name} detected`,
|
|
11320
12076
|
consequence: rule.consequence,
|
|
11321
12077
|
cwe: rule.cwe,
|
|
11322
12078
|
excerpt: redactSecret(line.trim()).slice(0, 200),
|
|
@@ -11346,6 +12102,23 @@ function scanText(relativePath, text, language = languageOf(relativePath)) {
|
|
|
11346
12102
|
});
|
|
11347
12103
|
}
|
|
11348
12104
|
});
|
|
12105
|
+
for (const match of evaluateTemplateRules(extensionOf(relativePath), lines)) {
|
|
12106
|
+
const index = match.line - 1;
|
|
12107
|
+
if (isSuppressed(suppressions, index, match.rule.id)) continue;
|
|
12108
|
+
findings.push({
|
|
12109
|
+
ruleId: match.rule.id,
|
|
12110
|
+
title: match.rule.title,
|
|
12111
|
+
file: relativePath,
|
|
12112
|
+
line: match.line,
|
|
12113
|
+
severity: match.severity,
|
|
12114
|
+
confidence: "pattern",
|
|
12115
|
+
message: `${match.rule.title} (${match.rule.cwe})`,
|
|
12116
|
+
consequence: match.rule.consequence,
|
|
12117
|
+
cwe: match.rule.cwe,
|
|
12118
|
+
excerpt: (lines[index] ?? "").trim().slice(0, 200),
|
|
12119
|
+
category: "code"
|
|
12120
|
+
});
|
|
12121
|
+
}
|
|
11349
12122
|
return findings;
|
|
11350
12123
|
}
|
|
11351
12124
|
function scanManifest(relativePath, filename, text) {
|
|
@@ -11371,8 +12144,8 @@ function meetsFailThreshold(findings, threshold) {
|
|
|
11371
12144
|
}
|
|
11372
12145
|
|
|
11373
12146
|
// ../../packages/scan/src/node/walk.ts
|
|
11374
|
-
var
|
|
11375
|
-
var
|
|
12147
|
+
var import_node_fs6 = require("fs");
|
|
12148
|
+
var import_node_path3 = require("path");
|
|
11376
12149
|
function compileExcludes(patterns) {
|
|
11377
12150
|
const matchers = patterns.map((p) => p.trim()).filter((p) => p.length > 0 && !p.startsWith("#")).map(compilePattern);
|
|
11378
12151
|
if (matchers.length === 0) return () => false;
|
|
@@ -11443,7 +12216,7 @@ function matchPrefix(parts, segs) {
|
|
|
11443
12216
|
}
|
|
11444
12217
|
function readIgnoreFile(root) {
|
|
11445
12218
|
try {
|
|
11446
|
-
return (0,
|
|
12219
|
+
return (0, import_node_fs6.readFileSync)((0, import_node_path3.join)(root, ".threatcrushignore"), "utf-8").split("\n");
|
|
11447
12220
|
} catch {
|
|
11448
12221
|
return [];
|
|
11449
12222
|
}
|
|
@@ -11452,22 +12225,23 @@ function scanPath(targetPath, options = {}) {
|
|
|
11452
12225
|
const maxFileBytes = options.maxFileBytes ?? 1024 * 1024;
|
|
11453
12226
|
const allowed = options.categories ? new Set(options.categories) : null;
|
|
11454
12227
|
const findings = [];
|
|
12228
|
+
const controls = new ControlAudit();
|
|
11455
12229
|
const unreadable = [];
|
|
11456
12230
|
let filesScanned = 0;
|
|
11457
12231
|
let suppressed = 0;
|
|
11458
12232
|
let excluded = 0;
|
|
11459
12233
|
const rootIsDirectory = (() => {
|
|
11460
12234
|
try {
|
|
11461
|
-
return (0,
|
|
12235
|
+
return (0, import_node_fs6.statSync)(targetPath).isDirectory();
|
|
11462
12236
|
} catch {
|
|
11463
12237
|
return true;
|
|
11464
12238
|
}
|
|
11465
12239
|
})();
|
|
11466
|
-
const walkRoot = rootIsDirectory ? targetPath : (0,
|
|
12240
|
+
const walkRoot = rootIsDirectory ? targetPath : (0, import_node_path3.dirname)(targetPath);
|
|
11467
12241
|
const isExcluded = compileExcludes([...options.exclude ?? [], ...readIgnoreFile(walkRoot)]);
|
|
11468
12242
|
const scanFile = (fullPath, filename) => {
|
|
11469
12243
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
11470
|
-
const extension = (0,
|
|
12244
|
+
const extension = (0, import_node_path3.extname)(filename).toLowerCase();
|
|
11471
12245
|
const isManifest = filename === "package.json" || filename === "requirements.txt";
|
|
11472
12246
|
const scannable = SCAN_EXTENSIONS.has(extension) || filename.startsWith(".env");
|
|
11473
12247
|
const mayDeclareInterpreter = !scannable && !isManifest && extension === "";
|
|
@@ -11479,32 +12253,33 @@ function scanPath(targetPath, options = {}) {
|
|
|
11479
12253
|
let handle;
|
|
11480
12254
|
let declared = null;
|
|
11481
12255
|
try {
|
|
11482
|
-
handle = (0,
|
|
12256
|
+
handle = (0, import_node_fs6.openSync)(fullPath, "r");
|
|
11483
12257
|
} catch {
|
|
11484
12258
|
unreadable.push(relativePath);
|
|
11485
12259
|
return;
|
|
11486
12260
|
}
|
|
11487
12261
|
try {
|
|
11488
|
-
if ((0,
|
|
12262
|
+
if ((0, import_node_fs6.fstatSync)(handle).size > maxFileBytes) return;
|
|
11489
12263
|
if (mayDeclareInterpreter) {
|
|
11490
12264
|
const prefix = Buffer.alloc(128);
|
|
11491
|
-
const read = (0,
|
|
12265
|
+
const read = (0, import_node_fs6.readSync)(handle, prefix, 0, prefix.length, 0);
|
|
11492
12266
|
declared = languageOfShebang(prefix.subarray(0, read).toString("utf-8").split("\n", 1)[0] ?? "");
|
|
11493
12267
|
if (!declared) return;
|
|
11494
12268
|
}
|
|
11495
|
-
text = (0,
|
|
12269
|
+
text = (0, import_node_fs6.readFileSync)(handle, "utf-8");
|
|
11496
12270
|
} catch {
|
|
11497
12271
|
unreadable.push(relativePath);
|
|
11498
12272
|
return;
|
|
11499
12273
|
} finally {
|
|
11500
12274
|
try {
|
|
11501
|
-
(0,
|
|
12275
|
+
(0, import_node_fs6.closeSync)(handle);
|
|
11502
12276
|
} catch {
|
|
11503
12277
|
}
|
|
11504
12278
|
}
|
|
11505
12279
|
filesScanned += 1;
|
|
11506
12280
|
options.onFile?.(relativePath);
|
|
11507
12281
|
suppressed += collectSuppressions(text.split("\n")).count;
|
|
12282
|
+
controls.observe(relativePath, text);
|
|
11508
12283
|
const fileFindings = [
|
|
11509
12284
|
...scanText(relativePath, text, declared ?? languageOf(filename)),
|
|
11510
12285
|
...isManifest ? scanManifest(relativePath, filename, text) : []
|
|
@@ -11515,13 +12290,13 @@ function scanPath(targetPath, options = {}) {
|
|
|
11515
12290
|
const walk = (currentPath) => {
|
|
11516
12291
|
let entries;
|
|
11517
12292
|
try {
|
|
11518
|
-
entries = (0,
|
|
12293
|
+
entries = (0, import_node_fs6.readdirSync)(currentPath, { withFileTypes: true });
|
|
11519
12294
|
} catch {
|
|
11520
12295
|
unreadable.push(toRelative(walkRoot, currentPath));
|
|
11521
12296
|
return;
|
|
11522
12297
|
}
|
|
11523
12298
|
for (const entry of entries) {
|
|
11524
|
-
const fullPath = (0,
|
|
12299
|
+
const fullPath = (0, import_node_path3.join)(currentPath, entry.name);
|
|
11525
12300
|
const relativePath = toRelative(walkRoot, fullPath);
|
|
11526
12301
|
if (entry.isDirectory()) {
|
|
11527
12302
|
if (SKIP_DIRS.has(entry.name)) continue;
|
|
@@ -11545,8 +12320,9 @@ function scanPath(targetPath, options = {}) {
|
|
|
11545
12320
|
} else if (isExcluded(toRelative(walkRoot, targetPath))) {
|
|
11546
12321
|
excluded += 1;
|
|
11547
12322
|
} else {
|
|
11548
|
-
scanFile(targetPath, (0,
|
|
12323
|
+
scanFile(targetPath, (0, import_node_path3.basename)(targetPath));
|
|
11549
12324
|
}
|
|
12325
|
+
if (options.missingControls) findings.push(...controls.findings());
|
|
11550
12326
|
const filtered = allowed ? findings.filter((f) => allowed.has(f.category)) : findings;
|
|
11551
12327
|
filtered.sort(
|
|
11552
12328
|
(a, b) => severityRank(b.severity) - severityRank(a.severity) || a.file.localeCompare(b.file) || a.line - b.line
|
|
@@ -11576,32 +12352,37 @@ function recordSensitiveFile(filename, relativePath, sink, fileFindings) {
|
|
|
11576
12352
|
}
|
|
11577
12353
|
}
|
|
11578
12354
|
function toRelative(base, target) {
|
|
11579
|
-
const rel = (0,
|
|
11580
|
-
return (rel === "" ? target : rel).split(
|
|
12355
|
+
const rel = (0, import_node_path3.relative)(base, target);
|
|
12356
|
+
return (rel === "" ? target : rel).split(import_node_path3.sep).join("/");
|
|
11581
12357
|
}
|
|
11582
12358
|
|
|
11583
12359
|
// ../../packages/scan/src/node/dependencies.ts
|
|
11584
|
-
var
|
|
11585
|
-
var
|
|
11586
|
-
var LOCKFILES = [
|
|
11587
|
-
{ file: "package-lock.json", ecosystem: "npm" },
|
|
11588
|
-
{ file: "pnpm-lock.yaml", ecosystem: "npm" },
|
|
11589
|
-
{ file: "yarn.lock", ecosystem: "npm" },
|
|
11590
|
-
{ file: "requirements.txt", ecosystem: "PyPI" },
|
|
11591
|
-
{ file: "Pipfile.lock", ecosystem: "PyPI" }
|
|
11592
|
-
];
|
|
12360
|
+
var import_node_fs7 = require("fs");
|
|
12361
|
+
var import_node_path4 = require("path");
|
|
11593
12362
|
var MAX_DEPS_PER_LOCKFILE = 50;
|
|
11594
12363
|
async function scanDependencies(targetPath) {
|
|
11595
12364
|
const findings = [];
|
|
11596
|
-
for (const { file, ecosystem } of LOCKFILES) {
|
|
11597
|
-
const lockPath = (0,
|
|
11598
|
-
if (!(0,
|
|
12365
|
+
for (const { file, ecosystem, parse } of LOCKFILES) {
|
|
12366
|
+
const lockPath = (0, import_node_path4.join)(targetPath, file);
|
|
12367
|
+
if (!(0, import_node_fs7.existsSync)(lockPath)) continue;
|
|
11599
12368
|
let deps;
|
|
11600
12369
|
try {
|
|
11601
|
-
deps =
|
|
12370
|
+
deps = dedupe(parse((0, import_node_fs7.readFileSync)(lockPath, "utf-8")));
|
|
11602
12371
|
} catch {
|
|
11603
12372
|
continue;
|
|
11604
12373
|
}
|
|
12374
|
+
if (deps.length === 0) {
|
|
12375
|
+
findings.push(incompleteFinding(file, "No dependencies could be read from this lockfile."));
|
|
12376
|
+
continue;
|
|
12377
|
+
}
|
|
12378
|
+
if (deps.length > MAX_DEPS_PER_LOCKFILE) {
|
|
12379
|
+
findings.push(
|
|
12380
|
+
incompleteFinding(
|
|
12381
|
+
file,
|
|
12382
|
+
`Only the first ${MAX_DEPS_PER_LOCKFILE} of ${deps.length} locked packages were checked against OSV.`
|
|
12383
|
+
)
|
|
12384
|
+
);
|
|
12385
|
+
}
|
|
11605
12386
|
for (const dep of deps.slice(0, MAX_DEPS_PER_LOCKFILE)) {
|
|
11606
12387
|
let vulns;
|
|
11607
12388
|
try {
|
|
@@ -11628,6 +12409,20 @@ async function scanDependencies(targetPath) {
|
|
|
11628
12409
|
}
|
|
11629
12410
|
return findings;
|
|
11630
12411
|
}
|
|
12412
|
+
function incompleteFinding(file, message) {
|
|
12413
|
+
return {
|
|
12414
|
+
ruleId: "dependency-scan-incomplete",
|
|
12415
|
+
title: "Dependency scan incomplete",
|
|
12416
|
+
file,
|
|
12417
|
+
line: 1,
|
|
12418
|
+
severity: "low",
|
|
12419
|
+
confidence: "evidence",
|
|
12420
|
+
message,
|
|
12421
|
+
consequence: "Advisories affecting the unchecked packages would not appear in this report.",
|
|
12422
|
+
excerpt: file,
|
|
12423
|
+
category: "dependency"
|
|
12424
|
+
};
|
|
12425
|
+
}
|
|
11631
12426
|
function severityFromCvss(score) {
|
|
11632
12427
|
if (!score) return "medium";
|
|
11633
12428
|
const value = Number.parseFloat(score);
|
|
@@ -11637,26 +12432,131 @@ function severityFromCvss(score) {
|
|
|
11637
12432
|
if (value >= 4) return "medium";
|
|
11638
12433
|
return "low";
|
|
11639
12434
|
}
|
|
11640
|
-
function
|
|
12435
|
+
function dedupe(deps) {
|
|
12436
|
+
const seen = /* @__PURE__ */ new Set();
|
|
12437
|
+
const unique = [];
|
|
12438
|
+
for (const dep of deps) {
|
|
12439
|
+
const key = `${dep.name}@${dep.version}`;
|
|
12440
|
+
if (seen.has(key)) continue;
|
|
12441
|
+
seen.add(key);
|
|
12442
|
+
unique.push(dep);
|
|
12443
|
+
}
|
|
12444
|
+
return unique;
|
|
12445
|
+
}
|
|
12446
|
+
function splitNameVersion(spec) {
|
|
12447
|
+
const at = spec.lastIndexOf("@");
|
|
12448
|
+
if (at <= 0) return null;
|
|
12449
|
+
const name = spec.slice(0, at);
|
|
12450
|
+
const version = spec.slice(at + 1);
|
|
12451
|
+
if (!name || !version) return null;
|
|
12452
|
+
return { name, version };
|
|
12453
|
+
}
|
|
12454
|
+
function exactVersion(raw) {
|
|
12455
|
+
const version = raw.trim().replace(/^[=v]+/, "");
|
|
12456
|
+
return /^[0-9][0-9a-zA-Z.+-]*$/.test(version) ? version : null;
|
|
12457
|
+
}
|
|
12458
|
+
function parsePackageLock(content) {
|
|
12459
|
+
const lock = JSON.parse(content);
|
|
12460
|
+
const packages = lock.packages ?? lock.dependencies ?? {};
|
|
12461
|
+
const deps = [];
|
|
12462
|
+
for (const [key, value] of Object.entries(packages)) {
|
|
12463
|
+
const name = key.replace(/^.*node_modules\//, "");
|
|
12464
|
+
const version = value?.version;
|
|
12465
|
+
if (name && version && !name.startsWith(".")) deps.push({ name, version });
|
|
12466
|
+
}
|
|
12467
|
+
return deps;
|
|
12468
|
+
}
|
|
12469
|
+
function parsePnpmLock(content) {
|
|
12470
|
+
const deps = [];
|
|
12471
|
+
let inPackages = false;
|
|
12472
|
+
for (const line of content.split("\n")) {
|
|
12473
|
+
if (/^[a-zA-Z]/.test(line)) {
|
|
12474
|
+
inPackages = line.startsWith("packages:");
|
|
12475
|
+
continue;
|
|
12476
|
+
}
|
|
12477
|
+
if (!inPackages) continue;
|
|
12478
|
+
const match = /^ {2}(?! )(.+):\s*$/.exec(line);
|
|
12479
|
+
if (!match?.[1]) continue;
|
|
12480
|
+
let key = match[1].trim().replace(/^['"]|['"]$/g, "");
|
|
12481
|
+
key = key.replace(/^\//, "");
|
|
12482
|
+
key = key.replace(/\(.*$/, "");
|
|
12483
|
+
let name;
|
|
12484
|
+
let rawVersion;
|
|
12485
|
+
const slashed = /^(@?[^@]+)\/([0-9][^/]*)$/.exec(key);
|
|
12486
|
+
if (slashed?.[1] && slashed[2]) {
|
|
12487
|
+
name = slashed[1];
|
|
12488
|
+
rawVersion = slashed[2];
|
|
12489
|
+
} else {
|
|
12490
|
+
const dep = splitNameVersion(key);
|
|
12491
|
+
if (!dep) continue;
|
|
12492
|
+
name = dep.name;
|
|
12493
|
+
rawVersion = dep.version;
|
|
12494
|
+
}
|
|
12495
|
+
const version = exactVersion(rawVersion.replace(/_.*$/, ""));
|
|
12496
|
+
if (version) deps.push({ name, version });
|
|
12497
|
+
}
|
|
12498
|
+
return deps;
|
|
12499
|
+
}
|
|
12500
|
+
function parseYarnLock(content) {
|
|
11641
12501
|
const deps = [];
|
|
11642
|
-
|
|
11643
|
-
|
|
11644
|
-
|
|
11645
|
-
|
|
11646
|
-
|
|
11647
|
-
const
|
|
11648
|
-
|
|
12502
|
+
let pendingName = null;
|
|
12503
|
+
for (const line of content.split("\n")) {
|
|
12504
|
+
if (line.startsWith("#") || line.trim() === "") continue;
|
|
12505
|
+
if (!/^\s/.test(line)) {
|
|
12506
|
+
pendingName = null;
|
|
12507
|
+
const header = line.replace(/:\s*$/, "");
|
|
12508
|
+
const first = header.split(",")[0]?.trim().replace(/^['"]|['"]$/g, "");
|
|
12509
|
+
if (!first) continue;
|
|
12510
|
+
if (!first.includes("@") || first === "__metadata") continue;
|
|
12511
|
+
if (/@(?:workspace|file|link|portal|exec|patch):/.test(first)) continue;
|
|
12512
|
+
const dep = splitNameVersion(first.replace(/@npm:/, "@"));
|
|
12513
|
+
if (dep) pendingName = dep.name;
|
|
12514
|
+
continue;
|
|
11649
12515
|
}
|
|
11650
|
-
|
|
12516
|
+
if (!pendingName) continue;
|
|
12517
|
+
const version = /^\s+version:?\s+["']?([^"'\s]+)["']?\s*$/.exec(line);
|
|
12518
|
+
if (!version?.[1]) continue;
|
|
12519
|
+
const exact = exactVersion(version[1]);
|
|
12520
|
+
if (exact) deps.push({ name: pendingName, version: exact });
|
|
12521
|
+
pendingName = null;
|
|
11651
12522
|
}
|
|
11652
|
-
|
|
11653
|
-
|
|
11654
|
-
|
|
11655
|
-
|
|
12523
|
+
return deps;
|
|
12524
|
+
}
|
|
12525
|
+
function parsePipfileLock(content) {
|
|
12526
|
+
const lock = JSON.parse(content);
|
|
12527
|
+
const deps = [];
|
|
12528
|
+
for (const section of ["default", "develop"]) {
|
|
12529
|
+
const packages = lock[section];
|
|
12530
|
+
if (!packages || typeof packages !== "object") continue;
|
|
12531
|
+
for (const [name, value] of Object.entries(packages)) {
|
|
12532
|
+
const version = exactVersion(String(value?.version ?? "").replace(/^==/, ""));
|
|
12533
|
+
if (name && version) deps.push({ name, version });
|
|
11656
12534
|
}
|
|
11657
12535
|
}
|
|
11658
12536
|
return deps;
|
|
11659
12537
|
}
|
|
12538
|
+
function parseRequirementsTxt(content) {
|
|
12539
|
+
const deps = [];
|
|
12540
|
+
for (const raw of content.split("\n")) {
|
|
12541
|
+
const line = raw.split("#")[0]?.split(";")[0]?.trim();
|
|
12542
|
+
if (!line || line.startsWith("-")) continue;
|
|
12543
|
+
const match = /^([a-zA-Z0-9._-]+)\s*(?:\[[^\]]*\])?\s*==\s*([^\s,]+)/.exec(line);
|
|
12544
|
+
if (!match?.[1] || !match[2]) continue;
|
|
12545
|
+
const version = exactVersion(match[2]);
|
|
12546
|
+
if (version) deps.push({ name: match[1], version });
|
|
12547
|
+
}
|
|
12548
|
+
return deps;
|
|
12549
|
+
}
|
|
12550
|
+
var LOCKFILES = [
|
|
12551
|
+
{ file: "package-lock.json", ecosystem: "npm", parse: parsePackageLock },
|
|
12552
|
+
{ file: "pnpm-lock.yaml", ecosystem: "npm", parse: parsePnpmLock },
|
|
12553
|
+
{ file: "yarn.lock", ecosystem: "npm", parse: parseYarnLock },
|
|
12554
|
+
{ file: "requirements.txt", ecosystem: "PyPI", parse: parseRequirementsTxt },
|
|
12555
|
+
{ file: "Pipfile.lock", ecosystem: "PyPI", parse: parsePipfileLock }
|
|
12556
|
+
];
|
|
12557
|
+
var LOCKFILE_PARSERS = Object.fromEntries(
|
|
12558
|
+
LOCKFILES.map((entry) => [entry.file, entry.parse])
|
|
12559
|
+
);
|
|
11660
12560
|
function isValidPackageName(name) {
|
|
11661
12561
|
return /^[@a-zA-Z0-9_.\-/]{1,214}$/.test(name);
|
|
11662
12562
|
}
|
|
@@ -11681,12 +12581,12 @@ async function queryOsv(name, version, ecosystem) {
|
|
|
11681
12581
|
}
|
|
11682
12582
|
|
|
11683
12583
|
// ../../packages/scan/src/node/sarif.ts
|
|
11684
|
-
var
|
|
11685
|
-
var
|
|
12584
|
+
var import_node_crypto2 = require("crypto");
|
|
12585
|
+
var import_node_path5 = require("path");
|
|
11686
12586
|
var FINGERPRINT_KEY = "threatcrush/contentHash/v1";
|
|
11687
12587
|
function fingerprintOf(finding) {
|
|
11688
12588
|
const content = finding.excerpt.replace(/\s+/g, " ").trim();
|
|
11689
|
-
return (0,
|
|
12589
|
+
return (0, import_node_crypto2.createHash)("sha256").update(`${finding.ruleId}
|
|
11690
12590
|
${finding.file}
|
|
11691
12591
|
${content}`).digest("hex").slice(0, 32);
|
|
11692
12592
|
}
|
|
@@ -11720,11 +12620,11 @@ function securitySeverity(severity) {
|
|
|
11720
12620
|
}
|
|
11721
12621
|
}
|
|
11722
12622
|
function toArtifactUri(filePath, base, prefix = "", root = base) {
|
|
11723
|
-
const absolute = (0,
|
|
11724
|
-
const relativePath = (0,
|
|
12623
|
+
const absolute = (0, import_node_path5.isAbsolute)(filePath) ? filePath : (0, import_node_path5.resolve)(root, filePath);
|
|
12624
|
+
const relativePath = (0, import_node_path5.relative)(base, absolute);
|
|
11725
12625
|
const escapedOut = relativePath.startsWith("..") || relativePath === "";
|
|
11726
12626
|
const chosen = escapedOut ? absolute : relativePath;
|
|
11727
|
-
const posix = chosen.split(
|
|
12627
|
+
const posix = chosen.split(import_node_path5.sep).join("/").replace(/^\.\//, "");
|
|
11728
12628
|
if (!prefix || escapedOut) return posix;
|
|
11729
12629
|
const trimmed = prefix.replace(/^\/+|\/+$/g, "");
|
|
11730
12630
|
return trimmed ? `${trimmed}/${posix}` : posix;
|
|
@@ -11814,11 +12714,11 @@ ${finding.consequence}` : `**${finding.title}**`
|
|
|
11814
12714
|
// src/commands/scan.ts
|
|
11815
12715
|
function readVersion() {
|
|
11816
12716
|
for (const candidate of [
|
|
11817
|
-
(0,
|
|
11818
|
-
(0,
|
|
12717
|
+
(0, import_node_path6.join)(__dirname, "..", "package.json"),
|
|
12718
|
+
(0, import_node_path6.join)(__dirname, "..", "..", "package.json")
|
|
11819
12719
|
]) {
|
|
11820
12720
|
try {
|
|
11821
|
-
return JSON.parse((0,
|
|
12721
|
+
return JSON.parse((0, import_node_fs8.readFileSync)(candidate, "utf-8")).version ?? "0.0.0";
|
|
11822
12722
|
} catch {
|
|
11823
12723
|
}
|
|
11824
12724
|
}
|
|
@@ -11885,7 +12785,7 @@ async function scanCommand(targetPath, options = {}) {
|
|
|
11885
12785
|
const say = machineReadable ? (line) => process.stderr.write(`${line}
|
|
11886
12786
|
`) : (line) => process.stdout.write(`${line}
|
|
11887
12787
|
`);
|
|
11888
|
-
if (!(0,
|
|
12788
|
+
if (!(0, import_node_fs8.existsSync)(targetPath)) {
|
|
11889
12789
|
say(source_default.red(`Scan target does not exist: ${targetPath}`));
|
|
11890
12790
|
process.exitCode = 2;
|
|
11891
12791
|
return failedResult(targetPath, `no such path: ${targetPath}`);
|
|
@@ -11901,6 +12801,7 @@ async function scanCommand(targetPath, options = {}) {
|
|
|
11901
12801
|
let seen = 0;
|
|
11902
12802
|
const report = scanPath(targetPath, {
|
|
11903
12803
|
exclude: options.exclude,
|
|
12804
|
+
missingControls: options.missingControls,
|
|
11904
12805
|
onFile: () => {
|
|
11905
12806
|
seen += 1;
|
|
11906
12807
|
if (spinner) spinner.text = `Scanning files... (${seen} files)`;
|
|
@@ -11980,7 +12881,7 @@ function emitMachineReadable(format, outcome, targetPath, options, say) {
|
|
|
11980
12881
|
// and it fails silently. `--path-prefix` covers the remaining case:
|
|
11981
12882
|
// a scan run from inside the subdirectory it is scanning.
|
|
11982
12883
|
base: process.cwd(),
|
|
11983
|
-
root: (0,
|
|
12884
|
+
root: (0, import_node_path6.resolve)(outcome.root)
|
|
11984
12885
|
}) : {
|
|
11985
12886
|
tool: "threatcrush",
|
|
11986
12887
|
version: PKG_VERSION,
|
|
@@ -11994,8 +12895,8 @@ function emitMachineReadable(format, outcome, targetPath, options, say) {
|
|
|
11994
12895
|
const serialized = `${JSON.stringify(payload, null, 2)}
|
|
11995
12896
|
`;
|
|
11996
12897
|
if (options.output) {
|
|
11997
|
-
(0,
|
|
11998
|
-
(0,
|
|
12898
|
+
(0, import_node_fs8.mkdirSync)((0, import_node_path6.dirname)((0, import_node_path6.resolve)(options.output)), { recursive: true });
|
|
12899
|
+
(0, import_node_fs8.writeFileSync)(options.output, serialized, "utf-8");
|
|
11999
12900
|
say(
|
|
12000
12901
|
source_default.gray(
|
|
12001
12902
|
` ${format.toUpperCase()} written to ${options.output} (${outcome.findings.length} finding(s))`
|
|
@@ -12047,13 +12948,13 @@ function printHuman(outcome) {
|
|
|
12047
12948
|
}
|
|
12048
12949
|
|
|
12049
12950
|
// src/commands/init.ts
|
|
12050
|
-
var
|
|
12951
|
+
var import_node_fs11 = require("fs");
|
|
12051
12952
|
var import_node_child_process = require("child_process");
|
|
12052
12953
|
var import_node_readline3 = __toESM(require("readline"));
|
|
12053
12954
|
|
|
12054
12955
|
// src/core/config.ts
|
|
12055
|
-
var
|
|
12056
|
-
var
|
|
12956
|
+
var import_node_fs9 = require("fs");
|
|
12957
|
+
var import_node_path7 = require("path");
|
|
12057
12958
|
var import_toml = __toESM(require_toml());
|
|
12058
12959
|
var DEFAULT_CONFIG_PATH = "/etc/threatcrush/threatcrushd.conf";
|
|
12059
12960
|
var DEFAULT_CONFDIR = "/etc/threatcrush/threatcrushd.conf.d";
|
|
@@ -12079,11 +12980,11 @@ var DEFAULT_CONFIG = {
|
|
|
12079
12980
|
};
|
|
12080
12981
|
function loadConfig(configPath) {
|
|
12081
12982
|
const path = configPath || DEFAULT_CONFIG_PATH;
|
|
12082
|
-
if (!(0,
|
|
12983
|
+
if (!(0, import_node_fs9.existsSync)(path)) {
|
|
12083
12984
|
return { ...DEFAULT_CONFIG };
|
|
12084
12985
|
}
|
|
12085
12986
|
try {
|
|
12086
|
-
const raw = (0,
|
|
12987
|
+
const raw = (0, import_node_fs9.readFileSync)(path, "utf-8");
|
|
12087
12988
|
const parsed = import_toml.default.parse(raw);
|
|
12088
12989
|
return {
|
|
12089
12990
|
daemon: { ...DEFAULT_CONFIG.daemon, ...parsed.daemon },
|
|
@@ -12099,13 +13000,13 @@ function loadConfig(configPath) {
|
|
|
12099
13000
|
function loadModuleConfigs(confDir) {
|
|
12100
13001
|
const dir = confDir || DEFAULT_CONFDIR;
|
|
12101
13002
|
const configs = /* @__PURE__ */ new Map();
|
|
12102
|
-
if (!(0,
|
|
13003
|
+
if (!(0, import_node_fs9.existsSync)(dir)) {
|
|
12103
13004
|
return configs;
|
|
12104
13005
|
}
|
|
12105
|
-
const files = (0,
|
|
13006
|
+
const files = (0, import_node_fs9.readdirSync)(dir).filter((f) => f.endsWith(".conf"));
|
|
12106
13007
|
for (const file of files) {
|
|
12107
13008
|
try {
|
|
12108
|
-
const raw = (0,
|
|
13009
|
+
const raw = (0, import_node_fs9.readFileSync)((0, import_node_path7.join)(dir, file), "utf-8");
|
|
12109
13010
|
const parsed = import_toml.default.parse(raw);
|
|
12110
13011
|
for (const [name, config] of Object.entries(parsed)) {
|
|
12111
13012
|
configs.set(name, config);
|
|
@@ -12134,23 +13035,23 @@ function generateModuleConfig(moduleName, defaults = {}) {
|
|
|
12134
13035
|
}
|
|
12135
13036
|
|
|
12136
13037
|
// src/core/cli-config.ts
|
|
12137
|
-
var
|
|
12138
|
-
var
|
|
13038
|
+
var import_node_fs10 = require("fs");
|
|
13039
|
+
var import_node_path8 = require("path");
|
|
12139
13040
|
var import_node_os4 = require("os");
|
|
12140
|
-
var CLI_CONFIG_DIR = (0,
|
|
12141
|
-
var CLI_CONFIG_PATH = (0,
|
|
13041
|
+
var CLI_CONFIG_DIR = (0, import_node_path8.join)((0, import_node_os4.homedir)(), ".threatcrush");
|
|
13042
|
+
var CLI_CONFIG_PATH = (0, import_node_path8.join)(CLI_CONFIG_DIR, "config.json");
|
|
12142
13043
|
function readCliConfig() {
|
|
12143
13044
|
try {
|
|
12144
|
-
return JSON.parse((0,
|
|
13045
|
+
return JSON.parse((0, import_node_fs10.readFileSync)(CLI_CONFIG_PATH, "utf-8"));
|
|
12145
13046
|
} catch {
|
|
12146
13047
|
return {};
|
|
12147
13048
|
}
|
|
12148
13049
|
}
|
|
12149
13050
|
function writeCliConfig(config) {
|
|
12150
|
-
if (!(0,
|
|
12151
|
-
(0,
|
|
13051
|
+
if (!(0, import_node_fs10.existsSync)(CLI_CONFIG_DIR)) (0, import_node_fs10.mkdirSync)(CLI_CONFIG_DIR, { recursive: true });
|
|
13052
|
+
(0, import_node_fs10.writeFileSync)(CLI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
|
|
12152
13053
|
try {
|
|
12153
|
-
(0,
|
|
13054
|
+
(0, import_node_fs10.chmodSync)(CLI_CONFIG_PATH, 384);
|
|
12154
13055
|
} catch {
|
|
12155
13056
|
}
|
|
12156
13057
|
}
|
|
@@ -12395,7 +13296,7 @@ function binaryExists(name) {
|
|
|
12395
13296
|
}
|
|
12396
13297
|
}
|
|
12397
13298
|
function findLogPath(paths) {
|
|
12398
|
-
return paths.find((p) => (0,
|
|
13299
|
+
return paths.find((p) => (0, import_node_fs11.existsSync)(p));
|
|
12399
13300
|
}
|
|
12400
13301
|
async function promptYesNo(question, fallback2) {
|
|
12401
13302
|
const rl = import_node_readline3.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -12511,11 +13412,11 @@ async function initCommand() {
|
|
|
12511
13412
|
}
|
|
12512
13413
|
} else {
|
|
12513
13414
|
const spinner2 = ora({ text: "Writing configuration files...", color: "green" }).start();
|
|
12514
|
-
(0,
|
|
12515
|
-
(0,
|
|
12516
|
-
(0,
|
|
13415
|
+
(0, import_node_fs11.mkdirSync)(confDDir, { recursive: true });
|
|
13416
|
+
(0, import_node_fs11.mkdirSync)("/var/log/threatcrush", { recursive: true });
|
|
13417
|
+
(0, import_node_fs11.mkdirSync)("/var/lib/threatcrush", { recursive: true });
|
|
12517
13418
|
const mainConfig = generateDefaultConfig(detected.map((d) => d.name));
|
|
12518
|
-
(0,
|
|
13419
|
+
(0, import_node_fs11.writeFileSync)(`${configDir}/threatcrushd.conf`, mainConfig);
|
|
12519
13420
|
for (const svc of detected) {
|
|
12520
13421
|
const svcDef = SERVICES_TO_DETECT.find((s) => s.name === svc.name);
|
|
12521
13422
|
if (!svcDef) continue;
|
|
@@ -12524,7 +13425,7 @@ async function initCommand() {
|
|
|
12524
13425
|
...svcDef.moduleConfig,
|
|
12525
13426
|
log_path: svc.logPath || svcDef.logPaths[0]
|
|
12526
13427
|
});
|
|
12527
|
-
(0,
|
|
13428
|
+
(0, import_node_fs11.writeFileSync)(`${confDDir}/${modName}.conf`, modConfig);
|
|
12528
13429
|
}
|
|
12529
13430
|
spinner2.succeed("Configuration written successfully");
|
|
12530
13431
|
console.log();
|
|
@@ -12542,8 +13443,8 @@ async function initCommand() {
|
|
|
12542
13443
|
}
|
|
12543
13444
|
function checkWriteAccess(dir) {
|
|
12544
13445
|
try {
|
|
12545
|
-
if (!(0,
|
|
12546
|
-
(0,
|
|
13446
|
+
if (!(0, import_node_fs11.existsSync)(dir)) {
|
|
13447
|
+
(0, import_node_fs11.mkdirSync)(dir, { recursive: true });
|
|
12547
13448
|
}
|
|
12548
13449
|
return true;
|
|
12549
13450
|
} catch {
|
|
@@ -12552,8 +13453,8 @@ function checkWriteAccess(dir) {
|
|
|
12552
13453
|
}
|
|
12553
13454
|
|
|
12554
13455
|
// src/core/module-loader.ts
|
|
12555
|
-
var
|
|
12556
|
-
var
|
|
13456
|
+
var import_node_fs12 = require("fs");
|
|
13457
|
+
var import_node_path9 = require("path");
|
|
12557
13458
|
var import_toml2 = __toESM(require_toml());
|
|
12558
13459
|
init_paths();
|
|
12559
13460
|
function discoverModules(moduleDir, confDir) {
|
|
@@ -12561,22 +13462,22 @@ function discoverModules(moduleDir, confDir) {
|
|
|
12561
13462
|
const configs = loadModuleConfigs(confDir || PATHS.confD);
|
|
12562
13463
|
const searchPaths = [
|
|
12563
13464
|
moduleDir || PATHS.moduleDir,
|
|
12564
|
-
(0,
|
|
13465
|
+
(0, import_node_path9.resolve)(process.cwd(), "modules")
|
|
12565
13466
|
];
|
|
12566
|
-
const builtinDir = (0,
|
|
12567
|
-
if ((0,
|
|
13467
|
+
const builtinDir = (0, import_node_path9.resolve)(__dirname || ".", "..", "modules");
|
|
13468
|
+
if ((0, import_node_fs12.existsSync)(builtinDir)) {
|
|
12568
13469
|
searchPaths.push(builtinDir);
|
|
12569
13470
|
}
|
|
12570
13471
|
for (const basePath of searchPaths) {
|
|
12571
|
-
if (!(0,
|
|
12572
|
-
const entries = (0,
|
|
13472
|
+
if (!(0, import_node_fs12.existsSync)(basePath)) continue;
|
|
13473
|
+
const entries = (0, import_node_fs12.readdirSync)(basePath, { withFileTypes: true });
|
|
12573
13474
|
for (const entry of entries) {
|
|
12574
13475
|
if (!entry.isDirectory()) continue;
|
|
12575
|
-
const modPath = (0,
|
|
12576
|
-
const manifestPath = (0,
|
|
12577
|
-
if (!(0,
|
|
13476
|
+
const modPath = (0, import_node_path9.join)(basePath, entry.name);
|
|
13477
|
+
const manifestPath = (0, import_node_path9.join)(modPath, "mod.toml");
|
|
13478
|
+
if (!(0, import_node_fs12.existsSync)(manifestPath)) continue;
|
|
12578
13479
|
try {
|
|
12579
|
-
const raw = (0,
|
|
13480
|
+
const raw = (0, import_node_fs12.readFileSync)(manifestPath, "utf-8");
|
|
12580
13481
|
const manifest = import_toml2.default.parse(raw);
|
|
12581
13482
|
const config = configs.get(manifest.module.name) || { enabled: true };
|
|
12582
13483
|
modules.push({
|
|
@@ -12695,11 +13596,163 @@ function formatUptime(seconds) {
|
|
|
12695
13596
|
|
|
12696
13597
|
// src/commands/modules.ts
|
|
12697
13598
|
var import_node_child_process2 = require("child_process");
|
|
12698
|
-
var
|
|
12699
|
-
var
|
|
13599
|
+
var import_node_fs14 = require("fs");
|
|
13600
|
+
var import_node_path11 = require("path");
|
|
12700
13601
|
var import_toml3 = __toESM(require_toml());
|
|
12701
13602
|
init_paths();
|
|
12702
13603
|
init_pidfile();
|
|
13604
|
+
|
|
13605
|
+
// src/daemon/module-trust.ts
|
|
13606
|
+
var import_node_crypto3 = require("crypto");
|
|
13607
|
+
var import_node_fs13 = require("fs");
|
|
13608
|
+
var import_node_path10 = require("path");
|
|
13609
|
+
init_paths();
|
|
13610
|
+
var TRUST_FILE = (0, import_node_path10.join)(PATHS.configDir, "trusted-modules.json");
|
|
13611
|
+
var PUBLISHER_KEYS_FILE = (0, import_node_path10.join)(PATHS.configDir, "publisher-keys.json");
|
|
13612
|
+
var DIGEST_EXCLUDED_DIRS = /* @__PURE__ */ new Set([".git"]);
|
|
13613
|
+
function collectFiles(root, dir = root, out = []) {
|
|
13614
|
+
for (const entry of (0, import_node_fs13.readdirSync)(dir, { withFileTypes: true }).sort(
|
|
13615
|
+
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
13616
|
+
)) {
|
|
13617
|
+
const full = (0, import_node_path10.join)(dir, entry.name);
|
|
13618
|
+
if (entry.isSymbolicLink()) {
|
|
13619
|
+
out.push({ path: full, isSymlink: true });
|
|
13620
|
+
} else if (entry.isDirectory()) {
|
|
13621
|
+
if (DIGEST_EXCLUDED_DIRS.has(entry.name)) continue;
|
|
13622
|
+
collectFiles(root, full, out);
|
|
13623
|
+
} else if (entry.isFile()) {
|
|
13624
|
+
out.push({ path: full, isSymlink: false });
|
|
13625
|
+
}
|
|
13626
|
+
}
|
|
13627
|
+
return out;
|
|
13628
|
+
}
|
|
13629
|
+
function computeModuleDigest(modulePath) {
|
|
13630
|
+
const hash = (0, import_node_crypto3.createHash)("sha256");
|
|
13631
|
+
for (const entry of collectFiles(modulePath)) {
|
|
13632
|
+
const relPath = (0, import_node_path10.relative)(modulePath, entry.path).split(import_node_path10.sep).join("/");
|
|
13633
|
+
hash.update(relPath);
|
|
13634
|
+
hash.update("\0");
|
|
13635
|
+
if (entry.isSymlink) {
|
|
13636
|
+
hash.update("symlink:");
|
|
13637
|
+
hash.update((0, import_node_fs13.readlinkSync)(entry.path));
|
|
13638
|
+
} else {
|
|
13639
|
+
hash.update("file:");
|
|
13640
|
+
hash.update((0, import_node_fs13.readFileSync)(entry.path));
|
|
13641
|
+
}
|
|
13642
|
+
hash.update("\0");
|
|
13643
|
+
}
|
|
13644
|
+
return hash.digest("hex");
|
|
13645
|
+
}
|
|
13646
|
+
function readTrustFile() {
|
|
13647
|
+
if (!(0, import_node_fs13.existsSync)(TRUST_FILE)) return { version: 1, modules: {} };
|
|
13648
|
+
try {
|
|
13649
|
+
const parsed = JSON.parse((0, import_node_fs13.readFileSync)(TRUST_FILE, "utf-8"));
|
|
13650
|
+
if (parsed.version !== 1 || typeof parsed.modules !== "object" || !parsed.modules) {
|
|
13651
|
+
return { version: 1, modules: {} };
|
|
13652
|
+
}
|
|
13653
|
+
return { version: 1, modules: parsed.modules };
|
|
13654
|
+
} catch {
|
|
13655
|
+
return { version: 1, modules: {} };
|
|
13656
|
+
}
|
|
13657
|
+
}
|
|
13658
|
+
function writeTrustFile(file) {
|
|
13659
|
+
(0, import_node_fs13.writeFileSync)(TRUST_FILE, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
|
|
13660
|
+
try {
|
|
13661
|
+
(0, import_node_fs13.chmodSync)(TRUST_FILE, 384);
|
|
13662
|
+
} catch {
|
|
13663
|
+
}
|
|
13664
|
+
}
|
|
13665
|
+
function trustModule(name, modulePath, source) {
|
|
13666
|
+
const file = readTrustFile();
|
|
13667
|
+
const record = {
|
|
13668
|
+
digest: computeModuleDigest(modulePath),
|
|
13669
|
+
trustedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
13670
|
+
source
|
|
13671
|
+
};
|
|
13672
|
+
file.modules[name] = record;
|
|
13673
|
+
writeTrustFile(file);
|
|
13674
|
+
return record;
|
|
13675
|
+
}
|
|
13676
|
+
function untrustModule(name) {
|
|
13677
|
+
const file = readTrustFile();
|
|
13678
|
+
if (!file.modules[name]) return false;
|
|
13679
|
+
delete file.modules[name];
|
|
13680
|
+
writeTrustFile(file);
|
|
13681
|
+
return true;
|
|
13682
|
+
}
|
|
13683
|
+
function listTrustedModules() {
|
|
13684
|
+
return readTrustFile().modules;
|
|
13685
|
+
}
|
|
13686
|
+
function readPublisherKeys() {
|
|
13687
|
+
if (!(0, import_node_fs13.existsSync)(PUBLISHER_KEYS_FILE)) return {};
|
|
13688
|
+
try {
|
|
13689
|
+
const parsed = JSON.parse((0, import_node_fs13.readFileSync)(PUBLISHER_KEYS_FILE, "utf-8"));
|
|
13690
|
+
return parsed && typeof parsed === "object" ? parsed : {};
|
|
13691
|
+
} catch {
|
|
13692
|
+
return {};
|
|
13693
|
+
}
|
|
13694
|
+
}
|
|
13695
|
+
function verifyModuleSignature(modulePath, digest) {
|
|
13696
|
+
const keys = readPublisherKeys();
|
|
13697
|
+
const pinnedKeyIds = Object.keys(keys);
|
|
13698
|
+
const sigPath = (0, import_node_path10.join)(modulePath, "mod.sig");
|
|
13699
|
+
if (pinnedKeyIds.length === 0) {
|
|
13700
|
+
return { ok: true };
|
|
13701
|
+
}
|
|
13702
|
+
if (!(0, import_node_fs13.existsSync)(sigPath)) {
|
|
13703
|
+
return { ok: false, reason: "publisher keys are pinned but the module ships no mod.sig" };
|
|
13704
|
+
}
|
|
13705
|
+
let parsed;
|
|
13706
|
+
try {
|
|
13707
|
+
parsed = JSON.parse((0, import_node_fs13.readFileSync)(sigPath, "utf-8"));
|
|
13708
|
+
} catch {
|
|
13709
|
+
return { ok: false, reason: "mod.sig is not valid JSON" };
|
|
13710
|
+
}
|
|
13711
|
+
if (!parsed.keyId || !parsed.signature) {
|
|
13712
|
+
return { ok: false, reason: "mod.sig is missing keyId or signature" };
|
|
13713
|
+
}
|
|
13714
|
+
const publicKey = keys[parsed.keyId];
|
|
13715
|
+
if (!publicKey) {
|
|
13716
|
+
return { ok: false, reason: `mod.sig references unpinned key "${parsed.keyId}"` };
|
|
13717
|
+
}
|
|
13718
|
+
try {
|
|
13719
|
+
const valid = (0, import_node_crypto3.verify)(
|
|
13720
|
+
null,
|
|
13721
|
+
Buffer.from(digest, "hex"),
|
|
13722
|
+
publicKey,
|
|
13723
|
+
Buffer.from(parsed.signature, "base64")
|
|
13724
|
+
);
|
|
13725
|
+
return valid ? { ok: true } : { ok: false, reason: "mod.sig signature does not match" };
|
|
13726
|
+
} catch (err) {
|
|
13727
|
+
return { ok: false, reason: `signature check failed: ${String(err.message || err)}` };
|
|
13728
|
+
}
|
|
13729
|
+
}
|
|
13730
|
+
function verifyModuleTrust(name, modulePath) {
|
|
13731
|
+
let digest;
|
|
13732
|
+
try {
|
|
13733
|
+
digest = computeModuleDigest(modulePath);
|
|
13734
|
+
} catch (err) {
|
|
13735
|
+
return { ok: false, reason: `could not hash module: ${String(err.message || err)}` };
|
|
13736
|
+
}
|
|
13737
|
+
const signature = verifyModuleSignature(modulePath, digest);
|
|
13738
|
+
if (!signature.ok) return signature;
|
|
13739
|
+
const record = readTrustFile().modules[name];
|
|
13740
|
+
if (!record) {
|
|
13741
|
+
return {
|
|
13742
|
+
ok: false,
|
|
13743
|
+
reason: `not trusted \u2014 review it, then run: threatcrush modules trust ${name}`
|
|
13744
|
+
};
|
|
13745
|
+
}
|
|
13746
|
+
if (record.digest !== digest) {
|
|
13747
|
+
return {
|
|
13748
|
+
ok: false,
|
|
13749
|
+
reason: `contents changed since it was trusted on ${record.trustedAt} \u2014 re-review it, then run: threatcrush modules trust ${name}`
|
|
13750
|
+
};
|
|
13751
|
+
}
|
|
13752
|
+
return { ok: true };
|
|
13753
|
+
}
|
|
13754
|
+
|
|
13755
|
+
// src/commands/modules.ts
|
|
12703
13756
|
var API_URL2 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
12704
13757
|
function modulesDir() {
|
|
12705
13758
|
ensureRuntimeDirs();
|
|
@@ -12712,7 +13765,7 @@ function safeModuleDirName(name, label = "module name") {
|
|
|
12712
13765
|
return name;
|
|
12713
13766
|
}
|
|
12714
13767
|
function moduleDestination(dir, name, label) {
|
|
12715
|
-
return (0,
|
|
13768
|
+
return (0, import_node_path11.join)(dir, safeModuleDirName(name, label));
|
|
12716
13769
|
}
|
|
12717
13770
|
function assertSafeTarballEntries(tarPath) {
|
|
12718
13771
|
const listing = (0, import_node_child_process2.execFileSync)("tar", ["-tzf", tarPath], { encoding: "utf-8" });
|
|
@@ -12725,12 +13778,12 @@ function assertSafeTarballEntries(tarPath) {
|
|
|
12725
13778
|
}
|
|
12726
13779
|
}
|
|
12727
13780
|
function validateManifest(modPath) {
|
|
12728
|
-
const manifestPath = (0,
|
|
12729
|
-
if (!(0,
|
|
13781
|
+
const manifestPath = (0, import_node_path11.join)(modPath, "mod.toml");
|
|
13782
|
+
if (!(0, import_node_fs14.existsSync)(manifestPath)) {
|
|
12730
13783
|
return { ok: false, error: `mod.toml not found at ${manifestPath}` };
|
|
12731
13784
|
}
|
|
12732
13785
|
try {
|
|
12733
|
-
const raw = (0,
|
|
13786
|
+
const raw = (0, import_node_fs14.readFileSync)(manifestPath, "utf-8");
|
|
12734
13787
|
const parsed = import_toml3.default.parse(raw);
|
|
12735
13788
|
const name = parsed.module?.name;
|
|
12736
13789
|
const version = parsed.module?.version;
|
|
@@ -12741,6 +13794,12 @@ function validateManifest(modPath) {
|
|
|
12741
13794
|
return { ok: false, error: `Invalid mod.toml: ${err.message}` };
|
|
12742
13795
|
}
|
|
12743
13796
|
}
|
|
13797
|
+
function printTrustRequired(name) {
|
|
13798
|
+
console.log();
|
|
13799
|
+
console.log(source_default.yellow(` ! ${name} is installed but NOT trusted, so threatcrushd will not load it.`));
|
|
13800
|
+
console.log(source_default.dim(" Review the module source, then run:"));
|
|
13801
|
+
console.log(source_default.dim(` ${source_default.white(`threatcrush modules trust ${name}`)}`));
|
|
13802
|
+
}
|
|
12744
13803
|
function notifyDaemonIfRunning() {
|
|
12745
13804
|
if (!findRunningDaemon()) return;
|
|
12746
13805
|
console.log(source_default.dim(` \u2139 threatcrushd is running \u2014 restart it to load/unload modules:`));
|
|
@@ -12784,8 +13843,8 @@ async function modulesInstallCommand(source) {
|
|
|
12784
13843
|
console.log();
|
|
12785
13844
|
const dir = modulesDir();
|
|
12786
13845
|
if (source.startsWith("./") || source.startsWith("/") || source.startsWith("~")) {
|
|
12787
|
-
const absPath = (0,
|
|
12788
|
-
if (!(0,
|
|
13846
|
+
const absPath = (0, import_node_path11.resolve)(source.startsWith("~") ? source.replace("~", process.env.HOME || "") : source);
|
|
13847
|
+
if (!(0, import_node_fs14.existsSync)(absPath)) {
|
|
12789
13848
|
console.log(source_default.red(` \u2717 Path not found: ${absPath}
|
|
12790
13849
|
`));
|
|
12791
13850
|
return;
|
|
@@ -12804,7 +13863,7 @@ async function modulesInstallCommand(source) {
|
|
|
12804
13863
|
`));
|
|
12805
13864
|
return;
|
|
12806
13865
|
}
|
|
12807
|
-
if ((0,
|
|
13866
|
+
if ((0, import_node_fs14.existsSync)(dest2)) {
|
|
12808
13867
|
console.log(source_default.yellow(` ! ${check.name} is already installed at ${dest2}`));
|
|
12809
13868
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${check.name}`)} first.
|
|
12810
13869
|
`));
|
|
@@ -12812,12 +13871,13 @@ async function modulesInstallCommand(source) {
|
|
|
12812
13871
|
}
|
|
12813
13872
|
const spinner2 = ora({ text: `Copying module files...`, color: "green" }).start();
|
|
12814
13873
|
try {
|
|
12815
|
-
(0,
|
|
13874
|
+
(0, import_node_fs14.cpSync)(absPath, dest2, { recursive: true, dereference: true });
|
|
12816
13875
|
spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
|
|
12817
13876
|
} catch (err) {
|
|
12818
13877
|
spinner2.fail(`Copy failed: ${err.message}`);
|
|
12819
13878
|
return;
|
|
12820
13879
|
}
|
|
13880
|
+
printTrustRequired(check.name);
|
|
12821
13881
|
notifyDaemonIfRunning();
|
|
12822
13882
|
console.log();
|
|
12823
13883
|
return;
|
|
@@ -12827,14 +13887,14 @@ async function modulesInstallCommand(source) {
|
|
|
12827
13887
|
let name;
|
|
12828
13888
|
let dest2;
|
|
12829
13889
|
try {
|
|
12830
|
-
name = safeModuleDirName((0,
|
|
13890
|
+
name = safeModuleDirName((0, import_node_path11.basename)(gitUrl.replace(/\.git$/, "")), "repository name");
|
|
12831
13891
|
dest2 = moduleDestination(dir, name);
|
|
12832
13892
|
} catch (err) {
|
|
12833
13893
|
console.log(source_default.red(` x ${err.message}
|
|
12834
13894
|
`));
|
|
12835
13895
|
return;
|
|
12836
13896
|
}
|
|
12837
|
-
if ((0,
|
|
13897
|
+
if ((0, import_node_fs14.existsSync)(dest2)) {
|
|
12838
13898
|
console.log(source_default.yellow(` ! ${name} is already installed at ${dest2}`));
|
|
12839
13899
|
console.log(source_default.dim(` Run ${source_default.white(`threatcrush modules remove ${name}`)} first.
|
|
12840
13900
|
`));
|
|
@@ -12851,12 +13911,13 @@ async function modulesInstallCommand(source) {
|
|
|
12851
13911
|
if (!check.ok) {
|
|
12852
13912
|
spinner2.fail(check.error);
|
|
12853
13913
|
try {
|
|
12854
|
-
(0,
|
|
13914
|
+
(0, import_node_fs14.rmSync)(dest2, { recursive: true, force: true });
|
|
12855
13915
|
} catch {
|
|
12856
13916
|
}
|
|
12857
13917
|
return;
|
|
12858
13918
|
}
|
|
12859
13919
|
spinner2.succeed(`Installed ${source_default.white(check.name)} v${check.version} \u2192 ${dest2}`);
|
|
13920
|
+
printTrustRequired(check.name);
|
|
12860
13921
|
notifyDaemonIfRunning();
|
|
12861
13922
|
console.log();
|
|
12862
13923
|
return;
|
|
@@ -12896,7 +13957,7 @@ async function modulesInstallCommand(source) {
|
|
|
12896
13957
|
`));
|
|
12897
13958
|
return;
|
|
12898
13959
|
}
|
|
12899
|
-
if ((0,
|
|
13960
|
+
if ((0, import_node_fs14.existsSync)(dest)) {
|
|
12900
13961
|
console.log(source_default.yellow(` ! ${mod.slug} is already installed at ${dest}
|
|
12901
13962
|
`));
|
|
12902
13963
|
return;
|
|
@@ -12922,7 +13983,7 @@ async function modulesInstallCommand(source) {
|
|
|
12922
13983
|
if (!check.ok) {
|
|
12923
13984
|
cloneSpinner.fail(check.error);
|
|
12924
13985
|
try {
|
|
12925
|
-
(0,
|
|
13986
|
+
(0, import_node_fs14.rmSync)(dest, { recursive: true, force: true });
|
|
12926
13987
|
} catch {
|
|
12927
13988
|
}
|
|
12928
13989
|
return;
|
|
@@ -12936,11 +13997,11 @@ async function modulesInstallCommand(source) {
|
|
|
12936
13997
|
dlSpinner.fail(`HTTP ${res.status}`);
|
|
12937
13998
|
return;
|
|
12938
13999
|
}
|
|
12939
|
-
const tar = (0,
|
|
12940
|
-
(0,
|
|
14000
|
+
const tar = (0, import_node_path11.join)(dir, `${mod.slug}.tar.gz`);
|
|
14001
|
+
(0, import_node_fs14.writeFileSync)(tar, Buffer.from(await res.arrayBuffer()));
|
|
12941
14002
|
assertSafeTarballEntries(tar);
|
|
12942
14003
|
(0, import_node_child_process2.execFileSync)("tar", ["-xzf", tar, "-C", dir], { stdio: "pipe" });
|
|
12943
|
-
(0,
|
|
14004
|
+
(0, import_node_fs14.rmSync)(tar, { force: true });
|
|
12944
14005
|
const check = validateManifest(dest);
|
|
12945
14006
|
if (!check.ok) {
|
|
12946
14007
|
dlSpinner.fail(check.error);
|
|
@@ -12955,6 +14016,7 @@ async function modulesInstallCommand(source) {
|
|
|
12955
14016
|
logger.info("No installable artifact provided for this module.");
|
|
12956
14017
|
return;
|
|
12957
14018
|
}
|
|
14019
|
+
printTrustRequired(mod.slug);
|
|
12958
14020
|
notifyDaemonIfRunning();
|
|
12959
14021
|
console.log();
|
|
12960
14022
|
}
|
|
@@ -12971,7 +14033,7 @@ async function modulesRemoveCommand(name) {
|
|
|
12971
14033
|
`));
|
|
12972
14034
|
return;
|
|
12973
14035
|
}
|
|
12974
|
-
if (!(0,
|
|
14036
|
+
if (!(0, import_node_fs14.existsSync)(target)) {
|
|
12975
14037
|
console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
|
|
12976
14038
|
`));
|
|
12977
14039
|
return;
|
|
@@ -12981,7 +14043,8 @@ async function modulesRemoveCommand(name) {
|
|
|
12981
14043
|
console.log(source_default.yellow(` ! Directory name "${name}" does not match manifest name "${check.name}"`));
|
|
12982
14044
|
}
|
|
12983
14045
|
try {
|
|
12984
|
-
(0,
|
|
14046
|
+
(0, import_node_fs14.rmSync)(target, { recursive: true, force: true });
|
|
14047
|
+
untrustModule(name);
|
|
12985
14048
|
console.log(source_default.green(` \u2713 Removed ${name} from ${dir}
|
|
12986
14049
|
`));
|
|
12987
14050
|
} catch (err) {
|
|
@@ -12991,6 +14054,72 @@ async function modulesRemoveCommand(name) {
|
|
|
12991
14054
|
}
|
|
12992
14055
|
notifyDaemonIfRunning();
|
|
12993
14056
|
}
|
|
14057
|
+
async function modulesTrustCommand(name) {
|
|
14058
|
+
banner();
|
|
14059
|
+
console.log(source_default.green.bold(" Trust Module"));
|
|
14060
|
+
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
14061
|
+
console.log();
|
|
14062
|
+
const dir = modulesDir();
|
|
14063
|
+
let target;
|
|
14064
|
+
try {
|
|
14065
|
+
target = moduleDestination(dir, name);
|
|
14066
|
+
} catch (err) {
|
|
14067
|
+
console.log(source_default.red(` x ${err.message}
|
|
14068
|
+
`));
|
|
14069
|
+
return;
|
|
14070
|
+
}
|
|
14071
|
+
if (!(0, import_node_fs14.existsSync)(target)) {
|
|
14072
|
+
console.log(source_default.yellow(` ! No module named "${name}" in ${dir}
|
|
14073
|
+
`));
|
|
14074
|
+
return;
|
|
14075
|
+
}
|
|
14076
|
+
const check = validateManifest(target);
|
|
14077
|
+
if (!check.ok) {
|
|
14078
|
+
console.log(source_default.red(` \u2717 ${check.error}
|
|
14079
|
+
`));
|
|
14080
|
+
return;
|
|
14081
|
+
}
|
|
14082
|
+
const record = trustModule(name, target, target);
|
|
14083
|
+
console.log(source_default.green(` \u2713 Trusted ${source_default.white(name)} v${check.version}`));
|
|
14084
|
+
console.log(source_default.dim(` digest ${record.digest.slice(0, 16)}\u2026`));
|
|
14085
|
+
console.log(
|
|
14086
|
+
source_default.dim(" threatcrushd will refuse to load it again if its contents change.\n")
|
|
14087
|
+
);
|
|
14088
|
+
notifyDaemonIfRunning();
|
|
14089
|
+
}
|
|
14090
|
+
async function modulesUntrustCommand(name) {
|
|
14091
|
+
banner();
|
|
14092
|
+
console.log(source_default.green.bold(" Revoke Module Trust"));
|
|
14093
|
+
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
14094
|
+
console.log();
|
|
14095
|
+
if (untrustModule(name)) {
|
|
14096
|
+
console.log(source_default.green(` \u2713 Revoked trust for ${source_default.white(name)}
|
|
14097
|
+
`));
|
|
14098
|
+
notifyDaemonIfRunning();
|
|
14099
|
+
} else {
|
|
14100
|
+
console.log(source_default.yellow(` ! ${name} was not trusted
|
|
14101
|
+
`));
|
|
14102
|
+
}
|
|
14103
|
+
}
|
|
14104
|
+
async function modulesTrustedCommand() {
|
|
14105
|
+
banner();
|
|
14106
|
+
console.log(source_default.green.bold(" Trusted Modules"));
|
|
14107
|
+
console.log(source_default.gray(" " + "\u2500".repeat(50)));
|
|
14108
|
+
console.log();
|
|
14109
|
+
const trusted = Object.entries(listTrustedModules());
|
|
14110
|
+
if (trusted.length === 0) {
|
|
14111
|
+
console.log(source_default.yellow(" No modules are trusted."));
|
|
14112
|
+
console.log(source_default.dim(" Installed modules stay dormant until you run:"));
|
|
14113
|
+
console.log(source_default.dim(` ${source_default.white("threatcrush modules trust <name>")}
|
|
14114
|
+
`));
|
|
14115
|
+
return;
|
|
14116
|
+
}
|
|
14117
|
+
for (const [name, record] of trusted) {
|
|
14118
|
+
console.log(` ${source_default.white(name)} ${source_default.dim(record.digest.slice(0, 16) + "\u2026")}`);
|
|
14119
|
+
console.log(source_default.dim(` trusted ${record.trustedAt}`));
|
|
14120
|
+
}
|
|
14121
|
+
console.log();
|
|
14122
|
+
}
|
|
12994
14123
|
async function modulesCommand(opts) {
|
|
12995
14124
|
const action = opts.action || "list";
|
|
12996
14125
|
switch (action) {
|
|
@@ -13024,10 +14153,31 @@ async function modulesCommand(opts) {
|
|
|
13024
14153
|
}
|
|
13025
14154
|
await modulesRemoveCommand(opts.name);
|
|
13026
14155
|
break;
|
|
14156
|
+
case "trust":
|
|
14157
|
+
if (!opts.name) {
|
|
14158
|
+
banner();
|
|
14159
|
+
console.log(source_default.red(" Module name required."));
|
|
14160
|
+
console.log(source_default.gray(" Usage: threatcrush modules trust <name>\n"));
|
|
14161
|
+
return;
|
|
14162
|
+
}
|
|
14163
|
+
await modulesTrustCommand(opts.name);
|
|
14164
|
+
break;
|
|
14165
|
+
case "untrust":
|
|
14166
|
+
if (!opts.name) {
|
|
14167
|
+
banner();
|
|
14168
|
+
console.log(source_default.red(" Module name required."));
|
|
14169
|
+
console.log(source_default.gray(" Usage: threatcrush modules untrust <name>\n"));
|
|
14170
|
+
return;
|
|
14171
|
+
}
|
|
14172
|
+
await modulesUntrustCommand(opts.name);
|
|
14173
|
+
break;
|
|
14174
|
+
case "trusted":
|
|
14175
|
+
await modulesTrustedCommand();
|
|
14176
|
+
break;
|
|
13027
14177
|
default:
|
|
13028
14178
|
banner();
|
|
13029
14179
|
console.log(source_default.yellow(` Unknown action: ${action}`));
|
|
13030
|
-
console.log(source_default.gray(" Available actions: list, install, remove\n"));
|
|
14180
|
+
console.log(source_default.gray(" Available actions: list, install, remove, trust, untrust, trusted\n"));
|
|
13031
14181
|
await modulesListCommand();
|
|
13032
14182
|
break;
|
|
13033
14183
|
}
|
|
@@ -13354,21 +14504,21 @@ async function pentestCommand(targetUrl) {
|
|
|
13354
14504
|
|
|
13355
14505
|
// src/commands/orgs.ts
|
|
13356
14506
|
var import_node_os5 = require("os");
|
|
13357
|
-
var
|
|
13358
|
-
var
|
|
14507
|
+
var import_node_fs15 = require("fs");
|
|
14508
|
+
var import_node_path12 = require("path");
|
|
13359
14509
|
var API_URL3 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
13360
|
-
var CONFIG_PATH = (0,
|
|
14510
|
+
var CONFIG_PATH = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".threatcrush", "config.json");
|
|
13361
14511
|
function readConfig() {
|
|
13362
14512
|
try {
|
|
13363
|
-
return JSON.parse((0,
|
|
14513
|
+
return JSON.parse((0, import_node_fs15.readFileSync)(CONFIG_PATH, "utf-8"));
|
|
13364
14514
|
} catch {
|
|
13365
14515
|
return {};
|
|
13366
14516
|
}
|
|
13367
14517
|
}
|
|
13368
14518
|
function writeConfig(config) {
|
|
13369
|
-
const dir = (0,
|
|
13370
|
-
if (!(0,
|
|
13371
|
-
(0,
|
|
14519
|
+
const dir = (0, import_node_path12.join)((0, import_node_os5.homedir)(), ".threatcrush");
|
|
14520
|
+
if (!(0, import_node_fs15.existsSync)(dir)) (0, import_node_fs15.mkdirSync)(dir, { recursive: true });
|
|
14521
|
+
(0, import_node_fs15.writeFileSync)(CONFIG_PATH, JSON.stringify(config, null, 2));
|
|
13372
14522
|
}
|
|
13373
14523
|
function getAuthHeaders() {
|
|
13374
14524
|
const config = readConfig();
|
|
@@ -13504,13 +14654,13 @@ async function useOrganization(slug) {
|
|
|
13504
14654
|
|
|
13505
14655
|
// src/commands/servers.ts
|
|
13506
14656
|
var import_node_os6 = require("os");
|
|
13507
|
-
var
|
|
13508
|
-
var
|
|
14657
|
+
var import_node_fs16 = require("fs");
|
|
14658
|
+
var import_node_path13 = require("path");
|
|
13509
14659
|
var API_URL4 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
13510
|
-
var CONFIG_PATH2 = (0,
|
|
14660
|
+
var CONFIG_PATH2 = (0, import_node_path13.join)((0, import_node_os6.homedir)(), ".threatcrush", "config.json");
|
|
13511
14661
|
function readConfig2() {
|
|
13512
14662
|
try {
|
|
13513
|
-
return JSON.parse((0,
|
|
14663
|
+
return JSON.parse((0, import_node_fs16.readFileSync)(CONFIG_PATH2, "utf-8"));
|
|
13514
14664
|
} catch {
|
|
13515
14665
|
return {};
|
|
13516
14666
|
}
|
|
@@ -13613,14 +14763,14 @@ function timeAgo(dateStr) {
|
|
|
13613
14763
|
|
|
13614
14764
|
// src/commands/connect.ts
|
|
13615
14765
|
var import_node_os7 = require("os");
|
|
13616
|
-
var
|
|
13617
|
-
var
|
|
14766
|
+
var import_node_fs17 = require("fs");
|
|
14767
|
+
var import_node_path14 = require("path");
|
|
13618
14768
|
var import_node_child_process3 = require("child_process");
|
|
13619
14769
|
var API_URL5 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
13620
|
-
var CONFIG_PATH3 = (0,
|
|
14770
|
+
var CONFIG_PATH3 = (0, import_node_path14.join)((0, import_node_os7.homedir)(), ".threatcrush", "config.json");
|
|
13621
14771
|
function readConfig3() {
|
|
13622
14772
|
try {
|
|
13623
|
-
return JSON.parse((0,
|
|
14773
|
+
return JSON.parse((0, import_node_fs17.readFileSync)(CONFIG_PATH3, "utf-8"));
|
|
13624
14774
|
} catch {
|
|
13625
14775
|
return {};
|
|
13626
14776
|
}
|
|
@@ -13775,20 +14925,21 @@ async function sshConnect(options) {
|
|
|
13775
14925
|
|
|
13776
14926
|
// src/commands/daemon.ts
|
|
13777
14927
|
var import_node_child_process7 = require("child_process");
|
|
13778
|
-
var
|
|
13779
|
-
var
|
|
13780
|
-
var
|
|
14928
|
+
var import_node_fs26 = require("fs");
|
|
14929
|
+
var import_node_path18 = require("path");
|
|
14930
|
+
var import_node_fs27 = require("fs");
|
|
13781
14931
|
|
|
13782
14932
|
// src/daemon/index.ts
|
|
13783
|
-
var
|
|
13784
|
-
var
|
|
14933
|
+
var import_node_fs25 = require("fs");
|
|
14934
|
+
var import_node_path17 = require("path");
|
|
13785
14935
|
init_paths();
|
|
13786
14936
|
init_pidfile();
|
|
13787
14937
|
|
|
13788
14938
|
// src/daemon/ipc-server.ts
|
|
13789
14939
|
var import_node_net2 = require("net");
|
|
13790
|
-
var
|
|
14940
|
+
var import_node_fs18 = require("fs");
|
|
13791
14941
|
init_paths();
|
|
14942
|
+
init_control_token();
|
|
13792
14943
|
|
|
13793
14944
|
// src/daemon/event-bus.ts
|
|
13794
14945
|
var import_node_events = require("events");
|
|
@@ -13833,10 +14984,12 @@ var IpcServer = class {
|
|
|
13833
14984
|
nextClientId = 1;
|
|
13834
14985
|
startedAt = /* @__PURE__ */ new Date();
|
|
13835
14986
|
counters = { events: 0, threats: 0, alerts: 0 };
|
|
14987
|
+
controlToken = "";
|
|
13836
14988
|
async start() {
|
|
13837
|
-
|
|
14989
|
+
this.controlToken = issueControlToken();
|
|
14990
|
+
if ((0, import_node_fs18.existsSync)(PATHS.socket)) {
|
|
13838
14991
|
try {
|
|
13839
|
-
(0,
|
|
14992
|
+
(0, import_node_fs18.unlinkSync)(PATHS.socket);
|
|
13840
14993
|
} catch {
|
|
13841
14994
|
}
|
|
13842
14995
|
}
|
|
@@ -13872,14 +15025,14 @@ var IpcServer = class {
|
|
|
13872
15025
|
return new Promise((resolve5) => {
|
|
13873
15026
|
if (!this.server) {
|
|
13874
15027
|
try {
|
|
13875
|
-
if ((0,
|
|
15028
|
+
if ((0, import_node_fs18.existsSync)(PATHS.socket)) (0, import_node_fs18.unlinkSync)(PATHS.socket);
|
|
13876
15029
|
} catch {
|
|
13877
15030
|
}
|
|
13878
15031
|
return resolve5();
|
|
13879
15032
|
}
|
|
13880
15033
|
this.server.close(() => {
|
|
13881
15034
|
try {
|
|
13882
|
-
if ((0,
|
|
15035
|
+
if ((0, import_node_fs18.existsSync)(PATHS.socket)) (0, import_node_fs18.unlinkSync)(PATHS.socket);
|
|
13883
15036
|
} catch {
|
|
13884
15037
|
}
|
|
13885
15038
|
resolve5();
|
|
@@ -13965,6 +15118,13 @@ var IpcServer = class {
|
|
|
13965
15118
|
for (const ch of req.params.channels) client.subscriptions.add(ch);
|
|
13966
15119
|
return this.send(client, { id: req.id, ok: true, result: { subscribed: [...client.subscriptions] } });
|
|
13967
15120
|
case "shutdown":
|
|
15121
|
+
if (!tokensMatch(this.controlToken, req.params?.token)) {
|
|
15122
|
+
return this.send(client, {
|
|
15123
|
+
id: req.id,
|
|
15124
|
+
ok: false,
|
|
15125
|
+
error: "shutdown requires the daemon control token (run as root, or use systemctl)"
|
|
15126
|
+
});
|
|
15127
|
+
}
|
|
13968
15128
|
this.send(client, { id: req.id, ok: true, result: "shutting down" });
|
|
13969
15129
|
setTimeout(() => process.emit("SIGTERM"), 50);
|
|
13970
15130
|
return;
|
|
@@ -13985,14 +15145,14 @@ var IpcServer = class {
|
|
|
13985
15145
|
};
|
|
13986
15146
|
|
|
13987
15147
|
// src/daemon/module-host.ts
|
|
13988
|
-
var
|
|
13989
|
-
var
|
|
15148
|
+
var import_node_fs22 = require("fs");
|
|
15149
|
+
var import_node_path15 = require("path");
|
|
13990
15150
|
var import_node_url = require("url");
|
|
13991
15151
|
var import_toml4 = __toESM(require_toml());
|
|
13992
15152
|
init_paths();
|
|
13993
15153
|
|
|
13994
15154
|
// src/daemon/watchers/log-watcher.ts
|
|
13995
|
-
var
|
|
15155
|
+
var import_node_fs19 = require("fs");
|
|
13996
15156
|
var import_node_readline4 = require("readline");
|
|
13997
15157
|
init_state();
|
|
13998
15158
|
var DEFAULT_SOURCES = [
|
|
@@ -14014,9 +15174,9 @@ var LogWatcher = class {
|
|
|
14014
15174
|
start() {
|
|
14015
15175
|
const started = [];
|
|
14016
15176
|
for (const src of this.sources) {
|
|
14017
|
-
if (!(0,
|
|
15177
|
+
if (!(0, import_node_fs19.existsSync)(src.path)) continue;
|
|
14018
15178
|
try {
|
|
14019
|
-
(0,
|
|
15179
|
+
(0, import_node_fs19.accessSync)(src.path, import_node_fs19.constants.R_OK);
|
|
14020
15180
|
} catch {
|
|
14021
15181
|
continue;
|
|
14022
15182
|
}
|
|
@@ -14036,7 +15196,7 @@ var LogWatcher = class {
|
|
|
14036
15196
|
}
|
|
14037
15197
|
tail(src) {
|
|
14038
15198
|
try {
|
|
14039
|
-
this.positions.set(src.path, (0,
|
|
15199
|
+
this.positions.set(src.path, (0, import_node_fs19.statSync)(src.path).size);
|
|
14040
15200
|
} catch {
|
|
14041
15201
|
this.positions.set(src.path, 0);
|
|
14042
15202
|
}
|
|
@@ -14047,7 +15207,7 @@ var LogWatcher = class {
|
|
|
14047
15207
|
poll(src) {
|
|
14048
15208
|
let stat;
|
|
14049
15209
|
try {
|
|
14050
|
-
stat = (0,
|
|
15210
|
+
stat = (0, import_node_fs19.statSync)(src.path);
|
|
14051
15211
|
} catch {
|
|
14052
15212
|
return;
|
|
14053
15213
|
}
|
|
@@ -14057,7 +15217,7 @@ var LogWatcher = class {
|
|
|
14057
15217
|
return;
|
|
14058
15218
|
}
|
|
14059
15219
|
if (stat.size === prev) return;
|
|
14060
|
-
const stream = (0,
|
|
15220
|
+
const stream = (0, import_node_fs19.createReadStream)(src.path, { start: prev, encoding: "utf-8" });
|
|
14061
15221
|
stream.on("error", () => this.positions.set(src.path, stat.size));
|
|
14062
15222
|
const rl = (0, import_node_readline4.createInterface)({ input: stream });
|
|
14063
15223
|
rl.on("error", () => {
|
|
@@ -14250,7 +15410,7 @@ function realtimeToDate(rt) {
|
|
|
14250
15410
|
|
|
14251
15411
|
// src/modules/network-monitor/index.ts
|
|
14252
15412
|
var import_node_child_process5 = require("child_process");
|
|
14253
|
-
var
|
|
15413
|
+
var import_node_fs20 = require("fs");
|
|
14254
15414
|
init_state();
|
|
14255
15415
|
var NetworkMonitor = class {
|
|
14256
15416
|
constructor(bus2) {
|
|
@@ -14289,7 +15449,7 @@ var NetworkMonitor = class {
|
|
|
14289
15449
|
hasConntrackOrSs() {
|
|
14290
15450
|
const ss = (0, import_node_child_process5.spawnSync)("ss", ["--version"], { stdio: "pipe" });
|
|
14291
15451
|
if (ss.status === 0) return true;
|
|
14292
|
-
return (0,
|
|
15452
|
+
return (0, import_node_fs20.existsSync)("/proc/net/tcp");
|
|
14293
15453
|
}
|
|
14294
15454
|
poll() {
|
|
14295
15455
|
try {
|
|
@@ -14428,7 +15588,7 @@ var NetworkMonitor = class {
|
|
|
14428
15588
|
};
|
|
14429
15589
|
|
|
14430
15590
|
// src/modules/dns-monitor/index.ts
|
|
14431
|
-
var
|
|
15591
|
+
var import_node_fs21 = require("fs");
|
|
14432
15592
|
var import_node_readline5 = require("readline");
|
|
14433
15593
|
init_state();
|
|
14434
15594
|
var DNS_LOG_SOURCES = [
|
|
@@ -14463,9 +15623,9 @@ var DnsMonitor = class {
|
|
|
14463
15623
|
entropyThreshold = 3.5;
|
|
14464
15624
|
start() {
|
|
14465
15625
|
const sources = DNS_LOG_SOURCES.filter((p) => {
|
|
14466
|
-
if (!(0,
|
|
15626
|
+
if (!(0, import_node_fs21.existsSync)(p)) return false;
|
|
14467
15627
|
try {
|
|
14468
|
-
(0,
|
|
15628
|
+
(0, import_node_fs21.accessSync)(p, import_node_fs21.constants.R_OK);
|
|
14469
15629
|
return true;
|
|
14470
15630
|
} catch {
|
|
14471
15631
|
return false;
|
|
@@ -14489,7 +15649,7 @@ var DnsMonitor = class {
|
|
|
14489
15649
|
}
|
|
14490
15650
|
tailLog(path) {
|
|
14491
15651
|
try {
|
|
14492
|
-
this.positions.set(path, (0,
|
|
15652
|
+
this.positions.set(path, (0, import_node_fs21.statSync)(path).size);
|
|
14493
15653
|
} catch {
|
|
14494
15654
|
this.positions.set(path, 0);
|
|
14495
15655
|
}
|
|
@@ -14499,7 +15659,7 @@ var DnsMonitor = class {
|
|
|
14499
15659
|
pollLog(path) {
|
|
14500
15660
|
let stat;
|
|
14501
15661
|
try {
|
|
14502
|
-
stat = (0,
|
|
15662
|
+
stat = (0, import_node_fs21.statSync)(path);
|
|
14503
15663
|
} catch {
|
|
14504
15664
|
return;
|
|
14505
15665
|
}
|
|
@@ -14509,7 +15669,7 @@ var DnsMonitor = class {
|
|
|
14509
15669
|
return;
|
|
14510
15670
|
}
|
|
14511
15671
|
if (stat.size === prev) return;
|
|
14512
|
-
const stream = (0,
|
|
15672
|
+
const stream = (0, import_node_fs21.createReadStream)(path, { start: prev, encoding: "utf-8" });
|
|
14513
15673
|
stream.on("error", () => this.positions.set(path, stat.size));
|
|
14514
15674
|
const rl = (0, import_node_readline5.createInterface)({ input: stream });
|
|
14515
15675
|
rl.on("line", (line) => this.parseDnsLine(line));
|
|
@@ -14741,15 +15901,15 @@ var ModuleHost = class {
|
|
|
14741
15901
|
for (const m of builtins) this.modules.set(m.name, m);
|
|
14742
15902
|
}
|
|
14743
15903
|
async discoverAndStartInstalled() {
|
|
14744
|
-
if (!(0,
|
|
15904
|
+
if (!(0, import_node_fs22.existsSync)(PATHS.moduleDir)) return;
|
|
14745
15905
|
const configs = loadModuleConfigs(PATHS.confD);
|
|
14746
|
-
const entries = (0,
|
|
15906
|
+
const entries = (0, import_node_fs22.readdirSync)(PATHS.moduleDir, { withFileTypes: true });
|
|
14747
15907
|
for (const entry of entries) {
|
|
14748
15908
|
if (!entry.isDirectory()) continue;
|
|
14749
|
-
const manifestPath = (0,
|
|
14750
|
-
if (!(0,
|
|
15909
|
+
const manifestPath = (0, import_node_path15.join)(PATHS.moduleDir, entry.name, "mod.toml");
|
|
15910
|
+
if (!(0, import_node_fs22.existsSync)(manifestPath)) continue;
|
|
14751
15911
|
try {
|
|
14752
|
-
const manifest = import_toml4.default.parse((0,
|
|
15912
|
+
const manifest = import_toml4.default.parse((0, import_node_fs22.readFileSync)(manifestPath, "utf-8"));
|
|
14753
15913
|
const name = manifest.module?.name || entry.name;
|
|
14754
15914
|
const defaults = manifest.module?.config?.defaults || {};
|
|
14755
15915
|
const config = {
|
|
@@ -14763,7 +15923,7 @@ var ModuleHost = class {
|
|
|
14763
15923
|
source: "installed",
|
|
14764
15924
|
status: config.enabled === false ? "disabled" : "loaded",
|
|
14765
15925
|
events: 0,
|
|
14766
|
-
path: (0,
|
|
15926
|
+
path: (0, import_node_path15.join)(PATHS.moduleDir, entry.name),
|
|
14767
15927
|
config
|
|
14768
15928
|
};
|
|
14769
15929
|
this.modules.set(name, hosted);
|
|
@@ -14778,7 +15938,7 @@ var ModuleHost = class {
|
|
|
14778
15938
|
status: "error",
|
|
14779
15939
|
events: 0,
|
|
14780
15940
|
detail: `manifest load failed: ${String(err.message || err)}`,
|
|
14781
|
-
path: (0,
|
|
15941
|
+
path: (0, import_node_path15.join)(PATHS.moduleDir, entry.name)
|
|
14782
15942
|
});
|
|
14783
15943
|
}
|
|
14784
15944
|
}
|
|
@@ -14790,6 +15950,13 @@ var ModuleHost = class {
|
|
|
14790
15950
|
hosted.detail = "no built entrypoint found; run npm install && npm run build in the module directory";
|
|
14791
15951
|
return;
|
|
14792
15952
|
}
|
|
15953
|
+
const trust = verifyModuleTrust(hosted.name, hosted.path);
|
|
15954
|
+
if (!trust.ok) {
|
|
15955
|
+
hosted.status = "error";
|
|
15956
|
+
hosted.detail = `refusing to load: ${trust.reason}`;
|
|
15957
|
+
this.bus.announceModule(hosted.name, "error", hosted.detail);
|
|
15958
|
+
return;
|
|
15959
|
+
}
|
|
14793
15960
|
try {
|
|
14794
15961
|
const imported = await import((0, import_node_url.pathToFileURL)(entrypoint).href);
|
|
14795
15962
|
const exported = imported.default || imported.module || imported;
|
|
@@ -14810,17 +15977,17 @@ var ModuleHost = class {
|
|
|
14810
15977
|
}
|
|
14811
15978
|
}
|
|
14812
15979
|
installedEntrypoint(modulePath) {
|
|
14813
|
-
const packageJson = (0,
|
|
15980
|
+
const packageJson = (0, import_node_path15.join)(modulePath, "package.json");
|
|
14814
15981
|
const candidates = [];
|
|
14815
|
-
if ((0,
|
|
15982
|
+
if ((0, import_node_fs22.existsSync)(packageJson)) {
|
|
14816
15983
|
try {
|
|
14817
|
-
const pkg = JSON.parse((0,
|
|
14818
|
-
if (pkg.main) candidates.push((0,
|
|
15984
|
+
const pkg = JSON.parse((0, import_node_fs22.readFileSync)(packageJson, "utf-8"));
|
|
15985
|
+
if (pkg.main) candidates.push((0, import_node_path15.join)(modulePath, pkg.main));
|
|
14819
15986
|
} catch {
|
|
14820
15987
|
}
|
|
14821
15988
|
}
|
|
14822
|
-
candidates.push((0,
|
|
14823
|
-
return candidates.find((candidate) => (0,
|
|
15989
|
+
candidates.push((0, import_node_path15.join)(modulePath, "dist", "index.js"), (0, import_node_path15.join)(modulePath, "index.js"));
|
|
15990
|
+
return candidates.find((candidate) => (0, import_node_fs22.existsSync)(candidate)) || null;
|
|
14824
15991
|
}
|
|
14825
15992
|
isThreatCrushModule(value) {
|
|
14826
15993
|
return Boolean(
|
|
@@ -15341,8 +16508,8 @@ var RuleEngine = class {
|
|
|
15341
16508
|
};
|
|
15342
16509
|
|
|
15343
16510
|
// src/daemon/rules/loader.ts
|
|
15344
|
-
var
|
|
15345
|
-
var
|
|
16511
|
+
var import_node_fs23 = require("fs");
|
|
16512
|
+
var import_node_path16 = require("path");
|
|
15346
16513
|
|
|
15347
16514
|
// src/daemon/rules/default-rules.ts
|
|
15348
16515
|
var DEFAULT_RULES = [
|
|
@@ -15626,11 +16793,11 @@ var RULES_DIR = "/etc/threatcrush/rules.d";
|
|
|
15626
16793
|
function loadAllRules(customDir) {
|
|
15627
16794
|
const rules = [...DEFAULT_RULES];
|
|
15628
16795
|
const dir = customDir || RULES_DIR;
|
|
15629
|
-
if ((0,
|
|
15630
|
-
const files = (0,
|
|
16796
|
+
if ((0, import_node_fs23.existsSync)(dir)) {
|
|
16797
|
+
const files = (0, import_node_fs23.readdirSync)(dir).filter((f) => f.endsWith(".json"));
|
|
15631
16798
|
for (const file of files) {
|
|
15632
16799
|
try {
|
|
15633
|
-
const raw = (0,
|
|
16800
|
+
const raw = (0, import_node_fs23.readFileSync)((0, import_node_path16.join)(dir, file), "utf-8");
|
|
15634
16801
|
const parsed = JSON.parse(raw);
|
|
15635
16802
|
const customRules = Array.isArray(parsed) ? parsed : [parsed];
|
|
15636
16803
|
for (const rule of customRules) {
|
|
@@ -15793,7 +16960,7 @@ function detectFirewallAdapter() {
|
|
|
15793
16960
|
}
|
|
15794
16961
|
|
|
15795
16962
|
// src/daemon/firewall/remediation.ts
|
|
15796
|
-
var
|
|
16963
|
+
var import_node_fs24 = require("fs");
|
|
15797
16964
|
init_state();
|
|
15798
16965
|
init_paths();
|
|
15799
16966
|
var DEFAULT_CONFIG2 = {
|
|
@@ -15950,7 +17117,7 @@ var RemediationManager = class {
|
|
|
15950
17117
|
}
|
|
15951
17118
|
logLine(line) {
|
|
15952
17119
|
try {
|
|
15953
|
-
(0,
|
|
17120
|
+
(0, import_node_fs24.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
15954
17121
|
`);
|
|
15955
17122
|
} catch {
|
|
15956
17123
|
}
|
|
@@ -16009,7 +17176,7 @@ async function flushTelemetry(timeoutMs = 2e3) {
|
|
|
16009
17176
|
// src/daemon/index.ts
|
|
16010
17177
|
function readVersion2() {
|
|
16011
17178
|
try {
|
|
16012
|
-
const pkg = JSON.parse((0,
|
|
17179
|
+
const pkg = JSON.parse((0, import_node_fs25.readFileSync)((0, import_node_path17.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
16013
17180
|
return pkg.version || "0.0.0";
|
|
16014
17181
|
} catch {
|
|
16015
17182
|
return "0.0.0";
|
|
@@ -16017,7 +17184,7 @@ function readVersion2() {
|
|
|
16017
17184
|
}
|
|
16018
17185
|
function logLine(line) {
|
|
16019
17186
|
try {
|
|
16020
|
-
(0,
|
|
17187
|
+
(0, import_node_fs25.appendFileSync)(PATHS.logFile, `${(/* @__PURE__ */ new Date()).toISOString()} ${line}
|
|
16021
17188
|
`);
|
|
16022
17189
|
} catch {
|
|
16023
17190
|
}
|
|
@@ -16045,7 +17212,7 @@ async function runDaemon() {
|
|
|
16045
17212
|
} catch (err) {
|
|
16046
17213
|
logLine(`[daemon] state db unavailable: ${err.message}`);
|
|
16047
17214
|
}
|
|
16048
|
-
const config = loadConfig((0,
|
|
17215
|
+
const config = loadConfig((0, import_node_fs25.existsSync)(PATHS.configFile) ? PATHS.configFile : void 0);
|
|
16049
17216
|
bus.on("event", (event) => {
|
|
16050
17217
|
logLine(`[event] ${event.severity} ${event.module} ${event.message}`);
|
|
16051
17218
|
});
|
|
@@ -16137,7 +17304,7 @@ async function runDaemon() {
|
|
|
16137
17304
|
init_paths();
|
|
16138
17305
|
init_pidfile();
|
|
16139
17306
|
init_ipc_client();
|
|
16140
|
-
var DAEMON_ENTRY = (0,
|
|
17307
|
+
var DAEMON_ENTRY = (0, import_node_path18.join)(__dirname, "daemon.js");
|
|
16141
17308
|
async function daemonForeground() {
|
|
16142
17309
|
await runDaemon();
|
|
16143
17310
|
}
|
|
@@ -16148,7 +17315,7 @@ async function daemonStart() {
|
|
|
16148
17315
|
return;
|
|
16149
17316
|
}
|
|
16150
17317
|
ensureRuntimeDirs();
|
|
16151
|
-
if (!(0,
|
|
17318
|
+
if (!(0, import_node_fs26.existsSync)(DAEMON_ENTRY)) {
|
|
16152
17319
|
console.log(source_default.red(` Daemon entry not found at ${DAEMON_ENTRY}.`));
|
|
16153
17320
|
console.log(source_default.dim(" Reinstall with `threatcrush update` or `pnpm run build` in the cli package."));
|
|
16154
17321
|
return;
|
|
@@ -16156,8 +17323,8 @@ async function daemonStart() {
|
|
|
16156
17323
|
let out;
|
|
16157
17324
|
let err;
|
|
16158
17325
|
try {
|
|
16159
|
-
out = (0,
|
|
16160
|
-
err = (0,
|
|
17326
|
+
out = (0, import_node_fs27.openSync)(PATHS.logFile, "a");
|
|
17327
|
+
err = (0, import_node_fs27.openSync)(PATHS.logFile, "a");
|
|
16161
17328
|
} catch (e) {
|
|
16162
17329
|
const code = e.code;
|
|
16163
17330
|
console.log(source_default.red(` \u2717 Cannot write daemon log at ${PATHS.logFile} (${code ?? "error"}).`));
|
|
@@ -16236,19 +17403,19 @@ async function daemonStop() {
|
|
|
16236
17403
|
|
|
16237
17404
|
// src/commands/service.ts
|
|
16238
17405
|
var import_node_child_process8 = require("child_process");
|
|
16239
|
-
var
|
|
16240
|
-
var
|
|
17406
|
+
var import_node_fs28 = require("fs");
|
|
17407
|
+
var import_node_path19 = require("path");
|
|
16241
17408
|
var UNIT_PATH = "/etc/systemd/system/threatcrushd.service";
|
|
16242
17409
|
function resolveTemplate() {
|
|
16243
|
-
const templatePath = (0,
|
|
16244
|
-
if (!(0,
|
|
17410
|
+
const templatePath = (0, import_node_path19.join)(__dirname, "systemd", "threatcrushd.service");
|
|
17411
|
+
if (!(0, import_node_fs28.existsSync)(templatePath)) {
|
|
16245
17412
|
throw new Error(`systemd unit template not found at ${templatePath}`);
|
|
16246
17413
|
}
|
|
16247
|
-
return (0,
|
|
17414
|
+
return (0, import_node_fs28.readFileSync)(templatePath, "utf-8");
|
|
16248
17415
|
}
|
|
16249
17416
|
function resolveBinPath() {
|
|
16250
17417
|
const arg = process.argv[1];
|
|
16251
|
-
if (arg && (0,
|
|
17418
|
+
if (arg && (0, import_node_fs28.existsSync)(arg)) return arg;
|
|
16252
17419
|
try {
|
|
16253
17420
|
return (0, import_node_child_process8.execSync)("command -v threatcrush", { encoding: "utf-8" }).trim();
|
|
16254
17421
|
} catch {
|
|
@@ -16269,7 +17436,7 @@ async function installServiceCommand() {
|
|
|
16269
17436
|
return;
|
|
16270
17437
|
}
|
|
16271
17438
|
const unit = resolveTemplate().replace("{{BIN_PATH}}", resolveBinPath());
|
|
16272
|
-
(0,
|
|
17439
|
+
(0, import_node_fs28.writeFileSync)(UNIT_PATH, unit, { mode: 420 });
|
|
16273
17440
|
console.log(source_default.green(` \u2713 Installed unit file: ${UNIT_PATH}`));
|
|
16274
17441
|
ensureSystemDirs();
|
|
16275
17442
|
try {
|
|
@@ -16284,29 +17451,31 @@ async function installServiceCommand() {
|
|
|
16284
17451
|
}
|
|
16285
17452
|
function ensureSystemDirs() {
|
|
16286
17453
|
const dirs = [
|
|
16287
|
-
{ path: "/etc/threatcrush" },
|
|
16288
|
-
{ path: "/etc/threatcrush/modules", sticky: true },
|
|
16289
|
-
{ path: "/etc/threatcrush/threatcrushd.conf.d" },
|
|
16290
|
-
{ path: "/var/log/threatcrush" },
|
|
16291
|
-
{ path: "/var/lib/threatcrush" },
|
|
16292
|
-
{ path: "/var/run/threatcrush" }
|
|
17454
|
+
{ path: "/etc/threatcrush", groupWritable: true },
|
|
17455
|
+
{ path: "/etc/threatcrush/modules", groupWritable: true, sticky: true },
|
|
17456
|
+
{ path: "/etc/threatcrush/threatcrushd.conf.d", groupWritable: true },
|
|
17457
|
+
{ path: "/var/log/threatcrush", groupWritable: false },
|
|
17458
|
+
{ path: "/var/lib/threatcrush", groupWritable: false },
|
|
17459
|
+
{ path: "/var/run/threatcrush", groupWritable: false }
|
|
16293
17460
|
];
|
|
16294
17461
|
let admGid = null;
|
|
16295
17462
|
try {
|
|
16296
|
-
admGid = (0,
|
|
17463
|
+
admGid = (0, import_node_fs28.statSync)("/var/log/auth.log").gid;
|
|
16297
17464
|
} catch {
|
|
16298
17465
|
}
|
|
16299
|
-
for (const { path, sticky } of dirs) {
|
|
17466
|
+
for (const { path, groupWritable, sticky } of dirs) {
|
|
16300
17467
|
try {
|
|
16301
|
-
(0,
|
|
17468
|
+
(0, import_node_fs28.mkdirSync)(path, { recursive: true });
|
|
16302
17469
|
} catch {
|
|
16303
17470
|
}
|
|
16304
|
-
|
|
16305
|
-
|
|
16306
|
-
(0,
|
|
17471
|
+
try {
|
|
17472
|
+
if (groupWritable && admGid !== null) {
|
|
17473
|
+
(0, import_node_fs28.chmodSync)(path, sticky ? 1533 : 509);
|
|
16307
17474
|
(0, import_node_child_process8.execSync)(`chgrp adm ${path}`, { stdio: "ignore" });
|
|
16308
|
-
}
|
|
17475
|
+
} else {
|
|
17476
|
+
(0, import_node_fs28.chmodSync)(path, 493);
|
|
16309
17477
|
}
|
|
17478
|
+
} catch {
|
|
16310
17479
|
}
|
|
16311
17480
|
}
|
|
16312
17481
|
console.log(source_default.green(" \u2713 Runtime dirs prepared (group `adm` may install modules / edit config without sudo)."));
|
|
@@ -16330,7 +17499,7 @@ async function uninstallServiceCommand() {
|
|
|
16330
17499
|
} catch {
|
|
16331
17500
|
}
|
|
16332
17501
|
try {
|
|
16333
|
-
if ((0,
|
|
17502
|
+
if ((0, import_node_fs28.existsSync)(UNIT_PATH)) {
|
|
16334
17503
|
(0, import_node_child_process8.execSync)(`rm -f ${UNIT_PATH}`);
|
|
16335
17504
|
console.log(source_default.green(` \u2713 Removed unit file: ${UNIT_PATH}`));
|
|
16336
17505
|
}
|
|
@@ -16429,8 +17598,8 @@ function welcomeCommand() {
|
|
|
16429
17598
|
}
|
|
16430
17599
|
|
|
16431
17600
|
// src/commands/properties.ts
|
|
16432
|
-
var
|
|
16433
|
-
var
|
|
17601
|
+
var import_node_fs29 = require("fs");
|
|
17602
|
+
var import_node_path20 = require("path");
|
|
16434
17603
|
var import_node_readline6 = __toESM(require("readline"));
|
|
16435
17604
|
var API_URL7 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
16436
17605
|
var KINDS = ["url", "api", "domain", "ip", "repo"];
|
|
@@ -16759,8 +17928,8 @@ async function propertiesRunsCommand(opts) {
|
|
|
16759
17928
|
}
|
|
16760
17929
|
}
|
|
16761
17930
|
function parseImportFile(path) {
|
|
16762
|
-
const ext = (0,
|
|
16763
|
-
const raw = (0,
|
|
17931
|
+
const ext = (0, import_node_path20.extname)(path).toLowerCase();
|
|
17932
|
+
const raw = (0, import_node_fs29.readFileSync)(path, "utf-8");
|
|
16764
17933
|
if (ext === ".json") {
|
|
16765
17934
|
const parsed = JSON.parse(raw);
|
|
16766
17935
|
if (!Array.isArray(parsed)) throw new Error("JSON must be an array of objects");
|
|
@@ -16950,7 +18119,7 @@ async function rulesCommand(opts) {
|
|
|
16950
18119
|
}
|
|
16951
18120
|
|
|
16952
18121
|
// src/commands/harden.ts
|
|
16953
|
-
var
|
|
18122
|
+
var import_node_fs30 = require("fs");
|
|
16954
18123
|
var import_node_child_process9 = require("child_process");
|
|
16955
18124
|
function tryExec(cmd) {
|
|
16956
18125
|
try {
|
|
@@ -16961,7 +18130,7 @@ function tryExec(cmd) {
|
|
|
16961
18130
|
}
|
|
16962
18131
|
function tryRead(path) {
|
|
16963
18132
|
try {
|
|
16964
|
-
return (0,
|
|
18133
|
+
return (0, import_node_fs30.readFileSync)(path, "utf-8");
|
|
16965
18134
|
} catch {
|
|
16966
18135
|
return null;
|
|
16967
18136
|
}
|
|
@@ -17065,8 +18234,8 @@ function checkSshWeakConfig() {
|
|
|
17065
18234
|
};
|
|
17066
18235
|
}
|
|
17067
18236
|
function checkAutoUpdates() {
|
|
17068
|
-
const unattended = (0,
|
|
17069
|
-
const dnfAuto = (0,
|
|
18237
|
+
const unattended = (0, import_node_fs30.existsSync)("/etc/apt/apt.conf.d/20auto-upgrades") || (0, import_node_fs30.existsSync)("/etc/apt/apt.conf.d/50unattended-upgrades");
|
|
18238
|
+
const dnfAuto = (0, import_node_fs30.existsSync)("/etc/dnf/automatic.conf");
|
|
17070
18239
|
if (unattended || dnfAuto) {
|
|
17071
18240
|
return {
|
|
17072
18241
|
key: "auto-updates",
|
|
@@ -17192,7 +18361,7 @@ function checkFail2ban() {
|
|
|
17192
18361
|
explanation: "fail2ban is installed and running."
|
|
17193
18362
|
};
|
|
17194
18363
|
}
|
|
17195
|
-
if ((0,
|
|
18364
|
+
if ((0, import_node_fs30.existsSync)("/etc/fail2ban/fail2ban.conf")) {
|
|
17196
18365
|
return {
|
|
17197
18366
|
key: checkKey,
|
|
17198
18367
|
status: "warn",
|
|
@@ -17423,7 +18592,7 @@ async function allowlistCommand(opts) {
|
|
|
17423
18592
|
init_paths();
|
|
17424
18593
|
var PKG_VERSION2 = "0.1.8";
|
|
17425
18594
|
try {
|
|
17426
|
-
const pkg = JSON.parse((0,
|
|
18595
|
+
const pkg = JSON.parse((0, import_node_fs31.readFileSync)((0, import_node_path21.join)(__dirname, "..", "package.json"), "utf-8"));
|
|
17427
18596
|
PKG_VERSION2 = pkg.version;
|
|
17428
18597
|
} catch {
|
|
17429
18598
|
}
|
|
@@ -17439,7 +18608,7 @@ ${source_default.dim(" C R U S H")}
|
|
|
17439
18608
|
var API_URL8 = process.env.THREATCRUSH_API_URL || "https://threatcrush.com";
|
|
17440
18609
|
var PKG_NAME = "@profullstack/threatcrush";
|
|
17441
18610
|
var DESKTOP_PKG_NAME = "@profullstack/threatcrush-desktop";
|
|
17442
|
-
var INSTALL_CONFIG_PATH = (0,
|
|
18611
|
+
var INSTALL_CONFIG_PATH = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush", "install.json");
|
|
17443
18612
|
function detectPackageManager() {
|
|
17444
18613
|
try {
|
|
17445
18614
|
const npmGlobal = (0, import_node_child_process10.execSync)("npm ls -g --depth=0 --json 2>/dev/null", { encoding: "utf-8" });
|
|
@@ -17465,7 +18634,7 @@ function detectPackageManager() {
|
|
|
17465
18634
|
}
|
|
17466
18635
|
function readInstallConfig() {
|
|
17467
18636
|
try {
|
|
17468
|
-
return JSON.parse((0,
|
|
18637
|
+
return JSON.parse((0, import_node_fs31.readFileSync)(INSTALL_CONFIG_PATH, "utf-8"));
|
|
17469
18638
|
} catch {
|
|
17470
18639
|
return {};
|
|
17471
18640
|
}
|
|
@@ -17579,6 +18748,9 @@ program2.command("scan").description("Scan codebase for vulnerabilities and secr
|
|
|
17579
18748
|
"skip paths matching this glob (repeatable); merged with a .threatcrushignore at the scan root",
|
|
17580
18749
|
(value, previous) => [...previous, value],
|
|
17581
18750
|
[]
|
|
18751
|
+
).option(
|
|
18752
|
+
"--missing-controls",
|
|
18753
|
+
"also report security controls the tree shows no evidence of (headers, CSRF, rate limiting, body size limits)"
|
|
17582
18754
|
).option("-v, --verbose", "list the paths that could not be read").action(async (targetPath, opts) => {
|
|
17583
18755
|
const format = (opts.format ?? "text").toLowerCase();
|
|
17584
18756
|
if (!["text", "json", "sarif"].includes(format)) {
|
|
@@ -17599,6 +18771,7 @@ program2.command("scan").description("Scan codebase for vulnerabilities and secr
|
|
|
17599
18771
|
pathPrefix: opts.pathPrefix,
|
|
17600
18772
|
dependencies: opts.deps,
|
|
17601
18773
|
exclude: opts.exclude,
|
|
18774
|
+
missingControls: opts.missingControls,
|
|
17602
18775
|
verbose: opts.verbose
|
|
17603
18776
|
});
|
|
17604
18777
|
});
|
|
@@ -17646,7 +18819,7 @@ program2.command("uninstall-service").description("Remove the threatcrushd syste
|
|
|
17646
18819
|
program2.command("logs").description("Tail daemon logs").action(async () => {
|
|
17647
18820
|
console.log(LOGO2);
|
|
17648
18821
|
const logPath = PATHS.logFile;
|
|
17649
|
-
if (!(0,
|
|
18822
|
+
if (!(0, import_node_fs31.existsSync)(logPath)) {
|
|
17650
18823
|
console.log(source_default.yellow(` No log file found at ${logPath}`));
|
|
17651
18824
|
console.log(source_default.dim(" Start the daemon first with `threatcrush start`\n"));
|
|
17652
18825
|
return;
|
|
@@ -17851,10 +19024,10 @@ storeCmd.command("search <query>").description("Search for modules in the store"
|
|
|
17851
19024
|
});
|
|
17852
19025
|
storeCmd.command("publish <url>").description("Publish a module from a git URL or web URL").action(async (url) => {
|
|
17853
19026
|
console.log(LOGO2);
|
|
17854
|
-
const configPath = (0,
|
|
19027
|
+
const configPath = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush", "config.json");
|
|
17855
19028
|
let email = "";
|
|
17856
19029
|
try {
|
|
17857
|
-
const config = JSON.parse((0,
|
|
19030
|
+
const config = JSON.parse((0, import_node_fs31.readFileSync)(configPath, "utf-8"));
|
|
17858
19031
|
email = config.email || "";
|
|
17859
19032
|
} catch {
|
|
17860
19033
|
}
|
|
@@ -17871,9 +19044,9 @@ storeCmd.command("publish <url>").description("Publish a module from a git URL o
|
|
|
17871
19044
|
return;
|
|
17872
19045
|
}
|
|
17873
19046
|
try {
|
|
17874
|
-
const dir = (0,
|
|
17875
|
-
if (!(0,
|
|
17876
|
-
(0,
|
|
19047
|
+
const dir = (0, import_node_path21.join)((0, import_node_os8.homedir)(), ".threatcrush");
|
|
19048
|
+
if (!(0, import_node_fs31.existsSync)(dir)) (0, import_node_fs31.mkdirSync)(dir, { recursive: true });
|
|
19049
|
+
(0, import_node_fs31.writeFileSync)(configPath, JSON.stringify({ email }, null, 2));
|
|
17877
19050
|
console.log(source_default.dim(` Saved email to ${configPath}`));
|
|
17878
19051
|
} catch {
|
|
17879
19052
|
}
|