@cejel/cejel 0.4.5 → 0.4.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -11
- package/dist/calibration/llm-detector.js +5 -5
- package/dist/{chunk-RW6YJVHZ.js → chunk-EP7YYWEP.js} +2 -2
- package/dist/{chunk-EPK7UTIK.js → chunk-OIGCYULH.js} +1 -1
- package/dist/{chunk-4IXESHG7.js → chunk-QTLLASOO.js} +1775 -381
- package/dist/{chunk-KGFGX3TP.js → chunk-UYJBSQCV.js} +1 -1
- package/dist/{chunk-EW6NAHE4.js → chunk-XHAETB7N.js} +1 -1
- package/dist/{chunk-R4DPQLS2.js → chunk-ZNVILRHK.js} +60 -3
- package/dist/index.js +39 -9
- package/dist/mcp/index.js +4 -4
- package/dist/packs/d-series/index.js +4 -4
- package/dist/packs/decision-contracts/index.js +3 -3
- package/package.json +1 -1
|
@@ -5,19 +5,24 @@ import {
|
|
|
5
5
|
describeGitFailure,
|
|
6
6
|
execGit,
|
|
7
7
|
isExpectedGitAbsence
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-OIGCYULH.js";
|
|
9
9
|
import {
|
|
10
10
|
external_exports
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-UYJBSQCV.js";
|
|
12
12
|
import {
|
|
13
|
+
MAX_REPOSITORY_CONTENT_BYTES,
|
|
14
|
+
contentReadSignalKey,
|
|
13
15
|
init_esm_shims,
|
|
14
16
|
readRepoText,
|
|
15
17
|
readRepoTextPrefix,
|
|
16
18
|
recordContentSkip,
|
|
19
|
+
recordContentSkipForCriteria,
|
|
17
20
|
recordFilesystemSkip,
|
|
18
21
|
trackContentReads,
|
|
19
|
-
withContentReadCriterion
|
|
20
|
-
|
|
22
|
+
withContentReadCriterion,
|
|
23
|
+
withContentReadSignal,
|
|
24
|
+
withContentReadSignals
|
|
25
|
+
} from "./chunk-ZNVILRHK.js";
|
|
21
26
|
|
|
22
27
|
// src/witan/schemas.ts
|
|
23
28
|
init_esm_shims();
|
|
@@ -117,10 +122,26 @@ var WitanEvidencePointerSchema = external_exports.object({
|
|
|
117
122
|
}).strict().refine((value) => Boolean(value.url ?? value.path ?? value.contentHash), {
|
|
118
123
|
message: "Witan evidence must include a path, url, or content hash."
|
|
119
124
|
});
|
|
125
|
+
var WitanInventoryScanDerivationSchema = external_exports.object({
|
|
126
|
+
kind: external_exports.literal("inventory-scan"),
|
|
127
|
+
inventoryScope: external_exports.enum(["tracked-scan-eligible", "tracked-complete"]),
|
|
128
|
+
inventoryCount: external_exports.number().int().nonnegative(),
|
|
129
|
+
patternSetId: external_exports.string().min(1).max(180),
|
|
130
|
+
patternCount: external_exports.number().int().positive(),
|
|
131
|
+
matchCount: external_exports.number().int().nonnegative()
|
|
132
|
+
}).strict();
|
|
133
|
+
var WitanFindingDerivationSchema = external_exports.discriminatedUnion("kind", [
|
|
134
|
+
WitanInventoryScanDerivationSchema
|
|
135
|
+
]);
|
|
120
136
|
var WitanFindingSchema = external_exports.object({
|
|
121
137
|
severity: WitanFindingSeveritySchema,
|
|
122
138
|
summary: external_exports.string().min(1).max(500),
|
|
123
|
-
evidence: WitanEvidencePointerSchema
|
|
139
|
+
evidence: WitanEvidencePointerSchema,
|
|
140
|
+
// Absence findings emitted by the native repository detector must name the exact
|
|
141
|
+
// inventory and versioned pattern set whose zero result supports the proposition.
|
|
142
|
+
// This remains optional at the shared schema boundary because positive findings and
|
|
143
|
+
// semantic derivations are separate contracts; native absence emitters require it.
|
|
144
|
+
derivation: WitanFindingDerivationSchema.optional()
|
|
124
145
|
}).strict();
|
|
125
146
|
var WitanCriterionMetricSchema = external_exports.object({
|
|
126
147
|
name: external_exports.string().min(1).max(80),
|
|
@@ -321,6 +342,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
|
|
|
321
342
|
}, StrictWitanReportSchema);
|
|
322
343
|
var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
323
344
|
var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
|
|
345
|
+
var WITAN_REPORT_FORMAT_VERSION = "1.0";
|
|
324
346
|
var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
|
|
325
347
|
external_exports.object({
|
|
326
348
|
status: external_exports.literal("scored"),
|
|
@@ -350,7 +372,16 @@ var WitanAttestationStatementSchema = external_exports.object({
|
|
|
350
372
|
version: external_exports.string().min(1).max(80)
|
|
351
373
|
}).strict(),
|
|
352
374
|
generatedAt: external_exports.string().datetime({ offset: true }),
|
|
375
|
+
// Optional on read for attestations emitted before Evidence Seam v1. New attestations
|
|
376
|
+
// always carry it, keeping report.json byte-stable while giving paired consumers an
|
|
377
|
+
// explicit report contract version.
|
|
378
|
+
reportFormatVersion: external_exports.string().regex(/^1\.\d+$/).optional(),
|
|
353
379
|
rubricVersion: external_exports.string().min(1).max(120),
|
|
380
|
+
// Only present when the scan ran under the GitHub Action (GITHUB_RUN_ATTEMPT observed in
|
|
381
|
+
// the runner environment). Absent, never fabricated or defaulted to "1", for a local CLI
|
|
382
|
+
// or any other CI. Not part of report.json: it is per-invocation, not per-repository-
|
|
383
|
+
// state, and report.json's reproducibility guarantee excludes exactly that kind of field.
|
|
384
|
+
githubRunAttempt: external_exports.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
354
385
|
repository: external_exports.object({
|
|
355
386
|
productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
|
|
356
387
|
url: external_exports.string().url().optional(),
|
|
@@ -392,13 +423,15 @@ var WITAN_RUBRIC_VERSION_V19 = "witan-rubric-v19-prospective-2026-08-09";
|
|
|
392
423
|
var WITAN_RUBRIC_VERSION_V20 = "witan-rubric-v20-prospective-2026-08-10";
|
|
393
424
|
var WITAN_RUBRIC_VERSION_V21 = "witan-rubric-v21-prospective-2026-08-10";
|
|
394
425
|
var WITAN_RUBRIC_VERSION_V22 = "witan-rubric-v22-prospective-2026-08-10";
|
|
426
|
+
var WITAN_RUBRIC_VERSION_V23 = "witan-rubric-v23-prospective-2026-09-06";
|
|
395
427
|
var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
|
|
396
428
|
var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
|
|
397
429
|
WITAN_RUBRIC_VERSION_V18,
|
|
398
430
|
WITAN_RUBRIC_VERSION_V19,
|
|
399
431
|
WITAN_RUBRIC_VERSION_V20,
|
|
400
432
|
WITAN_RUBRIC_VERSION_V21,
|
|
401
|
-
WITAN_RUBRIC_VERSION_V22
|
|
433
|
+
WITAN_RUBRIC_VERSION_V22,
|
|
434
|
+
WITAN_RUBRIC_VERSION_V23
|
|
402
435
|
]);
|
|
403
436
|
var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
|
|
404
437
|
WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
|
|
@@ -421,7 +454,9 @@ init_esm_shims();
|
|
|
421
454
|
// src/witan/abstention.ts
|
|
422
455
|
init_esm_shims();
|
|
423
456
|
var WITAN_NO_MEASUREMENT_REASON = "No free-core rubric criterion produced a measurable signal. Cejel abstains rather than publish a numeric zero for an entirely unmeasured repository.";
|
|
424
|
-
var WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the
|
|
457
|
+
var WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the tracked scan-eligible inventory matched no concrete test file, configured test runner, CI test command, or coverage configuration.";
|
|
458
|
+
var WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the bounded directory walk matched no concrete test file, configured test runner, CI test command, or coverage configuration.";
|
|
459
|
+
var WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
|
|
425
460
|
function isWitanNoMeasurementAbstention(report) {
|
|
426
461
|
return report.verdict === "insufficient_source" && report.insufficientSourceReason.startsWith(WITAN_NO_MEASUREMENT_REASON) && report.criteria.every(
|
|
427
462
|
({ status }) => status === "not_applicable" || status === "insufficient_data"
|
|
@@ -785,6 +820,32 @@ function glossaryEntryForMetric(metric2) {
|
|
|
785
820
|
function formatCertificateMetricLabel(metric2) {
|
|
786
821
|
return CERTIFICATE_METRIC_REGISTRY[metric2.name]?.displayLabel ?? metric2.label;
|
|
787
822
|
}
|
|
823
|
+
function formatCertificateMetricAppliedWeightPercent(criterion, metric2) {
|
|
824
|
+
const metrics = criterion.metrics;
|
|
825
|
+
const index = metrics.indexOf(metric2);
|
|
826
|
+
if (index === -1) {
|
|
827
|
+
throw new Error(
|
|
828
|
+
"formatCertificateMetricAppliedWeightPercent: metric does not belong to criterion.metrics"
|
|
829
|
+
);
|
|
830
|
+
}
|
|
831
|
+
const totalWeight = metrics.reduce((sum, m) => sum + m.weight, 0);
|
|
832
|
+
if (totalWeight <= 0) return 0;
|
|
833
|
+
const shares = metrics.map((m, i) => {
|
|
834
|
+
const raw = m.weight / totalWeight * 100;
|
|
835
|
+
const floor = Math.floor(raw);
|
|
836
|
+
return { i, floor, fraction: raw - floor };
|
|
837
|
+
});
|
|
838
|
+
const flooredTotal = shares.reduce((sum, s) => sum + s.floor, 0);
|
|
839
|
+
const byLargestRemainder = [...shares].sort((a, b) => b.fraction - a.fraction);
|
|
840
|
+
const result = new Map(shares.map((s) => [s.i, s.floor]));
|
|
841
|
+
let remainder = Math.round(100 - flooredTotal);
|
|
842
|
+
for (const s of byLargestRemainder) {
|
|
843
|
+
if (remainder <= 0) break;
|
|
844
|
+
result.set(s.i, (result.get(s.i) ?? s.floor) + 1);
|
|
845
|
+
remainder -= 1;
|
|
846
|
+
}
|
|
847
|
+
return result.get(index) ?? 0;
|
|
848
|
+
}
|
|
788
849
|
function glossaryEntriesForReport(report) {
|
|
789
850
|
const entries = new Map(CERTIFICATE_GLOSSARY.map((entry) => [entry.key, entry]));
|
|
790
851
|
for (const metric2 of report.criteria.flatMap((criterion) => criterion.metrics)) {
|
|
@@ -900,6 +961,74 @@ function formatCertificateMetricValue(criterion, metric2) {
|
|
|
900
961
|
`${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`
|
|
901
962
|
);
|
|
902
963
|
}
|
|
964
|
+
var NEXT_STEP_FINDING_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
|
|
965
|
+
var NEXT_STEP_FINDING_SEVERITY_RANK = {
|
|
966
|
+
critical: 0,
|
|
967
|
+
warning: 1,
|
|
968
|
+
info: 2
|
|
969
|
+
};
|
|
970
|
+
var MAX_NEXT_STEP_FINDINGS = 3;
|
|
971
|
+
function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
|
|
972
|
+
const steps = [];
|
|
973
|
+
if (unmeasured.length > 0) {
|
|
974
|
+
const ids = unmeasured.map((criterion) => criterion.id).join(", ");
|
|
975
|
+
steps.push(
|
|
976
|
+
`Obtain measurable evidence for ${ids} \u2014 ${unmeasured.length === 1 ? "this scored dimension has" : "these scored dimensions have"} no recorded result, and could change the verdict.`
|
|
977
|
+
);
|
|
978
|
+
}
|
|
979
|
+
const criterionFindings = report.criteria.flatMap(
|
|
980
|
+
(criterion) => criterion.findings.filter((finding) => NEXT_STEP_FINDING_SEVERITIES.has(finding.severity)).map((finding) => ({ criterionId: criterion.id, finding }))
|
|
981
|
+
);
|
|
982
|
+
const sortedFindings = [...criterionFindings].sort(
|
|
983
|
+
(a, b) => NEXT_STEP_FINDING_SEVERITY_RANK[a.finding.severity] - NEXT_STEP_FINDING_SEVERITY_RANK[b.finding.severity]
|
|
984
|
+
);
|
|
985
|
+
for (const { criterionId, finding } of sortedFindings.slice(0, MAX_NEXT_STEP_FINDINGS)) {
|
|
986
|
+
steps.push(
|
|
987
|
+
`Evidence is currently absent for the ${finding.severity} finding on ${criterionId}: obtain independent verification before relying on this criterion.`
|
|
988
|
+
);
|
|
989
|
+
}
|
|
990
|
+
if (sortedFindings.length > MAX_NEXT_STEP_FINDINGS) {
|
|
991
|
+
steps.push(
|
|
992
|
+
`${sortedFindings.length - MAX_NEXT_STEP_FINDINGS} more critical/warning finding(s) need the same independent verification; see the findings listed above for the complete set.`
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
if (a1 && coverageMetric && isCoverageNotMeasured(a1, coverageMetric)) {
|
|
996
|
+
steps.push(
|
|
997
|
+
"Obtain a coverage report or configured threshold for A1's static coverage metric; none was found, so it was not measured."
|
|
998
|
+
);
|
|
999
|
+
}
|
|
1000
|
+
if (!report.repo.headSha && report.criteria.some(
|
|
1001
|
+
(criterion) => criterion.metrics.some((metric2) => metric2.name === "pr_merge_ratio")
|
|
1002
|
+
)) {
|
|
1003
|
+
steps.push(
|
|
1004
|
+
"Restore Git history access so the recent-commit PR-reference proxy has commit subjects to inspect; its scoring zero here is a conservative default, not a detected outcome."
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
if (notApplicable.length > 0) {
|
|
1008
|
+
const ids = notApplicable.map((criterion) => criterion.id).join(", ");
|
|
1009
|
+
steps.push(
|
|
1010
|
+
`Confirm ${ids} ${notApplicable.length === 1 ? "is" : "are"} correctly marked not applicable to this repository; ${notApplicable.length === 1 ? "it was" : "they were"} not examined.`
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
const scanLimitationCount = report.scanLimitations?.length ?? 0;
|
|
1014
|
+
if (scanLimitationCount > 0) {
|
|
1015
|
+
steps.push(
|
|
1016
|
+
`Resolve the ${scanLimitationCount} scan limitation${scanLimitationCount === 1 ? "" : "s"} qualifying the readable evidence, then reproduce the scan.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
const skippedCount = report.contentReadSummary?.skipped ?? 0;
|
|
1020
|
+
if (skippedCount > 0) {
|
|
1021
|
+
steps.push(
|
|
1022
|
+
`Make the ${skippedCount} skipped content entr${skippedCount === 1 ? "y" : "ies"} readable (see the itemized reasons in the certificate), then reproduce the scan.`
|
|
1023
|
+
);
|
|
1024
|
+
}
|
|
1025
|
+
if (steps.length === 0) {
|
|
1026
|
+
steps.push(
|
|
1027
|
+
"No unresolved evidence gaps were identified across the categories Cejel tracks; reproduce the scan at the reported revision when one is available before relying on this certificate."
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
return steps;
|
|
1031
|
+
}
|
|
903
1032
|
function buildRelyingPartySummary(report) {
|
|
904
1033
|
const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
|
|
905
1034
|
const measured = applicable.filter((criterion) => criterion.status !== "insufficient_data");
|
|
@@ -947,7 +1076,7 @@ function buildRelyingPartySummary(report) {
|
|
|
947
1076
|
examined: `Cejel examined the repository evidence recorded for ${report.productDisplayName}${revision} under ${report.rubricVersion}.`,
|
|
948
1077
|
established: `The report established measured results for ${measured.length} of ${applicable.length} applicable rubric dimensions and recorded ${findings} evidence-backed finding${findings === 1 ? "" : "s"}.`,
|
|
949
1078
|
notEstablished: gaps.join(" "),
|
|
950
|
-
next:
|
|
1079
|
+
next: buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric).join(" ")
|
|
951
1080
|
};
|
|
952
1081
|
}
|
|
953
1082
|
function formatMetricNumber(value) {
|
|
@@ -1027,6 +1156,7 @@ function formatMetricMember(criterion, metric2) {
|
|
|
1027
1156
|
}
|
|
1028
1157
|
|
|
1029
1158
|
// src/witan/html.ts
|
|
1159
|
+
var CEJEL_CERTIFICATE_FORMAT_VERSION = "1.0";
|
|
1030
1160
|
function renderWitanHtmlReport(report, options = {}) {
|
|
1031
1161
|
const codeCriteria = criteriaByCategory(report, "code_trust");
|
|
1032
1162
|
const processCriteria = criteriaByCategory(report, "process_trust");
|
|
@@ -1045,6 +1175,7 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
1045
1175
|
<head>
|
|
1046
1176
|
<meta charset="utf-8">
|
|
1047
1177
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1178
|
+
<meta name="cejel-certificate-format" content="${CEJEL_CERTIFICATE_FORMAT_VERSION}">
|
|
1048
1179
|
<title>Cejel Trust Certificate - ${escapeHtml(report.productDisplayName)}</title>
|
|
1049
1180
|
<style>${CERTIFICATE_CSS}</style>
|
|
1050
1181
|
</head>
|
|
@@ -1064,6 +1195,7 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
1064
1195
|
${options.generatedAt ? `<div><dt>Date</dt><dd>${escapeHtml(formatDate(options.generatedAt))}</dd></div>` : ""}
|
|
1065
1196
|
<div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
|
|
1066
1197
|
<div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
|
|
1198
|
+
${options.runAttempt ? `<div><dt>Run attempt</dt><dd>${escapeHtml(options.runAttempt)}</dd></div>` : ""}
|
|
1067
1199
|
<div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
|
|
1068
1200
|
${// Gated on the rubric actually being prospective — a calibrated (v17) certificate's
|
|
1069
1201
|
// HTML stays byte-identical to before --rubric-pin existed.
|
|
@@ -1086,6 +1218,8 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
1086
1218
|
</div>
|
|
1087
1219
|
</header>
|
|
1088
1220
|
|
|
1221
|
+
${renderFindingsFirstSummary(report)}
|
|
1222
|
+
|
|
1089
1223
|
${renderRelyingPartySummary(relyingPartySummary)}
|
|
1090
1224
|
|
|
1091
1225
|
${(report.scanLimitations?.length ?? 0) > 0 ? `<section class="scan-limitations-section evidence-section" aria-label="Scan limitations">
|
|
@@ -1127,6 +1261,26 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
1127
1261
|
</html>
|
|
1128
1262
|
`;
|
|
1129
1263
|
}
|
|
1264
|
+
var FINDINGS_FIRST_SEVERITY_ORDER = {
|
|
1265
|
+
critical: 0,
|
|
1266
|
+
warning: 1,
|
|
1267
|
+
info: 2
|
|
1268
|
+
};
|
|
1269
|
+
function collectFindingsFirstEntries(report) {
|
|
1270
|
+
return report.criteria.flatMap(
|
|
1271
|
+
(criterion) => criterion.findings.filter((finding) => finding.severity === "critical" || finding.severity === "warning").map((finding) => ({ criterion, finding }))
|
|
1272
|
+
).sort(
|
|
1273
|
+
(a, b) => FINDINGS_FIRST_SEVERITY_ORDER[a.finding.severity] - FINDINGS_FIRST_SEVERITY_ORDER[b.finding.severity]
|
|
1274
|
+
);
|
|
1275
|
+
}
|
|
1276
|
+
function renderFindingsFirstSummary(report) {
|
|
1277
|
+
const entries = collectFindingsFirstEntries(report);
|
|
1278
|
+
const body = entries.length > 0 ? `<ul>${entries.map(({ criterion, finding }) => `<li>${renderFindingEvidence(criterion, finding)}</li>`).join("")}</ul>` : '<p class="findings-first-empty">No critical or warning findings were identified across any criterion in this scan.</p>';
|
|
1279
|
+
return `<section class="findings-first-summary" aria-labelledby="findings-first-heading">
|
|
1280
|
+
<h2 id="findings-first-heading">Critical and warning findings</h2>
|
|
1281
|
+
${body}
|
|
1282
|
+
</section>`;
|
|
1283
|
+
}
|
|
1130
1284
|
function renderRelyingPartySummary(summary) {
|
|
1131
1285
|
return `<section class="relying-party-summary" aria-labelledby="relying-party-heading">
|
|
1132
1286
|
<h2 id="relying-party-heading">How to read this certificate</h2>
|
|
@@ -1140,10 +1294,12 @@ function renderRelyingPartySummary(summary) {
|
|
|
1140
1294
|
}
|
|
1141
1295
|
function renderGlossary(entries) {
|
|
1142
1296
|
return `<section class="glossary" aria-labelledby="glossary-heading">
|
|
1143
|
-
<
|
|
1144
|
-
|
|
1297
|
+
<details>
|
|
1298
|
+
<summary><h2 id="glossary-heading">Plain-language glossary</h2></summary>
|
|
1299
|
+
<dl>${entries.map(
|
|
1145
1300
|
(entry) => `<div><dt id="glossary-${escapeAttribute(entry.key)}">${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.definition)}</dd></div>`
|
|
1146
1301
|
).join("")}</dl>
|
|
1302
|
+
</details>
|
|
1147
1303
|
</section>`;
|
|
1148
1304
|
}
|
|
1149
1305
|
function renderContentReadSummary(report) {
|
|
@@ -1218,12 +1374,12 @@ function renderCriterionColumn(title, criteria, gitHistoryUnavailable) {
|
|
|
1218
1374
|
</section>`;
|
|
1219
1375
|
}
|
|
1220
1376
|
function renderNotApplicableGroup(criteria) {
|
|
1221
|
-
return `<
|
|
1222
|
-
<h3 class="na-heading">Not applicable to this repository</h3>
|
|
1377
|
+
return `<details class="na-group">
|
|
1378
|
+
<summary><h3 class="na-heading">Not applicable to this repository</h3></summary>
|
|
1223
1379
|
<ul class="na-list">
|
|
1224
1380
|
${criteria.map(renderNotApplicableItem).join("")}
|
|
1225
1381
|
</ul>
|
|
1226
|
-
</
|
|
1382
|
+
</details>`;
|
|
1227
1383
|
}
|
|
1228
1384
|
function renderNotApplicableItem(criterion) {
|
|
1229
1385
|
const reason = criterion.notes ?? "Not applicable to this repository.";
|
|
@@ -1283,15 +1439,21 @@ function scoreBandForPresentation(score) {
|
|
|
1283
1439
|
if (score >= 1.5) return "warning";
|
|
1284
1440
|
return "critical";
|
|
1285
1441
|
}
|
|
1442
|
+
function renderMetricWeightBadge(criterion, metric2) {
|
|
1443
|
+
const appliedSharePercent = formatCertificateMetricAppliedWeightPercent(criterion, metric2);
|
|
1444
|
+
return `<span class="metric-weight">weight ${appliedSharePercent}% of ${escapeHtml(criterion.id)}</span>`;
|
|
1445
|
+
}
|
|
1286
1446
|
function renderMetric(criterion, metric2) {
|
|
1287
1447
|
const entry = glossaryEntryForMetric(metric2);
|
|
1288
1448
|
const tooltipId = `tooltip-${criterion.id}-${entry.key}`;
|
|
1289
1449
|
const formatted = formatCertificateMetricValue(criterion, metric2);
|
|
1450
|
+
const weightBadge = renderMetricWeightBadge(criterion, metric2);
|
|
1290
1451
|
const cappedEntry = formatted.includes("capped") ? CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "capped") : void 0;
|
|
1291
1452
|
const label = `<span class="term-help" tabindex="0" aria-describedby="${escapeAttribute(tooltipId)}"><strong>${escapeHtml(formatCertificateMetricLabel(metric2))}</strong><span class="term-tooltip" id="${escapeAttribute(tooltipId)}" role="tooltip">${escapeHtml(entry.definition)}</span></span>`;
|
|
1292
|
-
if (!cappedEntry)
|
|
1453
|
+
if (!cappedEntry)
|
|
1454
|
+
return `${label}<span class="metric-value">${escapeHtml(formatted)} ${weightBadge}</span>`;
|
|
1293
1455
|
const cappedTooltipId = `${tooltipId}-capped`;
|
|
1294
|
-
return `${label}<span class="term-help metric-value" tabindex="0" aria-describedby="${escapeAttribute(cappedTooltipId)}">${escapeHtml(formatted)}<span class="term-tooltip" id="${escapeAttribute(cappedTooltipId)}" role="tooltip">${escapeHtml(cappedEntry.definition)}</span></span>`;
|
|
1456
|
+
return `${label}<span class="term-help metric-value" tabindex="0" aria-describedby="${escapeAttribute(cappedTooltipId)}">${escapeHtml(formatted)} ${weightBadge}<span class="term-tooltip" id="${escapeAttribute(cappedTooltipId)}" role="tooltip">${escapeHtml(cappedEntry.definition)}</span></span>`;
|
|
1295
1457
|
}
|
|
1296
1458
|
function renderCategoryScore(score, coverage, category) {
|
|
1297
1459
|
const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
|
|
@@ -1439,6 +1601,9 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
|
1439
1601
|
.subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
|
|
1440
1602
|
.coverage-note { margin-top: 8px; color: var(--faint); font-family: var(--mono); font-size: 11px; }
|
|
1441
1603
|
.source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
1604
|
+
.findings-first-summary { margin-top: 20px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
1605
|
+
.findings-first-summary ul { margin: 0; padding-left: 18px; display: grid; gap: 10px; }
|
|
1606
|
+
.findings-first-empty { margin: 0; color: var(--muted); }
|
|
1442
1607
|
.relying-party-summary, .glossary { margin-top: 28px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
1443
1608
|
.relying-party-summary dl { display: grid; gap: 14px; margin: 0; }
|
|
1444
1609
|
.relying-party-summary dl div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 18px; }
|
|
@@ -1446,6 +1611,14 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
|
1446
1611
|
.glossary dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 22px; margin: 0; }
|
|
1447
1612
|
.glossary dl div { border-top: 1px solid var(--line); padding-top: 10px; }
|
|
1448
1613
|
.glossary dd { margin-top: 4px; font-size: 13px; }
|
|
1614
|
+
/* Track A3: native <details>/<summary> disclosure \u2014 the summary is the clickable control, a
|
|
1615
|
+
heading nests inside it as its accessible label (still exposed as a heading), and the heading's
|
|
1616
|
+
own vertical margin is neutralized by making it inline, so the marker sits flush against it. */
|
|
1617
|
+
summary { cursor: pointer; }
|
|
1618
|
+
summary h2, summary h3 { display: inline; }
|
|
1619
|
+
summary::marker { color: var(--periwinkle); }
|
|
1620
|
+
summary:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--periwinkle); border-radius: 3px; }
|
|
1621
|
+
.glossary details[open] dl, .na-group[open] .na-list { margin-top: 16px; }
|
|
1449
1622
|
.trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
|
|
1450
1623
|
.scan-limitations-section { margin-top: 28px; border-color: rgba(231, 191, 114, .44); }
|
|
1451
1624
|
.scan-limitations-section .scan-warning { list-style: none; margin-left: -18px; }
|
|
@@ -1477,6 +1650,7 @@ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
|
|
|
1477
1650
|
.criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
|
|
1478
1651
|
.criterion-metrics strong { color: var(--text); font-weight: 650; }
|
|
1479
1652
|
.criterion-metrics span { color: var(--muted); }
|
|
1653
|
+
.criterion-metrics .metric-weight { font-size: 10px; color: var(--faint); }
|
|
1480
1654
|
.criterion-metrics > li > .metric-value {
|
|
1481
1655
|
min-width: 0; white-space: normal; overflow-wrap: anywhere; text-align: right;
|
|
1482
1656
|
}
|
|
@@ -1639,6 +1813,8 @@ function createWitanAttestation(report, options) {
|
|
|
1639
1813
|
predicate: {
|
|
1640
1814
|
tool: { name: "cejel", version: options.toolVersion },
|
|
1641
1815
|
generatedAt: options.generatedAt,
|
|
1816
|
+
reportFormatVersion: WITAN_REPORT_FORMAT_VERSION,
|
|
1817
|
+
...options.githubRunAttempt ? { githubRunAttempt: options.githubRunAttempt } : {},
|
|
1642
1818
|
rubricVersion: report.rubricVersion,
|
|
1643
1819
|
repository: {
|
|
1644
1820
|
productSlug: report.productSlug,
|
|
@@ -1724,6 +1900,23 @@ init_esm_shims();
|
|
|
1724
1900
|
// src/witan/markdown.ts
|
|
1725
1901
|
init_esm_shims();
|
|
1726
1902
|
|
|
1903
|
+
// src/presentation-safety.ts
|
|
1904
|
+
init_esm_shims();
|
|
1905
|
+
var PRESENTATION_CONTROL_PATTERN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
1906
|
+
function sanitizePresentationLine(value, options) {
|
|
1907
|
+
const withoutControls = value.replace(PRESENTATION_CONTROL_PATTERN, "").trim();
|
|
1908
|
+
const safeValue = withoutControls.length > 0 ? withoutControls : options.fallback;
|
|
1909
|
+
if (safeValue.length <= options.maxLength) return safeValue;
|
|
1910
|
+
const ellipsis = ".".repeat(Math.min(3, options.maxLength));
|
|
1911
|
+
const contentLimit = Math.max(options.maxLength - ellipsis.length, 0);
|
|
1912
|
+
let prefix = "";
|
|
1913
|
+
for (const character of safeValue) {
|
|
1914
|
+
if (prefix.length + character.length > contentLimit) break;
|
|
1915
|
+
prefix += character;
|
|
1916
|
+
}
|
|
1917
|
+
return `${prefix}${ellipsis}`;
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1727
1920
|
// src/witan/repo-signals.ts
|
|
1728
1921
|
init_esm_shims();
|
|
1729
1922
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -1743,27 +1936,148 @@ function stripBom(text) {
|
|
|
1743
1936
|
|
|
1744
1937
|
// src/witan/repo-signals.ts
|
|
1745
1938
|
function usesV17DetectorClosure(rubricVersion) {
|
|
1746
|
-
return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
|
|
1939
|
+
return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
1940
|
+
}
|
|
1941
|
+
var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
|
|
1942
|
+
rubricGeneration: "legacy",
|
|
1943
|
+
inventoryProvenance: "directory-walk"
|
|
1944
|
+
};
|
|
1945
|
+
var INVENTORY_SCAN_PATTERN_SETS = {
|
|
1946
|
+
a1AuthenticatedAbsence: {
|
|
1947
|
+
id: "cejel.core-a1.test-integrity-surface.v1",
|
|
1948
|
+
count: 4
|
|
1949
|
+
},
|
|
1950
|
+
a1ConcreteTests: {
|
|
1951
|
+
id: "cejel.core-a1.concrete-test-files.v1",
|
|
1952
|
+
count: 16
|
|
1953
|
+
},
|
|
1954
|
+
a1CoverageConfigurationV1: {
|
|
1955
|
+
id: "cejel.core-a1.coverage-configuration.v1",
|
|
1956
|
+
count: 10
|
|
1957
|
+
},
|
|
1958
|
+
a1CoverageConfigurationV2: {
|
|
1959
|
+
id: "cejel.core-a1.coverage-configuration.v2",
|
|
1960
|
+
count: 14
|
|
1961
|
+
},
|
|
1962
|
+
a2CurrentSecret: {
|
|
1963
|
+
id: "cejel.core-a2.current-secret-shape.v1",
|
|
1964
|
+
count: 3
|
|
1965
|
+
},
|
|
1966
|
+
a2RlsPolicy: {
|
|
1967
|
+
id: "cejel.core-a2.rls-policy.v1",
|
|
1968
|
+
count: 3
|
|
1969
|
+
},
|
|
1970
|
+
a3CiOrReleaseDeploy: {
|
|
1971
|
+
id: "cejel.core-a3.ci-or-release-deploy.v1",
|
|
1972
|
+
count: 12
|
|
1973
|
+
},
|
|
1974
|
+
a4Lockfile: {
|
|
1975
|
+
id: "cejel.core-a4.lockfile.v1",
|
|
1976
|
+
count: 11
|
|
1977
|
+
},
|
|
1978
|
+
a5ClaimRealityArtifact: {
|
|
1979
|
+
id: "cejel.core-a5.claim-reality-artifact.v1",
|
|
1980
|
+
count: 3
|
|
1981
|
+
}
|
|
1982
|
+
};
|
|
1983
|
+
function inventoryScanDerivation(inventoryCount, patternSet, matchCount) {
|
|
1984
|
+
return {
|
|
1985
|
+
kind: "inventory-scan",
|
|
1986
|
+
inventoryScope: "tracked-scan-eligible",
|
|
1987
|
+
inventoryCount,
|
|
1988
|
+
patternSetId: patternSet.id,
|
|
1989
|
+
patternCount: patternSet.count,
|
|
1990
|
+
matchCount
|
|
1991
|
+
};
|
|
1992
|
+
}
|
|
1993
|
+
function inventoryAbsenceFinding(options) {
|
|
1994
|
+
const { context, summaries } = options;
|
|
1995
|
+
const summary = context.rubricGeneration === "legacy" ? summaries.legacy : context.inventoryProvenance === "tracked-scan-eligible" ? summaries.currentTracked : summaries.currentDirectoryWalk;
|
|
1996
|
+
const emitsDerivation = context.rubricGeneration === "current" && context.inventoryProvenance === "tracked-scan-eligible";
|
|
1997
|
+
return {
|
|
1998
|
+
severity: options.severity,
|
|
1999
|
+
summary,
|
|
2000
|
+
evidence: options.evidence,
|
|
2001
|
+
...emitsDerivation ? {
|
|
2002
|
+
derivation: inventoryScanDerivation(
|
|
2003
|
+
options.inventoryCount,
|
|
2004
|
+
options.patternSet,
|
|
2005
|
+
options.matchCount
|
|
2006
|
+
)
|
|
2007
|
+
} : {}
|
|
2008
|
+
};
|
|
2009
|
+
}
|
|
2010
|
+
function wholesaleCriterionAbstention(signal) {
|
|
2011
|
+
return {
|
|
2012
|
+
criterionId: signal.criterionId,
|
|
2013
|
+
positiveEvidence: [],
|
|
2014
|
+
findings: [],
|
|
2015
|
+
metrics: [],
|
|
2016
|
+
insufficientData: true,
|
|
2017
|
+
notes: "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
|
|
2018
|
+
};
|
|
1747
2019
|
}
|
|
1748
2020
|
function buildWitanInputFromRepo(options) {
|
|
1749
2021
|
const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
|
|
1750
2022
|
const affectedCriteria = tracked.affectedCriteria;
|
|
2023
|
+
const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
|
|
2024
|
+
if (rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
|
|
2025
|
+
const signals2 = (tracked.value.signals ?? []).map(
|
|
2026
|
+
(signal) => affectedCriteria.has(signal.criterionId) ? wholesaleCriterionAbstention(signal) : signal
|
|
2027
|
+
);
|
|
2028
|
+
const affectedCount = affectedCriteria.size;
|
|
2029
|
+
const scanLimitations2 = [...tracked.value.scanLimitations ?? []];
|
|
2030
|
+
if (affectedCount > 0 && scanLimitations2.length < 16) {
|
|
2031
|
+
scanLimitations2.push(
|
|
2032
|
+
`${affectedCount} ${affectedCount === 1 ? "criterion" : "criteria"} abstained because relevant repository content could not be read. Counts and errno classes are recorded in contentReadSummary; file paths are intentionally omitted.`
|
|
2033
|
+
);
|
|
2034
|
+
}
|
|
2035
|
+
return {
|
|
2036
|
+
...tracked.value,
|
|
2037
|
+
signals: signals2,
|
|
2038
|
+
...scanLimitations2.length > 0 ? { scanLimitations: scanLimitations2 } : {},
|
|
2039
|
+
contentReadSummary: tracked.summary
|
|
2040
|
+
};
|
|
2041
|
+
}
|
|
2042
|
+
const wholesaleAbstainedCriteria = [];
|
|
2043
|
+
const partiallyAbstainedSignals = [];
|
|
1751
2044
|
const signals = (tracked.value.signals ?? []).map((signal) => {
|
|
1752
2045
|
if (!affectedCriteria.has(signal.criterionId)) return signal;
|
|
2046
|
+
if (tracked.unattributedCriteria.has(signal.criterionId)) {
|
|
2047
|
+
wholesaleAbstainedCriteria.push(signal.criterionId);
|
|
2048
|
+
return wholesaleCriterionAbstention(signal);
|
|
2049
|
+
}
|
|
2050
|
+
const prefix = contentReadSignalKey(signal.criterionId, "");
|
|
2051
|
+
const affectedNames = [...tracked.affectedSignals].filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
|
|
2052
|
+
if (affectedNames.length === 0) {
|
|
2053
|
+
wholesaleAbstainedCriteria.push(signal.criterionId);
|
|
2054
|
+
return wholesaleCriterionAbstention(signal);
|
|
2055
|
+
}
|
|
2056
|
+
const affectedNameSet = new Set(affectedNames);
|
|
2057
|
+
const metrics = (signal.metrics ?? []).filter((metric2) => !affectedNameSet.has(metric2.name));
|
|
2058
|
+
const anythingSurvives = metrics.length > 0 || (signal.positiveEvidence?.length ?? 0) > 0 || (signal.findings?.length ?? 0) > 0;
|
|
2059
|
+
if (!anythingSurvives) {
|
|
2060
|
+
wholesaleAbstainedCriteria.push(signal.criterionId);
|
|
2061
|
+
return wholesaleCriterionAbstention(signal);
|
|
2062
|
+
}
|
|
2063
|
+
for (const name of affectedNames) {
|
|
2064
|
+
partiallyAbstainedSignals.push(`${signal.criterionId}.${name}`);
|
|
2065
|
+
}
|
|
1753
2066
|
return {
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
metrics: [],
|
|
1758
|
-
insufficientData: true,
|
|
1759
|
-
notes: "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
|
|
2067
|
+
...signal,
|
|
2068
|
+
metrics,
|
|
2069
|
+
notes: `Cejel abstained on ${[...affectedNames].sort().join(", ")} in this criterion because that content could not be read; other signals in this criterion were computed from readable files and are unaffected.`
|
|
1760
2070
|
};
|
|
1761
2071
|
});
|
|
1762
|
-
const affectedCount = affectedCriteria.size;
|
|
1763
2072
|
const scanLimitations = [...tracked.value.scanLimitations ?? []];
|
|
1764
|
-
if (
|
|
2073
|
+
if (wholesaleAbstainedCriteria.length > 0 && scanLimitations.length < 16) {
|
|
1765
2074
|
scanLimitations.push(
|
|
1766
|
-
`${
|
|
2075
|
+
`${wholesaleAbstainedCriteria.length} ${wholesaleAbstainedCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content could not be read and the affected signal could not be isolated. Counts and errno classes are recorded in contentReadSummary; file paths are intentionally omitted.`
|
|
2076
|
+
);
|
|
2077
|
+
}
|
|
2078
|
+
if (partiallyAbstainedSignals.length > 0 && scanLimitations.length < 16) {
|
|
2079
|
+
scanLimitations.push(
|
|
2080
|
+
`${partiallyAbstainedSignals.length} ${partiallyAbstainedSignals.length === 1 ? "signal" : "signals"} abstained because its own repository content could not be read, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedSignals].sort().join(", ")}.`
|
|
1767
2081
|
);
|
|
1768
2082
|
}
|
|
1769
2083
|
return {
|
|
@@ -1783,11 +2097,16 @@ function buildWitanInputFromRepoUntracked(options) {
|
|
|
1783
2097
|
const inventoryFiles = fileInventory.inventoryFiles;
|
|
1784
2098
|
const ignoredTargetReason = repoFiles.length === 0 ? explainIgnoredScanTarget(options.repoPath) : void 0;
|
|
1785
2099
|
const usesV17Detectors = usesV17DetectorClosure(rubricVersion);
|
|
1786
|
-
const
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
const
|
|
2100
|
+
const inventoryAbsenceContext = {
|
|
2101
|
+
rubricGeneration: usesV17Detectors ? "current" : "legacy",
|
|
2102
|
+
inventoryProvenance: fileInventory.repoFileInventoryScope === "tracked-scan-eligible" ? "tracked-scan-eligible" : "directory-walk"
|
|
2103
|
+
};
|
|
2104
|
+
const usesV18NativeRls = rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2105
|
+
const usesV19CommitYear = rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2106
|
+
const usesV20A3ExplicitGaps = rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2107
|
+
const usesV21ExecutedEscalations = rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2108
|
+
const usesV22PackageStartEntrypoint = rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2109
|
+
const usesV23CommandCoverage = rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
1791
2110
|
const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
|
|
1792
2111
|
const readableArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure(rubricVersion) ? applyReadableSourceRepresentationGate(
|
|
1793
2112
|
options.repoPath,
|
|
@@ -1823,7 +2142,9 @@ function buildWitanInputFromRepoUntracked(options) {
|
|
|
1823
2142
|
usesV20A3ExplicitGaps,
|
|
1824
2143
|
usesV21ExecutedEscalations,
|
|
1825
2144
|
usesV22PackageStartEntrypoint,
|
|
2145
|
+
usesV23CommandCoverage,
|
|
1826
2146
|
reviewableSourceProof,
|
|
2147
|
+
inventoryAbsenceContext,
|
|
1827
2148
|
scanLimitations
|
|
1828
2149
|
);
|
|
1829
2150
|
const archetype = usesV17Detectors && reviewableSourceProof?.passes === true && isV17RescuableAbstention(gatedArchetype.abstentionKind) && coreSignals.some(isMeasuredCriterionSignal) ? {
|
|
@@ -1833,7 +2154,10 @@ function buildWitanInputFromRepoUntracked(options) {
|
|
|
1833
2154
|
} : gatedArchetype;
|
|
1834
2155
|
return {
|
|
1835
2156
|
productSlug: options.productSlug,
|
|
1836
|
-
productDisplayName: options.productDisplayName,
|
|
2157
|
+
productDisplayName: sanitizePresentationLine(options.productDisplayName, {
|
|
2158
|
+
fallback: "Repository",
|
|
2159
|
+
maxLength: 120
|
|
2160
|
+
}),
|
|
1837
2161
|
repo: {
|
|
1838
2162
|
...headSha ? { headSha } : {}
|
|
1839
2163
|
},
|
|
@@ -2430,7 +2754,7 @@ function buildNotApplicableSignal(criterionId, reason) {
|
|
|
2430
2754
|
notApplicable: true
|
|
2431
2755
|
};
|
|
2432
2756
|
}
|
|
2433
|
-
function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, reviewableSourceProof, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
2757
|
+
function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23CommandCoverage, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
2434
2758
|
const signals = [];
|
|
2435
2759
|
const mono = resolveMonorepoContext(repoPath, scanLimitations);
|
|
2436
2760
|
const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
|
|
@@ -2442,7 +2766,9 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2442
2766
|
useV27Detectors,
|
|
2443
2767
|
useV33Detectors,
|
|
2444
2768
|
useV36Detectors,
|
|
2445
|
-
|
|
2769
|
+
useV23CommandCoverage,
|
|
2770
|
+
reviewableSourceProof,
|
|
2771
|
+
inventoryAbsenceContext
|
|
2446
2772
|
)
|
|
2447
2773
|
);
|
|
2448
2774
|
const a2Signal = collectCriterion(
|
|
@@ -2456,6 +2782,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2456
2782
|
useV39Detectors,
|
|
2457
2783
|
useV47Detectors,
|
|
2458
2784
|
useV18NativeRls,
|
|
2785
|
+
inventoryAbsenceContext,
|
|
2459
2786
|
scanLimitations
|
|
2460
2787
|
)
|
|
2461
2788
|
);
|
|
@@ -2466,7 +2793,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2466
2793
|
repoFiles,
|
|
2467
2794
|
useV27Detectors,
|
|
2468
2795
|
useV20A3ExplicitGaps,
|
|
2469
|
-
useV22PackageStartEntrypoint
|
|
2796
|
+
useV22PackageStartEntrypoint,
|
|
2797
|
+
inventoryAbsenceContext
|
|
2470
2798
|
)
|
|
2471
2799
|
);
|
|
2472
2800
|
const a4Signal = collectCriterion(
|
|
@@ -2476,12 +2804,18 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2476
2804
|
repoFiles,
|
|
2477
2805
|
mono,
|
|
2478
2806
|
useV27Detectors,
|
|
2479
|
-
useV47Detectors
|
|
2807
|
+
useV47Detectors,
|
|
2808
|
+
inventoryAbsenceContext
|
|
2480
2809
|
)
|
|
2481
2810
|
);
|
|
2482
2811
|
const a5Signal = collectCriterion(
|
|
2483
2812
|
"A5",
|
|
2484
|
-
() => collectA5ClaimRealityEvidence(
|
|
2813
|
+
() => collectA5ClaimRealityEvidence(
|
|
2814
|
+
repoPath,
|
|
2815
|
+
repoFiles,
|
|
2816
|
+
useV27Detectors,
|
|
2817
|
+
inventoryAbsenceContext
|
|
2818
|
+
)
|
|
2485
2819
|
);
|
|
2486
2820
|
const b1Signal = buildNotApplicableSignal(
|
|
2487
2821
|
"B1",
|
|
@@ -2532,7 +2866,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2532
2866
|
}
|
|
2533
2867
|
return signals;
|
|
2534
2868
|
}
|
|
2535
|
-
function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof) {
|
|
2869
|
+
function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23CommandCoverage, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
2536
2870
|
const evidence = [];
|
|
2537
2871
|
const findings = [];
|
|
2538
2872
|
const allTestFiles = findConcreteTestFiles(
|
|
@@ -2543,17 +2877,33 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2543
2877
|
const testFiles = allTestFiles.slice(0, 8);
|
|
2544
2878
|
const sourceFiles = repoFiles.filter(isSourceFile);
|
|
2545
2879
|
const runnerFiles = repoFiles.filter(
|
|
2546
|
-
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isTestRunnerConfig(repoPath, file)
|
|
2880
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && (isTestRunnerConfig(repoPath, file) || useV23CommandCoverage && isRecipeTestEntry(repoPath, file))
|
|
2547
2881
|
).slice(0, 6);
|
|
2548
|
-
const
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2882
|
+
const baseConfiguredRunnerFiles = useV33Detectors ? findConfiguredTestRunnerFiles(repoPath, repoFiles) : runnerFiles;
|
|
2883
|
+
const configuredRunnerFiles = useV23CommandCoverage ? [
|
|
2884
|
+
.../* @__PURE__ */ new Set([
|
|
2885
|
+
...baseConfiguredRunnerFiles,
|
|
2886
|
+
...repoFiles.filter((file) => isRecipeTestEntry(repoPath, file))
|
|
2887
|
+
])
|
|
2888
|
+
].sort() : baseConfiguredRunnerFiles;
|
|
2551
2889
|
const packageJson = findRootPackageJson(repoFiles);
|
|
2552
2890
|
const packageScripts = packageJson ? readPackageScripts(join(repoPath, packageJson)) : /* @__PURE__ */ new Map();
|
|
2891
|
+
const { coverageAnalysis, coveragePercent } = withContentReadSignal("A1", "coverage_percent", () => {
|
|
2892
|
+
const analysis = useV23CommandCoverage ? analyzeCoverageConfiguration(repoPath, repoFiles, useV27Detectors) : {
|
|
2893
|
+
coverageFiles: findCoverageConfigFiles(repoPath, repoFiles, useV27Detectors),
|
|
2894
|
+
unresolvedFiles: [],
|
|
2895
|
+
hasCoverageCommand: packageScripts.has("coverage")
|
|
2896
|
+
};
|
|
2897
|
+
return {
|
|
2898
|
+
coverageAnalysis: analysis,
|
|
2899
|
+
coveragePercent: readCoveragePercent(repoPath, analysis.coverageFiles.slice(0, 4))
|
|
2900
|
+
};
|
|
2901
|
+
});
|
|
2902
|
+
const allCoverageFiles = coverageAnalysis.coverageFiles;
|
|
2903
|
+
const coverageFiles = allCoverageFiles.slice(0, 4);
|
|
2553
2904
|
const packageJsonFiles = useV27Detectors ? repoFiles.filter(
|
|
2554
2905
|
(file) => /(^|\/)package\.json$/.test(file) && isAuthoredProductionPath(file)
|
|
2555
2906
|
) : packageJson ? [packageJson] : [];
|
|
2556
|
-
const coveragePercent = readCoveragePercent(repoPath, coverageFiles);
|
|
2557
2907
|
const ciWorkflows = repoFiles.filter(
|
|
2558
2908
|
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isCiWorkflow(file)
|
|
2559
2909
|
);
|
|
@@ -2568,7 +2918,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2568
2918
|
);
|
|
2569
2919
|
const verificationScriptCount = [
|
|
2570
2920
|
packageScripts.has("test") || Boolean(ciTestWorkflow),
|
|
2571
|
-
|
|
2921
|
+
coverageAnalysis.hasCoverageCommand,
|
|
2572
2922
|
packageScripts.has("lint") || ciHasLintCommand,
|
|
2573
2923
|
packageScripts.has("typecheck") || ciHasTypecheckCommand
|
|
2574
2924
|
].filter(Boolean).length;
|
|
@@ -2588,9 +2938,11 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2588
2938
|
if (packageJson && [...packageScripts.values()].some((script) => TEST_RUNNER_PATTERN.test(script))) {
|
|
2589
2939
|
evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Configured test runner"));
|
|
2590
2940
|
}
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2941
|
+
withContentReadSignal("A1", "coverage_percent", () => {
|
|
2942
|
+
for (const file of coverageFiles) {
|
|
2943
|
+
evidence.push(evidenceForRelative(repoPath, file, "coverage", "Coverage configuration"));
|
|
2944
|
+
}
|
|
2945
|
+
});
|
|
2594
2946
|
if (ciTestWorkflow) {
|
|
2595
2947
|
evidence.push(
|
|
2596
2948
|
evidenceForRelative(repoPath, ciTestWorkflow, "test_run", "CI workflow runs the test suite")
|
|
@@ -2629,7 +2981,12 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2629
2981
|
}
|
|
2630
2982
|
}
|
|
2631
2983
|
if (evidence.length === 0 && findings.length === 0 && configuredRunnerFiles.length === 0) {
|
|
2632
|
-
return reviewableSourceProof?.passes === true && reviewableSourceProof.firstReadablePath ? buildA1AuthenticatedAbsenceSignal(
|
|
2984
|
+
return reviewableSourceProof?.passes === true && reviewableSourceProof.firstReadablePath ? buildA1AuthenticatedAbsenceSignal(
|
|
2985
|
+
repoPath,
|
|
2986
|
+
reviewableSourceProof,
|
|
2987
|
+
repoFiles.length,
|
|
2988
|
+
inventoryAbsenceContext
|
|
2989
|
+
) : null;
|
|
2633
2990
|
}
|
|
2634
2991
|
if (testFiles.length === 0) {
|
|
2635
2992
|
const fallback = configuredRunnerFiles[0] ? evidenceForRelative(
|
|
@@ -2639,13 +2996,23 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2639
2996
|
"Configured test runner"
|
|
2640
2997
|
) : useV33Detectors ? void 0 : evidence[0] ?? findings[0]?.evidence;
|
|
2641
2998
|
if (fallback) {
|
|
2642
|
-
findings.push(
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2999
|
+
findings.push(
|
|
3000
|
+
inventoryAbsenceFinding({
|
|
3001
|
+
severity: "warning",
|
|
3002
|
+
summaries: {
|
|
3003
|
+
legacy: "A test runner is configured, but no concrete test files were detected.",
|
|
3004
|
+
currentTracked: "A test runner is configured, but the tracked scan-eligible inventory matched no concrete test file.",
|
|
3005
|
+
currentDirectoryWalk: "A test runner is configured, but the bounded directory walk matched no concrete test file."
|
|
3006
|
+
},
|
|
3007
|
+
evidence: fallback,
|
|
3008
|
+
context: inventoryAbsenceContext,
|
|
3009
|
+
inventoryCount: repoFiles.length,
|
|
3010
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a1ConcreteTests,
|
|
3011
|
+
matchCount: 0
|
|
3012
|
+
})
|
|
3013
|
+
);
|
|
2647
3014
|
}
|
|
2648
|
-
} else if (coverageFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
|
|
3015
|
+
} else if (coverageFiles.length === 0 && coverageAnalysis.unresolvedFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
|
|
2649
3016
|
if (isLeanTestToolchain) {
|
|
2650
3017
|
const leanEvidenceFile = packageJson ?? testFiles[0];
|
|
2651
3018
|
if (leanEvidenceFile) {
|
|
@@ -2661,17 +3028,27 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2661
3028
|
} else {
|
|
2662
3029
|
const firstEvidence = evidence[0];
|
|
2663
3030
|
if (!firstEvidence) return null;
|
|
2664
|
-
findings.push(
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
3031
|
+
findings.push(
|
|
3032
|
+
inventoryAbsenceFinding({
|
|
3033
|
+
severity: "info",
|
|
3034
|
+
summaries: {
|
|
3035
|
+
legacy: "Test suite files are present, but no coverage configuration was detected.",
|
|
3036
|
+
currentTracked: "Test suite files are present, but the tracked scan-eligible inventory matched no coverage configuration.",
|
|
3037
|
+
currentDirectoryWalk: "Test suite files are present, but the bounded directory walk matched no coverage configuration."
|
|
3038
|
+
},
|
|
3039
|
+
evidence: firstEvidence,
|
|
3040
|
+
context: inventoryAbsenceContext,
|
|
3041
|
+
inventoryCount: repoFiles.length,
|
|
3042
|
+
patternSet: useV23CommandCoverage ? INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfigurationV2 : INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfigurationV1,
|
|
3043
|
+
matchCount: 0
|
|
3044
|
+
})
|
|
3045
|
+
);
|
|
2669
3046
|
}
|
|
2670
3047
|
}
|
|
2671
|
-
const nonHollowShare =
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
reviewableSourceProof !== void 0
|
|
3048
|
+
const nonHollowShare = withContentReadSignal(
|
|
3049
|
+
"A1",
|
|
3050
|
+
"non_hollow_test_share",
|
|
3051
|
+
() => measureNonHollowTestShare(repoPath, allTestFiles, reviewableSourceProof !== void 0)
|
|
2675
3052
|
);
|
|
2676
3053
|
return {
|
|
2677
3054
|
criterionId: "A1",
|
|
@@ -2712,7 +3089,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2712
3089
|
label: "test command",
|
|
2713
3090
|
count: packageScripts.has("test") || Boolean(ciTestWorkflow) ? 1 : 0
|
|
2714
3091
|
},
|
|
2715
|
-
{ label: "coverage command", count:
|
|
3092
|
+
{ label: "coverage command", count: coverageAnalysis.hasCoverageCommand ? 1 : 0 },
|
|
2716
3093
|
{
|
|
2717
3094
|
label: "lint command",
|
|
2718
3095
|
count: packageScripts.has("lint") || ciHasLintCommand ? 1 : 0
|
|
@@ -2735,10 +3112,10 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2735
3112
|
"Penalizes skipped or placeholder-only test files; test-directory support scaffolding (helpers/fixtures with no test in them) is excluded from the denominator."
|
|
2736
3113
|
)
|
|
2737
3114
|
],
|
|
2738
|
-
notes: "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
|
|
3115
|
+
notes: coverageFiles.length === 0 && coverageAnalysis.unresolvedFiles.length > 0 ? `A1 is detected from real test files, test runner configuration, and optional coverage configuration. Coverage command reachability or flag ownership was unresolved in ${coverageAnalysis.unresolvedFiles.slice(0, 4).join(", ")}; the no-coverage absence claim was withheld.` : "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
|
|
2739
3116
|
};
|
|
2740
3117
|
}
|
|
2741
|
-
function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
|
|
3118
|
+
function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inventoryAbsenceContext) {
|
|
2742
3119
|
const sourcePath = proof.firstReadablePath;
|
|
2743
3120
|
if (!sourcePath) {
|
|
2744
3121
|
throw new Error("Cejel invariant: a passing reviewable-source proof must have a readable path");
|
|
@@ -2753,11 +3130,19 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
|
|
|
2753
3130
|
criterionId: "A1",
|
|
2754
3131
|
positiveEvidence: [anchor],
|
|
2755
3132
|
findings: [
|
|
2756
|
-
{
|
|
3133
|
+
inventoryAbsenceFinding({
|
|
2757
3134
|
severity: "critical",
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
3135
|
+
summaries: {
|
|
3136
|
+
legacy: WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY,
|
|
3137
|
+
currentTracked: WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
|
|
3138
|
+
currentDirectoryWalk: WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY
|
|
3139
|
+
},
|
|
3140
|
+
evidence: anchor,
|
|
3141
|
+
context: inventoryAbsenceContext,
|
|
3142
|
+
inventoryCount,
|
|
3143
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a1AuthenticatedAbsence,
|
|
3144
|
+
matchCount: 0
|
|
3145
|
+
})
|
|
2761
3146
|
],
|
|
2762
3147
|
metrics: [
|
|
2763
3148
|
metric(
|
|
@@ -2811,7 +3196,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof) {
|
|
|
2811
3196
|
notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
|
|
2812
3197
|
};
|
|
2813
3198
|
}
|
|
2814
|
-
function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, scanLimitations) {
|
|
3199
|
+
function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
|
|
2815
3200
|
const evidence = [];
|
|
2816
3201
|
const findings = [];
|
|
2817
3202
|
const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
|
|
@@ -2819,40 +3204,47 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
2819
3204
|
const migrationFiles = repoFiles.filter(
|
|
2820
3205
|
(file) => /(^|\/)(migrations?|drizzle|prisma)\//.test(file)
|
|
2821
3206
|
);
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
|
|
2828
|
-
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
|
|
2832
|
-
|
|
2833
|
-
|
|
2834
|
-
|
|
2835
|
-
|
|
2836
|
-
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2841
|
-
|
|
2842
|
-
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
3207
|
+
const { committedSecret, currentSecretFingerprintsByPath } = withContentReadSignal(
|
|
3208
|
+
"A2",
|
|
3209
|
+
"secret_cleanliness",
|
|
3210
|
+
() => {
|
|
3211
|
+
let match_;
|
|
3212
|
+
for (const path of repoFiles) {
|
|
3213
|
+
const authoredProductionPath = useV47Detectors ? isV47AuthoredProductionPath(path) : useV39Detectors ? isV39AuthoredProductionPath(path) : isAuthoredProductionPath(path);
|
|
3214
|
+
if (isIgnoredScanFile(path, useV47Detectors) || useV33Detectors && !authoredProductionPath) {
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
const found = findCommittedSecretInFile(
|
|
3218
|
+
repoPath,
|
|
3219
|
+
path,
|
|
3220
|
+
useV36Detectors,
|
|
3221
|
+
useV39Detectors,
|
|
3222
|
+
useV47Detectors
|
|
3223
|
+
);
|
|
3224
|
+
if (!found) continue;
|
|
3225
|
+
match_ = { path, match: found };
|
|
3226
|
+
break;
|
|
3227
|
+
}
|
|
3228
|
+
const fingerprintsByPath = /* @__PURE__ */ new Map();
|
|
3229
|
+
if (useV27Detectors) {
|
|
3230
|
+
for (const path of repoFiles.filter((candidate) => isCredentialHistoryPath(candidate))) {
|
|
3231
|
+
fingerprintsByPath.set(
|
|
2847
3232
|
path,
|
|
2848
|
-
|
|
2849
|
-
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
3233
|
+
new Set(
|
|
3234
|
+
findSecretFingerprintsInFile(
|
|
3235
|
+
repoPath,
|
|
3236
|
+
path,
|
|
3237
|
+
useV36Detectors,
|
|
3238
|
+
useV39Detectors,
|
|
3239
|
+
useV47Detectors
|
|
3240
|
+
)
|
|
3241
|
+
)
|
|
3242
|
+
);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
return { committedSecret: match_, currentSecretFingerprintsByPath: fingerprintsByPath };
|
|
2854
3246
|
}
|
|
2855
|
-
|
|
3247
|
+
);
|
|
2856
3248
|
const historySecretScan = committedSecret && !useV27Detectors ? null : collectHistorySecretEvidence(
|
|
2857
3249
|
repoPath,
|
|
2858
3250
|
useV27Detectors,
|
|
@@ -2864,10 +3256,14 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
2864
3256
|
useV47Detectors
|
|
2865
3257
|
);
|
|
2866
3258
|
const hasConfirmedSecretFinding = committedSecret != null || historySecretScan?.evidence != null;
|
|
2867
|
-
const cryptoHygiene =
|
|
2868
|
-
|
|
2869
|
-
|
|
2870
|
-
|
|
3259
|
+
const cryptoHygiene = withContentReadSignal(
|
|
3260
|
+
"A2",
|
|
3261
|
+
"crypto_comparison_hygiene",
|
|
3262
|
+
() => collectCryptoHygieneEvidence(
|
|
3263
|
+
repoPath,
|
|
3264
|
+
repoFiles.filter(isImplementationFile).slice(0, 60),
|
|
3265
|
+
useV39Detectors
|
|
3266
|
+
)
|
|
2871
3267
|
);
|
|
2872
3268
|
const implFiles = repoFiles.filter(isImplementationFile);
|
|
2873
3269
|
const gitignoreHasEnvRule = gitignore != null && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m);
|
|
@@ -2907,42 +3303,77 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
2907
3303
|
} else if (dbLayerImportFile != null) {
|
|
2908
3304
|
evidence.push(evidenceForRelative(repoPath, dbLayerImportFile, "artifact", "DB client import"));
|
|
2909
3305
|
}
|
|
2910
|
-
const v47RlsPolicyFiles = useV47Detectors ? findRlsPolicyFiles(repoPath, repoFiles) : [];
|
|
2911
|
-
const nativeRlsInventory = useV18NativeRls ? deriveRlsPolicyScopeInventory(repoPath, repoFiles) : null;
|
|
2912
|
-
const tenantWithoutRlsPremiseFiles = useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
2913
|
-
const v47TenantStorageFiles = useV47Detectors && !useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
2914
|
-
const rlsMigration = useV47Detectors ? v47RlsPolicyFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, RLS_PATTERN));
|
|
2915
3306
|
const tenantScopePattern = useV39Detectors ? TENANT_SCOPE_PATTERN_V11 : useV36Detectors ? TENANT_SCOPE_PATTERN_V10 : TENANT_SCOPE_PATTERN;
|
|
2916
|
-
const
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
3307
|
+
const {
|
|
3308
|
+
rlsMigration,
|
|
3309
|
+
tenantScopedFile,
|
|
3310
|
+
rlsPolicyCount,
|
|
3311
|
+
tenantScopedMigrationFileCount,
|
|
3312
|
+
nativeRlsInventory,
|
|
3313
|
+
tenantWithoutRlsPremiseFiles
|
|
3314
|
+
} = withContentReadSignals("A2", ["rls_policy_count", "tenant_scope_ratio"], () => {
|
|
3315
|
+
const v47RlsPolicyFiles = useV47Detectors ? findRlsPolicyFiles(repoPath, repoFiles) : [];
|
|
3316
|
+
const rlsInventory = useV18NativeRls ? deriveRlsPolicyScopeInventory(repoPath, repoFiles) : null;
|
|
3317
|
+
const tenantWithoutRlsFiles = useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
3318
|
+
const v47TenantStorageFiles = useV47Detectors && !useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
3319
|
+
return {
|
|
3320
|
+
rlsMigration: useV47Detectors ? v47RlsPolicyFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, RLS_PATTERN)),
|
|
3321
|
+
tenantScopedFile: useV18NativeRls ? rlsInventory?.storageFiles[0] : useV47Detectors ? v47TenantStorageFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, tenantScopePattern)),
|
|
3322
|
+
rlsPolicyCount: useV18NativeRls ? rlsInventory?.policyCount ?? 0 : useV47Detectors ? v47RlsPolicyFiles.length : countPatternMatches(
|
|
3323
|
+
repoPath,
|
|
3324
|
+
migrationFiles,
|
|
3325
|
+
/create policy|enable row level security|force row level security/gi
|
|
3326
|
+
),
|
|
3327
|
+
tenantScopedMigrationFileCount: useV18NativeRls ? rlsInventory?.storageFiles.length ?? 0 : useV47Detectors ? v47TenantStorageFiles.length : countFilesContaining(repoPath, migrationFiles, tenantScopePattern),
|
|
3328
|
+
nativeRlsInventory: rlsInventory,
|
|
3329
|
+
tenantWithoutRlsPremiseFiles: tenantWithoutRlsFiles
|
|
3330
|
+
};
|
|
3331
|
+
});
|
|
3332
|
+
const { gitignoreEnvFile, hasEnvReadsInImplementation, envHandlingDepth } = withContentReadSignal(
|
|
3333
|
+
"A2",
|
|
3334
|
+
"env_handling_depth",
|
|
3335
|
+
() => {
|
|
3336
|
+
const envFile = gitignore && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m) ? gitignore : null;
|
|
3337
|
+
const hasEnvReads = countFilesContaining(repoPath, repoFiles.filter(isImplementationFile), ENV_READ_PATTERN) > 0;
|
|
3338
|
+
const depth = (envExamples.length > 0 ? 1 : 0) + (envFile ? 1 : 0) + (hasEnvReads ? 1 : 0);
|
|
3339
|
+
return { gitignoreEnvFile: envFile, hasEnvReadsInImplementation: hasEnvReads, envHandlingDepth: depth };
|
|
3340
|
+
}
|
|
2921
3341
|
);
|
|
2922
|
-
const tenantScopedMigrationFileCount = useV18NativeRls ? nativeRlsInventory?.storageFiles.length ?? 0 : useV47Detectors ? v47TenantStorageFiles.length : countFilesContaining(repoPath, migrationFiles, tenantScopePattern);
|
|
2923
|
-
const gitignoreEnvFile = gitignore && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m) ? gitignore : null;
|
|
2924
|
-
const envHandlingDepth = (envExamples.length > 0 ? 1 : 0) + (gitignoreEnvFile ? 1 : 0) + (countFilesContaining(repoPath, repoFiles.filter(isImplementationFile), ENV_READ_PATTERN) > 0 ? 1 : 0);
|
|
2925
3342
|
const isMultiTenant = useV18NativeRls ? (nativeRlsInventory?.policyCount ?? 0) > 0 && (nativeRlsInventory?.scopeIdentifiers.length ?? 0) > 0 : tenantScopedMigrationFileCount > 0;
|
|
2926
3343
|
if (gitignoreEnvFile) {
|
|
2927
|
-
|
|
2928
|
-
|
|
3344
|
+
withContentReadSignal(
|
|
3345
|
+
"A2",
|
|
3346
|
+
"env_handling_depth",
|
|
3347
|
+
() => evidence.push(
|
|
3348
|
+
evidenceForRelative(repoPath, gitignoreEnvFile, "secret_scan", ".env files are gitignored")
|
|
3349
|
+
)
|
|
2929
3350
|
);
|
|
2930
3351
|
}
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
3352
|
+
withContentReadSignal("A2", "env_handling_depth", () => {
|
|
3353
|
+
for (const file of envExamples.slice(0, 3)) {
|
|
3354
|
+
evidence.push(evidenceForRelative(repoPath, file, "secret_scan", "Environment template"));
|
|
3355
|
+
}
|
|
3356
|
+
});
|
|
2934
3357
|
if (rlsMigration) {
|
|
2935
|
-
|
|
2936
|
-
|
|
3358
|
+
withContentReadSignals(
|
|
3359
|
+
"A2",
|
|
3360
|
+
["rls_policy_count", "tenant_scope_ratio"],
|
|
3361
|
+
() => evidence.push(
|
|
3362
|
+
evidenceForRelative(repoPath, rlsMigration, "artifact", "RLS or tenant migration")
|
|
3363
|
+
)
|
|
2937
3364
|
);
|
|
2938
3365
|
}
|
|
2939
3366
|
if (tenantScopedFile) {
|
|
2940
|
-
|
|
2941
|
-
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
3367
|
+
withContentReadSignals(
|
|
3368
|
+
"A2",
|
|
3369
|
+
["rls_policy_count", "tenant_scope_ratio"],
|
|
3370
|
+
() => evidence.push(
|
|
3371
|
+
evidenceForRelative(
|
|
3372
|
+
repoPath,
|
|
3373
|
+
tenantScopedFile,
|
|
3374
|
+
"artifact",
|
|
3375
|
+
useV18NativeRls ? `Repository-native RLS scope (${nativeRlsInventory?.scopeIdentifiers.join(", ")})` : "Tenant scoping signal"
|
|
3376
|
+
)
|
|
2946
3377
|
)
|
|
2947
3378
|
);
|
|
2948
3379
|
}
|
|
@@ -2964,16 +3395,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
2964
3395
|
)
|
|
2965
3396
|
});
|
|
2966
3397
|
} else if (committedEnvFilePath) {
|
|
2967
|
-
findings.push(
|
|
2968
|
-
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
2973
|
-
|
|
2974
|
-
|
|
2975
|
-
|
|
2976
|
-
|
|
3398
|
+
findings.push(
|
|
3399
|
+
inventoryAbsenceFinding({
|
|
3400
|
+
severity: useV33Detectors ? "info" : "warning",
|
|
3401
|
+
summaries: {
|
|
3402
|
+
legacy: "A non-template .env file is committed in the current repository tree; no secret-shaped value was detected.",
|
|
3403
|
+
currentTracked: "A tracked non-template .env file is present, while the bounded scan over the tracked scan-eligible inventory matched no secret-shaped value.",
|
|
3404
|
+
currentDirectoryWalk: "A non-template .env file is present in the bounded directory walk, which matched no secret-shaped value."
|
|
3405
|
+
},
|
|
3406
|
+
evidence: evidenceForRelative(
|
|
3407
|
+
repoPath,
|
|
3408
|
+
committedEnvFilePath,
|
|
3409
|
+
"secret_scan",
|
|
3410
|
+
"Committed .env file (no confirmed secret value found)"
|
|
3411
|
+
),
|
|
3412
|
+
context: inventoryAbsenceContext,
|
|
3413
|
+
inventoryCount: repoFiles.length,
|
|
3414
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a2CurrentSecret,
|
|
3415
|
+
matchCount: 0
|
|
3416
|
+
})
|
|
3417
|
+
);
|
|
2977
3418
|
}
|
|
2978
3419
|
if (historySecretScan?.evidence) {
|
|
2979
3420
|
const historyPath = historySecretScan.evidence.path ?? "";
|
|
@@ -3005,16 +3446,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
3005
3446
|
if (hasTenantWithoutRlsGap) {
|
|
3006
3447
|
const gapEvidence = rlsMigration ?? tenantScopedFile ?? tenantWithoutRlsPremiseFiles[0];
|
|
3007
3448
|
if (gapEvidence) {
|
|
3008
|
-
findings.push(
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3014
|
-
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3449
|
+
findings.push(
|
|
3450
|
+
inventoryAbsenceFinding({
|
|
3451
|
+
severity: "warning",
|
|
3452
|
+
summaries: {
|
|
3453
|
+
legacy: "Tenant-scoped schema detected but no row-level-security policies found \u2014 isolation gap.",
|
|
3454
|
+
currentTracked: "A tenant-scoped schema signal is present, while the tracked scan-eligible inventory matched no row-level-security policy.",
|
|
3455
|
+
currentDirectoryWalk: "A tenant-scoped schema signal is present, while the bounded directory walk matched no row-level-security policy."
|
|
3456
|
+
},
|
|
3457
|
+
evidence: evidenceForRelative(
|
|
3458
|
+
repoPath,
|
|
3459
|
+
gapEvidence,
|
|
3460
|
+
"artifact",
|
|
3461
|
+
"Tenant schema without RLS policies"
|
|
3462
|
+
),
|
|
3463
|
+
context: inventoryAbsenceContext,
|
|
3464
|
+
inventoryCount: repoFiles.length,
|
|
3465
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a2RlsPolicy,
|
|
3466
|
+
matchCount: 0
|
|
3467
|
+
})
|
|
3468
|
+
);
|
|
3018
3469
|
}
|
|
3019
3470
|
}
|
|
3020
3471
|
evidence.push(...cryptoHygiene.evidence);
|
|
@@ -3046,11 +3497,7 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
3046
3497
|
{ label: ".gitignore environment-file rule", count: gitignoreEnvFile ? 1 : 0 },
|
|
3047
3498
|
{
|
|
3048
3499
|
label: "environment reads in implementation code",
|
|
3049
|
-
count:
|
|
3050
|
-
repoPath,
|
|
3051
|
-
repoFiles.filter(isImplementationFile),
|
|
3052
|
-
ENV_READ_PATTERN
|
|
3053
|
-
) > 0 ? 1 : 0
|
|
3500
|
+
count: hasEnvReadsInImplementation ? 1 : 0
|
|
3054
3501
|
}
|
|
3055
3502
|
]
|
|
3056
3503
|
}
|
|
@@ -3199,7 +3646,7 @@ function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = fal
|
|
|
3199
3646
|
}
|
|
3200
3647
|
return { evidence, findings, hasSurface, clean: hasSurface && !insecureFound };
|
|
3201
3648
|
}
|
|
3202
|
-
function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false) {
|
|
3649
|
+
function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
3203
3650
|
const v20DirectHttpEntrypoint = useV20ExplicitGaps ? findV20DirectHttpServerEntrypointFile(repoPath, repoFiles) : null;
|
|
3204
3651
|
const v22PackageStartHttpEntrypoint = useV22PackageStartEntrypoint ? findV22PackageStartHttpServerEntrypointFile(repoPath, repoFiles) : null;
|
|
3205
3652
|
const v20RuntimeContainer = useV20ExplicitGaps ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, true) : null;
|
|
@@ -3229,37 +3676,51 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3229
3676
|
const envTemplate = repoFiles.find(
|
|
3230
3677
|
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isEnvTemplatePath(file)
|
|
3231
3678
|
);
|
|
3232
|
-
const healthChecks =
|
|
3233
|
-
|
|
3679
|
+
const healthChecks = withContentReadSignal(
|
|
3680
|
+
"A3",
|
|
3681
|
+
"prod_readiness_primitives",
|
|
3682
|
+
() => repoFiles.filter(
|
|
3683
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isHealthCheckSignalFile(repoPath, file)
|
|
3684
|
+
)
|
|
3234
3685
|
);
|
|
3235
3686
|
const healthCheck = healthChecks[0];
|
|
3236
|
-
const hasV20HealthOrReadinessRoute =
|
|
3237
|
-
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3687
|
+
const { hasV20HealthOrReadinessRoute, hasV22PackageStartHealthOrReadinessRoute } = withContentReadSignal("A3", "health_readiness_route", () => ({
|
|
3688
|
+
hasV20HealthOrReadinessRoute: useV20ExplicitGaps && repoFiles.some(
|
|
3689
|
+
(file) => isAuthoredProductionPath(file) && isImplementationFile(file) && fileContains(repoPath, file, V20_HEALTH_OR_READINESS_ROUTE_PATTERN)
|
|
3690
|
+
),
|
|
3691
|
+
hasV22PackageStartHealthOrReadinessRoute: v22PackageStartHttpEntrypoint !== null && fileContains(
|
|
3692
|
+
repoPath,
|
|
3693
|
+
v22PackageStartHttpEntrypoint,
|
|
3694
|
+
V20_HEALTH_OR_READINESS_ROUTE_PATTERN
|
|
3695
|
+
)
|
|
3696
|
+
}));
|
|
3244
3697
|
const serverEntrypoint = v22PackageStartHttpEntrypoint ?? findServerEntrypointFile(repoPath, repoFiles, useV27Detectors) ?? v20DirectHttpEntrypoint ?? null;
|
|
3245
3698
|
const runtimeContainer = useV27Detectors ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, useV20ExplicitGaps) : null;
|
|
3246
3699
|
const errorBoundaries = repoFiles.filter(
|
|
3247
3700
|
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /error-boundary|error\.(tsx|jsx|ts|js)$/.test(file)
|
|
3248
3701
|
);
|
|
3249
3702
|
const errorBoundary = errorBoundaries[0];
|
|
3250
|
-
const observabilityCount =
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3703
|
+
const observabilityCount = withContentReadSignal(
|
|
3704
|
+
"A3",
|
|
3705
|
+
"observability_depth",
|
|
3706
|
+
() => countFilesContaining(
|
|
3707
|
+
repoPath,
|
|
3708
|
+
repoFiles.filter(
|
|
3709
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isImplementationFile(file)
|
|
3710
|
+
),
|
|
3711
|
+
/sentry|otel|opentelemetry|datadog|prometheus|metrics|logger|logtail/i
|
|
3712
|
+
)
|
|
3256
3713
|
);
|
|
3257
|
-
const rollbackSafetyCount =
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3714
|
+
const rollbackSafetyCount = withContentReadSignal(
|
|
3715
|
+
"A3",
|
|
3716
|
+
"rollback_safety_depth",
|
|
3717
|
+
() => countFilesContaining(
|
|
3718
|
+
repoPath,
|
|
3719
|
+
repoFiles.filter(
|
|
3720
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /(^|\/)(docs|migrations?|drizzle|prisma|scripts)\//.test(file)
|
|
3721
|
+
),
|
|
3722
|
+
/rollback|roll back|migration safety|down migration|reversible|undo migration/i
|
|
3723
|
+
)
|
|
3263
3724
|
);
|
|
3264
3725
|
if (packageJson && (scripts.has("build") || scripts.has("typecheck"))) {
|
|
3265
3726
|
evidence.push(
|
|
@@ -3287,10 +3748,15 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3287
3748
|
);
|
|
3288
3749
|
if (envTemplate)
|
|
3289
3750
|
evidence.push(evidenceForRelative(repoPath, envTemplate, "prod_check", "Environment template"));
|
|
3290
|
-
if (healthCheck)
|
|
3291
|
-
|
|
3292
|
-
|
|
3751
|
+
if (healthCheck) {
|
|
3752
|
+
withContentReadSignal(
|
|
3753
|
+
"A3",
|
|
3754
|
+
"prod_readiness_primitives",
|
|
3755
|
+
() => evidence.push(
|
|
3756
|
+
evidenceForRelative(repoPath, healthCheck, "prod_check", "Health/readiness signal")
|
|
3757
|
+
)
|
|
3293
3758
|
);
|
|
3759
|
+
}
|
|
3294
3760
|
if (errorBoundary)
|
|
3295
3761
|
evidence.push(evidenceForRelative(repoPath, errorBoundary, "prod_check", "Error boundary"));
|
|
3296
3762
|
if (evidence.length === 0) {
|
|
@@ -3329,7 +3795,12 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3329
3795
|
)
|
|
3330
3796
|
});
|
|
3331
3797
|
} else if (runtimeContainer) {
|
|
3332
|
-
|
|
3798
|
+
const hasActiveHealthcheck = withContentReadSignal(
|
|
3799
|
+
"A3",
|
|
3800
|
+
"dockerfile_healthcheck",
|
|
3801
|
+
() => fileContains(repoPath, runtimeContainer, /^\s*HEALTHCHECK\s+(?!NONE\b)/im)
|
|
3802
|
+
);
|
|
3803
|
+
if (!hasActiveHealthcheck) {
|
|
3333
3804
|
findings.push({
|
|
3334
3805
|
severity: "info",
|
|
3335
3806
|
summary: "A runtime Dockerfile declares no active HEALTHCHECK instruction.",
|
|
@@ -3341,10 +3812,14 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3341
3812
|
)
|
|
3342
3813
|
});
|
|
3343
3814
|
}
|
|
3344
|
-
} else if (serverEntrypoint &&
|
|
3345
|
-
|
|
3346
|
-
|
|
3347
|
-
|
|
3815
|
+
} else if (serverEntrypoint && withContentReadSignal(
|
|
3816
|
+
"A3",
|
|
3817
|
+
"health_readiness_route",
|
|
3818
|
+
() => fileContains(
|
|
3819
|
+
repoPath,
|
|
3820
|
+
serverEntrypoint,
|
|
3821
|
+
/\b(?:request|req)\.(?:url|path|pathname|method)\b/i
|
|
3822
|
+
)
|
|
3348
3823
|
) && healthChecks.length === 0 && !hasV20HealthOrReadinessRoute && !hasV22PackageStartHealthOrReadinessRoute) {
|
|
3349
3824
|
findings.push({
|
|
3350
3825
|
severity: "info",
|
|
@@ -3361,11 +3836,21 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3361
3836
|
if (!workflow && releaseDeployConfigs.length === 0) {
|
|
3362
3837
|
const firstEvidence = evidence[0];
|
|
3363
3838
|
if (!firstEvidence) return null;
|
|
3364
|
-
findings.push(
|
|
3365
|
-
|
|
3366
|
-
|
|
3367
|
-
|
|
3368
|
-
|
|
3839
|
+
findings.push(
|
|
3840
|
+
inventoryAbsenceFinding({
|
|
3841
|
+
severity: "info",
|
|
3842
|
+
summaries: {
|
|
3843
|
+
legacy: "A deployable service surface exists, but no CI or release-deployment automation was detected.",
|
|
3844
|
+
currentTracked: "A deployable service signal is present, while the tracked scan-eligible inventory matched no CI workflow or release-deployment automation.",
|
|
3845
|
+
currentDirectoryWalk: "A deployable service signal is present, while the bounded directory walk matched no CI workflow or release-deployment automation."
|
|
3846
|
+
},
|
|
3847
|
+
evidence: firstEvidence,
|
|
3848
|
+
context: inventoryAbsenceContext,
|
|
3849
|
+
inventoryCount: repoFiles.length,
|
|
3850
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a3CiOrReleaseDeploy,
|
|
3851
|
+
matchCount: 0
|
|
3852
|
+
})
|
|
3853
|
+
);
|
|
3369
3854
|
}
|
|
3370
3855
|
return {
|
|
3371
3856
|
criterionId: "A3",
|
|
@@ -3435,27 +3920,46 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3435
3920
|
]
|
|
3436
3921
|
};
|
|
3437
3922
|
}
|
|
3438
|
-
function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors) {
|
|
3923
|
+
function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
3439
3924
|
const evidence = [];
|
|
3440
3925
|
const findings = [];
|
|
3441
3926
|
const manifests = repoFiles.filter(isDependencyManifest);
|
|
3442
3927
|
const lockfiles = repoFiles.filter(isLockfile);
|
|
3443
|
-
const manifest = pickPrimaryDependencyManifest(repoPath, manifests);
|
|
3444
3928
|
const lockfile = lockfiles[0];
|
|
3445
3929
|
const rootLockfile = mono ? mono.sharedFiles.find(isLockfile) : void 0;
|
|
3446
3930
|
const rootUpdateConfig = mono ? mono.sharedFiles.find(isDependencyUpdateConfig) : void 0;
|
|
3447
3931
|
const hasLockfile = Boolean(lockfile) || Boolean(rootLockfile);
|
|
3448
3932
|
const updateConfig = repoFiles.find(isDependencyUpdateConfig);
|
|
3449
3933
|
const hasUpdateConfig = Boolean(updateConfig) || Boolean(rootUpdateConfig);
|
|
3450
|
-
const auditConfig =
|
|
3451
|
-
|
|
3934
|
+
const auditConfig = withContentReadSignal(
|
|
3935
|
+
"A4",
|
|
3936
|
+
"dependency_automation_ratio",
|
|
3937
|
+
() => manifests.some((file) => packageJsonHasAuditScript(repoPath, file))
|
|
3938
|
+
);
|
|
3939
|
+
const { manifest, dependencySpecs } = withContentReadSignals(
|
|
3940
|
+
"A4",
|
|
3941
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
3942
|
+
() => ({
|
|
3943
|
+
// Cite the manifest that actually carries dependencies for evidence/findings, not
|
|
3944
|
+
// whichever sorts first (e.g. a near-empty requirements-windows.txt that precedes the
|
|
3945
|
+
// real requirements.txt), so the evidence pointer reflects the file the A4 score is
|
|
3946
|
+
// derived from. The score itself is unaffected: it aggregates specs across ALL manifests.
|
|
3947
|
+
manifest: pickPrimaryDependencyManifest(repoPath, manifests),
|
|
3948
|
+
dependencySpecs: readDependencySpecs(repoPath, manifests)
|
|
3949
|
+
})
|
|
3950
|
+
);
|
|
3452
3951
|
const pinnedDependencyCount = dependencySpecs.filter(
|
|
3453
3952
|
(dependency) => isPinnedDependencyVersion(dependency.version)
|
|
3454
3953
|
).length;
|
|
3455
|
-
if (manifest)
|
|
3456
|
-
|
|
3457
|
-
|
|
3954
|
+
if (manifest) {
|
|
3955
|
+
withContentReadSignals(
|
|
3956
|
+
"A4",
|
|
3957
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
3958
|
+
() => evidence.push(
|
|
3959
|
+
evidenceForRelative(repoPath, manifest, "dependency_report", "Dependency manifest")
|
|
3960
|
+
)
|
|
3458
3961
|
);
|
|
3962
|
+
}
|
|
3459
3963
|
if (lockfile)
|
|
3460
3964
|
evidence.push(
|
|
3461
3965
|
evidenceForRelative(repoPath, lockfile, "dependency_report", "Dependency lockfile")
|
|
@@ -3488,13 +3992,36 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
|
|
|
3488
3992
|
const deployableServiceFiles = useV47Detectors ? findDeployableServicePremiseFiles(repoPath, repoFiles, true) : [];
|
|
3489
3993
|
const isAppOrService = (useV47Detectors ? deployableServiceFiles.length > 0 : isDeployableService(repoPath, repoFiles, useV27Detectors)) || packagedApplicationFiles.length > 0;
|
|
3490
3994
|
if (isAppOrService && !hasLockfile && manifest) {
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3995
|
+
withContentReadSignals(
|
|
3996
|
+
"A4",
|
|
3997
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
3998
|
+
() => findings.push(
|
|
3999
|
+
inventoryAbsenceFinding({
|
|
4000
|
+
severity: "critical",
|
|
4001
|
+
summaries: {
|
|
4002
|
+
legacy: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
|
|
4003
|
+
currentTracked: "Dependency manifest is present, but the tracked scan-eligible inventory matched no recognized lockfile.",
|
|
4004
|
+
currentDirectoryWalk: "Dependency manifest is present, but the bounded directory walk matched no recognized lockfile."
|
|
4005
|
+
},
|
|
4006
|
+
evidence: evidenceForRelative(
|
|
4007
|
+
repoPath,
|
|
4008
|
+
manifest,
|
|
4009
|
+
"dependency_report",
|
|
4010
|
+
"Dependency manifest"
|
|
4011
|
+
),
|
|
4012
|
+
context: inventoryAbsenceContext,
|
|
4013
|
+
inventoryCount: repoFiles.length + (mono?.sharedFiles.length ?? 0),
|
|
4014
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a4Lockfile,
|
|
4015
|
+
matchCount: 0
|
|
4016
|
+
})
|
|
4017
|
+
)
|
|
4018
|
+
);
|
|
3496
4019
|
}
|
|
3497
|
-
if (manifest &&
|
|
4020
|
+
if (manifest && withContentReadSignal(
|
|
4021
|
+
"A4",
|
|
4022
|
+
"suspicious_dependencies",
|
|
4023
|
+
() => hasSuspiciousDependencies(repoPath, manifest, useV47Detectors)
|
|
4024
|
+
)) {
|
|
3498
4025
|
findings.push({
|
|
3499
4026
|
severity: "critical",
|
|
3500
4027
|
summary: "Dependency manifest contains obviously suspicious placeholder dependency names.",
|
|
@@ -3600,7 +4127,7 @@ function hasDeclaredVersionConstraint(dependency) {
|
|
|
3600
4127
|
if (/[~^<>=!]/.test(trimmed)) return true;
|
|
3601
4128
|
return /^v?\d/.test(trimmed);
|
|
3602
4129
|
}
|
|
3603
|
-
function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors) {
|
|
4130
|
+
function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
3604
4131
|
const reconciliationArtifacts = useV27Detectors ? findClaimRealityReconciliationArtifacts(repoPath, repoFiles) : [];
|
|
3605
4132
|
const claimSourceFiles = useV27Detectors ? findClaimSourceFiles(repoFiles) : repoFiles.filter(
|
|
3606
4133
|
(file) => /^(?:README|readme)\.md$/.test(file) || /^docs\/[^/]+\.(?:md|mdx)$/.test(file)
|
|
@@ -3648,16 +4175,24 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors) {
|
|
|
3648
4175
|
...allDocFiles,
|
|
3649
4176
|
...repoFiles.filter((file) => /(^|\/)(SECURITY|THREAT[_-]?MODEL)\.md$/i.test(file))
|
|
3650
4177
|
]);
|
|
3651
|
-
const negativeSpaceDoc =
|
|
3652
|
-
|
|
4178
|
+
const negativeSpaceDoc = withContentReadSignal(
|
|
4179
|
+
"A5",
|
|
4180
|
+
"negative_space_documentation",
|
|
4181
|
+
() => [...negativeSpaceCandidates].find(
|
|
4182
|
+
(file) => fileContains(repoPath, file, NEGATIVE_SPACE_SECTION_PATTERN) || fileContains(repoPath, file, NEGATIVE_SPACE_PHRASE_PATTERN)
|
|
4183
|
+
)
|
|
3653
4184
|
);
|
|
3654
4185
|
if (negativeSpaceDoc) {
|
|
3655
|
-
|
|
3656
|
-
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
4186
|
+
withContentReadSignal(
|
|
4187
|
+
"A5",
|
|
4188
|
+
"negative_space_documentation",
|
|
4189
|
+
() => evidence.push(
|
|
4190
|
+
evidenceForRelative(
|
|
4191
|
+
repoPath,
|
|
4192
|
+
negativeSpaceDoc,
|
|
4193
|
+
"claim_reconciliation",
|
|
4194
|
+
'Documented limitations / threat model / "not covered" section'
|
|
4195
|
+
)
|
|
3661
4196
|
)
|
|
3662
4197
|
);
|
|
3663
4198
|
}
|
|
@@ -3665,15 +4200,23 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors) {
|
|
|
3665
4200
|
criterionId: "A5",
|
|
3666
4201
|
positiveEvidence: evidence,
|
|
3667
4202
|
findings: reconciliationArtifacts.length > 0 ? [] : [
|
|
3668
|
-
{
|
|
4203
|
+
inventoryAbsenceFinding({
|
|
3669
4204
|
// Proxy-only A5: cap at warning (never critical) — a static proxy is lower-confidence
|
|
3670
4205
|
// than a curated registry or reconciliation artifact. Downgraded to info when the repo
|
|
3671
4206
|
// documents honest negative space: that scoping IS a form of claim-reality discipline,
|
|
3672
4207
|
// not an additional gap on top of the missing dedicated artifact.
|
|
3673
4208
|
severity: negativeSpaceDoc ? "info" : "warning",
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
4209
|
+
summaries: {
|
|
4210
|
+
legacy: negativeSpaceDoc ? "Claim source and implementation files are present; no dedicated claim-reality report artifact was supplied, but the repo explicitly documents what it does NOT cover/protect against \u2014 honest scoping, not overclaiming." : "Claim source and implementation files are present, but no dedicated claim-reality report artifact was supplied.",
|
|
4211
|
+
currentTracked: negativeSpaceDoc ? "Claim-source and implementation-file signals are present, while the tracked scan-eligible inventory matched no dedicated claim-reality report artifact; the repo separately documents what it does NOT cover or protect against." : "Claim-source and implementation-file signals are present, while the tracked scan-eligible inventory matched no dedicated claim-reality report artifact.",
|
|
4212
|
+
currentDirectoryWalk: negativeSpaceDoc ? "Claim-source and implementation-file signals are present, while the bounded directory walk matched no dedicated claim-reality report artifact; the repo separately documents what it does NOT cover or protect against." : "Claim-source and implementation-file signals are present, while the bounded directory walk matched no dedicated claim-reality report artifact."
|
|
4213
|
+
},
|
|
4214
|
+
evidence: firstEvidence,
|
|
4215
|
+
context: inventoryAbsenceContext,
|
|
4216
|
+
inventoryCount: repoFiles.length,
|
|
4217
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a5ClaimRealityArtifact,
|
|
4218
|
+
matchCount: 0
|
|
4219
|
+
})
|
|
3677
4220
|
],
|
|
3678
4221
|
// Recalibrated (goal_cejel_launch_hardening_combined_2026-07-06, Phase 3 H1):
|
|
3679
4222
|
// reconciliation_artifact_depth is usually 0 on the generic-proxy path, so its prior 0.3
|
|
@@ -3786,18 +4329,33 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
3786
4329
|
const workflows = repoFiles.filter(isCiWorkflow);
|
|
3787
4330
|
const workflow = workflows[0];
|
|
3788
4331
|
const rootWorkflows = mono ? mono.sharedFiles.filter(isCiWorkflow) : [];
|
|
3789
|
-
const defaultBranchCiCount =
|
|
4332
|
+
const defaultBranchCiCount = withContentReadSignal(
|
|
4333
|
+
"B3",
|
|
4334
|
+
"default_branch_ci_depth",
|
|
4335
|
+
() => workflows.filter((file) => workflowTargetsDefaultBranch(repoPath, file)).length + (mono ? rootWorkflows.filter((file) => workflowTargetsDefaultBranch(mono.root, file)).length : 0)
|
|
4336
|
+
);
|
|
3790
4337
|
if (packageJson && scripts.has("test")) {
|
|
3791
4338
|
evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Test script"));
|
|
3792
4339
|
}
|
|
3793
4340
|
if (packageJson && scripts.has("lint")) {
|
|
3794
4341
|
evidence.push(evidenceForRelative(repoPath, packageJson, "ci_run", "Lint script"));
|
|
3795
4342
|
}
|
|
3796
|
-
if (workflow)
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
4343
|
+
if (workflow) {
|
|
4344
|
+
withContentReadSignals(
|
|
4345
|
+
"B3",
|
|
4346
|
+
["ci_script_depth", "default_branch_ci_depth"],
|
|
4347
|
+
() => evidence.push(evidenceForRelative(repoPath, workflow, "ci_run", "CI workflow"))
|
|
3800
4348
|
);
|
|
4349
|
+
} else if (rootWorkflows[0] && mono) {
|
|
4350
|
+
const rootWorkflow = rootWorkflows[0];
|
|
4351
|
+
withContentReadSignals(
|
|
4352
|
+
"B3",
|
|
4353
|
+
["ci_script_depth", "default_branch_ci_depth"],
|
|
4354
|
+
() => evidence.push(
|
|
4355
|
+
evidenceForRelative(mono.root, rootWorkflow, "ci_run", "CI workflow (monorepo root)")
|
|
4356
|
+
)
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
3801
4359
|
if (evidence.length === 0) return null;
|
|
3802
4360
|
const CI_BUILD_COMMAND_PATTERN = /\b(cargo\s+build|cmake|ninja|make\s+(build|all))\b/i;
|
|
3803
4361
|
const CI_COMMAND_CATEGORIES = [
|
|
@@ -3807,10 +4365,19 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
3807
4365
|
CI_BUILD_COMMAND_PATTERN
|
|
3808
4366
|
];
|
|
3809
4367
|
const scriptDepth = ["test", "lint", "typecheck", "build"].filter((s) => scripts.has(s)).length;
|
|
3810
|
-
const
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
4368
|
+
const { repoCategoryHits, rootCategoryHits } = withContentReadSignal(
|
|
4369
|
+
"B3",
|
|
4370
|
+
"ci_script_depth",
|
|
4371
|
+
() => ({
|
|
4372
|
+
repoCategoryHits: CI_COMMAND_CATEGORIES.map(
|
|
4373
|
+
(pattern) => workflows.some((file) => fileContains(repoPath, file, pattern))
|
|
4374
|
+
),
|
|
4375
|
+
rootCategoryHits: mono ? CI_COMMAND_CATEGORIES.map(
|
|
4376
|
+
(pattern) => rootWorkflows.some((file) => fileContains(mono.root, file, pattern))
|
|
4377
|
+
) : []
|
|
4378
|
+
})
|
|
4379
|
+
);
|
|
4380
|
+
const ciCommandDepth = repoCategoryHits.filter(Boolean).length + rootCategoryHits.filter(Boolean).length;
|
|
3814
4381
|
const combinedDepth = scriptDepth + ciCommandDepth;
|
|
3815
4382
|
return {
|
|
3816
4383
|
criterionId: "B3",
|
|
@@ -3832,13 +4399,13 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
3832
4399
|
label: `package script: ${name === "typecheck" ? "type-check" : name}`,
|
|
3833
4400
|
count: scripts.has(name) ? 1 : 0
|
|
3834
4401
|
})),
|
|
3835
|
-
...CI_COMMAND_CATEGORIES.map((
|
|
4402
|
+
...CI_COMMAND_CATEGORIES.map((_pattern, index) => ({
|
|
3836
4403
|
label: `repository CI command: ${["test", "lint", "type-check", "build"][index]}`,
|
|
3837
|
-
count:
|
|
4404
|
+
count: repoCategoryHits[index] ? 1 : 0
|
|
3838
4405
|
})),
|
|
3839
|
-
...mono ? CI_COMMAND_CATEGORIES.map((
|
|
4406
|
+
...mono ? CI_COMMAND_CATEGORIES.map((_pattern, index) => ({
|
|
3840
4407
|
label: `monorepo-root CI command: ${["test", "lint", "type-check", "build"][index]}`,
|
|
3841
|
-
count:
|
|
4408
|
+
count: rootCategoryHits[index] ? 1 : 0
|
|
3842
4409
|
})) : []
|
|
3843
4410
|
]
|
|
3844
4411
|
}
|
|
@@ -3883,9 +4450,11 @@ function readAuditFreshnessCommitterYear(repoPath, scanLimitations) {
|
|
|
3883
4450
|
function collectB4AuditEvidence(repoPath, repoFiles, generatedAt, useV19CommitYear = false, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
3884
4451
|
const evidence = [];
|
|
3885
4452
|
const auditFiles = repoFiles.filter(isAuditFile);
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4453
|
+
withContentReadSignal("B4", "audit_freshness_depth", () => {
|
|
4454
|
+
for (const file of auditFiles.slice(0, 5)) {
|
|
4455
|
+
evidence.push(evidenceForRelative(repoPath, file, "audit_log", "Audit or changelog artifact"));
|
|
4456
|
+
}
|
|
4457
|
+
});
|
|
3889
4458
|
if (evidence.length === 0) {
|
|
3890
4459
|
return buildNotApplicableSignal(
|
|
3891
4460
|
"B4",
|
|
@@ -3930,7 +4499,11 @@ function collectB4AuditEvidence(repoPath, repoFiles, generatedAt, useV19CommitYe
|
|
|
3930
4499
|
metric(
|
|
3931
4500
|
"audit_freshness_depth",
|
|
3932
4501
|
"Audit freshness depth",
|
|
3933
|
-
|
|
4502
|
+
withContentReadSignal(
|
|
4503
|
+
"B4",
|
|
4504
|
+
"audit_freshness_depth",
|
|
4505
|
+
() => countFilesContaining(repoPath, auditFiles, freshnessPattern)
|
|
4506
|
+
),
|
|
3934
4507
|
Math.max(auditFiles.length, 1),
|
|
3935
4508
|
0.2,
|
|
3936
4509
|
"ratio",
|
|
@@ -3943,93 +4516,128 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
|
|
|
3943
4516
|
const evidence = [];
|
|
3944
4517
|
const findings = [];
|
|
3945
4518
|
const docFiles = repoFiles.filter((file) => /\.(md|mdx)$/i.test(file));
|
|
3946
|
-
const humanGateDoc =
|
|
3947
|
-
|
|
4519
|
+
const humanGateDoc = withContentReadSignal(
|
|
4520
|
+
"B6",
|
|
4521
|
+
"human_gate_documented",
|
|
4522
|
+
() => docFiles.find((file) => fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN))
|
|
3948
4523
|
);
|
|
3949
4524
|
const implFiles = repoFiles.filter(isImplementationFile);
|
|
3950
|
-
const gatedPrivilegeCheckFile =
|
|
3951
|
-
|
|
4525
|
+
const gatedPrivilegeCheckFile = withContentReadSignal(
|
|
4526
|
+
"B6",
|
|
4527
|
+
"fail_closed_privilege_check",
|
|
4528
|
+
() => implFiles.find(
|
|
4529
|
+
(file) => fileContains(repoPath, file, GATED_PRIVILEGE_CHECK_PATTERN) && fileContains(repoPath, file, SET_ROLE_PATTERN)
|
|
4530
|
+
)
|
|
3952
4531
|
);
|
|
3953
|
-
const killSwitchFile =
|
|
3954
|
-
|
|
4532
|
+
const killSwitchFile = withContentReadSignal(
|
|
4533
|
+
"B6",
|
|
4534
|
+
"kill_switch_fail_safe_present",
|
|
4535
|
+
() => implFiles.find(
|
|
4536
|
+
(file) => fileContains(repoPath, file, KILL_SWITCH_NAME_PATTERN) && fileContains(repoPath, file, KILL_SWITCH_FAIL_CLOSED_PATTERN)
|
|
4537
|
+
)
|
|
3955
4538
|
);
|
|
3956
4539
|
const codeownersFile = repoFiles.find((file) => /(^|\/)CODEOWNERS$/.test(file));
|
|
3957
|
-
const reviewGateDoc =
|
|
3958
|
-
|
|
4540
|
+
const reviewGateDoc = withContentReadSignal(
|
|
4541
|
+
"B6",
|
|
4542
|
+
"protected_path_review_gate",
|
|
4543
|
+
() => docFiles.find((file) => fileContains(repoPath, file, REQUIRED_REVIEW_PATTERN))
|
|
3959
4544
|
);
|
|
3960
4545
|
const protectedPathReviewGate = codeownersFile ?? reviewGateDoc;
|
|
3961
|
-
const executableFiles =
|
|
3962
|
-
(file) => fileContains(repoPath, file, SQL_EXEC_PATTERN)
|
|
3963
|
-
|
|
3964
|
-
|
|
3965
|
-
|
|
3966
|
-
|
|
3967
|
-
|
|
3968
|
-
|
|
4546
|
+
const { executableFiles, v21ExecutedEscalationFileSet, productionUngatedEscalationFiles } = withContentReadSignal("B6", "privilege_escalation_cleanliness", () => {
|
|
4547
|
+
const executable = implFiles.filter((file) => fileContains(repoPath, file, SQL_EXEC_PATTERN));
|
|
4548
|
+
const historicalUngated = executable.filter((file) => {
|
|
4549
|
+
if (useV39Detectors && !fileHasExecutedPrivilegeEscalation(repoPath, file)) return false;
|
|
4550
|
+
const hasEscalation = fileContains(repoPath, file, ROLE_MEMBERSHIP_GRANT_PATTERN) || fileContains(repoPath, file, SUPERUSER_ESCALATION_PATTERN);
|
|
4551
|
+
if (!hasEscalation) return false;
|
|
4552
|
+
return !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN);
|
|
4553
|
+
});
|
|
4554
|
+
const v21Executed = useV21ExecutedEscalations ? repoFiles.filter(
|
|
4555
|
+
(file) => isV39AuthoredProductionPath(file) && (fileHasV21RawSqlEscalation(repoPath, file) || isImplementationFile(file) && fileHasV21DriverEscalation(repoPath, file))
|
|
4556
|
+
) : [];
|
|
4557
|
+
const v21ExecutedSet = new Set(v21Executed);
|
|
4558
|
+
const ungated = [.../* @__PURE__ */ new Set([...historicalUngated, ...v21Executed])].filter(
|
|
4559
|
+
(file) => !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN)
|
|
4560
|
+
);
|
|
4561
|
+
const productionUngated = ungated.filter(
|
|
4562
|
+
(file) => useV39Detectors ? isV39AuthoredProductionPath(file) : !isTestOrFixturePath(file)
|
|
4563
|
+
);
|
|
4564
|
+
return {
|
|
4565
|
+
executableFiles: executable,
|
|
4566
|
+
v21ExecutedEscalationFileSet: v21ExecutedSet,
|
|
4567
|
+
productionUngatedEscalationFiles: productionUngated
|
|
4568
|
+
};
|
|
3969
4569
|
});
|
|
3970
|
-
const v21ExecutedEscalationFiles = useV21ExecutedEscalations ? repoFiles.filter(
|
|
3971
|
-
(file) => isV39AuthoredProductionPath(file) && (fileHasV21RawSqlEscalation(repoPath, file) || isImplementationFile(file) && fileHasV21DriverEscalation(repoPath, file))
|
|
3972
|
-
) : [];
|
|
3973
|
-
const v21ExecutedEscalationFileSet = new Set(v21ExecutedEscalationFiles);
|
|
3974
|
-
const ungatedEscalationFiles = [
|
|
3975
|
-
.../* @__PURE__ */ new Set([...historicalUngatedEscalationFiles, ...v21ExecutedEscalationFiles])
|
|
3976
|
-
].filter((file) => !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN));
|
|
3977
|
-
const productionUngatedEscalationFiles = ungatedEscalationFiles.filter(
|
|
3978
|
-
(file) => useV39Detectors ? isV39AuthoredProductionPath(file) : !isTestOrFixturePath(file)
|
|
3979
|
-
);
|
|
3980
4570
|
if (humanGateDoc) {
|
|
3981
|
-
|
|
3982
|
-
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
4571
|
+
withContentReadSignal(
|
|
4572
|
+
"B6",
|
|
4573
|
+
"human_gate_documented",
|
|
4574
|
+
() => evidence.push(
|
|
4575
|
+
evidenceForRelative(
|
|
4576
|
+
repoPath,
|
|
4577
|
+
humanGateDoc,
|
|
4578
|
+
"artifact",
|
|
4579
|
+
"Documents privileged operations as human-executed/gated"
|
|
4580
|
+
)
|
|
3987
4581
|
)
|
|
3988
4582
|
);
|
|
3989
4583
|
}
|
|
3990
4584
|
if (gatedPrivilegeCheckFile) {
|
|
3991
|
-
|
|
3992
|
-
|
|
3993
|
-
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
|
|
4585
|
+
withContentReadSignal(
|
|
4586
|
+
"B6",
|
|
4587
|
+
"fail_closed_privilege_check",
|
|
4588
|
+
() => evidence.push(
|
|
4589
|
+
evidenceForRelative(
|
|
4590
|
+
repoPath,
|
|
4591
|
+
gatedPrivilegeCheckFile,
|
|
4592
|
+
"artifact",
|
|
4593
|
+
"Fail-closed privilege-membership check before role elevation"
|
|
4594
|
+
)
|
|
3997
4595
|
)
|
|
3998
4596
|
);
|
|
3999
4597
|
}
|
|
4000
4598
|
if (killSwitchFile) {
|
|
4001
|
-
|
|
4002
|
-
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4599
|
+
withContentReadSignal(
|
|
4600
|
+
"B6",
|
|
4601
|
+
"kill_switch_fail_safe_present",
|
|
4602
|
+
() => evidence.push(
|
|
4603
|
+
evidenceForRelative(
|
|
4604
|
+
repoPath,
|
|
4605
|
+
killSwitchFile,
|
|
4606
|
+
"artifact",
|
|
4607
|
+
"Un-overridable kill-switch / fail-safe governance toggle"
|
|
4608
|
+
)
|
|
4007
4609
|
)
|
|
4008
4610
|
);
|
|
4009
4611
|
}
|
|
4010
4612
|
if (protectedPathReviewGate) {
|
|
4011
|
-
|
|
4012
|
-
|
|
4013
|
-
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4613
|
+
withContentReadSignal(
|
|
4614
|
+
"B6",
|
|
4615
|
+
"protected_path_review_gate",
|
|
4616
|
+
() => evidence.push(
|
|
4617
|
+
evidenceForRelative(
|
|
4618
|
+
repoPath,
|
|
4619
|
+
protectedPathReviewGate,
|
|
4620
|
+
"artifact",
|
|
4621
|
+
"CODEOWNERS/required-review gate on protected paths"
|
|
4622
|
+
)
|
|
4017
4623
|
)
|
|
4018
4624
|
);
|
|
4019
4625
|
}
|
|
4020
|
-
|
|
4021
|
-
const
|
|
4022
|
-
|
|
4023
|
-
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
|
|
4626
|
+
withContentReadSignal("B6", "privilege_escalation_cleanliness", () => {
|
|
4627
|
+
for (const file of productionUngatedEscalationFiles.slice(0, 5)) {
|
|
4628
|
+
const isV21ExecutedShape = v21ExecutedEscalationFileSet.has(file);
|
|
4629
|
+
findings.push({
|
|
4630
|
+
severity: "critical",
|
|
4631
|
+
summary: isV21ExecutedShape ? "This file contains an authored administrative SQL statement \u2014 role-membership grant, SUPERUSER escalation, or schema-wide table privilege grant \u2014 or executes one through a direct database-driver call, and does not itself contain text matching the human-gate marker pattern." : "This file executes a role-membership GRANT or SUPERUSER escalation and does not itself contain text matching the human-gate marker pattern.",
|
|
4632
|
+
evidence: evidenceForRelative(
|
|
4633
|
+
repoPath,
|
|
4634
|
+
file,
|
|
4635
|
+
"artifact",
|
|
4636
|
+
isV21ExecutedShape ? "Ungated authored or directly executed administrative SQL statement" : "Ungated privilege-escalation statement"
|
|
4637
|
+
)
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
});
|
|
4033
4641
|
if (evidence.length === 0 && findings.length === 0) {
|
|
4034
4642
|
return buildNotApplicableSignal(
|
|
4035
4643
|
"B6",
|
|
@@ -4051,7 +4659,7 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
|
|
|
4051
4659
|
}
|
|
4052
4660
|
)
|
|
4053
4661
|
] : [];
|
|
4054
|
-
const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 ||
|
|
4662
|
+
const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 || v21ExecutedEscalationFileSet.size > 0;
|
|
4055
4663
|
return {
|
|
4056
4664
|
criterionId: "B6",
|
|
4057
4665
|
positiveEvidence: evidence,
|
|
@@ -4348,10 +4956,20 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
|
|
|
4348
4956
|
continue;
|
|
4349
4957
|
}
|
|
4350
4958
|
try {
|
|
4351
|
-
|
|
4959
|
+
const stat = lstatSync(fullPath);
|
|
4960
|
+
if (!stat.isFile()) {
|
|
4352
4961
|
recordContentSkip(fullPath, "non_regular_file", true);
|
|
4353
4962
|
continue;
|
|
4354
4963
|
}
|
|
4964
|
+
if (stat.size > MAX_REPOSITORY_CONTENT_BYTES) {
|
|
4965
|
+
const affectedCriteria = affectedCriteriaForUnavailablePath(file);
|
|
4966
|
+
if (affectedCriteria.length > 0) {
|
|
4967
|
+
recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria, true);
|
|
4968
|
+
} else {
|
|
4969
|
+
recordContentSkip(fullPath, "too_large", true);
|
|
4970
|
+
}
|
|
4971
|
+
continue;
|
|
4972
|
+
}
|
|
4355
4973
|
} catch (error) {
|
|
4356
4974
|
recordFilesystemSkip(fullPath, error, false, true);
|
|
4357
4975
|
continue;
|
|
@@ -4363,6 +4981,39 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
|
|
|
4363
4981
|
}
|
|
4364
4982
|
return files.sort();
|
|
4365
4983
|
}
|
|
4984
|
+
function affectedCriteriaForUnavailablePath(file) {
|
|
4985
|
+
const affected = /* @__PURE__ */ new Set();
|
|
4986
|
+
if (isDependencyManifest(file) || isLockfile(file)) affected.add("A4");
|
|
4987
|
+
if (isCiWorkflow(file)) {
|
|
4988
|
+
affected.add("A1");
|
|
4989
|
+
affected.add("A3");
|
|
4990
|
+
affected.add("B2");
|
|
4991
|
+
affected.add("B3");
|
|
4992
|
+
}
|
|
4993
|
+
if (isDeployConfig(file)) {
|
|
4994
|
+
affected.add("A3");
|
|
4995
|
+
affected.add("B2");
|
|
4996
|
+
}
|
|
4997
|
+
if (isTestFile(file)) {
|
|
4998
|
+
affected.add("A1");
|
|
4999
|
+
affected.add("A2");
|
|
5000
|
+
}
|
|
5001
|
+
if (isImplementationFile(file)) {
|
|
5002
|
+
affected.add("A1");
|
|
5003
|
+
affected.add("A2");
|
|
5004
|
+
affected.add("A3");
|
|
5005
|
+
affected.add("B1");
|
|
5006
|
+
affected.add("B6");
|
|
5007
|
+
}
|
|
5008
|
+
if (/\.(?:md|mdx|rst|txt)$/i.test(file)) {
|
|
5009
|
+
affected.add("A5");
|
|
5010
|
+
affected.add("B4");
|
|
5011
|
+
affected.add("B5");
|
|
5012
|
+
affected.add("B6");
|
|
5013
|
+
}
|
|
5014
|
+
if (isAuditFile(file)) affected.add("B5");
|
|
5015
|
+
return [...affected];
|
|
5016
|
+
}
|
|
4366
5017
|
function directoryInventory(repoPath) {
|
|
4367
5018
|
const files = [];
|
|
4368
5019
|
const rootDevice = statSync(repoPath).dev;
|
|
@@ -4375,6 +5026,7 @@ function collectRepoFileInventory(repoPath) {
|
|
|
4375
5026
|
return {
|
|
4376
5027
|
repoFiles: parseGitTrackedFiles(repoPath, tracked.stdout, false),
|
|
4377
5028
|
inventoryFiles: parseGitTrackedFiles(repoPath, tracked.stdout, true),
|
|
5029
|
+
repoFileInventoryScope: "tracked-scan-eligible",
|
|
4378
5030
|
scanLimitations: []
|
|
4379
5031
|
};
|
|
4380
5032
|
}
|
|
@@ -4482,8 +5134,13 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
|
|
|
4482
5134
|
continue;
|
|
4483
5135
|
}
|
|
4484
5136
|
const repoRelativePath = relative(repoPath, fullPath);
|
|
4485
|
-
if (size >
|
|
4486
|
-
|
|
5137
|
+
if (size > MAX_REPOSITORY_CONTENT_BYTES) {
|
|
5138
|
+
const affectedCriteria = affectedCriteriaForUnavailablePath(repoRelativePath);
|
|
5139
|
+
if (affectedCriteria.length > 0) {
|
|
5140
|
+
recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria);
|
|
5141
|
+
} else {
|
|
5142
|
+
recordContentSkip(fullPath, "too_large");
|
|
5143
|
+
}
|
|
4487
5144
|
continue;
|
|
4488
5145
|
}
|
|
4489
5146
|
if (!isPotentialContentPath(repoRelativePath)) {
|
|
@@ -4925,6 +5582,9 @@ function isRecord(value) {
|
|
|
4925
5582
|
function isTestRunnerConfig(repoPath, file) {
|
|
4926
5583
|
return /(^|\/)(vitest|jest|pytest|playwright|cypress|mocha)\.config\.[cm]?[jt]s$/.test(file) || /(^|\/)(pytest\.ini|tox\.ini)$/.test(file) || /(^|\/)phpunit\.xml(\.dist)?$/.test(file) || /(^|\/)pyproject\.toml$/.test(file) && fileContains(repoPath, file, /^\[tool\.pytest(?:\.|\])/m) || /(^|\/)CMakeLists\.txt$/.test(file) && fileContains(repoPath, file, /\b(?:enable_testing|add_test|ctest_add_tests)\s*\(/i) || /(^|\/)Makefile$/.test(file) && fileContains(repoPath, file, /^\s*(?:test|tests|check)\s*:/m);
|
|
4927
5584
|
}
|
|
5585
|
+
function isRecipeTestEntry(repoPath, file) {
|
|
5586
|
+
return isRecipeFile(file) && fileContains(repoPath, file, /^\s*(?:test|tests|check)\s*:/m);
|
|
5587
|
+
}
|
|
4928
5588
|
function findCoverageConfigFiles(repoPath, repoFiles, authoredOnly = false) {
|
|
4929
5589
|
return repoFiles.filter(
|
|
4930
5590
|
(file) => (!authoredOnly || isAuthoredProductionPath(file)) && isCoverageConfig(repoPath, file)
|
|
@@ -5218,6 +5878,580 @@ function packageJsonHasCoverageTooling(repoPath, file) {
|
|
|
5218
5878
|
}
|
|
5219
5879
|
return false;
|
|
5220
5880
|
}
|
|
5881
|
+
function isV23CoverageConfig(repoPath, file) {
|
|
5882
|
+
if (/(^|\/)vitest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5883
|
+
const contents = stripJavaScriptComments(readRepoText(join(repoPath, file), "utf8"));
|
|
5884
|
+
const property = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents)?.[1]?.trim();
|
|
5885
|
+
if (!property || /^(?:false\b|null\b|undefined\b)/i.test(property)) return false;
|
|
5886
|
+
if (/^\{/.test(property) && /\benabled\s*:\s*false\b/i.test(contents)) return false;
|
|
5887
|
+
return true;
|
|
5888
|
+
}
|
|
5889
|
+
if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5890
|
+
const contents = stripJavaScriptComments(readRepoText(join(repoPath, file), "utf8"));
|
|
5891
|
+
return /(?:^|[{,])\s*["']?collectCoverage["']?\s*:\s*true\b/m.test(contents) || /(?:^|[{,])\s*["']?(?:coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:/m.test(
|
|
5892
|
+
contents
|
|
5893
|
+
);
|
|
5894
|
+
}
|
|
5895
|
+
if (/(^|\/)package\.json$/.test(file)) {
|
|
5896
|
+
return packageJsonHasStaticCoverageTooling(repoPath, file);
|
|
5897
|
+
}
|
|
5898
|
+
return isCoverageConfig(repoPath, file);
|
|
5899
|
+
}
|
|
5900
|
+
function coverageConfigurationDecision(repoPath, file) {
|
|
5901
|
+
const detectorState = isV23CoverageConfig(repoPath, file) ? "collects" : "none";
|
|
5902
|
+
if (/(^|\/)vitest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5903
|
+
const rawContents = readRepoText(join(repoPath, file), "utf8");
|
|
5904
|
+
const contents = stripJavaScriptComments(rawContents);
|
|
5905
|
+
const match = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents);
|
|
5906
|
+
if (!match) return null;
|
|
5907
|
+
return {
|
|
5908
|
+
path: file,
|
|
5909
|
+
source: "runner-configuration",
|
|
5910
|
+
locator: "coverage",
|
|
5911
|
+
evidence: rawContents.slice(match.index, match.index + match[0].length).trim(),
|
|
5912
|
+
detectorRecognized: true,
|
|
5913
|
+
detectorState
|
|
5914
|
+
};
|
|
5915
|
+
}
|
|
5916
|
+
if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5917
|
+
const rawContents = readRepoText(join(repoPath, file), "utf8");
|
|
5918
|
+
const contents = stripJavaScriptComments(rawContents);
|
|
5919
|
+
const match = /(?:^|[{,])\s*["']?(collectCoverage|coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:\s*([^,\r\n}]*)/m.exec(
|
|
5920
|
+
contents
|
|
5921
|
+
);
|
|
5922
|
+
if (!match) return null;
|
|
5923
|
+
return {
|
|
5924
|
+
path: file,
|
|
5925
|
+
source: "runner-configuration",
|
|
5926
|
+
locator: match[1] ?? "coverage",
|
|
5927
|
+
evidence: rawContents.slice(match.index, match.index + match[0].length).trim(),
|
|
5928
|
+
detectorRecognized: true,
|
|
5929
|
+
detectorState
|
|
5930
|
+
};
|
|
5931
|
+
}
|
|
5932
|
+
if (detectorState === "none") return null;
|
|
5933
|
+
return {
|
|
5934
|
+
path: file,
|
|
5935
|
+
source: "runner-configuration",
|
|
5936
|
+
locator: "coverage",
|
|
5937
|
+
evidence: file,
|
|
5938
|
+
detectorRecognized: true,
|
|
5939
|
+
detectorState
|
|
5940
|
+
};
|
|
5941
|
+
}
|
|
5942
|
+
function packageJsonHasStaticCoverageTooling(repoPath, file) {
|
|
5943
|
+
const fullPath = join(repoPath, file);
|
|
5944
|
+
if (!isRegularFile(fullPath)) return false;
|
|
5945
|
+
const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
|
|
5946
|
+
if (!parsed) return false;
|
|
5947
|
+
return ["nyc", "c8", "istanbul"].some((key) => {
|
|
5948
|
+
const config = parsed[key];
|
|
5949
|
+
return typeof config === "object" && config !== null && !Array.isArray(config);
|
|
5950
|
+
});
|
|
5951
|
+
}
|
|
5952
|
+
function analyzeCoverageConfiguration(repoPath, repoFiles, authoredOnly = false) {
|
|
5953
|
+
const eligibleFiles = repoFiles.filter(
|
|
5954
|
+
(file) => !authoredOnly || isAuthoredProductionPath(file)
|
|
5955
|
+
);
|
|
5956
|
+
const eligibleSet = new Set(eligibleFiles);
|
|
5957
|
+
const coverageFiles = /* @__PURE__ */ new Set();
|
|
5958
|
+
const unresolvedFiles = /* @__PURE__ */ new Set();
|
|
5959
|
+
const ciCommandsByFile = /* @__PURE__ */ new Map();
|
|
5960
|
+
const detectorDecisions = [];
|
|
5961
|
+
let hasCoverageCommand = false;
|
|
5962
|
+
for (const file of eligibleFiles.filter(isCiWorkflow)) {
|
|
5963
|
+
const commands = extractCiRunCommands(readRepoText(join(repoPath, file), "utf8"));
|
|
5964
|
+
ciCommandsByFile.set(file, commands);
|
|
5965
|
+
const commandDecisions = commands.map(
|
|
5966
|
+
(command, index) => coverageDetectorDecision(repoPath, eligibleSet, {
|
|
5967
|
+
path: file,
|
|
5968
|
+
source: "ci-run",
|
|
5969
|
+
locator: String(index),
|
|
5970
|
+
command
|
|
5971
|
+
})
|
|
5972
|
+
);
|
|
5973
|
+
detectorDecisions.push(...commandDecisions);
|
|
5974
|
+
const state = combineCoverageCommandStates(
|
|
5975
|
+
commandDecisions.map((decision) => decision.detectorState)
|
|
5976
|
+
);
|
|
5977
|
+
if (state === "collects") {
|
|
5978
|
+
coverageFiles.add(file);
|
|
5979
|
+
hasCoverageCommand = true;
|
|
5980
|
+
}
|
|
5981
|
+
if (state === "unresolved") unresolvedFiles.add(file);
|
|
5982
|
+
}
|
|
5983
|
+
const ciCommands = [...ciCommandsByFile.values()].flat();
|
|
5984
|
+
for (const file of eligibleFiles) {
|
|
5985
|
+
if (isCiWorkflow(file) || isRecipeFile(file)) continue;
|
|
5986
|
+
if (/(^|\/)package\.json$/.test(file)) {
|
|
5987
|
+
if (packageJsonHasStaticCoverageTooling(repoPath, file)) coverageFiles.add(file);
|
|
5988
|
+
const packageState = packageScriptCoverageState(
|
|
5989
|
+
repoPath,
|
|
5990
|
+
eligibleSet,
|
|
5991
|
+
file,
|
|
5992
|
+
file === "package.json" ? ciCommands : [],
|
|
5993
|
+
detectorDecisions
|
|
5994
|
+
);
|
|
5995
|
+
if (packageState === "collects") {
|
|
5996
|
+
coverageFiles.add(file);
|
|
5997
|
+
hasCoverageCommand = true;
|
|
5998
|
+
}
|
|
5999
|
+
if (packageState === "unresolved") unresolvedFiles.add(file);
|
|
6000
|
+
continue;
|
|
6001
|
+
}
|
|
6002
|
+
const configurationDecision = coverageConfigurationDecision(repoPath, file);
|
|
6003
|
+
if (configurationDecision) detectorDecisions.push(configurationDecision);
|
|
6004
|
+
if (configurationDecision?.detectorState === "collects") coverageFiles.add(file);
|
|
6005
|
+
}
|
|
6006
|
+
for (const file of eligibleFiles.filter(isRecipeFile)) {
|
|
6007
|
+
const state = recipeCoverageState(
|
|
6008
|
+
repoPath,
|
|
6009
|
+
eligibleSet,
|
|
6010
|
+
file,
|
|
6011
|
+
ciCommands,
|
|
6012
|
+
detectorDecisions
|
|
6013
|
+
);
|
|
6014
|
+
if (state === "collects") {
|
|
6015
|
+
coverageFiles.add(file);
|
|
6016
|
+
hasCoverageCommand = true;
|
|
6017
|
+
}
|
|
6018
|
+
if (state === "unresolved") unresolvedFiles.add(file);
|
|
6019
|
+
}
|
|
6020
|
+
for (const file of coverageFiles) unresolvedFiles.delete(file);
|
|
6021
|
+
return {
|
|
6022
|
+
coverageFiles: [...coverageFiles].sort(),
|
|
6023
|
+
unresolvedFiles: [...unresolvedFiles].sort(),
|
|
6024
|
+
hasCoverageCommand,
|
|
6025
|
+
detectorDecisions
|
|
6026
|
+
};
|
|
6027
|
+
}
|
|
6028
|
+
function stripJavaScriptComments(contents) {
|
|
6029
|
+
let output = "";
|
|
6030
|
+
let quote = null;
|
|
6031
|
+
let escaped = false;
|
|
6032
|
+
let lineComment = false;
|
|
6033
|
+
let blockComment = false;
|
|
6034
|
+
for (let index = 0; index < contents.length; index += 1) {
|
|
6035
|
+
const character = contents[index] ?? "";
|
|
6036
|
+
const next = contents[index + 1] ?? "";
|
|
6037
|
+
if (lineComment) {
|
|
6038
|
+
if (character === "\n") {
|
|
6039
|
+
lineComment = false;
|
|
6040
|
+
output += "\n";
|
|
6041
|
+
} else {
|
|
6042
|
+
output += " ";
|
|
6043
|
+
}
|
|
6044
|
+
continue;
|
|
6045
|
+
}
|
|
6046
|
+
if (blockComment) {
|
|
6047
|
+
if (character === "*" && next === "/") {
|
|
6048
|
+
output += " ";
|
|
6049
|
+
index += 1;
|
|
6050
|
+
blockComment = false;
|
|
6051
|
+
} else {
|
|
6052
|
+
output += character === "\n" ? "\n" : " ";
|
|
6053
|
+
}
|
|
6054
|
+
continue;
|
|
6055
|
+
}
|
|
6056
|
+
if (escaped) {
|
|
6057
|
+
output += character;
|
|
6058
|
+
escaped = false;
|
|
6059
|
+
continue;
|
|
6060
|
+
}
|
|
6061
|
+
if (quote) {
|
|
6062
|
+
output += character;
|
|
6063
|
+
if (character === "\\") escaped = true;
|
|
6064
|
+
else if (character === quote) quote = null;
|
|
6065
|
+
continue;
|
|
6066
|
+
}
|
|
6067
|
+
if (character === "'" || character === '"' || character === "`") {
|
|
6068
|
+
quote = character;
|
|
6069
|
+
output += character;
|
|
6070
|
+
continue;
|
|
6071
|
+
}
|
|
6072
|
+
if (character === "/" && next === "/") {
|
|
6073
|
+
output += " ";
|
|
6074
|
+
index += 1;
|
|
6075
|
+
lineComment = true;
|
|
6076
|
+
continue;
|
|
6077
|
+
}
|
|
6078
|
+
if (character === "/" && next === "*") {
|
|
6079
|
+
output += " ";
|
|
6080
|
+
index += 1;
|
|
6081
|
+
blockComment = true;
|
|
6082
|
+
continue;
|
|
6083
|
+
}
|
|
6084
|
+
output += character;
|
|
6085
|
+
}
|
|
6086
|
+
return output;
|
|
6087
|
+
}
|
|
6088
|
+
function packageScriptCoverageState(repoPath, repoFiles, packageJson, ciCommands, detectorDecisions) {
|
|
6089
|
+
const scripts = readPackageScripts(join(repoPath, packageJson));
|
|
6090
|
+
if (scripts.size === 0) return "none";
|
|
6091
|
+
const roots = /* @__PURE__ */ new Set();
|
|
6092
|
+
let unresolved = false;
|
|
6093
|
+
if (scripts.has("test")) roots.add("test");
|
|
6094
|
+
for (const command of ciCommands) {
|
|
6095
|
+
const references = packageScriptReferences(command, scripts);
|
|
6096
|
+
for (const name of references.names) roots.add(name);
|
|
6097
|
+
if (references.dynamic && [...scripts.values()].some(
|
|
6098
|
+
(candidate) => commandInvokesCoverageTool(candidate) || containsPotentialCoverageFlag(candidate)
|
|
6099
|
+
)) {
|
|
6100
|
+
unresolved = true;
|
|
6101
|
+
}
|
|
6102
|
+
}
|
|
6103
|
+
const queue = [...roots];
|
|
6104
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6105
|
+
while (queue.length > 0) {
|
|
6106
|
+
const name = queue.shift();
|
|
6107
|
+
if (!name || visited.has(name)) continue;
|
|
6108
|
+
const command = scripts.get(name);
|
|
6109
|
+
if (command === void 0) continue;
|
|
6110
|
+
visited.add(name);
|
|
6111
|
+
const decision = coverageDetectorDecision(repoPath, repoFiles, {
|
|
6112
|
+
path: packageJson,
|
|
6113
|
+
source: "package-script",
|
|
6114
|
+
locator: name,
|
|
6115
|
+
command
|
|
6116
|
+
});
|
|
6117
|
+
detectorDecisions.push(decision);
|
|
6118
|
+
const state = decision.detectorState;
|
|
6119
|
+
if (state === "collects") return "collects";
|
|
6120
|
+
if (state === "unresolved") unresolved = true;
|
|
6121
|
+
const references = packageScriptReferences(command, scripts);
|
|
6122
|
+
for (const referenced of references.names) {
|
|
6123
|
+
if (!visited.has(referenced)) queue.push(referenced);
|
|
6124
|
+
}
|
|
6125
|
+
for (const lifecycle of [`pre${name}`, `post${name}`]) {
|
|
6126
|
+
if (scripts.has(lifecycle) && !visited.has(lifecycle)) queue.push(lifecycle);
|
|
6127
|
+
}
|
|
6128
|
+
if (references.dynamic && [...scripts.values()].some(
|
|
6129
|
+
(candidate) => commandInvokesCoverageTool(candidate) || containsPotentialCoverageFlag(candidate)
|
|
6130
|
+
)) {
|
|
6131
|
+
unresolved = true;
|
|
6132
|
+
}
|
|
6133
|
+
}
|
|
6134
|
+
return unresolved ? "unresolved" : "none";
|
|
6135
|
+
}
|
|
6136
|
+
function packageScriptReferences(command, scripts) {
|
|
6137
|
+
const names = /* @__PURE__ */ new Set();
|
|
6138
|
+
let dynamic = false;
|
|
6139
|
+
for (const segment of splitShellCommandSegments(command)) {
|
|
6140
|
+
for (const match of segment.matchAll(
|
|
6141
|
+
/\bpnpm\s+run\s+\/((?:\\.|[^/])+)\/([dgimsuvy]*)/giu
|
|
6142
|
+
)) {
|
|
6143
|
+
const source = match[1];
|
|
6144
|
+
if (!source) continue;
|
|
6145
|
+
try {
|
|
6146
|
+
const flags = (match[2] ?? "").replace(/[dgy]/g, "");
|
|
6147
|
+
const selector = new RegExp(source, flags);
|
|
6148
|
+
for (const name of scripts.keys()) {
|
|
6149
|
+
if (selector.test(name)) names.add(name);
|
|
6150
|
+
selector.lastIndex = 0;
|
|
6151
|
+
}
|
|
6152
|
+
} catch {
|
|
6153
|
+
dynamic = true;
|
|
6154
|
+
}
|
|
6155
|
+
}
|
|
6156
|
+
for (const match of segment.matchAll(
|
|
6157
|
+
/\b(?:npm|pnpm|yarn)\s+(?:run\s+)?([A-Za-z0-9][A-Za-z0-9:._-]*)/giu
|
|
6158
|
+
)) {
|
|
6159
|
+
const name = match[1];
|
|
6160
|
+
if (name && scripts.has(name)) names.add(name);
|
|
6161
|
+
}
|
|
6162
|
+
if (/\b(?:npm|pnpm|yarn)\s+run\s+(?:\$|["']\$|`)/u.test(segment)) dynamic = true;
|
|
6163
|
+
}
|
|
6164
|
+
return { names: [...names], dynamic };
|
|
6165
|
+
}
|
|
6166
|
+
function extractCiRunCommands(contents) {
|
|
6167
|
+
const lines = contents.split(/\r?\n/);
|
|
6168
|
+
const commands = [];
|
|
6169
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
6170
|
+
const line = lines[index] ?? "";
|
|
6171
|
+
const match = /^(\s*)(?:-\s*)?run\s*:\s*(.*)$/u.exec(line);
|
|
6172
|
+
if (!match) continue;
|
|
6173
|
+
const indentation = match[1]?.length ?? 0;
|
|
6174
|
+
const value = match[2]?.trim() ?? "";
|
|
6175
|
+
if (value === "|" || value === ">" || value.startsWith("|-") || value.startsWith(">-")) {
|
|
6176
|
+
const block = [];
|
|
6177
|
+
for (index += 1; index < lines.length; index += 1) {
|
|
6178
|
+
const nested = lines[index] ?? "";
|
|
6179
|
+
if (nested.trim().length === 0) {
|
|
6180
|
+
block.push("");
|
|
6181
|
+
continue;
|
|
6182
|
+
}
|
|
6183
|
+
const nestedIndentation = /^\s*/u.exec(nested)?.[0].length ?? 0;
|
|
6184
|
+
if (nestedIndentation <= indentation) {
|
|
6185
|
+
index -= 1;
|
|
6186
|
+
break;
|
|
6187
|
+
}
|
|
6188
|
+
block.push(nested.slice(Math.min(nested.length, indentation + 2)));
|
|
6189
|
+
}
|
|
6190
|
+
commands.push(block.join(value.startsWith(">") ? " " : "\n"));
|
|
6191
|
+
continue;
|
|
6192
|
+
}
|
|
6193
|
+
if (value.length === 0) continue;
|
|
6194
|
+
const quoted = /^(?:"([\s\S]*)"|'([\s\S]*)')$/u.exec(value);
|
|
6195
|
+
commands.push(quoted ? quoted[1] ?? quoted[2] ?? "" : value);
|
|
6196
|
+
}
|
|
6197
|
+
return commands;
|
|
6198
|
+
}
|
|
6199
|
+
function isRecipeFile(file) {
|
|
6200
|
+
return /(^|\/)(?:Makefile|makefile|GNUmakefile|[Jj]ustfile)$/.test(file);
|
|
6201
|
+
}
|
|
6202
|
+
function recipeCoverageState(repoPath, repoFiles, file, ciCommands, detectorDecisions) {
|
|
6203
|
+
const targets = readRecipeTargets(readRepoText(join(repoPath, file), "utf8"));
|
|
6204
|
+
const roots = new Set(["test", "tests", "check"].filter((target) => targets.has(target)));
|
|
6205
|
+
const runner = /[Jj]ustfile$/.test(file) ? "just" : "make";
|
|
6206
|
+
for (const command of ciCommands) {
|
|
6207
|
+
for (const match of command.matchAll(new RegExp(`\\b${runner}\\s+([A-Za-z0-9._-]+)`, "gu"))) {
|
|
6208
|
+
const target = match[1];
|
|
6209
|
+
if (target && targets.has(target)) roots.add(target);
|
|
6210
|
+
}
|
|
6211
|
+
}
|
|
6212
|
+
const queue = [...roots];
|
|
6213
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6214
|
+
let unresolved = false;
|
|
6215
|
+
while (queue.length > 0) {
|
|
6216
|
+
const name = queue.shift();
|
|
6217
|
+
if (!name || visited.has(name)) continue;
|
|
6218
|
+
const target = targets.get(name);
|
|
6219
|
+
if (!target) continue;
|
|
6220
|
+
visited.add(name);
|
|
6221
|
+
const commandDecisions = target.commands.map(
|
|
6222
|
+
(command, index) => coverageDetectorDecision(repoPath, repoFiles, {
|
|
6223
|
+
path: file,
|
|
6224
|
+
source: "recipe-command",
|
|
6225
|
+
locator: `${name}:${index}`,
|
|
6226
|
+
command
|
|
6227
|
+
})
|
|
6228
|
+
);
|
|
6229
|
+
detectorDecisions.push(...commandDecisions);
|
|
6230
|
+
const state = combineCoverageCommandStates(
|
|
6231
|
+
commandDecisions.map((decision) => decision.detectorState)
|
|
6232
|
+
);
|
|
6233
|
+
if (state === "collects") return "collects";
|
|
6234
|
+
if (state === "unresolved") unresolved = true;
|
|
6235
|
+
for (const dependency of target.dependencies) {
|
|
6236
|
+
if (targets.has(dependency) && !visited.has(dependency)) queue.push(dependency);
|
|
6237
|
+
}
|
|
6238
|
+
for (const command of target.commands) {
|
|
6239
|
+
const pattern = new RegExp(`(?:\\$\\(MAKE\\)|\\b${runner})\\s+([A-Za-z0-9._-]+)`, "gu");
|
|
6240
|
+
for (const match of command.matchAll(pattern)) {
|
|
6241
|
+
const referenced = match[1];
|
|
6242
|
+
if (referenced && targets.has(referenced) && !visited.has(referenced)) queue.push(referenced);
|
|
6243
|
+
}
|
|
6244
|
+
}
|
|
6245
|
+
}
|
|
6246
|
+
return unresolved ? "unresolved" : "none";
|
|
6247
|
+
}
|
|
6248
|
+
function readRecipeTargets(contents) {
|
|
6249
|
+
const targets = /* @__PURE__ */ new Map();
|
|
6250
|
+
let active = null;
|
|
6251
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
6252
|
+
const header = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*:\s*([^#]*)/u.exec(line);
|
|
6253
|
+
if (header) {
|
|
6254
|
+
const name = header[1];
|
|
6255
|
+
if (!name) continue;
|
|
6256
|
+
active = {
|
|
6257
|
+
dependencies: (header[2] ?? "").trim().split(/\s+/u).filter(Boolean),
|
|
6258
|
+
commands: []
|
|
6259
|
+
};
|
|
6260
|
+
targets.set(name, active);
|
|
6261
|
+
continue;
|
|
6262
|
+
}
|
|
6263
|
+
if (active && /^\s+/u.test(line) && line.trim().length > 0) {
|
|
6264
|
+
active.commands.push(line.trim().replace(/^[@+-]+/u, ""));
|
|
6265
|
+
continue;
|
|
6266
|
+
}
|
|
6267
|
+
if (line.trim().length > 0 && !line.trimStart().startsWith("#")) active = null;
|
|
6268
|
+
}
|
|
6269
|
+
return targets;
|
|
6270
|
+
}
|
|
6271
|
+
function coverageCommandEvaluation(repoPath, repoFiles, command, depth = 0) {
|
|
6272
|
+
if (depth > 4) {
|
|
6273
|
+
const recognized = containsPotentialCoverageFlag(command);
|
|
6274
|
+
return { recognized, state: recognized ? "unresolved" : "none" };
|
|
6275
|
+
}
|
|
6276
|
+
const normalized = normalizeCoverageCommandPrefixes(command);
|
|
6277
|
+
const segments = normalized.splitShellOperators ? splitShellCommandSegments(normalized.segment) : [normalized.segment];
|
|
6278
|
+
const evaluations = segments.map((rawSegment) => {
|
|
6279
|
+
const candidate = normalizeCoverageCommandPrefixes(rawSegment);
|
|
6280
|
+
if (commandInvokesCoverageTool(candidate.segment)) {
|
|
6281
|
+
return { recognized: true, state: "collects" };
|
|
6282
|
+
}
|
|
6283
|
+
const words = shellWords(candidate.segment);
|
|
6284
|
+
if (words.length === 0) return { recognized: false, state: "none" };
|
|
6285
|
+
const direct = coverageFlagCommandState(words);
|
|
6286
|
+
if (direct !== "none") return { recognized: true, state: direct };
|
|
6287
|
+
const hasCoverageSyntax = containsPotentialCoverageFlag(candidate.segment);
|
|
6288
|
+
if (isRecognizedCoverageFlagRunner(words)) {
|
|
6289
|
+
return { recognized: hasCoverageSyntax, state: "none" };
|
|
6290
|
+
}
|
|
6291
|
+
const wrapper = reachableShellWrapper(words, repoFiles);
|
|
6292
|
+
if (wrapper) {
|
|
6293
|
+
return coverageCommandEvaluation(
|
|
6294
|
+
repoPath,
|
|
6295
|
+
repoFiles,
|
|
6296
|
+
readRepoText(join(repoPath, wrapper), "utf8"),
|
|
6297
|
+
depth + 1
|
|
6298
|
+
);
|
|
6299
|
+
}
|
|
6300
|
+
const executable = commandExecutable(words);
|
|
6301
|
+
const potentialFlagState = flagState(
|
|
6302
|
+
words.slice(1),
|
|
6303
|
+
["--experimental-test-coverage", "--collectcoverage", "--collect-coverage", "--coverage", "--cov"],
|
|
6304
|
+
[
|
|
6305
|
+
"--no-experimental-test-coverage",
|
|
6306
|
+
"--no-collectcoverage",
|
|
6307
|
+
"--no-collect-coverage",
|
|
6308
|
+
"--no-coverage",
|
|
6309
|
+
"--no-cov"
|
|
6310
|
+
]
|
|
6311
|
+
);
|
|
6312
|
+
if (hasCoverageSyntax && potentialFlagState !== "none" && executable && /(?:test|spec|runner)/i.test(executable) && !/(?:docs?|map|report|upload|codecov|coveralls)/i.test(executable)) {
|
|
6313
|
+
return { recognized: true, state: "unresolved" };
|
|
6314
|
+
}
|
|
6315
|
+
return { recognized: false, state: "none" };
|
|
6316
|
+
});
|
|
6317
|
+
return {
|
|
6318
|
+
recognized: evaluations.some((evaluation) => evaluation.recognized),
|
|
6319
|
+
state: combineCoverageCommandStates(
|
|
6320
|
+
evaluations.map((evaluation) => evaluation.state)
|
|
6321
|
+
)
|
|
6322
|
+
};
|
|
6323
|
+
}
|
|
6324
|
+
function coverageDetectorDecision(repoPath, repoFiles, input) {
|
|
6325
|
+
const evaluation = coverageCommandEvaluation(repoPath, repoFiles, input.command);
|
|
6326
|
+
return {
|
|
6327
|
+
path: input.path,
|
|
6328
|
+
source: input.source,
|
|
6329
|
+
locator: input.locator,
|
|
6330
|
+
evidence: input.command,
|
|
6331
|
+
detectorRecognized: evaluation.recognized,
|
|
6332
|
+
detectorState: evaluation.state
|
|
6333
|
+
};
|
|
6334
|
+
}
|
|
6335
|
+
function combineCoverageCommandStates(states) {
|
|
6336
|
+
if (states.includes("collects")) return "collects";
|
|
6337
|
+
return states.includes("unresolved") ? "unresolved" : "none";
|
|
6338
|
+
}
|
|
6339
|
+
function shellWords(segment) {
|
|
6340
|
+
const words = [];
|
|
6341
|
+
let offset = 0;
|
|
6342
|
+
while (offset < segment.length) {
|
|
6343
|
+
const scanned = scanShellWord(segment, offset);
|
|
6344
|
+
if (!scanned) break;
|
|
6345
|
+
words.push(scanned.word);
|
|
6346
|
+
if (scanned.end <= offset) break;
|
|
6347
|
+
offset = scanned.end;
|
|
6348
|
+
}
|
|
6349
|
+
return words;
|
|
6350
|
+
}
|
|
6351
|
+
function commandExecutable(words) {
|
|
6352
|
+
const normalized = unwrapPackageManagerExecutable(words);
|
|
6353
|
+
const executable = normalized[0];
|
|
6354
|
+
return executable ? basename(executable).replace(/\.cmd$/i, "").toLowerCase() : void 0;
|
|
6355
|
+
}
|
|
6356
|
+
function unwrapPackageManagerExecutable(words) {
|
|
6357
|
+
if (words.length === 0) return [];
|
|
6358
|
+
const first = basename(words[0] ?? "").toLowerCase();
|
|
6359
|
+
if (first === "pnpm" || first === "yarn") {
|
|
6360
|
+
const second = words[1]?.toLowerCase();
|
|
6361
|
+
return words.slice(second === "exec" || second === "dlx" ? 2 : 1);
|
|
6362
|
+
}
|
|
6363
|
+
if (first === "npx") return words.slice(1).filter((word) => !/^--(?:yes|no-install)$/u.test(word));
|
|
6364
|
+
if (first === "npm" && words[1]?.toLowerCase() === "exec") {
|
|
6365
|
+
let offset = 2;
|
|
6366
|
+
while (/^--(?:yes|no-install)$/u.test(words[offset] ?? "")) offset += 1;
|
|
6367
|
+
if (words[offset] === "--") offset += 1;
|
|
6368
|
+
return words.slice(offset);
|
|
6369
|
+
}
|
|
6370
|
+
return [...words];
|
|
6371
|
+
}
|
|
6372
|
+
function coverageFlagCommandState(words) {
|
|
6373
|
+
const command = unwrapPackageManagerExecutable(words);
|
|
6374
|
+
const executable = basename(command[0] ?? "").replace(/\.cmd$/i, "").toLowerCase();
|
|
6375
|
+
const args = command.slice(1);
|
|
6376
|
+
if (!executable) return "none";
|
|
6377
|
+
if (executable === "node") {
|
|
6378
|
+
if (!args.includes("--test")) return "none";
|
|
6379
|
+
return flagState(args, ["--experimental-test-coverage"], ["--no-experimental-test-coverage"]);
|
|
6380
|
+
}
|
|
6381
|
+
if (executable === "pytest" || executable === "py.test") {
|
|
6382
|
+
return flagState(args, ["--cov"], ["--no-cov"]);
|
|
6383
|
+
}
|
|
6384
|
+
if (executable === "bun" || executable === "deno") {
|
|
6385
|
+
if (args[0]?.toLowerCase() !== "test") return "none";
|
|
6386
|
+
return flagState(args.slice(1), ["--coverage"], ["--no-coverage"]);
|
|
6387
|
+
}
|
|
6388
|
+
if (executable === "jest") {
|
|
6389
|
+
return flagState(
|
|
6390
|
+
args,
|
|
6391
|
+
["--coverage", "--collectcoverage", "--collect-coverage"],
|
|
6392
|
+
["--no-coverage", "--no-collectcoverage", "--no-collect-coverage"]
|
|
6393
|
+
);
|
|
6394
|
+
}
|
|
6395
|
+
if (executable === "vitest" || executable === "bnt") {
|
|
6396
|
+
return flagState(args, ["--coverage"], ["--no-coverage"]);
|
|
6397
|
+
}
|
|
6398
|
+
return "none";
|
|
6399
|
+
}
|
|
6400
|
+
function isRecognizedCoverageFlagRunner(words) {
|
|
6401
|
+
const command = unwrapPackageManagerExecutable(words);
|
|
6402
|
+
const executable = basename(command[0] ?? "").replace(/\.cmd$/i, "").toLowerCase();
|
|
6403
|
+
if (["bnt", "vitest", "jest", "pytest", "py.test"].includes(executable)) return true;
|
|
6404
|
+
if (executable === "node") return command.slice(1).includes("--test");
|
|
6405
|
+
return (executable === "bun" || executable === "deno") && command[1]?.toLowerCase() === "test";
|
|
6406
|
+
}
|
|
6407
|
+
function flagState(args, enablingNames, disablingNames) {
|
|
6408
|
+
let enabled = false;
|
|
6409
|
+
let disabled = false;
|
|
6410
|
+
let unresolved = false;
|
|
6411
|
+
const enabling = new Set(enablingNames.map((name) => name.toLowerCase()));
|
|
6412
|
+
const disabling = new Set(disablingNames.map((name) => name.toLowerCase()));
|
|
6413
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
6414
|
+
const raw = args[index] ?? "";
|
|
6415
|
+
const lower = raw.toLowerCase();
|
|
6416
|
+
if (disabling.has(lower)) {
|
|
6417
|
+
disabled = true;
|
|
6418
|
+
continue;
|
|
6419
|
+
}
|
|
6420
|
+
const [name, assigned] = lower.split("=", 2);
|
|
6421
|
+
if (!name || !enabling.has(name)) continue;
|
|
6422
|
+
if (assigned !== void 0) {
|
|
6423
|
+
if (assigned === "false" || assigned === "0" || assigned === "off") disabled = true;
|
|
6424
|
+
else if (/\$|\{|\}/u.test(assigned)) unresolved = true;
|
|
6425
|
+
else enabled = true;
|
|
6426
|
+
continue;
|
|
6427
|
+
}
|
|
6428
|
+
const next = args[index + 1]?.toLowerCase();
|
|
6429
|
+
if (next === "false" || next === "0" || next === "off") {
|
|
6430
|
+
disabled = true;
|
|
6431
|
+
index += 1;
|
|
6432
|
+
} else if (next && /\$|\{|\}/u.test(next)) {
|
|
6433
|
+
unresolved = true;
|
|
6434
|
+
index += 1;
|
|
6435
|
+
} else {
|
|
6436
|
+
enabled = true;
|
|
6437
|
+
}
|
|
6438
|
+
}
|
|
6439
|
+
if (unresolved || enabled && disabled) return "unresolved";
|
|
6440
|
+
return enabled && !disabled ? "collects" : "none";
|
|
6441
|
+
}
|
|
6442
|
+
function containsPotentialCoverageFlag(command) {
|
|
6443
|
+
return /(?:^|\s)--(?:experimental-test-coverage|collect-?coverage|no-coverage|coverage|no-cov|cov)(?:[=\s]|$)/iu.test(
|
|
6444
|
+
command
|
|
6445
|
+
);
|
|
6446
|
+
}
|
|
6447
|
+
function reachableShellWrapper(words, repoFiles) {
|
|
6448
|
+
const executable = basename(words[0] ?? "").toLowerCase();
|
|
6449
|
+
const candidate = /^(?:ba|da|k|z)?sh$/u.test(executable) ? words[1] : words[0];
|
|
6450
|
+
if (!candidate || !/(?:^|\/)\.?[^\s]*\.sh$/i.test(candidate)) return void 0;
|
|
6451
|
+
const normalized = candidate.replace(/^\.\//u, "");
|
|
6452
|
+
if (normalized.startsWith("/") || normalized.split("/").includes("..")) return void 0;
|
|
6453
|
+
return repoFiles.has(normalized) ? normalized : void 0;
|
|
6454
|
+
}
|
|
5221
6455
|
function commandInvokesCoverageTool(command) {
|
|
5222
6456
|
return normalizedCommandInvokesCoverageTool(normalizeCoverageCommandPrefixes(command), 0);
|
|
5223
6457
|
}
|
|
@@ -6475,12 +7709,11 @@ function hasEnvPathInGitHistory(repoPath, scanLimitations) {
|
|
|
6475
7709
|
|
|
6476
7710
|
// src/witan/public-scan.ts
|
|
6477
7711
|
init_esm_shims();
|
|
6478
|
-
import { resolve as resolve2 } from "path";
|
|
6479
7712
|
|
|
6480
7713
|
// src/witan/ingest.ts
|
|
6481
7714
|
init_esm_shims();
|
|
6482
|
-
import { readFileSync as readFileSync4,
|
|
6483
|
-
import { join as join2 } from "path";
|
|
7715
|
+
import { lstatSync as lstatSync3, opendirSync, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
7716
|
+
import { join as join2, resolve as resolve3 } from "path";
|
|
6484
7717
|
|
|
6485
7718
|
// src/witan/finding-limits.ts
|
|
6486
7719
|
init_esm_shims();
|
|
@@ -6504,49 +7737,118 @@ function clampFinding(finding) {
|
|
|
6504
7737
|
// src/witan/generic-adapter.ts
|
|
6505
7738
|
init_esm_shims();
|
|
6506
7739
|
import { readFileSync } from "fs";
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
"
|
|
6516
|
-
|
|
6517
|
-
|
|
6518
|
-
"
|
|
6519
|
-
|
|
6520
|
-
|
|
7740
|
+
|
|
7741
|
+
// src/witan/ingest-files.ts
|
|
7742
|
+
init_esm_shims();
|
|
7743
|
+
import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
|
|
7744
|
+
import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
7745
|
+
var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
|
|
7746
|
+
function isContainedPath(candidate, directory) {
|
|
7747
|
+
const relativePath = relative2(directory, candidate);
|
|
7748
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) && !isAbsolute(relativePath);
|
|
7749
|
+
}
|
|
7750
|
+
function isMissingIngestPath(error) {
|
|
7751
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
7752
|
+
}
|
|
7753
|
+
function resolveIngestFilePath(filePath, inputDirectory = dirname2(filePath)) {
|
|
7754
|
+
const lexicalDirectory = resolve2(inputDirectory);
|
|
7755
|
+
const lexicalCandidate = resolve2(filePath);
|
|
7756
|
+
const directoryStat = lstatSync2(lexicalDirectory);
|
|
7757
|
+
if (directoryStat.isSymbolicLink()) {
|
|
7758
|
+
throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
|
|
7759
|
+
}
|
|
7760
|
+
if (!directoryStat.isDirectory()) {
|
|
7761
|
+
throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
|
|
7762
|
+
}
|
|
7763
|
+
const candidateStat = lstatSync2(lexicalCandidate);
|
|
7764
|
+
if (candidateStat.isSymbolicLink()) {
|
|
7765
|
+
throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
|
|
7766
|
+
}
|
|
7767
|
+
if (!candidateStat.isFile()) {
|
|
7768
|
+
throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
|
|
7769
|
+
}
|
|
7770
|
+
if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
|
|
7771
|
+
throw new Error(
|
|
7772
|
+
`Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
|
|
7773
|
+
);
|
|
7774
|
+
}
|
|
7775
|
+
const resolvedDirectory = realpathSync2(lexicalDirectory);
|
|
7776
|
+
const resolvedCandidate = realpathSync2(lexicalCandidate);
|
|
7777
|
+
if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
|
|
7778
|
+
throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
|
|
7779
|
+
}
|
|
7780
|
+
return resolvedCandidate;
|
|
7781
|
+
}
|
|
7782
|
+
function isResolvedIngestPathContained(candidate, directory) {
|
|
7783
|
+
return isContainedPath(candidate, directory);
|
|
7784
|
+
}
|
|
7785
|
+
|
|
7786
|
+
// src/witan/generic-adapter.ts
|
|
7787
|
+
var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.0";
|
|
7788
|
+
var CEJEL_GENERIC_INGEST_CONTRACT_MAJOR = 1;
|
|
7789
|
+
var GenericFindingV1Schema = external_exports.object({
|
|
7790
|
+
ruleId: external_exports.string().min(1),
|
|
7791
|
+
severity: external_exports.enum(["critical", "warning", "info"]),
|
|
7792
|
+
message: external_exports.string().min(1),
|
|
7793
|
+
location: external_exports.string().min(1).optional()
|
|
7794
|
+
}).passthrough();
|
|
7795
|
+
var GenericSignalV1Schema = external_exports.object({
|
|
7796
|
+
dimension: external_exports.enum(["A1", "A2", "A3", "A4", "A5", "B1", "B2", "B3", "B4", "B5", "B6"]),
|
|
7797
|
+
weight: external_exports.number().min(0).max(1).default(0.5),
|
|
7798
|
+
findings: external_exports.array(GenericFindingV1Schema).min(1)
|
|
7799
|
+
}).passthrough();
|
|
7800
|
+
var GenericSignalDocumentV1Schema = external_exports.object({
|
|
7801
|
+
version: external_exports.string().regex(/^1\.\d+$/),
|
|
7802
|
+
tool: external_exports.string().trim().min(1),
|
|
7803
|
+
signals: external_exports.array(GenericSignalV1Schema)
|
|
7804
|
+
}).passthrough();
|
|
6521
7805
|
function isGenericSignalDocument(raw) {
|
|
6522
7806
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
6523
7807
|
const doc = raw;
|
|
6524
7808
|
return typeof doc.tool === "string" && doc.tool.length > 0 && Array.isArray(doc.signals);
|
|
6525
7809
|
}
|
|
7810
|
+
function assertSupportedContractVersion(version) {
|
|
7811
|
+
if (typeof version !== "string") {
|
|
7812
|
+
throw new Error(
|
|
7813
|
+
`Cejel: generic ingest contract version is required; expected "version": "${CEJEL_GENERIC_INGEST_CONTRACT_VERSION}". Refusing to guess an unversioned contract.`
|
|
7814
|
+
);
|
|
7815
|
+
}
|
|
7816
|
+
const match = /^(\d+)\.(\d+)$/.exec(version);
|
|
7817
|
+
if (!match) {
|
|
7818
|
+
throw new Error(
|
|
7819
|
+
`Cejel: invalid generic ingest contract version "${version}"; expected <major>.<minor> (current ${CEJEL_GENERIC_INGEST_CONTRACT_VERSION}).`
|
|
7820
|
+
);
|
|
7821
|
+
}
|
|
7822
|
+
const major = Number(match[1]);
|
|
7823
|
+
if (major !== CEJEL_GENERIC_INGEST_CONTRACT_MAJOR) {
|
|
7824
|
+
throw new Error(
|
|
7825
|
+
`Cejel: unsupported generic ingest contract major version ${major} (declared "${version}"); this Cejel build supports major ${CEJEL_GENERIC_INGEST_CONTRACT_MAJOR}. Refusing to guess.`
|
|
7826
|
+
);
|
|
7827
|
+
}
|
|
7828
|
+
}
|
|
7829
|
+
function formatValidationIssues(error) {
|
|
7830
|
+
return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<document>"}: ${issue.message}`).join("; ");
|
|
7831
|
+
}
|
|
6526
7832
|
function parseGenericJson(raw) {
|
|
6527
7833
|
if (!isGenericSignalDocument(raw)) return [];
|
|
6528
|
-
|
|
6529
|
-
const
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
const findings = [];
|
|
6535
|
-
for (const finding of signal.findings ?? []) {
|
|
6536
|
-
const severity = finding.severity;
|
|
6537
|
-
if (!severity || !VALID_SEVERITIES.has(severity)) continue;
|
|
6538
|
-
const ruleId = finding.ruleId ?? "unknown";
|
|
6539
|
-
findings.push({
|
|
6540
|
-
ruleId,
|
|
6541
|
-
severity,
|
|
6542
|
-
message: finding.message ?? ruleId,
|
|
6543
|
-
...finding.location ? { location: finding.location } : {}
|
|
6544
|
-
});
|
|
6545
|
-
}
|
|
6546
|
-
if (findings.length === 0) continue;
|
|
6547
|
-
signals.push({ source, dimension, weight, findings });
|
|
7834
|
+
assertSupportedContractVersion(raw.version);
|
|
7835
|
+
const parsed = GenericSignalDocumentV1Schema.safeParse(raw);
|
|
7836
|
+
if (!parsed.success) {
|
|
7837
|
+
throw new Error(
|
|
7838
|
+
`Cejel: invalid generic ingest contract v${raw.version}: ${formatValidationIssues(parsed.error)}`
|
|
7839
|
+
);
|
|
6548
7840
|
}
|
|
6549
|
-
return signals
|
|
7841
|
+
return parsed.data.signals.map((signal) => ({
|
|
7842
|
+
source: parsed.data.tool,
|
|
7843
|
+
dimension: signal.dimension,
|
|
7844
|
+
weight: signal.weight,
|
|
7845
|
+
findings: signal.findings.map((finding) => ({
|
|
7846
|
+
ruleId: finding.ruleId,
|
|
7847
|
+
severity: finding.severity,
|
|
7848
|
+
message: finding.message,
|
|
7849
|
+
...finding.location ? { location: finding.location } : {}
|
|
7850
|
+
}))
|
|
7851
|
+
}));
|
|
6550
7852
|
}
|
|
6551
7853
|
|
|
6552
7854
|
// src/witan/sarif-adapter.ts
|
|
@@ -6808,30 +8110,35 @@ function rawToolName(raw) {
|
|
|
6808
8110
|
}
|
|
6809
8111
|
var MAX_INGEST_SOURCE_LENGTH = 120;
|
|
6810
8112
|
function sanitizeIngestSource(source) {
|
|
6811
|
-
|
|
6812
|
-
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
return `${codePoints.slice(0, MAX_INGEST_SOURCE_LENGTH - 3).join("")}...`;
|
|
8113
|
+
return sanitizePresentationLine(source, {
|
|
8114
|
+
fallback: "unknown",
|
|
8115
|
+
maxLength: MAX_INGEST_SOURCE_LENGTH
|
|
8116
|
+
});
|
|
6816
8117
|
}
|
|
6817
8118
|
function parseIngestFile(filePath, options = {}) {
|
|
8119
|
+
const resolvedFilePath = resolveIngestFilePath(filePath);
|
|
6818
8120
|
let raw;
|
|
6819
8121
|
try {
|
|
6820
|
-
raw = JSON.parse(stripBom(readFileSync4(
|
|
8122
|
+
raw = JSON.parse(stripBom(readFileSync4(resolvedFilePath, "utf8")));
|
|
6821
8123
|
} catch (error) {
|
|
6822
8124
|
const message = error instanceof Error ? error.message : String(error);
|
|
6823
8125
|
throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
|
|
6824
8126
|
}
|
|
8127
|
+
const rawCount = countRawEntries(raw);
|
|
8128
|
+
if (options.maxFindingCandidates !== void 0 && rawCount > options.maxFindingCandidates) {
|
|
8129
|
+
throw new Error(
|
|
8130
|
+
`Cejel: ${rawCount.toLocaleString("en-US")} ingest finding candidates exceed the ${options.maxFindingCandidates.toLocaleString("en-US")} remaining retained ingest finding budget; refusing before adapter materialization.`
|
|
8131
|
+
);
|
|
8132
|
+
}
|
|
6825
8133
|
let signals;
|
|
6826
8134
|
if (looksLikeSarif(raw)) signals = parseSarifJson(raw, options.extraSarifDimensionRules);
|
|
6827
8135
|
else if (looksLikeScorecard(raw)) signals = parseScorecardJson(raw);
|
|
6828
8136
|
else if (isGenericSignalDocument(raw)) signals = parseGenericJson(raw);
|
|
6829
8137
|
if (!signals) {
|
|
6830
8138
|
throw new Error(
|
|
6831
|
-
`Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the generic Cejel external-signal shape (a "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
|
|
8139
|
+
`Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the versioned generic Cejel external-signal shape (a "version" string + "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
|
|
6832
8140
|
);
|
|
6833
8141
|
}
|
|
6834
|
-
const rawCount = countRawEntries(raw);
|
|
6835
8142
|
const mappedCount = signals.reduce((sum, signal) => sum + signal.findings.length, 0);
|
|
6836
8143
|
if (rawCount > 0 && mappedCount === 0) {
|
|
6837
8144
|
process.stderr.write(
|
|
@@ -6846,30 +8153,83 @@ function parseIngestFile(filePath, options = {}) {
|
|
|
6846
8153
|
findings: signal.findings.map(clampFinding)
|
|
6847
8154
|
}));
|
|
6848
8155
|
}
|
|
6849
|
-
function expandIngestPattern(pattern) {
|
|
8156
|
+
function expandIngestPattern(pattern, maxMatches = Number.POSITIVE_INFINITY) {
|
|
6850
8157
|
if (!pattern.includes("*")) return [pattern];
|
|
6851
8158
|
const lastSlash = pattern.lastIndexOf("/");
|
|
6852
8159
|
const dir = lastSlash === -1 ? "." : pattern.slice(0, lastSlash);
|
|
6853
8160
|
const filePattern = lastSlash === -1 ? pattern : pattern.slice(lastSlash + 1);
|
|
6854
8161
|
const regexSource = filePattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
6855
8162
|
const regex = new RegExp(`^${regexSource}$`);
|
|
6856
|
-
|
|
8163
|
+
const entries = [];
|
|
6857
8164
|
try {
|
|
6858
|
-
|
|
6859
|
-
|
|
8165
|
+
const directoryStat = lstatSync3(resolve3(dir));
|
|
8166
|
+
if (directoryStat.isSymbolicLink()) {
|
|
8167
|
+
throw new Error(`Cejel: refusing symlinked ingest directory: ${dir}`);
|
|
8168
|
+
}
|
|
8169
|
+
if (!directoryStat.isDirectory()) return [];
|
|
8170
|
+
const directory = opendirSync(dir);
|
|
8171
|
+
try {
|
|
8172
|
+
for (; ; ) {
|
|
8173
|
+
const entry = directory.readSync();
|
|
8174
|
+
if (!entry) break;
|
|
8175
|
+
if (!regex.test(entry.name)) continue;
|
|
8176
|
+
entries.push(entry.name);
|
|
8177
|
+
if (entries.length >= maxMatches) break;
|
|
8178
|
+
}
|
|
8179
|
+
} finally {
|
|
8180
|
+
directory.closeSync();
|
|
8181
|
+
}
|
|
8182
|
+
} catch (error) {
|
|
8183
|
+
if (!isMissingIngestPath(error)) throw error;
|
|
6860
8184
|
return [];
|
|
6861
8185
|
}
|
|
6862
|
-
return entries.
|
|
8186
|
+
return entries.sort().map((name) => {
|
|
8187
|
+
const candidate = join2(dir, name);
|
|
8188
|
+
resolveIngestFilePath(candidate, dir);
|
|
8189
|
+
return candidate;
|
|
8190
|
+
});
|
|
6863
8191
|
}
|
|
6864
|
-
function discoverIngestInputs(repoPath) {
|
|
8192
|
+
function discoverIngestInputs(repoPath, maxMatches = Number.POSITIVE_INFINITY) {
|
|
8193
|
+
const resolvedRoot = realpathSync3(repoPath);
|
|
8194
|
+
const cejelDir = join2(repoPath, ".cejel");
|
|
6865
8195
|
const inputsDir = join2(repoPath, ".cejel", "inputs");
|
|
6866
|
-
|
|
8196
|
+
const entries = [];
|
|
6867
8197
|
try {
|
|
6868
|
-
|
|
6869
|
-
|
|
8198
|
+
const cejelStat = lstatSync3(cejelDir);
|
|
8199
|
+
if (cejelStat.isSymbolicLink()) {
|
|
8200
|
+
throw new Error(`Cejel: refusing symlinked ingest path component: ${cejelDir}`);
|
|
8201
|
+
}
|
|
8202
|
+
if (!cejelStat.isDirectory()) {
|
|
8203
|
+
throw new Error(`Cejel: refusing non-directory ingest path component: ${cejelDir}`);
|
|
8204
|
+
}
|
|
8205
|
+
const inputsStat = lstatSync3(inputsDir);
|
|
8206
|
+
if (inputsStat.isSymbolicLink()) {
|
|
8207
|
+
throw new Error(`Cejel: refusing symlinked ingest path component: ${inputsDir}`);
|
|
8208
|
+
}
|
|
8209
|
+
if (!inputsStat.isDirectory()) {
|
|
8210
|
+
throw new Error(`Cejel: refusing non-directory ingest path component: ${inputsDir}`);
|
|
8211
|
+
}
|
|
8212
|
+
const resolvedInputs = realpathSync3(inputsDir);
|
|
8213
|
+
if (!isResolvedIngestPathContained(resolvedInputs, resolvedRoot)) {
|
|
8214
|
+
throw new Error(`Cejel: refusing auto-discovery directory outside the repository root.`);
|
|
8215
|
+
}
|
|
8216
|
+
const directory = opendirSync(inputsDir);
|
|
8217
|
+
try {
|
|
8218
|
+
for (; ; ) {
|
|
8219
|
+
const entry = directory.readSync();
|
|
8220
|
+
if (!entry) break;
|
|
8221
|
+
if (!/\.(sarif|json)$/i.test(entry.name)) continue;
|
|
8222
|
+
entries.push(entry.name);
|
|
8223
|
+
if (entries.length >= maxMatches) break;
|
|
8224
|
+
}
|
|
8225
|
+
} finally {
|
|
8226
|
+
directory.closeSync();
|
|
8227
|
+
}
|
|
8228
|
+
} catch (error) {
|
|
8229
|
+
if (!isMissingIngestPath(error)) throw error;
|
|
6870
8230
|
return [];
|
|
6871
8231
|
}
|
|
6872
|
-
return entries.
|
|
8232
|
+
return entries.sort().map((name) => resolveIngestFilePath(join2(inputsDir, name), inputsDir));
|
|
6873
8233
|
}
|
|
6874
8234
|
|
|
6875
8235
|
// src/witan/scoring.ts
|
|
@@ -6937,7 +8297,7 @@ var WITAN_RUBRIC = [
|
|
|
6937
8297
|
|
|
6938
8298
|
// src/witan/scoring.ts
|
|
6939
8299
|
function usesV17DetectorClosure2(rubricVersion) {
|
|
6940
|
-
return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
|
|
8300
|
+
return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
6941
8301
|
}
|
|
6942
8302
|
var MAX_SIGNAL_ADJUSTMENT = 0.8;
|
|
6943
8303
|
function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
|
|
@@ -7096,7 +8456,7 @@ function statusForScore(score) {
|
|
|
7096
8456
|
}
|
|
7097
8457
|
function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubricVersion) {
|
|
7098
8458
|
if (adjustment === 0) return nativeStatus;
|
|
7099
|
-
if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22) {
|
|
8459
|
+
if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22 && rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
|
|
7100
8460
|
return statusForScore(roundScore(Math.max(0, nativeScore - adjustment)));
|
|
7101
8461
|
}
|
|
7102
8462
|
const statusFloor = {
|
|
@@ -7141,7 +8501,7 @@ function mergeSignalsByCriterion(signals) {
|
|
|
7141
8501
|
return merged;
|
|
7142
8502
|
}
|
|
7143
8503
|
function usesMetricScoring(rubricVersion) {
|
|
7144
|
-
return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V9 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
|
|
8504
|
+
return rubricVersion === WITAN_RUBRIC_VERSION_V1 || rubricVersion === WITAN_RUBRIC_VERSION_V2 || rubricVersion === WITAN_RUBRIC_VERSION_V3 || rubricVersion === WITAN_RUBRIC_VERSION_V4 || rubricVersion === WITAN_RUBRIC_VERSION_V5 || rubricVersion === WITAN_RUBRIC_VERSION_V6 || rubricVersion === WITAN_RUBRIC_VERSION_V7 || rubricVersion === WITAN_RUBRIC_VERSION_V8 || rubricVersion === WITAN_RUBRIC_VERSION_V9 || rubricVersion === WITAN_RUBRIC_VERSION_V10 || rubricVersion === WITAN_RUBRIC_VERSION_V11 || rubricVersion === WITAN_RUBRIC_VERSION_V12 || rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V18 || rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
|
|
7145
8505
|
}
|
|
7146
8506
|
function unmeasuredStatus(rubricVersion) {
|
|
7147
8507
|
return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
|
|
@@ -7281,7 +8641,11 @@ function scoreMetrics(metrics) {
|
|
|
7281
8641
|
function capScoreForFindings(score, findings, rubricVersion, criterionId) {
|
|
7282
8642
|
if (findings.some((finding) => finding.severity === "critical")) {
|
|
7283
8643
|
const criticalFindings = findings.filter(({ severity }) => severity === "critical");
|
|
7284
|
-
const authenticatedA1Absence = usesV17DetectorClosure2(rubricVersion) && criterionId === "A1" && criticalFindings.length === 1 &&
|
|
8644
|
+
const authenticatedA1Absence = usesV17DetectorClosure2(rubricVersion) && criterionId === "A1" && criticalFindings.length === 1 && [
|
|
8645
|
+
WITAN_AUTHENTICATED_A1_ABSENCE_SUMMARY,
|
|
8646
|
+
WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY,
|
|
8647
|
+
WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY
|
|
8648
|
+
].includes(criticalFindings[0]?.summary ?? "");
|
|
7285
8649
|
return authenticatedA1Absence ? Math.min(score, 1.4) : 1.4;
|
|
7286
8650
|
}
|
|
7287
8651
|
if (findings.some((finding) => finding.severity === "warning")) return Math.min(score, 2.4);
|
|
@@ -7517,7 +8881,7 @@ function calibratedCriterionStateV11(criterionId, metrics) {
|
|
|
7517
8881
|
return void 0;
|
|
7518
8882
|
}
|
|
7519
8883
|
function ensureFindingsExplainStatus(criterionId, score, status, findings, evidence, rubricVersion) {
|
|
7520
|
-
if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22 && findings.length > 0) {
|
|
8884
|
+
if (rubricVersion !== WITAN_RUBRIC_VERSION_V8 && rubricVersion !== WITAN_RUBRIC_VERSION_V9 && rubricVersion !== WITAN_RUBRIC_VERSION_V10 && rubricVersion !== WITAN_RUBRIC_VERSION_V11 && rubricVersion !== WITAN_RUBRIC_VERSION_V12 && rubricVersion !== WITAN_RUBRIC_VERSION_V13 && rubricVersion !== WITAN_RUBRIC_VERSION_V14 && rubricVersion !== WITAN_RUBRIC_VERSION_V15 && rubricVersion !== WITAN_RUBRIC_VERSION_V16 && rubricVersion !== WITAN_RUBRIC_VERSION_V17 && rubricVersion !== WITAN_RUBRIC_VERSION_V18 && rubricVersion !== WITAN_RUBRIC_VERSION_V19 && rubricVersion !== WITAN_RUBRIC_VERSION_V20 && rubricVersion !== WITAN_RUBRIC_VERSION_V21 && rubricVersion !== WITAN_RUBRIC_VERSION_V22 && rubricVersion !== WITAN_RUBRIC_VERSION_V23 && findings.length > 0) {
|
|
7521
8885
|
return [...findings];
|
|
7522
8886
|
}
|
|
7523
8887
|
if (status !== "critical" && status !== "warning") return [...findings];
|
|
@@ -7553,6 +8917,8 @@ function roundScore(score) {
|
|
|
7553
8917
|
}
|
|
7554
8918
|
|
|
7555
8919
|
// src/witan/public-scan.ts
|
|
8920
|
+
var MAX_INGEST_DOCUMENTS = 128;
|
|
8921
|
+
var MAX_RETAINED_INGEST_FINDINGS = 1e4;
|
|
7556
8922
|
function scoreRepoWithPublicCejel(options) {
|
|
7557
8923
|
if (options.rubricVersion !== void 0) {
|
|
7558
8924
|
assertSelectableRubricVersion(options.rubricVersion);
|
|
@@ -7580,36 +8946,58 @@ function scoreRepoWithPublicCejel(options) {
|
|
|
7580
8946
|
}
|
|
7581
8947
|
function resolvePublicIngestSignals(options) {
|
|
7582
8948
|
const seen = /* @__PURE__ */ new Set();
|
|
7583
|
-
const
|
|
8949
|
+
const documents = [];
|
|
8950
|
+
const addDocument = (file, provenance) => {
|
|
8951
|
+
const resolved = resolveIngestFilePath(file);
|
|
8952
|
+
if (seen.has(resolved)) return;
|
|
8953
|
+
seen.add(resolved);
|
|
8954
|
+
documents.push({ file: resolved, provenance });
|
|
8955
|
+
if (documents.length > MAX_INGEST_DOCUMENTS) {
|
|
8956
|
+
throw new Error(
|
|
8957
|
+
`Cejel: ${documents.length} unique ingest documents exceed the ${MAX_INGEST_DOCUMENTS} ingest document budget; refusing to produce a partial certificate.`
|
|
8958
|
+
);
|
|
8959
|
+
}
|
|
8960
|
+
};
|
|
7584
8961
|
for (const pattern of options.ingestPatterns ?? []) {
|
|
7585
|
-
const matches = expandIngestPattern(pattern);
|
|
8962
|
+
const matches = expandIngestPattern(pattern, MAX_INGEST_DOCUMENTS + 1);
|
|
7586
8963
|
if (matches.length === 0 && options.warnOnEmptyIngestMatch) {
|
|
7587
8964
|
process.stderr.write(`Cejel: --ingest pattern matched no files: ${pattern}
|
|
7588
8965
|
`);
|
|
7589
8966
|
}
|
|
7590
8967
|
for (const match of matches) {
|
|
7591
|
-
|
|
7592
|
-
if (seen.has(resolved)) continue;
|
|
7593
|
-
seen.add(resolved);
|
|
7594
|
-
explicitFiles.push(match);
|
|
8968
|
+
addDocument(match, "operator_supplied");
|
|
7595
8969
|
}
|
|
7596
8970
|
}
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7600
|
-
|
|
7601
|
-
|
|
7602
|
-
|
|
7603
|
-
|
|
7604
|
-
|
|
7605
|
-
|
|
8971
|
+
if (options.autoDiscoverIngest) {
|
|
8972
|
+
for (const discovered of discoverIngestInputs(
|
|
8973
|
+
options.repoPath,
|
|
8974
|
+
MAX_INGEST_DOCUMENTS + 1
|
|
8975
|
+
)) {
|
|
8976
|
+
addDocument(discovered, "auto_discovered");
|
|
8977
|
+
}
|
|
8978
|
+
}
|
|
8979
|
+
const signals = [];
|
|
8980
|
+
let retainedFindings = 0;
|
|
8981
|
+
for (const document of documents) {
|
|
8982
|
+
const parsed = parseIngestFile(document.file, {
|
|
8983
|
+
maxFindingCandidates: MAX_RETAINED_INGEST_FINDINGS - retainedFindings,
|
|
8984
|
+
provenance: document.provenance
|
|
8985
|
+
});
|
|
8986
|
+
const documentFindings = parsed.reduce((count, signal) => count + signal.findings.length, 0);
|
|
8987
|
+
retainedFindings += documentFindings;
|
|
8988
|
+
if (retainedFindings > MAX_RETAINED_INGEST_FINDINGS) {
|
|
8989
|
+
throw new Error(
|
|
8990
|
+
`Cejel: ${retainedFindings.toLocaleString("en-US")} retained findings exceed the ${MAX_RETAINED_INGEST_FINDINGS.toLocaleString("en-US")} retained ingest finding budget; refusing to truncate evidence.`
|
|
8991
|
+
);
|
|
8992
|
+
}
|
|
8993
|
+
signals.push(...parsed);
|
|
7606
8994
|
}
|
|
7607
8995
|
return signals;
|
|
7608
8996
|
}
|
|
7609
8997
|
|
|
7610
8998
|
// src/product-identity.ts
|
|
7611
8999
|
init_esm_shims();
|
|
7612
|
-
import { existsSync, readFileSync as readFileSync5 } from "fs";
|
|
9000
|
+
import { existsSync, lstatSync as lstatSync4, readFileSync as readFileSync5 } from "fs";
|
|
7613
9001
|
import { basename as basename2, join as join3 } from "path";
|
|
7614
9002
|
var SLUG_PATTERN = /^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/;
|
|
7615
9003
|
var FALLBACK_SLUG = "repo";
|
|
@@ -7617,7 +9005,10 @@ var FALLBACK_DISPLAY_NAME = "Repository";
|
|
|
7617
9005
|
function deriveProductIdentity(repoPath, productName) {
|
|
7618
9006
|
const packageJsonPath = join3(repoPath, "package.json");
|
|
7619
9007
|
const raw = productName ?? readPackageName(packageJsonPath) ?? basename2(repoPath);
|
|
7620
|
-
const displayName =
|
|
9008
|
+
const displayName = sanitizePresentationLine(raw, {
|
|
9009
|
+
fallback: FALLBACK_DISPLAY_NAME,
|
|
9010
|
+
maxLength: 120
|
|
9011
|
+
});
|
|
7621
9012
|
return {
|
|
7622
9013
|
productSlug: slugify(raw),
|
|
7623
9014
|
productDisplayName: displayName
|
|
@@ -7626,6 +9017,8 @@ function deriveProductIdentity(repoPath, productName) {
|
|
|
7626
9017
|
function readPackageName(packageJsonPath) {
|
|
7627
9018
|
if (!existsSync(packageJsonPath)) return void 0;
|
|
7628
9019
|
try {
|
|
9020
|
+
const stat = lstatSync4(packageJsonPath);
|
|
9021
|
+
if (!stat.isFile() || stat.size > MAX_REPOSITORY_CONTENT_BYTES) return void 0;
|
|
7629
9022
|
const parsed = JSON.parse(readFileSync5(packageJsonPath, "utf8"));
|
|
7630
9023
|
if (typeof parsed === "object" && parsed !== null && "name" in parsed) {
|
|
7631
9024
|
const name = parsed.name;
|
|
@@ -7808,6 +9201,7 @@ export {
|
|
|
7808
9201
|
CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
|
|
7809
9202
|
CERTIFICATE_GLOSSARY,
|
|
7810
9203
|
formatCertificateMetricLabel,
|
|
9204
|
+
formatCertificateMetricAppliedWeightPercent,
|
|
7811
9205
|
formatCertificateMetricValue,
|
|
7812
9206
|
buildRelyingPartySummary,
|
|
7813
9207
|
WITAN_RUBRIC_VERSION_V22,
|