mjolnir-qa 4.0.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +269 -1
- package/README.br.md +10 -10
- package/README.md +12 -12
- package/dist/cli.d.mts +181 -4
- package/dist/cli.mjs +1481 -120
- package/dist/mcp/stdio.mjs +390 -193
- package/dist/scan-pipeline-DNYbnWjE.mjs +2 -0
- package/dist/{scan-pipeline-CCJdEKNa.mjs → scan-pipeline-DT8DWAMl.mjs} +344 -179
- package/package.json +25 -8
- package/dist/scan-pipeline-Bg-46cLS.mjs +0 -2
|
@@ -126,6 +126,7 @@ const DEDUCTIONS = {
|
|
|
126
126
|
//#region src/engine/completion.ts
|
|
127
127
|
function deriveCompletion(input) {
|
|
128
128
|
const truncationReasons = [...new Set(input.truncationReasons)].sort();
|
|
129
|
+
const degradations = [...input.degradations ?? []].sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
|
|
129
130
|
const reasons = /* @__PURE__ */ new Set();
|
|
130
131
|
if (input.discoveryTruncated) reasons.add("discovery-truncated");
|
|
131
132
|
if (input.rulesPartial) reasons.add("rules-partial");
|
|
@@ -135,14 +136,16 @@ function deriveCompletion(input) {
|
|
|
135
136
|
if (input.scopeUnrecognized > 0) reasons.add(`scope-unrecognized:${input.scopeUnrecognized}`);
|
|
136
137
|
if (input.parseFailed > 0) reasons.add(`parse-failed:${input.parseFailed}`);
|
|
137
138
|
if (input.parseFallbacks && input.parseFallbacks > 0) reasons.add(`parse-fallbacks:${input.parseFallbacks}`);
|
|
139
|
+
if (input.astFallbackFiles && input.astFallbackFiles > 0) reasons.add(`ast-budget-fallback-files:${input.astFallbackFiles}`);
|
|
138
140
|
if (input.scopeDegraded) reasons.add(`scope-degraded:${input.scopeDegraded}`);
|
|
139
141
|
if (input.runtimeIncomplete) reasons.add("runtime-incomplete");
|
|
140
142
|
if (input.identityIncomplete) reasons.add("identity-incomplete");
|
|
141
143
|
for (const reason of truncationReasons) reasons.add(`truncated:${reason}`);
|
|
144
|
+
for (const entry of degradations) reasons.add(`degraded:${entry.reason}:${entry.count}`);
|
|
142
145
|
const discoveryPartial = input.discoveryTruncated || input.scopeIgnored > 0 || input.scopeUnrecognized > 0 || input.scopeDegraded !== void 0;
|
|
143
146
|
const rulesPartial = input.rulesPartial || input.rulesCrashed > 0 || input.parseFailed > 0;
|
|
144
147
|
return {
|
|
145
|
-
partial: discoveryPartial || rulesPartial || input.skippedFiles > 0 || input.runtimeIncomplete === true || input.identityIncomplete === true || truncationReasons.length > 0,
|
|
148
|
+
partial: discoveryPartial || rulesPartial || input.skippedFiles > 0 || input.runtimeIncomplete === true || input.identityIncomplete === true || truncationReasons.length > 0 || degradations.length > 0,
|
|
146
149
|
analysisStatus: {
|
|
147
150
|
discovery: discoveryPartial ? "partial" : "complete",
|
|
148
151
|
rules: rulesPartial ? "partial" : "complete",
|
|
@@ -150,11 +153,49 @@ function deriveCompletion(input) {
|
|
|
150
153
|
rulesCrashed: input.rulesCrashed,
|
|
151
154
|
parseFallbacks: input.parseFallbacks ?? 0,
|
|
152
155
|
...truncationReasons.length > 0 ? { truncationReasons } : {},
|
|
156
|
+
...degradations.length > 0 ? { degradations } : {},
|
|
153
157
|
reasons: [...reasons].sort()
|
|
154
158
|
}
|
|
155
159
|
};
|
|
156
160
|
}
|
|
157
161
|
//#endregion
|
|
162
|
+
//#region src/engine/degradation-ledger.ts
|
|
163
|
+
/**
|
|
164
|
+
* Append-only. Records are never edited and never removed by a recording
|
|
165
|
+
* site; a reader takes a window, and the window is defined by index, so
|
|
166
|
+
* "delete my entry" is not an operation this module offers.
|
|
167
|
+
*/
|
|
168
|
+
const records = [];
|
|
169
|
+
/** Record one capability loss. `count` is for a site that fails N at once. */
|
|
170
|
+
function recordDegradation(reason, count = 1) {
|
|
171
|
+
if (!Number.isInteger(count) || count < 1) return;
|
|
172
|
+
for (let index = 0; index < count; index++) records.push({ reason });
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Mark where this reader's window opens. Everything appended from here on is
|
|
176
|
+
* what the reader will see; see the file header for what a concurrent scan
|
|
177
|
+
* does to that boundary.
|
|
178
|
+
*/
|
|
179
|
+
function beginDegradationWindow() {
|
|
180
|
+
return records.length;
|
|
181
|
+
}
|
|
182
|
+
/** The records appended since `marker`, without consuming them. */
|
|
183
|
+
function degradationsSince(marker) {
|
|
184
|
+
return records.slice(marker);
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Per-reason counts for a window, sorted by reason so the output is
|
|
188
|
+
* byte-deterministic for the same set of degradations.
|
|
189
|
+
*/
|
|
190
|
+
function summarizeDegradations(window) {
|
|
191
|
+
const counts = /* @__PURE__ */ new Map();
|
|
192
|
+
for (const record of window) counts.set(record.reason, (counts.get(record.reason) ?? 0) + 1);
|
|
193
|
+
return [...counts.entries()].map(([reason, count]) => ({
|
|
194
|
+
reason,
|
|
195
|
+
count
|
|
196
|
+
})).sort((a, b) => a.reason < b.reason ? -1 : a.reason > b.reason ? 1 : 0);
|
|
197
|
+
}
|
|
198
|
+
//#endregion
|
|
158
199
|
//#region src/engine/runtime-corroboration.ts
|
|
159
200
|
/**
|
|
160
201
|
* Stamp runtime corroboration + trust levels onto findings (mutates in
|
|
@@ -1105,6 +1146,120 @@ function createExecutor(name, concurrency, runOne) {
|
|
|
1105
1146
|
};
|
|
1106
1147
|
}
|
|
1107
1148
|
//#endregion
|
|
1149
|
+
//#region src/scope/git-resolve.ts
|
|
1150
|
+
/**
|
|
1151
|
+
* Absolute-path git resolution (audit S1).
|
|
1152
|
+
*
|
|
1153
|
+
* `execFileSync("git", …)` resolves through PATH — including the CWD on
|
|
1154
|
+
* Windows (CreateProcess searches the current directory before PATH for
|
|
1155
|
+
* extension-less names depending on NoDefaultCurrentDirectoryInExePath).
|
|
1156
|
+
* Scanning an UNTRUSTED repo therefore let a checked-in `git.exe` (or
|
|
1157
|
+
* `git.bat`/`git.cmd`) hijack Mjölnir's own git invocations: the attacker
|
|
1158
|
+
* controls the diff output, the merge-base decision, and
|
|
1159
|
+
* `--scope changed`'s "what is new" answer — the exact data CI gates on.
|
|
1160
|
+
*
|
|
1161
|
+
* The fix: resolve git ONCE per process to an ABSOLUTE path by walking
|
|
1162
|
+
* the PATH directly (never consulting the CWD), verify the candidate is
|
|
1163
|
+
* an existing file, and pass that path to execFileSync. Resolution is
|
|
1164
|
+
* memoized; a failure to find a real git anywhere on PATH is recorded
|
|
1165
|
+
* and callers degrade without invoking a bare name.
|
|
1166
|
+
*/
|
|
1167
|
+
let resolvedGit = void 0;
|
|
1168
|
+
/**
|
|
1169
|
+
* The absolute path of the git binary Mjölnir will invoke, or null when
|
|
1170
|
+
* PATH carries no executable `git` at all. Callers must degrade without
|
|
1171
|
+
* invoking a bare name.
|
|
1172
|
+
*/
|
|
1173
|
+
function resolveGitPath() {
|
|
1174
|
+
if (resolvedGit !== void 0) return resolvedGit;
|
|
1175
|
+
const candidates = [process.platform === "win32" ? "git.exe" : "git"];
|
|
1176
|
+
if (process.platform === "win32") {
|
|
1177
|
+
const pathExt = process.env["PATHEXT"]?.split(";").filter(Boolean) ?? [];
|
|
1178
|
+
for (const ext of pathExt) candidates.push(`git${ext.toLowerCase()}`);
|
|
1179
|
+
}
|
|
1180
|
+
const pathValue = process.env["PATH"] ?? "";
|
|
1181
|
+
for (const dir of pathValue.split(delimiter)) {
|
|
1182
|
+
if (dir === "") continue;
|
|
1183
|
+
if (!isAbsolute(dir)) continue;
|
|
1184
|
+
for (const candidate of candidates) {
|
|
1185
|
+
const full = join(dir, candidate);
|
|
1186
|
+
try {
|
|
1187
|
+
if (existsSync(full)) {
|
|
1188
|
+
resolvedGit = full;
|
|
1189
|
+
return resolvedGit;
|
|
1190
|
+
}
|
|
1191
|
+
} catch {}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
`${pathValue.length}`;
|
|
1195
|
+
resolvedGit = null;
|
|
1196
|
+
return resolvedGit;
|
|
1197
|
+
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Run git with the S1-resolved absolute binary path. Returns stdout, or
|
|
1200
|
+
* null when git is missing or the command fails — the same contract as
|
|
1201
|
+
* the previous inline `git()` helpers, with the hijack surface closed.
|
|
1202
|
+
*/
|
|
1203
|
+
function runGit(root, args) {
|
|
1204
|
+
const exe = resolveGitPath();
|
|
1205
|
+
if (!exe) return null;
|
|
1206
|
+
try {
|
|
1207
|
+
return execFileSync(exe, [
|
|
1208
|
+
"-C",
|
|
1209
|
+
root,
|
|
1210
|
+
...args
|
|
1211
|
+
], {
|
|
1212
|
+
encoding: "utf8",
|
|
1213
|
+
stdio: [
|
|
1214
|
+
"ignore",
|
|
1215
|
+
"pipe",
|
|
1216
|
+
"ignore"
|
|
1217
|
+
],
|
|
1218
|
+
timeout: 15e3
|
|
1219
|
+
});
|
|
1220
|
+
} catch {
|
|
1221
|
+
return null;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
//#endregion
|
|
1225
|
+
//#region src/lib/safe-json.ts
|
|
1226
|
+
/**
|
|
1227
|
+
* Reusable safe-JSON parse-and-validate helper.
|
|
1228
|
+
*
|
|
1229
|
+
* Replaces the `JSON.parse(text) as T` pattern with a two-step process:
|
|
1230
|
+
* 1. Try/catch around JSON.parse (descriptive error on bad JSON)
|
|
1231
|
+
* 2. Runtime validation of the parsed shape (caller-supplied predicate)
|
|
1232
|
+
*
|
|
1233
|
+
* The `validate` predicate is intentionally lightweight — it checks the
|
|
1234
|
+
* expected shape exists rather than exhaustively type-guarding every field.
|
|
1235
|
+
* Internal files (cache, hashes) use `isRecord`; user-facing files
|
|
1236
|
+
* (config, plugins) use stricter checks that throw descriptive errors.
|
|
1237
|
+
*/
|
|
1238
|
+
const MAX_JSON_BYTES = 16777216;
|
|
1239
|
+
function parseJsonFile(text, source, validate) {
|
|
1240
|
+
if (Buffer.byteLength(text, "utf8") > MAX_JSON_BYTES) throw new Error(`JSON source exceeds the ${MAX_JSON_BYTES}-byte limit: ${source}`);
|
|
1241
|
+
let parsed;
|
|
1242
|
+
try {
|
|
1243
|
+
parsed = JSON.parse(text);
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
throw new Error(`invalid JSON in ${source}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1246
|
+
}
|
|
1247
|
+
if (!validate(parsed)) throw new Error(`unexpected shape in ${source}`, { cause: parsed });
|
|
1248
|
+
return parsed;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Minimal validator: value is a plain object (not null, not array).
|
|
1252
|
+
* Sufficient for internal files (cache, hashes) where downstream
|
|
1253
|
+
* code already checks individual fields.
|
|
1254
|
+
*/
|
|
1255
|
+
function isRecord(v) {
|
|
1256
|
+
try {
|
|
1257
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1258
|
+
} catch {
|
|
1259
|
+
return false;
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
//#endregion
|
|
1108
1263
|
//#region src/engine/candidate-binding.ts
|
|
1109
1264
|
/**
|
|
1110
1265
|
* Candidate binding (plan V5-010).
|
|
@@ -1125,9 +1280,6 @@ const SHA40 = /^[a-f0-9]{40}$/;
|
|
|
1125
1280
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
1126
1281
|
const STATES = ["WORKING_CANDIDATE", "RELEASE_CANDIDATE"];
|
|
1127
1282
|
const AUTHORIZATION = ["NOT_AUTHORIZED", "AUTHORIZED"];
|
|
1128
|
-
function isRecord$1(value) {
|
|
1129
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1130
|
-
}
|
|
1131
1283
|
function sha256File(path) {
|
|
1132
1284
|
try {
|
|
1133
1285
|
return createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
@@ -1135,17 +1287,41 @@ function sha256File(path) {
|
|
|
1135
1287
|
return;
|
|
1136
1288
|
}
|
|
1137
1289
|
}
|
|
1290
|
+
/**
|
|
1291
|
+
* One git line, or nothing.
|
|
1292
|
+
*
|
|
1293
|
+
* Audit S1: the binary is the ABSOLUTE path `resolveGitPath()` found by
|
|
1294
|
+
* walking PATH itself, never the bare name. `bindRepository()` runs on every
|
|
1295
|
+
* scan (`scan-pipeline.ts`), and a bare `execFileSync("git", …)` lets a
|
|
1296
|
+
* `git.exe`/`git.bat`/`git.cmd` CHECKED INTO AN UNTRUSTED REPO hijack the
|
|
1297
|
+
* call on Windows, where CreateProcess searches the current directory before
|
|
1298
|
+
* PATH. The attacker then controls the `commit` and `tree` that land in
|
|
1299
|
+
* `runIdentity` and the evidence graph — the provenance a release decision
|
|
1300
|
+
* reads. This was the last bare-name exec left in `src/`; the invariant that
|
|
1301
|
+
* keeps it that way is `tests/contract/no-bare-process-exec.spec.ts`.
|
|
1302
|
+
*
|
|
1303
|
+
* The `-C <root>` shape (not `cwd`) matches every other hardened call site.
|
|
1304
|
+
* With no git on PATH at all, the answer is nothing — a scan of an unpacked
|
|
1305
|
+
* tarball has no commit, and its identity must say so rather than carrying a
|
|
1306
|
+
* placeholder that looks like provenance.
|
|
1307
|
+
*/
|
|
1138
1308
|
function gitLine(root, args) {
|
|
1309
|
+
const exe = resolveGitPath();
|
|
1310
|
+
if (!exe) return void 0;
|
|
1139
1311
|
try {
|
|
1140
|
-
const trimmed = execFileSync(
|
|
1141
|
-
|
|
1312
|
+
const trimmed = execFileSync(exe, [
|
|
1313
|
+
"-C",
|
|
1314
|
+
root,
|
|
1315
|
+
...args
|
|
1316
|
+
], {
|
|
1142
1317
|
encoding: "utf8",
|
|
1143
1318
|
stdio: [
|
|
1144
1319
|
"ignore",
|
|
1145
1320
|
"pipe",
|
|
1146
1321
|
"ignore"
|
|
1147
1322
|
],
|
|
1148
|
-
windowsHide: true
|
|
1323
|
+
windowsHide: true,
|
|
1324
|
+
timeout: 15e3
|
|
1149
1325
|
}).trim();
|
|
1150
1326
|
return trimmed.length > 0 ? trimmed : void 0;
|
|
1151
1327
|
} catch {
|
|
@@ -1176,9 +1352,9 @@ function bindRepository(root) {
|
|
|
1176
1352
|
* contradiction the release decision evaluator treats as BLOCKED).
|
|
1177
1353
|
*/
|
|
1178
1354
|
function parseCandidateBinding(raw) {
|
|
1179
|
-
if (!isRecord
|
|
1355
|
+
if (!isRecord(raw)) return null;
|
|
1180
1356
|
const identity = raw.identity;
|
|
1181
|
-
if (!isRecord
|
|
1357
|
+
if (!isRecord(identity)) return null;
|
|
1182
1358
|
const state = identity.state;
|
|
1183
1359
|
const candidateSha = identity.candidateSha;
|
|
1184
1360
|
const baseSha = identity.baseSha;
|
|
@@ -1227,7 +1403,7 @@ function readCandidateBinding(root, manifestPath = CANDIDATE_MANIFEST_PATH) {
|
|
|
1227
1403
|
* scripts/sync-sarif-version.cjs and guarded by the version-consistency
|
|
1228
1404
|
* spec. cli.ts re-exports this as CLI_VERSION.
|
|
1229
1405
|
*/
|
|
1230
|
-
const ENGINE_VERSION = "
|
|
1406
|
+
const ENGINE_VERSION = "5.0.0";
|
|
1231
1407
|
//#endregion
|
|
1232
1408
|
//#region src/engine/contract-versions.ts
|
|
1233
1409
|
/**
|
|
@@ -1356,9 +1532,12 @@ function discoverWorkspace(rootDir) {
|
|
|
1356
1532
|
const root = findProjectRoot(rootDir);
|
|
1357
1533
|
if (!root) return null;
|
|
1358
1534
|
let pkg = {};
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1535
|
+
const manifestPath = join(root, "package.json");
|
|
1536
|
+
if (existsSync(manifestPath)) try {
|
|
1537
|
+
pkg = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
1538
|
+
} catch {
|
|
1539
|
+
recordDegradation("workspace-manifest-unreadable");
|
|
1540
|
+
}
|
|
1362
1541
|
const rawWorkspaces = pkg["workspaces"];
|
|
1363
1542
|
let globs = [];
|
|
1364
1543
|
if (Array.isArray(rawWorkspaces)) globs = rawWorkspaces.filter((w) => typeof w === "string");
|
|
@@ -1375,40 +1554,6 @@ function basename$1(p) {
|
|
|
1375
1554
|
return parts[parts.length - 1];
|
|
1376
1555
|
}
|
|
1377
1556
|
//#endregion
|
|
1378
|
-
//#region src/lib/safe-json.ts
|
|
1379
|
-
/**
|
|
1380
|
-
* Reusable safe-JSON parse-and-validate helper.
|
|
1381
|
-
*
|
|
1382
|
-
* Replaces the `JSON.parse(text) as T` pattern with a two-step process:
|
|
1383
|
-
* 1. Try/catch around JSON.parse (descriptive error on bad JSON)
|
|
1384
|
-
* 2. Runtime validation of the parsed shape (caller-supplied predicate)
|
|
1385
|
-
*
|
|
1386
|
-
* The `validate` predicate is intentionally lightweight — it checks the
|
|
1387
|
-
* expected shape exists rather than exhaustively type-guarding every field.
|
|
1388
|
-
* Internal files (cache, hashes) use `isRecord`; user-facing files
|
|
1389
|
-
* (config, plugins) use stricter checks that throw descriptive errors.
|
|
1390
|
-
*/
|
|
1391
|
-
const MAX_JSON_BYTES = 16777216;
|
|
1392
|
-
function parseJsonFile(text, source, validate) {
|
|
1393
|
-
if (Buffer.byteLength(text, "utf8") > MAX_JSON_BYTES) throw new Error(`JSON source exceeds the ${MAX_JSON_BYTES}-byte limit: ${source}`);
|
|
1394
|
-
let parsed;
|
|
1395
|
-
try {
|
|
1396
|
-
parsed = JSON.parse(text);
|
|
1397
|
-
} catch (err) {
|
|
1398
|
-
throw new Error(`invalid JSON in ${source}: ${err instanceof Error ? err.message : String(err)}`, { cause: err });
|
|
1399
|
-
}
|
|
1400
|
-
if (!validate(parsed)) throw new Error(`unexpected shape in ${source}`, { cause: parsed });
|
|
1401
|
-
return parsed;
|
|
1402
|
-
}
|
|
1403
|
-
/**
|
|
1404
|
-
* Minimal validator: value is a plain object (not null, not array).
|
|
1405
|
-
* Sufficient for internal files (cache, hashes) where downstream
|
|
1406
|
-
* code already checks individual fields.
|
|
1407
|
-
*/
|
|
1408
|
-
function isRecord(v) {
|
|
1409
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1410
|
-
}
|
|
1411
|
-
//#endregion
|
|
1412
1557
|
//#region src/lib/fs-bounded.ts
|
|
1413
1558
|
function closeQuiet(fd) {
|
|
1414
1559
|
try {
|
|
@@ -1638,6 +1783,12 @@ function validate(cfg, knownRuleIds) {
|
|
|
1638
1783
|
* Expiry is evaluated only from an explicit ISO `expires` date. A missing
|
|
1639
1784
|
* date remains active and is reported as having no expiry. Config-file mtime
|
|
1640
1785
|
* is never an expiry anchor.
|
|
1786
|
+
*
|
|
1787
|
+
* `now` defaults to the current instant so existing callers stay correct, but
|
|
1788
|
+
* a verdict function that reads the clock invisibly is a verdict nobody can
|
|
1789
|
+
* test at a boundary. `tests/contract/deterministic-clock.spec.ts` audits
|
|
1790
|
+
* `src/config/**` for a call site that supplies its own `new Date()` instead
|
|
1791
|
+
* of threading one through.
|
|
1641
1792
|
*/
|
|
1642
1793
|
function isSuppressionActive(ign, now = /* @__PURE__ */ new Date()) {
|
|
1643
1794
|
if (!ign.expires) return true;
|
|
@@ -2038,6 +2189,7 @@ function parseTsFile(file) {
|
|
|
2038
2189
|
}
|
|
2039
2190
|
return p.createSourceFile(file.path, file.text);
|
|
2040
2191
|
} catch {
|
|
2192
|
+
recordDegradation("ast-parse-failed");
|
|
2041
2193
|
return;
|
|
2042
2194
|
}
|
|
2043
2195
|
}
|
|
@@ -2093,6 +2245,7 @@ function commentAndStringRanges(ctx) {
|
|
|
2093
2245
|
}
|
|
2094
2246
|
return ranges;
|
|
2095
2247
|
} catch {
|
|
2248
|
+
recordDegradation("ast-range-scan-failed");
|
|
2096
2249
|
return [];
|
|
2097
2250
|
}
|
|
2098
2251
|
}
|
|
@@ -2140,6 +2293,7 @@ function getCodeOnlyText(file) {
|
|
|
2140
2293
|
for (const r of ranges) for (let i = r.start; i < r.end && i < chars.length; i++) if (chars[i] !== "\n" && chars[i] !== "\r") chars[i] = " ";
|
|
2141
2294
|
return chars.join("");
|
|
2142
2295
|
} catch {
|
|
2296
|
+
recordDegradation("ast-mask-unavailable");
|
|
2143
2297
|
return file.text;
|
|
2144
2298
|
}
|
|
2145
2299
|
}
|
|
@@ -2524,6 +2678,44 @@ function frameworkFilterApplies(rule, file) {
|
|
|
2524
2678
|
return rule.frameworks.some((f) => tags.includes(f));
|
|
2525
2679
|
}
|
|
2526
2680
|
//#endregion
|
|
2681
|
+
//#region src/engine/shared-run-rules.ts
|
|
2682
|
+
/**
|
|
2683
|
+
* Run ONE rule with crash isolation, and return what it produced.
|
|
2684
|
+
*
|
|
2685
|
+
* This is the block that was copy-pasted into all seven language adapters:
|
|
2686
|
+
*
|
|
2687
|
+
* try { for (const f of rule.run(x)) emit(f, rule.id, rule.category); }
|
|
2688
|
+
* catch (error) { onCrash?.(rule.id, error); }
|
|
2689
|
+
*
|
|
2690
|
+
* Extracting it is worth doing for one reason beyond line count. Crash
|
|
2691
|
+
* isolation is the mechanism that lets a third-party or experimental
|
|
2692
|
+
* detector fail without ending a scan, and it is the mechanism the whole
|
|
2693
|
+
* `rulesCrashed` count depends on. Seven copies means seven chances to
|
|
2694
|
+
* write `catch {}` and lose the count, and `tests/contract/
|
|
2695
|
+
* no-uncounted-degradation.spec.ts` can only audit what it can see.
|
|
2696
|
+
*
|
|
2697
|
+
* `null` means the rule CRASHED, which is not the same as an empty array:
|
|
2698
|
+
* an empty array is the rule abstaining, and the adapters skip emission for
|
|
2699
|
+
* both but the distinction is why the caller must not conflate them.
|
|
2700
|
+
*
|
|
2701
|
+
* Deliberately NOT extracted: the per-adapter filter chain and the budget
|
|
2702
|
+
* check. Those genuinely differ — the TypeScript adapter injects an AST,
|
|
2703
|
+
* framework tags, a lazy `codeText` getter, `appliesTo` scoping, config-only
|
|
2704
|
+
* gating and `configGateMatches`, and it ABORTS the whole file on budget
|
|
2705
|
+
* exhaustion where `runRulesShared` only `continue`s to the next rule.
|
|
2706
|
+
* Routing every adapter through `runRulesShared` as written would silently
|
|
2707
|
+
* change which rules run and when, which is precisely the unmeasured claim
|
|
2708
|
+
* this program exists to delete. This helper takes the part they share.
|
|
2709
|
+
*/
|
|
2710
|
+
function runRuleIsolated(rule, file, onCrash) {
|
|
2711
|
+
try {
|
|
2712
|
+
return rule.run(file);
|
|
2713
|
+
} catch (error) {
|
|
2714
|
+
onCrash?.(rule.id, error);
|
|
2715
|
+
return null;
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2718
|
+
//#endregion
|
|
2527
2719
|
//#region src/adapters/typescript.ts
|
|
2528
2720
|
/**
|
|
2529
2721
|
* TypeScript/JavaScript adapter (R1).
|
|
@@ -2733,11 +2925,9 @@ const typescriptAdapter = {
|
|
|
2733
2925
|
budget.onExceeded();
|
|
2734
2926
|
return;
|
|
2735
2927
|
}
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
onCrash?.(rule.id, error);
|
|
2740
|
-
}
|
|
2928
|
+
const produced = runRuleIsolated(rule, enriched, onCrash);
|
|
2929
|
+
if (produced === null) continue;
|
|
2930
|
+
for (const f of produced) emit(f, rule.id, rule.category);
|
|
2741
2931
|
}
|
|
2742
2932
|
}
|
|
2743
2933
|
};
|
|
@@ -2765,6 +2955,7 @@ function loadWorkspaceShim(root) {
|
|
|
2765
2955
|
workspaceGlobs: []
|
|
2766
2956
|
};
|
|
2767
2957
|
} catch {
|
|
2958
|
+
recordDegradation("workspace-manifest-unreadable");
|
|
2768
2959
|
return null;
|
|
2769
2960
|
}
|
|
2770
2961
|
}
|
|
@@ -3119,11 +3310,9 @@ const pythonAdapter = {
|
|
|
3119
3310
|
budget.onExceeded();
|
|
3120
3311
|
return;
|
|
3121
3312
|
}
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
onCrash?.(rule.id, error);
|
|
3126
|
-
}
|
|
3313
|
+
const produced = runRuleIsolated(rule, enriched, onCrash);
|
|
3314
|
+
if (produced === null) continue;
|
|
3315
|
+
for (const f of produced) emit(f, rule.id, rule.category);
|
|
3127
3316
|
}
|
|
3128
3317
|
},
|
|
3129
3318
|
async parseAst(file) {
|
|
@@ -3276,11 +3465,9 @@ const javaAdapter = {
|
|
|
3276
3465
|
budget.onExceeded();
|
|
3277
3466
|
return;
|
|
3278
3467
|
}
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
onCrash?.(rule.id, error);
|
|
3283
|
-
}
|
|
3468
|
+
const produced = runRuleIsolated(rule, enriched, onCrash);
|
|
3469
|
+
if (produced === null) continue;
|
|
3470
|
+
for (const f of produced) emit(f, rule.id, rule.category);
|
|
3284
3471
|
}
|
|
3285
3472
|
}
|
|
3286
3473
|
};
|
|
@@ -3434,11 +3621,9 @@ const csharpAdapter = {
|
|
|
3434
3621
|
budget.onExceeded();
|
|
3435
3622
|
return;
|
|
3436
3623
|
}
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
onCrash?.(rule.id, error);
|
|
3441
|
-
}
|
|
3624
|
+
const produced = runRuleIsolated(rule, enriched, onCrash);
|
|
3625
|
+
if (produced === null) continue;
|
|
3626
|
+
for (const f of produced) emit(f, rule.id, rule.category);
|
|
3442
3627
|
}
|
|
3443
3628
|
}
|
|
3444
3629
|
};
|
|
@@ -3616,6 +3801,7 @@ const githubActionsAdapter = {
|
|
|
3616
3801
|
try {
|
|
3617
3802
|
entries = readdirSync(wfDir);
|
|
3618
3803
|
} catch {
|
|
3804
|
+
recordDegradation("ci-workflow-listing-unreadable");
|
|
3619
3805
|
return;
|
|
3620
3806
|
}
|
|
3621
3807
|
for (const name of entries) {
|
|
@@ -3656,15 +3842,13 @@ const githubActionsAdapter = {
|
|
|
3656
3842
|
budget.onExceeded();
|
|
3657
3843
|
return;
|
|
3658
3844
|
}
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3665
|
-
|
|
3666
|
-
onCrash?.(rule.id, error);
|
|
3667
|
-
}
|
|
3845
|
+
const produced = runRuleIsolated(rule, {
|
|
3846
|
+
path: file.path,
|
|
3847
|
+
text: file.text,
|
|
3848
|
+
ast: doc
|
|
3849
|
+
}, onCrash);
|
|
3850
|
+
if (produced === null) continue;
|
|
3851
|
+
for (const f of produced) emit(f, rule.id, rule.category);
|
|
3668
3852
|
}
|
|
3669
3853
|
}
|
|
3670
3854
|
};
|
|
@@ -3974,15 +4158,13 @@ const azurePipelinesAdapter = {
|
|
|
3974
4158
|
budget.onExceeded();
|
|
3975
4159
|
return;
|
|
3976
4160
|
}
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
onCrash?.(rule.id, error);
|
|
3985
|
-
}
|
|
4161
|
+
const produced = runRuleIsolated(rule, {
|
|
4162
|
+
path: file.path,
|
|
4163
|
+
text: file.text,
|
|
4164
|
+
ast: doc
|
|
4165
|
+
}, onCrash);
|
|
4166
|
+
if (produced === null) continue;
|
|
4167
|
+
for (const f of produced) emit(f, rule.id, rule.category);
|
|
3986
4168
|
}
|
|
3987
4169
|
}
|
|
3988
4170
|
};
|
|
@@ -4053,14 +4235,12 @@ const jenkinsAdapter = {
|
|
|
4053
4235
|
budget.onExceeded();
|
|
4054
4236
|
return;
|
|
4055
4237
|
}
|
|
4056
|
-
|
|
4057
|
-
|
|
4058
|
-
|
|
4059
|
-
|
|
4060
|
-
|
|
4061
|
-
|
|
4062
|
-
onCrash?.(rule.id, error);
|
|
4063
|
-
}
|
|
4238
|
+
const produced = runRuleIsolated(rule, {
|
|
4239
|
+
path: file.path,
|
|
4240
|
+
text: file.text
|
|
4241
|
+
}, onCrash);
|
|
4242
|
+
if (produced === null) continue;
|
|
4243
|
+
for (const f of produced) emit(f, rule.id, rule.category);
|
|
4064
4244
|
}
|
|
4065
4245
|
}
|
|
4066
4246
|
};
|
|
@@ -4224,82 +4404,6 @@ const SEARCHED_FOR = [
|
|
|
4224
4404
|
}
|
|
4225
4405
|
];
|
|
4226
4406
|
//#endregion
|
|
4227
|
-
//#region src/scope/git-resolve.ts
|
|
4228
|
-
/**
|
|
4229
|
-
* Absolute-path git resolution (audit S1).
|
|
4230
|
-
*
|
|
4231
|
-
* `execFileSync("git", …)` resolves through PATH — including the CWD on
|
|
4232
|
-
* Windows (CreateProcess searches the current directory before PATH for
|
|
4233
|
-
* extension-less names depending on NoDefaultCurrentDirectoryInExePath).
|
|
4234
|
-
* Scanning an UNTRUSTED repo therefore let a checked-in `git.exe` (or
|
|
4235
|
-
* `git.bat`/`git.cmd`) hijack Mjölnir's own git invocations: the attacker
|
|
4236
|
-
* controls the diff output, the merge-base decision, and
|
|
4237
|
-
* `--scope changed`'s "what is new" answer — the exact data CI gates on.
|
|
4238
|
-
*
|
|
4239
|
-
* The fix: resolve git ONCE per process to an ABSOLUTE path by walking
|
|
4240
|
-
* the PATH directly (never consulting the CWD), verify the candidate is
|
|
4241
|
-
* an existing file, and pass that path to execFileSync. Resolution is
|
|
4242
|
-
* memoized; a failure to find a real git anywhere on PATH is recorded
|
|
4243
|
-
* and callers degrade without invoking a bare name.
|
|
4244
|
-
*/
|
|
4245
|
-
let resolvedGit = void 0;
|
|
4246
|
-
/**
|
|
4247
|
-
* The absolute path of the git binary Mjölnir will invoke, or null when
|
|
4248
|
-
* PATH carries no executable `git` at all. Callers must degrade without
|
|
4249
|
-
* invoking a bare name.
|
|
4250
|
-
*/
|
|
4251
|
-
function resolveGitPath() {
|
|
4252
|
-
if (resolvedGit !== void 0) return resolvedGit;
|
|
4253
|
-
const candidates = [process.platform === "win32" ? "git.exe" : "git"];
|
|
4254
|
-
if (process.platform === "win32") {
|
|
4255
|
-
const pathExt = process.env["PATHEXT"]?.split(";").filter(Boolean) ?? [];
|
|
4256
|
-
for (const ext of pathExt) candidates.push(`git${ext.toLowerCase()}`);
|
|
4257
|
-
}
|
|
4258
|
-
const pathValue = process.env["PATH"] ?? "";
|
|
4259
|
-
for (const dir of pathValue.split(delimiter)) {
|
|
4260
|
-
if (dir === "") continue;
|
|
4261
|
-
if (!isAbsolute(dir)) continue;
|
|
4262
|
-
for (const candidate of candidates) {
|
|
4263
|
-
const full = join(dir, candidate);
|
|
4264
|
-
try {
|
|
4265
|
-
if (existsSync(full)) {
|
|
4266
|
-
resolvedGit = full;
|
|
4267
|
-
return resolvedGit;
|
|
4268
|
-
}
|
|
4269
|
-
} catch {}
|
|
4270
|
-
}
|
|
4271
|
-
}
|
|
4272
|
-
`${pathValue.length}`;
|
|
4273
|
-
resolvedGit = null;
|
|
4274
|
-
return resolvedGit;
|
|
4275
|
-
}
|
|
4276
|
-
/**
|
|
4277
|
-
* Run git with the S1-resolved absolute binary path. Returns stdout, or
|
|
4278
|
-
* null when git is missing or the command fails — the same contract as
|
|
4279
|
-
* the previous inline `git()` helpers, with the hijack surface closed.
|
|
4280
|
-
*/
|
|
4281
|
-
function runGit(root, args) {
|
|
4282
|
-
const exe = resolveGitPath();
|
|
4283
|
-
if (!exe) return null;
|
|
4284
|
-
try {
|
|
4285
|
-
return execFileSync(exe, [
|
|
4286
|
-
"-C",
|
|
4287
|
-
root,
|
|
4288
|
-
...args
|
|
4289
|
-
], {
|
|
4290
|
-
encoding: "utf8",
|
|
4291
|
-
stdio: [
|
|
4292
|
-
"ignore",
|
|
4293
|
-
"pipe",
|
|
4294
|
-
"ignore"
|
|
4295
|
-
],
|
|
4296
|
-
timeout: 15e3
|
|
4297
|
-
});
|
|
4298
|
-
} catch {
|
|
4299
|
-
return null;
|
|
4300
|
-
}
|
|
4301
|
-
}
|
|
4302
|
-
//#endregion
|
|
4303
4407
|
//#region src/scope/changed.ts
|
|
4304
4408
|
/**
|
|
4305
4409
|
* Changed-scope engine (Sprint-Plan W6, Product-MVP §9 `--scope changed`).
|
|
@@ -11781,6 +11885,53 @@ const RETIRED_RULE_IDS = [
|
|
|
11781
11885
|
"QA-CS-111"
|
|
11782
11886
|
];
|
|
11783
11887
|
//#endregion
|
|
11888
|
+
//#region src/lib/compare.ts
|
|
11889
|
+
/**
|
|
11890
|
+
* Order that does not depend on who is asking.
|
|
11891
|
+
*
|
|
11892
|
+
* `String.prototype.localeCompare` with no locale argument resolves against
|
|
11893
|
+
* the PROCESS's default locale — the ICU collation chosen by `LANG`,
|
|
11894
|
+
* `LC_ALL` or the OS. So a sort written as
|
|
11895
|
+
* `[...xs].sort((a, b) => a.localeCompare(b))` produces a different order on
|
|
11896
|
+
* a developer's `de_DE` machine than on an en-US CI runner. For a display
|
|
11897
|
+
* list that is a nuisance; for anything that reaches a hash, a cache key, a
|
|
11898
|
+
* digest or a machine contract it is a correctness bug, because the same
|
|
11899
|
+
* repository then produces two different fingerprints.
|
|
11900
|
+
*
|
|
11901
|
+
* Two comparators, one per kind of site:
|
|
11902
|
+
*
|
|
11903
|
+
* compareCodePoints — pure UTF-16 code-unit order. Identical everywhere,
|
|
11904
|
+
* independent of ICU version, and already what `Array.prototype.sort`
|
|
11905
|
+
* defaults to. Use for every value that reaches a fingerprint, a digest,
|
|
11906
|
+
* a cache key, or a contract, and for any ordering with no human-facing
|
|
11907
|
+
* reason to be pretty.
|
|
11908
|
+
*
|
|
11909
|
+
* compareLocalized(locale) — the same collator, PINNED to a named locale
|
|
11910
|
+
* instead of inheriting the environment, for a surface that genuinely
|
|
11911
|
+
* wants readable order (a table of rule titles, a Mermaid legend). Pinned
|
|
11912
|
+
* rather than removed because a reader-facing list of "Test / Logic /
|
|
11913
|
+
* Mock" should not be ordered by code unit.
|
|
11914
|
+
*
|
|
11915
|
+
* A bare `localeCompare` with no second argument in `src/` is a defect and
|
|
11916
|
+
* `tests/contract/deterministic-ordering.spec.ts` fails the build on it.
|
|
11917
|
+
*/
|
|
11918
|
+
/** Code-unit order. Locale-free, ICU-free, and the Array.sort default. */
|
|
11919
|
+
function compareCodePoints(a, b) {
|
|
11920
|
+
if (a === b) return 0;
|
|
11921
|
+
return a < b ? -1 : 1;
|
|
11922
|
+
}
|
|
11923
|
+
/**
|
|
11924
|
+
* A readable-order comparator pinned to `locale`.
|
|
11925
|
+
*
|
|
11926
|
+
* The pin is the point: `a.localeCompare(b)` reads the environment, and the
|
|
11927
|
+
* environment is a machine setting, not a property of the code. Callers that
|
|
11928
|
+
* want stable output ask for a named locale and get the same answer on every
|
|
11929
|
+
* machine that ships the same ICU data.
|
|
11930
|
+
*/
|
|
11931
|
+
function compareLocalized(locale) {
|
|
11932
|
+
return (a, b) => a.localeCompare(b, locale);
|
|
11933
|
+
}
|
|
11934
|
+
//#endregion
|
|
11784
11935
|
//#region src/scorer/scorer.ts
|
|
11785
11936
|
/**
|
|
11786
11937
|
* Transparent scorer (Product-MVP §8).
|
|
@@ -11837,7 +11988,7 @@ function computeDimensions(findings) {
|
|
|
11837
11988
|
deductions.set(f.category, deductions.get(f.category) + deductionFor(f));
|
|
11838
11989
|
}
|
|
11839
11990
|
for (const dim of byCategory.values()) dim.score = Math.max(0, 100 - deductions.get(dim.category));
|
|
11840
|
-
return [...byCategory.values()].sort((a, b) => a.category
|
|
11991
|
+
return [...byCategory.values()].sort((a, b) => compareCodePoints(a.category, b.category));
|
|
11841
11992
|
}
|
|
11842
11993
|
/**
|
|
11843
11994
|
* Deduction-mass ceilings (product-gap-remediation master plan P2,
|
|
@@ -12279,7 +12430,7 @@ function groupByRootCause(findings) {
|
|
|
12279
12430
|
function correlateFindings(findings) {
|
|
12280
12431
|
const conclusions = [];
|
|
12281
12432
|
const rootCauseGroups = groupByRootCause(findings);
|
|
12282
|
-
for (const [rootCause, group] of Array.from(rootCauseGroups.entries()).sort((a, b) => a[0]
|
|
12433
|
+
for (const [rootCause, group] of Array.from(rootCauseGroups.entries()).sort((a, b) => compareLocalized("en")(a[0], b[0]))) {
|
|
12283
12434
|
if (group.length < 2) continue;
|
|
12284
12435
|
const findingIds = group.map((f) => f.findingId ?? f.ruleId);
|
|
12285
12436
|
conclusions.push({
|
|
@@ -12291,7 +12442,7 @@ function correlateFindings(findings) {
|
|
|
12291
12442
|
});
|
|
12292
12443
|
}
|
|
12293
12444
|
const fileGroups = groupByFile(findings);
|
|
12294
|
-
for (const [file, group] of Array.from(fileGroups.entries()).sort((a, b) => a[0]
|
|
12445
|
+
for (const [file, group] of Array.from(fileGroups.entries()).sort((a, b) => compareCodePoints(a[0], b[0]))) {
|
|
12295
12446
|
if (group.length < 2) continue;
|
|
12296
12447
|
const alreadyConvergent = /* @__PURE__ */ new Set();
|
|
12297
12448
|
for (const c of conclusions) if (c.conclusionType === "CONVERGENT") for (const id of c.findingIds) alreadyConvergent.add(id);
|
|
@@ -13336,9 +13487,10 @@ function hashDir(dir, hash, depth) {
|
|
|
13336
13487
|
try {
|
|
13337
13488
|
entries = readdirSync(dir, { withFileTypes: true });
|
|
13338
13489
|
} catch {
|
|
13490
|
+
recordDegradation("rules-tree-listing-unreadable");
|
|
13339
13491
|
return;
|
|
13340
13492
|
}
|
|
13341
|
-
entries.sort((a, b) => a.name
|
|
13493
|
+
entries.sort((a, b) => compareCodePoints(a.name, b.name));
|
|
13342
13494
|
for (const entry of entries) {
|
|
13343
13495
|
if (entry.name === "node_modules" || entry.name === ".git") continue;
|
|
13344
13496
|
const full = join(dir, entry.name);
|
|
@@ -14916,11 +15068,11 @@ function listFiles(dir) {
|
|
|
14916
15068
|
* `mjolnir suppressions` — every suppression stays visible.
|
|
14917
15069
|
*/
|
|
14918
15070
|
const ui = plainContext();
|
|
14919
|
-
function loadSuppressions(root) {
|
|
15071
|
+
function loadSuppressions(root, now = /* @__PURE__ */ new Date()) {
|
|
14920
15072
|
const { config } = loadConfig(root);
|
|
14921
15073
|
const entries = (config.ignore ?? []).map((ign) => ({
|
|
14922
15074
|
...ign,
|
|
14923
|
-
status: isSuppressionActive(ign,
|
|
15075
|
+
status: isSuppressionActive(ign, now) ? "active" : "expired"
|
|
14924
15076
|
}));
|
|
14925
15077
|
return {
|
|
14926
15078
|
total: entries.length,
|
|
@@ -15689,6 +15841,7 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
15689
15841
|
let rulesPartial = false;
|
|
15690
15842
|
let parseFailed = 0;
|
|
15691
15843
|
let parseFallbacks = 0;
|
|
15844
|
+
let astFallbackFiles = 0;
|
|
15692
15845
|
let scanned = 0;
|
|
15693
15846
|
let analyzed = 0;
|
|
15694
15847
|
for (const path of testFiles) {
|
|
@@ -15732,6 +15885,10 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
15732
15885
|
});
|
|
15733
15886
|
}
|
|
15734
15887
|
const wantsAst = adapter.parseAst !== void 0 && Date.now() <= deadline;
|
|
15888
|
+
if (adapter.parseAst !== void 0 && !wantsAst) {
|
|
15889
|
+
truncationReasons.add("ast-budget-fallback");
|
|
15890
|
+
astFallbackFiles++;
|
|
15891
|
+
}
|
|
15735
15892
|
const identity = (mode) => ({
|
|
15736
15893
|
relPath,
|
|
15737
15894
|
adapterId: adapter.id,
|
|
@@ -15849,7 +16006,8 @@ async function runFileAnalysisPhase(findings, testFiles, workspace, activeRules,
|
|
|
15849
16006
|
parseFailed,
|
|
15850
16007
|
parseFallbacks,
|
|
15851
16008
|
scanned,
|
|
15852
|
-
analyzed
|
|
16009
|
+
analyzed,
|
|
16010
|
+
astFallbackFiles
|
|
15853
16011
|
};
|
|
15854
16012
|
}
|
|
15855
16013
|
/**
|
|
@@ -16077,6 +16235,8 @@ function assembleScanResult(o) {
|
|
|
16077
16235
|
scopeUnrecognized: o.scopeUnrecognized,
|
|
16078
16236
|
parseFailed: o.parseFailed,
|
|
16079
16237
|
parseFallbacks: o.parseFallbacks ?? 0,
|
|
16238
|
+
...o.astFallbackFiles !== void 0 ? { astFallbackFiles: o.astFallbackFiles } : {},
|
|
16239
|
+
...o.degradations !== void 0 ? { degradations: o.degradations } : {},
|
|
16080
16240
|
...o.scopeInfo.degraded !== void 0 ? { scopeDegraded: o.scopeInfo.degraded } : {},
|
|
16081
16241
|
...o.runtimeIncomplete !== void 0 ? { runtimeIncomplete: o.runtimeIncomplete } : {},
|
|
16082
16242
|
identityIncomplete
|
|
@@ -16195,13 +16355,15 @@ function assembleScanResult(o) {
|
|
|
16195
16355
|
*/
|
|
16196
16356
|
async function runScan(args, hooks = {}) {
|
|
16197
16357
|
const started = Date.now();
|
|
16358
|
+
const degradationWindow = beginDegradationWindow();
|
|
16198
16359
|
const requestedDuration = Number.isFinite(args.maxDurationMs) ? args.maxDurationMs : DEFAULT_MAX_DURATION_MS;
|
|
16199
16360
|
const deadline = started + Math.min(Math.max(1, requestedDuration), MAX_DURATION_MS);
|
|
16200
16361
|
const discovered = discoverWorkspace(args.target);
|
|
16201
16362
|
const targetAbs = resolve(args.target);
|
|
16202
16363
|
const scanRoot = discovered && discovered.root !== targetAbs && targetAbs.startsWith(discovered.root + sep) ? {
|
|
16203
16364
|
...discovered,
|
|
16204
|
-
root: targetAbs
|
|
16365
|
+
root: targetAbs,
|
|
16366
|
+
packageJson: {}
|
|
16205
16367
|
} : discovered ?? fallbackWorkspace(targetAbs);
|
|
16206
16368
|
const workspace = scanRoot;
|
|
16207
16369
|
if (args.verbose) hooks.onConfigWarning?.(`verbose: scan root (config/ignore anchor): ${workspace.root}`);
|
|
@@ -16286,6 +16448,7 @@ async function runScan(args, hooks = {}) {
|
|
|
16286
16448
|
rulesPartial = analysis.rulesPartial;
|
|
16287
16449
|
parseFailed = analysis.parseFailed;
|
|
16288
16450
|
const parseFallbacks = analysis.parseFallbacks;
|
|
16451
|
+
const astFallbackFiles = analysis.astFallbackFiles;
|
|
16289
16452
|
const scanned = analysis.scanned;
|
|
16290
16453
|
const analyzed = analysis.analyzed;
|
|
16291
16454
|
const postScan = applyPostScanProcessing(findings, workspace, args, hooks, scanRoot, declarationsByFile, testDeclarationCount, tierByRuleId, REVISION_BY_RULE_ID);
|
|
@@ -16304,6 +16467,8 @@ async function runScan(args, hooks = {}) {
|
|
|
16304
16467
|
scopeUnrecognized,
|
|
16305
16468
|
parseFailed,
|
|
16306
16469
|
parseFallbacks,
|
|
16470
|
+
astFallbackFiles,
|
|
16471
|
+
degradations: summarizeDegradations(degradationsSince(degradationWindow)),
|
|
16307
16472
|
scanned,
|
|
16308
16473
|
analyzed,
|
|
16309
16474
|
testFiles,
|
|
@@ -16331,6 +16496,6 @@ async function runScan(args, hooks = {}) {
|
|
|
16331
16496
|
return result;
|
|
16332
16497
|
}
|
|
16333
16498
|
//#endregion
|
|
16334
|
-
export { BADGE_BAND as $, plainContext as A,
|
|
16499
|
+
export { BADGE_BAND as $, plainContext as A, ConfigValidationError as At, shouldUseAscii as B, MEASURED_FP as Bt, runForensics as C, parseTsFile as Ct, nextStep as D, createIgnoreMatcher as Dt, buildFooter as E, LIMITS$1 as Et, padTo as F, isRecord as Ft, countOrNull as G, DEDUCTIONS as Gt, atomicTempPath as H, degradationsSince as Ht, palette as I, parseJsonFile as It, evidenceTag as J, SEVERITY_ORDER as Jt, deriveScoreState as K, QA_IMPACT_LABELS as Kt, sanitizeData as L, resolveGitPath as Lt, severityIcon as M, loadConfig as Mt, box as N, readFileBounded as Nt, okIcon as O, globToRegExp as Ot, measure as P, ENGINE_VERSION as Pt, TRUST_RUNGS as Q, isValidCategory as Qt, scoreGauge as R, runGit as Rt, renderSuppressions as S, computeCodeText as St, FLAKE_GLYPH as T, DEFAULT_IGNORE_MATCHER as Tt, sweepStaleTempFiles as U, recordDegradation as Ut, wrapText as V, beginDegradationWindow as Vt, writeFileAtomic as W, summarizeDegradations as Wt, testsAnalyzedCell as X, deriveEvidenceLevel as Xt, headlineFor as Y, TRUST_ORDER as Yt, verdictFor as Z, isAdvisoryFinding as Zt, summarizeForensicVerdicts as _, SEARCHED_FOR as _t, applyPostScanProcessing as a, correlateFindings as at, loadLocalRules as b, parseWorkflow as bt, discoverAndParseRuntimeReport as c, capForTier as ct, isValidFindingRecord as d, massCeiling as dt, SCORE as et, pathMatchesGlob as f, compareCodePoints as ft, selectAdapter as g, SCAN_ADAPTERS as gt, scan_pipeline_exports as h, getRule as ht, SUITE_INVALIDATING_RULE_IDS as i, getReachableFiles as it, sectionHeader as j, isSuppressionActive as jt, panel as k, isLintFixtureDir as kt, discoverTestFilesPhase as l, computeDimensions as lt, runScan as m, RULES as mt, KNOWN_RULE_IDS as n, TINT as nt, assembleScanResult as o, classifyProvenance as ot, runFileAnalysisPhase as p, RETIRED_RULE_IDS as pt, evidenceLevelOf as q, RULE_CATEGORIES as qt, OVERLAP_META_BY_RULE_ID as r, buildDependencyGraph as rt, buildUniversalRules as s, computeAgenticProfile as st, EVIDENCE_OVERRIDES as t, STATUS as tt, fallbackWorkspace as u, deductionFor as ut, pluginsGateOpen as v, isAzurePipelineFixture as vt, sanitizeErrorText as w, detectFrameworks as wt, loadSuppressions as x, parseYamlGuarded as xt, renderGateNotice as y, parseAzurePipeline as yt, shouldColorize as z, buildEvidenceGraph as zt };
|
|
16335
16500
|
|
|
16336
|
-
//# sourceMappingURL=scan-pipeline-
|
|
16501
|
+
//# sourceMappingURL=scan-pipeline-DT8DWAMl.mjs.map
|