@cejel/cejel 0.4.6 → 0.4.8
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 +35 -11
- package/dist/calibration/llm-detector.js +5 -5
- package/dist/{chunk-VEJLHO5F.js → chunk-6R3VJGJ2.js} +1479 -345
- package/dist/{chunk-QRWLHTGR.js → chunk-ERNYMFH4.js} +2 -2
- package/dist/{chunk-Y6NO5HAI.js → chunk-J6R5OHN7.js} +1 -1
- package/dist/{chunk-BZXBLDCJ.js → chunk-KNQBI3IX.js} +1 -1
- package/dist/{chunk-H55KECVT.js → chunk-V3B2V4WA.js} +73 -9
- package/dist/{chunk-2IBG5OBF.js → chunk-ZPQOZLB7.js} +1 -1
- package/dist/index.js +41 -10
- package/dist/mcp/index.js +5 -5
- package/dist/packs/d-series/index.js +4 -4
- package/dist/packs/decision-contracts/index.js +3 -3
- package/package.json +1 -1
|
@@ -5,21 +5,24 @@ import {
|
|
|
5
5
|
describeGitFailure,
|
|
6
6
|
execGit,
|
|
7
7
|
isExpectedGitAbsence
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-KNQBI3IX.js";
|
|
9
9
|
import {
|
|
10
10
|
external_exports
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-J6R5OHN7.js";
|
|
12
12
|
import {
|
|
13
13
|
MAX_REPOSITORY_CONTENT_BYTES,
|
|
14
|
+
classifyAbstentionReason,
|
|
15
|
+
contentReadSignalKey,
|
|
14
16
|
init_esm_shims,
|
|
15
17
|
readRepoText,
|
|
16
18
|
readRepoTextPrefix,
|
|
17
19
|
recordContentSkip,
|
|
18
|
-
recordContentSkipForCriteria,
|
|
19
20
|
recordFilesystemSkip,
|
|
20
21
|
trackContentReads,
|
|
21
|
-
withContentReadCriterion
|
|
22
|
-
|
|
22
|
+
withContentReadCriterion,
|
|
23
|
+
withContentReadSignal,
|
|
24
|
+
withContentReadSignals
|
|
25
|
+
} from "./chunk-V3B2V4WA.js";
|
|
23
26
|
|
|
24
27
|
// src/witan/schemas.ts
|
|
25
28
|
init_esm_shims();
|
|
@@ -159,6 +162,13 @@ var WitanCriterionMetricSchema = external_exports.object({
|
|
|
159
162
|
condition: external_exports.string().min(1).max(240).optional()
|
|
160
163
|
}).strict().optional()
|
|
161
164
|
}).strict();
|
|
165
|
+
var WitanContentReadSkipReasonSchema = external_exports.enum([
|
|
166
|
+
"unreadable",
|
|
167
|
+
"too_large",
|
|
168
|
+
"excluded_by_extension",
|
|
169
|
+
"denied_path",
|
|
170
|
+
"non_regular_file"
|
|
171
|
+
]);
|
|
162
172
|
var WitanCriterionSignalSchema = external_exports.object({
|
|
163
173
|
criterionId: WitanCriterionIdSchema,
|
|
164
174
|
positiveEvidence: external_exports.array(WitanEvidencePointerSchema).default([]),
|
|
@@ -168,9 +178,19 @@ var WitanCriterionSignalSchema = external_exports.object({
|
|
|
168
178
|
notApplicable: external_exports.literal(true).optional(),
|
|
169
179
|
// A collector reached relevant files but could not read their contents. This is a
|
|
170
180
|
// measurement gap, never negative evidence: scoreCriterion maps it to insufficient_data.
|
|
171
|
-
insufficientData: external_exports.literal(true).optional()
|
|
181
|
+
insufficientData: external_exports.literal(true).optional(),
|
|
182
|
+
// Why insufficientData is true. Omitted (or 'unreadable'/'denied_path') means an
|
|
183
|
+
// environmental read failure — evidence Cejel expected to see and could not; that stays in
|
|
184
|
+
// the composite denominator at score 0 so losing evidence never flatters the certificate
|
|
185
|
+
// (ADR-0001 does not cover this case — it is genuine evidence loss). 'too_large',
|
|
186
|
+
// 'excluded_by_extension', and 'non_regular_file' are self-imposed coverage limits Cejel
|
|
187
|
+
// chose itself: those are excluded from the composite exactly like ordinary
|
|
188
|
+
// insufficient_data (ADR-0001: coverage is disclosed, never discounts a score).
|
|
189
|
+
insufficientDataReason: WitanContentReadSkipReasonSchema.optional()
|
|
172
190
|
}).strict().refine((signal) => !(signal.notApplicable && signal.insufficientData), {
|
|
173
191
|
message: "A criterion cannot be both not applicable and insufficient data."
|
|
192
|
+
}).refine((signal) => signal.insufficientDataReason === void 0 || signal.insufficientData === true, {
|
|
193
|
+
message: "insufficientDataReason may only be set alongside insufficientData: true."
|
|
174
194
|
});
|
|
175
195
|
var WitanContentReadSummarySchema = external_exports.object({
|
|
176
196
|
skipped: external_exports.number().int().nonnegative(),
|
|
@@ -298,7 +318,14 @@ var WitanReportCommonSchema = external_exports.object({
|
|
|
298
318
|
consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
|
|
299
319
|
archetype: WitanRepoArchetypeSchema.optional(),
|
|
300
320
|
scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).optional(),
|
|
301
|
-
contentReadSummary: WitanContentReadSummarySchema.optional()
|
|
321
|
+
contentReadSummary: WitanContentReadSummarySchema.optional(),
|
|
322
|
+
// The @cejel/cejel version that produced this report. Absent on reports from before this
|
|
323
|
+
// field existed and on any caller that does not supply one (createWitanReport never defaults
|
|
324
|
+
// it). Constant within an installed version, so re-running the same version on the same
|
|
325
|
+
// commit stays byte-stable (the exact property v0.3.1 removed generatedAt to protect); it
|
|
326
|
+
// differs across versions, which lets a consumer key incremental reuse on it
|
|
327
|
+
// (goal_cejel_0_4_8_abstention_scoring_fix_2026-09-08).
|
|
328
|
+
toolVersion: external_exports.string().min(1).max(80).optional()
|
|
302
329
|
});
|
|
303
330
|
var WitanScoredReportSchema = WitanReportCommonSchema.extend({
|
|
304
331
|
verdict: WitanScoredVerdictSchema,
|
|
@@ -339,7 +366,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
|
|
|
339
366
|
}, StrictWitanReportSchema);
|
|
340
367
|
var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
341
368
|
var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
|
|
342
|
-
var WITAN_REPORT_FORMAT_VERSION = "1.
|
|
369
|
+
var WITAN_REPORT_FORMAT_VERSION = "1.1";
|
|
343
370
|
var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
|
|
344
371
|
external_exports.object({
|
|
345
372
|
status: external_exports.literal("scored"),
|
|
@@ -374,6 +401,11 @@ var WitanAttestationStatementSchema = external_exports.object({
|
|
|
374
401
|
// explicit report contract version.
|
|
375
402
|
reportFormatVersion: external_exports.string().regex(/^1\.\d+$/).optional(),
|
|
376
403
|
rubricVersion: external_exports.string().min(1).max(120),
|
|
404
|
+
// Only present when the scan ran under the GitHub Action (GITHUB_RUN_ATTEMPT observed in
|
|
405
|
+
// the runner environment). Absent, never fabricated or defaulted to "1", for a local CLI
|
|
406
|
+
// or any other CI. Not part of report.json: it is per-invocation, not per-repository-
|
|
407
|
+
// state, and report.json's reproducibility guarantee excludes exactly that kind of field.
|
|
408
|
+
githubRunAttempt: external_exports.string().regex(/^[1-9][0-9]*$/).optional(),
|
|
377
409
|
repository: external_exports.object({
|
|
378
410
|
productSlug: external_exports.string().regex(/^[a-z0-9][a-z0-9-]{1,62}[a-z0-9]$/),
|
|
379
411
|
url: external_exports.string().url().optional(),
|
|
@@ -415,13 +447,15 @@ var WITAN_RUBRIC_VERSION_V19 = "witan-rubric-v19-prospective-2026-08-09";
|
|
|
415
447
|
var WITAN_RUBRIC_VERSION_V20 = "witan-rubric-v20-prospective-2026-08-10";
|
|
416
448
|
var WITAN_RUBRIC_VERSION_V21 = "witan-rubric-v21-prospective-2026-08-10";
|
|
417
449
|
var WITAN_RUBRIC_VERSION_V22 = "witan-rubric-v22-prospective-2026-08-10";
|
|
450
|
+
var WITAN_RUBRIC_VERSION_V23 = "witan-rubric-v23-prospective-2026-09-06";
|
|
418
451
|
var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
|
|
419
452
|
var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
|
|
420
453
|
WITAN_RUBRIC_VERSION_V18,
|
|
421
454
|
WITAN_RUBRIC_VERSION_V19,
|
|
422
455
|
WITAN_RUBRIC_VERSION_V20,
|
|
423
456
|
WITAN_RUBRIC_VERSION_V21,
|
|
424
|
-
WITAN_RUBRIC_VERSION_V22
|
|
457
|
+
WITAN_RUBRIC_VERSION_V22,
|
|
458
|
+
WITAN_RUBRIC_VERSION_V23
|
|
425
459
|
]);
|
|
426
460
|
var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
|
|
427
461
|
WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
|
|
@@ -810,6 +844,32 @@ function glossaryEntryForMetric(metric2) {
|
|
|
810
844
|
function formatCertificateMetricLabel(metric2) {
|
|
811
845
|
return CERTIFICATE_METRIC_REGISTRY[metric2.name]?.displayLabel ?? metric2.label;
|
|
812
846
|
}
|
|
847
|
+
function formatCertificateMetricAppliedWeightPercent(criterion, metric2) {
|
|
848
|
+
const metrics = criterion.metrics;
|
|
849
|
+
const index = metrics.indexOf(metric2);
|
|
850
|
+
if (index === -1) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
"formatCertificateMetricAppliedWeightPercent: metric does not belong to criterion.metrics"
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const totalWeight = metrics.reduce((sum, m) => sum + m.weight, 0);
|
|
856
|
+
if (totalWeight <= 0) return 0;
|
|
857
|
+
const shares = metrics.map((m, i) => {
|
|
858
|
+
const raw = m.weight / totalWeight * 100;
|
|
859
|
+
const floor = Math.floor(raw);
|
|
860
|
+
return { i, floor, fraction: raw - floor };
|
|
861
|
+
});
|
|
862
|
+
const flooredTotal = shares.reduce((sum, s) => sum + s.floor, 0);
|
|
863
|
+
const byLargestRemainder = [...shares].sort((a, b) => b.fraction - a.fraction);
|
|
864
|
+
const result = new Map(shares.map((s) => [s.i, s.floor]));
|
|
865
|
+
let remainder = Math.round(100 - flooredTotal);
|
|
866
|
+
for (const s of byLargestRemainder) {
|
|
867
|
+
if (remainder <= 0) break;
|
|
868
|
+
result.set(s.i, (result.get(s.i) ?? s.floor) + 1);
|
|
869
|
+
remainder -= 1;
|
|
870
|
+
}
|
|
871
|
+
return result.get(index) ?? 0;
|
|
872
|
+
}
|
|
813
873
|
function glossaryEntriesForReport(report) {
|
|
814
874
|
const entries = new Map(CERTIFICATE_GLOSSARY.map((entry) => [entry.key, entry]));
|
|
815
875
|
for (const metric2 of report.criteria.flatMap((criterion) => criterion.metrics)) {
|
|
@@ -925,6 +985,74 @@ function formatCertificateMetricValue(criterion, metric2) {
|
|
|
925
985
|
`${formatMetricNumber(metric2.value)}/${formatMetricNumber(metric2.max)}${unit}`
|
|
926
986
|
);
|
|
927
987
|
}
|
|
988
|
+
var NEXT_STEP_FINDING_SEVERITIES = /* @__PURE__ */ new Set(["critical", "warning"]);
|
|
989
|
+
var NEXT_STEP_FINDING_SEVERITY_RANK = {
|
|
990
|
+
critical: 0,
|
|
991
|
+
warning: 1,
|
|
992
|
+
info: 2
|
|
993
|
+
};
|
|
994
|
+
var MAX_NEXT_STEP_FINDINGS = 3;
|
|
995
|
+
function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
|
|
996
|
+
const steps = [];
|
|
997
|
+
if (unmeasured.length > 0) {
|
|
998
|
+
const ids = unmeasured.map((criterion) => criterion.id).join(", ");
|
|
999
|
+
steps.push(
|
|
1000
|
+
`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.`
|
|
1001
|
+
);
|
|
1002
|
+
}
|
|
1003
|
+
const criterionFindings = report.criteria.flatMap(
|
|
1004
|
+
(criterion) => criterion.findings.filter((finding) => NEXT_STEP_FINDING_SEVERITIES.has(finding.severity)).map((finding) => ({ criterionId: criterion.id, finding }))
|
|
1005
|
+
);
|
|
1006
|
+
const sortedFindings = [...criterionFindings].sort(
|
|
1007
|
+
(a, b) => NEXT_STEP_FINDING_SEVERITY_RANK[a.finding.severity] - NEXT_STEP_FINDING_SEVERITY_RANK[b.finding.severity]
|
|
1008
|
+
);
|
|
1009
|
+
for (const { criterionId, finding } of sortedFindings.slice(0, MAX_NEXT_STEP_FINDINGS)) {
|
|
1010
|
+
steps.push(
|
|
1011
|
+
`Evidence is currently absent for the ${finding.severity} finding on ${criterionId}: obtain independent verification before relying on this criterion.`
|
|
1012
|
+
);
|
|
1013
|
+
}
|
|
1014
|
+
if (sortedFindings.length > MAX_NEXT_STEP_FINDINGS) {
|
|
1015
|
+
steps.push(
|
|
1016
|
+
`${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.`
|
|
1017
|
+
);
|
|
1018
|
+
}
|
|
1019
|
+
if (a1 && coverageMetric && isCoverageNotMeasured(a1, coverageMetric)) {
|
|
1020
|
+
steps.push(
|
|
1021
|
+
"Obtain a coverage report or configured threshold for A1's static coverage metric; none was found, so it was not measured."
|
|
1022
|
+
);
|
|
1023
|
+
}
|
|
1024
|
+
if (!report.repo.headSha && report.criteria.some(
|
|
1025
|
+
(criterion) => criterion.metrics.some((metric2) => metric2.name === "pr_merge_ratio")
|
|
1026
|
+
)) {
|
|
1027
|
+
steps.push(
|
|
1028
|
+
"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."
|
|
1029
|
+
);
|
|
1030
|
+
}
|
|
1031
|
+
if (notApplicable.length > 0) {
|
|
1032
|
+
const ids = notApplicable.map((criterion) => criterion.id).join(", ");
|
|
1033
|
+
steps.push(
|
|
1034
|
+
`Confirm ${ids} ${notApplicable.length === 1 ? "is" : "are"} correctly marked not applicable to this repository; ${notApplicable.length === 1 ? "it was" : "they were"} not examined.`
|
|
1035
|
+
);
|
|
1036
|
+
}
|
|
1037
|
+
const scanLimitationCount = report.scanLimitations?.length ?? 0;
|
|
1038
|
+
if (scanLimitationCount > 0) {
|
|
1039
|
+
steps.push(
|
|
1040
|
+
`Resolve the ${scanLimitationCount} scan limitation${scanLimitationCount === 1 ? "" : "s"} qualifying the readable evidence, then reproduce the scan.`
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
const skippedCount = report.contentReadSummary?.skipped ?? 0;
|
|
1044
|
+
if (skippedCount > 0) {
|
|
1045
|
+
steps.push(
|
|
1046
|
+
`Make the ${skippedCount} skipped content entr${skippedCount === 1 ? "y" : "ies"} readable (see the itemized reasons in the certificate), then reproduce the scan.`
|
|
1047
|
+
);
|
|
1048
|
+
}
|
|
1049
|
+
if (steps.length === 0) {
|
|
1050
|
+
steps.push(
|
|
1051
|
+
"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."
|
|
1052
|
+
);
|
|
1053
|
+
}
|
|
1054
|
+
return steps;
|
|
1055
|
+
}
|
|
928
1056
|
function buildRelyingPartySummary(report) {
|
|
929
1057
|
const applicable = report.criteria.filter((criterion) => criterion.status !== "not_applicable");
|
|
930
1058
|
const measured = applicable.filter((criterion) => criterion.status !== "insufficient_data");
|
|
@@ -972,7 +1100,7 @@ function buildRelyingPartySummary(report) {
|
|
|
972
1100
|
examined: `Cejel examined the repository evidence recorded for ${report.productDisplayName}${revision} under ${report.rubricVersion}.`,
|
|
973
1101
|
established: `The report established measured results for ${measured.length} of ${applicable.length} applicable rubric dimensions and recorded ${findings} evidence-backed finding${findings === 1 ? "" : "s"}.`,
|
|
974
1102
|
notEstablished: gaps.join(" "),
|
|
975
|
-
next:
|
|
1103
|
+
next: buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric).join(" ")
|
|
976
1104
|
};
|
|
977
1105
|
}
|
|
978
1106
|
function formatMetricNumber(value) {
|
|
@@ -1091,6 +1219,7 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
1091
1219
|
${options.generatedAt ? `<div><dt>Date</dt><dd>${escapeHtml(formatDate(options.generatedAt))}</dd></div>` : ""}
|
|
1092
1220
|
<div><dt>Run</dt><dd>${escapeHtml(renderRepo(report))}</dd></div>
|
|
1093
1221
|
<div><dt>CLI</dt><dd>${escapeHtml(options.cliVersion ? `Cejel ${options.cliVersion}` : "Not recorded")}</dd></div>
|
|
1222
|
+
${options.runAttempt ? `<div><dt>Run attempt</dt><dd>${escapeHtml(options.runAttempt)}</dd></div>` : ""}
|
|
1094
1223
|
<div><dt>Rubric</dt><dd>${escapeHtml(report.rubricVersion)}</dd></div>
|
|
1095
1224
|
${// Gated on the rubric actually being prospective — a calibrated (v17) certificate's
|
|
1096
1225
|
// HTML stays byte-identical to before --rubric-pin existed.
|
|
@@ -1113,6 +1242,8 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
1113
1242
|
</div>
|
|
1114
1243
|
</header>
|
|
1115
1244
|
|
|
1245
|
+
${renderFindingsFirstSummary(report)}
|
|
1246
|
+
|
|
1116
1247
|
${renderRelyingPartySummary(relyingPartySummary)}
|
|
1117
1248
|
|
|
1118
1249
|
${(report.scanLimitations?.length ?? 0) > 0 ? `<section class="scan-limitations-section evidence-section" aria-label="Scan limitations">
|
|
@@ -1154,6 +1285,26 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
1154
1285
|
</html>
|
|
1155
1286
|
`;
|
|
1156
1287
|
}
|
|
1288
|
+
var FINDINGS_FIRST_SEVERITY_ORDER = {
|
|
1289
|
+
critical: 0,
|
|
1290
|
+
warning: 1,
|
|
1291
|
+
info: 2
|
|
1292
|
+
};
|
|
1293
|
+
function collectFindingsFirstEntries(report) {
|
|
1294
|
+
return report.criteria.flatMap(
|
|
1295
|
+
(criterion) => criterion.findings.filter((finding) => finding.severity === "critical" || finding.severity === "warning").map((finding) => ({ criterion, finding }))
|
|
1296
|
+
).sort(
|
|
1297
|
+
(a, b) => FINDINGS_FIRST_SEVERITY_ORDER[a.finding.severity] - FINDINGS_FIRST_SEVERITY_ORDER[b.finding.severity]
|
|
1298
|
+
);
|
|
1299
|
+
}
|
|
1300
|
+
function renderFindingsFirstSummary(report) {
|
|
1301
|
+
const entries = collectFindingsFirstEntries(report);
|
|
1302
|
+
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>';
|
|
1303
|
+
return `<section class="findings-first-summary" aria-labelledby="findings-first-heading">
|
|
1304
|
+
<h2 id="findings-first-heading">Critical and warning findings</h2>
|
|
1305
|
+
${body}
|
|
1306
|
+
</section>`;
|
|
1307
|
+
}
|
|
1157
1308
|
function renderRelyingPartySummary(summary) {
|
|
1158
1309
|
return `<section class="relying-party-summary" aria-labelledby="relying-party-heading">
|
|
1159
1310
|
<h2 id="relying-party-heading">How to read this certificate</h2>
|
|
@@ -1167,10 +1318,12 @@ function renderRelyingPartySummary(summary) {
|
|
|
1167
1318
|
}
|
|
1168
1319
|
function renderGlossary(entries) {
|
|
1169
1320
|
return `<section class="glossary" aria-labelledby="glossary-heading">
|
|
1170
|
-
<
|
|
1171
|
-
|
|
1321
|
+
<details>
|
|
1322
|
+
<summary><h2 id="glossary-heading">Plain-language glossary</h2></summary>
|
|
1323
|
+
<dl>${entries.map(
|
|
1172
1324
|
(entry) => `<div><dt id="glossary-${escapeAttribute(entry.key)}">${escapeHtml(entry.term)}</dt><dd>${escapeHtml(entry.definition)}</dd></div>`
|
|
1173
1325
|
).join("")}</dl>
|
|
1326
|
+
</details>
|
|
1174
1327
|
</section>`;
|
|
1175
1328
|
}
|
|
1176
1329
|
function renderContentReadSummary(report) {
|
|
@@ -1245,12 +1398,12 @@ function renderCriterionColumn(title, criteria, gitHistoryUnavailable) {
|
|
|
1245
1398
|
</section>`;
|
|
1246
1399
|
}
|
|
1247
1400
|
function renderNotApplicableGroup(criteria) {
|
|
1248
|
-
return `<
|
|
1249
|
-
<h3 class="na-heading">Not applicable to this repository</h3>
|
|
1401
|
+
return `<details class="na-group">
|
|
1402
|
+
<summary><h3 class="na-heading">Not applicable to this repository</h3></summary>
|
|
1250
1403
|
<ul class="na-list">
|
|
1251
1404
|
${criteria.map(renderNotApplicableItem).join("")}
|
|
1252
1405
|
</ul>
|
|
1253
|
-
</
|
|
1406
|
+
</details>`;
|
|
1254
1407
|
}
|
|
1255
1408
|
function renderNotApplicableItem(criterion) {
|
|
1256
1409
|
const reason = criterion.notes ?? "Not applicable to this repository.";
|
|
@@ -1310,15 +1463,21 @@ function scoreBandForPresentation(score) {
|
|
|
1310
1463
|
if (score >= 1.5) return "warning";
|
|
1311
1464
|
return "critical";
|
|
1312
1465
|
}
|
|
1466
|
+
function renderMetricWeightBadge(criterion, metric2) {
|
|
1467
|
+
const appliedSharePercent = formatCertificateMetricAppliedWeightPercent(criterion, metric2);
|
|
1468
|
+
return `<span class="metric-weight">weight ${appliedSharePercent}% of ${escapeHtml(criterion.id)}</span>`;
|
|
1469
|
+
}
|
|
1313
1470
|
function renderMetric(criterion, metric2) {
|
|
1314
1471
|
const entry = glossaryEntryForMetric(metric2);
|
|
1315
1472
|
const tooltipId = `tooltip-${criterion.id}-${entry.key}`;
|
|
1316
1473
|
const formatted = formatCertificateMetricValue(criterion, metric2);
|
|
1474
|
+
const weightBadge = renderMetricWeightBadge(criterion, metric2);
|
|
1317
1475
|
const cappedEntry = formatted.includes("capped") ? CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "capped") : void 0;
|
|
1318
1476
|
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>`;
|
|
1319
|
-
if (!cappedEntry)
|
|
1477
|
+
if (!cappedEntry)
|
|
1478
|
+
return `${label}<span class="metric-value">${escapeHtml(formatted)} ${weightBadge}</span>`;
|
|
1320
1479
|
const cappedTooltipId = `${tooltipId}-capped`;
|
|
1321
|
-
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>`;
|
|
1480
|
+
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>`;
|
|
1322
1481
|
}
|
|
1323
1482
|
function renderCategoryScore(score, coverage, category) {
|
|
1324
1483
|
const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
|
|
@@ -1466,6 +1625,9 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
|
1466
1625
|
.subscores { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; color: var(--muted); font-family: var(--mono); font-size: 12px; }
|
|
1467
1626
|
.coverage-note { margin-top: 8px; color: var(--faint); font-family: var(--mono); font-size: 11px; }
|
|
1468
1627
|
.source-counts { margin: 8px 0 0; padding-left: 18px; font-family: var(--mono); font-size: 12px; color: var(--muted); }
|
|
1628
|
+
.findings-first-summary { margin-top: 20px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
1629
|
+
.findings-first-summary ul { margin: 0; padding-left: 18px; display: grid; gap: 10px; }
|
|
1630
|
+
.findings-first-empty { margin: 0; color: var(--muted); }
|
|
1469
1631
|
.relying-party-summary, .glossary { margin-top: 28px; border: 1px solid var(--line-strong); border-radius: 8px; background: var(--surface); padding: 22px; }
|
|
1470
1632
|
.relying-party-summary dl { display: grid; gap: 14px; margin: 0; }
|
|
1471
1633
|
.relying-party-summary dl div { display: grid; grid-template-columns: 190px minmax(0, 1fr); gap: 18px; }
|
|
@@ -1473,6 +1635,14 @@ dd { margin: 0; color: var(--muted); overflow-wrap: anywhere; }
|
|
|
1473
1635
|
.glossary dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 22px; margin: 0; }
|
|
1474
1636
|
.glossary dl div { border-top: 1px solid var(--line); padding-top: 10px; }
|
|
1475
1637
|
.glossary dd { margin-top: 4px; font-size: 13px; }
|
|
1638
|
+
/* Track A3: native <details>/<summary> disclosure \u2014 the summary is the clickable control, a
|
|
1639
|
+
heading nests inside it as its accessible label (still exposed as a heading), and the heading's
|
|
1640
|
+
own vertical margin is neutralized by making it inline, so the marker sits flush against it. */
|
|
1641
|
+
summary { cursor: pointer; }
|
|
1642
|
+
summary h2, summary h3 { display: inline; }
|
|
1643
|
+
summary::marker { color: var(--periwinkle); }
|
|
1644
|
+
summary:focus-visible { outline: none; box-shadow: 0 0 0 2px var(--periwinkle); border-radius: 3px; }
|
|
1645
|
+
.glossary details[open] dl, .na-group[open] .na-list { margin-top: 16px; }
|
|
1476
1646
|
.trust-grid, .evidence-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; margin-top: 28px; }
|
|
1477
1647
|
.scan-limitations-section { margin-top: 28px; border-color: rgba(231, 191, 114, .44); }
|
|
1478
1648
|
.scan-limitations-section .scan-warning { list-style: none; margin-left: -18px; }
|
|
@@ -1504,6 +1674,7 @@ h3 { font-size: 15px; line-height: 1.35; font-weight: 600; margin-bottom: 0; }
|
|
|
1504
1674
|
.criterion-metrics li { display: flex; justify-content: space-between; gap: 12px; border-bottom: 1px solid rgba(238, 244, 247, .08); padding-bottom: 6px; }
|
|
1505
1675
|
.criterion-metrics strong { color: var(--text); font-weight: 650; }
|
|
1506
1676
|
.criterion-metrics span { color: var(--muted); }
|
|
1677
|
+
.criterion-metrics .metric-weight { font-size: 10px; color: var(--faint); }
|
|
1507
1678
|
.criterion-metrics > li > .metric-value {
|
|
1508
1679
|
min-width: 0; white-space: normal; overflow-wrap: anywhere; text-align: right;
|
|
1509
1680
|
}
|
|
@@ -1667,6 +1838,7 @@ function createWitanAttestation(report, options) {
|
|
|
1667
1838
|
tool: { name: "cejel", version: options.toolVersion },
|
|
1668
1839
|
generatedAt: options.generatedAt,
|
|
1669
1840
|
reportFormatVersion: WITAN_REPORT_FORMAT_VERSION,
|
|
1841
|
+
...options.githubRunAttempt ? { githubRunAttempt: options.githubRunAttempt } : {},
|
|
1670
1842
|
rubricVersion: report.rubricVersion,
|
|
1671
1843
|
repository: {
|
|
1672
1844
|
productSlug: report.productSlug,
|
|
@@ -1788,7 +1960,7 @@ function stripBom(text) {
|
|
|
1788
1960
|
|
|
1789
1961
|
// src/witan/repo-signals.ts
|
|
1790
1962
|
function usesV17DetectorClosure(rubricVersion) {
|
|
1791
|
-
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;
|
|
1963
|
+
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;
|
|
1792
1964
|
}
|
|
1793
1965
|
var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
|
|
1794
1966
|
rubricGeneration: "legacy",
|
|
@@ -1803,10 +1975,14 @@ var INVENTORY_SCAN_PATTERN_SETS = {
|
|
|
1803
1975
|
id: "cejel.core-a1.concrete-test-files.v1",
|
|
1804
1976
|
count: 16
|
|
1805
1977
|
},
|
|
1806
|
-
|
|
1978
|
+
a1CoverageConfigurationV1: {
|
|
1807
1979
|
id: "cejel.core-a1.coverage-configuration.v1",
|
|
1808
1980
|
count: 10
|
|
1809
1981
|
},
|
|
1982
|
+
a1CoverageConfigurationV2: {
|
|
1983
|
+
id: "cejel.core-a1.coverage-configuration.v2",
|
|
1984
|
+
count: 14
|
|
1985
|
+
},
|
|
1810
1986
|
a2CurrentSecret: {
|
|
1811
1987
|
id: "cejel.core-a2.current-secret-shape.v1",
|
|
1812
1988
|
count: 3
|
|
@@ -1855,25 +2031,127 @@ function inventoryAbsenceFinding(options) {
|
|
|
1855
2031
|
} : {}
|
|
1856
2032
|
};
|
|
1857
2033
|
}
|
|
2034
|
+
function wholesaleCriterionAbstention(signal, reasons) {
|
|
2035
|
+
const { insufficientDataReason } = classifyAbstentionReason(reasons);
|
|
2036
|
+
return {
|
|
2037
|
+
criterionId: signal.criterionId,
|
|
2038
|
+
positiveEvidence: [],
|
|
2039
|
+
findings: [],
|
|
2040
|
+
metrics: [],
|
|
2041
|
+
insufficientData: true,
|
|
2042
|
+
...insufficientDataReason !== void 0 ? { insufficientDataReason } : {},
|
|
2043
|
+
notes: insufficientDataReason !== void 0 ? "Cejel abstained on this criterion because relevant repository content was declined under the repository content size limit or excluded from scanning by policy. This is a disclosed coverage limit, not a read failure, and does not affect the composite score." : "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
|
|
2044
|
+
};
|
|
2045
|
+
}
|
|
2046
|
+
function skipReasonsForCriterion(criterionId, tracked) {
|
|
2047
|
+
const combined = new Set(tracked.criterionSkipReasons.get(criterionId) ?? []);
|
|
2048
|
+
const prefix = contentReadSignalKey(criterionId, "");
|
|
2049
|
+
for (const [key, reasons] of tracked.signalSkipReasons) {
|
|
2050
|
+
if (!key.startsWith(prefix)) continue;
|
|
2051
|
+
for (const reason of reasons) combined.add(reason);
|
|
2052
|
+
}
|
|
2053
|
+
return combined;
|
|
2054
|
+
}
|
|
2055
|
+
function skipReasonsForSignal(criterionId, signalName, tracked) {
|
|
2056
|
+
const reasons = tracked.signalSkipReasons.get(contentReadSignalKey(criterionId, signalName));
|
|
2057
|
+
return reasons ? new Set(reasons) : void 0;
|
|
2058
|
+
}
|
|
1858
2059
|
function buildWitanInputFromRepo(options) {
|
|
1859
2060
|
const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
|
|
1860
2061
|
const affectedCriteria = tracked.affectedCriteria;
|
|
2062
|
+
const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
|
|
2063
|
+
if (rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
|
|
2064
|
+
const readFailureCriteria = [];
|
|
2065
|
+
const coverageLimitedCriteria = [];
|
|
2066
|
+
const signals2 = (tracked.value.signals ?? []).map((signal) => {
|
|
2067
|
+
if (!affectedCriteria.has(signal.criterionId)) return signal;
|
|
2068
|
+
const reasons = skipReasonsForCriterion(signal.criterionId, tracked);
|
|
2069
|
+
const { insufficientDataReason } = classifyAbstentionReason(reasons);
|
|
2070
|
+
(insufficientDataReason !== void 0 ? coverageLimitedCriteria : readFailureCriteria).push(
|
|
2071
|
+
signal.criterionId
|
|
2072
|
+
);
|
|
2073
|
+
return wholesaleCriterionAbstention(signal, reasons);
|
|
2074
|
+
});
|
|
2075
|
+
const scanLimitations2 = [...tracked.value.scanLimitations ?? []];
|
|
2076
|
+
if (readFailureCriteria.length > 0 && scanLimitations2.length < 16) {
|
|
2077
|
+
scanLimitations2.push(
|
|
2078
|
+
`${readFailureCriteria.length} ${readFailureCriteria.length === 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.`
|
|
2079
|
+
);
|
|
2080
|
+
}
|
|
2081
|
+
if (coverageLimitedCriteria.length > 0 && scanLimitations2.length < 16) {
|
|
2082
|
+
scanLimitations2.push(
|
|
2083
|
+
`${coverageLimitedCriteria.length} ${coverageLimitedCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content was declined under the repository content size limit or excluded from scanning by policy \u2014 a disclosed coverage limit, not a read failure. This does not affect the composite score.`
|
|
2084
|
+
);
|
|
2085
|
+
}
|
|
2086
|
+
return {
|
|
2087
|
+
...tracked.value,
|
|
2088
|
+
signals: signals2,
|
|
2089
|
+
...scanLimitations2.length > 0 ? { scanLimitations: scanLimitations2 } : {},
|
|
2090
|
+
contentReadSummary: tracked.summary
|
|
2091
|
+
};
|
|
2092
|
+
}
|
|
2093
|
+
const wholesaleAbstainedReadFailureCriteria = [];
|
|
2094
|
+
const wholesaleAbstainedCoverageLimitedCriteria = [];
|
|
2095
|
+
const partiallyAbstainedReadFailureSignals = [];
|
|
2096
|
+
const partiallyAbstainedCoverageLimitedSignals = [];
|
|
1861
2097
|
const signals = (tracked.value.signals ?? []).map((signal) => {
|
|
1862
2098
|
if (!affectedCriteria.has(signal.criterionId)) return signal;
|
|
2099
|
+
if (tracked.unattributedCriteria.has(signal.criterionId)) {
|
|
2100
|
+
const reasons = tracked.criterionSkipReasons.get(signal.criterionId);
|
|
2101
|
+
const { insufficientDataReason: insufficientDataReason2 } = classifyAbstentionReason(reasons);
|
|
2102
|
+
(insufficientDataReason2 !== void 0 ? wholesaleAbstainedCoverageLimitedCriteria : wholesaleAbstainedReadFailureCriteria).push(signal.criterionId);
|
|
2103
|
+
return wholesaleCriterionAbstention(signal, reasons);
|
|
2104
|
+
}
|
|
2105
|
+
const prefix = contentReadSignalKey(signal.criterionId, "");
|
|
2106
|
+
const affectedNames = [...tracked.affectedSignals].filter((key) => key.startsWith(prefix)).map((key) => key.slice(prefix.length));
|
|
2107
|
+
if (affectedNames.length === 0) {
|
|
2108
|
+
wholesaleAbstainedReadFailureCriteria.push(signal.criterionId);
|
|
2109
|
+
return wholesaleCriterionAbstention(signal);
|
|
2110
|
+
}
|
|
2111
|
+
const affectedNameSet = new Set(affectedNames);
|
|
2112
|
+
const metrics = (signal.metrics ?? []).filter((metric2) => !affectedNameSet.has(metric2.name));
|
|
2113
|
+
const anythingSurvives = metrics.length > 0 || (signal.positiveEvidence?.length ?? 0) > 0 || (signal.findings?.length ?? 0) > 0;
|
|
2114
|
+
const droppedReasons = /* @__PURE__ */ new Set();
|
|
2115
|
+
for (const name of affectedNames) {
|
|
2116
|
+
for (const reason of skipReasonsForSignal(signal.criterionId, name, tracked) ?? []) {
|
|
2117
|
+
droppedReasons.add(reason);
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
if (!anythingSurvives) {
|
|
2121
|
+
const { insufficientDataReason: insufficientDataReason2 } = classifyAbstentionReason(droppedReasons);
|
|
2122
|
+
(insufficientDataReason2 !== void 0 ? wholesaleAbstainedCoverageLimitedCriteria : wholesaleAbstainedReadFailureCriteria).push(signal.criterionId);
|
|
2123
|
+
return wholesaleCriterionAbstention(signal, droppedReasons);
|
|
2124
|
+
}
|
|
2125
|
+
const { insufficientDataReason } = classifyAbstentionReason(droppedReasons);
|
|
2126
|
+
const isReadFailure = insufficientDataReason === void 0;
|
|
2127
|
+
for (const name of affectedNames) {
|
|
2128
|
+
(isReadFailure ? partiallyAbstainedReadFailureSignals : partiallyAbstainedCoverageLimitedSignals).push(`${signal.criterionId}.${name}`);
|
|
2129
|
+
}
|
|
1863
2130
|
return {
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
metrics: [],
|
|
1868
|
-
insufficientData: true,
|
|
1869
|
-
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."
|
|
2131
|
+
...signal,
|
|
2132
|
+
metrics,
|
|
2133
|
+
notes: isReadFailure ? `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.` : `Cejel declined ${[...affectedNames].sort().join(", ")} in this criterion under the repository content size limit or an extension exclusion; other signals in this criterion were computed from readable files and are unaffected. This is a disclosed coverage limit, not a read failure.`
|
|
1870
2134
|
};
|
|
1871
2135
|
});
|
|
1872
|
-
const affectedCount = affectedCriteria.size;
|
|
1873
2136
|
const scanLimitations = [...tracked.value.scanLimitations ?? []];
|
|
1874
|
-
if (
|
|
2137
|
+
if (wholesaleAbstainedReadFailureCriteria.length > 0 && scanLimitations.length < 16) {
|
|
2138
|
+
scanLimitations.push(
|
|
2139
|
+
`${wholesaleAbstainedReadFailureCriteria.length} ${wholesaleAbstainedReadFailureCriteria.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.`
|
|
2140
|
+
);
|
|
2141
|
+
}
|
|
2142
|
+
if (wholesaleAbstainedCoverageLimitedCriteria.length > 0 && scanLimitations.length < 16) {
|
|
2143
|
+
scanLimitations.push(
|
|
2144
|
+
`${wholesaleAbstainedCoverageLimitedCriteria.length} ${wholesaleAbstainedCoverageLimitedCriteria.length === 1 ? "criterion" : "criteria"} abstained because relevant repository content was declined under the repository content size limit or excluded from scanning by policy, and the affected signal could not be isolated. This is a disclosed coverage limit, not a read failure, and does not affect the composite score.`
|
|
2145
|
+
);
|
|
2146
|
+
}
|
|
2147
|
+
if (partiallyAbstainedReadFailureSignals.length > 0 && scanLimitations.length < 16) {
|
|
1875
2148
|
scanLimitations.push(
|
|
1876
|
-
`${
|
|
2149
|
+
`${partiallyAbstainedReadFailureSignals.length} ${partiallyAbstainedReadFailureSignals.length === 1 ? "signal" : "signals"} abstained because its own repository content could not be read, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedReadFailureSignals].sort().join(", ")}.`
|
|
2150
|
+
);
|
|
2151
|
+
}
|
|
2152
|
+
if (partiallyAbstainedCoverageLimitedSignals.length > 0 && scanLimitations.length < 16) {
|
|
2153
|
+
scanLimitations.push(
|
|
2154
|
+
`${partiallyAbstainedCoverageLimitedSignals.length} ${partiallyAbstainedCoverageLimitedSignals.length === 1 ? "signal" : "signals"} declined under the repository content size limit or an extension exclusion, while other signals in the same criterion were unaffected: ${[...partiallyAbstainedCoverageLimitedSignals].sort().join(", ")}. This is a disclosed coverage limit, not a read failure.`
|
|
1877
2155
|
);
|
|
1878
2156
|
}
|
|
1879
2157
|
return {
|
|
@@ -1897,11 +2175,13 @@ function buildWitanInputFromRepoUntracked(options) {
|
|
|
1897
2175
|
rubricGeneration: usesV17Detectors ? "current" : "legacy",
|
|
1898
2176
|
inventoryProvenance: fileInventory.repoFileInventoryScope === "tracked-scan-eligible" ? "tracked-scan-eligible" : "directory-walk"
|
|
1899
2177
|
};
|
|
1900
|
-
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;
|
|
1901
|
-
const usesV19CommitYear = rubricVersion === WITAN_RUBRIC_VERSION_V19 || rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
|
|
1902
|
-
const usesV20A3ExplicitGaps = rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
|
|
1903
|
-
const usesV21ExecutedEscalations = rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22;
|
|
1904
|
-
const usesV22PackageStartEntrypoint = rubricVersion === WITAN_RUBRIC_VERSION_V22;
|
|
2178
|
+
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;
|
|
2179
|
+
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;
|
|
2180
|
+
const usesV20A3ExplicitGaps = rubricVersion === WITAN_RUBRIC_VERSION_V20 || rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2181
|
+
const usesV21ExecutedEscalations = rubricVersion === WITAN_RUBRIC_VERSION_V21 || rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2182
|
+
const usesV22PackageStartEntrypoint = rubricVersion === WITAN_RUBRIC_VERSION_V22 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2183
|
+
const usesV23CommandCoverage = rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
2184
|
+
const usesV23PemPrivateKeyGrammar = rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
1905
2185
|
const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
|
|
1906
2186
|
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(
|
|
1907
2187
|
options.repoPath,
|
|
@@ -1937,6 +2217,8 @@ function buildWitanInputFromRepoUntracked(options) {
|
|
|
1937
2217
|
usesV20A3ExplicitGaps,
|
|
1938
2218
|
usesV21ExecutedEscalations,
|
|
1939
2219
|
usesV22PackageStartEntrypoint,
|
|
2220
|
+
usesV23CommandCoverage,
|
|
2221
|
+
usesV23PemPrivateKeyGrammar,
|
|
1940
2222
|
reviewableSourceProof,
|
|
1941
2223
|
inventoryAbsenceContext,
|
|
1942
2224
|
scanLimitations
|
|
@@ -2548,7 +2830,7 @@ function buildNotApplicableSignal(criterionId, reason) {
|
|
|
2548
2830
|
notApplicable: true
|
|
2549
2831
|
};
|
|
2550
2832
|
}
|
|
2551
|
-
function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
2833
|
+
function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23CommandCoverage, useV23PemPrivateKeyGrammar, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
2552
2834
|
const signals = [];
|
|
2553
2835
|
const mono = resolveMonorepoContext(repoPath, scanLimitations);
|
|
2554
2836
|
const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
|
|
@@ -2560,6 +2842,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2560
2842
|
useV27Detectors,
|
|
2561
2843
|
useV33Detectors,
|
|
2562
2844
|
useV36Detectors,
|
|
2845
|
+
useV23CommandCoverage,
|
|
2563
2846
|
reviewableSourceProof,
|
|
2564
2847
|
inventoryAbsenceContext
|
|
2565
2848
|
)
|
|
@@ -2574,6 +2857,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2574
2857
|
useV36Detectors,
|
|
2575
2858
|
useV39Detectors,
|
|
2576
2859
|
useV47Detectors,
|
|
2860
|
+
useV23PemPrivateKeyGrammar,
|
|
2577
2861
|
useV18NativeRls,
|
|
2578
2862
|
inventoryAbsenceContext,
|
|
2579
2863
|
scanLimitations
|
|
@@ -2659,7 +2943,7 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
2659
2943
|
}
|
|
2660
2944
|
return signals;
|
|
2661
2945
|
}
|
|
2662
|
-
function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
2946
|
+
function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23CommandCoverage, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
2663
2947
|
const evidence = [];
|
|
2664
2948
|
const findings = [];
|
|
2665
2949
|
const allTestFiles = findConcreteTestFiles(
|
|
@@ -2670,17 +2954,33 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2670
2954
|
const testFiles = allTestFiles.slice(0, 8);
|
|
2671
2955
|
const sourceFiles = repoFiles.filter(isSourceFile);
|
|
2672
2956
|
const runnerFiles = repoFiles.filter(
|
|
2673
|
-
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isTestRunnerConfig(repoPath, file)
|
|
2957
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && (isTestRunnerConfig(repoPath, file) || useV23CommandCoverage && isRecipeTestEntry(repoPath, file))
|
|
2674
2958
|
).slice(0, 6);
|
|
2675
|
-
const
|
|
2676
|
-
const
|
|
2677
|
-
|
|
2959
|
+
const baseConfiguredRunnerFiles = useV33Detectors ? findConfiguredTestRunnerFiles(repoPath, repoFiles) : runnerFiles;
|
|
2960
|
+
const configuredRunnerFiles = useV23CommandCoverage ? [
|
|
2961
|
+
.../* @__PURE__ */ new Set([
|
|
2962
|
+
...baseConfiguredRunnerFiles,
|
|
2963
|
+
...repoFiles.filter((file) => isRecipeTestEntry(repoPath, file))
|
|
2964
|
+
])
|
|
2965
|
+
].sort() : baseConfiguredRunnerFiles;
|
|
2678
2966
|
const packageJson = findRootPackageJson(repoFiles);
|
|
2679
2967
|
const packageScripts = packageJson ? readPackageScripts(join(repoPath, packageJson)) : /* @__PURE__ */ new Map();
|
|
2968
|
+
const { coverageAnalysis, coveragePercent } = withContentReadSignal("A1", "coverage_percent", () => {
|
|
2969
|
+
const analysis = useV23CommandCoverage ? analyzeCoverageConfiguration(repoPath, repoFiles, useV27Detectors) : {
|
|
2970
|
+
coverageFiles: findCoverageConfigFiles(repoPath, repoFiles, useV27Detectors),
|
|
2971
|
+
unresolvedFiles: [],
|
|
2972
|
+
hasCoverageCommand: packageScripts.has("coverage")
|
|
2973
|
+
};
|
|
2974
|
+
return {
|
|
2975
|
+
coverageAnalysis: analysis,
|
|
2976
|
+
coveragePercent: readCoveragePercent(repoPath, analysis.coverageFiles.slice(0, 4))
|
|
2977
|
+
};
|
|
2978
|
+
});
|
|
2979
|
+
const allCoverageFiles = coverageAnalysis.coverageFiles;
|
|
2980
|
+
const coverageFiles = allCoverageFiles.slice(0, 4);
|
|
2680
2981
|
const packageJsonFiles = useV27Detectors ? repoFiles.filter(
|
|
2681
2982
|
(file) => /(^|\/)package\.json$/.test(file) && isAuthoredProductionPath(file)
|
|
2682
2983
|
) : packageJson ? [packageJson] : [];
|
|
2683
|
-
const coveragePercent = readCoveragePercent(repoPath, coverageFiles);
|
|
2684
2984
|
const ciWorkflows = repoFiles.filter(
|
|
2685
2985
|
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isCiWorkflow(file)
|
|
2686
2986
|
);
|
|
@@ -2695,7 +2995,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2695
2995
|
);
|
|
2696
2996
|
const verificationScriptCount = [
|
|
2697
2997
|
packageScripts.has("test") || Boolean(ciTestWorkflow),
|
|
2698
|
-
|
|
2998
|
+
coverageAnalysis.hasCoverageCommand,
|
|
2699
2999
|
packageScripts.has("lint") || ciHasLintCommand,
|
|
2700
3000
|
packageScripts.has("typecheck") || ciHasTypecheckCommand
|
|
2701
3001
|
].filter(Boolean).length;
|
|
@@ -2715,9 +3015,11 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2715
3015
|
if (packageJson && [...packageScripts.values()].some((script) => TEST_RUNNER_PATTERN.test(script))) {
|
|
2716
3016
|
evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Configured test runner"));
|
|
2717
3017
|
}
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
3018
|
+
withContentReadSignal("A1", "coverage_percent", () => {
|
|
3019
|
+
for (const file of coverageFiles) {
|
|
3020
|
+
evidence.push(evidenceForRelative(repoPath, file, "coverage", "Coverage configuration"));
|
|
3021
|
+
}
|
|
3022
|
+
});
|
|
2721
3023
|
if (ciTestWorkflow) {
|
|
2722
3024
|
evidence.push(
|
|
2723
3025
|
evidenceForRelative(repoPath, ciTestWorkflow, "test_run", "CI workflow runs the test suite")
|
|
@@ -2787,7 +3089,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2787
3089
|
})
|
|
2788
3090
|
);
|
|
2789
3091
|
}
|
|
2790
|
-
} else if (coverageFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
|
|
3092
|
+
} else if (coverageFiles.length === 0 && coverageAnalysis.unresolvedFiles.length === 0 && (!useV33Detectors || configuredRunnerFiles.length > 0)) {
|
|
2791
3093
|
if (isLeanTestToolchain) {
|
|
2792
3094
|
const leanEvidenceFile = packageJson ?? testFiles[0];
|
|
2793
3095
|
if (leanEvidenceFile) {
|
|
@@ -2814,16 +3116,16 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2814
3116
|
evidence: firstEvidence,
|
|
2815
3117
|
context: inventoryAbsenceContext,
|
|
2816
3118
|
inventoryCount: repoFiles.length,
|
|
2817
|
-
patternSet: INVENTORY_SCAN_PATTERN_SETS.
|
|
3119
|
+
patternSet: useV23CommandCoverage ? INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfigurationV2 : INVENTORY_SCAN_PATTERN_SETS.a1CoverageConfigurationV1,
|
|
2818
3120
|
matchCount: 0
|
|
2819
3121
|
})
|
|
2820
3122
|
);
|
|
2821
3123
|
}
|
|
2822
3124
|
}
|
|
2823
|
-
const nonHollowShare =
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
reviewableSourceProof !== void 0
|
|
3125
|
+
const nonHollowShare = withContentReadSignal(
|
|
3126
|
+
"A1",
|
|
3127
|
+
"non_hollow_test_share",
|
|
3128
|
+
() => measureNonHollowTestShare(repoPath, allTestFiles, reviewableSourceProof !== void 0)
|
|
2827
3129
|
);
|
|
2828
3130
|
return {
|
|
2829
3131
|
criterionId: "A1",
|
|
@@ -2864,7 +3166,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2864
3166
|
label: "test command",
|
|
2865
3167
|
count: packageScripts.has("test") || Boolean(ciTestWorkflow) ? 1 : 0
|
|
2866
3168
|
},
|
|
2867
|
-
{ label: "coverage command", count:
|
|
3169
|
+
{ label: "coverage command", count: coverageAnalysis.hasCoverageCommand ? 1 : 0 },
|
|
2868
3170
|
{
|
|
2869
3171
|
label: "lint command",
|
|
2870
3172
|
count: packageScripts.has("lint") || ciHasLintCommand ? 1 : 0
|
|
@@ -2887,7 +3189,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
2887
3189
|
"Penalizes skipped or placeholder-only test files; test-directory support scaffolding (helpers/fixtures with no test in them) is excluded from the denominator."
|
|
2888
3190
|
)
|
|
2889
3191
|
],
|
|
2890
|
-
notes: "A1 is detected from real test files, test runner configuration, and optional coverage configuration."
|
|
3192
|
+
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."
|
|
2891
3193
|
};
|
|
2892
3194
|
}
|
|
2893
3195
|
function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inventoryAbsenceContext) {
|
|
@@ -2971,7 +3273,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
|
|
|
2971
3273
|
notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files.`
|
|
2972
3274
|
};
|
|
2973
3275
|
}
|
|
2974
|
-
function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
|
|
3276
|
+
function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV18NativeRls, inventoryAbsenceContext, scanLimitations) {
|
|
2975
3277
|
const evidence = [];
|
|
2976
3278
|
const findings = [];
|
|
2977
3279
|
const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
|
|
@@ -2979,40 +3281,51 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
2979
3281
|
const migrationFiles = repoFiles.filter(
|
|
2980
3282
|
(file) => /(^|\/)(migrations?|drizzle|prisma)\//.test(file)
|
|
2981
3283
|
);
|
|
2982
|
-
|
|
2983
|
-
|
|
2984
|
-
|
|
2985
|
-
|
|
2986
|
-
|
|
2987
|
-
|
|
2988
|
-
|
|
2989
|
-
|
|
2990
|
-
|
|
2991
|
-
|
|
2992
|
-
|
|
2993
|
-
|
|
2994
|
-
|
|
2995
|
-
|
|
2996
|
-
|
|
2997
|
-
|
|
2998
|
-
|
|
2999
|
-
|
|
3000
|
-
|
|
3001
|
-
|
|
3002
|
-
|
|
3003
|
-
|
|
3004
|
-
|
|
3005
|
-
|
|
3006
|
-
|
|
3284
|
+
const { committedSecret, currentSecretFingerprintsByPath } = withContentReadSignal(
|
|
3285
|
+
"A2",
|
|
3286
|
+
"secret_cleanliness",
|
|
3287
|
+
() => {
|
|
3288
|
+
let match_;
|
|
3289
|
+
for (const path of repoFiles) {
|
|
3290
|
+
const authoredProductionPath = useV47Detectors ? isV47AuthoredProductionPath(path) : useV39Detectors ? isV39AuthoredProductionPath(path) : isAuthoredProductionPath(path);
|
|
3291
|
+
if (isIgnoredScanFile(path, useV47Detectors) || useV33Detectors && !authoredProductionPath) {
|
|
3292
|
+
continue;
|
|
3293
|
+
}
|
|
3294
|
+
const found = findCommittedSecretInFile(
|
|
3295
|
+
repoPath,
|
|
3296
|
+
path,
|
|
3297
|
+
useV36Detectors,
|
|
3298
|
+
useV39Detectors,
|
|
3299
|
+
useV47Detectors,
|
|
3300
|
+
useV23PemPrivateKeyGrammar
|
|
3301
|
+
);
|
|
3302
|
+
if (!found) continue;
|
|
3303
|
+
match_ = { path, match: found };
|
|
3304
|
+
break;
|
|
3305
|
+
}
|
|
3306
|
+
const fingerprintsByPath = /* @__PURE__ */ new Map();
|
|
3307
|
+
if (useV27Detectors) {
|
|
3308
|
+
for (const path of repoFiles.filter(
|
|
3309
|
+
(candidate) => isCredentialHistoryPath(candidate, useV36Detectors, useV23PemPrivateKeyGrammar)
|
|
3310
|
+
)) {
|
|
3311
|
+
fingerprintsByPath.set(
|
|
3007
3312
|
path,
|
|
3008
|
-
|
|
3009
|
-
|
|
3010
|
-
|
|
3011
|
-
|
|
3012
|
-
|
|
3013
|
-
|
|
3313
|
+
new Set(
|
|
3314
|
+
findSecretFingerprintsInFile(
|
|
3315
|
+
repoPath,
|
|
3316
|
+
path,
|
|
3317
|
+
useV36Detectors,
|
|
3318
|
+
useV39Detectors,
|
|
3319
|
+
useV47Detectors,
|
|
3320
|
+
useV23PemPrivateKeyGrammar
|
|
3321
|
+
)
|
|
3322
|
+
)
|
|
3323
|
+
);
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
return { committedSecret: match_, currentSecretFingerprintsByPath: fingerprintsByPath };
|
|
3014
3327
|
}
|
|
3015
|
-
|
|
3328
|
+
);
|
|
3016
3329
|
const historySecretScan = committedSecret && !useV27Detectors ? null : collectHistorySecretEvidence(
|
|
3017
3330
|
repoPath,
|
|
3018
3331
|
useV27Detectors,
|
|
@@ -3021,13 +3334,18 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
3021
3334
|
useV33Detectors,
|
|
3022
3335
|
useV36Detectors,
|
|
3023
3336
|
useV39Detectors,
|
|
3024
|
-
useV47Detectors
|
|
3337
|
+
useV47Detectors,
|
|
3338
|
+
useV23PemPrivateKeyGrammar
|
|
3025
3339
|
);
|
|
3026
3340
|
const hasConfirmedSecretFinding = committedSecret != null || historySecretScan?.evidence != null;
|
|
3027
|
-
const cryptoHygiene =
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3341
|
+
const cryptoHygiene = withContentReadSignal(
|
|
3342
|
+
"A2",
|
|
3343
|
+
"crypto_comparison_hygiene",
|
|
3344
|
+
() => collectCryptoHygieneEvidence(
|
|
3345
|
+
repoPath,
|
|
3346
|
+
repoFiles.filter(isImplementationFile).slice(0, 60),
|
|
3347
|
+
useV39Detectors
|
|
3348
|
+
)
|
|
3031
3349
|
);
|
|
3032
3350
|
const implFiles = repoFiles.filter(isImplementationFile);
|
|
3033
3351
|
const gitignoreHasEnvRule = gitignore != null && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m);
|
|
@@ -3067,57 +3385,93 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
3067
3385
|
} else if (dbLayerImportFile != null) {
|
|
3068
3386
|
evidence.push(evidenceForRelative(repoPath, dbLayerImportFile, "artifact", "DB client import"));
|
|
3069
3387
|
}
|
|
3070
|
-
const v47RlsPolicyFiles = useV47Detectors ? findRlsPolicyFiles(repoPath, repoFiles) : [];
|
|
3071
|
-
const nativeRlsInventory = useV18NativeRls ? deriveRlsPolicyScopeInventory(repoPath, repoFiles) : null;
|
|
3072
|
-
const tenantWithoutRlsPremiseFiles = useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
3073
|
-
const v47TenantStorageFiles = useV47Detectors && !useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
3074
|
-
const rlsMigration = useV47Detectors ? v47RlsPolicyFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, RLS_PATTERN));
|
|
3075
3388
|
const tenantScopePattern = useV39Detectors ? TENANT_SCOPE_PATTERN_V11 : useV36Detectors ? TENANT_SCOPE_PATTERN_V10 : TENANT_SCOPE_PATTERN;
|
|
3076
|
-
const
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3389
|
+
const {
|
|
3390
|
+
rlsMigration,
|
|
3391
|
+
tenantScopedFile,
|
|
3392
|
+
rlsPolicyCount,
|
|
3393
|
+
tenantScopedMigrationFileCount,
|
|
3394
|
+
nativeRlsInventory,
|
|
3395
|
+
tenantWithoutRlsPremiseFiles
|
|
3396
|
+
} = withContentReadSignals("A2", ["rls_policy_count", "tenant_scope_ratio"], () => {
|
|
3397
|
+
const v47RlsPolicyFiles = useV47Detectors ? findRlsPolicyFiles(repoPath, repoFiles) : [];
|
|
3398
|
+
const rlsInventory = useV18NativeRls ? deriveRlsPolicyScopeInventory(repoPath, repoFiles) : null;
|
|
3399
|
+
const tenantWithoutRlsFiles = useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
3400
|
+
const v47TenantStorageFiles = useV47Detectors && !useV18NativeRls ? findTenantStoragePremiseFiles(repoPath, repoFiles) : [];
|
|
3401
|
+
return {
|
|
3402
|
+
rlsMigration: useV47Detectors ? v47RlsPolicyFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, RLS_PATTERN)),
|
|
3403
|
+
tenantScopedFile: useV18NativeRls ? rlsInventory?.storageFiles[0] : useV47Detectors ? v47TenantStorageFiles[0] : migrationFiles.find((file) => fileContains(repoPath, file, tenantScopePattern)),
|
|
3404
|
+
rlsPolicyCount: useV18NativeRls ? rlsInventory?.policyCount ?? 0 : useV47Detectors ? v47RlsPolicyFiles.length : countPatternMatches(
|
|
3405
|
+
repoPath,
|
|
3406
|
+
migrationFiles,
|
|
3407
|
+
/create policy|enable row level security|force row level security/gi
|
|
3408
|
+
),
|
|
3409
|
+
tenantScopedMigrationFileCount: useV18NativeRls ? rlsInventory?.storageFiles.length ?? 0 : useV47Detectors ? v47TenantStorageFiles.length : countFilesContaining(repoPath, migrationFiles, tenantScopePattern),
|
|
3410
|
+
nativeRlsInventory: rlsInventory,
|
|
3411
|
+
tenantWithoutRlsPremiseFiles: tenantWithoutRlsFiles
|
|
3412
|
+
};
|
|
3413
|
+
});
|
|
3414
|
+
const { gitignoreEnvFile, hasEnvReadsInImplementation, envHandlingDepth } = withContentReadSignal(
|
|
3415
|
+
"A2",
|
|
3416
|
+
"env_handling_depth",
|
|
3417
|
+
() => {
|
|
3418
|
+
const envFile = gitignore && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m) ? gitignore : null;
|
|
3419
|
+
const hasEnvReads = countFilesContaining(repoPath, repoFiles.filter(isImplementationFile), ENV_READ_PATTERN) > 0;
|
|
3420
|
+
const depth = (envExamples.length > 0 ? 1 : 0) + (envFile ? 1 : 0) + (hasEnvReads ? 1 : 0);
|
|
3421
|
+
return { gitignoreEnvFile: envFile, hasEnvReadsInImplementation: hasEnvReads, envHandlingDepth: depth };
|
|
3422
|
+
}
|
|
3081
3423
|
);
|
|
3082
|
-
const tenantScopedMigrationFileCount = useV18NativeRls ? nativeRlsInventory?.storageFiles.length ?? 0 : useV47Detectors ? v47TenantStorageFiles.length : countFilesContaining(repoPath, migrationFiles, tenantScopePattern);
|
|
3083
|
-
const gitignoreEnvFile = gitignore && fileContains(repoPath, gitignore, /^\.env(\*|\b)|\.env\./m) ? gitignore : null;
|
|
3084
|
-
const envHandlingDepth = (envExamples.length > 0 ? 1 : 0) + (gitignoreEnvFile ? 1 : 0) + (countFilesContaining(repoPath, repoFiles.filter(isImplementationFile), ENV_READ_PATTERN) > 0 ? 1 : 0);
|
|
3085
3424
|
const isMultiTenant = useV18NativeRls ? (nativeRlsInventory?.policyCount ?? 0) > 0 && (nativeRlsInventory?.scopeIdentifiers.length ?? 0) > 0 : tenantScopedMigrationFileCount > 0;
|
|
3086
3425
|
if (gitignoreEnvFile) {
|
|
3087
|
-
|
|
3088
|
-
|
|
3426
|
+
withContentReadSignal(
|
|
3427
|
+
"A2",
|
|
3428
|
+
"env_handling_depth",
|
|
3429
|
+
() => evidence.push(
|
|
3430
|
+
evidenceForRelative(repoPath, gitignoreEnvFile, "secret_scan", ".env files are gitignored")
|
|
3431
|
+
)
|
|
3089
3432
|
);
|
|
3090
3433
|
}
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3434
|
+
withContentReadSignal("A2", "env_handling_depth", () => {
|
|
3435
|
+
for (const file of envExamples.slice(0, 3)) {
|
|
3436
|
+
evidence.push(evidenceForRelative(repoPath, file, "secret_scan", "Environment template"));
|
|
3437
|
+
}
|
|
3438
|
+
});
|
|
3094
3439
|
if (rlsMigration) {
|
|
3095
|
-
|
|
3096
|
-
|
|
3440
|
+
withContentReadSignals(
|
|
3441
|
+
"A2",
|
|
3442
|
+
["rls_policy_count", "tenant_scope_ratio"],
|
|
3443
|
+
() => evidence.push(
|
|
3444
|
+
evidenceForRelative(repoPath, rlsMigration, "artifact", "RLS or tenant migration")
|
|
3445
|
+
)
|
|
3097
3446
|
);
|
|
3098
3447
|
}
|
|
3099
3448
|
if (tenantScopedFile) {
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3449
|
+
withContentReadSignals(
|
|
3450
|
+
"A2",
|
|
3451
|
+
["rls_policy_count", "tenant_scope_ratio"],
|
|
3452
|
+
() => evidence.push(
|
|
3453
|
+
evidenceForRelative(
|
|
3454
|
+
repoPath,
|
|
3455
|
+
tenantScopedFile,
|
|
3456
|
+
"artifact",
|
|
3457
|
+
useV18NativeRls ? `Repository-native RLS scope (${nativeRlsInventory?.scopeIdentifiers.join(", ")})` : "Tenant scoping signal"
|
|
3458
|
+
)
|
|
3106
3459
|
)
|
|
3107
3460
|
);
|
|
3108
3461
|
}
|
|
3109
3462
|
if (committedSecret) {
|
|
3110
3463
|
const isTestPath = isTestOrFixturePath(committedSecret.path);
|
|
3111
3464
|
const isDefaultAdmin = committedSecret.match.kind === "default_admin";
|
|
3465
|
+
const isPemPrivateKey = committedSecret.match.kind === "pem_private_key";
|
|
3112
3466
|
findings.push({
|
|
3113
3467
|
severity: isTestPath ? "info" : "critical",
|
|
3114
|
-
summary: isDefaultAdmin ? isTestPath ? `Default administrative credential in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production deployment default; verify.` : "An explicit default administrative credential appears committed in production configuration." : isTestPath ? `Secret-shaped value in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears committed in the scanned repository.",
|
|
3468
|
+
summary: isDefaultAdmin ? isTestPath ? `Default administrative credential in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production deployment default; verify.` : "An explicit default administrative credential appears committed in production configuration." : isPemPrivateKey ? isTestPath ? `PEM-formatted private key in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production leak; verify.` : "A PEM-formatted private key appears committed in the scanned repository." : isTestPath ? `Secret-shaped value in a test/fixture file (${committedSecret.path}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears committed in the scanned repository.",
|
|
3115
3469
|
evidence: evidenceForRelativeAtLine(
|
|
3116
3470
|
repoPath,
|
|
3117
3471
|
committedSecret.path,
|
|
3118
3472
|
"secret_scan",
|
|
3119
3473
|
secretEvidenceLabel(
|
|
3120
|
-
isDefaultAdmin ? "Committed default administrative credential" : "Committed secret-shaped value",
|
|
3474
|
+
isDefaultAdmin ? "Committed default administrative credential" : isPemPrivateKey ? "Committed PEM-formatted private key" : "Committed secret-shaped value",
|
|
3121
3475
|
committedSecret.match
|
|
3122
3476
|
),
|
|
3123
3477
|
committedSecret.match.line
|
|
@@ -3148,9 +3502,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
3148
3502
|
if (historySecretScan?.evidence) {
|
|
3149
3503
|
const historyPath = historySecretScan.evidence.path ?? "";
|
|
3150
3504
|
const isTestPath = isTestOrFixturePath(historyPath);
|
|
3505
|
+
const isPemPrivateKey = historySecretScan.secretKind === "pem_private_key";
|
|
3151
3506
|
findings.push({
|
|
3152
3507
|
severity: isTestPath ? "info" : "critical",
|
|
3153
|
-
summary: isTestPath ? `Secret-shaped value in git history for a test/fixture file (${historyPath}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears in recent git history.",
|
|
3508
|
+
summary: isPemPrivateKey ? isTestPath ? `PEM-formatted private key in git history for a test/fixture file (${historyPath}) \u2014 likely fixture data, not a production leak; verify.` : "A PEM-formatted private key appears in recent git history." : isTestPath ? `Secret-shaped value in git history for a test/fixture file (${historyPath}) \u2014 likely fixture data, not a production leak; verify.` : "Secret-shaped value appears in recent git history.",
|
|
3154
3509
|
evidence: historySecretScan.evidence
|
|
3155
3510
|
});
|
|
3156
3511
|
} else if (historySecretScan?.truncated) {
|
|
@@ -3226,11 +3581,7 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
3226
3581
|
{ label: ".gitignore environment-file rule", count: gitignoreEnvFile ? 1 : 0 },
|
|
3227
3582
|
{
|
|
3228
3583
|
label: "environment reads in implementation code",
|
|
3229
|
-
count:
|
|
3230
|
-
repoPath,
|
|
3231
|
-
repoFiles.filter(isImplementationFile),
|
|
3232
|
-
ENV_READ_PATTERN
|
|
3233
|
-
) > 0 ? 1 : 0
|
|
3584
|
+
count: hasEnvReadsInImplementation ? 1 : 0
|
|
3234
3585
|
}
|
|
3235
3586
|
]
|
|
3236
3587
|
}
|
|
@@ -3409,37 +3760,51 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3409
3760
|
const envTemplate = repoFiles.find(
|
|
3410
3761
|
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isEnvTemplatePath(file)
|
|
3411
3762
|
);
|
|
3412
|
-
const healthChecks =
|
|
3413
|
-
|
|
3763
|
+
const healthChecks = withContentReadSignal(
|
|
3764
|
+
"A3",
|
|
3765
|
+
"prod_readiness_primitives",
|
|
3766
|
+
() => repoFiles.filter(
|
|
3767
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isHealthCheckSignalFile(repoPath, file)
|
|
3768
|
+
)
|
|
3414
3769
|
);
|
|
3415
3770
|
const healthCheck = healthChecks[0];
|
|
3416
|
-
const hasV20HealthOrReadinessRoute =
|
|
3417
|
-
|
|
3418
|
-
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3771
|
+
const { hasV20HealthOrReadinessRoute, hasV22PackageStartHealthOrReadinessRoute } = withContentReadSignal("A3", "health_readiness_route", () => ({
|
|
3772
|
+
hasV20HealthOrReadinessRoute: useV20ExplicitGaps && repoFiles.some(
|
|
3773
|
+
(file) => isAuthoredProductionPath(file) && isImplementationFile(file) && fileContains(repoPath, file, V20_HEALTH_OR_READINESS_ROUTE_PATTERN)
|
|
3774
|
+
),
|
|
3775
|
+
hasV22PackageStartHealthOrReadinessRoute: v22PackageStartHttpEntrypoint !== null && fileContains(
|
|
3776
|
+
repoPath,
|
|
3777
|
+
v22PackageStartHttpEntrypoint,
|
|
3778
|
+
V20_HEALTH_OR_READINESS_ROUTE_PATTERN
|
|
3779
|
+
)
|
|
3780
|
+
}));
|
|
3424
3781
|
const serverEntrypoint = v22PackageStartHttpEntrypoint ?? findServerEntrypointFile(repoPath, repoFiles, useV27Detectors) ?? v20DirectHttpEntrypoint ?? null;
|
|
3425
3782
|
const runtimeContainer = useV27Detectors ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, useV20ExplicitGaps) : null;
|
|
3426
3783
|
const errorBoundaries = repoFiles.filter(
|
|
3427
3784
|
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /error-boundary|error\.(tsx|jsx|ts|js)$/.test(file)
|
|
3428
3785
|
);
|
|
3429
3786
|
const errorBoundary = errorBoundaries[0];
|
|
3430
|
-
const observabilityCount =
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
|
|
3434
|
-
|
|
3435
|
-
|
|
3787
|
+
const observabilityCount = withContentReadSignal(
|
|
3788
|
+
"A3",
|
|
3789
|
+
"observability_depth",
|
|
3790
|
+
() => countFilesContaining(
|
|
3791
|
+
repoPath,
|
|
3792
|
+
repoFiles.filter(
|
|
3793
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && isImplementationFile(file)
|
|
3794
|
+
),
|
|
3795
|
+
/sentry|otel|opentelemetry|datadog|prometheus|metrics|logger|logtail/i
|
|
3796
|
+
)
|
|
3436
3797
|
);
|
|
3437
|
-
const rollbackSafetyCount =
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3798
|
+
const rollbackSafetyCount = withContentReadSignal(
|
|
3799
|
+
"A3",
|
|
3800
|
+
"rollback_safety_depth",
|
|
3801
|
+
() => countFilesContaining(
|
|
3802
|
+
repoPath,
|
|
3803
|
+
repoFiles.filter(
|
|
3804
|
+
(file) => (!useV27Detectors || isAuthoredProductionPath(file)) && /(^|\/)(docs|migrations?|drizzle|prisma|scripts)\//.test(file)
|
|
3805
|
+
),
|
|
3806
|
+
/rollback|roll back|migration safety|down migration|reversible|undo migration/i
|
|
3807
|
+
)
|
|
3443
3808
|
);
|
|
3444
3809
|
if (packageJson && (scripts.has("build") || scripts.has("typecheck"))) {
|
|
3445
3810
|
evidence.push(
|
|
@@ -3467,10 +3832,15 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3467
3832
|
);
|
|
3468
3833
|
if (envTemplate)
|
|
3469
3834
|
evidence.push(evidenceForRelative(repoPath, envTemplate, "prod_check", "Environment template"));
|
|
3470
|
-
if (healthCheck)
|
|
3471
|
-
|
|
3472
|
-
|
|
3835
|
+
if (healthCheck) {
|
|
3836
|
+
withContentReadSignal(
|
|
3837
|
+
"A3",
|
|
3838
|
+
"prod_readiness_primitives",
|
|
3839
|
+
() => evidence.push(
|
|
3840
|
+
evidenceForRelative(repoPath, healthCheck, "prod_check", "Health/readiness signal")
|
|
3841
|
+
)
|
|
3473
3842
|
);
|
|
3843
|
+
}
|
|
3474
3844
|
if (errorBoundary)
|
|
3475
3845
|
evidence.push(evidenceForRelative(repoPath, errorBoundary, "prod_check", "Error boundary"));
|
|
3476
3846
|
if (evidence.length === 0) {
|
|
@@ -3509,7 +3879,12 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3509
3879
|
)
|
|
3510
3880
|
});
|
|
3511
3881
|
} else if (runtimeContainer) {
|
|
3512
|
-
|
|
3882
|
+
const hasActiveHealthcheck = withContentReadSignal(
|
|
3883
|
+
"A3",
|
|
3884
|
+
"dockerfile_healthcheck",
|
|
3885
|
+
() => fileContains(repoPath, runtimeContainer, /^\s*HEALTHCHECK\s+(?!NONE\b)/im)
|
|
3886
|
+
);
|
|
3887
|
+
if (!hasActiveHealthcheck) {
|
|
3513
3888
|
findings.push({
|
|
3514
3889
|
severity: "info",
|
|
3515
3890
|
summary: "A runtime Dockerfile declares no active HEALTHCHECK instruction.",
|
|
@@ -3521,10 +3896,14 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
3521
3896
|
)
|
|
3522
3897
|
});
|
|
3523
3898
|
}
|
|
3524
|
-
} else if (serverEntrypoint &&
|
|
3525
|
-
|
|
3526
|
-
|
|
3527
|
-
|
|
3899
|
+
} else if (serverEntrypoint && withContentReadSignal(
|
|
3900
|
+
"A3",
|
|
3901
|
+
"health_readiness_route",
|
|
3902
|
+
() => fileContains(
|
|
3903
|
+
repoPath,
|
|
3904
|
+
serverEntrypoint,
|
|
3905
|
+
/\b(?:request|req)\.(?:url|path|pathname|method)\b/i
|
|
3906
|
+
)
|
|
3528
3907
|
) && healthChecks.length === 0 && !hasV20HealthOrReadinessRoute && !hasV22PackageStartHealthOrReadinessRoute) {
|
|
3529
3908
|
findings.push({
|
|
3530
3909
|
severity: "info",
|
|
@@ -3630,22 +4009,41 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
|
|
|
3630
4009
|
const findings = [];
|
|
3631
4010
|
const manifests = repoFiles.filter(isDependencyManifest);
|
|
3632
4011
|
const lockfiles = repoFiles.filter(isLockfile);
|
|
3633
|
-
const manifest = pickPrimaryDependencyManifest(repoPath, manifests);
|
|
3634
4012
|
const lockfile = lockfiles[0];
|
|
3635
4013
|
const rootLockfile = mono ? mono.sharedFiles.find(isLockfile) : void 0;
|
|
3636
4014
|
const rootUpdateConfig = mono ? mono.sharedFiles.find(isDependencyUpdateConfig) : void 0;
|
|
3637
4015
|
const hasLockfile = Boolean(lockfile) || Boolean(rootLockfile);
|
|
3638
4016
|
const updateConfig = repoFiles.find(isDependencyUpdateConfig);
|
|
3639
4017
|
const hasUpdateConfig = Boolean(updateConfig) || Boolean(rootUpdateConfig);
|
|
3640
|
-
const auditConfig =
|
|
3641
|
-
|
|
4018
|
+
const auditConfig = withContentReadSignal(
|
|
4019
|
+
"A4",
|
|
4020
|
+
"dependency_automation_ratio",
|
|
4021
|
+
() => manifests.some((file) => packageJsonHasAuditScript(repoPath, file))
|
|
4022
|
+
);
|
|
4023
|
+
const { manifest, dependencySpecs } = withContentReadSignals(
|
|
4024
|
+
"A4",
|
|
4025
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
4026
|
+
() => ({
|
|
4027
|
+
// Cite the manifest that actually carries dependencies for evidence/findings, not
|
|
4028
|
+
// whichever sorts first (e.g. a near-empty requirements-windows.txt that precedes the
|
|
4029
|
+
// real requirements.txt), so the evidence pointer reflects the file the A4 score is
|
|
4030
|
+
// derived from. The score itself is unaffected: it aggregates specs across ALL manifests.
|
|
4031
|
+
manifest: pickPrimaryDependencyManifest(repoPath, manifests),
|
|
4032
|
+
dependencySpecs: readDependencySpecs(repoPath, manifests)
|
|
4033
|
+
})
|
|
4034
|
+
);
|
|
3642
4035
|
const pinnedDependencyCount = dependencySpecs.filter(
|
|
3643
4036
|
(dependency) => isPinnedDependencyVersion(dependency.version)
|
|
3644
4037
|
).length;
|
|
3645
|
-
if (manifest)
|
|
3646
|
-
|
|
3647
|
-
|
|
4038
|
+
if (manifest) {
|
|
4039
|
+
withContentReadSignals(
|
|
4040
|
+
"A4",
|
|
4041
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
4042
|
+
() => evidence.push(
|
|
4043
|
+
evidenceForRelative(repoPath, manifest, "dependency_report", "Dependency manifest")
|
|
4044
|
+
)
|
|
3648
4045
|
);
|
|
4046
|
+
}
|
|
3649
4047
|
if (lockfile)
|
|
3650
4048
|
evidence.push(
|
|
3651
4049
|
evidenceForRelative(repoPath, lockfile, "dependency_report", "Dependency lockfile")
|
|
@@ -3678,28 +4076,36 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
|
|
|
3678
4076
|
const deployableServiceFiles = useV47Detectors ? findDeployableServicePremiseFiles(repoPath, repoFiles, true) : [];
|
|
3679
4077
|
const isAppOrService = (useV47Detectors ? deployableServiceFiles.length > 0 : isDeployableService(repoPath, repoFiles, useV27Detectors)) || packagedApplicationFiles.length > 0;
|
|
3680
4078
|
if (isAppOrService && !hasLockfile && manifest) {
|
|
3681
|
-
|
|
3682
|
-
|
|
3683
|
-
|
|
3684
|
-
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
3693
|
-
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
4079
|
+
withContentReadSignals(
|
|
4080
|
+
"A4",
|
|
4081
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
4082
|
+
() => findings.push(
|
|
4083
|
+
inventoryAbsenceFinding({
|
|
4084
|
+
severity: "critical",
|
|
4085
|
+
summaries: {
|
|
4086
|
+
legacy: "Dependency manifest is present without a detected lockfile \u2014 non-reproducible installs.",
|
|
4087
|
+
currentTracked: "Dependency manifest is present, but the tracked scan-eligible inventory matched no recognized lockfile.",
|
|
4088
|
+
currentDirectoryWalk: "Dependency manifest is present, but the bounded directory walk matched no recognized lockfile."
|
|
4089
|
+
},
|
|
4090
|
+
evidence: evidenceForRelative(
|
|
4091
|
+
repoPath,
|
|
4092
|
+
manifest,
|
|
4093
|
+
"dependency_report",
|
|
4094
|
+
"Dependency manifest"
|
|
4095
|
+
),
|
|
4096
|
+
context: inventoryAbsenceContext,
|
|
4097
|
+
inventoryCount: repoFiles.length + (mono?.sharedFiles.length ?? 0),
|
|
4098
|
+
patternSet: INVENTORY_SCAN_PATTERN_SETS.a4Lockfile,
|
|
4099
|
+
matchCount: 0
|
|
4100
|
+
})
|
|
4101
|
+
)
|
|
3700
4102
|
);
|
|
3701
4103
|
}
|
|
3702
|
-
if (manifest &&
|
|
4104
|
+
if (manifest && withContentReadSignal(
|
|
4105
|
+
"A4",
|
|
4106
|
+
"suspicious_dependencies",
|
|
4107
|
+
() => hasSuspiciousDependencies(repoPath, manifest, useV47Detectors)
|
|
4108
|
+
)) {
|
|
3703
4109
|
findings.push({
|
|
3704
4110
|
severity: "critical",
|
|
3705
4111
|
summary: "Dependency manifest contains obviously suspicious placeholder dependency names.",
|
|
@@ -3853,16 +4259,24 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, inv
|
|
|
3853
4259
|
...allDocFiles,
|
|
3854
4260
|
...repoFiles.filter((file) => /(^|\/)(SECURITY|THREAT[_-]?MODEL)\.md$/i.test(file))
|
|
3855
4261
|
]);
|
|
3856
|
-
const negativeSpaceDoc =
|
|
3857
|
-
|
|
4262
|
+
const negativeSpaceDoc = withContentReadSignal(
|
|
4263
|
+
"A5",
|
|
4264
|
+
"negative_space_documentation",
|
|
4265
|
+
() => [...negativeSpaceCandidates].find(
|
|
4266
|
+
(file) => fileContains(repoPath, file, NEGATIVE_SPACE_SECTION_PATTERN) || fileContains(repoPath, file, NEGATIVE_SPACE_PHRASE_PATTERN)
|
|
4267
|
+
)
|
|
3858
4268
|
);
|
|
3859
4269
|
if (negativeSpaceDoc) {
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
4270
|
+
withContentReadSignal(
|
|
4271
|
+
"A5",
|
|
4272
|
+
"negative_space_documentation",
|
|
4273
|
+
() => evidence.push(
|
|
4274
|
+
evidenceForRelative(
|
|
4275
|
+
repoPath,
|
|
4276
|
+
negativeSpaceDoc,
|
|
4277
|
+
"claim_reconciliation",
|
|
4278
|
+
'Documented limitations / threat model / "not covered" section'
|
|
4279
|
+
)
|
|
3866
4280
|
)
|
|
3867
4281
|
);
|
|
3868
4282
|
}
|
|
@@ -3999,18 +4413,33 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
3999
4413
|
const workflows = repoFiles.filter(isCiWorkflow);
|
|
4000
4414
|
const workflow = workflows[0];
|
|
4001
4415
|
const rootWorkflows = mono ? mono.sharedFiles.filter(isCiWorkflow) : [];
|
|
4002
|
-
const defaultBranchCiCount =
|
|
4416
|
+
const defaultBranchCiCount = withContentReadSignal(
|
|
4417
|
+
"B3",
|
|
4418
|
+
"default_branch_ci_depth",
|
|
4419
|
+
() => workflows.filter((file) => workflowTargetsDefaultBranch(repoPath, file)).length + (mono ? rootWorkflows.filter((file) => workflowTargetsDefaultBranch(mono.root, file)).length : 0)
|
|
4420
|
+
);
|
|
4003
4421
|
if (packageJson && scripts.has("test")) {
|
|
4004
4422
|
evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Test script"));
|
|
4005
4423
|
}
|
|
4006
4424
|
if (packageJson && scripts.has("lint")) {
|
|
4007
4425
|
evidence.push(evidenceForRelative(repoPath, packageJson, "ci_run", "Lint script"));
|
|
4008
4426
|
}
|
|
4009
|
-
if (workflow)
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4427
|
+
if (workflow) {
|
|
4428
|
+
withContentReadSignals(
|
|
4429
|
+
"B3",
|
|
4430
|
+
["ci_script_depth", "default_branch_ci_depth"],
|
|
4431
|
+
() => evidence.push(evidenceForRelative(repoPath, workflow, "ci_run", "CI workflow"))
|
|
4432
|
+
);
|
|
4433
|
+
} else if (rootWorkflows[0] && mono) {
|
|
4434
|
+
const rootWorkflow = rootWorkflows[0];
|
|
4435
|
+
withContentReadSignals(
|
|
4436
|
+
"B3",
|
|
4437
|
+
["ci_script_depth", "default_branch_ci_depth"],
|
|
4438
|
+
() => evidence.push(
|
|
4439
|
+
evidenceForRelative(mono.root, rootWorkflow, "ci_run", "CI workflow (monorepo root)")
|
|
4440
|
+
)
|
|
4013
4441
|
);
|
|
4442
|
+
}
|
|
4014
4443
|
if (evidence.length === 0) return null;
|
|
4015
4444
|
const CI_BUILD_COMMAND_PATTERN = /\b(cargo\s+build|cmake|ninja|make\s+(build|all))\b/i;
|
|
4016
4445
|
const CI_COMMAND_CATEGORIES = [
|
|
@@ -4020,10 +4449,19 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
4020
4449
|
CI_BUILD_COMMAND_PATTERN
|
|
4021
4450
|
];
|
|
4022
4451
|
const scriptDepth = ["test", "lint", "typecheck", "build"].filter((s) => scripts.has(s)).length;
|
|
4023
|
-
const
|
|
4024
|
-
|
|
4025
|
-
|
|
4026
|
-
|
|
4452
|
+
const { repoCategoryHits, rootCategoryHits } = withContentReadSignal(
|
|
4453
|
+
"B3",
|
|
4454
|
+
"ci_script_depth",
|
|
4455
|
+
() => ({
|
|
4456
|
+
repoCategoryHits: CI_COMMAND_CATEGORIES.map(
|
|
4457
|
+
(pattern) => workflows.some((file) => fileContains(repoPath, file, pattern))
|
|
4458
|
+
),
|
|
4459
|
+
rootCategoryHits: mono ? CI_COMMAND_CATEGORIES.map(
|
|
4460
|
+
(pattern) => rootWorkflows.some((file) => fileContains(mono.root, file, pattern))
|
|
4461
|
+
) : []
|
|
4462
|
+
})
|
|
4463
|
+
);
|
|
4464
|
+
const ciCommandDepth = repoCategoryHits.filter(Boolean).length + rootCategoryHits.filter(Boolean).length;
|
|
4027
4465
|
const combinedDepth = scriptDepth + ciCommandDepth;
|
|
4028
4466
|
return {
|
|
4029
4467
|
criterionId: "B3",
|
|
@@ -4045,13 +4483,13 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
4045
4483
|
label: `package script: ${name === "typecheck" ? "type-check" : name}`,
|
|
4046
4484
|
count: scripts.has(name) ? 1 : 0
|
|
4047
4485
|
})),
|
|
4048
|
-
...CI_COMMAND_CATEGORIES.map((
|
|
4486
|
+
...CI_COMMAND_CATEGORIES.map((_pattern, index) => ({
|
|
4049
4487
|
label: `repository CI command: ${["test", "lint", "type-check", "build"][index]}`,
|
|
4050
|
-
count:
|
|
4488
|
+
count: repoCategoryHits[index] ? 1 : 0
|
|
4051
4489
|
})),
|
|
4052
|
-
...mono ? CI_COMMAND_CATEGORIES.map((
|
|
4490
|
+
...mono ? CI_COMMAND_CATEGORIES.map((_pattern, index) => ({
|
|
4053
4491
|
label: `monorepo-root CI command: ${["test", "lint", "type-check", "build"][index]}`,
|
|
4054
|
-
count:
|
|
4492
|
+
count: rootCategoryHits[index] ? 1 : 0
|
|
4055
4493
|
})) : []
|
|
4056
4494
|
]
|
|
4057
4495
|
}
|
|
@@ -4096,9 +4534,11 @@ function readAuditFreshnessCommitterYear(repoPath, scanLimitations) {
|
|
|
4096
4534
|
function collectB4AuditEvidence(repoPath, repoFiles, generatedAt, useV19CommitYear = false, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
4097
4535
|
const evidence = [];
|
|
4098
4536
|
const auditFiles = repoFiles.filter(isAuditFile);
|
|
4099
|
-
|
|
4100
|
-
|
|
4101
|
-
|
|
4537
|
+
withContentReadSignal("B4", "audit_freshness_depth", () => {
|
|
4538
|
+
for (const file of auditFiles.slice(0, 5)) {
|
|
4539
|
+
evidence.push(evidenceForRelative(repoPath, file, "audit_log", "Audit or changelog artifact"));
|
|
4540
|
+
}
|
|
4541
|
+
});
|
|
4102
4542
|
if (evidence.length === 0) {
|
|
4103
4543
|
return buildNotApplicableSignal(
|
|
4104
4544
|
"B4",
|
|
@@ -4143,7 +4583,11 @@ function collectB4AuditEvidence(repoPath, repoFiles, generatedAt, useV19CommitYe
|
|
|
4143
4583
|
metric(
|
|
4144
4584
|
"audit_freshness_depth",
|
|
4145
4585
|
"Audit freshness depth",
|
|
4146
|
-
|
|
4586
|
+
withContentReadSignal(
|
|
4587
|
+
"B4",
|
|
4588
|
+
"audit_freshness_depth",
|
|
4589
|
+
() => countFilesContaining(repoPath, auditFiles, freshnessPattern)
|
|
4590
|
+
),
|
|
4147
4591
|
Math.max(auditFiles.length, 1),
|
|
4148
4592
|
0.2,
|
|
4149
4593
|
"ratio",
|
|
@@ -4156,93 +4600,128 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
|
|
|
4156
4600
|
const evidence = [];
|
|
4157
4601
|
const findings = [];
|
|
4158
4602
|
const docFiles = repoFiles.filter((file) => /\.(md|mdx)$/i.test(file));
|
|
4159
|
-
const humanGateDoc =
|
|
4160
|
-
|
|
4603
|
+
const humanGateDoc = withContentReadSignal(
|
|
4604
|
+
"B6",
|
|
4605
|
+
"human_gate_documented",
|
|
4606
|
+
() => docFiles.find((file) => fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN))
|
|
4161
4607
|
);
|
|
4162
4608
|
const implFiles = repoFiles.filter(isImplementationFile);
|
|
4163
|
-
const gatedPrivilegeCheckFile =
|
|
4164
|
-
|
|
4609
|
+
const gatedPrivilegeCheckFile = withContentReadSignal(
|
|
4610
|
+
"B6",
|
|
4611
|
+
"fail_closed_privilege_check",
|
|
4612
|
+
() => implFiles.find(
|
|
4613
|
+
(file) => fileContains(repoPath, file, GATED_PRIVILEGE_CHECK_PATTERN) && fileContains(repoPath, file, SET_ROLE_PATTERN)
|
|
4614
|
+
)
|
|
4165
4615
|
);
|
|
4166
|
-
const killSwitchFile =
|
|
4167
|
-
|
|
4616
|
+
const killSwitchFile = withContentReadSignal(
|
|
4617
|
+
"B6",
|
|
4618
|
+
"kill_switch_fail_safe_present",
|
|
4619
|
+
() => implFiles.find(
|
|
4620
|
+
(file) => fileContains(repoPath, file, KILL_SWITCH_NAME_PATTERN) && fileContains(repoPath, file, KILL_SWITCH_FAIL_CLOSED_PATTERN)
|
|
4621
|
+
)
|
|
4168
4622
|
);
|
|
4169
4623
|
const codeownersFile = repoFiles.find((file) => /(^|\/)CODEOWNERS$/.test(file));
|
|
4170
|
-
const reviewGateDoc =
|
|
4171
|
-
|
|
4624
|
+
const reviewGateDoc = withContentReadSignal(
|
|
4625
|
+
"B6",
|
|
4626
|
+
"protected_path_review_gate",
|
|
4627
|
+
() => docFiles.find((file) => fileContains(repoPath, file, REQUIRED_REVIEW_PATTERN))
|
|
4172
4628
|
);
|
|
4173
4629
|
const protectedPathReviewGate = codeownersFile ?? reviewGateDoc;
|
|
4174
|
-
const executableFiles =
|
|
4175
|
-
(file) => fileContains(repoPath, file, SQL_EXEC_PATTERN)
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4181
|
-
|
|
4630
|
+
const { executableFiles, v21ExecutedEscalationFileSet, productionUngatedEscalationFiles } = withContentReadSignal("B6", "privilege_escalation_cleanliness", () => {
|
|
4631
|
+
const executable = implFiles.filter((file) => fileContains(repoPath, file, SQL_EXEC_PATTERN));
|
|
4632
|
+
const historicalUngated = executable.filter((file) => {
|
|
4633
|
+
if (useV39Detectors && !fileHasExecutedPrivilegeEscalation(repoPath, file)) return false;
|
|
4634
|
+
const hasEscalation = fileContains(repoPath, file, ROLE_MEMBERSHIP_GRANT_PATTERN) || fileContains(repoPath, file, SUPERUSER_ESCALATION_PATTERN);
|
|
4635
|
+
if (!hasEscalation) return false;
|
|
4636
|
+
return !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN);
|
|
4637
|
+
});
|
|
4638
|
+
const v21Executed = useV21ExecutedEscalations ? repoFiles.filter(
|
|
4639
|
+
(file) => isV39AuthoredProductionPath(file) && (fileHasV21RawSqlEscalation(repoPath, file) || isImplementationFile(file) && fileHasV21DriverEscalation(repoPath, file))
|
|
4640
|
+
) : [];
|
|
4641
|
+
const v21ExecutedSet = new Set(v21Executed);
|
|
4642
|
+
const ungated = [.../* @__PURE__ */ new Set([...historicalUngated, ...v21Executed])].filter(
|
|
4643
|
+
(file) => !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN)
|
|
4644
|
+
);
|
|
4645
|
+
const productionUngated = ungated.filter(
|
|
4646
|
+
(file) => useV39Detectors ? isV39AuthoredProductionPath(file) : !isTestOrFixturePath(file)
|
|
4647
|
+
);
|
|
4648
|
+
return {
|
|
4649
|
+
executableFiles: executable,
|
|
4650
|
+
v21ExecutedEscalationFileSet: v21ExecutedSet,
|
|
4651
|
+
productionUngatedEscalationFiles: productionUngated
|
|
4652
|
+
};
|
|
4182
4653
|
});
|
|
4183
|
-
const v21ExecutedEscalationFiles = useV21ExecutedEscalations ? repoFiles.filter(
|
|
4184
|
-
(file) => isV39AuthoredProductionPath(file) && (fileHasV21RawSqlEscalation(repoPath, file) || isImplementationFile(file) && fileHasV21DriverEscalation(repoPath, file))
|
|
4185
|
-
) : [];
|
|
4186
|
-
const v21ExecutedEscalationFileSet = new Set(v21ExecutedEscalationFiles);
|
|
4187
|
-
const ungatedEscalationFiles = [
|
|
4188
|
-
.../* @__PURE__ */ new Set([...historicalUngatedEscalationFiles, ...v21ExecutedEscalationFiles])
|
|
4189
|
-
].filter((file) => !fileContains(repoPath, file, HUMAN_GATE_MARKER_PATTERN));
|
|
4190
|
-
const productionUngatedEscalationFiles = ungatedEscalationFiles.filter(
|
|
4191
|
-
(file) => useV39Detectors ? isV39AuthoredProductionPath(file) : !isTestOrFixturePath(file)
|
|
4192
|
-
);
|
|
4193
4654
|
if (humanGateDoc) {
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
4199
|
-
|
|
4655
|
+
withContentReadSignal(
|
|
4656
|
+
"B6",
|
|
4657
|
+
"human_gate_documented",
|
|
4658
|
+
() => evidence.push(
|
|
4659
|
+
evidenceForRelative(
|
|
4660
|
+
repoPath,
|
|
4661
|
+
humanGateDoc,
|
|
4662
|
+
"artifact",
|
|
4663
|
+
"Documents privileged operations as human-executed/gated"
|
|
4664
|
+
)
|
|
4200
4665
|
)
|
|
4201
4666
|
);
|
|
4202
4667
|
}
|
|
4203
4668
|
if (gatedPrivilegeCheckFile) {
|
|
4204
|
-
|
|
4205
|
-
|
|
4206
|
-
|
|
4207
|
-
|
|
4208
|
-
|
|
4209
|
-
|
|
4669
|
+
withContentReadSignal(
|
|
4670
|
+
"B6",
|
|
4671
|
+
"fail_closed_privilege_check",
|
|
4672
|
+
() => evidence.push(
|
|
4673
|
+
evidenceForRelative(
|
|
4674
|
+
repoPath,
|
|
4675
|
+
gatedPrivilegeCheckFile,
|
|
4676
|
+
"artifact",
|
|
4677
|
+
"Fail-closed privilege-membership check before role elevation"
|
|
4678
|
+
)
|
|
4210
4679
|
)
|
|
4211
4680
|
);
|
|
4212
4681
|
}
|
|
4213
4682
|
if (killSwitchFile) {
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
|
|
4218
|
-
|
|
4219
|
-
|
|
4683
|
+
withContentReadSignal(
|
|
4684
|
+
"B6",
|
|
4685
|
+
"kill_switch_fail_safe_present",
|
|
4686
|
+
() => evidence.push(
|
|
4687
|
+
evidenceForRelative(
|
|
4688
|
+
repoPath,
|
|
4689
|
+
killSwitchFile,
|
|
4690
|
+
"artifact",
|
|
4691
|
+
"Un-overridable kill-switch / fail-safe governance toggle"
|
|
4692
|
+
)
|
|
4220
4693
|
)
|
|
4221
4694
|
);
|
|
4222
4695
|
}
|
|
4223
4696
|
if (protectedPathReviewGate) {
|
|
4224
|
-
|
|
4225
|
-
|
|
4226
|
-
|
|
4227
|
-
|
|
4228
|
-
|
|
4229
|
-
|
|
4697
|
+
withContentReadSignal(
|
|
4698
|
+
"B6",
|
|
4699
|
+
"protected_path_review_gate",
|
|
4700
|
+
() => evidence.push(
|
|
4701
|
+
evidenceForRelative(
|
|
4702
|
+
repoPath,
|
|
4703
|
+
protectedPathReviewGate,
|
|
4704
|
+
"artifact",
|
|
4705
|
+
"CODEOWNERS/required-review gate on protected paths"
|
|
4706
|
+
)
|
|
4230
4707
|
)
|
|
4231
4708
|
);
|
|
4232
4709
|
}
|
|
4233
|
-
|
|
4234
|
-
const
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4710
|
+
withContentReadSignal("B6", "privilege_escalation_cleanliness", () => {
|
|
4711
|
+
for (const file of productionUngatedEscalationFiles.slice(0, 5)) {
|
|
4712
|
+
const isV21ExecutedShape = v21ExecutedEscalationFileSet.has(file);
|
|
4713
|
+
findings.push({
|
|
4714
|
+
severity: "critical",
|
|
4715
|
+
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.",
|
|
4716
|
+
evidence: evidenceForRelative(
|
|
4717
|
+
repoPath,
|
|
4718
|
+
file,
|
|
4719
|
+
"artifact",
|
|
4720
|
+
isV21ExecutedShape ? "Ungated authored or directly executed administrative SQL statement" : "Ungated privilege-escalation statement"
|
|
4721
|
+
)
|
|
4722
|
+
});
|
|
4723
|
+
}
|
|
4724
|
+
});
|
|
4246
4725
|
if (evidence.length === 0 && findings.length === 0) {
|
|
4247
4726
|
return buildNotApplicableSignal(
|
|
4248
4727
|
"B6",
|
|
@@ -4264,7 +4743,7 @@ function collectB6PrivilegedOpsGatingEvidence(repoPath, repoFiles, useV39Detecto
|
|
|
4264
4743
|
}
|
|
4265
4744
|
)
|
|
4266
4745
|
] : [];
|
|
4267
|
-
const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 ||
|
|
4746
|
+
const hasPrivilegedOpsSurface = humanGateDoc != null || gatedPrivilegeCheckFile != null || executableFiles.length > 0 || v21ExecutedEscalationFileSet.size > 0;
|
|
4268
4747
|
return {
|
|
4269
4748
|
criterionId: "B6",
|
|
4270
4749
|
positiveEvidence: evidence,
|
|
@@ -4408,6 +4887,7 @@ var GATED_PRIVILEGE_CHECK_PATTERN = /pg_has_role\s*\(|has_role\s*\(|is not a mem
|
|
|
4408
4887
|
var SET_ROLE_PATTERN = /\bset\s+(local\s+)?role\b/i;
|
|
4409
4888
|
var REQUIRED_REVIEW_PATTERN = /required review|branch protection|protected branch/i;
|
|
4410
4889
|
var HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT = 5e3;
|
|
4890
|
+
var V23_PEM_WIDENED_HISTORY_BLOB_LIMIT = 500;
|
|
4411
4891
|
var SECRET_IDENTIFIER_KEYWORD_PATTERN = /secret|token|api[_-]?key|password|access[_-]?key/gi;
|
|
4412
4892
|
var SINGLE_WHITESPACE_PATTERN = /\s/u;
|
|
4413
4893
|
var SECRET_VALUE_BRANDED_PATTERN = /^(?:sk-[A-Za-z0-9_-]{24,}|sk-ant-api\d{2}-[A-Za-z0-9_-]{80,}|ghp_[A-Za-z0-9_]{30,}|xox[baprs]-[A-Za-z0-9-]{24,}|AKIA[0-9A-Z]{16}|vcp_[A-Za-z0-9_:-]{24,})$/;
|
|
@@ -4567,12 +5047,7 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
|
|
|
4567
5047
|
continue;
|
|
4568
5048
|
}
|
|
4569
5049
|
if (stat.size > MAX_REPOSITORY_CONTENT_BYTES) {
|
|
4570
|
-
|
|
4571
|
-
if (affectedCriteria.length > 0) {
|
|
4572
|
-
recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria, true);
|
|
4573
|
-
} else {
|
|
4574
|
-
recordContentSkip(fullPath, "too_large", true);
|
|
4575
|
-
}
|
|
5050
|
+
recordContentSkip(fullPath, "too_large", true);
|
|
4576
5051
|
continue;
|
|
4577
5052
|
}
|
|
4578
5053
|
} catch (error) {
|
|
@@ -4586,39 +5061,6 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded) {
|
|
|
4586
5061
|
}
|
|
4587
5062
|
return files.sort();
|
|
4588
5063
|
}
|
|
4589
|
-
function affectedCriteriaForUnavailablePath(file) {
|
|
4590
|
-
const affected = /* @__PURE__ */ new Set();
|
|
4591
|
-
if (isDependencyManifest(file) || isLockfile(file)) affected.add("A4");
|
|
4592
|
-
if (isCiWorkflow(file)) {
|
|
4593
|
-
affected.add("A1");
|
|
4594
|
-
affected.add("A3");
|
|
4595
|
-
affected.add("B2");
|
|
4596
|
-
affected.add("B3");
|
|
4597
|
-
}
|
|
4598
|
-
if (isDeployConfig(file)) {
|
|
4599
|
-
affected.add("A3");
|
|
4600
|
-
affected.add("B2");
|
|
4601
|
-
}
|
|
4602
|
-
if (isTestFile(file)) {
|
|
4603
|
-
affected.add("A1");
|
|
4604
|
-
affected.add("A2");
|
|
4605
|
-
}
|
|
4606
|
-
if (isImplementationFile(file)) {
|
|
4607
|
-
affected.add("A1");
|
|
4608
|
-
affected.add("A2");
|
|
4609
|
-
affected.add("A3");
|
|
4610
|
-
affected.add("B1");
|
|
4611
|
-
affected.add("B6");
|
|
4612
|
-
}
|
|
4613
|
-
if (/\.(?:md|mdx|rst|txt)$/i.test(file)) {
|
|
4614
|
-
affected.add("A5");
|
|
4615
|
-
affected.add("B4");
|
|
4616
|
-
affected.add("B5");
|
|
4617
|
-
affected.add("B6");
|
|
4618
|
-
}
|
|
4619
|
-
if (isAuditFile(file)) affected.add("B5");
|
|
4620
|
-
return [...affected];
|
|
4621
|
-
}
|
|
4622
5064
|
function directoryInventory(repoPath) {
|
|
4623
5065
|
const files = [];
|
|
4624
5066
|
const rootDevice = statSync(repoPath).dev;
|
|
@@ -4740,12 +5182,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
|
|
|
4740
5182
|
}
|
|
4741
5183
|
const repoRelativePath = relative(repoPath, fullPath);
|
|
4742
5184
|
if (size > MAX_REPOSITORY_CONTENT_BYTES) {
|
|
4743
|
-
|
|
4744
|
-
if (affectedCriteria.length > 0) {
|
|
4745
|
-
recordContentSkipForCriteria(fullPath, "too_large", affectedCriteria);
|
|
4746
|
-
} else {
|
|
4747
|
-
recordContentSkip(fullPath, "too_large");
|
|
4748
|
-
}
|
|
5185
|
+
recordContentSkip(fullPath, "too_large");
|
|
4749
5186
|
continue;
|
|
4750
5187
|
}
|
|
4751
5188
|
if (!isPotentialContentPath(repoRelativePath)) {
|
|
@@ -5187,6 +5624,9 @@ function isRecord(value) {
|
|
|
5187
5624
|
function isTestRunnerConfig(repoPath, file) {
|
|
5188
5625
|
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);
|
|
5189
5626
|
}
|
|
5627
|
+
function isRecipeTestEntry(repoPath, file) {
|
|
5628
|
+
return isRecipeFile(file) && fileContains(repoPath, file, /^\s*(?:test|tests|check)\s*:/m);
|
|
5629
|
+
}
|
|
5190
5630
|
function findCoverageConfigFiles(repoPath, repoFiles, authoredOnly = false) {
|
|
5191
5631
|
return repoFiles.filter(
|
|
5192
5632
|
(file) => (!authoredOnly || isAuthoredProductionPath(file)) && isCoverageConfig(repoPath, file)
|
|
@@ -5480,6 +5920,580 @@ function packageJsonHasCoverageTooling(repoPath, file) {
|
|
|
5480
5920
|
}
|
|
5481
5921
|
return false;
|
|
5482
5922
|
}
|
|
5923
|
+
function isV23CoverageConfig(repoPath, file) {
|
|
5924
|
+
if (/(^|\/)vitest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5925
|
+
const contents = stripJavaScriptComments(readRepoText(join(repoPath, file), "utf8"));
|
|
5926
|
+
const property = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents)?.[1]?.trim();
|
|
5927
|
+
if (!property || /^(?:false\b|null\b|undefined\b)/i.test(property)) return false;
|
|
5928
|
+
if (/^\{/.test(property) && /\benabled\s*:\s*false\b/i.test(contents)) return false;
|
|
5929
|
+
return true;
|
|
5930
|
+
}
|
|
5931
|
+
if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5932
|
+
const contents = stripJavaScriptComments(readRepoText(join(repoPath, file), "utf8"));
|
|
5933
|
+
return /(?:^|[{,])\s*["']?collectCoverage["']?\s*:\s*true\b/m.test(contents) || /(?:^|[{,])\s*["']?(?:coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:/m.test(
|
|
5934
|
+
contents
|
|
5935
|
+
);
|
|
5936
|
+
}
|
|
5937
|
+
if (/(^|\/)package\.json$/.test(file)) {
|
|
5938
|
+
return packageJsonHasStaticCoverageTooling(repoPath, file);
|
|
5939
|
+
}
|
|
5940
|
+
return isCoverageConfig(repoPath, file);
|
|
5941
|
+
}
|
|
5942
|
+
function coverageConfigurationDecision(repoPath, file) {
|
|
5943
|
+
const detectorState = isV23CoverageConfig(repoPath, file) ? "collects" : "none";
|
|
5944
|
+
if (/(^|\/)vitest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5945
|
+
const rawContents = readRepoText(join(repoPath, file), "utf8");
|
|
5946
|
+
const contents = stripJavaScriptComments(rawContents);
|
|
5947
|
+
const match = /(?:^|[{,])\s*["']?coverage["']?\s*:\s*([^,\r\n}]*)/im.exec(contents);
|
|
5948
|
+
if (!match) return null;
|
|
5949
|
+
return {
|
|
5950
|
+
path: file,
|
|
5951
|
+
source: "runner-configuration",
|
|
5952
|
+
locator: "coverage",
|
|
5953
|
+
evidence: rawContents.slice(match.index, match.index + match[0].length).trim(),
|
|
5954
|
+
detectorRecognized: true,
|
|
5955
|
+
detectorState
|
|
5956
|
+
};
|
|
5957
|
+
}
|
|
5958
|
+
if (/(^|\/)jest\.config\.[cm]?[jt]s$/.test(file)) {
|
|
5959
|
+
const rawContents = readRepoText(join(repoPath, file), "utf8");
|
|
5960
|
+
const contents = stripJavaScriptComments(rawContents);
|
|
5961
|
+
const match = /(?:^|[{,])\s*["']?(collectCoverage|coverageThreshold|coverageProvider|coverageDirectory)["']?\s*:\s*([^,\r\n}]*)/m.exec(
|
|
5962
|
+
contents
|
|
5963
|
+
);
|
|
5964
|
+
if (!match) return null;
|
|
5965
|
+
return {
|
|
5966
|
+
path: file,
|
|
5967
|
+
source: "runner-configuration",
|
|
5968
|
+
locator: match[1] ?? "coverage",
|
|
5969
|
+
evidence: rawContents.slice(match.index, match.index + match[0].length).trim(),
|
|
5970
|
+
detectorRecognized: true,
|
|
5971
|
+
detectorState
|
|
5972
|
+
};
|
|
5973
|
+
}
|
|
5974
|
+
if (detectorState === "none") return null;
|
|
5975
|
+
return {
|
|
5976
|
+
path: file,
|
|
5977
|
+
source: "runner-configuration",
|
|
5978
|
+
locator: "coverage",
|
|
5979
|
+
evidence: file,
|
|
5980
|
+
detectorRecognized: true,
|
|
5981
|
+
detectorState
|
|
5982
|
+
};
|
|
5983
|
+
}
|
|
5984
|
+
function packageJsonHasStaticCoverageTooling(repoPath, file) {
|
|
5985
|
+
const fullPath = join(repoPath, file);
|
|
5986
|
+
if (!isRegularFile(fullPath)) return false;
|
|
5987
|
+
const parsed = parseJsonObject(readRepoText(fullPath, "utf8"));
|
|
5988
|
+
if (!parsed) return false;
|
|
5989
|
+
return ["nyc", "c8", "istanbul"].some((key) => {
|
|
5990
|
+
const config = parsed[key];
|
|
5991
|
+
return typeof config === "object" && config !== null && !Array.isArray(config);
|
|
5992
|
+
});
|
|
5993
|
+
}
|
|
5994
|
+
function analyzeCoverageConfiguration(repoPath, repoFiles, authoredOnly = false) {
|
|
5995
|
+
const eligibleFiles = repoFiles.filter(
|
|
5996
|
+
(file) => !authoredOnly || isAuthoredProductionPath(file)
|
|
5997
|
+
);
|
|
5998
|
+
const eligibleSet = new Set(eligibleFiles);
|
|
5999
|
+
const coverageFiles = /* @__PURE__ */ new Set();
|
|
6000
|
+
const unresolvedFiles = /* @__PURE__ */ new Set();
|
|
6001
|
+
const ciCommandsByFile = /* @__PURE__ */ new Map();
|
|
6002
|
+
const detectorDecisions = [];
|
|
6003
|
+
let hasCoverageCommand = false;
|
|
6004
|
+
for (const file of eligibleFiles.filter(isCiWorkflow)) {
|
|
6005
|
+
const commands = extractCiRunCommands(readRepoText(join(repoPath, file), "utf8"));
|
|
6006
|
+
ciCommandsByFile.set(file, commands);
|
|
6007
|
+
const commandDecisions = commands.map(
|
|
6008
|
+
(command, index) => coverageDetectorDecision(repoPath, eligibleSet, {
|
|
6009
|
+
path: file,
|
|
6010
|
+
source: "ci-run",
|
|
6011
|
+
locator: String(index),
|
|
6012
|
+
command
|
|
6013
|
+
})
|
|
6014
|
+
);
|
|
6015
|
+
detectorDecisions.push(...commandDecisions);
|
|
6016
|
+
const state = combineCoverageCommandStates(
|
|
6017
|
+
commandDecisions.map((decision) => decision.detectorState)
|
|
6018
|
+
);
|
|
6019
|
+
if (state === "collects") {
|
|
6020
|
+
coverageFiles.add(file);
|
|
6021
|
+
hasCoverageCommand = true;
|
|
6022
|
+
}
|
|
6023
|
+
if (state === "unresolved") unresolvedFiles.add(file);
|
|
6024
|
+
}
|
|
6025
|
+
const ciCommands = [...ciCommandsByFile.values()].flat();
|
|
6026
|
+
for (const file of eligibleFiles) {
|
|
6027
|
+
if (isCiWorkflow(file) || isRecipeFile(file)) continue;
|
|
6028
|
+
if (/(^|\/)package\.json$/.test(file)) {
|
|
6029
|
+
if (packageJsonHasStaticCoverageTooling(repoPath, file)) coverageFiles.add(file);
|
|
6030
|
+
const packageState = packageScriptCoverageState(
|
|
6031
|
+
repoPath,
|
|
6032
|
+
eligibleSet,
|
|
6033
|
+
file,
|
|
6034
|
+
file === "package.json" ? ciCommands : [],
|
|
6035
|
+
detectorDecisions
|
|
6036
|
+
);
|
|
6037
|
+
if (packageState === "collects") {
|
|
6038
|
+
coverageFiles.add(file);
|
|
6039
|
+
hasCoverageCommand = true;
|
|
6040
|
+
}
|
|
6041
|
+
if (packageState === "unresolved") unresolvedFiles.add(file);
|
|
6042
|
+
continue;
|
|
6043
|
+
}
|
|
6044
|
+
const configurationDecision = coverageConfigurationDecision(repoPath, file);
|
|
6045
|
+
if (configurationDecision) detectorDecisions.push(configurationDecision);
|
|
6046
|
+
if (configurationDecision?.detectorState === "collects") coverageFiles.add(file);
|
|
6047
|
+
}
|
|
6048
|
+
for (const file of eligibleFiles.filter(isRecipeFile)) {
|
|
6049
|
+
const state = recipeCoverageState(
|
|
6050
|
+
repoPath,
|
|
6051
|
+
eligibleSet,
|
|
6052
|
+
file,
|
|
6053
|
+
ciCommands,
|
|
6054
|
+
detectorDecisions
|
|
6055
|
+
);
|
|
6056
|
+
if (state === "collects") {
|
|
6057
|
+
coverageFiles.add(file);
|
|
6058
|
+
hasCoverageCommand = true;
|
|
6059
|
+
}
|
|
6060
|
+
if (state === "unresolved") unresolvedFiles.add(file);
|
|
6061
|
+
}
|
|
6062
|
+
for (const file of coverageFiles) unresolvedFiles.delete(file);
|
|
6063
|
+
return {
|
|
6064
|
+
coverageFiles: [...coverageFiles].sort(),
|
|
6065
|
+
unresolvedFiles: [...unresolvedFiles].sort(),
|
|
6066
|
+
hasCoverageCommand,
|
|
6067
|
+
detectorDecisions
|
|
6068
|
+
};
|
|
6069
|
+
}
|
|
6070
|
+
function stripJavaScriptComments(contents) {
|
|
6071
|
+
let output = "";
|
|
6072
|
+
let quote = null;
|
|
6073
|
+
let escaped = false;
|
|
6074
|
+
let lineComment = false;
|
|
6075
|
+
let blockComment = false;
|
|
6076
|
+
for (let index = 0; index < contents.length; index += 1) {
|
|
6077
|
+
const character = contents[index] ?? "";
|
|
6078
|
+
const next = contents[index + 1] ?? "";
|
|
6079
|
+
if (lineComment) {
|
|
6080
|
+
if (character === "\n") {
|
|
6081
|
+
lineComment = false;
|
|
6082
|
+
output += "\n";
|
|
6083
|
+
} else {
|
|
6084
|
+
output += " ";
|
|
6085
|
+
}
|
|
6086
|
+
continue;
|
|
6087
|
+
}
|
|
6088
|
+
if (blockComment) {
|
|
6089
|
+
if (character === "*" && next === "/") {
|
|
6090
|
+
output += " ";
|
|
6091
|
+
index += 1;
|
|
6092
|
+
blockComment = false;
|
|
6093
|
+
} else {
|
|
6094
|
+
output += character === "\n" ? "\n" : " ";
|
|
6095
|
+
}
|
|
6096
|
+
continue;
|
|
6097
|
+
}
|
|
6098
|
+
if (escaped) {
|
|
6099
|
+
output += character;
|
|
6100
|
+
escaped = false;
|
|
6101
|
+
continue;
|
|
6102
|
+
}
|
|
6103
|
+
if (quote) {
|
|
6104
|
+
output += character;
|
|
6105
|
+
if (character === "\\") escaped = true;
|
|
6106
|
+
else if (character === quote) quote = null;
|
|
6107
|
+
continue;
|
|
6108
|
+
}
|
|
6109
|
+
if (character === "'" || character === '"' || character === "`") {
|
|
6110
|
+
quote = character;
|
|
6111
|
+
output += character;
|
|
6112
|
+
continue;
|
|
6113
|
+
}
|
|
6114
|
+
if (character === "/" && next === "/") {
|
|
6115
|
+
output += " ";
|
|
6116
|
+
index += 1;
|
|
6117
|
+
lineComment = true;
|
|
6118
|
+
continue;
|
|
6119
|
+
}
|
|
6120
|
+
if (character === "/" && next === "*") {
|
|
6121
|
+
output += " ";
|
|
6122
|
+
index += 1;
|
|
6123
|
+
blockComment = true;
|
|
6124
|
+
continue;
|
|
6125
|
+
}
|
|
6126
|
+
output += character;
|
|
6127
|
+
}
|
|
6128
|
+
return output;
|
|
6129
|
+
}
|
|
6130
|
+
function packageScriptCoverageState(repoPath, repoFiles, packageJson, ciCommands, detectorDecisions) {
|
|
6131
|
+
const scripts = readPackageScripts(join(repoPath, packageJson));
|
|
6132
|
+
if (scripts.size === 0) return "none";
|
|
6133
|
+
const roots = /* @__PURE__ */ new Set();
|
|
6134
|
+
let unresolved = false;
|
|
6135
|
+
if (scripts.has("test")) roots.add("test");
|
|
6136
|
+
for (const command of ciCommands) {
|
|
6137
|
+
const references = packageScriptReferences(command, scripts);
|
|
6138
|
+
for (const name of references.names) roots.add(name);
|
|
6139
|
+
if (references.dynamic && [...scripts.values()].some(
|
|
6140
|
+
(candidate) => commandInvokesCoverageTool(candidate) || containsPotentialCoverageFlag(candidate)
|
|
6141
|
+
)) {
|
|
6142
|
+
unresolved = true;
|
|
6143
|
+
}
|
|
6144
|
+
}
|
|
6145
|
+
const queue = [...roots];
|
|
6146
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6147
|
+
while (queue.length > 0) {
|
|
6148
|
+
const name = queue.shift();
|
|
6149
|
+
if (!name || visited.has(name)) continue;
|
|
6150
|
+
const command = scripts.get(name);
|
|
6151
|
+
if (command === void 0) continue;
|
|
6152
|
+
visited.add(name);
|
|
6153
|
+
const decision = coverageDetectorDecision(repoPath, repoFiles, {
|
|
6154
|
+
path: packageJson,
|
|
6155
|
+
source: "package-script",
|
|
6156
|
+
locator: name,
|
|
6157
|
+
command
|
|
6158
|
+
});
|
|
6159
|
+
detectorDecisions.push(decision);
|
|
6160
|
+
const state = decision.detectorState;
|
|
6161
|
+
if (state === "collects") return "collects";
|
|
6162
|
+
if (state === "unresolved") unresolved = true;
|
|
6163
|
+
const references = packageScriptReferences(command, scripts);
|
|
6164
|
+
for (const referenced of references.names) {
|
|
6165
|
+
if (!visited.has(referenced)) queue.push(referenced);
|
|
6166
|
+
}
|
|
6167
|
+
for (const lifecycle of [`pre${name}`, `post${name}`]) {
|
|
6168
|
+
if (scripts.has(lifecycle) && !visited.has(lifecycle)) queue.push(lifecycle);
|
|
6169
|
+
}
|
|
6170
|
+
if (references.dynamic && [...scripts.values()].some(
|
|
6171
|
+
(candidate) => commandInvokesCoverageTool(candidate) || containsPotentialCoverageFlag(candidate)
|
|
6172
|
+
)) {
|
|
6173
|
+
unresolved = true;
|
|
6174
|
+
}
|
|
6175
|
+
}
|
|
6176
|
+
return unresolved ? "unresolved" : "none";
|
|
6177
|
+
}
|
|
6178
|
+
function packageScriptReferences(command, scripts) {
|
|
6179
|
+
const names = /* @__PURE__ */ new Set();
|
|
6180
|
+
let dynamic = false;
|
|
6181
|
+
for (const segment of splitShellCommandSegments(command)) {
|
|
6182
|
+
for (const match of segment.matchAll(
|
|
6183
|
+
/\bpnpm\s+run\s+\/((?:\\.|[^/])+)\/([dgimsuvy]*)/giu
|
|
6184
|
+
)) {
|
|
6185
|
+
const source = match[1];
|
|
6186
|
+
if (!source) continue;
|
|
6187
|
+
try {
|
|
6188
|
+
const flags = (match[2] ?? "").replace(/[dgy]/g, "");
|
|
6189
|
+
const selector = new RegExp(source, flags);
|
|
6190
|
+
for (const name of scripts.keys()) {
|
|
6191
|
+
if (selector.test(name)) names.add(name);
|
|
6192
|
+
selector.lastIndex = 0;
|
|
6193
|
+
}
|
|
6194
|
+
} catch {
|
|
6195
|
+
dynamic = true;
|
|
6196
|
+
}
|
|
6197
|
+
}
|
|
6198
|
+
for (const match of segment.matchAll(
|
|
6199
|
+
/\b(?:npm|pnpm|yarn)\s+(?:run\s+)?([A-Za-z0-9][A-Za-z0-9:._-]*)/giu
|
|
6200
|
+
)) {
|
|
6201
|
+
const name = match[1];
|
|
6202
|
+
if (name && scripts.has(name)) names.add(name);
|
|
6203
|
+
}
|
|
6204
|
+
if (/\b(?:npm|pnpm|yarn)\s+run\s+(?:\$|["']\$|`)/u.test(segment)) dynamic = true;
|
|
6205
|
+
}
|
|
6206
|
+
return { names: [...names], dynamic };
|
|
6207
|
+
}
|
|
6208
|
+
function extractCiRunCommands(contents) {
|
|
6209
|
+
const lines = contents.split(/\r?\n/);
|
|
6210
|
+
const commands = [];
|
|
6211
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
6212
|
+
const line = lines[index] ?? "";
|
|
6213
|
+
const match = /^(\s*)(?:-\s*)?run\s*:\s*(.*)$/u.exec(line);
|
|
6214
|
+
if (!match) continue;
|
|
6215
|
+
const indentation = match[1]?.length ?? 0;
|
|
6216
|
+
const value = match[2]?.trim() ?? "";
|
|
6217
|
+
if (value === "|" || value === ">" || value.startsWith("|-") || value.startsWith(">-")) {
|
|
6218
|
+
const block = [];
|
|
6219
|
+
for (index += 1; index < lines.length; index += 1) {
|
|
6220
|
+
const nested = lines[index] ?? "";
|
|
6221
|
+
if (nested.trim().length === 0) {
|
|
6222
|
+
block.push("");
|
|
6223
|
+
continue;
|
|
6224
|
+
}
|
|
6225
|
+
const nestedIndentation = /^\s*/u.exec(nested)?.[0].length ?? 0;
|
|
6226
|
+
if (nestedIndentation <= indentation) {
|
|
6227
|
+
index -= 1;
|
|
6228
|
+
break;
|
|
6229
|
+
}
|
|
6230
|
+
block.push(nested.slice(Math.min(nested.length, indentation + 2)));
|
|
6231
|
+
}
|
|
6232
|
+
commands.push(block.join(value.startsWith(">") ? " " : "\n"));
|
|
6233
|
+
continue;
|
|
6234
|
+
}
|
|
6235
|
+
if (value.length === 0) continue;
|
|
6236
|
+
const quoted = /^(?:"([\s\S]*)"|'([\s\S]*)')$/u.exec(value);
|
|
6237
|
+
commands.push(quoted ? quoted[1] ?? quoted[2] ?? "" : value);
|
|
6238
|
+
}
|
|
6239
|
+
return commands;
|
|
6240
|
+
}
|
|
6241
|
+
function isRecipeFile(file) {
|
|
6242
|
+
return /(^|\/)(?:Makefile|makefile|GNUmakefile|[Jj]ustfile)$/.test(file);
|
|
6243
|
+
}
|
|
6244
|
+
function recipeCoverageState(repoPath, repoFiles, file, ciCommands, detectorDecisions) {
|
|
6245
|
+
const targets = readRecipeTargets(readRepoText(join(repoPath, file), "utf8"));
|
|
6246
|
+
const roots = new Set(["test", "tests", "check"].filter((target) => targets.has(target)));
|
|
6247
|
+
const runner = /[Jj]ustfile$/.test(file) ? "just" : "make";
|
|
6248
|
+
for (const command of ciCommands) {
|
|
6249
|
+
for (const match of command.matchAll(new RegExp(`\\b${runner}\\s+([A-Za-z0-9._-]+)`, "gu"))) {
|
|
6250
|
+
const target = match[1];
|
|
6251
|
+
if (target && targets.has(target)) roots.add(target);
|
|
6252
|
+
}
|
|
6253
|
+
}
|
|
6254
|
+
const queue = [...roots];
|
|
6255
|
+
const visited = /* @__PURE__ */ new Set();
|
|
6256
|
+
let unresolved = false;
|
|
6257
|
+
while (queue.length > 0) {
|
|
6258
|
+
const name = queue.shift();
|
|
6259
|
+
if (!name || visited.has(name)) continue;
|
|
6260
|
+
const target = targets.get(name);
|
|
6261
|
+
if (!target) continue;
|
|
6262
|
+
visited.add(name);
|
|
6263
|
+
const commandDecisions = target.commands.map(
|
|
6264
|
+
(command, index) => coverageDetectorDecision(repoPath, repoFiles, {
|
|
6265
|
+
path: file,
|
|
6266
|
+
source: "recipe-command",
|
|
6267
|
+
locator: `${name}:${index}`,
|
|
6268
|
+
command
|
|
6269
|
+
})
|
|
6270
|
+
);
|
|
6271
|
+
detectorDecisions.push(...commandDecisions);
|
|
6272
|
+
const state = combineCoverageCommandStates(
|
|
6273
|
+
commandDecisions.map((decision) => decision.detectorState)
|
|
6274
|
+
);
|
|
6275
|
+
if (state === "collects") return "collects";
|
|
6276
|
+
if (state === "unresolved") unresolved = true;
|
|
6277
|
+
for (const dependency of target.dependencies) {
|
|
6278
|
+
if (targets.has(dependency) && !visited.has(dependency)) queue.push(dependency);
|
|
6279
|
+
}
|
|
6280
|
+
for (const command of target.commands) {
|
|
6281
|
+
const pattern = new RegExp(`(?:\\$\\(MAKE\\)|\\b${runner})\\s+([A-Za-z0-9._-]+)`, "gu");
|
|
6282
|
+
for (const match of command.matchAll(pattern)) {
|
|
6283
|
+
const referenced = match[1];
|
|
6284
|
+
if (referenced && targets.has(referenced) && !visited.has(referenced)) queue.push(referenced);
|
|
6285
|
+
}
|
|
6286
|
+
}
|
|
6287
|
+
}
|
|
6288
|
+
return unresolved ? "unresolved" : "none";
|
|
6289
|
+
}
|
|
6290
|
+
function readRecipeTargets(contents) {
|
|
6291
|
+
const targets = /* @__PURE__ */ new Map();
|
|
6292
|
+
let active = null;
|
|
6293
|
+
for (const line of contents.split(/\r?\n/)) {
|
|
6294
|
+
const header = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*:\s*([^#]*)/u.exec(line);
|
|
6295
|
+
if (header) {
|
|
6296
|
+
const name = header[1];
|
|
6297
|
+
if (!name) continue;
|
|
6298
|
+
active = {
|
|
6299
|
+
dependencies: (header[2] ?? "").trim().split(/\s+/u).filter(Boolean),
|
|
6300
|
+
commands: []
|
|
6301
|
+
};
|
|
6302
|
+
targets.set(name, active);
|
|
6303
|
+
continue;
|
|
6304
|
+
}
|
|
6305
|
+
if (active && /^\s+/u.test(line) && line.trim().length > 0) {
|
|
6306
|
+
active.commands.push(line.trim().replace(/^[@+-]+/u, ""));
|
|
6307
|
+
continue;
|
|
6308
|
+
}
|
|
6309
|
+
if (line.trim().length > 0 && !line.trimStart().startsWith("#")) active = null;
|
|
6310
|
+
}
|
|
6311
|
+
return targets;
|
|
6312
|
+
}
|
|
6313
|
+
function coverageCommandEvaluation(repoPath, repoFiles, command, depth = 0) {
|
|
6314
|
+
if (depth > 4) {
|
|
6315
|
+
const recognized = containsPotentialCoverageFlag(command);
|
|
6316
|
+
return { recognized, state: recognized ? "unresolved" : "none" };
|
|
6317
|
+
}
|
|
6318
|
+
const normalized = normalizeCoverageCommandPrefixes(command);
|
|
6319
|
+
const segments = normalized.splitShellOperators ? splitShellCommandSegments(normalized.segment) : [normalized.segment];
|
|
6320
|
+
const evaluations = segments.map((rawSegment) => {
|
|
6321
|
+
const candidate = normalizeCoverageCommandPrefixes(rawSegment);
|
|
6322
|
+
if (commandInvokesCoverageTool(candidate.segment)) {
|
|
6323
|
+
return { recognized: true, state: "collects" };
|
|
6324
|
+
}
|
|
6325
|
+
const words = shellWords(candidate.segment);
|
|
6326
|
+
if (words.length === 0) return { recognized: false, state: "none" };
|
|
6327
|
+
const direct = coverageFlagCommandState(words);
|
|
6328
|
+
if (direct !== "none") return { recognized: true, state: direct };
|
|
6329
|
+
const hasCoverageSyntax = containsPotentialCoverageFlag(candidate.segment);
|
|
6330
|
+
if (isRecognizedCoverageFlagRunner(words)) {
|
|
6331
|
+
return { recognized: hasCoverageSyntax, state: "none" };
|
|
6332
|
+
}
|
|
6333
|
+
const wrapper = reachableShellWrapper(words, repoFiles);
|
|
6334
|
+
if (wrapper) {
|
|
6335
|
+
return coverageCommandEvaluation(
|
|
6336
|
+
repoPath,
|
|
6337
|
+
repoFiles,
|
|
6338
|
+
readRepoText(join(repoPath, wrapper), "utf8"),
|
|
6339
|
+
depth + 1
|
|
6340
|
+
);
|
|
6341
|
+
}
|
|
6342
|
+
const executable = commandExecutable(words);
|
|
6343
|
+
const potentialFlagState = flagState(
|
|
6344
|
+
words.slice(1),
|
|
6345
|
+
["--experimental-test-coverage", "--collectcoverage", "--collect-coverage", "--coverage", "--cov"],
|
|
6346
|
+
[
|
|
6347
|
+
"--no-experimental-test-coverage",
|
|
6348
|
+
"--no-collectcoverage",
|
|
6349
|
+
"--no-collect-coverage",
|
|
6350
|
+
"--no-coverage",
|
|
6351
|
+
"--no-cov"
|
|
6352
|
+
]
|
|
6353
|
+
);
|
|
6354
|
+
if (hasCoverageSyntax && potentialFlagState !== "none" && executable && /(?:test|spec|runner)/i.test(executable) && !/(?:docs?|map|report|upload|codecov|coveralls)/i.test(executable)) {
|
|
6355
|
+
return { recognized: true, state: "unresolved" };
|
|
6356
|
+
}
|
|
6357
|
+
return { recognized: false, state: "none" };
|
|
6358
|
+
});
|
|
6359
|
+
return {
|
|
6360
|
+
recognized: evaluations.some((evaluation) => evaluation.recognized),
|
|
6361
|
+
state: combineCoverageCommandStates(
|
|
6362
|
+
evaluations.map((evaluation) => evaluation.state)
|
|
6363
|
+
)
|
|
6364
|
+
};
|
|
6365
|
+
}
|
|
6366
|
+
function coverageDetectorDecision(repoPath, repoFiles, input) {
|
|
6367
|
+
const evaluation = coverageCommandEvaluation(repoPath, repoFiles, input.command);
|
|
6368
|
+
return {
|
|
6369
|
+
path: input.path,
|
|
6370
|
+
source: input.source,
|
|
6371
|
+
locator: input.locator,
|
|
6372
|
+
evidence: input.command,
|
|
6373
|
+
detectorRecognized: evaluation.recognized,
|
|
6374
|
+
detectorState: evaluation.state
|
|
6375
|
+
};
|
|
6376
|
+
}
|
|
6377
|
+
function combineCoverageCommandStates(states) {
|
|
6378
|
+
if (states.includes("collects")) return "collects";
|
|
6379
|
+
return states.includes("unresolved") ? "unresolved" : "none";
|
|
6380
|
+
}
|
|
6381
|
+
function shellWords(segment) {
|
|
6382
|
+
const words = [];
|
|
6383
|
+
let offset = 0;
|
|
6384
|
+
while (offset < segment.length) {
|
|
6385
|
+
const scanned = scanShellWord(segment, offset);
|
|
6386
|
+
if (!scanned) break;
|
|
6387
|
+
words.push(scanned.word);
|
|
6388
|
+
if (scanned.end <= offset) break;
|
|
6389
|
+
offset = scanned.end;
|
|
6390
|
+
}
|
|
6391
|
+
return words;
|
|
6392
|
+
}
|
|
6393
|
+
function commandExecutable(words) {
|
|
6394
|
+
const normalized = unwrapPackageManagerExecutable(words);
|
|
6395
|
+
const executable = normalized[0];
|
|
6396
|
+
return executable ? basename(executable).replace(/\.cmd$/i, "").toLowerCase() : void 0;
|
|
6397
|
+
}
|
|
6398
|
+
function unwrapPackageManagerExecutable(words) {
|
|
6399
|
+
if (words.length === 0) return [];
|
|
6400
|
+
const first = basename(words[0] ?? "").toLowerCase();
|
|
6401
|
+
if (first === "pnpm" || first === "yarn") {
|
|
6402
|
+
const second = words[1]?.toLowerCase();
|
|
6403
|
+
return words.slice(second === "exec" || second === "dlx" ? 2 : 1);
|
|
6404
|
+
}
|
|
6405
|
+
if (first === "npx") return words.slice(1).filter((word) => !/^--(?:yes|no-install)$/u.test(word));
|
|
6406
|
+
if (first === "npm" && words[1]?.toLowerCase() === "exec") {
|
|
6407
|
+
let offset = 2;
|
|
6408
|
+
while (/^--(?:yes|no-install)$/u.test(words[offset] ?? "")) offset += 1;
|
|
6409
|
+
if (words[offset] === "--") offset += 1;
|
|
6410
|
+
return words.slice(offset);
|
|
6411
|
+
}
|
|
6412
|
+
return [...words];
|
|
6413
|
+
}
|
|
6414
|
+
function coverageFlagCommandState(words) {
|
|
6415
|
+
const command = unwrapPackageManagerExecutable(words);
|
|
6416
|
+
const executable = basename(command[0] ?? "").replace(/\.cmd$/i, "").toLowerCase();
|
|
6417
|
+
const args = command.slice(1);
|
|
6418
|
+
if (!executable) return "none";
|
|
6419
|
+
if (executable === "node") {
|
|
6420
|
+
if (!args.includes("--test")) return "none";
|
|
6421
|
+
return flagState(args, ["--experimental-test-coverage"], ["--no-experimental-test-coverage"]);
|
|
6422
|
+
}
|
|
6423
|
+
if (executable === "pytest" || executable === "py.test") {
|
|
6424
|
+
return flagState(args, ["--cov"], ["--no-cov"]);
|
|
6425
|
+
}
|
|
6426
|
+
if (executable === "bun" || executable === "deno") {
|
|
6427
|
+
if (args[0]?.toLowerCase() !== "test") return "none";
|
|
6428
|
+
return flagState(args.slice(1), ["--coverage"], ["--no-coverage"]);
|
|
6429
|
+
}
|
|
6430
|
+
if (executable === "jest") {
|
|
6431
|
+
return flagState(
|
|
6432
|
+
args,
|
|
6433
|
+
["--coverage", "--collectcoverage", "--collect-coverage"],
|
|
6434
|
+
["--no-coverage", "--no-collectcoverage", "--no-collect-coverage"]
|
|
6435
|
+
);
|
|
6436
|
+
}
|
|
6437
|
+
if (executable === "vitest" || executable === "bnt") {
|
|
6438
|
+
return flagState(args, ["--coverage"], ["--no-coverage"]);
|
|
6439
|
+
}
|
|
6440
|
+
return "none";
|
|
6441
|
+
}
|
|
6442
|
+
function isRecognizedCoverageFlagRunner(words) {
|
|
6443
|
+
const command = unwrapPackageManagerExecutable(words);
|
|
6444
|
+
const executable = basename(command[0] ?? "").replace(/\.cmd$/i, "").toLowerCase();
|
|
6445
|
+
if (["bnt", "vitest", "jest", "pytest", "py.test"].includes(executable)) return true;
|
|
6446
|
+
if (executable === "node") return command.slice(1).includes("--test");
|
|
6447
|
+
return (executable === "bun" || executable === "deno") && command[1]?.toLowerCase() === "test";
|
|
6448
|
+
}
|
|
6449
|
+
function flagState(args, enablingNames, disablingNames) {
|
|
6450
|
+
let enabled = false;
|
|
6451
|
+
let disabled = false;
|
|
6452
|
+
let unresolved = false;
|
|
6453
|
+
const enabling = new Set(enablingNames.map((name) => name.toLowerCase()));
|
|
6454
|
+
const disabling = new Set(disablingNames.map((name) => name.toLowerCase()));
|
|
6455
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
6456
|
+
const raw = args[index] ?? "";
|
|
6457
|
+
const lower = raw.toLowerCase();
|
|
6458
|
+
if (disabling.has(lower)) {
|
|
6459
|
+
disabled = true;
|
|
6460
|
+
continue;
|
|
6461
|
+
}
|
|
6462
|
+
const [name, assigned] = lower.split("=", 2);
|
|
6463
|
+
if (!name || !enabling.has(name)) continue;
|
|
6464
|
+
if (assigned !== void 0) {
|
|
6465
|
+
if (assigned === "false" || assigned === "0" || assigned === "off") disabled = true;
|
|
6466
|
+
else if (/\$|\{|\}/u.test(assigned)) unresolved = true;
|
|
6467
|
+
else enabled = true;
|
|
6468
|
+
continue;
|
|
6469
|
+
}
|
|
6470
|
+
const next = args[index + 1]?.toLowerCase();
|
|
6471
|
+
if (next === "false" || next === "0" || next === "off") {
|
|
6472
|
+
disabled = true;
|
|
6473
|
+
index += 1;
|
|
6474
|
+
} else if (next && /\$|\{|\}/u.test(next)) {
|
|
6475
|
+
unresolved = true;
|
|
6476
|
+
index += 1;
|
|
6477
|
+
} else {
|
|
6478
|
+
enabled = true;
|
|
6479
|
+
}
|
|
6480
|
+
}
|
|
6481
|
+
if (unresolved || enabled && disabled) return "unresolved";
|
|
6482
|
+
return enabled && !disabled ? "collects" : "none";
|
|
6483
|
+
}
|
|
6484
|
+
function containsPotentialCoverageFlag(command) {
|
|
6485
|
+
return /(?:^|\s)--(?:experimental-test-coverage|collect-?coverage|no-coverage|coverage|no-cov|cov)(?:[=\s]|$)/iu.test(
|
|
6486
|
+
command
|
|
6487
|
+
);
|
|
6488
|
+
}
|
|
6489
|
+
function reachableShellWrapper(words, repoFiles) {
|
|
6490
|
+
const executable = basename(words[0] ?? "").toLowerCase();
|
|
6491
|
+
const candidate = /^(?:ba|da|k|z)?sh$/u.test(executable) ? words[1] : words[0];
|
|
6492
|
+
if (!candidate || !/(?:^|\/)\.?[^\s]*\.sh$/i.test(candidate)) return void 0;
|
|
6493
|
+
const normalized = candidate.replace(/^\.\//u, "");
|
|
6494
|
+
if (normalized.startsWith("/") || normalized.split("/").includes("..")) return void 0;
|
|
6495
|
+
return repoFiles.has(normalized) ? normalized : void 0;
|
|
6496
|
+
}
|
|
5483
6497
|
function commandInvokesCoverageTool(command) {
|
|
5484
6498
|
return normalizedCommandInvokesCoverageTool(normalizeCoverageCommandPrefixes(command), 0);
|
|
5485
6499
|
}
|
|
@@ -6094,7 +7108,69 @@ function fileContains(repoPath, file, pattern) {
|
|
|
6094
7108
|
if (!isRegularFile(fullPath)) return false;
|
|
6095
7109
|
return pattern.test(readRepoText(fullPath, "utf8"));
|
|
6096
7110
|
}
|
|
6097
|
-
|
|
7111
|
+
var PEM_PRIVATE_KEY_ASSIGNMENT_PATTERN = /(?:["'])?([A-Za-z_$][\w$.-]*)(?:["'])?\s*[:=]\s*(?:\|[-+0-9]*\s*\r?\n[ \t]*)?["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\2-----/g;
|
|
7112
|
+
var BARE_PEM_PRIVATE_KEY_PATTERN = /["'`]?-----BEGIN((?: (?:RSA|DSA|EC|OPENSSH|ENCRYPTED))? PRIVATE KEY)-----([\s\S]{0,32768}?)-----END\1-----/g;
|
|
7113
|
+
var BARE_PEM_KEY_FILE_PATTERN = /(?:^|\/)id_(?:rsa|dsa|ecdsa|ed25519)$|\.(?:pem|key)$/i;
|
|
7114
|
+
function isBarePemKeyFile(file) {
|
|
7115
|
+
return BARE_PEM_KEY_FILE_PATTERN.test(file);
|
|
7116
|
+
}
|
|
7117
|
+
var DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN = /(?<![A-Za-z0-9])(?:dev|development|local|localhost|self[-_]?signed|snakeoil|insecure|dummy|sample|demo|placeholder|fixture|generated)(?![A-Za-z0-9])/i;
|
|
7118
|
+
var ROTATED_OR_REVOKED_KEY_CONTEXT_PATTERN = /(?<![A-Za-z0-9])(?:rotated|revoked|expired|deprecated|compromised|invalidated|superseded)(?![A-Za-z0-9])|do[-_ ]not[-_ ]use|no longer valid/i;
|
|
7119
|
+
function isPlausiblePemKeyBody(rawBody) {
|
|
7120
|
+
const normalized = rawBody.replace(/\\[nr]|[\r\n\s]/g, "");
|
|
7121
|
+
if (normalized.length < 40) return false;
|
|
7122
|
+
if (!/^[A-Za-z0-9+/=]+$/.test(normalized)) return false;
|
|
7123
|
+
return !isPlaceholderSecretValue(normalized);
|
|
7124
|
+
}
|
|
7125
|
+
function scanForPemPrivateKeyMatch(pattern, contents, lines, hasIdentifierGroup) {
|
|
7126
|
+
pattern.lastIndex = 0;
|
|
7127
|
+
let match;
|
|
7128
|
+
while ((match = pattern.exec(contents)) !== null) {
|
|
7129
|
+
const full = match[0];
|
|
7130
|
+
const identifier = hasIdentifierGroup ? match[1] ?? "" : "";
|
|
7131
|
+
const body = (hasIdentifierGroup ? match[3] : match[2]) ?? "";
|
|
7132
|
+
const line = contents.slice(0, match.index).split("\n").length;
|
|
7133
|
+
const contextLines = lines.slice(Math.max(0, line - 5), line + 4).join("\n");
|
|
7134
|
+
const isDevOrSelfSigned = DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN.test(
|
|
7135
|
+
`${identifier}
|
|
7136
|
+
${contextLines}`
|
|
7137
|
+
);
|
|
7138
|
+
const isRotatedOrRevoked = ROTATED_OR_REVOKED_KEY_CONTEXT_PATTERN.test(contextLines);
|
|
7139
|
+
if (isPlausiblePemKeyBody(body) && !isDevOrSelfSigned && !isRotatedOrRevoked) {
|
|
7140
|
+
const value = full.slice(full.indexOf("-----BEGIN"));
|
|
7141
|
+
const characterClasses = [
|
|
7142
|
+
/[a-z]/.test(value) ? "lower" : "",
|
|
7143
|
+
/[A-Z]/.test(value) ? "upper" : "",
|
|
7144
|
+
/\d/.test(value) ? "digit" : "",
|
|
7145
|
+
/[^A-Za-z0-9]/.test(value) ? "symbol" : ""
|
|
7146
|
+
].filter(Boolean).join("+");
|
|
7147
|
+
return {
|
|
7148
|
+
identifier,
|
|
7149
|
+
line,
|
|
7150
|
+
valueLength: value.length,
|
|
7151
|
+
characterClasses: characterClasses || "other",
|
|
7152
|
+
valueFingerprint: createHash2("sha256").update(value).digest("hex"),
|
|
7153
|
+
kind: "pem_private_key"
|
|
7154
|
+
};
|
|
7155
|
+
}
|
|
7156
|
+
}
|
|
7157
|
+
return null;
|
|
7158
|
+
}
|
|
7159
|
+
function findPemPrivateKeyAssignment(contents, file) {
|
|
7160
|
+
if (!contents.includes("-----BEGIN")) return null;
|
|
7161
|
+
if (DEV_OR_SELF_SIGNED_KEY_CONTEXT_PATTERN.test(file)) return null;
|
|
7162
|
+
const lines = contents.split("\n");
|
|
7163
|
+
const assignmentMatch = scanForPemPrivateKeyMatch(
|
|
7164
|
+
PEM_PRIVATE_KEY_ASSIGNMENT_PATTERN,
|
|
7165
|
+
contents,
|
|
7166
|
+
lines,
|
|
7167
|
+
true
|
|
7168
|
+
);
|
|
7169
|
+
if (assignmentMatch) return assignmentMatch;
|
|
7170
|
+
if (!isBarePemKeyFile(file)) return null;
|
|
7171
|
+
return scanForPemPrivateKeyMatch(BARE_PEM_PRIVATE_KEY_PATTERN, contents, lines, false);
|
|
7172
|
+
}
|
|
7173
|
+
function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
6098
7174
|
const fullPath = join(repoPath, file);
|
|
6099
7175
|
if (!isRegularFile(fullPath)) return null;
|
|
6100
7176
|
const contents = readRepoText(fullPath, "utf8");
|
|
@@ -6110,6 +7186,10 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
|
|
|
6110
7186
|
allowCredentialNamedDigest: useV36Detectors
|
|
6111
7187
|
});
|
|
6112
7188
|
if (secret) return secret;
|
|
7189
|
+
if (useV23PemPrivateKeyGrammar) {
|
|
7190
|
+
const pemSecret = findPemPrivateKeyAssignment(secretScanContents, file);
|
|
7191
|
+
if (pemSecret) return pemSecret;
|
|
7192
|
+
}
|
|
6113
7193
|
if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
|
|
6114
7194
|
const weakCredential = findWeakExplicitEnvCredential(secretScanContents);
|
|
6115
7195
|
if (weakCredential) return weakCredential;
|
|
@@ -6202,7 +7282,7 @@ function hasTemplateOnlyEnvContent(repoPath, file) {
|
|
|
6202
7282
|
return value.length === 0 || isPlaceholderSecretValue(value);
|
|
6203
7283
|
});
|
|
6204
7284
|
}
|
|
6205
|
-
function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
|
|
7285
|
+
function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
6206
7286
|
const fullPath = join(repoPath, file);
|
|
6207
7287
|
if (!isRegularFile(fullPath)) return [];
|
|
6208
7288
|
const contents = readRepoText(fullPath, "utf8");
|
|
@@ -6219,6 +7299,10 @@ function findSecretFingerprintsInFile(repoPath, file, useV36Detectors = false, u
|
|
|
6219
7299
|
Number.POSITIVE_INFINITY,
|
|
6220
7300
|
{ allowCredentialNamedDigest: useV36Detectors }
|
|
6221
7301
|
).map(({ valueFingerprint }) => valueFingerprint);
|
|
7302
|
+
if (useV23PemPrivateKeyGrammar) {
|
|
7303
|
+
const pemSecret = findPemPrivateKeyAssignment(scanContents, file);
|
|
7304
|
+
if (pemSecret) fingerprints.push(pemSecret.valueFingerprint);
|
|
7305
|
+
}
|
|
6222
7306
|
if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
|
|
6223
7307
|
const weakCredential = findWeakExplicitEnvCredential(scanContents);
|
|
6224
7308
|
if (weakCredential) fingerprints.push(weakCredential.valueFingerprint);
|
|
@@ -6362,24 +7446,32 @@ function declareHistoryFailure(scanLimitations, reason, operation, maxBufferByte
|
|
|
6362
7446
|
`${describeGitFailure(reason, operation, maxBufferBytes)} Cejel continued with reduced Git history evidence.`
|
|
6363
7447
|
);
|
|
6364
7448
|
}
|
|
6365
|
-
function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, scanLimitations, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false) {
|
|
7449
|
+
function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFingerprintsByPath, scanLimitations, authoredProductionOnly = false, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
6366
7450
|
const historyEntries = readCredentialHistoryEntries(
|
|
6367
7451
|
repoPath,
|
|
6368
7452
|
scanLimitations,
|
|
6369
|
-
useV36Detectors
|
|
7453
|
+
useV36Detectors,
|
|
7454
|
+
useV23PemPrivateKeyGrammar
|
|
6370
7455
|
);
|
|
6371
7456
|
const head = excludeHead ? readGitHead(repoPath) : null;
|
|
6372
7457
|
let scannedFiles = 0;
|
|
7458
|
+
let v23WidenedOnlyScanned = 0;
|
|
6373
7459
|
let envPathEvidence = null;
|
|
6374
7460
|
for (const entry of historyEntries) {
|
|
6375
7461
|
if (head && entry.commit === head) continue;
|
|
6376
7462
|
if (authoredProductionOnly && !(useV47Detectors ? isV47AuthoredProductionPath(entry.path) : useV39Detectors ? isV39AuthoredProductionPath(entry.path) : isAuthoredProductionPath(entry.path))) {
|
|
6377
7463
|
continue;
|
|
6378
7464
|
}
|
|
6379
|
-
|
|
6380
|
-
|
|
7465
|
+
const isV23WidenedOnly = useV23PemPrivateKeyGrammar && isV23PemCredentialHistoryPath(historyPathBasename(entry.path.toLowerCase())) && !isCredentialHistoryPath(entry.path, useV36Detectors, false);
|
|
7466
|
+
if (isV23WidenedOnly) {
|
|
7467
|
+
if (v23WidenedOnlyScanned >= V23_PEM_WIDENED_HISTORY_BLOB_LIMIT) continue;
|
|
7468
|
+
v23WidenedOnlyScanned += 1;
|
|
7469
|
+
} else {
|
|
7470
|
+
if (scannedFiles >= HISTORY_SECRET_SCAN_CREDENTIAL_BLOB_LIMIT) {
|
|
7471
|
+
return { evidence: null, secretKind: null, envPathEvidence, truncated: true };
|
|
7472
|
+
}
|
|
7473
|
+
scannedFiles += 1;
|
|
6381
7474
|
}
|
|
6382
|
-
scannedFiles += 1;
|
|
6383
7475
|
const contents = readGitBlob(repoPath, entry.commit, entry.path, scanLimitations);
|
|
6384
7476
|
if (!contents) continue;
|
|
6385
7477
|
const scanContents = prepareCredentialScanContents(
|
|
@@ -6393,6 +7485,12 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
|
|
|
6393
7485
|
let secretMatch = findRealSecretAssignment(scanContents, currentFingerprints, {
|
|
6394
7486
|
allowCredentialNamedDigest: useV36Detectors
|
|
6395
7487
|
});
|
|
7488
|
+
if (!secretMatch && useV23PemPrivateKeyGrammar) {
|
|
7489
|
+
const pemMatch = findPemPrivateKeyAssignment(scanContents, entry.path);
|
|
7490
|
+
if (pemMatch && !currentFingerprints.has(pemMatch.valueFingerprint)) {
|
|
7491
|
+
secretMatch = pemMatch;
|
|
7492
|
+
}
|
|
7493
|
+
}
|
|
6396
7494
|
if (!secretMatch && useV39Detectors && isEnvHistoryPath(entry.path, useV36Detectors, useV47Detectors)) {
|
|
6397
7495
|
const weakCredential = findWeakExplicitEnvCredential(scanContents);
|
|
6398
7496
|
if (weakCredential && !currentFingerprints.has(weakCredential.valueFingerprint)) {
|
|
@@ -6403,11 +7501,15 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
|
|
|
6403
7501
|
return {
|
|
6404
7502
|
evidence: {
|
|
6405
7503
|
kind: "secret_scan",
|
|
6406
|
-
label: secretEvidenceLabel(
|
|
7504
|
+
label: secretEvidenceLabel(
|
|
7505
|
+
secretMatch.kind === "pem_private_key" ? "Committed PEM-formatted private key in recent git history" : "Committed secret in recent git history",
|
|
7506
|
+
secretMatch
|
|
7507
|
+
),
|
|
6407
7508
|
path: entry.path,
|
|
6408
7509
|
line: secretMatch.line,
|
|
6409
7510
|
contentHash: entry.commit
|
|
6410
7511
|
},
|
|
7512
|
+
secretKind: secretMatch.kind ?? null,
|
|
6411
7513
|
envPathEvidence,
|
|
6412
7514
|
truncated: false
|
|
6413
7515
|
};
|
|
@@ -6421,9 +7523,9 @@ function collectHistorySecretEvidence(repoPath, excludeHead, currentSecretFinger
|
|
|
6421
7523
|
};
|
|
6422
7524
|
}
|
|
6423
7525
|
}
|
|
6424
|
-
return { evidence: null, envPathEvidence, truncated: false };
|
|
7526
|
+
return { evidence: null, secretKind: null, envPathEvidence, truncated: false };
|
|
6425
7527
|
}
|
|
6426
|
-
function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
|
|
7528
|
+
function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
6427
7529
|
const entries = [];
|
|
6428
7530
|
const seen = /* @__PURE__ */ new Set();
|
|
6429
7531
|
const appendEntry = (entry) => {
|
|
@@ -6468,7 +7570,7 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
|
|
|
6468
7570
|
scanLimitations.add(CONTROL_CHARACTER_HISTORY_PATH_LIMITATION);
|
|
6469
7571
|
continue;
|
|
6470
7572
|
}
|
|
6471
|
-
if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
|
|
7573
|
+
if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
|
|
6472
7574
|
appendEntry({ commit: currentCommit, path });
|
|
6473
7575
|
}
|
|
6474
7576
|
}
|
|
@@ -6476,17 +7578,19 @@ function readCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors
|
|
|
6476
7578
|
for (const entry of readDeletedCredentialHistoryEntries(
|
|
6477
7579
|
repoPath,
|
|
6478
7580
|
scanLimitations,
|
|
6479
|
-
useV36Detectors
|
|
7581
|
+
useV36Detectors,
|
|
7582
|
+
useV23PemPrivateKeyGrammar
|
|
6480
7583
|
)) {
|
|
6481
7584
|
appendEntry(entry);
|
|
6482
7585
|
}
|
|
6483
7586
|
return entries;
|
|
6484
7587
|
}
|
|
6485
|
-
function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false) {
|
|
7588
|
+
function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
6486
7589
|
const deletedPaths = readDeletedCredentialHistoryPaths(
|
|
6487
7590
|
repoPath,
|
|
6488
7591
|
scanLimitations,
|
|
6489
|
-
useV36Detectors
|
|
7592
|
+
useV36Detectors,
|
|
7593
|
+
useV23PemPrivateKeyGrammar
|
|
6490
7594
|
);
|
|
6491
7595
|
const entries = [];
|
|
6492
7596
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -6500,7 +7604,7 @@ function readDeletedCredentialHistoryEntries(repoPath, scanLimitations, useV36De
|
|
|
6500
7604
|
}
|
|
6501
7605
|
return entries;
|
|
6502
7606
|
}
|
|
6503
|
-
function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false) {
|
|
7607
|
+
function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
6504
7608
|
const historyResult = execGit(
|
|
6505
7609
|
[
|
|
6506
7610
|
"log",
|
|
@@ -6528,7 +7632,7 @@ function readDeletedCredentialHistoryPaths(repoPath, scanLimitations, useV36Dete
|
|
|
6528
7632
|
continue;
|
|
6529
7633
|
}
|
|
6530
7634
|
if (!path || seen.has(path)) continue;
|
|
6531
|
-
if (!isCredentialHistoryPath(path, useV36Detectors)) continue;
|
|
7635
|
+
if (!isCredentialHistoryPath(path, useV36Detectors, useV23PemPrivateKeyGrammar)) continue;
|
|
6532
7636
|
seen.add(path);
|
|
6533
7637
|
paths.push(path);
|
|
6534
7638
|
}
|
|
@@ -6546,14 +7650,24 @@ function readHistoryCommitsForPath(repoPath, path, scanLimitations) {
|
|
|
6546
7650
|
}
|
|
6547
7651
|
return historyResult.stdout.split("\n").map((line) => line.trim()).filter((line) => /^[0-9a-f]{40}$/i.test(line));
|
|
6548
7652
|
}
|
|
6549
|
-
|
|
7653
|
+
var V23_KEY_SHAPED_JSON_PATH_PATTERN = /(?<![A-Za-z0-9])(?:service[-_]?account|private[-_]?key|creds?|key)(?![A-Za-z0-9])/i;
|
|
7654
|
+
function historyPathBasename(lowerPath) {
|
|
7655
|
+
return lowerPath.split("/").at(-1) ?? lowerPath;
|
|
7656
|
+
}
|
|
7657
|
+
function isV23PemCredentialHistoryPath(basename3) {
|
|
7658
|
+
return basename3.endsWith(".json") && V23_KEY_SHAPED_JSON_PATH_PATTERN.test(basename3);
|
|
7659
|
+
}
|
|
7660
|
+
function isCredentialHistoryPath(path, _useV36Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
6550
7661
|
if (isHardExcludedPath(path)) return false;
|
|
6551
7662
|
const lowerPath = path.toLowerCase();
|
|
6552
|
-
const basename3 = lowerPath
|
|
7663
|
+
const basename3 = historyPathBasename(lowerPath);
|
|
6553
7664
|
if (/\.env(?:\.|$)/.test(basename3)) {
|
|
6554
7665
|
return true;
|
|
6555
7666
|
}
|
|
6556
|
-
|
|
7667
|
+
if (lowerPath.includes("secret") || lowerPath.includes("credential") || basename3.endsWith(".pem") || basename3.endsWith(".key") || basename3 === "id_rsa" || basename3.endsWith(".p12") || basename3.endsWith(".pfx")) {
|
|
7668
|
+
return true;
|
|
7669
|
+
}
|
|
7670
|
+
return useV23PemPrivateKeyGrammar && isV23PemCredentialHistoryPath(basename3);
|
|
6557
7671
|
}
|
|
6558
7672
|
function isEnvHistoryPath(path, useV36Detectors = false, useV47Detectors = false) {
|
|
6559
7673
|
if (isEnvTemplatePath(path, useV47Detectors)) return false;
|
|
@@ -7325,14 +8439,14 @@ var WITAN_RUBRIC = [
|
|
|
7325
8439
|
|
|
7326
8440
|
// src/witan/scoring.ts
|
|
7327
8441
|
function usesV17DetectorClosure2(rubricVersion) {
|
|
7328
|
-
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;
|
|
8442
|
+
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;
|
|
7329
8443
|
}
|
|
7330
8444
|
var MAX_SIGNAL_ADJUSTMENT = 0.8;
|
|
7331
|
-
function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
|
|
8445
|
+
function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
|
|
7332
8446
|
const parsedInput = WitanReportInputSchema.parse(input);
|
|
7333
8447
|
const signalsByCriterion = mergeSignalsByCriterion(parsedInput.signals);
|
|
7334
8448
|
const readFailureAbstentions = new Set(
|
|
7335
|
-
[...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true).map((signal) => signal.criterionId)
|
|
8449
|
+
[...signalsByCriterion.values()].filter((signal) => signal.insufficientData === true && !isSelfImposedAbstention(signal)).map((signal) => signal.criterionId)
|
|
7336
8450
|
);
|
|
7337
8451
|
const hasInputSignals = inputSignals != null && inputSignals.length > 0;
|
|
7338
8452
|
const inputSignalsByDimension = /* @__PURE__ */ new Map();
|
|
@@ -7437,7 +8551,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC) {
|
|
|
7437
8551
|
// alter it (see classifyRepoArchetype in repo-signals.ts); per-criterion scoring above is
|
|
7438
8552
|
// unaffected by archetype, only presentation layers (badge/terminal/verdict) key off it.
|
|
7439
8553
|
...parsedInput.archetype ? { archetype: parsedInput.archetype } : {},
|
|
7440
|
-
...insufficientSourceReason ? { insufficientSourceReason } : {}
|
|
8554
|
+
...insufficientSourceReason ? { insufficientSourceReason } : {},
|
|
8555
|
+
...toolVersion ? { toolVersion } : {}
|
|
7441
8556
|
});
|
|
7442
8557
|
}
|
|
7443
8558
|
function applySignalAdjustment(nativeScore, dimension, signals) {
|
|
@@ -7484,7 +8599,7 @@ function statusForScore(score) {
|
|
|
7484
8599
|
}
|
|
7485
8600
|
function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubricVersion) {
|
|
7486
8601
|
if (adjustment === 0) return nativeStatus;
|
|
7487
|
-
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) {
|
|
8602
|
+
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) {
|
|
7488
8603
|
return statusForScore(roundScore(Math.max(0, nativeScore - adjustment)));
|
|
7489
8604
|
}
|
|
7490
8605
|
const statusFloor = {
|
|
@@ -7507,6 +8622,14 @@ function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubri
|
|
|
7507
8622
|
);
|
|
7508
8623
|
return (severity[adjustedSemanticStatus] ?? nativeSeverity) > nativeSeverity ? adjustedSemanticStatus : nativeStatus;
|
|
7509
8624
|
}
|
|
8625
|
+
var SELF_IMPOSED_ABSTENTION_REASONS = /* @__PURE__ */ new Set([
|
|
8626
|
+
"too_large",
|
|
8627
|
+
"excluded_by_extension",
|
|
8628
|
+
"non_regular_file"
|
|
8629
|
+
]);
|
|
8630
|
+
function isSelfImposedAbstention(signal) {
|
|
8631
|
+
return signal.insufficientDataReason !== void 0 && SELF_IMPOSED_ABSTENTION_REASONS.has(signal.insufficientDataReason);
|
|
8632
|
+
}
|
|
7510
8633
|
function mergeSignalsByCriterion(signals) {
|
|
7511
8634
|
const merged = /* @__PURE__ */ new Map();
|
|
7512
8635
|
for (const signal of signals) {
|
|
@@ -7515,21 +8638,25 @@ function mergeSignalsByCriterion(signals) {
|
|
|
7515
8638
|
merged.set(signal.criterionId, signal);
|
|
7516
8639
|
continue;
|
|
7517
8640
|
}
|
|
8641
|
+
const existingAbstains = existing.insufficientData === true;
|
|
8642
|
+
const signalAbstains = signal.insufficientData === true;
|
|
8643
|
+
const abstentionReason = !existingAbstains && !signalAbstains ? void 0 : [existing, signal].filter((candidate) => candidate.insufficientData === true).some((candidate) => !isSelfImposedAbstention(candidate)) ? void 0 : signal.insufficientDataReason ?? existing.insufficientDataReason;
|
|
7518
8644
|
merged.set(signal.criterionId, {
|
|
7519
8645
|
criterionId: signal.criterionId,
|
|
7520
8646
|
positiveEvidence: [...existing.positiveEvidence, ...signal.positiveEvidence],
|
|
7521
8647
|
findings: [...existing.findings, ...signal.findings],
|
|
7522
8648
|
metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
|
|
7523
8649
|
notes: signal.notes ?? existing.notes,
|
|
7524
|
-
|
|
7525
|
-
|
|
7526
|
-
|
|
8650
|
+
...existingAbstains || signalAbstains ? {
|
|
8651
|
+
insufficientData: true,
|
|
8652
|
+
...abstentionReason !== void 0 ? { insufficientDataReason: abstentionReason } : {}
|
|
8653
|
+
} : existing.notApplicable === true || signal.notApplicable === true ? { notApplicable: true } : {}
|
|
7527
8654
|
});
|
|
7528
8655
|
}
|
|
7529
8656
|
return merged;
|
|
7530
8657
|
}
|
|
7531
8658
|
function usesMetricScoring(rubricVersion) {
|
|
7532
|
-
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;
|
|
8659
|
+
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;
|
|
7533
8660
|
}
|
|
7534
8661
|
function unmeasuredStatus(rubricVersion) {
|
|
7535
8662
|
return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
|
|
@@ -7909,7 +9036,7 @@ function calibratedCriterionStateV11(criterionId, metrics) {
|
|
|
7909
9036
|
return void 0;
|
|
7910
9037
|
}
|
|
7911
9038
|
function ensureFindingsExplainStatus(criterionId, score, status, findings, evidence, rubricVersion) {
|
|
7912
|
-
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) {
|
|
9039
|
+
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) {
|
|
7913
9040
|
return [...findings];
|
|
7914
9041
|
}
|
|
7915
9042
|
if (status !== "critical" && status !== "warning") return [...findings];
|
|
@@ -7959,7 +9086,12 @@ function scoreRepoWithPublicCejel(options) {
|
|
|
7959
9086
|
...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
|
|
7960
9087
|
});
|
|
7961
9088
|
const inputSignals = resolvePublicIngestSignals(options);
|
|
7962
|
-
const report = createWitanReport(
|
|
9089
|
+
const report = createWitanReport(
|
|
9090
|
+
input,
|
|
9091
|
+
inputSignals.length > 0 ? inputSignals : void 0,
|
|
9092
|
+
void 0,
|
|
9093
|
+
options.toolVersion
|
|
9094
|
+
);
|
|
7963
9095
|
if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
|
|
7964
9096
|
return report;
|
|
7965
9097
|
}
|
|
@@ -8217,7 +9349,8 @@ function runCejelScan(options) {
|
|
|
8217
9349
|
// the repository. Direct public/leaderboard scans default this off because their targets
|
|
8218
9350
|
// are third-party repositories and must opt in explicitly.
|
|
8219
9351
|
autoDiscoverIngest: true,
|
|
8220
|
-
warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch
|
|
9352
|
+
warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch,
|
|
9353
|
+
...options.toolVersion ? { toolVersion: options.toolVersion } : {}
|
|
8221
9354
|
});
|
|
8222
9355
|
const summary = buildWitanCliSummary(report);
|
|
8223
9356
|
return { report, summary, generatedAt };
|
|
@@ -8229,6 +9362,7 @@ export {
|
|
|
8229
9362
|
CALLER_CONTEXT_PRODUCT_IDENTITY_NOTICE,
|
|
8230
9363
|
CERTIFICATE_GLOSSARY,
|
|
8231
9364
|
formatCertificateMetricLabel,
|
|
9365
|
+
formatCertificateMetricAppliedWeightPercent,
|
|
8232
9366
|
formatCertificateMetricValue,
|
|
8233
9367
|
buildRelyingPartySummary,
|
|
8234
9368
|
WITAN_RUBRIC_VERSION_V22,
|