@cejel/cejel 0.6.6 → 0.6.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 +27 -7
- package/dist/calibration/llm-detector.js +5 -5
- package/dist/{chunk-RH6FKREX.js → chunk-6LVNNJTG.js} +1 -1
- package/dist/{chunk-FSMZV3Y6.js → chunk-AMVP7L26.js} +1 -1
- package/dist/{chunk-O3MHSK25.js → chunk-AWLKQUI4.js} +56 -6
- package/dist/{chunk-JNTTFAHQ.js → chunk-KJVFCZZ3.js} +2784 -223
- package/dist/{chunk-BG4ZRBQ5.js → chunk-KNTBED5T.js} +3 -3
- package/dist/{chunk-VGBFZBZQ.js → chunk-NIWSYJ6N.js} +1 -1
- package/dist/{chunk-5PFYOIOS.js → chunk-WXPGEQBE.js} +2 -2
- package/dist/index.js +219 -56
- 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,16 +5,17 @@ import {
|
|
|
5
5
|
describeGitFailure,
|
|
6
6
|
execGit,
|
|
7
7
|
isExpectedGitAbsence
|
|
8
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-NIWSYJ6N.js";
|
|
9
9
|
import {
|
|
10
10
|
external_exports
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-AMVP7L26.js";
|
|
12
12
|
import {
|
|
13
13
|
MAX_REPOSITORY_CONTENT_BYTES,
|
|
14
14
|
abstainSignalOnWithheldPaths,
|
|
15
15
|
classifyAbstentionReason,
|
|
16
16
|
contentReadSignalKey,
|
|
17
17
|
init_esm_shims,
|
|
18
|
+
isGitLfsPointerText,
|
|
18
19
|
readRepoText,
|
|
19
20
|
readRepoTextPrefix,
|
|
20
21
|
recordContentSkip,
|
|
@@ -24,8 +25,9 @@ import {
|
|
|
24
25
|
withContentReadCriterion,
|
|
25
26
|
withContentReadSignal,
|
|
26
27
|
withContentReadSignals,
|
|
27
|
-
withheldRepoPaths
|
|
28
|
-
|
|
28
|
+
withheldRepoPaths,
|
|
29
|
+
withoutContentReadAttribution
|
|
30
|
+
} from "./chunk-AWLKQUI4.js";
|
|
29
31
|
|
|
30
32
|
// src/witan/schemas.ts
|
|
31
33
|
init_esm_shims();
|
|
@@ -174,14 +176,18 @@ var WitanContentReadSkipReasonSchema = external_exports.enum([
|
|
|
174
176
|
"too_large",
|
|
175
177
|
"excluded_by_extension",
|
|
176
178
|
"denied_path",
|
|
177
|
-
"non_regular_file"
|
|
179
|
+
"non_regular_file",
|
|
180
|
+
// Additive, report format 1.6 (#519): the checkout held a Git LFS pointer in place of content.
|
|
181
|
+
"lfs_pointer"
|
|
178
182
|
]);
|
|
183
|
+
var WitanCriterionDisclosuresSchema = external_exports.array(external_exports.string().min(1).max(1e3)).min(1).max(4);
|
|
179
184
|
var WitanCriterionSignalSchema = external_exports.object({
|
|
180
185
|
criterionId: WitanCriterionIdSchema,
|
|
181
186
|
positiveEvidence: external_exports.array(WitanEvidencePointerSchema).default([]),
|
|
182
187
|
findings: external_exports.array(WitanFindingSchema).default([]),
|
|
183
188
|
metrics: external_exports.array(WitanCriterionMetricSchema).default([]),
|
|
184
189
|
notes: external_exports.string().min(1).max(1e3).optional(),
|
|
190
|
+
disclosures: WitanCriterionDisclosuresSchema.optional(),
|
|
185
191
|
notApplicable: external_exports.literal(true).optional(),
|
|
186
192
|
// A collector reached relevant files but could not read their contents. This is a
|
|
187
193
|
// measurement gap, never negative evidence: scoreCriterion maps it to insufficient_data.
|
|
@@ -206,12 +212,18 @@ var WitanContentReadSummarySchema = external_exports.object({
|
|
|
206
212
|
tooLarge: external_exports.number().int().nonnegative(),
|
|
207
213
|
excludedByExtension: external_exports.number().int().nonnegative(),
|
|
208
214
|
deniedPath: external_exports.number().int().nonnegative(),
|
|
209
|
-
nonRegularFile: external_exports.number().int().nonnegative()
|
|
215
|
+
nonRegularFile: external_exports.number().int().nonnegative(),
|
|
216
|
+
// Additive-optional, report format 1.6 (#519): files whose checkout held a Git LFS pointer
|
|
217
|
+
// in place of their content. Present only when non-zero.
|
|
218
|
+
lfsPointer: external_exports.number().int().positive().optional()
|
|
210
219
|
}).strict(),
|
|
211
220
|
unreadableByErrno: external_exports.record(external_exports.string().min(1).max(32), external_exports.number().int().positive()),
|
|
212
221
|
affectedCriteria: external_exports.array(WitanCriterionIdSchema)
|
|
213
222
|
}).strict().superRefine((summary, context) => {
|
|
214
|
-
const reasonTotal = Object.values(summary.byReason).reduce(
|
|
223
|
+
const reasonTotal = Object.values(summary.byReason).reduce(
|
|
224
|
+
(sum, count) => sum + (count ?? 0),
|
|
225
|
+
0
|
|
226
|
+
);
|
|
215
227
|
if (reasonTotal !== summary.skipped) {
|
|
216
228
|
context.addIssue({
|
|
217
229
|
code: external_exports.ZodIssueCode.custom,
|
|
@@ -266,11 +278,17 @@ var WitanCriterionScoreSchema = external_exports.object({
|
|
|
266
278
|
category: WitanCriterionCategorySchema,
|
|
267
279
|
score: external_exports.number().min(0).max(4),
|
|
268
280
|
nativeScore: external_exports.number().min(0).max(4).optional(),
|
|
281
|
+
// Additive-optional (report format 1.7): the status the native evidence alone produced, set
|
|
282
|
+
// beside nativeScore whenever ingested signals adjusted this criterion. When it differs from
|
|
283
|
+
// `status`, imported evidence moved the status; a reader must not take the moved status as a
|
|
284
|
+
// native Cejel finding. Absent when no ingested signal adjusted the criterion.
|
|
285
|
+
nativeStatus: WitanCriterionStatusSchema.optional(),
|
|
269
286
|
status: WitanCriterionStatusSchema,
|
|
270
287
|
evidence: external_exports.array(WitanEvidencePointerSchema),
|
|
271
288
|
findings: external_exports.array(WitanFindingSchema),
|
|
272
289
|
metrics: external_exports.array(WitanCriterionMetricSchema).default([]),
|
|
273
|
-
notes: external_exports.string().min(1).max(1e3).optional()
|
|
290
|
+
notes: external_exports.string().min(1).max(1e3).optional(),
|
|
291
|
+
disclosures: WitanCriterionDisclosuresSchema.optional()
|
|
274
292
|
}).strict();
|
|
275
293
|
var WitanInputSignalFindingSchema = external_exports.object({
|
|
276
294
|
ruleId: external_exports.string().min(1).max(200),
|
|
@@ -309,6 +327,62 @@ var WitanConsumedSignalSummarySchema = external_exports.object({
|
|
|
309
327
|
// this field always carries every finding.
|
|
310
328
|
findings: external_exports.array(WitanInputSignalFindingSchema).default([])
|
|
311
329
|
}).strict();
|
|
330
|
+
var WitanInputBindingStatusSchema = external_exports.enum([
|
|
331
|
+
"matched",
|
|
332
|
+
"mismatched-repository",
|
|
333
|
+
"mismatched-commit",
|
|
334
|
+
"stale",
|
|
335
|
+
"unbound",
|
|
336
|
+
"unverifiable"
|
|
337
|
+
]);
|
|
338
|
+
var WitanInputBindingCheckSchema = external_exports.enum(["matched", "mismatched", "absent", "unverifiable"]);
|
|
339
|
+
var WitanInputBindingRecordSchema = external_exports.object({
|
|
340
|
+
sha256: external_exports.string().regex(/^[a-f0-9]{64}$/),
|
|
341
|
+
format: external_exports.enum(["sarif", "scorecard", "generic"]),
|
|
342
|
+
provenance: WitanIngestProvenanceSchema,
|
|
343
|
+
producers: external_exports.array(
|
|
344
|
+
external_exports.object({
|
|
345
|
+
name: external_exports.string().min(1).max(120),
|
|
346
|
+
version: external_exports.string().min(1).max(80).optional(),
|
|
347
|
+
commit: external_exports.string().min(1).max(80).optional()
|
|
348
|
+
}).strict()
|
|
349
|
+
).max(8),
|
|
350
|
+
subjects: external_exports.array(
|
|
351
|
+
external_exports.object({
|
|
352
|
+
repository: external_exports.string().min(1).max(300).optional(),
|
|
353
|
+
// Additive-optional (1.7, #525 follow-up): the input declared a repository Cejel could
|
|
354
|
+
// not normalize to host/path. The declared text is withheld, never recorded, because
|
|
355
|
+
// it may carry URL credentials. Never treated as the named subject repository.
|
|
356
|
+
repositoryWithheld: external_exports.literal(true).optional(),
|
|
357
|
+
commit: external_exports.string().min(1).max(80).optional()
|
|
358
|
+
}).strict()
|
|
359
|
+
).max(8),
|
|
360
|
+
// Additive-optional (1.7, #525 follow-up): distinct declared subjects beyond the 8 recorded.
|
|
361
|
+
// Every declared subject is checked; only the record is capped. Absent when none were omitted.
|
|
362
|
+
// Bounded by the 8 MiB ingest document budget: each distinct subject takes at least one byte.
|
|
363
|
+
subjectsOmitted: external_exports.number().int().min(1).max(8 * 1024 * 1024).optional(),
|
|
364
|
+
observedAt: external_exports.string().datetime().optional(),
|
|
365
|
+
status: WitanInputBindingStatusSchema,
|
|
366
|
+
checks: external_exports.object({
|
|
367
|
+
repository: WitanInputBindingCheckSchema,
|
|
368
|
+
commit: WitanInputBindingCheckSchema,
|
|
369
|
+
freshness: external_exports.enum(["within-window", "stale", "absent", "unverifiable"])
|
|
370
|
+
}).strict(),
|
|
371
|
+
duplicate: external_exports.enum(["same-path", "same-content"]).optional(),
|
|
372
|
+
// False only for a same-path duplicate, which the ingest funnel has always skipped. A
|
|
373
|
+
// same-content duplicate at a different path is scored, exactly as before binding existed.
|
|
374
|
+
scored: external_exports.boolean()
|
|
375
|
+
}).strict();
|
|
376
|
+
var WitanInputBindingSchema = external_exports.object({
|
|
377
|
+
policy: external_exports.enum(["record-only", "strict"]),
|
|
378
|
+
freshnessWindowSeconds: external_exports.number().int().min(0),
|
|
379
|
+
scannedSubject: external_exports.object({
|
|
380
|
+
repository: external_exports.string().min(1).max(300).optional(),
|
|
381
|
+
commit: external_exports.string().min(7).max(64).optional(),
|
|
382
|
+
commitTime: external_exports.string().datetime().optional()
|
|
383
|
+
}).strict(),
|
|
384
|
+
inputs: external_exports.array(WitanInputBindingRecordSchema).max(256)
|
|
385
|
+
}).strict();
|
|
312
386
|
var WitanScoredVerdictSchema = external_exports.enum([
|
|
313
387
|
"verified",
|
|
314
388
|
"conditional",
|
|
@@ -343,6 +417,9 @@ var WitanReportCommonSchema = external_exports.object({
|
|
|
343
417
|
// Rubric-driven: length matches whatever rubric produced this report, not a fixed enum.
|
|
344
418
|
criteria: external_exports.array(WitanCriterionScoreSchema).min(1),
|
|
345
419
|
consumedSignals: external_exports.array(WitanConsumedSignalSummarySchema).optional(),
|
|
420
|
+
// Additive-optional (report format 1.7): present only when at least one document was ingested,
|
|
421
|
+
// so a no-ingest report stays byte-identical. See WitanInputBindingSchema.
|
|
422
|
+
inputBinding: WitanInputBindingSchema.optional(),
|
|
346
423
|
archetype: WitanRepoArchetypeSchema.optional(),
|
|
347
424
|
scanLimitations: external_exports.array(external_exports.string().min(1).max(1e3)).max(16).optional(),
|
|
348
425
|
// Additive-optional (report format 1.5, issue #428): disclosures about the commit history the
|
|
@@ -401,7 +478,7 @@ var WitanReportSchema = external_exports.preprocess((value) => {
|
|
|
401
478
|
}, StrictWitanReportSchema);
|
|
402
479
|
var WITAN_ATTESTATION_STATEMENT_TYPE = "https://in-toto.io/Statement/v1";
|
|
403
480
|
var WITAN_ATTESTATION_PREDICATE_TYPE = "https://cejel.dev/attestations/scan/v1";
|
|
404
|
-
var WITAN_REPORT_FORMAT_VERSION = "1.
|
|
481
|
+
var WITAN_REPORT_FORMAT_VERSION = "1.8";
|
|
405
482
|
var WitanAttestationOutcomeSchema = external_exports.discriminatedUnion("status", [
|
|
406
483
|
external_exports.object({
|
|
407
484
|
status: external_exports.literal("scored"),
|
|
@@ -534,6 +611,7 @@ var WITAN_RUBRIC_VERSION_V23 = "witan-rubric-v23-prospective-2026-09-06";
|
|
|
534
611
|
var WITAN_RUBRIC_VERSION_V24 = "witan-rubric-v24-prospective-2026-09-15";
|
|
535
612
|
var WITAN_RUBRIC_VERSION_V25 = "witan-rubric-v25-prospective-2026-10-12";
|
|
536
613
|
var WITAN_RUBRIC_VERSION_V26 = "witan-rubric-v26-prospective-2026-10-14";
|
|
614
|
+
var WITAN_RUBRIC_VERSION_V27 = "witan-rubric-v27-prospective-2026-10-19";
|
|
537
615
|
var WITAN_LAST_CALIBRATED_RUBRIC_VERSION = WITAN_RUBRIC_VERSION_V17;
|
|
538
616
|
var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
|
|
539
617
|
WITAN_RUBRIC_VERSION_V18,
|
|
@@ -544,7 +622,8 @@ var WITAN_PROSPECTIVE_RUBRIC_VERSIONS = Object.freeze([
|
|
|
544
622
|
WITAN_RUBRIC_VERSION_V23,
|
|
545
623
|
WITAN_RUBRIC_VERSION_V24,
|
|
546
624
|
WITAN_RUBRIC_VERSION_V25,
|
|
547
|
-
WITAN_RUBRIC_VERSION_V26
|
|
625
|
+
WITAN_RUBRIC_VERSION_V26,
|
|
626
|
+
WITAN_RUBRIC_VERSION_V27
|
|
548
627
|
]);
|
|
549
628
|
var WITAN_SELECTABLE_RUBRIC_VERSIONS = Object.freeze([
|
|
550
629
|
WITAN_LAST_CALIBRATED_RUBRIC_VERSION,
|
|
@@ -572,6 +651,9 @@ var WITAN_DIRECTORY_WALK_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source i
|
|
|
572
651
|
var WITAN_LEGACY_AUTHENTICATED_A1_ABSENCE_SUMMARY = "Reviewable source is present, but the complete tracked inventory contains no concrete test file, configured test runner, CI test command, or coverage configuration.";
|
|
573
652
|
var WITAN_A5_RECONCILIATION_UNSETTLED_DISCLOSURE = "Whether this repository's claims are reconciled against its implementation cannot be settled from static evidence; Cejel makes no finding on it.";
|
|
574
653
|
var WITAN_A2_TENANT_ISOLATION_UNSETTLED_DISCLOSURE = "Whether tenant isolation is enforced outside the database cannot be settled from static evidence; Cejel makes no finding on it.";
|
|
654
|
+
var WITAN_A3_BUILD_STEP_UNSETTLED_DISCLOSURE = "Whether this package has a build or type-check step outside the files read cannot be settled from static evidence; Cejel makes no finding on it.";
|
|
655
|
+
var WITAN_A2_PLACEHOLDER_SET_ASIDE_PREFIX = "Cejel read and set aside ";
|
|
656
|
+
var WITAN_B3_UNSHOWN_CHECK_PREFIX = "B3 counted workflows Cejel did not see run a check: ";
|
|
575
657
|
function isWitanNoMeasurementAbstention(report) {
|
|
576
658
|
return report.verdict === "insufficient_source" && report.insufficientSourceReason.startsWith(WITAN_NO_MEASUREMENT_REASON) && report.criteria.every(
|
|
577
659
|
({ status }) => status === "not_applicable" || status === "insufficient_data"
|
|
@@ -635,6 +717,76 @@ function collectExternalFindings(consumedSignals) {
|
|
|
635
717
|
});
|
|
636
718
|
}
|
|
637
719
|
|
|
720
|
+
// src/witan/input-binding-presentation.ts
|
|
721
|
+
init_esm_shims();
|
|
722
|
+
var INHERITED_FINDING_NOTES = Object.freeze({
|
|
723
|
+
"scorecard:Dependency-Update-Tool": "Restates evidence Cejel reads natively: A4 dependency_automation_ratio credits the same Dependabot or Renovate configuration. Not independent corroboration; for the same absence this adjusts A4 a second time.",
|
|
724
|
+
"scorecard:Security-Policy": "Restates evidence Cejel reads natively: B4 counts SECURITY.md as an audit-trail file. Not independent corroboration.",
|
|
725
|
+
"scorecard:SAST": "Scorecard SAST reports whether a static-analysis tool runs on commits. It is not a secrets or data-layer finding; the current ingest mapping folds it into A2."
|
|
726
|
+
});
|
|
727
|
+
function inheritedFindingNote(ruleId) {
|
|
728
|
+
return Object.hasOwn(INHERITED_FINDING_NOTES, ruleId) ? INHERITED_FINDING_NOTES[ruleId] : void 0;
|
|
729
|
+
}
|
|
730
|
+
function formatScore(score) {
|
|
731
|
+
return score.toFixed(1);
|
|
732
|
+
}
|
|
733
|
+
function importedAdjustmentSentence(criterion) {
|
|
734
|
+
if (criterion.nativeScore === void 0) return void 0;
|
|
735
|
+
if (criterion.status === "not_applicable" || criterion.status === "insufficient_data") {
|
|
736
|
+
return `Ingested findings were folded into this dimension, but it is ${criterion.status}, so they moved no score.`;
|
|
737
|
+
}
|
|
738
|
+
const statusMoved = criterion.nativeStatus !== void 0 && criterion.nativeStatus !== criterion.status;
|
|
739
|
+
if (criterion.nativeScore === criterion.score) {
|
|
740
|
+
return statusMoved ? `Imported evidence moved this status: Cejel's own evidence gave ${criterion.nativeStatus} at ${formatScore(criterion.nativeScore)}/4.0; ingested findings did not move the score but moved the status to ${criterion.status}. The move is not a native Cejel finding.` : `Ingested findings were folded into this dimension but did not move its score from Cejel's own ${formatScore(criterion.nativeScore)}/4.0; the status is the native one.`;
|
|
741
|
+
}
|
|
742
|
+
const difference = Math.abs(criterion.nativeScore - criterion.score);
|
|
743
|
+
const delta = difference < 0.05 ? "less than 0.1" : formatScore(difference);
|
|
744
|
+
if (statusMoved) {
|
|
745
|
+
return `Imported evidence moved this status: Cejel's own evidence gave ${criterion.nativeStatus} at ${formatScore(criterion.nativeScore)}/4.0; ingested findings lowered the score by ${delta} to ${formatScore(criterion.score)}, which moved the status to ${criterion.status}. The move is not a native Cejel finding.`;
|
|
746
|
+
}
|
|
747
|
+
return `Ingested findings lowered this score by ${delta} from Cejel's own ${formatScore(criterion.nativeScore)}/4.0; the status is the native one.`;
|
|
748
|
+
}
|
|
749
|
+
var STATUS_ORDER = [
|
|
750
|
+
"matched",
|
|
751
|
+
"unbound",
|
|
752
|
+
"unverifiable",
|
|
753
|
+
"stale",
|
|
754
|
+
"mismatched-commit",
|
|
755
|
+
"mismatched-repository"
|
|
756
|
+
];
|
|
757
|
+
function inputBindingHeadline(binding) {
|
|
758
|
+
const counts = /* @__PURE__ */ new Map();
|
|
759
|
+
for (const input of binding.inputs) counts.set(input.status, (counts.get(input.status) ?? 0) + 1);
|
|
760
|
+
const parts = STATUS_ORDER.filter((status) => counts.has(status)).map(
|
|
761
|
+
(status) => `${counts.get(status)} ${status}`
|
|
762
|
+
);
|
|
763
|
+
const duplicates = binding.inputs.filter((input) => input.duplicate !== void 0).length;
|
|
764
|
+
if (duplicates > 0) parts.push(`${duplicates} duplicate`);
|
|
765
|
+
const n = binding.inputs.length;
|
|
766
|
+
const policy = binding.policy === "strict" ? "strict: mismatched, stale and duplicate inputs were refused" : "record only: binding status did not affect any score";
|
|
767
|
+
return `${n} ingested input${n === 1 ? "" : "s"}: ${parts.join(", ")} (${policy}).`;
|
|
768
|
+
}
|
|
769
|
+
function describeSubject(record) {
|
|
770
|
+
if (record.subjects.length === 0) return "no declared subject";
|
|
771
|
+
const listed = record.subjects.map((subject) => {
|
|
772
|
+
const repository = subject.repository ?? (subject.repositoryWithheld ? "repository withheld (declared, not normalizable)" : "repository not declared");
|
|
773
|
+
return `${repository} @ ${subject.commit ?? "commit not declared"}`;
|
|
774
|
+
}).join("; ");
|
|
775
|
+
return record.subjectsOmitted ? `${listed}; and ${record.subjectsOmitted} more declared subject${record.subjectsOmitted === 1 ? "" : "s"}, all checked, not recorded` : listed;
|
|
776
|
+
}
|
|
777
|
+
function describeProducer(record) {
|
|
778
|
+
if (record.producers.length === 0) return record.format;
|
|
779
|
+
return record.producers.map((producer) => producer.version ? `${producer.name} ${producer.version}` : producer.name).join(", ");
|
|
780
|
+
}
|
|
781
|
+
function describeInputBindingRecord(record) {
|
|
782
|
+
const duplicate = record.duplicate ? `; duplicate (${record.duplicate}${record.scored ? ", scored again as before" : ", not scored again"})` : "";
|
|
783
|
+
return `${record.status}: ${describeProducer(record)} (${record.format}, ${record.provenance}); subject ${describeSubject(record)}; observed ${record.observedAt ?? "not declared"}; sha256:${record.sha256}${duplicate}`;
|
|
784
|
+
}
|
|
785
|
+
function describeScannedSubject(binding) {
|
|
786
|
+
const subject = binding.scannedSubject;
|
|
787
|
+
return `Compared with: ${subject.repository ?? "repository not named (pass --subject-repository)"} @ ${subject.commit ?? "no git HEAD"}${subject.commitTime ? `, committed ${subject.commitTime}` : ""}; stale means observed more than ${binding.freshnessWindowSeconds} seconds before that commit time.`;
|
|
788
|
+
}
|
|
789
|
+
|
|
638
790
|
// src/witan/finding-presentation.ts
|
|
639
791
|
init_esm_shims();
|
|
640
792
|
|
|
@@ -662,7 +814,11 @@ var WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS = Object.freeze({
|
|
|
662
814
|
// fires on it and, by amendment 1, A5 is scored without reconciliation_artifact_depth. The A5
|
|
663
815
|
// component and the report components of the specimens whose code-trust score that moves differ
|
|
664
816
|
// from v25's. A2 does not move on this corpus.
|
|
665
|
-
[WITAN_RUBRIC_VERSION_V26]: "sha256:4530317d56fc73eb89287e344ad626e2b9a96908ef12eb9e9c13b839bd599dae"
|
|
817
|
+
[WITAN_RUBRIC_VERSION_V26]: "sha256:4530317d56fc73eb89287e344ad626e2b9a96908ef12eb9e9c13b839bd599dae",
|
|
818
|
+
// v17's closure plus the v27 false-assurance gate (#517-#520, #442). On this corpus only A4
|
|
819
|
+
// moves against v17: specimens whose CI runs `npm ci` with no committed lockfile score
|
|
820
|
+
// lockfile_coverage 0 under library norms. No report-level component differs from v17's.
|
|
821
|
+
[WITAN_RUBRIC_VERSION_V27]: "sha256:fb4edfa0bdf0d8153af3f84a4b72d4eab6da3fd6e0b2e3e494cea9542ded45a0"
|
|
666
822
|
});
|
|
667
823
|
function rubricBehaviourFingerprint(rubricVersion) {
|
|
668
824
|
return WITAN_RUBRIC_BEHAVIOUR_FINGERPRINTS[rubricVersion];
|
|
@@ -730,7 +886,7 @@ var WITAN_RUBRIC = [
|
|
|
730
886
|
|
|
731
887
|
// src/witan/scoring.ts
|
|
732
888
|
function usesV17DetectorClosure(rubricVersion) {
|
|
733
|
-
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_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
889
|
+
return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V27 || 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_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23;
|
|
734
890
|
}
|
|
735
891
|
var MAX_SIGNAL_ADJUSTMENT = 0.8;
|
|
736
892
|
function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersion) {
|
|
@@ -764,7 +920,8 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
|
|
|
764
920
|
evidence: scored.evidence,
|
|
765
921
|
findings: scored.findings,
|
|
766
922
|
metrics: scored.metrics,
|
|
767
|
-
...signal?.notes ? { notes: signal.notes } : {}
|
|
923
|
+
...signal?.notes ? { notes: signal.notes } : {},
|
|
924
|
+
...signal?.disclosures ? { disclosures: signal.disclosures } : {}
|
|
768
925
|
};
|
|
769
926
|
}
|
|
770
927
|
const { adjustedScore, adjustment, summaries } = applySignalAdjustment(
|
|
@@ -786,11 +943,16 @@ function createWitanReport(input, inputSignals, rubric = WITAN_RUBRIC, toolVersi
|
|
|
786
943
|
category: rubricCriterion.category,
|
|
787
944
|
score: finalScore,
|
|
788
945
|
...adjustment !== 0 ? { nativeScore: scored.score } : {},
|
|
946
|
+
// Beside nativeScore, so a reader can tell a status that imported evidence moved from one
|
|
947
|
+
// the native evidence produced (report format 1.7). Presentation-only: finalStatus above is
|
|
948
|
+
// computed exactly as before.
|
|
949
|
+
...adjustment !== 0 ? { nativeStatus: scored.status } : {},
|
|
789
950
|
status: finalStatus,
|
|
790
951
|
evidence: scored.evidence,
|
|
791
952
|
findings: scored.findings,
|
|
792
953
|
metrics: scored.metrics,
|
|
793
|
-
...signal?.notes ? { notes: signal.notes } : {}
|
|
954
|
+
...signal?.notes ? { notes: signal.notes } : {},
|
|
955
|
+
...signal?.disclosures ? { disclosures: signal.disclosures } : {}
|
|
794
956
|
};
|
|
795
957
|
});
|
|
796
958
|
const categoryOrder = [];
|
|
@@ -901,7 +1063,7 @@ function statusForScore(score) {
|
|
|
901
1063
|
}
|
|
902
1064
|
function statusAfterInputAdjustment(nativeStatus, nativeScore, adjustment, rubricVersion) {
|
|
903
1065
|
if (adjustment === 0) return nativeStatus;
|
|
904
|
-
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_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
|
|
1066
|
+
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_V27 && 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_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
|
|
905
1067
|
return statusForScore(roundScore(Math.max(0, nativeScore - adjustment)));
|
|
906
1068
|
}
|
|
907
1069
|
const statusFloor = {
|
|
@@ -949,6 +1111,7 @@ function mergeSignalsByCriterion(signals) {
|
|
|
949
1111
|
findings: [...existing.findings, ...signal.findings],
|
|
950
1112
|
metrics: [...existing.metrics ?? [], ...signal.metrics ?? []],
|
|
951
1113
|
notes: signal.notes ?? existing.notes,
|
|
1114
|
+
...mergedDisclosures(existing.disclosures, signal.disclosures),
|
|
952
1115
|
...existingAbstains || signalAbstains ? {
|
|
953
1116
|
insufficientData: true,
|
|
954
1117
|
...abstentionReason !== void 0 ? { insufficientDataReason: abstentionReason } : {}
|
|
@@ -957,8 +1120,12 @@ function mergeSignalsByCriterion(signals) {
|
|
|
957
1120
|
}
|
|
958
1121
|
return merged;
|
|
959
1122
|
}
|
|
1123
|
+
function mergedDisclosures(existing, incoming) {
|
|
1124
|
+
const disclosures = [.../* @__PURE__ */ new Set([...existing ?? [], ...incoming ?? []])].slice(0, 4);
|
|
1125
|
+
return disclosures.length > 0 ? { disclosures } : {};
|
|
1126
|
+
}
|
|
960
1127
|
function usesMetricScoring(rubricVersion) {
|
|
961
|
-
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_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
|
|
1128
|
+
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_V27 || 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_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V23 || rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0;
|
|
962
1129
|
}
|
|
963
1130
|
function unmeasuredStatus(rubricVersion) {
|
|
964
1131
|
return rubricVersion === WITAN_TRADING_RUBRIC_VERSION_V0 ? "unverified" : "insufficient_data";
|
|
@@ -1346,7 +1513,7 @@ function calibratedCriterionStateV11(criterionId, metrics) {
|
|
|
1346
1513
|
return void 0;
|
|
1347
1514
|
}
|
|
1348
1515
|
function ensureFindingsExplainStatus(criterionId, score, status, findings, evidence, rubricVersion) {
|
|
1349
|
-
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_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23 && findings.length > 0) {
|
|
1516
|
+
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_V27 && 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_V25 && rubricVersion !== WITAN_RUBRIC_VERSION_V26 && rubricVersion !== WITAN_RUBRIC_VERSION_V24 && rubricVersion !== WITAN_RUBRIC_VERSION_V23 && findings.length > 0) {
|
|
1350
1517
|
return [...findings];
|
|
1351
1518
|
}
|
|
1352
1519
|
if (status !== "critical" && status !== "warning") return [...findings];
|
|
@@ -1856,6 +2023,12 @@ function buildNextSteps(report, unmeasured, notApplicable, a1, coverageMetric) {
|
|
|
1856
2023
|
`Resolve the ${actionableSkips.count} content entr${actionableSkips.count === 1 ? "y" : "ies"} skipped as ${joinWithOr(actionableSkips.reasons)} (see the itemized reasons in the certificate), then reproduce the scan.`
|
|
1857
2024
|
);
|
|
1858
2025
|
}
|
|
2026
|
+
const lfsPointers = report.contentReadSummary?.byReason.lfsPointer ?? 0;
|
|
2027
|
+
if (lfsPointers > 0) {
|
|
2028
|
+
steps.push(
|
|
2029
|
+
`Fetch the Git LFS content for the ${lfsPointers} file${lfsPointers === 1 ? "" : "s"} whose checkout holds a pointer (git lfs pull), then reproduce the scan.`
|
|
2030
|
+
);
|
|
2031
|
+
}
|
|
1859
2032
|
if (steps.length === 0) {
|
|
1860
2033
|
steps.push(
|
|
1861
2034
|
"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."
|
|
@@ -1942,7 +2115,9 @@ var WITHHELD_PATH_REASON_LABELS = {
|
|
|
1942
2115
|
non_regular_file: "not a regular file",
|
|
1943
2116
|
unreadable: "unreadable",
|
|
1944
2117
|
denied_path: "denied path",
|
|
1945
|
-
excluded_by_extension: "excluded by extension"
|
|
2118
|
+
excluded_by_extension: "excluded by extension",
|
|
2119
|
+
// Never a withheld-path reason today (an LFS pointer is read, not withheld); the map is total.
|
|
2120
|
+
lfs_pointer: "a Git LFS pointer"
|
|
1946
2121
|
};
|
|
1947
2122
|
function describeWithheldReasons(entries) {
|
|
1948
2123
|
return [...new Set(entries.map((entry) => WITHHELD_PATH_REASON_LABELS[entry.reason]))].sort().join(", ");
|
|
@@ -1973,6 +2148,7 @@ function buildWithheldPathsGapSentence(report) {
|
|
|
1973
2148
|
}
|
|
1974
2149
|
var SETTLING_EVIDENCE_DISCLOSURES = [
|
|
1975
2150
|
{ criterionId: "A2", sentence: WITAN_A2_TENANT_ISOLATION_UNSETTLED_DISCLOSURE },
|
|
2151
|
+
{ criterionId: "A3", sentence: WITAN_A3_BUILD_STEP_UNSETTLED_DISCLOSURE },
|
|
1976
2152
|
{ criterionId: "A5", sentence: WITAN_A5_RECONCILIATION_UNSETTLED_DISCLOSURE }
|
|
1977
2153
|
];
|
|
1978
2154
|
function settlingEvidenceDisclosures(report) {
|
|
@@ -1982,6 +2158,18 @@ function settlingEvidenceDisclosures(report) {
|
|
|
1982
2158
|
)
|
|
1983
2159
|
).map(({ sentence }) => sentence);
|
|
1984
2160
|
}
|
|
2161
|
+
function placeholderSetAsideDisclosures(report) {
|
|
2162
|
+
return criterionDisclosures(report, "A2", WITAN_A2_PLACEHOLDER_SET_ASIDE_PREFIX);
|
|
2163
|
+
}
|
|
2164
|
+
function unshownCheckWorkflowDisclosures(report) {
|
|
2165
|
+
const b3 = report.criteria.find((criterion) => criterion.id === "B3");
|
|
2166
|
+
if (!b3 || b3.status === "not_applicable" || b3.status === "insufficient_data") return [];
|
|
2167
|
+
return criterionDisclosures(report, "B3", WITAN_B3_UNSHOWN_CHECK_PREFIX);
|
|
2168
|
+
}
|
|
2169
|
+
function criterionDisclosures(report, criterionId, prefix) {
|
|
2170
|
+
const criterion = report.criteria.find((entry) => entry.id === criterionId);
|
|
2171
|
+
return (criterion?.disclosures ?? []).filter((sentence) => sentence.startsWith(prefix));
|
|
2172
|
+
}
|
|
1985
2173
|
function describeEstablishedDimensions(report, measured, applicable) {
|
|
1986
2174
|
const total = report.criteria.length;
|
|
1987
2175
|
const counts = formatMeasuredDimensionCounts(computeMeasuredCoverage(report));
|
|
@@ -2034,6 +2222,12 @@ function buildRelyingPartySummary(report) {
|
|
|
2034
2222
|
`${contentEntries(actionableSkips, "was", "were")} skipped for the reasons itemized in the certificate.`
|
|
2035
2223
|
);
|
|
2036
2224
|
}
|
|
2225
|
+
const lfsPointerCount = report.contentReadSummary?.byReason.lfsPointer ?? 0;
|
|
2226
|
+
if (lfsPointerCount > 0) {
|
|
2227
|
+
gaps.push(
|
|
2228
|
+
`${lfsPointerCount} file${lfsPointerCount === 1 ? " is a Git LFS pointer" : "s are Git LFS pointers"} in this checkout, so ${lfsPointerCount === 1 ? "its" : "their"} content was not read. A criterion that read one either abstained (it is listed among the affected criteria) or scored the pointer's text.`
|
|
2229
|
+
);
|
|
2230
|
+
}
|
|
2037
2231
|
const excludedByExtension = report.contentReadSummary?.byReason.excludedByExtension ?? 0;
|
|
2038
2232
|
if (excludedByExtension > 0) {
|
|
2039
2233
|
gaps.push(
|
|
@@ -2041,6 +2235,8 @@ function buildRelyingPartySummary(report) {
|
|
|
2041
2235
|
);
|
|
2042
2236
|
}
|
|
2043
2237
|
gaps.push(...settlingEvidenceDisclosures(report));
|
|
2238
|
+
gaps.push(...placeholderSetAsideDisclosures(report));
|
|
2239
|
+
gaps.push(...unshownCheckWorkflowDisclosures(report));
|
|
2044
2240
|
const withheldPathsSentence = buildWithheldPathsGapSentence(report);
|
|
2045
2241
|
if (withheldPathsSentence !== void 0) {
|
|
2046
2242
|
gaps.push(withheldPathsSentence);
|
|
@@ -2184,7 +2380,7 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
2184
2380
|
<aside class="score-panel" aria-label="Summary trust score">
|
|
2185
2381
|
<div class="score-badge">Summary</div>
|
|
2186
2382
|
${report.verdict === "insufficient_source" ? `<div class="verdict">${escapeHtml(renderWitanAbstentionLabel(report))}</div>
|
|
2187
|
-
<p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${
|
|
2383
|
+
<p class="insufficient-note">${escapeHtml(report.insufficientSourceReason)}</p>` : `<div class="score">${formatScore2(report.overallScore)}</div>
|
|
2188
2384
|
<div class="score-unit">/ 4.0 overall</div>
|
|
2189
2385
|
<div class="verdict">${escapeHtml(renderVerdict(report.overallScore))}</div>
|
|
2190
2386
|
<div class="subscores">
|
|
@@ -2241,6 +2437,8 @@ function renderWitanHtmlReport(report, options = {}) {
|
|
|
2241
2437
|
</article>
|
|
2242
2438
|
</section>` : ""}
|
|
2243
2439
|
|
|
2440
|
+
${renderInputBindingSection(report)}
|
|
2441
|
+
|
|
2244
2442
|
${renderGlossary(glossaryEntriesForReport(report))}
|
|
2245
2443
|
</main>
|
|
2246
2444
|
</body>
|
|
@@ -2305,7 +2503,8 @@ function renderContentReadSummary(report) {
|
|
|
2305
2503
|
<li>Too large: ${summary.byReason.tooLarge}</li>
|
|
2306
2504
|
<li>Excluded by extension: ${summary.byReason.excludedByExtension}</li>
|
|
2307
2505
|
<li>Denied path: ${summary.byReason.deniedPath}</li>
|
|
2308
|
-
<li>Non-regular file: ${summary.byReason.nonRegularFile}</li
|
|
2506
|
+
<li>Non-regular file: ${summary.byReason.nonRegularFile}</li>${summary.byReason.lfsPointer ? `
|
|
2507
|
+
<li>Git LFS pointer (content not in this checkout): ${summary.byReason.lfsPointer}</li>` : ""}
|
|
2309
2508
|
</ul>
|
|
2310
2509
|
${affected}
|
|
2311
2510
|
</section>`;
|
|
@@ -2340,7 +2539,7 @@ function formatSourceProvenanceLine(summary) {
|
|
|
2340
2539
|
function renderExternalFindingItems(findings) {
|
|
2341
2540
|
const shown = findings.slice(0, EXTERNAL_FINDINGS_DISPLAY_LIMIT);
|
|
2342
2541
|
const items = shown.map(
|
|
2343
|
-
(f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span
|
|
2542
|
+
(f) => `<li><strong>${escapeHtml(f.label)} \u2192 ${escapeHtml(f.dimension)}</strong><span>${escapeHtml(f.severity)}: ${escapeHtml(f.ruleId)} \u2014 ${escapeHtml(f.message)}${f.location ? ` (${escapeHtml(f.location)})` : ""}</span>${renderInheritedFindingNote(f.ruleId)}</li>`
|
|
2344
2543
|
);
|
|
2345
2544
|
const remaining = findings.length - shown.length;
|
|
2346
2545
|
if (remaining > 0) {
|
|
@@ -2348,6 +2547,21 @@ function renderExternalFindingItems(findings) {
|
|
|
2348
2547
|
}
|
|
2349
2548
|
return items;
|
|
2350
2549
|
}
|
|
2550
|
+
function renderInheritedFindingNote(ruleId) {
|
|
2551
|
+
const note = inheritedFindingNote(ruleId);
|
|
2552
|
+
return note ? `<span class="external-note">${escapeHtml(note)}</span>` : "";
|
|
2553
|
+
}
|
|
2554
|
+
function renderInputBindingSection(report) {
|
|
2555
|
+
const binding = report.inputBinding;
|
|
2556
|
+
if (!binding) return "";
|
|
2557
|
+
return `<section class="external-findings-section" aria-label="Ingested inputs">
|
|
2558
|
+
<article class="evidence-section">
|
|
2559
|
+
<h2>Ingested inputs</h2>
|
|
2560
|
+
<p class="external-note">${escapeHtml(inputBindingHeadline(binding))} ${escapeHtml(describeScannedSubject(binding))}</p>
|
|
2561
|
+
<ul>${binding.inputs.map((input) => `<li>${escapeHtml(describeInputBindingRecord(input))}</li>`).join("")}</ul>
|
|
2562
|
+
</article>
|
|
2563
|
+
</section>`;
|
|
2564
|
+
}
|
|
2351
2565
|
function criteriaByCategory(report, category) {
|
|
2352
2566
|
return report.criteria.filter((criterion) => criterion.category === category);
|
|
2353
2567
|
}
|
|
@@ -2391,8 +2605,8 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
|
|
|
2391
2605
|
</div>
|
|
2392
2606
|
${renderStatusChip(criterion.id, criterion.status)}
|
|
2393
2607
|
</div>
|
|
2394
|
-
<div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" :
|
|
2395
|
-
${statusReconciliation}
|
|
2608
|
+
<div class="criterion-score">${criterion.status === "not_applicable" ? "N/A" : criterion.status === "insufficient_data" ? "No data" : formatScore2(criterion.score)}</div>
|
|
2609
|
+
${statusReconciliation}${renderImportedAdjustment(criterion)}
|
|
2396
2610
|
${historyWarning}
|
|
2397
2611
|
<ul class="criterion-metrics">
|
|
2398
2612
|
${metrics.length > 0 ? metrics.map((item) => `<li>${item}</li>`).join("") : "<li>No measured depth metrics supplied.</li>"}
|
|
@@ -2402,6 +2616,11 @@ function renderCriterionCard(criterion, gitHistoryUnavailable) {
|
|
|
2402
2616
|
</ul>
|
|
2403
2617
|
</article>`;
|
|
2404
2618
|
}
|
|
2619
|
+
function renderImportedAdjustment(criterion) {
|
|
2620
|
+
const sentence = importedAdjustmentSentence(criterion);
|
|
2621
|
+
return sentence ? `
|
|
2622
|
+
<p class="status-explanation">${escapeHtml(sentence)}</p>` : "";
|
|
2623
|
+
}
|
|
2405
2624
|
function renderStatusReconciliation(criterion) {
|
|
2406
2625
|
if (criterion.status === "not_applicable") {
|
|
2407
2626
|
return '<p class="status-explanation"><strong>Why no label comparison applies:</strong> this dimension is not applicable to this repository, so it has no weighted numeric score band to compare.</p>';
|
|
@@ -2414,14 +2633,14 @@ function renderStatusReconciliation(criterion) {
|
|
|
2414
2633
|
}
|
|
2415
2634
|
const numericBand = scoreBandForPresentation(criterion.score);
|
|
2416
2635
|
if (numericBand === criterion.status) {
|
|
2417
|
-
return `<p class="status-explanation"><strong>How the labels agree:</strong> the ${escapeHtml(criterion.status)} dimension band matches the ${
|
|
2636
|
+
return `<p class="status-explanation"><strong>How the labels agree:</strong> the ${escapeHtml(criterion.status)} dimension band matches the ${formatScore2(criterion.score)}/4.0 weighted score (the ${numericBand} numeric band).</p>`;
|
|
2418
2637
|
}
|
|
2419
2638
|
if (criterion.status !== "verified" && criterion.status !== "info" && criterion.status !== "warning" && criterion.status !== "critical") {
|
|
2420
2639
|
return `<p class="status-explanation"><strong>Why no label comparison applies:</strong> this report contains an unrecognized dimension state (${escapeHtml(String(criterion.status))}), so Cejel will not infer a comparison.</p>`;
|
|
2421
2640
|
}
|
|
2422
2641
|
const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
|
|
2423
2642
|
const help = entry ? `<span class="term-help" tabindex="0" aria-describedby="tooltip-${criterion.id}-labels"><strong>Why the labels differ:</strong><span class="term-tooltip" id="tooltip-${criterion.id}-labels" role="tooltip">${escapeHtml(entry.definition)}</span></span>` : "<strong>Why the labels differ:</strong>";
|
|
2424
|
-
return `<p class="status-explanation">${help} the ${escapeHtml(criterion.status)} dimension band is calibrated from criterion evidence thresholds and findings, independently of the ${
|
|
2643
|
+
return `<p class="status-explanation">${help} the ${escapeHtml(criterion.status)} dimension band is calibrated from criterion evidence thresholds and findings, independently of the ${formatScore2(criterion.score)}/4.0 weighted score (which falls in the ${numericBand} numeric band).</p>`;
|
|
2425
2644
|
}
|
|
2426
2645
|
function scoreBandForPresentation(score) {
|
|
2427
2646
|
if (score >= 3.5) return "verified";
|
|
@@ -2447,7 +2666,7 @@ function renderMetric(criterion, metric2) {
|
|
|
2447
2666
|
}
|
|
2448
2667
|
function renderCategoryScore(score, coverage, category) {
|
|
2449
2668
|
const categoryCoverage = coverage.byCategory.find((entry) => entry.category === category);
|
|
2450
|
-
return categoryCoverage !== void 0 && categoryCoverage.measured === 0 || score === null ? "not measured" :
|
|
2669
|
+
return categoryCoverage !== void 0 && categoryCoverage.measured === 0 || score === null ? "not measured" : formatScore2(score);
|
|
2451
2670
|
}
|
|
2452
2671
|
function renderStatusChip(criterionId, status) {
|
|
2453
2672
|
const entry = CERTIFICATE_GLOSSARY.find((candidate) => candidate.key === "labels");
|
|
@@ -2513,7 +2732,7 @@ function renderMachineVerdict(verdict) {
|
|
|
2513
2732
|
return "Insufficient source";
|
|
2514
2733
|
}
|
|
2515
2734
|
}
|
|
2516
|
-
function
|
|
2735
|
+
function formatScore2(score) {
|
|
2517
2736
|
return score.toFixed(1);
|
|
2518
2737
|
}
|
|
2519
2738
|
function formatDate(value) {
|
|
@@ -2704,7 +2923,7 @@ function badgeMessage(report, verdict) {
|
|
|
2704
2923
|
if ((report.scanLimitations?.length ?? 0) > 0) return "unrated: limited evidence";
|
|
2705
2924
|
if (isWitanNoMeasurementAbstention(report)) return "unrated: no measurable evidence";
|
|
2706
2925
|
if (report.verdict === "insufficient_source") return "unrated: no source";
|
|
2707
|
-
return `${
|
|
2926
|
+
return `${formatScore3(report.overallScore)}/4.0 ${verdict.toLowerCase()}`;
|
|
2708
2927
|
}
|
|
2709
2928
|
function renderWitanBadgeEndpoint(report) {
|
|
2710
2929
|
const verdict = renderReportVerdict(report);
|
|
@@ -2748,7 +2967,7 @@ function renderWitanBadgeSvg(report) {
|
|
|
2748
2967
|
</svg>
|
|
2749
2968
|
`;
|
|
2750
2969
|
}
|
|
2751
|
-
function
|
|
2970
|
+
function formatScore3(score) {
|
|
2752
2971
|
return score.toFixed(1);
|
|
2753
2972
|
}
|
|
2754
2973
|
function escapeXml(value) {
|
|
@@ -2885,12 +3104,6 @@ function verifyWitanAttestationBinding(statement, report, options = {}) {
|
|
|
2885
3104
|
return { valid: errors.length === 0, errors };
|
|
2886
3105
|
}
|
|
2887
3106
|
|
|
2888
|
-
// src/witan/index.ts
|
|
2889
|
-
init_esm_shims();
|
|
2890
|
-
|
|
2891
|
-
// src/witan/markdown.ts
|
|
2892
|
-
init_esm_shims();
|
|
2893
|
-
|
|
2894
3107
|
// src/presentation-safety.ts
|
|
2895
3108
|
init_esm_shims();
|
|
2896
3109
|
var PRESENTATION_CONTROL_PATTERN = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}]/gu;
|
|
@@ -2907,17 +3120,530 @@ function sanitizePresentationLine(value, options) {
|
|
|
2907
3120
|
}
|
|
2908
3121
|
return `${prefix}${ellipsis}`;
|
|
2909
3122
|
}
|
|
3123
|
+
function stripTerminalControls(text) {
|
|
3124
|
+
return text.replace(/[\t\n]/g, " ").replace(/[\u0000-\u001f\u007f-\u009f]/g, "");
|
|
3125
|
+
}
|
|
2910
3126
|
|
|
2911
|
-
// src/witan/
|
|
3127
|
+
// src/witan/input-binding.ts
|
|
2912
3128
|
init_esm_shims();
|
|
2913
3129
|
import { createHash as createHash3 } from "crypto";
|
|
3130
|
+
|
|
3131
|
+
// src/witan/linear-strips.ts
|
|
3132
|
+
init_esm_shims();
|
|
3133
|
+
var LINE_TERMINATOR_PATTERN = /[\n\r\u2028\u2029]/;
|
|
3134
|
+
var WHITESPACE_PATTERN = /\s/;
|
|
3135
|
+
function lastLineTerminator(text) {
|
|
3136
|
+
for (let index = text.length - 1; index >= 0; index -= 1) {
|
|
3137
|
+
if (LINE_TERMINATOR_PATTERN.test(text[index] ?? "")) return index;
|
|
3138
|
+
}
|
|
3139
|
+
return -1;
|
|
3140
|
+
}
|
|
3141
|
+
function stripTrailingHashComment(text) {
|
|
3142
|
+
for (let index = text.indexOf("#", lastLineTerminator(text) + 1); index !== -1; index = text.indexOf("#", index + 1)) {
|
|
3143
|
+
if (index === 0 || !WHITESPACE_PATTERN.test(text[index - 1] ?? "")) continue;
|
|
3144
|
+
return text.slice(0, index).trimEnd();
|
|
3145
|
+
}
|
|
3146
|
+
return text;
|
|
3147
|
+
}
|
|
3148
|
+
function stripHashCommentAtStartOrAfterBlank(text) {
|
|
3149
|
+
for (let index = text.indexOf("#", lastLineTerminator(text) + 1); index !== -1; index = text.indexOf("#", index + 1)) {
|
|
3150
|
+
if (index === 0) return "";
|
|
3151
|
+
if (WHITESPACE_PATTERN.test(text[index - 1] ?? "")) return text.slice(0, index - 1);
|
|
3152
|
+
}
|
|
3153
|
+
return text;
|
|
3154
|
+
}
|
|
3155
|
+
function stripFromQueryOrFragment(text) {
|
|
3156
|
+
const from = lastLineTerminator(text) + 1;
|
|
3157
|
+
for (let index = from; index < text.length; index += 1) {
|
|
3158
|
+
const character = text[index];
|
|
3159
|
+
if (character === "?" || character === "#") return text.slice(0, index);
|
|
3160
|
+
}
|
|
3161
|
+
return text;
|
|
3162
|
+
}
|
|
3163
|
+
function stripTrailingCloseParentheses(segment) {
|
|
3164
|
+
let end = segment.length;
|
|
3165
|
+
while (end > 0 && segment[end - 1] === ")") end -= 1;
|
|
3166
|
+
if (end === segment.length) return segment;
|
|
3167
|
+
return segment.slice(0, end).trimEnd();
|
|
3168
|
+
}
|
|
3169
|
+
function stripTrailingSlashes(text) {
|
|
3170
|
+
let end = text.length;
|
|
3171
|
+
while (end > 0 && text[end - 1] === "/") end -= 1;
|
|
3172
|
+
return end === text.length ? text : text.slice(0, end);
|
|
3173
|
+
}
|
|
3174
|
+
function hasScheduleCronTrigger(text) {
|
|
3175
|
+
const key = "schedule:";
|
|
3176
|
+
for (let index = text.indexOf(key); index !== -1; index = text.indexOf(key, index + 1)) {
|
|
3177
|
+
let before = index - 1;
|
|
3178
|
+
let newlineBefore = false;
|
|
3179
|
+
while (before >= 0 && WHITESPACE_PATTERN.test(text[before] ?? "")) {
|
|
3180
|
+
if (text[before] === "\n") newlineBefore = true;
|
|
3181
|
+
before -= 1;
|
|
3182
|
+
}
|
|
3183
|
+
if (before !== -1 && !newlineBefore) continue;
|
|
3184
|
+
let after = index + key.length;
|
|
3185
|
+
let newlineAfter = false;
|
|
3186
|
+
while (after < text.length && WHITESPACE_PATTERN.test(text[after] ?? "")) {
|
|
3187
|
+
if (text[after] === "\n") newlineAfter = true;
|
|
3188
|
+
after += 1;
|
|
3189
|
+
}
|
|
3190
|
+
if (!newlineAfter || text[after] !== "-") continue;
|
|
3191
|
+
after += 1;
|
|
3192
|
+
while (after < text.length && WHITESPACE_PATTERN.test(text[after] ?? "")) after += 1;
|
|
3193
|
+
if (text.startsWith("cron:", after)) return true;
|
|
3194
|
+
}
|
|
3195
|
+
return false;
|
|
3196
|
+
}
|
|
3197
|
+
function isShellScriptPath(text) {
|
|
3198
|
+
if (!/\.sh$/i.test(text.slice(-3))) return false;
|
|
3199
|
+
let start = text.length;
|
|
3200
|
+
while (start > 0 && !WHITESPACE_PATTERN.test(text[start - 1] ?? "")) start -= 1;
|
|
3201
|
+
if (start === 0) return true;
|
|
3202
|
+
const slash = text.indexOf("/", start);
|
|
3203
|
+
return slash !== -1 && text.length - (slash + 1) >= 3;
|
|
3204
|
+
}
|
|
3205
|
+
function execOnTerminatorFreeLine(pattern, line) {
|
|
3206
|
+
return LINE_TERMINATOR_PATTERN.test(line) ? null : pattern.exec(line);
|
|
3207
|
+
}
|
|
3208
|
+
var VERSION_OPERATOR_CHARACTERS = /* @__PURE__ */ new Set([">", "<", "=", "~", "!", "^"]);
|
|
3209
|
+
var VERSION_DIGIT_PATTERN = /[\d.]/;
|
|
3210
|
+
function firstVersionConstraint(text) {
|
|
3211
|
+
let index = 0;
|
|
3212
|
+
while (index < text.length) {
|
|
3213
|
+
if (!VERSION_OPERATOR_CHARACTERS.has(text[index] ?? "")) {
|
|
3214
|
+
index += 1;
|
|
3215
|
+
continue;
|
|
3216
|
+
}
|
|
3217
|
+
const start = index;
|
|
3218
|
+
while (index < text.length && VERSION_OPERATOR_CHARACTERS.has(text[index] ?? "")) index += 1;
|
|
3219
|
+
if (index < text.length && VERSION_DIGIT_PATTERN.test(text[index] ?? "")) {
|
|
3220
|
+
let end = index;
|
|
3221
|
+
while (end < text.length && VERSION_DIGIT_PATTERN.test(text[end] ?? "")) end += 1;
|
|
3222
|
+
return text.slice(start, end);
|
|
3223
|
+
}
|
|
3224
|
+
}
|
|
3225
|
+
return void 0;
|
|
3226
|
+
}
|
|
3227
|
+
|
|
3228
|
+
// src/witan/input-binding.ts
|
|
3229
|
+
var INGEST_FRESHNESS_WINDOW_SECONDS = 86400;
|
|
3230
|
+
var MAX_IDENTITIES_PER_INPUT = 8;
|
|
3231
|
+
var MAX_PRODUCER_NAME_LENGTH = 120;
|
|
3232
|
+
var MAX_PRODUCER_VERSION_LENGTH = 80;
|
|
3233
|
+
var MAX_REPOSITORY_LENGTH = 300;
|
|
3234
|
+
var MAX_COMMIT_LENGTH = 80;
|
|
3235
|
+
var COMMIT_PATTERN = /^[0-9a-f]{7,64}$/;
|
|
3236
|
+
var FULL_COMMIT_LENGTHS = /* @__PURE__ */ new Set([40, 64]);
|
|
3237
|
+
function sha256OfBytes(bytes) {
|
|
3238
|
+
return createHash3("sha256").update(bytes).digest("hex");
|
|
3239
|
+
}
|
|
3240
|
+
function normalizeRepositoryIdentity(value) {
|
|
3241
|
+
let text = value.trim();
|
|
3242
|
+
if (text.length === 0 || text.length > 2048) return void 0;
|
|
3243
|
+
const scp = /^[^@/\s]+@([^:/\s]+):(.+)$/.exec(text);
|
|
3244
|
+
if (scp) text = `${scp[1]}/${scp[2]}`;
|
|
3245
|
+
else {
|
|
3246
|
+
const scheme = /^[a-z][a-z0-9+.-]*:\/\/(.*)$/i.exec(text);
|
|
3247
|
+
if (scheme) text = scheme[1] ?? "";
|
|
3248
|
+
const at = text.indexOf("@");
|
|
3249
|
+
const slash = text.indexOf("/");
|
|
3250
|
+
if (at !== -1 && (slash === -1 || at < slash)) text = text.slice(at + 1);
|
|
3251
|
+
}
|
|
3252
|
+
text = stripFromQueryOrFragment(text);
|
|
3253
|
+
const [hostWithPort, ...pathParts] = text.split("/");
|
|
3254
|
+
const host = (hostWithPort ?? "").replace(/:\d+$/, "").toLowerCase();
|
|
3255
|
+
const path = pathParts.filter((part) => part.length > 0).join("/").replace(/(?:\/|\.git)+$/i, "").toLowerCase();
|
|
3256
|
+
if (!/^[a-z0-9.-]+\.[a-z]{2,}$|^localhost$/.test(host) || path.length === 0) return void 0;
|
|
3257
|
+
if (/[\p{Cc}\p{Cf}\s]/u.test(path)) return void 0;
|
|
3258
|
+
if (path.includes("@")) return void 0;
|
|
3259
|
+
const normalized = `${host}/${path}`;
|
|
3260
|
+
return normalized.length <= MAX_REPOSITORY_LENGTH ? normalized : void 0;
|
|
3261
|
+
}
|
|
3262
|
+
function normalizeCommit(value) {
|
|
3263
|
+
if (typeof value !== "string") return void 0;
|
|
3264
|
+
const trimmed = value.trim().toLowerCase();
|
|
3265
|
+
if (trimmed.length === 0) return void 0;
|
|
3266
|
+
return sanitizePresentationLine(trimmed, { fallback: "unknown", maxLength: MAX_COMMIT_LENGTH });
|
|
3267
|
+
}
|
|
3268
|
+
function boundedText(value, maxLength) {
|
|
3269
|
+
if (typeof value !== "string" || value.trim().length === 0) return void 0;
|
|
3270
|
+
return sanitizePresentationLine(value, { fallback: "unknown", maxLength });
|
|
3271
|
+
}
|
|
3272
|
+
function parseObservedAt(value) {
|
|
3273
|
+
if (typeof value !== "string") return void 0;
|
|
3274
|
+
const trimmed = value.trim();
|
|
3275
|
+
const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(trimmed);
|
|
3276
|
+
if (dateOnly) return plainIsoInstant(Date.parse(`${trimmed}T23:59:59Z`));
|
|
3277
|
+
if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:Z|[+-]\d{2}:?\d{2})$/i.test(trimmed)) {
|
|
3278
|
+
return void 0;
|
|
3279
|
+
}
|
|
3280
|
+
return plainIsoInstant(Date.parse(trimmed));
|
|
3281
|
+
}
|
|
3282
|
+
var PLAIN_ISO_INSTANT = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
|
|
3283
|
+
function plainIsoInstant(millis) {
|
|
3284
|
+
if (!Number.isFinite(millis)) return void 0;
|
|
3285
|
+
const date = new Date(millis);
|
|
3286
|
+
if (Number.isNaN(date.getTime())) return void 0;
|
|
3287
|
+
const iso = date.toISOString();
|
|
3288
|
+
return PLAIN_ISO_INSTANT.test(iso) ? iso : void 0;
|
|
3289
|
+
}
|
|
3290
|
+
function earliest(values) {
|
|
3291
|
+
let best;
|
|
3292
|
+
let bestMillis = Number.POSITIVE_INFINITY;
|
|
3293
|
+
for (const value of values) {
|
|
3294
|
+
if (value === void 0) continue;
|
|
3295
|
+
const millis = Date.parse(value);
|
|
3296
|
+
if (Number.isNaN(millis)) continue;
|
|
3297
|
+
if (millis < bestMillis) {
|
|
3298
|
+
best = value;
|
|
3299
|
+
bestMillis = millis;
|
|
3300
|
+
}
|
|
3301
|
+
}
|
|
3302
|
+
return best;
|
|
3303
|
+
}
|
|
3304
|
+
var SubjectSet = class {
|
|
3305
|
+
byKey = /* @__PURE__ */ new Map();
|
|
3306
|
+
add(subject) {
|
|
3307
|
+
const key = JSON.stringify([subject.repository ?? null, subject.repositoryWithheld ?? false, subject.commit ?? null]);
|
|
3308
|
+
if (!this.byKey.has(key)) this.byKey.set(key, subject);
|
|
3309
|
+
}
|
|
3310
|
+
get size() {
|
|
3311
|
+
return this.byKey.size;
|
|
3312
|
+
}
|
|
3313
|
+
values() {
|
|
3314
|
+
return [...this.byKey.values()];
|
|
3315
|
+
}
|
|
3316
|
+
};
|
|
3317
|
+
function pushUnique(items, item) {
|
|
3318
|
+
if (items.length >= MAX_IDENTITIES_PER_INPUT) return;
|
|
3319
|
+
const key = JSON.stringify(item);
|
|
3320
|
+
if (!items.some((existing) => JSON.stringify(existing) === key)) items.push(item);
|
|
3321
|
+
}
|
|
3322
|
+
function subjectFrom(repository, commit) {
|
|
3323
|
+
const declaredRepository = repository !== void 0 && repository !== null && !(typeof repository === "string" && repository.trim().length === 0);
|
|
3324
|
+
const normalizedRepository = typeof repository === "string" && declaredRepository ? normalizeRepositoryIdentity(repository) : void 0;
|
|
3325
|
+
const normalizedCommit = normalizeCommit(commit);
|
|
3326
|
+
if (!declaredRepository && normalizedCommit === void 0) return void 0;
|
|
3327
|
+
return {
|
|
3328
|
+
...normalizedRepository !== void 0 ? { repository: normalizedRepository } : declaredRepository ? { repositoryWithheld: true } : {},
|
|
3329
|
+
...normalizedCommit !== void 0 ? { commit: normalizedCommit } : {}
|
|
3330
|
+
};
|
|
3331
|
+
}
|
|
3332
|
+
function asRecord(value) {
|
|
3333
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : void 0;
|
|
3334
|
+
}
|
|
3335
|
+
function extractScorecardIdentity(raw) {
|
|
3336
|
+
const doc = asRecord(raw) ?? {};
|
|
3337
|
+
const repo = asRecord(doc.repo) ?? {};
|
|
3338
|
+
const scorecard = asRecord(doc.scorecard) ?? {};
|
|
3339
|
+
const subject = subjectFrom(repo.name, repo.commit);
|
|
3340
|
+
const version = boundedText(scorecard.version, MAX_PRODUCER_VERSION_LENGTH);
|
|
3341
|
+
const commit = normalizeCommit(scorecard.commit);
|
|
3342
|
+
const observedAt = parseObservedAt(doc.date);
|
|
3343
|
+
return {
|
|
3344
|
+
producers: [
|
|
3345
|
+
{
|
|
3346
|
+
name: "scorecard",
|
|
3347
|
+
...version !== void 0 ? { version } : {},
|
|
3348
|
+
...commit !== void 0 ? { commit } : {}
|
|
3349
|
+
}
|
|
3350
|
+
],
|
|
3351
|
+
subjects: subject ? [subject] : [],
|
|
3352
|
+
...observedAt !== void 0 ? { observedAt } : {}
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
function extractSarifIdentity(raw) {
|
|
3356
|
+
const doc = asRecord(raw) ?? {};
|
|
3357
|
+
const runs = Array.isArray(doc.runs) ? doc.runs : [];
|
|
3358
|
+
const producers = [];
|
|
3359
|
+
const subjects = new SubjectSet();
|
|
3360
|
+
const times = [];
|
|
3361
|
+
let runWithoutSubject = false;
|
|
3362
|
+
for (const rawRun of runs) {
|
|
3363
|
+
const run = asRecord(rawRun) ?? {};
|
|
3364
|
+
const driver = asRecord(asRecord(run.tool)?.driver) ?? {};
|
|
3365
|
+
const name = boundedText(driver.name, MAX_PRODUCER_NAME_LENGTH) ?? "unknown";
|
|
3366
|
+
const version = boundedText(driver.semanticVersion ?? driver.version, MAX_PRODUCER_VERSION_LENGTH);
|
|
3367
|
+
pushUnique(producers, { name, ...version !== void 0 ? { version } : {} });
|
|
3368
|
+
const provenance = Array.isArray(run.versionControlProvenance) ? run.versionControlProvenance : [];
|
|
3369
|
+
let runDeclaredSubject = false;
|
|
3370
|
+
for (const rawEntry of provenance) {
|
|
3371
|
+
const entry = asRecord(rawEntry) ?? {};
|
|
3372
|
+
const subject = subjectFrom(entry.repositoryUri, entry.revisionId);
|
|
3373
|
+
if (subject) {
|
|
3374
|
+
subjects.add(subject);
|
|
3375
|
+
runDeclaredSubject = true;
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
if (!runDeclaredSubject) runWithoutSubject = true;
|
|
3379
|
+
const invocations = Array.isArray(run.invocations) ? run.invocations : [];
|
|
3380
|
+
for (const rawInvocation of invocations) {
|
|
3381
|
+
const invocation = asRecord(rawInvocation) ?? {};
|
|
3382
|
+
times.push(parseObservedAt(invocation.startTimeUtc), parseObservedAt(invocation.endTimeUtc));
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
if (runWithoutSubject && subjects.size > 0) subjects.add({});
|
|
3386
|
+
const observedAt = earliest(times);
|
|
3387
|
+
return {
|
|
3388
|
+
producers,
|
|
3389
|
+
subjects: subjects.values(),
|
|
3390
|
+
...observedAt !== void 0 ? { observedAt } : {}
|
|
3391
|
+
};
|
|
3392
|
+
}
|
|
3393
|
+
function genericIdentity(fields) {
|
|
3394
|
+
const name = boundedText(fields.producer?.name ?? fields.tool, MAX_PRODUCER_NAME_LENGTH) ?? "unknown";
|
|
3395
|
+
const version = boundedText(fields.producer?.version, MAX_PRODUCER_VERSION_LENGTH);
|
|
3396
|
+
const subject = fields.subject ? subjectFrom(fields.subject.repository, fields.subject.commit) : void 0;
|
|
3397
|
+
const observedAt = parseObservedAt(fields.observedAt);
|
|
3398
|
+
return {
|
|
3399
|
+
producers: [{ name, ...version !== void 0 ? { version } : {} }],
|
|
3400
|
+
subjects: subject ? [subject] : [],
|
|
3401
|
+
...observedAt !== void 0 ? { observedAt } : {}
|
|
3402
|
+
};
|
|
3403
|
+
}
|
|
3404
|
+
function declaresRepository(subject) {
|
|
3405
|
+
return subject.repository !== void 0 || subject.repositoryWithheld === true;
|
|
3406
|
+
}
|
|
3407
|
+
function checkRepository(subjects, scanned) {
|
|
3408
|
+
const declared = subjects.filter(declaresRepository);
|
|
3409
|
+
if (declared.length === 0) return "absent";
|
|
3410
|
+
if (scanned.repository === void 0) return "unverifiable";
|
|
3411
|
+
return declared.every((subject) => subject.repository === scanned.repository) ? "matched" : "mismatched";
|
|
3412
|
+
}
|
|
3413
|
+
function subjectDisagrees(subject, scanned) {
|
|
3414
|
+
if (scanned.repository !== void 0 && declaresRepository(subject)) {
|
|
3415
|
+
if (subject.repository !== scanned.repository) return true;
|
|
3416
|
+
}
|
|
3417
|
+
if (scanned.commit !== void 0 && subject.commit !== void 0) {
|
|
3418
|
+
if (!commitMatches(subject.commit, scanned.commit)) return true;
|
|
3419
|
+
}
|
|
3420
|
+
return false;
|
|
3421
|
+
}
|
|
3422
|
+
function commitMatches(inputCommit, headSha) {
|
|
3423
|
+
if (!COMMIT_PATTERN.test(inputCommit)) return false;
|
|
3424
|
+
const head = headSha.toLowerCase();
|
|
3425
|
+
if (FULL_COMMIT_LENGTHS.has(inputCommit.length)) return inputCommit === head;
|
|
3426
|
+
return head.startsWith(inputCommit);
|
|
3427
|
+
}
|
|
3428
|
+
function checkCommit(subjects, scanned) {
|
|
3429
|
+
const commits = subjects.map((subject) => subject.commit).filter((commit) => commit !== void 0);
|
|
3430
|
+
if (commits.length === 0) return "absent";
|
|
3431
|
+
if (scanned.commit === void 0) return "unverifiable";
|
|
3432
|
+
const head = scanned.commit;
|
|
3433
|
+
return commits.every((commit) => commitMatches(commit, head)) ? "matched" : "mismatched";
|
|
3434
|
+
}
|
|
3435
|
+
function checkFreshness(observedAt, scanned, windowSeconds) {
|
|
3436
|
+
if (observedAt === void 0) return "absent";
|
|
3437
|
+
if (scanned.commitTime === void 0) return "unverifiable";
|
|
3438
|
+
const observed = Date.parse(observedAt);
|
|
3439
|
+
const committed = Date.parse(scanned.commitTime);
|
|
3440
|
+
return observed < committed - windowSeconds * 1e3 ? "stale" : "within-window";
|
|
3441
|
+
}
|
|
3442
|
+
function bindingStatusForChecks(checks, everySubjectDeclaresCommit = true) {
|
|
3443
|
+
if (checks.repository === "mismatched") return "mismatched-repository";
|
|
3444
|
+
if (checks.commit === "mismatched") return "mismatched-commit";
|
|
3445
|
+
if (checks.freshness === "stale") return "stale";
|
|
3446
|
+
if (checks.commit === "matched" && !everySubjectDeclaresCommit) return "unbound";
|
|
3447
|
+
if (checks.commit === "matched") return "matched";
|
|
3448
|
+
if (checks.commit === "unverifiable") return "unverifiable";
|
|
3449
|
+
return "unbound";
|
|
3450
|
+
}
|
|
3451
|
+
function recordedSubjects(subjects, scanned) {
|
|
3452
|
+
if (subjects.length <= MAX_IDENTITIES_PER_INPUT) return { subjects: [...subjects], omitted: 0 };
|
|
3453
|
+
const rank = (subject) => subjectDisagrees(subject, scanned) ? 0 : subject.commit === void 0 ? 1 : 2;
|
|
3454
|
+
const ordered = subjects.map((subject, index) => ({ subject, index, rank: rank(subject) })).sort((a, b) => a.rank - b.rank || a.index - b.index).slice(0, MAX_IDENTITIES_PER_INPUT).map((entry) => entry.subject);
|
|
3455
|
+
return { subjects: ordered, omitted: subjects.length - MAX_IDENTITIES_PER_INPUT };
|
|
3456
|
+
}
|
|
3457
|
+
function evaluateInputBindingRecord(document, scanned, windowSeconds = INGEST_FRESHNESS_WINDOW_SECONDS) {
|
|
3458
|
+
const allSubjects = document.identity.subjects;
|
|
3459
|
+
const checks = {
|
|
3460
|
+
repository: checkRepository(allSubjects, scanned),
|
|
3461
|
+
commit: checkCommit(allSubjects, scanned),
|
|
3462
|
+
freshness: checkFreshness(document.identity.observedAt, scanned, windowSeconds)
|
|
3463
|
+
};
|
|
3464
|
+
const recorded = recordedSubjects(allSubjects, scanned);
|
|
3465
|
+
return {
|
|
3466
|
+
sha256: document.sha256,
|
|
3467
|
+
format: document.format,
|
|
3468
|
+
provenance: document.provenance,
|
|
3469
|
+
producers: [...document.identity.producers],
|
|
3470
|
+
subjects: recorded.subjects,
|
|
3471
|
+
...recorded.omitted > 0 ? { subjectsOmitted: recorded.omitted } : {},
|
|
3472
|
+
...document.identity.observedAt !== void 0 ? { observedAt: document.identity.observedAt } : {},
|
|
3473
|
+
status: bindingStatusForChecks(
|
|
3474
|
+
checks,
|
|
3475
|
+
allSubjects.every((subject) => subject.commit !== void 0)
|
|
3476
|
+
),
|
|
3477
|
+
checks,
|
|
3478
|
+
...document.duplicate !== void 0 ? { duplicate: document.duplicate } : {},
|
|
3479
|
+
scored: document.duplicate !== "same-path"
|
|
3480
|
+
};
|
|
3481
|
+
}
|
|
3482
|
+
function compareRecords(a, b) {
|
|
3483
|
+
return a.sha256.localeCompare(b.sha256) || (a.duplicate ?? "").localeCompare(b.duplicate ?? "") || a.provenance.localeCompare(b.provenance);
|
|
3484
|
+
}
|
|
3485
|
+
function buildInputBinding(documents, scanned, policy, windowSeconds = INGEST_FRESHNESS_WINDOW_SECONDS) {
|
|
3486
|
+
return {
|
|
3487
|
+
policy,
|
|
3488
|
+
freshnessWindowSeconds: windowSeconds,
|
|
3489
|
+
scannedSubject: {
|
|
3490
|
+
...scanned.repository !== void 0 ? { repository: scanned.repository } : {},
|
|
3491
|
+
...scanned.commit !== void 0 ? { commit: scanned.commit } : {},
|
|
3492
|
+
...scanned.commitTime !== void 0 ? { commitTime: scanned.commitTime } : {}
|
|
3493
|
+
},
|
|
3494
|
+
inputs: documents.map((document) => evaluateInputBindingRecord(document, scanned, windowSeconds)).sort(compareRecords)
|
|
3495
|
+
};
|
|
3496
|
+
}
|
|
3497
|
+
var STRICT_INGEST_REFUSED_STATUSES = /* @__PURE__ */ new Set([
|
|
3498
|
+
"mismatched-repository",
|
|
3499
|
+
"mismatched-commit",
|
|
3500
|
+
"stale"
|
|
3501
|
+
]);
|
|
3502
|
+
function strictIngestRefusalReasons(record) {
|
|
3503
|
+
const reasons = [];
|
|
3504
|
+
if (STRICT_INGEST_REFUSED_STATUSES.has(record.status)) reasons.push(record.status);
|
|
3505
|
+
if (record.duplicate !== void 0) reasons.push(`duplicate (${record.duplicate})`);
|
|
3506
|
+
return reasons;
|
|
3507
|
+
}
|
|
3508
|
+
function describeSubjects(record) {
|
|
3509
|
+
if (record.subjects.length === 0) return "no subject";
|
|
3510
|
+
const listed = record.subjects.map(
|
|
3511
|
+
(subject) => `${subject.repository ?? (subject.repositoryWithheld ? "(repository withheld: not normalizable)" : "?")}@${subject.commit ?? "?"}`
|
|
3512
|
+
).join(", ");
|
|
3513
|
+
return record.subjectsOmitted ? `${listed} and ${record.subjectsOmitted} more` : listed;
|
|
3514
|
+
}
|
|
3515
|
+
function assertStrictIngestBinding(documents, binding) {
|
|
3516
|
+
const problems = [];
|
|
3517
|
+
for (const document of documents) {
|
|
3518
|
+
const record = evaluateInputBindingRecord(
|
|
3519
|
+
document,
|
|
3520
|
+
binding.scannedSubject,
|
|
3521
|
+
binding.freshnessWindowSeconds
|
|
3522
|
+
);
|
|
3523
|
+
const reasons = strictIngestRefusalReasons(record);
|
|
3524
|
+
if (reasons.length === 0) continue;
|
|
3525
|
+
problems.push(
|
|
3526
|
+
// displayPath is the operator's or repository's own path text; a committed file name can
|
|
3527
|
+
// carry terminal controls, and this message goes to stderr.
|
|
3528
|
+
` - ${stripTerminalControls(document.displayPath)} (sha256:${record.sha256.slice(0, 12)}, ${record.format}): ${reasons.join(", ")}; input subject ${describeSubjects(record)}${record.observedAt ? `, observed ${record.observedAt}` : ""}`
|
|
3529
|
+
);
|
|
3530
|
+
}
|
|
3531
|
+
if (problems.length === 0) return;
|
|
3532
|
+
const scanned = binding.scannedSubject;
|
|
3533
|
+
throw new Error(
|
|
3534
|
+
[
|
|
3535
|
+
`Cejel: --strict-ingest refused ${problems.length} ingest input${problems.length === 1 ? "" : "s"}; no certificate was written.`,
|
|
3536
|
+
...problems,
|
|
3537
|
+
` scanned subject: ${scanned.repository ?? "(repository not named; pass --subject-repository to compare it)"}@${scanned.commit ?? "(no git HEAD)"}${scanned.commitTime ? `, committed ${scanned.commitTime}` : ""}; freshness window ${binding.freshnessWindowSeconds}s before that commit time.`,
|
|
3538
|
+
" Supply inputs produced for this repository at this commit, remove duplicates, or drop --strict-ingest to record the binding status without refusing."
|
|
3539
|
+
].join("\n")
|
|
3540
|
+
);
|
|
3541
|
+
}
|
|
3542
|
+
function verifyRecordedInputDigests(report, inputPaths, readFile) {
|
|
3543
|
+
const binding = report.inputBinding;
|
|
3544
|
+
if (!binding) {
|
|
3545
|
+
return {
|
|
3546
|
+
valid: false,
|
|
3547
|
+
lines: [
|
|
3548
|
+
"Cejel: input digests not verified \u2014 report.json records no inputBinding (it ingested nothing, or predates report format 1.7)."
|
|
3549
|
+
]
|
|
3550
|
+
};
|
|
3551
|
+
}
|
|
3552
|
+
const recorded = new Set(binding.inputs.filter((input) => input.scored).map((input) => input.sha256));
|
|
3553
|
+
const supplied = /* @__PURE__ */ new Map();
|
|
3554
|
+
for (const path of inputPaths) supplied.set(sha256OfBytes(readFile(path)), path);
|
|
3555
|
+
const lines = [];
|
|
3556
|
+
for (const [digest, path] of supplied) {
|
|
3557
|
+
if (!recorded.has(digest)) {
|
|
3558
|
+
lines.push(`Cejel: input ${path} (sha256:${digest}) is not an input report.json records.`);
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
for (const digest of recorded) {
|
|
3562
|
+
if (!supplied.has(digest)) {
|
|
3563
|
+
lines.push(`Cejel: report.json records input sha256:${digest}, which no supplied --input matches.`);
|
|
3564
|
+
}
|
|
3565
|
+
}
|
|
3566
|
+
if (lines.length > 0) return { valid: false, lines };
|
|
3567
|
+
return {
|
|
3568
|
+
valid: true,
|
|
3569
|
+
lines: [`Cejel: ${recorded.size} ingested input digest${recorded.size === 1 ? "" : "s"} verified against report.json.`]
|
|
3570
|
+
};
|
|
3571
|
+
}
|
|
3572
|
+
|
|
3573
|
+
// src/witan/ingest-files.ts
|
|
3574
|
+
init_esm_shims();
|
|
3575
|
+
import { lstatSync, realpathSync } from "fs";
|
|
3576
|
+
import { dirname, isAbsolute, relative, resolve, sep } from "path";
|
|
3577
|
+
var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
|
|
3578
|
+
function isContainedPath(candidate, directory) {
|
|
3579
|
+
const relativePath = relative(directory, candidate);
|
|
3580
|
+
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath);
|
|
3581
|
+
}
|
|
3582
|
+
function isMissingIngestPath(error) {
|
|
3583
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
3584
|
+
}
|
|
3585
|
+
function resolveIngestFilePath(filePath, inputDirectory = dirname(filePath)) {
|
|
3586
|
+
const lexicalDirectory = resolve(inputDirectory);
|
|
3587
|
+
const lexicalCandidate = resolve(filePath);
|
|
3588
|
+
const directoryStat = lstatSync(lexicalDirectory);
|
|
3589
|
+
if (directoryStat.isSymbolicLink()) {
|
|
3590
|
+
throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
|
|
3591
|
+
}
|
|
3592
|
+
if (!directoryStat.isDirectory()) {
|
|
3593
|
+
throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
|
|
3594
|
+
}
|
|
3595
|
+
const candidateStat = lstatSync(lexicalCandidate);
|
|
3596
|
+
if (candidateStat.isSymbolicLink()) {
|
|
3597
|
+
throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
|
|
3598
|
+
}
|
|
3599
|
+
if (!candidateStat.isFile()) {
|
|
3600
|
+
throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
|
|
3601
|
+
}
|
|
3602
|
+
if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
|
|
3603
|
+
throw new Error(
|
|
3604
|
+
`Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
|
|
3605
|
+
);
|
|
3606
|
+
}
|
|
3607
|
+
const resolvedDirectory = realpathSync(lexicalDirectory);
|
|
3608
|
+
const resolvedCandidate = realpathSync(lexicalCandidate);
|
|
3609
|
+
if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
|
|
3610
|
+
throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
|
|
3611
|
+
}
|
|
3612
|
+
return resolvedCandidate;
|
|
3613
|
+
}
|
|
3614
|
+
function isResolvedIngestPathContained(candidate, directory) {
|
|
3615
|
+
return isContainedPath(candidate, directory);
|
|
3616
|
+
}
|
|
3617
|
+
|
|
3618
|
+
// src/witan/index.ts
|
|
3619
|
+
init_esm_shims();
|
|
3620
|
+
|
|
3621
|
+
// src/witan/markdown.ts
|
|
3622
|
+
init_esm_shims();
|
|
3623
|
+
|
|
3624
|
+
// src/witan/repo-signals.ts
|
|
3625
|
+
init_esm_shims();
|
|
3626
|
+
import { createHash as createHash4 } from "crypto";
|
|
2914
3627
|
import {
|
|
2915
|
-
lstatSync,
|
|
3628
|
+
lstatSync as lstatSync2,
|
|
2916
3629
|
readdirSync,
|
|
2917
|
-
realpathSync,
|
|
3630
|
+
realpathSync as realpathSync2,
|
|
2918
3631
|
statSync
|
|
2919
3632
|
} from "fs";
|
|
2920
|
-
import { basename, dirname, extname, join, posix, relative, resolve, sep } from "path";
|
|
3633
|
+
import { basename, dirname as dirname2, extname, join, posix, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
3634
|
+
|
|
3635
|
+
// src/witan/history-depth.ts
|
|
3636
|
+
init_esm_shims();
|
|
3637
|
+
var SHALLOW_HISTORY_LIMITATION = "This repository is a shallow clone (git rev-parse --is-shallow-repository reports true), so its commit history is truncated. A2 (credentials and .env files in earlier commits) and B2 (pull-request merge ratio over recent commits) read history and saw only the commits present; on a full clone their results may differ. Scores are computed exactly as for any other scan. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
|
|
3638
|
+
var SHALLOW_HISTORY_V27_ABSTENTION = "This repository is a shallow clone (git rev-parse --is-shallow-repository reports true), so its commit history is truncated. A2 (credentials and .env files in earlier commits) and B2 (pull-request merge ratio over recent commits) read history. Under this rubric, where either would be scored, A2 abstains unless a confirmed critical secret finding stands, and B2 abstains when fewer commits are visible than it reads; each abstention counts as 0 in the composite, as any evidence Cejel expected to read and could not does. A criterion that is not applicable stays not applicable. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
|
|
3639
|
+
function isShallowRepository(repoPath) {
|
|
3640
|
+
const result = execGit(["rev-parse", "--is-shallow-repository"], { cwd: repoPath });
|
|
3641
|
+
return result.ok && result.stdout.trim() === "true";
|
|
3642
|
+
}
|
|
3643
|
+
function detectHistoryLimitations(repoPath, v27Abstains = false) {
|
|
3644
|
+
if (!isShallowRepository(repoPath)) return void 0;
|
|
3645
|
+
return [v27Abstains ? SHALLOW_HISTORY_V27_ABSTENTION : SHALLOW_HISTORY_LIMITATION];
|
|
3646
|
+
}
|
|
2921
3647
|
|
|
2922
3648
|
// src/witan/json-safe.ts
|
|
2923
3649
|
init_esm_shims();
|
|
@@ -2927,7 +3653,10 @@ function stripBom(text) {
|
|
|
2927
3653
|
|
|
2928
3654
|
// src/witan/repo-signals.ts
|
|
2929
3655
|
function usesV17DetectorClosure2(rubricVersion) {
|
|
2930
|
-
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 || rubricVersion === WITAN_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26;
|
|
3656
|
+
return rubricVersion === WITAN_RUBRIC_VERSION_V17 || rubricVersion === WITAN_RUBRIC_VERSION_V27 || 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_RUBRIC_VERSION_V24 || rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26;
|
|
3657
|
+
}
|
|
3658
|
+
function usesV27FalseAssuranceClosure(rubricVersion) {
|
|
3659
|
+
return rubricVersion === WITAN_RUBRIC_VERSION_V27;
|
|
2931
3660
|
}
|
|
2932
3661
|
var LEGACY_INVENTORY_ABSENCE_CONTEXT = {
|
|
2933
3662
|
rubricGeneration: "legacy",
|
|
@@ -3027,9 +3756,16 @@ function wholesaleCriterionAbstention(signal, reasons) {
|
|
|
3027
3756
|
metrics: [],
|
|
3028
3757
|
insufficientData: true,
|
|
3029
3758
|
...insufficientDataReason !== void 0 ? { insufficientDataReason } : {},
|
|
3030
|
-
|
|
3759
|
+
// What the collector read and set aside is still true of the content it did read. B3's names
|
|
3760
|
+
// workflows in a count this abstention removes, so it goes with the count.
|
|
3761
|
+
...withoutCountDisclosures(signal.disclosures),
|
|
3762
|
+
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." : reasons?.size === 1 && reasons.has("lfs_pointer") ? "Cejel abstained on this criterion because relevant files in this checkout are Git LFS pointers, so their content was not read. Fetch the LFS content (git lfs pull) and re-scan." : "Cejel abstained on this criterion because one or more relevant repository files could not be read. Resolve the content-read limitations and re-scan."
|
|
3031
3763
|
};
|
|
3032
3764
|
}
|
|
3765
|
+
function withoutCountDisclosures(disclosures) {
|
|
3766
|
+
const kept = (disclosures ?? []).filter((text) => !text.startsWith(WITAN_B3_UNSHOWN_CHECK_PREFIX));
|
|
3767
|
+
return kept.length > 0 ? { disclosures: kept } : {};
|
|
3768
|
+
}
|
|
3033
3769
|
function skipReasonsForCriterion(criterionId, tracked) {
|
|
3034
3770
|
const combined = new Set(tracked.criterionSkipReasons.get(criterionId) ?? []);
|
|
3035
3771
|
const prefix = contentReadSignalKey(criterionId, "");
|
|
@@ -3044,15 +3780,24 @@ function skipReasonsForSignal(criterionId, signalName, tracked) {
|
|
|
3044
3780
|
return reasons ? new Set(reasons) : void 0;
|
|
3045
3781
|
}
|
|
3046
3782
|
function buildWitanInputFromRepo(options) {
|
|
3047
|
-
const tracked = trackContentReads(options.repoPath, () => buildWitanInputFromRepoUntracked(options));
|
|
3048
|
-
const affectedCriteria = tracked.affectedCriteria;
|
|
3049
3783
|
const rubricVersion = options.rubricVersion ?? WITAN_RUBRIC_VERSION;
|
|
3784
|
+
const tracked = trackContentReads(
|
|
3785
|
+
options.repoPath,
|
|
3786
|
+
() => buildWitanInputFromRepoUntracked(options),
|
|
3787
|
+
{ lfsPointerAbstention: usesV27FalseAssuranceClosure(rubricVersion) }
|
|
3788
|
+
);
|
|
3789
|
+
const affectedCriteria = tracked.affectedCriteria;
|
|
3050
3790
|
if (rubricVersion !== WITAN_RUBRIC_VERSION_V23) {
|
|
3051
3791
|
const readFailureCriteria = [];
|
|
3052
3792
|
const coverageLimitedCriteria = [];
|
|
3793
|
+
const lfsKept = /* @__PURE__ */ new Set();
|
|
3053
3794
|
const signals2 = (tracked.value.signals ?? []).map((signal) => {
|
|
3054
3795
|
if (!affectedCriteria.has(signal.criterionId)) return signal;
|
|
3055
3796
|
const reasons = skipReasonsForCriterion(signal.criterionId, tracked);
|
|
3797
|
+
if (usesV27FalseAssuranceClosure(rubricVersion) && reasons.size > 0 && [...reasons].every((reason) => reason === "lfs_pointer") && (signal.findings ?? []).some(({ severity }) => severity === "critical")) {
|
|
3798
|
+
lfsKept.add(signal.criterionId);
|
|
3799
|
+
return signal;
|
|
3800
|
+
}
|
|
3056
3801
|
const { insufficientDataReason } = classifyAbstentionReason(reasons);
|
|
3057
3802
|
(insufficientDataReason !== void 0 ? coverageLimitedCriteria : readFailureCriteria).push(
|
|
3058
3803
|
signal.criterionId
|
|
@@ -3074,7 +3819,10 @@ function buildWitanInputFromRepo(options) {
|
|
|
3074
3819
|
...tracked.value,
|
|
3075
3820
|
signals: signals2,
|
|
3076
3821
|
...scanLimitations2.length > 0 ? { scanLimitations: scanLimitations2 } : {},
|
|
3077
|
-
contentReadSummary:
|
|
3822
|
+
contentReadSummary: lfsKept.size > 0 ? {
|
|
3823
|
+
...tracked.summary,
|
|
3824
|
+
affectedCriteria: tracked.summary.affectedCriteria.filter((id) => !lfsKept.has(id))
|
|
3825
|
+
} : tracked.summary
|
|
3078
3826
|
};
|
|
3079
3827
|
}
|
|
3080
3828
|
const wholesaleAbstainedReadFailureCriteria = [];
|
|
@@ -3205,6 +3953,8 @@ function buildWitanInputFromRepoUntracked(options) {
|
|
|
3205
3953
|
const usesV24SecretContentContext = rubricVersion === WITAN_RUBRIC_VERSION_V24;
|
|
3206
3954
|
const usesV25InlineHealthRoute = rubricVersion === WITAN_RUBRIC_VERSION_V25 || rubricVersion === WITAN_RUBRIC_VERSION_V26;
|
|
3207
3955
|
const usesV26SettlingEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
|
|
3956
|
+
const usesV26BuildStepEvidence = rubricVersion === WITAN_RUBRIC_VERSION_V26;
|
|
3957
|
+
const usesV27Closure = usesV27FalseAssuranceClosure(rubricVersion);
|
|
3208
3958
|
const structuralArchetype = classifyRepoArchetype(inventoryFiles, rubricVersion);
|
|
3209
3959
|
const readableArchetype = rubricVersion === WITAN_RUBRIC_VERSION_V13 || rubricVersion === WITAN_RUBRIC_VERSION_V14 || rubricVersion === WITAN_RUBRIC_VERSION_V15 || rubricVersion === WITAN_RUBRIC_VERSION_V16 || usesV17DetectorClosure2(rubricVersion) ? applyReadableSourceRepresentationGate(
|
|
3210
3960
|
options.repoPath,
|
|
@@ -3246,6 +3996,8 @@ function buildWitanInputFromRepoUntracked(options) {
|
|
|
3246
3996
|
usesV24SecretContentContext,
|
|
3247
3997
|
usesV25InlineHealthRoute,
|
|
3248
3998
|
usesV26SettlingEvidence,
|
|
3999
|
+
usesV26BuildStepEvidence,
|
|
4000
|
+
usesV27Closure,
|
|
3249
4001
|
reviewableSourceProof,
|
|
3250
4002
|
inventoryAbsenceContext,
|
|
3251
4003
|
scanLimitations
|
|
@@ -3583,7 +4335,7 @@ function isRepresentativeSourceText(contents) {
|
|
|
3583
4335
|
return disallowedControls / contents.length <= 0.01;
|
|
3584
4336
|
}
|
|
3585
4337
|
function representativeSourceRank(rankKey, file) {
|
|
3586
|
-
return
|
|
4338
|
+
return createHash4("sha256").update(`${rankKey}:${file}`).digest("hex");
|
|
3587
4339
|
}
|
|
3588
4340
|
function explainNoMeasurementSourceCoverage(repoPath, rubricVersion) {
|
|
3589
4341
|
if (!usesV17DetectorClosure2(rubricVersion)) return void 0;
|
|
@@ -3835,9 +4587,9 @@ function explainIgnoredScanTarget(repoPath) {
|
|
|
3835
4587
|
}
|
|
3836
4588
|
const top = topResult.stdout.trim();
|
|
3837
4589
|
if (!top) return void 0;
|
|
3838
|
-
const topResolved =
|
|
3839
|
-
const targetResolved =
|
|
3840
|
-
const target =
|
|
4590
|
+
const topResolved = realpathSync2(top);
|
|
4591
|
+
const targetResolved = realpathSync2(repoPath);
|
|
4592
|
+
const target = relative2(topResolved, targetResolved).split(sep2).join("/");
|
|
3841
4593
|
if (!target || target.startsWith("../")) return void 0;
|
|
3842
4594
|
const ignored = execGit(["check-ignore", "--quiet", "--", `${target}/`], {
|
|
3843
4595
|
cwd: topResolved
|
|
@@ -3860,7 +4612,7 @@ function buildNotApplicableSignal(criterionId, reason) {
|
|
|
3860
4612
|
notApplicable: true
|
|
3861
4613
|
};
|
|
3862
4614
|
}
|
|
3863
|
-
function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23StrictCoverageAndRecipeRunners, useV23PemPrivateKeyGrammar, useV23WithheldPathAbstention, useV24SecretContentContext, useV25InlineHealthRoute, useV26SettlingEvidence, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
4615
|
+
function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV18NativeRls, useV19CommitYear, useV20A3ExplicitGaps, useV21ExecutedEscalations, useV22PackageStartEntrypoint, useV23StrictCoverageAndRecipeRunners, useV23PemPrivateKeyGrammar, useV23WithheldPathAbstention, useV24SecretContentContext, useV25InlineHealthRoute, useV26SettlingEvidence, useV26BuildStepEvidence, useV27FalseAssuranceClosure, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, scanLimitations = /* @__PURE__ */ new Set()) {
|
|
3864
4616
|
const signals = [];
|
|
3865
4617
|
const mono = resolveMonorepoContext(repoPath, scanLimitations);
|
|
3866
4618
|
const collectCriterion = (criterionId, collect) => withContentReadCriterion(criterionId, collect);
|
|
@@ -3874,7 +4626,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
3874
4626
|
useV36Detectors,
|
|
3875
4627
|
useV23StrictCoverageAndRecipeRunners,
|
|
3876
4628
|
reviewableSourceProof,
|
|
3877
|
-
inventoryAbsenceContext
|
|
4629
|
+
inventoryAbsenceContext,
|
|
4630
|
+
useV27FalseAssuranceClosure
|
|
3878
4631
|
)
|
|
3879
4632
|
);
|
|
3880
4633
|
const a2Signal = collectCriterion(
|
|
@@ -3892,7 +4645,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
3892
4645
|
useV18NativeRls,
|
|
3893
4646
|
inventoryAbsenceContext,
|
|
3894
4647
|
scanLimitations,
|
|
3895
|
-
useV26SettlingEvidence
|
|
4648
|
+
useV26SettlingEvidence,
|
|
4649
|
+
useV27FalseAssuranceClosure
|
|
3896
4650
|
)
|
|
3897
4651
|
);
|
|
3898
4652
|
const a3Signal = collectCriterion(
|
|
@@ -3905,7 +4659,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
3905
4659
|
useV22PackageStartEntrypoint,
|
|
3906
4660
|
useV23WithheldPathAbstention,
|
|
3907
4661
|
inventoryAbsenceContext,
|
|
3908
|
-
useV25InlineHealthRoute
|
|
4662
|
+
useV25InlineHealthRoute,
|
|
4663
|
+
useV26BuildStepEvidence
|
|
3909
4664
|
)
|
|
3910
4665
|
);
|
|
3911
4666
|
const a4Signal = collectCriterion(
|
|
@@ -3916,7 +4671,8 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
3916
4671
|
mono,
|
|
3917
4672
|
useV27Detectors,
|
|
3918
4673
|
useV47Detectors,
|
|
3919
|
-
inventoryAbsenceContext
|
|
4674
|
+
inventoryAbsenceContext,
|
|
4675
|
+
useV27FalseAssuranceClosure
|
|
3920
4676
|
)
|
|
3921
4677
|
);
|
|
3922
4678
|
const a5Signal = collectCriterion(
|
|
@@ -3934,10 +4690,13 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
3934
4690
|
"B1",
|
|
3935
4691
|
"Repository scans do not evaluate the dispatch-trace process dimension."
|
|
3936
4692
|
);
|
|
3937
|
-
const b2Signal = collectCriterion(
|
|
4693
|
+
const b2Signal = collectCriterion(
|
|
4694
|
+
"B2",
|
|
4695
|
+
() => collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure)
|
|
4696
|
+
);
|
|
3938
4697
|
const b3Signal = collectCriterion(
|
|
3939
4698
|
"B3",
|
|
3940
|
-
() => collectB3CiDisciplineEvidence(repoPath, repoFiles, mono)
|
|
4699
|
+
() => collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAssuranceClosure)
|
|
3941
4700
|
);
|
|
3942
4701
|
const b4Signal = collectCriterion(
|
|
3943
4702
|
"B4",
|
|
@@ -3979,14 +4738,16 @@ function collectRepoSignals(repoPath, generatedAt, repoFiles, useV27Detectors, u
|
|
|
3979
4738
|
}
|
|
3980
4739
|
return signals;
|
|
3981
4740
|
}
|
|
3982
|
-
function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23StrictCoverageAndRecipeRunners, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
4741
|
+
function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV23StrictCoverageAndRecipeRunners, reviewableSourceProof, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV27FalseAssuranceClosure = false) {
|
|
3983
4742
|
const evidence = [];
|
|
3984
4743
|
const findings = [];
|
|
3985
|
-
const
|
|
4744
|
+
const v17TestFiles = findConcreteTestFiles(
|
|
3986
4745
|
repoPath,
|
|
3987
4746
|
repoFiles,
|
|
3988
4747
|
reviewableSourceProof ? WITAN_RUBRIC_VERSION_V17 : void 0
|
|
3989
4748
|
);
|
|
4749
|
+
const allTestFiles = useV27FalseAssuranceClosure ? v17TestFiles.filter((file) => !PYTEST_CONFTEST_PATTERN.test(file)) : v17TestFiles;
|
|
4750
|
+
const runWideSkips = useV27FalseAssuranceClosure ? findPytestRunWideSkips(repoPath, repoFiles) : [];
|
|
3990
4751
|
const testFiles = allTestFiles.slice(0, 8);
|
|
3991
4752
|
const sourceFiles = repoFiles.filter(isSourceFile);
|
|
3992
4753
|
const runnerFiles = repoFiles.filter(
|
|
@@ -4042,13 +4803,31 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
4042
4803
|
const verificationScriptCount = verificationScriptComponents.filter((c) => c.count > 0).length;
|
|
4043
4804
|
const usesLeanBuiltInTestRunner = packageJson != null && [...packageScripts.values()].some(
|
|
4044
4805
|
(script) => LEAN_TEST_RUNNER_SCRIPT_PATTERN.test(script)
|
|
4045
|
-
) ||
|
|
4806
|
+
) || // v17's first eight, under every rubric: v27's conftest exclusion must not move a file into
|
|
4807
|
+
// this sample and change the finding below.
|
|
4808
|
+
v17TestFiles.slice(0, 8).some((file) => fileContains(repoPath, file, LEAN_TEST_RUNNER_IMPORT_PATTERN));
|
|
4046
4809
|
const hasHeavyTestDependency = packageJsonFiles.some(
|
|
4047
4810
|
(manifest) => packageJsonHasHeavyTestDependency(repoPath, manifest)
|
|
4048
4811
|
);
|
|
4049
4812
|
const isLeanTestToolchain = usesLeanBuiltInTestRunner && !hasHeavyTestDependency;
|
|
4050
4813
|
for (const file of testFiles) {
|
|
4051
|
-
|
|
4814
|
+
const testPath = join(repoPath, file);
|
|
4815
|
+
const isLfsPointer = isRegularFile(testPath) && isGitLfsPointerText(readRepoText(testPath, "utf8"));
|
|
4816
|
+
evidence.push(
|
|
4817
|
+
evidenceForRelative(
|
|
4818
|
+
repoPath,
|
|
4819
|
+
file,
|
|
4820
|
+
"test_run",
|
|
4821
|
+
isLfsPointer ? "Detected test file (Git LFS pointer; its content is not in this checkout)" : "Detected test file"
|
|
4822
|
+
)
|
|
4823
|
+
);
|
|
4824
|
+
}
|
|
4825
|
+
if (useV27FalseAssuranceClosure && allTestFiles.length === 0) {
|
|
4826
|
+
for (const file of v17TestFiles.slice(0, 8)) {
|
|
4827
|
+
evidence.push(
|
|
4828
|
+
evidenceForRelative(repoPath, file, "test_run", "pytest configuration (conftest.py), not a test file")
|
|
4829
|
+
);
|
|
4830
|
+
}
|
|
4052
4831
|
}
|
|
4053
4832
|
for (const file of runnerFiles) {
|
|
4054
4833
|
evidence.push(evidenceForRelative(repoPath, file, "test_run", "Configured test runner"));
|
|
@@ -4067,7 +4846,7 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
4067
4846
|
);
|
|
4068
4847
|
}
|
|
4069
4848
|
const scheduledHealthWorkflow = ciWorkflows.find(
|
|
4070
|
-
(file) =>
|
|
4849
|
+
(file) => fileSatisfies(repoPath, file, hasScheduleCronTrigger) && fileContains(repoPath, file, CI_TEST_COMMAND_PATTERN) && (!useV36Detectors || SCHEDULED_HEALTH_PATH_PATTERN.test(file) || fileContains(repoPath, file, SCHEDULED_HEALTH_INTENT_PATTERN))
|
|
4071
4850
|
);
|
|
4072
4851
|
if (scheduledHealthWorkflow) {
|
|
4073
4852
|
const isPublished = fileContains(
|
|
@@ -4168,8 +4947,30 @@ function collectA1TestIntegrityEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
4168
4947
|
const nonHollowShare = withContentReadSignal(
|
|
4169
4948
|
"A1",
|
|
4170
4949
|
"non_hollow_test_share",
|
|
4171
|
-
() => measureNonHollowTestShare(
|
|
4950
|
+
() => measureNonHollowTestShare(
|
|
4951
|
+
repoPath,
|
|
4952
|
+
v17TestFiles,
|
|
4953
|
+
reviewableSourceProof !== void 0,
|
|
4954
|
+
useV27FalseAssuranceClosure ? {
|
|
4955
|
+
disabledByRunWideSkip: (file) => runWideSkips.some((skip) => isGovernedPytestFile(skip, file))
|
|
4956
|
+
} : void 0
|
|
4957
|
+
)
|
|
4172
4958
|
);
|
|
4959
|
+
for (const skip of runWideSkips.slice(0, 2)) {
|
|
4960
|
+
const governed = allTestFiles.filter((file) => isGovernedPytestFile(skip, file)).length;
|
|
4961
|
+
if (governed === 0) continue;
|
|
4962
|
+
findings.push({
|
|
4963
|
+
severity: "warning",
|
|
4964
|
+
summary: `A pytest collection hook in ${skip.path} ${skip.kind === "skip" ? "marks every collected test skipped" : "removes every collected test"}, so the ${governed} pytest file${governed === 1 ? "" : "s"} under ${skip.scope === "" ? "the repository root" : `${skip.scope}/`} cannot run.`,
|
|
4965
|
+
evidence: evidenceForRelativeAtLine(
|
|
4966
|
+
repoPath,
|
|
4967
|
+
skip.path,
|
|
4968
|
+
"test_run",
|
|
4969
|
+
"Run-wide pytest skip hook",
|
|
4970
|
+
skip.line
|
|
4971
|
+
)
|
|
4972
|
+
});
|
|
4973
|
+
}
|
|
4173
4974
|
return {
|
|
4174
4975
|
criterionId: "A1",
|
|
4175
4976
|
positiveEvidence: evidence,
|
|
@@ -4306,7 +5107,7 @@ function buildA1AuthenticatedAbsenceSignal(repoPath, proof, inventoryCount, inve
|
|
|
4306
5107
|
notes: `V17 authenticated absence: ${proof.eligibleSourceFileCount} criterion-ratable of ${proof.sourceShapedFileCount} source-shaped files; sampled ${proof.sampledFileCount}; ${proof.readableSampledFileCount} representative-text files. Verification-script signals (lint/typecheck/coverage/CI commands) are independent of test-file presence and are still credited above where detected.`
|
|
4307
5108
|
};
|
|
4308
5109
|
}
|
|
4309
|
-
function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV24SecretContentContext, useV18NativeRls, inventoryAbsenceContext, scanLimitations, useV26SettlingEvidence = false) {
|
|
5110
|
+
function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33Detectors, useV36Detectors, useV39Detectors, useV47Detectors, useV23PemPrivateKeyGrammar, useV24SecretContentContext, useV18NativeRls, inventoryAbsenceContext, scanLimitations, useV26SettlingEvidence = false, useV27FalseAssuranceClosure = false) {
|
|
4310
5111
|
const evidence = [];
|
|
4311
5112
|
const findings = [];
|
|
4312
5113
|
const gitignore = repoFiles.find((file) => basename(file) === ".gitignore");
|
|
@@ -4314,22 +5115,26 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4314
5115
|
const migrationFiles = repoFiles.filter(
|
|
4315
5116
|
(file) => /(^|\/)(migrations?|drizzle|prisma)\//.test(file)
|
|
4316
5117
|
);
|
|
4317
|
-
const { committedSecret, ambiguousSecret, currentSecretFingerprintsByPath } = withContentReadSignal("A2", "secret_cleanliness", () => {
|
|
5118
|
+
const { committedSecret, ambiguousSecret, currentSecretFingerprintsByPath, placeholderSetAside } = withContentReadSignal("A2", "secret_cleanliness", () => {
|
|
4318
5119
|
let match_;
|
|
4319
5120
|
let ambiguous_;
|
|
5121
|
+
const setAsideFiles = [];
|
|
4320
5122
|
for (const path of repoFiles) {
|
|
4321
5123
|
const authoredProductionPath = useV24SecretContentContext ? isAuthoredProductionPath(path) : useV47Detectors ? isV47AuthoredProductionPath(path) : useV39Detectors ? isV39AuthoredProductionPath(path) : isAuthoredProductionPath(path);
|
|
4322
5124
|
if ((useV24SecretContentContext ? isV24IgnoredScanFile(path) : isIgnoredScanFile(path, useV47Detectors)) || useV33Detectors && !authoredProductionPath) {
|
|
4323
5125
|
continue;
|
|
4324
5126
|
}
|
|
5127
|
+
const setAside = { count: 0 };
|
|
4325
5128
|
if (useV24SecretContentContext) {
|
|
4326
5129
|
const classified = findV24ClassifiedSecretInFile(
|
|
4327
5130
|
repoPath,
|
|
4328
5131
|
path,
|
|
4329
5132
|
useV36Detectors,
|
|
4330
5133
|
useV39Detectors,
|
|
4331
|
-
useV47Detectors
|
|
5134
|
+
useV47Detectors,
|
|
5135
|
+
setAside
|
|
4332
5136
|
);
|
|
5137
|
+
if (setAside.count > 0) setAsideFiles.push({ path, count: setAside.count });
|
|
4333
5138
|
if (!classified) continue;
|
|
4334
5139
|
if (classified.classification === "real") {
|
|
4335
5140
|
match_ = { path, match: classified.match };
|
|
@@ -4344,8 +5149,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4344
5149
|
useV36Detectors,
|
|
4345
5150
|
useV39Detectors,
|
|
4346
5151
|
useV47Detectors,
|
|
4347
|
-
useV23PemPrivateKeyGrammar
|
|
5152
|
+
useV23PemPrivateKeyGrammar,
|
|
5153
|
+
setAside
|
|
4348
5154
|
);
|
|
5155
|
+
if (setAside.count > 0) setAsideFiles.push({ path, count: setAside.count });
|
|
4349
5156
|
if (!found) continue;
|
|
4350
5157
|
match_ = { path, match: found };
|
|
4351
5158
|
break;
|
|
@@ -4376,7 +5183,10 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4376
5183
|
// abstention exists for repositories Cejel could not classify, not as a way for one
|
|
4377
5184
|
// unclassifiable value to withhold a metric a confirmed leak already decided.
|
|
4378
5185
|
ambiguousSecret: match_ ? void 0 : ambiguous_,
|
|
4379
|
-
currentSecretFingerprintsByPath: fingerprintsByPath
|
|
5186
|
+
currentSecretFingerprintsByPath: fingerprintsByPath,
|
|
5187
|
+
// Only a scan that read every eligible file can say what it set aside; one that stopped at
|
|
5188
|
+
// a confirmed secret did not, and the certificate already reports that secret.
|
|
5189
|
+
placeholderSetAside: match_ ? [] : setAsideFiles
|
|
4380
5190
|
};
|
|
4381
5191
|
});
|
|
4382
5192
|
const historySecretScan = committedSecret && !useV27Detectors ? null : collectHistorySecretEvidence(
|
|
@@ -4410,10 +5220,13 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4410
5220
|
const hasDataLayer = migrationFiles.length > 0 || dbLayerImportFile !== void 0;
|
|
4411
5221
|
const hasSecretsSurface = envExamples.length > 0 || committedEnvFilePath !== void 0 || gitignoreHasEnvRule || hasEnvInHistory;
|
|
4412
5222
|
if (!hasDataLayer && !hasSecretsSurface && !hasConfirmedSecretFinding && !cryptoHygiene.hasSurface) {
|
|
4413
|
-
return
|
|
4414
|
-
|
|
4415
|
-
|
|
4416
|
-
|
|
5223
|
+
return {
|
|
5224
|
+
...buildNotApplicableSignal(
|
|
5225
|
+
"A2",
|
|
5226
|
+
"No data layer (DB/ORM/migrations) or ratable secrets surface detected \u2014 A2 not applicable to this repo archetype. A ratable surface requires .env* files, .gitignore .env rule, committed/history .env path, or detected signing/HMAC/secret-comparison code; bare env reads (process.env / os.environ / std::env::) do not qualify."
|
|
5227
|
+
),
|
|
5228
|
+
...placeholderSetAsideDisclosures2(placeholderSetAside)
|
|
5229
|
+
};
|
|
4417
5230
|
}
|
|
4418
5231
|
if (committedEnvFilePath) {
|
|
4419
5232
|
evidence.push(
|
|
@@ -4480,7 +5293,12 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4480
5293
|
"A2",
|
|
4481
5294
|
"env_handling_depth",
|
|
4482
5295
|
() => evidence.push(
|
|
4483
|
-
evidenceForRelative(
|
|
5296
|
+
evidenceForRelative(
|
|
5297
|
+
repoPath,
|
|
5298
|
+
gitignoreEnvFile,
|
|
5299
|
+
"secret_scan",
|
|
5300
|
+
gitignoreEnvRuleLabel(repoFiles.filter(isCommittedEnvFormatPath))
|
|
5301
|
+
)
|
|
4484
5302
|
)
|
|
4485
5303
|
);
|
|
4486
5304
|
}
|
|
@@ -4644,6 +5462,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4644
5462
|
findings.push(...cryptoHygiene.findings);
|
|
4645
5463
|
if (evidence.length === 0 && findings.length === 0) return null;
|
|
4646
5464
|
const hasCriticalFinding = findings.some((finding) => finding.severity === "critical");
|
|
5465
|
+
if (useV27FalseAssuranceClosure && !hasCriticalFinding && isShallowRepository(repoPath)) {
|
|
5466
|
+
return shallowHistoryAbstention("A2", V27_A2_SHALLOW_HISTORY_NOTE);
|
|
5467
|
+
}
|
|
4647
5468
|
const secretCleanliness = hasCriticalFinding ? 0 : 1;
|
|
4648
5469
|
const abstainsSecretCleanliness = useV24SecretContentContext && ambiguousSecret != null && !hasCriticalFinding;
|
|
4649
5470
|
const baseMetrics = [
|
|
@@ -4655,7 +5476,9 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4655
5476
|
1,
|
|
4656
5477
|
isMultiTenant ? 0.25 : 0.7,
|
|
4657
5478
|
"clean",
|
|
4658
|
-
"Credits absence of committed or recent-history secret findings in the static scan."
|
|
5479
|
+
"Credits absence of committed or recent-history secret findings in the static scan.",
|
|
5480
|
+
void 0,
|
|
5481
|
+
secretCleanliness === 1 && placeholderSetAside.length > 0 ? { condition: placeholderSetAsideCondition(placeholderSetAside) } : void 0
|
|
4659
5482
|
)
|
|
4660
5483
|
],
|
|
4661
5484
|
metric(
|
|
@@ -4729,9 +5552,25 @@ function collectA2IsolationEvidence(repoPath, repoFiles, useV27Detectors, useV33
|
|
|
4729
5552
|
"Cejel abstained on secret cleanliness: the current-tree scan matched a secret-shaped value it could not classify from content as either a credential or an example. The metric is withheld rather than scored; every other A2 signal is unaffected."
|
|
4730
5553
|
] : [],
|
|
4731
5554
|
...tenantIsolationUnsettled ? [WITAN_A2_TENANT_ISOLATION_UNSETTLED_DISCLOSURE] : []
|
|
4732
|
-
].join(" ")
|
|
5555
|
+
].join(" "),
|
|
5556
|
+
...placeholderSetAsideDisclosures2(placeholderSetAside)
|
|
4733
5557
|
};
|
|
4734
5558
|
}
|
|
5559
|
+
function isCommittedEnvFormatPath(path) {
|
|
5560
|
+
const name = basename(path);
|
|
5561
|
+
return /^\.env(?:\..+)?$|.\.env$/i.test(name) && !isEnvTemplatePath(path, true) && !/(?:example|sample|template)/i.test(name);
|
|
5562
|
+
}
|
|
5563
|
+
function gitignoreEnvRuleLabel(committedEnvFiles) {
|
|
5564
|
+
const count = committedEnvFiles.length;
|
|
5565
|
+
if (count === 0) return ".env files are gitignored";
|
|
5566
|
+
const lead = `.gitignore has an .env rule, but the scanned tree includes ${count} env file${count === 1 ? "" : "s"}`;
|
|
5567
|
+
for (let shown = Math.min(count, 2); shown > 0; shown -= 1) {
|
|
5568
|
+
const more = count - shown;
|
|
5569
|
+
const label = `${lead}: ${committedEnvFiles.slice(0, shown).join(", ")}${more > 0 ? ` and ${more} more` : ""}`;
|
|
5570
|
+
if (label.length <= EVIDENCE_LABEL_MAX_LENGTH) return label;
|
|
5571
|
+
}
|
|
5572
|
+
return lead;
|
|
5573
|
+
}
|
|
4735
5574
|
function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = false) {
|
|
4736
5575
|
const evidence = [];
|
|
4737
5576
|
const findings = [];
|
|
@@ -4828,7 +5667,7 @@ function collectCryptoHygieneEvidence(repoPath, implFiles, useV39Detectors = fal
|
|
|
4828
5667
|
}
|
|
4829
5668
|
return { evidence, findings, hasSurface, clean: hasSurface && !insecureFound };
|
|
4830
5669
|
}
|
|
4831
|
-
function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, useV23WithheldPathAbstention = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV25InlineHealthRoute = false) {
|
|
5670
|
+
function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, useV20ExplicitGaps = false, useV22PackageStartEntrypoint = false, useV23WithheldPathAbstention = false, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV25InlineHealthRoute = false, useV26BuildStepEvidence = false) {
|
|
4832
5671
|
const v20DirectHttpEntrypoint = useV20ExplicitGaps ? findV20DirectHttpServerEntrypointFile(repoPath, repoFiles) : null;
|
|
4833
5672
|
const v22PackageStartHttpEntrypoint = useV22PackageStartEntrypoint ? findV22PackageStartHttpServerEntrypointFile(repoPath, repoFiles) : null;
|
|
4834
5673
|
const v20RuntimeContainer = useV20ExplicitGaps ? findRuntimeContainerEntrypointFile(repoPath, repoFiles, true) : null;
|
|
@@ -5045,13 +5884,22 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
5045
5884
|
}
|
|
5046
5885
|
}
|
|
5047
5886
|
if (evidence.length === 0) return null;
|
|
5887
|
+
let buildStepSettlement;
|
|
5048
5888
|
if (useV20ExplicitGaps) {
|
|
5049
5889
|
const workspaceManifests = packageJson ? findDeclaredWorkspaceManifests(repoPath, repoFiles, packageJson) : NO_DECLARED_WORKSPACE_MANIFESTS;
|
|
5050
5890
|
const hasBuildOrTypecheck = scripts.has("build") || scripts.has("typecheck") || workspaceManifests.declared > workspaceManifests.read.length || workspaceManifests.read.some((file) => {
|
|
5051
5891
|
const workspaceScripts = readPackageScripts(join(repoPath, file));
|
|
5052
5892
|
return workspaceScripts.has("build") || workspaceScripts.has("typecheck");
|
|
5053
5893
|
});
|
|
5054
|
-
|
|
5894
|
+
buildStepSettlement = useV26BuildStepEvidence && packageJson ? settleV26BuildStepEvidence(
|
|
5895
|
+
repoPath,
|
|
5896
|
+
repoFiles,
|
|
5897
|
+
packageJson,
|
|
5898
|
+
workspaceManifests,
|
|
5899
|
+
workflows,
|
|
5900
|
+
(file) => !useV27Detectors || isAuthoredProductionPath(file)
|
|
5901
|
+
) : void 0;
|
|
5902
|
+
if (packageJson && !hasBuildOrTypecheck && (buildStepSettlement === void 0 || buildStepSettlement === "fires")) {
|
|
5055
5903
|
const workspaceRead = workspaceManifests.read.length;
|
|
5056
5904
|
findings.push({
|
|
5057
5905
|
severity: "info",
|
|
@@ -5192,10 +6040,11 @@ function collectA3ProdReadinessEvidence(repoPath, repoFiles, useV27Detectors, us
|
|
|
5192
6040
|
"Counts static rollback or migration-safety artifacts.",
|
|
5193
6041
|
"saturating_count"
|
|
5194
6042
|
)
|
|
5195
|
-
]
|
|
6043
|
+
],
|
|
6044
|
+
...buildStepSettlement === "unsettled" ? { notes: WITAN_A3_BUILD_STEP_UNSETTLED_DISCLOSURE } : {}
|
|
5196
6045
|
};
|
|
5197
6046
|
}
|
|
5198
|
-
function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT) {
|
|
6047
|
+
function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors, useV47Detectors, inventoryAbsenceContext = LEGACY_INVENTORY_ABSENCE_CONTEXT, useV27FalseAssuranceClosure = false) {
|
|
5199
6048
|
const evidence = [];
|
|
5200
6049
|
const findings = [];
|
|
5201
6050
|
const manifests = repoFiles.filter(isDependencyManifest);
|
|
@@ -5308,6 +6157,62 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
|
|
|
5308
6157
|
)
|
|
5309
6158
|
});
|
|
5310
6159
|
}
|
|
6160
|
+
const checkoutPaths = [...repoFiles, ...withheldRepoPaths().map(({ path }) => path)];
|
|
6161
|
+
const knownPaths = [...checkoutPaths, ...mono?.sharedFiles ?? []];
|
|
6162
|
+
const knownBasenames = new Set(knownPaths.map((path) => basename(path)));
|
|
6163
|
+
const findRequirement = () => findLockfileRequirement(
|
|
6164
|
+
repoPath,
|
|
6165
|
+
repoFiles.filter(isGitHubActionsWorkflow),
|
|
6166
|
+
checkoutInventory(checkoutPaths),
|
|
6167
|
+
(lockfiles2) => lockfiles2.some((name) => knownBasenames.has(name))
|
|
6168
|
+
);
|
|
6169
|
+
const lockfileRequirement = hasLockfile || !manifest ? void 0 : useV27FalseAssuranceClosure ? withContentReadSignal("A4", "lockfile_coverage", findRequirement) : withoutContentReadAttribution(findRequirement);
|
|
6170
|
+
const leadingEvidence = [];
|
|
6171
|
+
if (lockfileRequirement) {
|
|
6172
|
+
const cite = () => evidenceForRelativeAtLine(
|
|
6173
|
+
repoPath,
|
|
6174
|
+
lockfileRequirement.file,
|
|
6175
|
+
"dependency_report",
|
|
6176
|
+
`CI step runs \`${lockfileRequirement.command}\`, which needs ${joinLockfileNames(lockfileRequirement.lockfiles)}; the scanned inventory holds none`,
|
|
6177
|
+
lockfileRequirement.line
|
|
6178
|
+
);
|
|
6179
|
+
leadingEvidence.push(
|
|
6180
|
+
useV27FalseAssuranceClosure ? withContentReadSignal("A4", "lockfile_coverage", cite) : withoutContentReadAttribution(cite)
|
|
6181
|
+
);
|
|
6182
|
+
}
|
|
6183
|
+
if (!isAppOrService && manifest) {
|
|
6184
|
+
const unconstrained = withContentReadSignals(
|
|
6185
|
+
"A4",
|
|
6186
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
6187
|
+
() => {
|
|
6188
|
+
const unconstrainedCount = (file) => readDependencySpecs(repoPath, [file]).filter(
|
|
6189
|
+
(dependency) => !hasDeclaredVersionConstraint(dependency)
|
|
6190
|
+
).length;
|
|
6191
|
+
if (unconstrainedCount(manifest) > 0) return void 0;
|
|
6192
|
+
for (const file of manifests) {
|
|
6193
|
+
if (file === manifest) continue;
|
|
6194
|
+
const count = unconstrainedCount(file);
|
|
6195
|
+
if (count > 0) return { file, count };
|
|
6196
|
+
}
|
|
6197
|
+
return void 0;
|
|
6198
|
+
}
|
|
6199
|
+
);
|
|
6200
|
+
if (unconstrained) {
|
|
6201
|
+
leadingEvidence.push(
|
|
6202
|
+
withContentReadSignals(
|
|
6203
|
+
"A4",
|
|
6204
|
+
["pinned_dependency_ratio", "declared_version_range_ratio", "dependency_count_sanity"],
|
|
6205
|
+
() => evidenceForRelative(
|
|
6206
|
+
repoPath,
|
|
6207
|
+
unconstrained.file,
|
|
6208
|
+
"dependency_report",
|
|
6209
|
+
`Dependency manifest with ${unconstrained.count} ${unconstrained.count === 1 ? "dependency" : "dependencies"} declared without a version constraint`
|
|
6210
|
+
)
|
|
6211
|
+
)
|
|
6212
|
+
);
|
|
6213
|
+
}
|
|
6214
|
+
}
|
|
6215
|
+
evidence.unshift(...leadingEvidence);
|
|
5311
6216
|
const automationMetric = metric(
|
|
5312
6217
|
"dependency_automation_ratio",
|
|
5313
6218
|
"Dependency automation ratio",
|
|
@@ -5368,6 +6273,18 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
|
|
|
5368
6273
|
"present",
|
|
5369
6274
|
"Credited when present (reproducible dev/CI installs); a library without a committed lockfile is not penalized \u2014 consumers resolve their own trees."
|
|
5370
6275
|
)
|
|
6276
|
+
] : useV27FalseAssuranceClosure && lockfileRequirement ? [
|
|
6277
|
+
// v27 (#520): the repository's own CI requires a lockfile, so its absence is not the
|
|
6278
|
+
// library norm. A zero term can only lower the weighted mean.
|
|
6279
|
+
metric(
|
|
6280
|
+
"lockfile_coverage",
|
|
6281
|
+
"Lockfile coverage",
|
|
6282
|
+
0,
|
|
6283
|
+
1,
|
|
6284
|
+
0.3,
|
|
6285
|
+
"present",
|
|
6286
|
+
"Scored 0: this repository's own CI requires a lockfile, and the scanned inventory holds no recognized lockfile."
|
|
6287
|
+
)
|
|
5371
6288
|
] : [],
|
|
5372
6289
|
automationMetric,
|
|
5373
6290
|
metric(
|
|
@@ -5385,8 +6302,373 @@ function collectA4DependencyEvidence(repoPath, repoFiles, mono, useV27Detectors,
|
|
|
5385
6302
|
positiveEvidence: evidence,
|
|
5386
6303
|
findings,
|
|
5387
6304
|
metrics: isAppOrService ? appMetrics : libraryMetrics,
|
|
5388
|
-
notes: isAppOrService ? `A4 scored against app/service norms (${packagedApplicationFiles.length > 0 ? "packaged application" : "deploy surface"} detected): pinned dependencies and a lockfile are required for reproducible installs.` : "A4 scored against library/CLI norms (no deploy surface detected \u2014 same archetype line as A3): declared version ranges are correct library behavior; a committed lockfile is credited but not required."
|
|
6305
|
+
notes: isAppOrService ? `A4 scored against app/service norms (${packagedApplicationFiles.length > 0 ? "packaged application" : "deploy surface"} detected): pinned dependencies and a lockfile are required for reproducible installs.` : lockfileRequirement ? `A4 scored against library/CLI norms (no deploy surface detected \u2014 same archetype line as A3): declared version ranges are correct library behavior. An unconditional step in this repository's CI runs \`${lockfileRequirement.command}\` (${lockfileRequirement.file} line ${lockfileRequirement.line}), which exits with an error without ${joinLockfileNames(lockfileRequirement.lockfiles)}, and the scanned inventory holds none.`.slice(0, 1e3) : "A4 scored against library/CLI norms (no deploy surface detected \u2014 same archetype line as A3): declared version ranges are correct library behavior; a committed lockfile is credited but not required."
|
|
6306
|
+
};
|
|
6307
|
+
}
|
|
6308
|
+
var LOCKFILE_REQUIRING_COMMANDS = [
|
|
6309
|
+
{
|
|
6310
|
+
tool: "npm",
|
|
6311
|
+
subcommands: ["ci"],
|
|
6312
|
+
flags: null,
|
|
6313
|
+
lockfiles: ["package-lock.json", "npm-shrinkwrap.json"],
|
|
6314
|
+
generators: ["install", "i", "add", "update", "up"],
|
|
6315
|
+
directoryFlags: ["--prefix", "-C"],
|
|
6316
|
+
manifestFlags: []
|
|
6317
|
+
},
|
|
6318
|
+
{
|
|
6319
|
+
tool: "pnpm",
|
|
6320
|
+
subcommands: ["install", "i"],
|
|
6321
|
+
flags: ["--frozen-lockfile"],
|
|
6322
|
+
lockfiles: ["pnpm-lock.yaml"],
|
|
6323
|
+
generators: ["install", "i", "add", "update", "up"],
|
|
6324
|
+
directoryFlags: ["--dir", "-C"],
|
|
6325
|
+
manifestFlags: []
|
|
6326
|
+
},
|
|
6327
|
+
{
|
|
6328
|
+
tool: "uv",
|
|
6329
|
+
subcommands: ["run", "sync"],
|
|
6330
|
+
flags: ["--locked", "--frozen"],
|
|
6331
|
+
lockfiles: ["uv.lock"],
|
|
6332
|
+
generators: ["run", "sync", "lock", "add"],
|
|
6333
|
+
directoryFlags: ["--directory", "--project"],
|
|
6334
|
+
manifestFlags: []
|
|
6335
|
+
},
|
|
6336
|
+
{
|
|
6337
|
+
tool: "cargo",
|
|
6338
|
+
subcommands: ["build", "test", "check", "clippy", "doc", "run", "bench"],
|
|
6339
|
+
flags: ["--locked", "--frozen"],
|
|
6340
|
+
lockfiles: ["Cargo.lock"],
|
|
6341
|
+
generators: ["build", "test", "check", "clippy", "doc", "run", "bench", "generate-lockfile", "update"],
|
|
6342
|
+
directoryFlags: ["-C"],
|
|
6343
|
+
manifestFlags: ["--manifest-path"]
|
|
6344
|
+
},
|
|
6345
|
+
{
|
|
6346
|
+
tool: "poetry",
|
|
6347
|
+
subcommands: ["check"],
|
|
6348
|
+
flags: ["--lock"],
|
|
6349
|
+
lockfiles: ["poetry.lock"],
|
|
6350
|
+
generators: ["lock", "install", "add", "update"],
|
|
6351
|
+
directoryFlags: ["--directory", "-C", "--project", "-P"],
|
|
6352
|
+
manifestFlags: []
|
|
6353
|
+
}
|
|
6354
|
+
];
|
|
6355
|
+
var CONDITIONAL_SHELL_WORDS = /* @__PURE__ */ new Set([
|
|
6356
|
+
"if",
|
|
6357
|
+
"then",
|
|
6358
|
+
"else",
|
|
6359
|
+
"elif",
|
|
6360
|
+
"fi",
|
|
6361
|
+
"case",
|
|
6362
|
+
"esac",
|
|
6363
|
+
"[",
|
|
6364
|
+
"[[",
|
|
6365
|
+
"test",
|
|
6366
|
+
"while",
|
|
6367
|
+
"until",
|
|
6368
|
+
"for",
|
|
6369
|
+
"do",
|
|
6370
|
+
"done"
|
|
6371
|
+
]);
|
|
6372
|
+
var SHELL_COMMAND_SEPARATORS = /* @__PURE__ */ new Set(["&&", "||", ";", "|"]);
|
|
6373
|
+
var DIRECTORY_CHANGE_COMMANDS = /* @__PURE__ */ new Set(["cd", "pushd"]);
|
|
6374
|
+
function joinLockfileNames(names) {
|
|
6375
|
+
return names.length === 1 ? names[0] ?? "" : `${names.slice(0, -1).join(", ")} or ${names[names.length - 1]}`;
|
|
6376
|
+
}
|
|
6377
|
+
function isGitHubActionsWorkflow(file) {
|
|
6378
|
+
return /^\.github\/workflows\/[^/]+\.ya?ml$/.test(file);
|
|
6379
|
+
}
|
|
6380
|
+
function beforeShellComment(line) {
|
|
6381
|
+
const trimmed = line.trimStart();
|
|
6382
|
+
if (trimmed.startsWith("#")) return "";
|
|
6383
|
+
const cut = [trimmed.indexOf(" #"), trimmed.indexOf(" #")].filter((index) => index !== -1);
|
|
6384
|
+
return cut.length > 0 ? trimmed.slice(0, Math.min(...cut)) : trimmed;
|
|
6385
|
+
}
|
|
6386
|
+
function shellCommands(line) {
|
|
6387
|
+
const commands = [[]];
|
|
6388
|
+
for (const raw of beforeShellComment(line).split(/[ \t]+/)) {
|
|
6389
|
+
const word = raw.endsWith(";") && raw.length > 1 ? raw.slice(0, -1) : raw;
|
|
6390
|
+
if (word === "") continue;
|
|
6391
|
+
if (SHELL_COMMAND_SEPARATORS.has(word)) {
|
|
6392
|
+
commands.push([]);
|
|
6393
|
+
continue;
|
|
6394
|
+
}
|
|
6395
|
+
commands[commands.length - 1]?.push(word);
|
|
6396
|
+
if (raw.endsWith(";") && raw.length > 1) commands.push([]);
|
|
6397
|
+
}
|
|
6398
|
+
return commands.filter((command) => command.length > 0);
|
|
6399
|
+
}
|
|
6400
|
+
var OMITTED_COMMAND_WORDS_MARKER = "\u2026";
|
|
6401
|
+
function matchLockfileRequiringCommand(words, rule) {
|
|
6402
|
+
if (words[0] !== rule.tool) return void 0;
|
|
6403
|
+
const subcommand = rule.subcommands.find((candidate) => candidate === words[1]);
|
|
6404
|
+
if (subcommand === void 0) return void 0;
|
|
6405
|
+
if (rule.flags === null) return `${rule.tool} ${subcommand}`;
|
|
6406
|
+
const flags = rule.flags;
|
|
6407
|
+
const end = words.indexOf("--");
|
|
6408
|
+
const own = end === -1 ? words.slice(2) : words.slice(2, end);
|
|
6409
|
+
for (const [index, word] of own.entries()) {
|
|
6410
|
+
const flag = flags.find((candidate) => candidate === (word.split("=")[0] ?? word));
|
|
6411
|
+
if (flag === void 0) continue;
|
|
6412
|
+
return `${rule.tool} ${subcommand}${index > 0 ? ` ${OMITTED_COMMAND_WORDS_MARKER}` : ""} ${flag}`;
|
|
6413
|
+
}
|
|
6414
|
+
return void 0;
|
|
6415
|
+
}
|
|
6416
|
+
function commandHead(words) {
|
|
6417
|
+
let inverted = false;
|
|
6418
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
6419
|
+
const raw = words[index] ?? "";
|
|
6420
|
+
let lead = 0;
|
|
6421
|
+
while (lead < raw.length && (raw[lead] === "(" || raw[lead] === "{" || raw[lead] === "!")) lead += 1;
|
|
6422
|
+
const prefix = raw.slice(0, lead);
|
|
6423
|
+
if (prefix.includes("!") || prefix.includes("((")) inverted = true;
|
|
6424
|
+
if (lead < raw.length) return { word: raw.slice(lead), argsFrom: index + 1, inverted };
|
|
6425
|
+
}
|
|
6426
|
+
return { word: "", argsFrom: words.length, inverted };
|
|
6427
|
+
}
|
|
6428
|
+
function isConditionalShellCommand(words) {
|
|
6429
|
+
const head = commandHead(words);
|
|
6430
|
+
if (head.inverted || CONDITIONAL_SHELL_WORDS.has(head.word)) return true;
|
|
6431
|
+
if (head.word !== "set") return false;
|
|
6432
|
+
const args = words.slice(head.argsFrom);
|
|
6433
|
+
return args.some(
|
|
6434
|
+
(arg, index) => arg === "+o" && args[index + 1] === "errexit" || arg.length > 1 && arg.startsWith("+") && /^[A-Za-z]+$/.test(arg.slice(1)) && arg.includes("e")
|
|
6435
|
+
);
|
|
6436
|
+
}
|
|
6437
|
+
var RUN_KEY_PATTERN = /^([ \t]*)(?:-[ \t]+)?run:[ \t]*(.*)$/;
|
|
6438
|
+
var IF_KEY_PATTERN = /^[ \t]*(?:-[ \t]+)?if:/;
|
|
6439
|
+
var CONTINUE_ON_ERROR_KEY_PATTERN = /^[ \t]*(?:-[ \t]+)?continue-on-error:[ \t]*(.*)$/;
|
|
6440
|
+
var STEPS_KEY_PATTERN = /^[ \t]*steps:[ \t]*$/;
|
|
6441
|
+
var LIST_ITEM_PATTERN = /^[ \t]*-[ \t]/;
|
|
6442
|
+
var BLOCK_SCALAR_KEY_PATTERN = /^[ \t]*(?:-[ \t]+)?[^\s:#'"-][^:]*:[ \t]*[|>][-+0-9]*(?:[ \t]+(?:#.*)?)?$/;
|
|
6443
|
+
var BLOCK_SCALAR_VALUE_PATTERN = /^[|>][-+0-9]*$/;
|
|
6444
|
+
function lineIndent(line) {
|
|
6445
|
+
return line.length - line.trimStart().length;
|
|
6446
|
+
}
|
|
6447
|
+
function isStepOrJobGuard(line) {
|
|
6448
|
+
if (IF_KEY_PATTERN.test(line)) return true;
|
|
6449
|
+
const continueOnError = CONTINUE_ON_ERROR_KEY_PATTERN.exec(line);
|
|
6450
|
+
if (!continueOnError) return false;
|
|
6451
|
+
const value = beforeShellComment(continueOnError[1] ?? "").trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
6452
|
+
return value !== "false";
|
|
6453
|
+
}
|
|
6454
|
+
function workflowKeyIndent(line, indent) {
|
|
6455
|
+
return indent + (line.trimStart().startsWith("-") ? 2 : 0);
|
|
6456
|
+
}
|
|
6457
|
+
function blockScalarBodyLines(lines, indents) {
|
|
6458
|
+
const body = new Array(lines.length).fill(false);
|
|
6459
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
6460
|
+
const raw = lines[index] ?? "";
|
|
6461
|
+
if (!BLOCK_SCALAR_KEY_PATTERN.test(raw)) continue;
|
|
6462
|
+
const keyIndent = workflowKeyIndent(raw, indents[index] ?? 0);
|
|
6463
|
+
let next = index + 1;
|
|
6464
|
+
for (; next < lines.length; next += 1) {
|
|
6465
|
+
if ((lines[next] ?? "").trim() !== "" && (indents[next] ?? 0) <= keyIndent) break;
|
|
6466
|
+
body[next] = true;
|
|
6467
|
+
}
|
|
6468
|
+
index = next - 1;
|
|
6469
|
+
}
|
|
6470
|
+
return body;
|
|
6471
|
+
}
|
|
6472
|
+
function nextAtOrBelowIndexes(structural, indents) {
|
|
6473
|
+
const next = new Array(structural.length).fill(structural.length);
|
|
6474
|
+
const open = [];
|
|
6475
|
+
for (let index = 0; index < structural.length; index += 1) {
|
|
6476
|
+
if (!structural[index]) continue;
|
|
6477
|
+
const indent = indents[index] ?? 0;
|
|
6478
|
+
while (open.length > 0 && (indents[open[open.length - 1] ?? 0] ?? 0) >= indent) {
|
|
6479
|
+
next[open.pop() ?? 0] = index;
|
|
6480
|
+
}
|
|
6481
|
+
open.push(index);
|
|
6482
|
+
}
|
|
6483
|
+
return next;
|
|
6484
|
+
}
|
|
6485
|
+
function leastGuardIndentWithin(structural, indents, isGuard) {
|
|
6486
|
+
const least = new Array(structural.length).fill(Number.POSITIVE_INFINITY);
|
|
6487
|
+
const open = [];
|
|
6488
|
+
const close = () => {
|
|
6489
|
+
const top = open.pop() ?? 0;
|
|
6490
|
+
const below = open[open.length - 1];
|
|
6491
|
+
if (below !== void 0) least[below] = Math.min(least[below] ?? 0, least[top] ?? 0);
|
|
5389
6492
|
};
|
|
6493
|
+
for (let index = 0; index < structural.length; index += 1) {
|
|
6494
|
+
if (!structural[index]) continue;
|
|
6495
|
+
const indent = indents[index] ?? 0;
|
|
6496
|
+
while (open.length > 0 && (indents[open[open.length - 1] ?? 0] ?? 0) >= indent) close();
|
|
6497
|
+
if (isGuard[index]) least[index] = indent;
|
|
6498
|
+
open.push(index);
|
|
6499
|
+
}
|
|
6500
|
+
while (open.length > 0) close();
|
|
6501
|
+
return least;
|
|
6502
|
+
}
|
|
6503
|
+
function githubActionsRunSteps(contents) {
|
|
6504
|
+
const lines = contents.split(/\r?\n/);
|
|
6505
|
+
const indents = lines.map(lineIndent);
|
|
6506
|
+
const inBlockScalar = blockScalarBodyLines(lines, indents);
|
|
6507
|
+
const structural = lines.map((raw, index) => !inBlockScalar[index] && raw.trim() !== "");
|
|
6508
|
+
const isGuard = lines.map((raw, index) => (structural[index] ?? false) && isStepOrJobGuard(raw));
|
|
6509
|
+
const itemAtOrAbove = new Array(lines.length);
|
|
6510
|
+
const stepsKeyAbove = new Array(lines.length);
|
|
6511
|
+
const lessIndentedAbove = new Array(lines.length);
|
|
6512
|
+
{
|
|
6513
|
+
let lastItem = -1;
|
|
6514
|
+
let lastStepsKey = -1;
|
|
6515
|
+
const open = [];
|
|
6516
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
6517
|
+
const raw = lines[index] ?? "";
|
|
6518
|
+
if (structural[index] && LIST_ITEM_PATTERN.test(raw)) lastItem = index;
|
|
6519
|
+
itemAtOrAbove[index] = lastItem;
|
|
6520
|
+
stepsKeyAbove[index] = lastStepsKey;
|
|
6521
|
+
if (!structural[index]) {
|
|
6522
|
+
lessIndentedAbove[index] = -1;
|
|
6523
|
+
continue;
|
|
6524
|
+
}
|
|
6525
|
+
if (STEPS_KEY_PATTERN.test(raw)) lastStepsKey = index;
|
|
6526
|
+
const indent = indents[index] ?? 0;
|
|
6527
|
+
while (open.length > 0 && (indents[open[open.length - 1] ?? 0] ?? 0) >= indent) open.pop();
|
|
6528
|
+
lessIndentedAbove[index] = open.length > 0 ? open[open.length - 1] ?? -1 : -1;
|
|
6529
|
+
open.push(index);
|
|
6530
|
+
}
|
|
6531
|
+
}
|
|
6532
|
+
const nextAtOrBelow = nextAtOrBelowIndexes(structural, indents);
|
|
6533
|
+
const leastGuardIndent = leastGuardIndentWithin(structural, indents, isGuard);
|
|
6534
|
+
const guardedJobs = /* @__PURE__ */ new Set();
|
|
6535
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
6536
|
+
if (isGuard[index]) guardedJobs.add(`${lessIndentedAbove[index] ?? -1}:${indents[index] ?? 0}`);
|
|
6537
|
+
}
|
|
6538
|
+
const steps = [];
|
|
6539
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
6540
|
+
if (!structural[index]) continue;
|
|
6541
|
+
const raw = lines[index] ?? "";
|
|
6542
|
+
const match = execOnTerminatorFreeLine(RUN_KEY_PATTERN, raw);
|
|
6543
|
+
if (!match) continue;
|
|
6544
|
+
const keyIndent = workflowKeyIndent(raw, (match[1] ?? "").length);
|
|
6545
|
+
const value = (match[2] ?? "").trim();
|
|
6546
|
+
const body = [];
|
|
6547
|
+
if (BLOCK_SCALAR_VALUE_PATTERN.test(beforeShellComment(value).trim())) {
|
|
6548
|
+
for (let next = index + 1; next < lines.length && inBlockScalar[next]; next += 1) {
|
|
6549
|
+
const text = lines[next] ?? "";
|
|
6550
|
+
if (text.trim() !== "") body.push({ line: next + 1, text });
|
|
6551
|
+
}
|
|
6552
|
+
} else if (value !== "") {
|
|
6553
|
+
body.push({ line: index + 1, text: value.replace(/^(['"])(.*)\1$/, "$2") });
|
|
6554
|
+
}
|
|
6555
|
+
const stepStart = itemAtOrAbove[index] ?? -1;
|
|
6556
|
+
if (stepStart === -1) {
|
|
6557
|
+
steps.push({ lines: body, conditional: true });
|
|
6558
|
+
continue;
|
|
6559
|
+
}
|
|
6560
|
+
const placed = stepStart === index || (nextAtOrBelow[stepStart] ?? lines.length) > index;
|
|
6561
|
+
const stepConditional = (leastGuardIndent[stepStart] ?? Number.POSITIVE_INFINITY) <= keyIndent;
|
|
6562
|
+
const stepsKey = stepsKeyAbove[stepStart] ?? -1;
|
|
6563
|
+
const jobConditional = stepsKey !== -1 && guardedJobs.has(`${lessIndentedAbove[stepsKey] ?? -1}:${indents[stepsKey] ?? 0}`);
|
|
6564
|
+
steps.push({ lines: body, conditional: !placed || stepConditional || jobConditional });
|
|
6565
|
+
}
|
|
6566
|
+
return steps;
|
|
6567
|
+
}
|
|
6568
|
+
var WORKFLOW_OTHER_REPOSITORY_PATTERN = /^[ \t]*(?:-[ \t]+)?repository:[ \t]*\S/;
|
|
6569
|
+
var WORKFLOW_DOWNLOAD_ARTIFACT_PATTERN = /^[ \t]*(?:-[ \t]+)?uses:[ \t]*['"]?actions\/download-artifact@/;
|
|
6570
|
+
var WORKFLOW_WORKING_DIRECTORY_PATTERN = /^[ \t]*(?:-[ \t]+)?working-directory:[ \t]*(.*)$/;
|
|
6571
|
+
var EXPANDED_PATH_PATTERN = /[$~`*?[\]{}'"\\]/;
|
|
6572
|
+
function checkoutInventory(paths) {
|
|
6573
|
+
const directories = /* @__PURE__ */ new Set();
|
|
6574
|
+
for (const path of paths) {
|
|
6575
|
+
for (let end = path.lastIndexOf("/"); end > 0; end = path.lastIndexOf("/", end - 1)) {
|
|
6576
|
+
const directory = path.slice(0, end);
|
|
6577
|
+
if (directories.has(directory)) break;
|
|
6578
|
+
directories.add(directory);
|
|
6579
|
+
}
|
|
6580
|
+
}
|
|
6581
|
+
return { files: new Set(paths), directories };
|
|
6582
|
+
}
|
|
6583
|
+
function literalCheckoutPath(written) {
|
|
6584
|
+
let end = written.length;
|
|
6585
|
+
while (end > 1 && written[end - 1] === "/") end -= 1;
|
|
6586
|
+
let path = written.slice(0, end);
|
|
6587
|
+
while (path.startsWith("./")) path = path.slice(2);
|
|
6588
|
+
if (path === "" || path.startsWith("/") || EXPANDED_PATH_PATTERN.test(path)) return void 0;
|
|
6589
|
+
return path;
|
|
6590
|
+
}
|
|
6591
|
+
function isCheckoutDirectory(inventory, written) {
|
|
6592
|
+
const path = literalCheckoutPath(written);
|
|
6593
|
+
return path !== void 0 && (path === "." || inventory.directories.has(path));
|
|
6594
|
+
}
|
|
6595
|
+
function isCheckoutFile(inventory, written) {
|
|
6596
|
+
const path = literalCheckoutPath(written);
|
|
6597
|
+
return path !== void 0 && inventory.files.has(path);
|
|
6598
|
+
}
|
|
6599
|
+
function commandStaysInCheckout(words, rule, inventory) {
|
|
6600
|
+
const end = words.indexOf("--");
|
|
6601
|
+
const own = end === -1 ? words.slice(2) : words.slice(2, end);
|
|
6602
|
+
const checks = [
|
|
6603
|
+
[rule.directoryFlags, isCheckoutDirectory],
|
|
6604
|
+
[rule.manifestFlags, isCheckoutFile]
|
|
6605
|
+
];
|
|
6606
|
+
for (const [index, word] of own.entries()) {
|
|
6607
|
+
for (const [flags, holds] of checks) {
|
|
6608
|
+
for (const flag of flags) {
|
|
6609
|
+
let value;
|
|
6610
|
+
if (word === flag) value = own[index + 1] ?? "";
|
|
6611
|
+
else if (word.startsWith(`${flag}=`)) value = word.slice(flag.length + 1);
|
|
6612
|
+
else if (!flag.startsWith("--") && word.startsWith(flag)) value = word.slice(flag.length);
|
|
6613
|
+
else continue;
|
|
6614
|
+
if (!holds(inventory, value)) return false;
|
|
6615
|
+
}
|
|
6616
|
+
}
|
|
6617
|
+
}
|
|
6618
|
+
return true;
|
|
6619
|
+
}
|
|
6620
|
+
function workflowReadsOnlyThisTree(inventory, contents) {
|
|
6621
|
+
for (const raw of contents.split(/\r?\n/)) {
|
|
6622
|
+
if (WORKFLOW_OTHER_REPOSITORY_PATTERN.test(raw)) return false;
|
|
6623
|
+
if (WORKFLOW_DOWNLOAD_ARTIFACT_PATTERN.test(raw)) return false;
|
|
6624
|
+
const workingDirectory = WORKFLOW_WORKING_DIRECTORY_PATTERN.exec(raw);
|
|
6625
|
+
if (workingDirectory) {
|
|
6626
|
+
const value = beforeShellComment(workingDirectory[1] ?? "").trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
6627
|
+
if (!isCheckoutDirectory(inventory, value)) return false;
|
|
6628
|
+
}
|
|
6629
|
+
}
|
|
6630
|
+
return true;
|
|
6631
|
+
}
|
|
6632
|
+
function findLockfileRequirement(repoPath, workflows, inventory, isSatisfied) {
|
|
6633
|
+
for (const file of workflows) {
|
|
6634
|
+
const fullPath = join(repoPath, file);
|
|
6635
|
+
if (!isRegularFile(fullPath)) continue;
|
|
6636
|
+
const contents = readRepoText(fullPath, "utf8");
|
|
6637
|
+
if (!workflowReadsOnlyThisTree(inventory, contents)) continue;
|
|
6638
|
+
const steps = githubActionsRunSteps(contents);
|
|
6639
|
+
const commandsOf = (step) => step.lines.flatMap(({ line, text }) => shellCommands(text).map((words) => ({ line, words })));
|
|
6640
|
+
const stepCommands = steps.map(commandsOf);
|
|
6641
|
+
const mayBeGenerated = /* @__PURE__ */ new Set();
|
|
6642
|
+
for (const { words } of stepCommands.flat()) {
|
|
6643
|
+
for (const rule of LOCKFILE_REQUIRING_COMMANDS) {
|
|
6644
|
+
if (mayBeGenerated.has(rule)) continue;
|
|
6645
|
+
const generates = words[0] === rule.tool && rule.generators.includes(words[1] ?? "") && matchLockfileRequiringCommand(words, rule) === void 0;
|
|
6646
|
+
const names = words.some((word) => rule.lockfiles.some((lockfile) => word.includes(lockfile)));
|
|
6647
|
+
if (generates || names) mayBeGenerated.add(rule);
|
|
6648
|
+
}
|
|
6649
|
+
}
|
|
6650
|
+
for (const [stepIndex, step] of steps.entries()) {
|
|
6651
|
+
if (step.conditional) continue;
|
|
6652
|
+
const commands = stepCommands[stepIndex] ?? [];
|
|
6653
|
+
const shellIsConditional = step.lines.some(({ text }) => beforeShellComment(text).includes("<<")) || commands.some(({ words }) => isConditionalShellCommand(words)) || step.lines.some(({ text }) => /(^|[ \t])\|\|([ \t]|$)/.test(beforeShellComment(text)));
|
|
6654
|
+
if (shellIsConditional) continue;
|
|
6655
|
+
const leavesCheckout = commands.some(({ words }) => {
|
|
6656
|
+
const head = commandHead(words);
|
|
6657
|
+
return DIRECTORY_CHANGE_COMMANDS.has(head.word) && !isCheckoutDirectory(inventory, words[head.argsFrom] ?? "");
|
|
6658
|
+
});
|
|
6659
|
+
if (leavesCheckout) continue;
|
|
6660
|
+
for (const { line, words } of commands) {
|
|
6661
|
+
for (const rule of LOCKFILE_REQUIRING_COMMANDS) {
|
|
6662
|
+
const command = matchLockfileRequiringCommand(words, rule);
|
|
6663
|
+
if (command === void 0 || isSatisfied(rule.lockfiles) || mayBeGenerated.has(rule) || !commandStaysInCheckout(words, rule, inventory)) {
|
|
6664
|
+
continue;
|
|
6665
|
+
}
|
|
6666
|
+
return { file, line, command: command.slice(0, 80), lockfiles: rule.lockfiles };
|
|
6667
|
+
}
|
|
6668
|
+
}
|
|
6669
|
+
}
|
|
6670
|
+
}
|
|
6671
|
+
return void 0;
|
|
5390
6672
|
}
|
|
5391
6673
|
var LIBRARY_DEPENDENCY_COUNT_SANE_MAX = 120;
|
|
5392
6674
|
function libraryDependencyCountSanity(specCount) {
|
|
@@ -5591,7 +6873,20 @@ function collectA5ClaimRealityEvidence(repoPath, repoFiles, useV27Detectors, use
|
|
|
5591
6873
|
...reconciliationUnsettled ? { notes: WITAN_A5_RECONCILIATION_UNSETTLED_DISCLOSURE } : {}
|
|
5592
6874
|
};
|
|
5593
6875
|
}
|
|
5594
|
-
|
|
6876
|
+
var RECENT_COMMIT_WINDOW = 12;
|
|
6877
|
+
var V27_A2_SHALLOW_HISTORY_NOTE = "A2 abstained: this repository is a shallow clone, so the scan for credentials and .env files in earlier commits read only the commits present, and no confirmed critical finding stands on what was read. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
|
|
6878
|
+
var V27_B2_SHALLOW_HISTORY_NOTE = `B2 abstained: this repository is a shallow clone with fewer than ${RECENT_COMMIT_WINDOW} commits visible, so the pull-request merge ratio cannot be read over its window. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).`;
|
|
6879
|
+
function shallowHistoryAbstention(criterionId, notes) {
|
|
6880
|
+
return {
|
|
6881
|
+
criterionId,
|
|
6882
|
+
positiveEvidence: [],
|
|
6883
|
+
findings: [],
|
|
6884
|
+
metrics: [],
|
|
6885
|
+
insufficientData: true,
|
|
6886
|
+
notes
|
|
6887
|
+
};
|
|
6888
|
+
}
|
|
6889
|
+
function collectB2PrTraceEvidence(repoPath, repoFiles, useV27FalseAssuranceClosure = false) {
|
|
5595
6890
|
const workflows = repoFiles.filter(isCiWorkflow);
|
|
5596
6891
|
const prTemplate = repoFiles.find(
|
|
5597
6892
|
(file) => /(^|\/)(pull_request_template|PULL_REQUEST_TEMPLATE)(\.md$|\/[^/]+\.md$)/.test(file)
|
|
@@ -5600,15 +6895,21 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
|
|
|
5600
6895
|
(file) => /branch.*protection|review.*gate/i.test(file) || GITHUB_CODEOWNERS_PATH_PATTERN.test(file)
|
|
5601
6896
|
);
|
|
5602
6897
|
const citedWorkflows = workflows.slice(0, 3);
|
|
5603
|
-
const
|
|
5604
|
-
|
|
5605
|
-
|
|
5606
|
-
|
|
5607
|
-
|
|
5608
|
-
|
|
6898
|
+
const unreadWorkflows = unreadCiWorkflowReasons(() => true);
|
|
6899
|
+
const workflowLabel = (file) => fittedCiWorkflowCitationLabel(
|
|
6900
|
+
b2WorkflowCitationBaseLabels(repoPath, file),
|
|
6901
|
+
(baseLabel) => ciWorkflowCitationLabel(
|
|
6902
|
+
baseLabel,
|
|
6903
|
+
"counted",
|
|
6904
|
+
workflows.length,
|
|
6905
|
+
citedWorkflows.length,
|
|
6906
|
+
unreadWorkflows
|
|
6907
|
+
)
|
|
5609
6908
|
);
|
|
5610
6909
|
const evidence = [
|
|
5611
|
-
...citedWorkflows.map(
|
|
6910
|
+
...citedWorkflows.map(
|
|
6911
|
+
(file) => evidenceForRelative(repoPath, file, "ci_run", workflowLabel(file))
|
|
6912
|
+
),
|
|
5612
6913
|
...prTemplate ? [evidenceForRelative(repoPath, prTemplate, "pull_request", "Pull request template")] : [],
|
|
5613
6914
|
...branchProtectionDoc ? [
|
|
5614
6915
|
evidenceForRelative(
|
|
@@ -5620,6 +6921,9 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
|
|
|
5620
6921
|
] : []
|
|
5621
6922
|
];
|
|
5622
6923
|
if (evidence.length === 0) return null;
|
|
6924
|
+
if (useV27FalseAssuranceClosure && isShallowRepository(repoPath) && readRecentCommits(repoPath).length < RECENT_COMMIT_WINDOW) {
|
|
6925
|
+
return shallowHistoryAbstention("B2", V27_B2_SHALLOW_HISTORY_NOTE);
|
|
6926
|
+
}
|
|
5623
6927
|
return {
|
|
5624
6928
|
criterionId: "B2",
|
|
5625
6929
|
positiveEvidence: evidence,
|
|
@@ -5663,18 +6967,46 @@ function collectB2PrTraceEvidence(repoPath, repoFiles) {
|
|
|
5663
6967
|
]
|
|
5664
6968
|
};
|
|
5665
6969
|
}
|
|
5666
|
-
function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
6970
|
+
function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono, useV27FalseAssuranceClosure = false) {
|
|
5667
6971
|
const evidence = [];
|
|
5668
6972
|
const packageJson = findRootPackageJson(repoFiles);
|
|
5669
6973
|
const scripts = packageJson ? readPackageScripts(join(repoPath, packageJson)) : /* @__PURE__ */ new Map();
|
|
5670
6974
|
const workflows = repoFiles.filter(isCiWorkflow);
|
|
5671
6975
|
const workflow = workflows[0];
|
|
5672
6976
|
const rootWorkflows = mono ? mono.sharedFiles.filter(isCiWorkflow) : [];
|
|
5673
|
-
const
|
|
6977
|
+
const knownPaths = [...repoFiles, ...withheldRepoPaths().map(({ path }) => path)];
|
|
6978
|
+
const ownInventory = {
|
|
6979
|
+
scanned: new Set(repoFiles),
|
|
6980
|
+
known: new Set(knownPaths),
|
|
6981
|
+
hasYarnPlugins: knownPaths.some((path) => path.startsWith(".yarn/plugins/"))
|
|
6982
|
+
};
|
|
6983
|
+
const countsAsCi = (root) => (file) => !useV27FalseAssuranceClosure || !isPublishOnlyWorkflow(root, file, root === repoPath ? ownInventory : null);
|
|
6984
|
+
const { defaultBranchCiCount, defaultBranchCiWorkflows, rootDefaultBranchCiWorkflows } = withContentReadSignal(
|
|
5674
6985
|
"B3",
|
|
5675
6986
|
"default_branch_ci_depth",
|
|
5676
|
-
() =>
|
|
6987
|
+
() => {
|
|
6988
|
+
const counted = workflows.filter(countsAsCi(repoPath)).filter((file) => workflowTargetsDefaultBranch(repoPath, file));
|
|
6989
|
+
const rootCounted = mono ? rootWorkflows.filter(countsAsCi(mono.root)).filter((file) => workflowTargetsDefaultBranch(mono.root, file)) : [];
|
|
6990
|
+
return {
|
|
6991
|
+
defaultBranchCiCount: counted.length + rootCounted.length,
|
|
6992
|
+
defaultBranchCiWorkflows: counted,
|
|
6993
|
+
rootDefaultBranchCiWorkflows: rootCounted
|
|
6994
|
+
};
|
|
6995
|
+
}
|
|
6996
|
+
);
|
|
6997
|
+
const unshownCheckWorkflows = mergeB3UnshownCheckWorkflows(
|
|
6998
|
+
findB3UnshownCheckWorkflows(repoPath, defaultBranchCiWorkflows),
|
|
6999
|
+
mono ? findB3UnshownCheckWorkflows(
|
|
7000
|
+
mono.root,
|
|
7001
|
+
rootDefaultBranchCiWorkflows,
|
|
7002
|
+
(file) => `${file} at the monorepo root`
|
|
7003
|
+
) : { printOnly: [] }
|
|
5677
7004
|
);
|
|
7005
|
+
const publishOnlyWorkflows = useV27FalseAssuranceClosure ? withContentReadSignals(
|
|
7006
|
+
"B3",
|
|
7007
|
+
["ci_script_depth", "default_branch_ci_depth"],
|
|
7008
|
+
() => workflows.filter((file) => isPublishOnlyWorkflow(repoPath, file, ownInventory))
|
|
7009
|
+
) : [];
|
|
5678
7010
|
if (packageJson && scripts.has("test")) {
|
|
5679
7011
|
evidence.push(evidenceForRelative(repoPath, packageJson, "test_run", "Test script"));
|
|
5680
7012
|
}
|
|
@@ -5756,10 +7088,10 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
5756
7088
|
"ci_script_depth",
|
|
5757
7089
|
() => ({
|
|
5758
7090
|
repoCategoryHits: CI_COMMAND_CATEGORIES.map(
|
|
5759
|
-
(pattern) => workflows.some((file) => fileContains(repoPath, file, pattern))
|
|
7091
|
+
(pattern) => workflows.filter(countsAsCi(repoPath)).some((file) => fileContains(repoPath, file, pattern))
|
|
5760
7092
|
),
|
|
5761
7093
|
rootCategoryHits: mono ? CI_COMMAND_CATEGORIES.map(
|
|
5762
|
-
(pattern) => rootWorkflows.some((file) => fileContains(mono.root, file, pattern))
|
|
7094
|
+
(pattern) => rootWorkflows.filter(countsAsCi(mono.root)).some((file) => fileContains(mono.root, file, pattern))
|
|
5763
7095
|
) : []
|
|
5764
7096
|
})
|
|
5765
7097
|
);
|
|
@@ -5805,10 +7137,655 @@ function collectB3CiDisciplineEvidence(repoPath, repoFiles, mono) {
|
|
|
5805
7137
|
0.55,
|
|
5806
7138
|
"workflows",
|
|
5807
7139
|
"Counts CI workflows that target pull requests or the default branch, up to 4.",
|
|
5808
|
-
"saturating_count"
|
|
7140
|
+
"saturating_count",
|
|
7141
|
+
b3UnshownCheckCount(unshownCheckWorkflows) > 0 ? { condition: b3UnshownCheckCondition(unshownCheckWorkflows) } : void 0
|
|
5809
7142
|
)
|
|
5810
|
-
]
|
|
7143
|
+
],
|
|
7144
|
+
...publishOnlyWorkflows.length > 0 ? { notes: b3PublishOnlyNote(publishOnlyWorkflows) } : {},
|
|
7145
|
+
...b3UnshownCheckDisclosures(unshownCheckWorkflows)
|
|
7146
|
+
};
|
|
7147
|
+
}
|
|
7148
|
+
function b3UnshownCheckDisclosures(unshown) {
|
|
7149
|
+
const sentence = b3UnshownCheckDisclosure(unshown, CRITERION_DISCLOSURE_MAX_LENGTH);
|
|
7150
|
+
return sentence ? { disclosures: [sentence] } : {};
|
|
7151
|
+
}
|
|
7152
|
+
function b3PublishOnlyNote(publishOnlyWorkflows) {
|
|
7153
|
+
return `${publishOnlyWorkflows.length === 1 ? "1 workflow that only publishes, releases or deploys" : `${publishOnlyWorkflows.length} workflows that only publish, release or deploy`} (no pull-request trigger, and every step Cejel reads only checks out, sets up a toolchain, configures credentials or uploads an already-built artifact) ${publishOnlyWorkflows.length === 1 ? "is" : "are"} not counted as CI verification: ${publishOnlyWorkflows.slice(0, 4).join(", ")}${publishOnlyWorkflows.length > 4 ? ", ..." : ""}.`.slice(0, CRITERION_NOTES_MAX_LENGTH);
|
|
7154
|
+
}
|
|
7155
|
+
var PUBLISH_OR_DEPLOY_STEP_PATTERN = /\b(?:npm|pnpm|yarn|bun)[ \t]+(?:npm[ \t]+)?publish\b|\bpypa\/gh-action-pypi-publish\b|\btwine[ \t]+upload\b|\b(?:uv|poetry|flit|hatch|cargo)[ \t]+publish\b|\bgem[ \t]+push\b|\bgh[ \t]+release[ \t]+(?:create|upload)\b|\bsoftprops\/action-gh-release\b|\bgoreleaser\b|\bsemantic-release\b|\bchangesets\/action\b|\bdocker[ \t]+(?:image[ \t]+)?push\b|\bdocker\/build-push-action\b|\b(?:vercel|netlify|flyctl|fly|firebase|serverless|wrangler)[ \t]+deploy\b|\bkubectl[ \t]+apply\b|\bhelm[ \t]+upgrade\b|\bterraform[ \t]+apply\b/i;
|
|
7156
|
+
var PUBLISH_ONLY_EXTRA_VERIFICATION_PATTERN = /\b(?:tox|nox|pre-commit|bazel[ \t]+test|gradlew?[ \t]+(?:test|check|build)|mvnw?[ \t]+(?:verify|test|package)|make[ \t]+(?:check|test)|just[ \t]+test|cargo[ \t]+(?:clippy|check|nextest)|turbo[ \t]+(?:run[ \t]+)?test)\b|[ \t]-r[ \t]+test\b|\brun[ \t]+test\b/i;
|
|
7157
|
+
var PULL_REQUEST_TRIGGERS = ["pull_request", "pull_request_target", "merge_group"];
|
|
7158
|
+
var PULL_REQUEST_TRIGGER_PATTERN = /\b(?:pull_request|pull_request_target|merge_group)\b/;
|
|
7159
|
+
var REUSABLE_WORKFLOW_CALL_PATTERN = /^[ \t]*uses:[ \t]*\S*\.github\/workflows\//m;
|
|
7160
|
+
function isPublishOnlyWorkflow(repoPath, file, inventory) {
|
|
7161
|
+
if (!isGitHubActionsWorkflow(file)) return false;
|
|
7162
|
+
const fullPath = join(repoPath, file);
|
|
7163
|
+
if (!isRegularFile(fullPath)) return false;
|
|
7164
|
+
const contents = readRepoText(fullPath, "utf8");
|
|
7165
|
+
if (!PUBLISH_OR_DEPLOY_STEP_PATTERN.test(contents) || PULL_REQUEST_TRIGGER_PATTERN.test(contents) || REUSABLE_WORKFLOW_CALL_PATTERN.test(contents) || CI_TEST_COMMAND_PATTERN.test(contents) || CI_LINT_COMMAND_PATTERN.test(contents) || CI_TYPECHECK_COMMAND_PATTERN.test(contents) || PUBLISH_ONLY_EXTRA_VERIFICATION_PATTERN.test(contents) || YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents)) {
|
|
7166
|
+
return false;
|
|
7167
|
+
}
|
|
7168
|
+
const steps = placedPublishWorkflowSteps(contents);
|
|
7169
|
+
if (steps === null) return false;
|
|
7170
|
+
let publishes = false;
|
|
7171
|
+
const needs = /* @__PURE__ */ new Set();
|
|
7172
|
+
for (const step of steps) {
|
|
7173
|
+
const classified = step.uses !== void 0 ? { kind: classifyPublishOnlyUsesStep(step.uses, step.inputs) } : classifyPublishOnlyRunLines(step.run ?? []);
|
|
7174
|
+
if (classified.kind === "other") return false;
|
|
7175
|
+
if (classified.kind === "publish") publishes = true;
|
|
7176
|
+
for (const need of classified.needs ?? []) needs.add(need);
|
|
7177
|
+
}
|
|
7178
|
+
if (!publishes) return false;
|
|
7179
|
+
if (needs.size > 0 && inventory === null) return false;
|
|
7180
|
+
if (needs.has("package") && !packageManifestRunsNoRepositoryCode(repoPath, inventory)) {
|
|
7181
|
+
return false;
|
|
7182
|
+
}
|
|
7183
|
+
return true;
|
|
7184
|
+
}
|
|
7185
|
+
function classifyPublishOnlyRunLines(lines) {
|
|
7186
|
+
let kind = "setup";
|
|
7187
|
+
const needs = /* @__PURE__ */ new Set();
|
|
7188
|
+
for (const text of joinShellContinuationLines(lines)) {
|
|
7189
|
+
for (const words of shellCommands(text)) {
|
|
7190
|
+
const command = classifyPublishOnlyRunCommand(words);
|
|
7191
|
+
if (command.kind === "other") return { kind: "other" };
|
|
7192
|
+
if (command.kind === "publish") kind = "publish";
|
|
7193
|
+
for (const need of command.needs ?? []) needs.add(need);
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
return { kind, needs: [...needs] };
|
|
7197
|
+
}
|
|
7198
|
+
var PACKAGE_LIFECYCLE_SCRIPTS = [
|
|
7199
|
+
"preinstall",
|
|
7200
|
+
"install",
|
|
7201
|
+
"postinstall",
|
|
7202
|
+
"preprepare",
|
|
7203
|
+
"prepare",
|
|
7204
|
+
"postprepare",
|
|
7205
|
+
"prepack",
|
|
7206
|
+
"postpack",
|
|
7207
|
+
"prepublish",
|
|
7208
|
+
"prepublishOnly",
|
|
7209
|
+
"publish",
|
|
7210
|
+
"postpublish",
|
|
7211
|
+
"dependencies"
|
|
7212
|
+
];
|
|
7213
|
+
var PACKAGE_MANAGER_CODE_CONFIG_FILES = [
|
|
7214
|
+
".npmrc",
|
|
7215
|
+
".yarnrc",
|
|
7216
|
+
".yarnrc.yml",
|
|
7217
|
+
".pnpmfile.cjs",
|
|
7218
|
+
"pnpmfile.js",
|
|
7219
|
+
"bunfig.toml",
|
|
7220
|
+
"pnpm-workspace.yaml",
|
|
7221
|
+
"lerna.json",
|
|
7222
|
+
"binding.gyp"
|
|
7223
|
+
];
|
|
7224
|
+
var PACKAGE_DEPENDENCY_FIELDS = ["dependencies", "devDependencies", "optionalDependencies", "peerDependencies"];
|
|
7225
|
+
function packageManifestRunsNoRepositoryCode(root, inventory) {
|
|
7226
|
+
if (!inventory.scanned.has("package.json")) return false;
|
|
7227
|
+
if (PACKAGE_MANAGER_CODE_CONFIG_FILES.some((file) => inventory.known.has(file))) return false;
|
|
7228
|
+
if (inventory.hasYarnPlugins) return false;
|
|
7229
|
+
const manifest = parseJsonObject(readRepoText(join(root, "package.json"), "utf8"));
|
|
7230
|
+
if (!manifest || "workspaces" in manifest || "gypfile" in manifest) return false;
|
|
7231
|
+
const scripts = manifest.scripts;
|
|
7232
|
+
if (scripts !== void 0 && !isRecord(scripts)) return false;
|
|
7233
|
+
if (isRecord(scripts) && PACKAGE_LIFECYCLE_SCRIPTS.some((name) => name in scripts)) return false;
|
|
7234
|
+
for (const field of PACKAGE_DEPENDENCY_FIELDS) {
|
|
7235
|
+
const dependencies = manifest[field];
|
|
7236
|
+
if (dependencies === void 0) continue;
|
|
7237
|
+
if (!isRecord(dependencies)) return false;
|
|
7238
|
+
for (const spec of Object.values(dependencies)) {
|
|
7239
|
+
if (typeof spec !== "string") return false;
|
|
7240
|
+
if (/^(?:file|link|workspace|portal):|^\.{0,2}\//.test(spec.trim())) return false;
|
|
7241
|
+
}
|
|
7242
|
+
}
|
|
7243
|
+
return true;
|
|
7244
|
+
}
|
|
7245
|
+
var PUBLISH_ONLY_JOB_KEYS = /* @__PURE__ */ new Set(["runs-on", "name", "timeout-minutes", "needs", "permissions", "environment", "steps"]);
|
|
7246
|
+
var PUBLISH_ONLY_JOB_MAPPING_KEYS = /* @__PURE__ */ new Set(["permissions", "environment"]);
|
|
7247
|
+
var PUBLISH_ONLY_STEP_KEYS = /* @__PURE__ */ new Set(["name", "id", "uses", "run", "with", "env"]);
|
|
7248
|
+
var PLAIN_MAPPING_ENTRY_PATTERN = /^([A-Za-z_][A-Za-z0-9_-]*):(?:[ \t]+([^]*))?$/;
|
|
7249
|
+
var NEEDS_FLOW_SEQUENCE_PATTERN = /^\[[ \t]*[A-Za-z0-9_-]+(?:[ \t]*,[ \t]*[A-Za-z0-9_-]+)*[ \t]*\]$/;
|
|
7250
|
+
function isPlainOneLineValue(value) {
|
|
7251
|
+
return value !== "" && !/^[[{&*|>!]/.test(value) && isOneLineScalar(value);
|
|
7252
|
+
}
|
|
7253
|
+
function placedPublishWorkflowSteps(contents) {
|
|
7254
|
+
if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents) || OTHER_LEADING_BLANK_PATTERN.test(contents)) return null;
|
|
7255
|
+
const lines = contents.split(/\r?\n/);
|
|
7256
|
+
const isContent = (line) => line.trim() !== "" && !line.trimStart().startsWith("#");
|
|
7257
|
+
let jobsStart = -1;
|
|
7258
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
7259
|
+
const raw = lines[index] ?? "";
|
|
7260
|
+
if (!isContent(raw) || /^[ \t]/.test(raw)) continue;
|
|
7261
|
+
if (!PRINT_ONLY_TOP_LEVEL_KEY_PATTERN.test(raw)) return null;
|
|
7262
|
+
if (/^jobs:[ \t]*$/.test(stripYamlComment(raw))) {
|
|
7263
|
+
if (jobsStart !== -1) return null;
|
|
7264
|
+
jobsStart = index;
|
|
7265
|
+
}
|
|
7266
|
+
}
|
|
7267
|
+
if (jobsStart === -1) return null;
|
|
7268
|
+
const steps = [];
|
|
7269
|
+
let jobIndent = -1;
|
|
7270
|
+
let keyIndent = -1;
|
|
7271
|
+
let inSteps = false;
|
|
7272
|
+
let stepKeyColumn = -1;
|
|
7273
|
+
let step = null;
|
|
7274
|
+
let body = null;
|
|
7275
|
+
const openStep = () => step;
|
|
7276
|
+
const closeStep = () => {
|
|
7277
|
+
if (step === null) return true;
|
|
7278
|
+
const placed = step;
|
|
7279
|
+
step = null;
|
|
7280
|
+
if (placed.uses === void 0 === (placed.run === void 0)) return false;
|
|
7281
|
+
steps.push(placed);
|
|
7282
|
+
return true;
|
|
7283
|
+
};
|
|
7284
|
+
for (let index = jobsStart + 1; index < lines.length; index += 1) {
|
|
7285
|
+
const raw = lines[index] ?? "";
|
|
7286
|
+
if (raw.trim() === "") continue;
|
|
7287
|
+
const indent = lineIndent(raw);
|
|
7288
|
+
if (body) {
|
|
7289
|
+
if (indent > body.column) {
|
|
7290
|
+
if (body.kind === "plain") return null;
|
|
7291
|
+
if (body.kind === "run") {
|
|
7292
|
+
openStep()?.run?.push(raw.trim());
|
|
7293
|
+
continue;
|
|
7294
|
+
}
|
|
7295
|
+
if (body.textColumn !== -1 && indent > body.textColumn) continue;
|
|
7296
|
+
body.textColumn = -1;
|
|
7297
|
+
if (!isContent(raw)) continue;
|
|
7298
|
+
if (body.entryColumn === -1) body.entryColumn = indent;
|
|
7299
|
+
if (indent !== body.entryColumn) return null;
|
|
7300
|
+
const entry = PLAIN_MAPPING_ENTRY_PATTERN.exec(stripYamlComment(raw).trim());
|
|
7301
|
+
if (!entry) return null;
|
|
7302
|
+
const name = entry[1] ?? "";
|
|
7303
|
+
const value2 = (entry[2] ?? "").trim();
|
|
7304
|
+
if (body.owner === "env" && isCodeLoadingEnvKey(name)) return null;
|
|
7305
|
+
if (body.owner === "with") {
|
|
7306
|
+
openStep()?.inputs.add(name);
|
|
7307
|
+
if (BLOCK_SCALAR_INDICATOR_PATTERN.test(value2)) {
|
|
7308
|
+
body.textColumn = indent;
|
|
7309
|
+
continue;
|
|
7310
|
+
}
|
|
7311
|
+
}
|
|
7312
|
+
if (!isPlainOneLineValue(value2)) return null;
|
|
7313
|
+
continue;
|
|
7314
|
+
}
|
|
7315
|
+
body = null;
|
|
7316
|
+
}
|
|
7317
|
+
if (!isContent(raw)) continue;
|
|
7318
|
+
const text = stripYamlComment(raw).trimEnd();
|
|
7319
|
+
if (indent === 0) break;
|
|
7320
|
+
if (jobIndent === -1) jobIndent = indent;
|
|
7321
|
+
if (indent < jobIndent) return null;
|
|
7322
|
+
if (indent === jobIndent) {
|
|
7323
|
+
if (!closeStep()) return null;
|
|
7324
|
+
if (!/^[ \t]*[A-Za-z_][A-Za-z0-9_-]*:[ \t]*$/.test(text)) return null;
|
|
7325
|
+
keyIndent = -1;
|
|
7326
|
+
inSteps = false;
|
|
7327
|
+
continue;
|
|
7328
|
+
}
|
|
7329
|
+
if (keyIndent === -1) keyIndent = indent;
|
|
7330
|
+
const isItem = /^[ \t]*-[ \t]+/.test(text);
|
|
7331
|
+
if (indent < keyIndent) return null;
|
|
7332
|
+
if (indent === keyIndent && !(inSteps && isItem)) {
|
|
7333
|
+
if (!closeStep()) return null;
|
|
7334
|
+
inSteps = false;
|
|
7335
|
+
const key2 = UNQUOTED_KEY_PATTERN.exec(text.trim());
|
|
7336
|
+
if (!key2 || !PUBLISH_ONLY_JOB_KEYS.has(key2[1] ?? "")) return null;
|
|
7337
|
+
const value2 = (key2[2] ?? "").trim();
|
|
7338
|
+
if (key2[1] === "steps") {
|
|
7339
|
+
if (value2 !== "") return null;
|
|
7340
|
+
inSteps = true;
|
|
7341
|
+
continue;
|
|
7342
|
+
}
|
|
7343
|
+
if (value2 === "" && PUBLISH_ONLY_JOB_MAPPING_KEYS.has(key2[1] ?? "")) {
|
|
7344
|
+
body = { kind: "mapping", column: indent, entryColumn: -1, textColumn: -1, owner: "job" };
|
|
7345
|
+
continue;
|
|
7346
|
+
}
|
|
7347
|
+
if (key2[1] === "needs" && NEEDS_FLOW_SEQUENCE_PATTERN.test(value2)) continue;
|
|
7348
|
+
if (!isPlainOneLineValue(value2)) return null;
|
|
7349
|
+
body = { kind: "plain", column: indent };
|
|
7350
|
+
continue;
|
|
7351
|
+
}
|
|
7352
|
+
if (!inSteps) return null;
|
|
7353
|
+
let keyText = text;
|
|
7354
|
+
let column = indent;
|
|
7355
|
+
if (isItem) {
|
|
7356
|
+
if (!closeStep()) return null;
|
|
7357
|
+
const item = /^([ \t]*-[ \t]+)([^]*)$/.exec(text);
|
|
7358
|
+
keyText = item?.[2] ?? "";
|
|
7359
|
+
column = (item?.[1] ?? "").length;
|
|
7360
|
+
stepKeyColumn = column;
|
|
7361
|
+
step = { inputs: /* @__PURE__ */ new Set() };
|
|
7362
|
+
} else if (indent !== stepKeyColumn || !step) {
|
|
7363
|
+
return null;
|
|
7364
|
+
}
|
|
7365
|
+
const key = UNQUOTED_KEY_PATTERN.exec(keyText.trim());
|
|
7366
|
+
if (!key || !PUBLISH_ONLY_STEP_KEYS.has(key[1] ?? "")) return null;
|
|
7367
|
+
const value = (key[2] ?? "").trim();
|
|
7368
|
+
const current = step ?? { inputs: /* @__PURE__ */ new Set() };
|
|
7369
|
+
if (key[1] === "with" || key[1] === "env") {
|
|
7370
|
+
if (value !== "") return null;
|
|
7371
|
+
body = { kind: "mapping", column, entryColumn: -1, textColumn: -1, owner: key[1] };
|
|
7372
|
+
} else if (key[1] === "uses") {
|
|
7373
|
+
if (current.uses !== void 0 || !isPlainOneLineValue(value)) return null;
|
|
7374
|
+
current.uses = value.replace(/^(['"])(.*)\1$/, "$2");
|
|
7375
|
+
body = { kind: "plain", column };
|
|
7376
|
+
} else if (key[1] === "run") {
|
|
7377
|
+
if (current.run !== void 0) return null;
|
|
7378
|
+
if (BLOCK_SCALAR_INDICATOR_PATTERN.test(value)) {
|
|
7379
|
+
current.run = [];
|
|
7380
|
+
body = { kind: "run", column };
|
|
7381
|
+
} else {
|
|
7382
|
+
if (!isPlainOneLineValue(value) || /^["']/.test(value)) return null;
|
|
7383
|
+
current.run = [value];
|
|
7384
|
+
body = { kind: "plain", column };
|
|
7385
|
+
}
|
|
7386
|
+
} else {
|
|
7387
|
+
if (!isPlainOneLineValue(value)) return null;
|
|
7388
|
+
body = { kind: "plain", column };
|
|
7389
|
+
}
|
|
7390
|
+
}
|
|
7391
|
+
if (!closeStep()) return null;
|
|
7392
|
+
return steps;
|
|
7393
|
+
}
|
|
7394
|
+
function joinShellContinuationLines(lines) {
|
|
7395
|
+
const joined = [];
|
|
7396
|
+
let pending = "";
|
|
7397
|
+
for (const line of lines) {
|
|
7398
|
+
const code = beforeShellComment(line).trimEnd();
|
|
7399
|
+
if (code.endsWith("\\")) {
|
|
7400
|
+
pending += `${code.slice(0, -1)} `;
|
|
7401
|
+
continue;
|
|
7402
|
+
}
|
|
7403
|
+
joined.push(`${pending}${line}`);
|
|
7404
|
+
pending = "";
|
|
7405
|
+
}
|
|
7406
|
+
if (pending !== "") joined.push(pending);
|
|
7407
|
+
return joined;
|
|
7408
|
+
}
|
|
7409
|
+
var CODE_LOADING_ENV_KEYS = /* @__PURE__ */ new Set([
|
|
7410
|
+
"BASH_ENV",
|
|
7411
|
+
"ENV",
|
|
7412
|
+
"NODE_OPTIONS",
|
|
7413
|
+
"LD_PRELOAD",
|
|
7414
|
+
"LD_AUDIT",
|
|
7415
|
+
"LD_LIBRARY_PATH",
|
|
7416
|
+
"PYTHONPATH",
|
|
7417
|
+
"PYTHONSTARTUP",
|
|
7418
|
+
"PERL5OPT",
|
|
7419
|
+
"PERL5LIB",
|
|
7420
|
+
"RUBYOPT",
|
|
7421
|
+
"PATH",
|
|
7422
|
+
"NPM_CONFIG_NODE_OPTIONS",
|
|
7423
|
+
"NPM_CONFIG_SCRIPT_SHELL"
|
|
7424
|
+
]);
|
|
7425
|
+
function isCodeLoadingEnvKey(name) {
|
|
7426
|
+
return CODE_LOADING_ENV_KEYS.has(name) || CODE_LOADING_ENV_KEYS.has(name.toUpperCase());
|
|
7427
|
+
}
|
|
7428
|
+
var PACKAGE_TARGET_FLAGS = /* @__PURE__ */ new Set([
|
|
7429
|
+
"--prefix",
|
|
7430
|
+
"-C",
|
|
7431
|
+
"--dir",
|
|
7432
|
+
"--cwd",
|
|
7433
|
+
"--workspace",
|
|
7434
|
+
"-w",
|
|
7435
|
+
"--workspaces",
|
|
7436
|
+
"-ws",
|
|
7437
|
+
"--filter",
|
|
7438
|
+
"-F",
|
|
7439
|
+
"-r",
|
|
7440
|
+
"--recursive",
|
|
7441
|
+
"--global",
|
|
7442
|
+
"-g"
|
|
7443
|
+
]);
|
|
7444
|
+
function flagName(word) {
|
|
7445
|
+
const equals = word.indexOf("=");
|
|
7446
|
+
return equals === -1 ? word : word.slice(0, equals);
|
|
7447
|
+
}
|
|
7448
|
+
function targetsRootPackageOnly(args) {
|
|
7449
|
+
return args.every((word) => word.startsWith("-") && !PACKAGE_TARGET_FLAGS.has(flagName(word)));
|
|
7450
|
+
}
|
|
7451
|
+
function isRegistryConfigKey(key) {
|
|
7452
|
+
return key.startsWith("//") || key === "registry" || key.endsWith(":registry");
|
|
7453
|
+
}
|
|
7454
|
+
function classifyPublishOnlyRunCommand(input) {
|
|
7455
|
+
const other = { kind: "other" };
|
|
7456
|
+
if (input.some((word) => /[`&|;<>]|\$\(/.test(word))) return other;
|
|
7457
|
+
let start = 0;
|
|
7458
|
+
while (start < input.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(input[start] ?? "")) {
|
|
7459
|
+
if (isCodeLoadingEnvKey(flagName(input[start] ?? ""))) return other;
|
|
7460
|
+
start += 1;
|
|
7461
|
+
}
|
|
7462
|
+
if (start === input.length) return other;
|
|
7463
|
+
const words = input.slice(start);
|
|
7464
|
+
const [first = "", second = "", third = ""] = words;
|
|
7465
|
+
if (words.some((word) => word.startsWith("--dry-run"))) return other;
|
|
7466
|
+
switch (first) {
|
|
7467
|
+
case "npm":
|
|
7468
|
+
case "pnpm":
|
|
7469
|
+
case "yarn": {
|
|
7470
|
+
if (second === "config") {
|
|
7471
|
+
return third === "set" && isRegistryConfigKey(words[3] ?? "") && words.length <= 5 ? { kind: "setup" } : other;
|
|
7472
|
+
}
|
|
7473
|
+
const publishFrom = second === "publish" ? 2 : first === "yarn" && second === "npm" && third === "publish" ? 3 : -1;
|
|
7474
|
+
if (publishFrom === -1) return other;
|
|
7475
|
+
return targetsRootPackageOnly(words.slice(publishFrom)) ? { kind: "publish", needs: ["package"] } : other;
|
|
7476
|
+
}
|
|
7477
|
+
case "twine":
|
|
7478
|
+
return second === "upload" ? { kind: "publish" } : other;
|
|
7479
|
+
case "uv":
|
|
7480
|
+
return second === "publish" && !words.some((word) => word.includes("build")) ? { kind: "publish" } : other;
|
|
7481
|
+
case "poetry":
|
|
7482
|
+
return second === "publish" && !words.some((word) => word.startsWith("--build")) ? { kind: "publish" } : other;
|
|
7483
|
+
case "gh":
|
|
7484
|
+
return second === "release" && (third === "create" || third === "upload") ? { kind: "publish" } : other;
|
|
7485
|
+
case "cargo":
|
|
7486
|
+
return second === "publish" && words.includes("--no-verify") && !words.includes("-n") ? { kind: "publish" } : other;
|
|
7487
|
+
default:
|
|
7488
|
+
return other;
|
|
7489
|
+
}
|
|
7490
|
+
}
|
|
7491
|
+
var PUBLISH_ONLY_SETUP_ACTIONS = /* @__PURE__ */ new Map([
|
|
7492
|
+
["actions/checkout", ["fetch-depth", "token", "persist-credentials", "fetch-tags", "show-progress"]],
|
|
7493
|
+
["actions/cache", null],
|
|
7494
|
+
["actions/upload-artifact", null],
|
|
7495
|
+
["actions/download-artifact", ["name", "path", "pattern", "merge-multiple", "run-id", "github-token"]],
|
|
7496
|
+
["actions/setup-node", null],
|
|
7497
|
+
["actions/setup-python", null],
|
|
7498
|
+
["actions/setup-go", null],
|
|
7499
|
+
["actions/setup-java", null],
|
|
7500
|
+
["actions/setup-dotnet", null],
|
|
7501
|
+
["astral-sh/setup-uv", null],
|
|
7502
|
+
["oven-sh/setup-bun", null],
|
|
7503
|
+
["pnpm/action-setup", ["version", "package_json_file", "dest", "standalone"]],
|
|
7504
|
+
["docker/login-action", null]
|
|
7505
|
+
]);
|
|
7506
|
+
function classifyPublishOnlyUsesStep(uses, inputs) {
|
|
7507
|
+
const at = uses.indexOf("@");
|
|
7508
|
+
if (at === -1) return "other";
|
|
7509
|
+
const action = uses.slice(0, at);
|
|
7510
|
+
if (action === "pypa/gh-action-pypi-publish") return "publish";
|
|
7511
|
+
if (action === "softprops/action-gh-release") return inputs.has("files") ? "publish" : "other";
|
|
7512
|
+
if (!PUBLISH_ONLY_SETUP_ACTIONS.has(action)) return "other";
|
|
7513
|
+
const allowed = PUBLISH_ONLY_SETUP_ACTIONS.get(action);
|
|
7514
|
+
return allowed === null || [...inputs].every((input) => (allowed ?? []).includes(input)) ? "setup" : "other";
|
|
7515
|
+
}
|
|
7516
|
+
var WORKFLOW_ON_KEY_PATTERN = /^(?:on|"on"|'on'|true):(?:[ \t]|$)/;
|
|
7517
|
+
var WORKFLOW_TRIGGER_NAME_PATTERN = /^[a-z_]+$/;
|
|
7518
|
+
var YAML_LINE_BREAK_NOT_SPLIT_PATTERN = /\r(?!\n)|[\u0085\u2028\u2029]/;
|
|
7519
|
+
function stripYamlComment(text) {
|
|
7520
|
+
return text.replace(/(?:^|[ \t])#[^]*$/, "");
|
|
7521
|
+
}
|
|
7522
|
+
function unquoteYamlScalar(text) {
|
|
7523
|
+
return text.trim().replace(/^(['"])(.*)\1$/, "$2");
|
|
7524
|
+
}
|
|
7525
|
+
function yamlInlineList(text) {
|
|
7526
|
+
const value = stripYamlComment(text).trim();
|
|
7527
|
+
if (value === "") return [];
|
|
7528
|
+
const flow = /^\[(.*)\]$/.exec(value);
|
|
7529
|
+
if (flow) {
|
|
7530
|
+
return (flow[1] ?? "").split(",").map(unquoteYamlScalar).filter((item) => item !== "");
|
|
7531
|
+
}
|
|
7532
|
+
return /^[{&*|>]/.test(value) ? null : [unquoteYamlScalar(value)];
|
|
7533
|
+
}
|
|
7534
|
+
function githubWorkflowTriggers(contents) {
|
|
7535
|
+
if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents)) return null;
|
|
7536
|
+
const lines = contents.split(/\r?\n/);
|
|
7537
|
+
const onIndex = lines.findIndex((line) => WORKFLOW_ON_KEY_PATTERN.test(line));
|
|
7538
|
+
if (onIndex === -1) return null;
|
|
7539
|
+
const onLine = lines[onIndex] ?? "";
|
|
7540
|
+
const inline = yamlInlineList(onLine.slice(onLine.indexOf(":") + 1));
|
|
7541
|
+
if (inline === null) return null;
|
|
7542
|
+
const triggers = /* @__PURE__ */ new Set();
|
|
7543
|
+
if (inline.length > 0) {
|
|
7544
|
+
for (const name of inline) {
|
|
7545
|
+
if (!WORKFLOW_TRIGGER_NAME_PATTERN.test(name)) return null;
|
|
7546
|
+
triggers.add(name);
|
|
7547
|
+
}
|
|
7548
|
+
return triggers;
|
|
7549
|
+
}
|
|
7550
|
+
const isContent = (line) => line.trim() !== "" && !line.trimStart().startsWith("#");
|
|
7551
|
+
const firstIndex = lines.findIndex((line, index) => index > onIndex && isContent(line));
|
|
7552
|
+
if (firstIndex !== -1 && /^-[ \t]/.test(lines[firstIndex] ?? "")) {
|
|
7553
|
+
for (let index = firstIndex; index < lines.length; index += 1) {
|
|
7554
|
+
const raw = lines[index] ?? "";
|
|
7555
|
+
if (!isContent(raw)) continue;
|
|
7556
|
+
const item = /^-[ \t]+([^]*)$/.exec(raw);
|
|
7557
|
+
if (!item) break;
|
|
7558
|
+
const name = unquoteYamlScalar(stripYamlComment(item[1] ?? ""));
|
|
7559
|
+
if (!WORKFLOW_TRIGGER_NAME_PATTERN.test(name)) return null;
|
|
7560
|
+
triggers.add(name);
|
|
7561
|
+
}
|
|
7562
|
+
return triggers.size > 0 ? triggers : null;
|
|
7563
|
+
}
|
|
7564
|
+
let triggerIndent = -1;
|
|
7565
|
+
let mappingForm = null;
|
|
7566
|
+
let inTrigger = false;
|
|
7567
|
+
for (let index = onIndex + 1; index < lines.length; index += 1) {
|
|
7568
|
+
const raw = lines[index] ?? "";
|
|
7569
|
+
if (!isContent(raw)) continue;
|
|
7570
|
+
const indent = lineIndent(raw);
|
|
7571
|
+
if (indent === 0) break;
|
|
7572
|
+
if (triggerIndent === -1) triggerIndent = indent;
|
|
7573
|
+
if (indent < triggerIndent) return null;
|
|
7574
|
+
if (indent > triggerIndent) {
|
|
7575
|
+
if (!inTrigger) return null;
|
|
7576
|
+
continue;
|
|
7577
|
+
}
|
|
7578
|
+
const item = /^[ \t]*-[ \t]+([^]*)$/.exec(raw);
|
|
7579
|
+
if (item && mappingForm === true) continue;
|
|
7580
|
+
if (item) {
|
|
7581
|
+
mappingForm = false;
|
|
7582
|
+
inTrigger = false;
|
|
7583
|
+
const name = unquoteYamlScalar(stripYamlComment(item[1] ?? ""));
|
|
7584
|
+
if (!WORKFLOW_TRIGGER_NAME_PATTERN.test(name)) return null;
|
|
7585
|
+
triggers.add(name);
|
|
7586
|
+
continue;
|
|
7587
|
+
}
|
|
7588
|
+
if (mappingForm === false) return null;
|
|
7589
|
+
mappingForm = true;
|
|
7590
|
+
const key = /^[ \t]*([A-Za-z_]+):([^]*)$/.exec(raw);
|
|
7591
|
+
if (!key || !WORKFLOW_TRIGGER_NAME_PATTERN.test(key[1] ?? "")) return null;
|
|
7592
|
+
const rest = stripYamlComment(key[2] ?? "").trim();
|
|
7593
|
+
if (rest !== "" && rest !== "{}" && rest !== "null" && rest !== "~") return null;
|
|
7594
|
+
triggers.add(key[1] ?? "");
|
|
7595
|
+
inTrigger = true;
|
|
7596
|
+
}
|
|
7597
|
+
return triggers.size > 0 ? triggers : null;
|
|
7598
|
+
}
|
|
7599
|
+
var PULL_REQUEST_TRIGGER_NAMES = new Set(PULL_REQUEST_TRIGGERS);
|
|
7600
|
+
var PULL_REQUEST_CI_WORKFLOW_LABEL = "Pull-request CI workflow";
|
|
7601
|
+
var WORKFLOW_WITHOUT_PULL_REQUEST_TRIGGER_LABEL = "CI workflow with no pull-request trigger";
|
|
7602
|
+
var WORKFLOW_TRIGGER_NOT_READ_LABEL = "CI workflow (trigger not read)";
|
|
7603
|
+
function b2WorkflowCitationBaseLabels(repoPath, file) {
|
|
7604
|
+
if (!/^\.github\/workflows\//.test(file)) return [WORKFLOW_TRIGGER_NOT_READ_LABEL];
|
|
7605
|
+
const fullPath = join(repoPath, file);
|
|
7606
|
+
const triggers = isRegularFile(fullPath) ? githubWorkflowTriggers(withoutContentReadAttribution(() => readRepoText(fullPath, "utf8"))) : null;
|
|
7607
|
+
if (!triggers) return [WORKFLOW_TRIGGER_NOT_READ_LABEL];
|
|
7608
|
+
const names = [...triggers.keys()];
|
|
7609
|
+
if (names.some((name) => PULL_REQUEST_TRIGGER_NAMES.has(name))) {
|
|
7610
|
+
return [PULL_REQUEST_CI_WORKFLOW_LABEL];
|
|
7611
|
+
}
|
|
7612
|
+
const named = [3, 2, 1].filter((shown) => shown <= names.length).map(
|
|
7613
|
+
(shown) => `${WORKFLOW_WITHOUT_PULL_REQUEST_TRIGGER_LABEL} (on: ${names.slice(0, shown).join(", ")}${names.length > shown ? ", ..." : ""})`
|
|
7614
|
+
);
|
|
7615
|
+
return [...named, WORKFLOW_WITHOUT_PULL_REQUEST_TRIGGER_LABEL];
|
|
7616
|
+
}
|
|
7617
|
+
function fittedCiWorkflowCitationLabel(candidates, label) {
|
|
7618
|
+
for (const candidate of candidates) {
|
|
7619
|
+
const full = label(candidate);
|
|
7620
|
+
if (full.length <= EVIDENCE_LABEL_MAX_LENGTH) return full;
|
|
7621
|
+
}
|
|
7622
|
+
return label(candidates[candidates.length - 1] ?? WORKFLOW_TRIGGER_NOT_READ_LABEL);
|
|
7623
|
+
}
|
|
7624
|
+
var PRINT_ONLY_RUN_LINE_PATTERN = /^(?:(?:echo|printf)(?:[ \t][^|&;`$()<>\\]*)?|true|:|exit[ \t]+0)$/;
|
|
7625
|
+
var BLOCK_SCALAR_INDICATOR_PATTERN = /^[|>][-+0-9]*$/;
|
|
7626
|
+
var PRINT_ONLY_JOB_KEYS = /* @__PURE__ */ new Set(["runs-on", "name", "timeout-minutes", "steps"]);
|
|
7627
|
+
var PRINT_ONLY_TOP_LEVEL_KEY_PATTERN = /^(?:name|run-name|on|true|permissions|concurrency|jobs)[ \t]*:/;
|
|
7628
|
+
var OTHER_LEADING_BLANK_PATTERN = /^[ \t]*[\v\f\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff]/m;
|
|
7629
|
+
var PRINT_ONLY_STEP_KEYS = /* @__PURE__ */ new Set(["name", "uses", "run"]);
|
|
7630
|
+
var UNQUOTED_KEY_PATTERN = /^([a-z][a-z-]*):(?:[ \t]+([^]*))?$/;
|
|
7631
|
+
var CHECKOUT_ACTION_PATTERN = /^actions\/checkout@[A-Za-z0-9._/-]+$/;
|
|
7632
|
+
function isPrintOnlyShellLine(text) {
|
|
7633
|
+
const command = text.trim();
|
|
7634
|
+
const doubles = command.split('"').length - 1;
|
|
7635
|
+
const singles = command.split("'").length - 1;
|
|
7636
|
+
const balanced = (doubles === 0 || singles === 0) && doubles % 2 === 0 && singles % 2 === 0;
|
|
7637
|
+
return balanced && PRINT_ONLY_RUN_LINE_PATTERN.test(command);
|
|
7638
|
+
}
|
|
7639
|
+
function isOneLineScalar(value) {
|
|
7640
|
+
const quote = value[0];
|
|
7641
|
+
if (quote !== '"' && quote !== "'") return true;
|
|
7642
|
+
return value.length > 1 && value.endsWith(quote);
|
|
7643
|
+
}
|
|
7644
|
+
function isPrintOnlyWorkflow(contents) {
|
|
7645
|
+
if (YAML_LINE_BREAK_NOT_SPLIT_PATTERN.test(contents) || OTHER_LEADING_BLANK_PATTERN.test(contents)) return false;
|
|
7646
|
+
const lines = contents.split(/\r?\n/);
|
|
7647
|
+
const isContent = (line) => line.trim() !== "" && !line.trimStart().startsWith("#");
|
|
7648
|
+
let jobsStart = -1;
|
|
7649
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
7650
|
+
const raw = lines[index] ?? "";
|
|
7651
|
+
if (!isContent(raw) || /^[ \t]/.test(raw)) continue;
|
|
7652
|
+
if (!PRINT_ONLY_TOP_LEVEL_KEY_PATTERN.test(raw)) return false;
|
|
7653
|
+
if (/^jobs:[ \t]*$/.test(stripYamlComment(raw))) {
|
|
7654
|
+
if (jobsStart !== -1) return false;
|
|
7655
|
+
jobsStart = index;
|
|
7656
|
+
}
|
|
7657
|
+
}
|
|
7658
|
+
if (jobsStart === -1) return false;
|
|
7659
|
+
let jobIndent = -1;
|
|
7660
|
+
let keyIndent = -1;
|
|
7661
|
+
let inSteps = false;
|
|
7662
|
+
let stepKeyColumn = -1;
|
|
7663
|
+
let step = null;
|
|
7664
|
+
let body = null;
|
|
7665
|
+
let runSteps = 0;
|
|
7666
|
+
const closeStep = () => {
|
|
7667
|
+
if (step && step.run && step.uses) return false;
|
|
7668
|
+
if (step?.run) runSteps += 1;
|
|
7669
|
+
step = null;
|
|
7670
|
+
return true;
|
|
5811
7671
|
};
|
|
7672
|
+
for (let index = jobsStart + 1; index < lines.length; index += 1) {
|
|
7673
|
+
const raw = lines[index] ?? "";
|
|
7674
|
+
if (raw.trim() === "") continue;
|
|
7675
|
+
const indent = lineIndent(raw);
|
|
7676
|
+
if (body) {
|
|
7677
|
+
if (indent > body.column) {
|
|
7678
|
+
if (!body.block) return false;
|
|
7679
|
+
const text2 = raw.trim();
|
|
7680
|
+
if (!text2.startsWith("#") && !isPrintOnlyShellLine(text2)) return false;
|
|
7681
|
+
continue;
|
|
7682
|
+
}
|
|
7683
|
+
body = null;
|
|
7684
|
+
}
|
|
7685
|
+
if (!isContent(raw)) continue;
|
|
7686
|
+
const text = stripYamlComment(raw).trimEnd();
|
|
7687
|
+
if (indent === 0) break;
|
|
7688
|
+
if (jobIndent === -1) jobIndent = indent;
|
|
7689
|
+
if (indent < jobIndent) return false;
|
|
7690
|
+
if (indent === jobIndent) {
|
|
7691
|
+
if (!closeStep()) return false;
|
|
7692
|
+
if (!/^[ \t]*[A-Za-z_][A-Za-z0-9_-]*:[ \t]*$/.test(text)) return false;
|
|
7693
|
+
keyIndent = -1;
|
|
7694
|
+
inSteps = false;
|
|
7695
|
+
continue;
|
|
7696
|
+
}
|
|
7697
|
+
if (keyIndent === -1) keyIndent = indent;
|
|
7698
|
+
const isItem = /^[ \t]*-[ \t]+/.test(text);
|
|
7699
|
+
if (indent < keyIndent) return false;
|
|
7700
|
+
if (indent === keyIndent && !(inSteps && isItem)) {
|
|
7701
|
+
if (!closeStep()) return false;
|
|
7702
|
+
inSteps = false;
|
|
7703
|
+
const key2 = UNQUOTED_KEY_PATTERN.exec(text.trim());
|
|
7704
|
+
if (!key2 || !PRINT_ONLY_JOB_KEYS.has(key2[1] ?? "")) return false;
|
|
7705
|
+
const value2 = key2[2] ?? "";
|
|
7706
|
+
if (key2[1] === "steps") {
|
|
7707
|
+
if (value2 !== "") return false;
|
|
7708
|
+
inSteps = true;
|
|
7709
|
+
continue;
|
|
7710
|
+
}
|
|
7711
|
+
if (value2 === "" || /^[[{&*|>!]/.test(value2) || !isOneLineScalar(value2)) return false;
|
|
7712
|
+
continue;
|
|
7713
|
+
}
|
|
7714
|
+
if (!inSteps) return false;
|
|
7715
|
+
let keyText = text;
|
|
7716
|
+
let column = indent;
|
|
7717
|
+
if (isItem) {
|
|
7718
|
+
if (!closeStep()) return false;
|
|
7719
|
+
const item = /^([ \t]*-[ \t]+)([^]*)$/.exec(text);
|
|
7720
|
+
keyText = item?.[2] ?? "";
|
|
7721
|
+
column = (item?.[1] ?? "").length;
|
|
7722
|
+
stepKeyColumn = column;
|
|
7723
|
+
step = {};
|
|
7724
|
+
} else if (indent !== stepKeyColumn || !step) {
|
|
7725
|
+
return false;
|
|
7726
|
+
}
|
|
7727
|
+
const key = UNQUOTED_KEY_PATTERN.exec(keyText.trim());
|
|
7728
|
+
if (!key || !PRINT_ONLY_STEP_KEYS.has(key[1] ?? "")) return false;
|
|
7729
|
+
const value = key[2] ?? "";
|
|
7730
|
+
const current = step ?? {};
|
|
7731
|
+
if (key[1] === "name") {
|
|
7732
|
+
if (value === "" || /^[[{&*|>!]/.test(value) || !isOneLineScalar(value)) return false;
|
|
7733
|
+
} else if (key[1] === "uses") {
|
|
7734
|
+
if (current.uses || !CHECKOUT_ACTION_PATTERN.test(value)) return false;
|
|
7735
|
+
current.uses = true;
|
|
7736
|
+
} else {
|
|
7737
|
+
if (current.run) return false;
|
|
7738
|
+
current.run = true;
|
|
7739
|
+
if (BLOCK_SCALAR_INDICATOR_PATTERN.test(value)) {
|
|
7740
|
+
body = { column, block: true };
|
|
7741
|
+
} else {
|
|
7742
|
+
if (value === "" || !isPrintOnlyShellLine(value) || /^["']/.test(value)) return false;
|
|
7743
|
+
body = { column, block: false };
|
|
7744
|
+
}
|
|
7745
|
+
}
|
|
7746
|
+
}
|
|
7747
|
+
if (!closeStep()) return false;
|
|
7748
|
+
return runSteps > 0;
|
|
7749
|
+
}
|
|
7750
|
+
function findB3UnshownCheckWorkflows(root, counted, describePath = (file) => file) {
|
|
7751
|
+
const printOnly = [];
|
|
7752
|
+
for (const file of counted.filter((path) => /^\.github\/workflows\//.test(path))) {
|
|
7753
|
+
const fullPath = join(root, file);
|
|
7754
|
+
if (!isRegularFile(fullPath)) continue;
|
|
7755
|
+
const contents = withoutContentReadAttribution(() => readRepoText(fullPath, "utf8"));
|
|
7756
|
+
if (isPrintOnlyWorkflow(contents)) printOnly.push(describePath(file));
|
|
7757
|
+
}
|
|
7758
|
+
return { printOnly };
|
|
7759
|
+
}
|
|
7760
|
+
function mergeB3UnshownCheckWorkflows(own, root) {
|
|
7761
|
+
return { printOnly: [...own.printOnly, ...root.printOnly] };
|
|
7762
|
+
}
|
|
7763
|
+
function b3UnshownCheckCount(unshown) {
|
|
7764
|
+
return new Set(unshown.printOnly).size;
|
|
7765
|
+
}
|
|
7766
|
+
function b3UnshownCheckDisclosure(unshown, budget = Number.POSITIVE_INFINITY) {
|
|
7767
|
+
const count = b3UnshownCheckCount(unshown);
|
|
7768
|
+
if (count === 0) return "";
|
|
7769
|
+
const tail = ` The PR-gate CI workflow count still includes ${count === 1 ? "it" : "them"}.`;
|
|
7770
|
+
const parts = (shown) => [
|
|
7771
|
+
`${unshown.printOnly.slice(0, shown).join(", ")}${unshown.printOnly.length > shown ? ` and ${unshown.printOnly.length - shown} more` : ""} only print${unshown.printOnly.length === 1 ? "s" : ""} text`
|
|
7772
|
+
];
|
|
7773
|
+
for (let shown = 3; shown > 0; shown -= 1) {
|
|
7774
|
+
const sentence = `${WITAN_B3_UNSHOWN_CHECK_PREFIX}${parts(shown).join("; ")}.${tail}`;
|
|
7775
|
+
if (sentence.length <= budget) return sentence;
|
|
7776
|
+
}
|
|
7777
|
+
const countOnly = `${WITAN_B3_UNSHOWN_CHECK_PREFIX}${count} workflow${count === 1 ? "" : "s"}.${tail}`;
|
|
7778
|
+
return countOnly.length <= budget ? countOnly : "";
|
|
7779
|
+
}
|
|
7780
|
+
function b3UnshownCheckCondition(unshown) {
|
|
7781
|
+
const count = b3UnshownCheckCount(unshown);
|
|
7782
|
+
const files = unshown.printOnly.map((path) => `${basename(path)} only prints text`);
|
|
7783
|
+
const lead = `includes ${count} not shown to run a check`;
|
|
7784
|
+
for (let shown = Math.min(files.length, 3); shown > 0; shown -= 1) {
|
|
7785
|
+
const condition = `${lead}: ${files.slice(0, shown).join("; ")}${files.length > shown ? "; ..." : ""}`;
|
|
7786
|
+
if (condition.length <= METRIC_CONDITION_MAX_LENGTH) return condition;
|
|
7787
|
+
}
|
|
7788
|
+
return lead;
|
|
5812
7789
|
}
|
|
5813
7790
|
function auditFreshnessCommitterYearFromGitResult(result, scanLimitations) {
|
|
5814
7791
|
if (!result.ok) {
|
|
@@ -6167,7 +8144,6 @@ function hasCredibleTestScript(scripts) {
|
|
|
6167
8144
|
var CI_TEST_COMMAND_PATTERN = /\b(pytest|go\s+test|cargo\s+test|mvn\s+test|gradle\s+test|dotnet\s+test|ctest|make\s+test|jest|vitest|mocha|rspec|phpunit|pnpm\s+(?:run\s+)?test|npm\s+(?:run\s+)?test|yarn\s+(?:run\s+)?test|node\s+--test|node:test)\b|--target[=\s]+test\b/i;
|
|
6168
8145
|
var CI_LINT_COMMAND_PATTERN = /\b(eslint|pylint|flake8|black|ruff)\b/i;
|
|
6169
8146
|
var CI_TYPECHECK_COMMAND_PATTERN = /\b(tsc|mypy|pyright)\b/i;
|
|
6170
|
-
var SCHEDULE_TRIGGER_PATTERN = /(^|\n)\s*schedule:\s*\r?\n\s*-\s*cron:/;
|
|
6171
8147
|
var PUBLISHED_RESULT_MARKER_PATTERN = /actions\/deploy-pages|actions\/upload-pages-artifact|peaceiris\/actions-gh-pages|git\s+push\b|create-or-update-comment/i;
|
|
6172
8148
|
var EPHEMERAL_ARTIFACT_ONLY_PATTERN = /actions\/upload-artifact/i;
|
|
6173
8149
|
var SCHEDULED_HEALTH_PATH_PATTERN = /(?:health|smoke|monitor|status|uptime|canary)/i;
|
|
@@ -6485,7 +8461,7 @@ function parseGitTrackedFiles(repoPath, output, includeHardExcluded, registerWit
|
|
|
6485
8461
|
continue;
|
|
6486
8462
|
}
|
|
6487
8463
|
try {
|
|
6488
|
-
const stat =
|
|
8464
|
+
const stat = lstatSync2(fullPath);
|
|
6489
8465
|
if (!stat.isFile()) {
|
|
6490
8466
|
recordContentSkip(fullPath, "non_regular_file", true);
|
|
6491
8467
|
withhold(file, "non_regular_file");
|
|
@@ -6560,19 +8536,19 @@ function resolveMonorepoContext(repoPath, scanLimitations) {
|
|
|
6560
8536
|
}
|
|
6561
8537
|
const top = topResult.stdout.trim();
|
|
6562
8538
|
if (!top) return null;
|
|
6563
|
-
const rootResolved =
|
|
8539
|
+
const rootResolved = resolve2(top);
|
|
6564
8540
|
const repoResolved = (() => {
|
|
6565
8541
|
try {
|
|
6566
|
-
return
|
|
8542
|
+
return realpathSync2(repoPath);
|
|
6567
8543
|
} catch {
|
|
6568
|
-
return
|
|
8544
|
+
return resolve2(repoPath);
|
|
6569
8545
|
}
|
|
6570
8546
|
})();
|
|
6571
8547
|
if (rootResolved === repoResolved) return null;
|
|
6572
|
-
if (!repoResolved.startsWith(rootResolved +
|
|
8548
|
+
if (!repoResolved.startsWith(rootResolved + sep2)) return null;
|
|
6573
8549
|
const rootFiles = listGitTrackedFiles(rootResolved, scanLimitations);
|
|
6574
8550
|
if (!rootFiles || rootFiles.length === 0) return null;
|
|
6575
|
-
const subRel = repoResolved.slice(rootResolved.length + 1).split(
|
|
8551
|
+
const subRel = repoResolved.slice(rootResolved.length + 1).split(sep2).join("/");
|
|
6576
8552
|
const subPrefix = subRel ? `${subRel}/` : "";
|
|
6577
8553
|
const sharedFiles = rootFiles.filter(
|
|
6578
8554
|
(file) => (isLockfile(file) || isCiWorkflow(file) || isDependencyUpdateConfig(file)) && (subPrefix === "" || !file.startsWith(subPrefix))
|
|
@@ -6605,7 +8581,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
|
|
|
6605
8581
|
}
|
|
6606
8582
|
if (entry.isDirectory()) {
|
|
6607
8583
|
try {
|
|
6608
|
-
if (
|
|
8584
|
+
if (lstatSync2(fullPath, { bigint: true }).dev !== BigInt(rootDevice)) {
|
|
6609
8585
|
recordContentSkip(fullPath, "denied_path");
|
|
6610
8586
|
continue;
|
|
6611
8587
|
}
|
|
@@ -6616,7 +8592,7 @@ function visitRepoDir(repoPath, dirPath, files, rootDevice) {
|
|
|
6616
8592
|
visitRepoDir(repoPath, fullPath, files, rootDevice);
|
|
6617
8593
|
continue;
|
|
6618
8594
|
}
|
|
6619
|
-
const repoRelativePath =
|
|
8595
|
+
const repoRelativePath = relative2(repoPath, fullPath);
|
|
6620
8596
|
if (!entry.isFile()) {
|
|
6621
8597
|
recordContentSkip(fullPath, "non_regular_file");
|
|
6622
8598
|
registerWithheldRepoPath(repoRelativePath, "non_regular_file");
|
|
@@ -6896,7 +8872,7 @@ function hasLiveFrameworkAssertion({ commentFree, code }) {
|
|
|
6896
8872
|
function hasFrameworkSuiteLevelSkip({ code }) {
|
|
6897
8873
|
return DOTNET_IGNORED_CLASS_PATTERN.test(code) || SCALATEST_IMPORT_PATTERN.test(code) && SCALATEST_IGNORED_CLASS_PATTERN.test(code) || KOTEST_IMPORT_PATTERN.test(code) && KOTEST_IGNORED_CLASS_PATTERN.test(code);
|
|
6898
8874
|
}
|
|
6899
|
-
function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
|
|
8875
|
+
function measureNonHollowTestShare(repoPath, files, useV17Detectors = false, runWideSkip) {
|
|
6900
8876
|
let nonHollowCount = 0;
|
|
6901
8877
|
let ratedCount = 0;
|
|
6902
8878
|
for (const file of files) {
|
|
@@ -6906,16 +8882,193 @@ function measureNonHollowTestShare(repoPath, files, useV17Detectors = false) {
|
|
|
6906
8882
|
let maskedSource;
|
|
6907
8883
|
const masked = () => maskedSource ??= maskCommentsAndStrings(contents);
|
|
6908
8884
|
const frameworkSuiteSkipped = /\bIgnored?\b/.test(contents) && hasFrameworkSuiteLevelSkip(masked());
|
|
6909
|
-
const
|
|
8885
|
+
const asserting = JS_TEST_ASSERTION_PATTERN.test(contents) || CPP_TEST_ASSERTION_PATTERN.test(contents) || MULTI_ECOSYSTEM_TEST_ASSERTION_PATTERN.test(contents) || hasLiveAvaAssertion(contents) || hasLiveFrameworkAssertion(masked());
|
|
8886
|
+
const cannotRunAsTest = runWideSkip !== void 0 && (PYTEST_CONFTEST_PATTERN.test(file) || runWideSkip.disabledByRunWideSkip(file) || hasUnconditionalPytestModuleSkip(file, contents));
|
|
8887
|
+
const nonHollow = asserting && !SUITE_LEVEL_SKIP_PATTERN.test(contents) && !frameworkSuiteSkipped && !cannotRunAsTest;
|
|
6910
8888
|
if (nonHollow) {
|
|
6911
8889
|
nonHollowCount += 1;
|
|
6912
8890
|
ratedCount += 1;
|
|
6913
|
-
} else if (NAME_SHAPED_TEST_FILE_PATTERN.test(file) || useV17Detectors && NAME_SHAPED_TEST_FILE_PATTERN_V17.test(file) || frameworkSuiteSkipped && SUITE_SKIP_NAME_SHAPED_TEST_FILE_PATTERN.test(file)) {
|
|
8891
|
+
} else if (NAME_SHAPED_TEST_FILE_PATTERN.test(file) || useV17Detectors && NAME_SHAPED_TEST_FILE_PATTERN_V17.test(file) || frameworkSuiteSkipped && SUITE_SKIP_NAME_SHAPED_TEST_FILE_PATTERN.test(file) || cannotRunAsTest && asserting) {
|
|
6914
8892
|
ratedCount += 1;
|
|
6915
8893
|
}
|
|
6916
8894
|
}
|
|
6917
8895
|
return { nonHollowCount, ratedCount };
|
|
6918
8896
|
}
|
|
8897
|
+
var PYTEST_CONFTEST_PATTERN = /(^|\/)conftest\.py$/;
|
|
8898
|
+
var PYTEST_MODULE_SKIP_MARK_PATTERN = /\bpytest\.mark\.skip\b(?!if)/;
|
|
8899
|
+
var PYTEST_MODULE_SKIP_ASSIGNMENT_PATTERN = /^pytestmark[ \t]*=(?!=)/;
|
|
8900
|
+
var PYTEST_MODULE_SKIP_MAX_STATEMENT_LINES = 20;
|
|
8901
|
+
function pythonCodeOutsideStrings(line) {
|
|
8902
|
+
let code = "";
|
|
8903
|
+
let quote = "";
|
|
8904
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
8905
|
+
const character = line[index] ?? "";
|
|
8906
|
+
if (quote !== "") {
|
|
8907
|
+
if (character === "\\") index += 1;
|
|
8908
|
+
else if (character === quote) {
|
|
8909
|
+
code += character;
|
|
8910
|
+
quote = "";
|
|
8911
|
+
}
|
|
8912
|
+
continue;
|
|
8913
|
+
}
|
|
8914
|
+
if (character === "#") break;
|
|
8915
|
+
if (character === '"' || character === "'") quote = character;
|
|
8916
|
+
code += character;
|
|
8917
|
+
}
|
|
8918
|
+
return code;
|
|
8919
|
+
}
|
|
8920
|
+
var PYTEST_MARK_NAME_GLOBAL_PATTERN = /\bpytestmark\b/g;
|
|
8921
|
+
function countPytestMarkMentions(text) {
|
|
8922
|
+
return [...text.matchAll(PYTEST_MARK_NAME_GLOBAL_PATTERN)].length;
|
|
8923
|
+
}
|
|
8924
|
+
function pythonBracketDelta(code) {
|
|
8925
|
+
let depth = 0;
|
|
8926
|
+
for (const character of code) {
|
|
8927
|
+
if (character === "(" || character === "[" || character === "{") depth += 1;
|
|
8928
|
+
else if (character === ")" || character === "]" || character === "}") depth -= 1;
|
|
8929
|
+
}
|
|
8930
|
+
return depth;
|
|
8931
|
+
}
|
|
8932
|
+
function hasUnconditionalPytestModuleSkip(file, contents) {
|
|
8933
|
+
if (!/\.py$/.test(file)) return false;
|
|
8934
|
+
const lines = contents.split("\n");
|
|
8935
|
+
let mentionCount = 0;
|
|
8936
|
+
let start = -1;
|
|
8937
|
+
for (const [index, line] of lines.entries()) {
|
|
8938
|
+
const count = Math.max(
|
|
8939
|
+
countPytestMarkMentions(stripPythonLineComment(line)),
|
|
8940
|
+
countPytestMarkMentions(pythonCodeOutsideStrings(line))
|
|
8941
|
+
);
|
|
8942
|
+
if (count > 0 && start === -1) start = index;
|
|
8943
|
+
mentionCount += count;
|
|
8944
|
+
}
|
|
8945
|
+
if (mentionCount !== 1) return false;
|
|
8946
|
+
const first = lines[start] ?? "";
|
|
8947
|
+
if (!PYTEST_MODULE_SKIP_ASSIGNMENT_PATTERN.test(first) || !PYTEST_MODULE_SKIP_MARK_PATTERN.test(pythonCodeOutsideStrings(first))) {
|
|
8948
|
+
return false;
|
|
8949
|
+
}
|
|
8950
|
+
const statement = [];
|
|
8951
|
+
let depth = 0;
|
|
8952
|
+
for (let index = start; index < lines.length; index += 1) {
|
|
8953
|
+
if (statement.length === PYTEST_MODULE_SKIP_MAX_STATEMENT_LINES) return false;
|
|
8954
|
+
const raw = (lines[index] ?? "").replace(/\r$/, "");
|
|
8955
|
+
const code = pythonCodeOutsideStrings(raw);
|
|
8956
|
+
if (/\\[ \t]*$/.test(code) || /\\[ \t]*$/.test(raw)) return false;
|
|
8957
|
+
statement.push(stripPythonLineComment(raw));
|
|
8958
|
+
depth += pythonBracketDelta(code);
|
|
8959
|
+
if (depth <= 0) break;
|
|
8960
|
+
}
|
|
8961
|
+
if (depth > 0) return false;
|
|
8962
|
+
return !/\bif\b/.test(statement.join("\n"));
|
|
8963
|
+
}
|
|
8964
|
+
function stripPythonLineComment(line) {
|
|
8965
|
+
const hash = line.indexOf("#");
|
|
8966
|
+
return hash === -1 ? line : line.slice(0, hash);
|
|
8967
|
+
}
|
|
8968
|
+
function isGovernedPytestFile(skip, file) {
|
|
8969
|
+
if (!/\.py$/.test(file) || PYTEST_CONFTEST_PATTERN.test(file)) return false;
|
|
8970
|
+
return skip.scope === "" || file.startsWith(`${skip.scope}/`);
|
|
8971
|
+
}
|
|
8972
|
+
var PYTEST_COLLECTION_HOOK_PATTERN = /^def[ \t]+pytest_collection_modifyitems[ \t]*\(/;
|
|
8973
|
+
var PYTEST_COLLECTION_HOOK_NAME = "pytest_collection_modifyitems";
|
|
8974
|
+
var PYTEST_COLLECTION_HOOK_NAME_PATTERN = /\bpytest_collection_modifyitems\b/;
|
|
8975
|
+
var PYTEST_HOOK_CONDITIONAL_PATTERN = /^(?:if|elif|else|while|try|except|finally|with|match|case|return|break|continue|yield)\b|\bskipif\b|\bif\b/;
|
|
8976
|
+
var PYTEST_HOOK_FOR_EVERY_ITEM_PATTERN = /^for[ \t]+([A-Za-z_]\w*)[ \t]+in[ \t]+items[ \t]*:(.*)$/;
|
|
8977
|
+
var PYTEST_HOOK_REBIND_ITEMS_PATTERN = /^(?:del[ \t]+)?items[ \t]*(?:[-+*/%&|^@]?=(?!=)|,|$)|\bitems[ \t]*:=/;
|
|
8978
|
+
var PYTEST_HOOK_CLEAR_ITEMS_PATTERN = /^(?:items\.clear\(\s*\)|items\[\s*:\s*\][ \t]*=[ \t]*\[\s*\]|del[ \t]+items\[\s*:\s*\])/;
|
|
8979
|
+
var PYTEST_HOOK_REFILL_ITEMS_PATTERN = /^items(?:[ \t]*\.[ \t]*(?:extend|append|insert)[ \t]*\(|[ \t]*\+=|[ \t]*\[[^\]]*\][ \t]*=(?!=))/;
|
|
8980
|
+
var PYTEST_HOOK_EMPTY_REFILL_PATTERN = /^items(?:[ \t]*\.[ \t]*extend[ \t]*\([ \t]*|[ \t]*\+=[ \t]*)(?:\[[ \t]*\]|\([ \t]*\))[ \t]*(?:\)[ \t]*)?$/;
|
|
8981
|
+
function pythonLineStatements(line) {
|
|
8982
|
+
if (!line.includes(";") || /['"]/.test(line)) return [line];
|
|
8983
|
+
return line.split(";").map((statement) => statement.trim()).filter((statement) => statement !== "");
|
|
8984
|
+
}
|
|
8985
|
+
var PYTEST_HOOK_SKIP_MARKER_PATTERN = /^([A-Za-z_]\w*)[ \t]*\.[ \t]*add_marker[ \t]*\([ \t]*pytest\.mark\.skip\b(?!if)/;
|
|
8986
|
+
var PYTHON_ASSIGNMENT_TARGET_PATTERN = /^(?:del[ \t]+)?([A-Za-z_]\w*)[ \t]*(?:[-+*/%&|^@]?=(?!=)|,)/;
|
|
8987
|
+
var PYTHON_IDENTIFIER_PATTERN = /[A-Za-z_]\w*/g;
|
|
8988
|
+
function containsPythonIdentifier(text, name) {
|
|
8989
|
+
for (const match of text.matchAll(PYTHON_IDENTIFIER_PATTERN)) if (match[0] === name) return true;
|
|
8990
|
+
return false;
|
|
8991
|
+
}
|
|
8992
|
+
function mayRebindPythonName(text, name) {
|
|
8993
|
+
if (PYTHON_ASSIGNMENT_TARGET_PATTERN.exec(text)?.[1] === name) return true;
|
|
8994
|
+
const code = pythonCodeOutsideStrings(text);
|
|
8995
|
+
return /\bfor\b|\bas\b|:=/.test(code) && containsPythonIdentifier(code, name);
|
|
8996
|
+
}
|
|
8997
|
+
function hookSkipsEveryItem(body) {
|
|
8998
|
+
if (body.some(({ text }) => PYTEST_HOOK_REBIND_ITEMS_PATTERN.test(text))) return false;
|
|
8999
|
+
const hookIndent = body[0]?.indent;
|
|
9000
|
+
const skips = (text, name) => PYTEST_HOOK_SKIP_MARKER_PATTERN.exec(text)?.[1] === name;
|
|
9001
|
+
for (let index = 0; index < body.length; index += 1) {
|
|
9002
|
+
const statement = body[index];
|
|
9003
|
+
if (statement.indent !== hookIndent) continue;
|
|
9004
|
+
const loop = PYTEST_HOOK_FOR_EVERY_ITEM_PATTERN.exec(statement.text);
|
|
9005
|
+
if (!loop) continue;
|
|
9006
|
+
const name = loop[1] ?? "";
|
|
9007
|
+
const inline = (loop[2] ?? "").trim();
|
|
9008
|
+
if (inline !== "") {
|
|
9009
|
+
if (skips(inline, name)) return true;
|
|
9010
|
+
continue;
|
|
9011
|
+
}
|
|
9012
|
+
const childIndent = body[index + 1]?.indent;
|
|
9013
|
+
if (childIndent === void 0 || childIndent <= statement.indent) continue;
|
|
9014
|
+
let skipped = false;
|
|
9015
|
+
let rebound = false;
|
|
9016
|
+
for (let inner = index + 1; inner < body.length; inner += 1) {
|
|
9017
|
+
const { indent, text } = body[inner];
|
|
9018
|
+
if (indent <= statement.indent) break;
|
|
9019
|
+
if (mayRebindPythonName(text, name)) rebound = true;
|
|
9020
|
+
if (indent === childIndent && skips(text, name)) skipped = true;
|
|
9021
|
+
}
|
|
9022
|
+
if (skipped && !rebound) return true;
|
|
9023
|
+
}
|
|
9024
|
+
return false;
|
|
9025
|
+
}
|
|
9026
|
+
function findPytestRunWideSkips(repoPath, repoFiles) {
|
|
9027
|
+
const found = [];
|
|
9028
|
+
for (const file of repoFiles) {
|
|
9029
|
+
if (!PYTEST_CONFTEST_PATTERN.test(file) || GENERATED_OR_VENDOR_PATH_PATTERN.test(file)) continue;
|
|
9030
|
+
const fullPath = join(repoPath, file);
|
|
9031
|
+
if (!isRegularFile(fullPath)) continue;
|
|
9032
|
+
const lines = readRepoText(fullPath, "utf8").split("\n");
|
|
9033
|
+
let last = -1;
|
|
9034
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
9035
|
+
const line = lines[index] ?? "";
|
|
9036
|
+
if (PYTEST_COLLECTION_HOOK_PATTERN.test(line)) {
|
|
9037
|
+
last = index;
|
|
9038
|
+
} else if (line.includes(PYTEST_COLLECTION_HOOK_NAME) && !line.trimStart().startsWith("#") && PYTEST_COLLECTION_HOOK_NAME_PATTERN.test(line)) {
|
|
9039
|
+
last = -1;
|
|
9040
|
+
break;
|
|
9041
|
+
}
|
|
9042
|
+
}
|
|
9043
|
+
for (let index = last; index !== -1; index = -1) {
|
|
9044
|
+
const indented = [];
|
|
9045
|
+
for (let next = index + 1; next < lines.length; next += 1) {
|
|
9046
|
+
const raw = lines[next] ?? "";
|
|
9047
|
+
const trimmed = raw.trim();
|
|
9048
|
+
if (trimmed === "" || trimmed.startsWith("#")) continue;
|
|
9049
|
+
const indent = raw.length - raw.trimStart().length;
|
|
9050
|
+
if (indent === 0) break;
|
|
9051
|
+
for (const text of pythonLineStatements(stripPythonLineComment(trimmed).trim())) {
|
|
9052
|
+
indented.push({ indent, text });
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
const body = indented.map(({ text }) => text);
|
|
9056
|
+
if (body.some((statement) => PYTEST_HOOK_CONDITIONAL_PATTERN.test(statement))) continue;
|
|
9057
|
+
const hookIndent = indented[0]?.indent;
|
|
9058
|
+
const clears = indented.some(
|
|
9059
|
+
({ indent, text }) => indent === hookIndent && PYTEST_HOOK_CLEAR_ITEMS_PATTERN.test(text)
|
|
9060
|
+
);
|
|
9061
|
+
const refills = body.some(
|
|
9062
|
+
(statement) => !PYTEST_HOOK_CLEAR_ITEMS_PATTERN.test(statement) && !PYTEST_HOOK_EMPTY_REFILL_PATTERN.test(statement) && PYTEST_HOOK_REFILL_ITEMS_PATTERN.test(statement)
|
|
9063
|
+
);
|
|
9064
|
+
const kind = clears && !refills ? "clear" : hookSkipsEveryItem(indented) ? "skip" : void 0;
|
|
9065
|
+
if (!kind) continue;
|
|
9066
|
+
const scope = dirname2(file);
|
|
9067
|
+
found.push({ path: file, line: index + 1, scope: scope === "." ? "" : scope, kind });
|
|
9068
|
+
}
|
|
9069
|
+
}
|
|
9070
|
+
return found;
|
|
9071
|
+
}
|
|
6919
9072
|
function countFilesContaining(repoPath, files, pattern) {
|
|
6920
9073
|
return files.filter((file) => fileContains(repoPath, file, pattern)).length;
|
|
6921
9074
|
}
|
|
@@ -7092,10 +9245,10 @@ function readPyprojectDependencySpecs(path) {
|
|
|
7092
9245
|
const dep = rawDep.replace(/["'\s]/g, "");
|
|
7093
9246
|
if (!dep) continue;
|
|
7094
9247
|
const nameMatch = /^([A-Za-z0-9][\w.-]*)/.exec(dep);
|
|
7095
|
-
const
|
|
9248
|
+
const versionConstraint = firstVersionConstraint(dep);
|
|
7096
9249
|
const depName = nameMatch?.[1];
|
|
7097
9250
|
if (depName) {
|
|
7098
|
-
specs.push({ name: depName, version:
|
|
9251
|
+
specs.push({ name: depName, version: versionConstraint ?? dep });
|
|
7099
9252
|
}
|
|
7100
9253
|
}
|
|
7101
9254
|
}
|
|
@@ -7869,7 +10022,7 @@ function isClaimArtifactDeclaringWorkflow(file) {
|
|
|
7869
10022
|
function normaliseDeclaredClaimArtifactPath(candidate, baseDir) {
|
|
7870
10023
|
if (!DECLARED_CLAIM_ARTIFACT_PATH_PATTERN.test(candidate)) return null;
|
|
7871
10024
|
const joined = candidate.startsWith("/") ? posix.normalize(candidate.slice(1)) : posix.normalize(posix.join(baseDir, candidate));
|
|
7872
|
-
const path = joined
|
|
10025
|
+
const path = stripTrailingSlashes(joined);
|
|
7873
10026
|
if (path === "" || path === "." || path === ".." || path.startsWith("../") || path.startsWith("/")) {
|
|
7874
10027
|
return null;
|
|
7875
10028
|
}
|
|
@@ -7879,7 +10032,7 @@ function markdownLinkTarget(target) {
|
|
|
7879
10032
|
if (/^[a-z][a-z0-9+.-]*:/i.test(target) || target.startsWith("//") || target.startsWith("#")) {
|
|
7880
10033
|
return null;
|
|
7881
10034
|
}
|
|
7882
|
-
const withoutSuffix = target
|
|
10035
|
+
const withoutSuffix = stripFromQueryOrFragment(target);
|
|
7883
10036
|
try {
|
|
7884
10037
|
return decodeURIComponent(withoutSuffix);
|
|
7885
10038
|
} catch {
|
|
@@ -7954,7 +10107,7 @@ function workflowClaimArtifactDeclarations(contents) {
|
|
|
7954
10107
|
writeContinues = false;
|
|
7955
10108
|
};
|
|
7956
10109
|
contents.split(/\r?\n/).forEach((rawLine, index) => {
|
|
7957
|
-
const line = rawLine
|
|
10110
|
+
const line = stripHashCommentAtStartOrAfterBlank(rawLine);
|
|
7958
10111
|
if (line.trim() === "") return;
|
|
7959
10112
|
const indent = line.length - line.trimStart().length;
|
|
7960
10113
|
if (stepsIndent !== null && (indent < stepsIndent || indent === stepsIndent && !line.trimStart().startsWith("- "))) {
|
|
@@ -8078,7 +10231,7 @@ function vitestCoveragePropertyCollects(contents) {
|
|
|
8078
10231
|
return true;
|
|
8079
10232
|
}
|
|
8080
10233
|
function viteConfigCollectsCoverage(repoPath, file) {
|
|
8081
|
-
const directory =
|
|
10234
|
+
const directory = dirname2(file);
|
|
8082
10235
|
for (const extension of ["ts", "mts", "cts", "js", "mjs", "cjs"]) {
|
|
8083
10236
|
const sibling = join(repoPath, directory, `vitest.config.${extension}`);
|
|
8084
10237
|
if (!isRegularFile(sibling)) continue;
|
|
@@ -8778,7 +10931,7 @@ function containsPotentialCoverageFlag(command) {
|
|
|
8778
10931
|
function reachableShellWrapper(words, repoFiles) {
|
|
8779
10932
|
const executable = basename(words[0] ?? "").toLowerCase();
|
|
8780
10933
|
const candidate = /^(?:ba|da|k|z)?sh$/u.test(executable) ? words[1] : words[0];
|
|
8781
|
-
if (!candidate ||
|
|
10934
|
+
if (!candidate || !isShellScriptPath(candidate)) return void 0;
|
|
8782
10935
|
const normalized = candidate.replace(/^\.\//u, "");
|
|
8783
10936
|
if (normalized.startsWith("/") || normalized.split("/").includes("..")) return void 0;
|
|
8784
10937
|
return repoFiles.has(normalized) ? normalized : void 0;
|
|
@@ -9346,7 +11499,7 @@ function packageJsonDescribesPackagedApplication(repoPath, manifest, repoFiles)
|
|
|
9346
11499
|
return hasDesktopRuntime && hasPackagingCommand && hasApplicationMetadata;
|
|
9347
11500
|
}
|
|
9348
11501
|
function findTauriApplicationMetadata(repoPath, manifest, repoFiles) {
|
|
9349
|
-
const manifestDirectory =
|
|
11502
|
+
const manifestDirectory = dirname2(manifest);
|
|
9350
11503
|
const prefix = manifestDirectory === "." ? "" : `${manifestDirectory}/`;
|
|
9351
11504
|
const candidates = [`${prefix}src-tauri/tauri.conf.json`, `${prefix}tauri.conf.json`];
|
|
9352
11505
|
return candidates.some((file) => {
|
|
@@ -9396,31 +11549,38 @@ function findRootPackageJson(repoFiles) {
|
|
|
9396
11549
|
var WORKSPACE_PATTERN_LIMIT = 64;
|
|
9397
11550
|
var WORKSPACE_PACKAGE_MANIFEST_LIMIT = 200;
|
|
9398
11551
|
var NO_DECLARED_WORKSPACE_MANIFESTS = { read: [], declared: 0 };
|
|
11552
|
+
var UNREADABLE_WORKSPACE_DECLARATION = {
|
|
11553
|
+
read: [],
|
|
11554
|
+
declared: 0,
|
|
11555
|
+
unreadable: true
|
|
11556
|
+
};
|
|
9399
11557
|
function findDeclaredWorkspaceManifests(repoPath, repoFiles, rootPackageJson) {
|
|
9400
11558
|
const patterns = [];
|
|
9401
11559
|
if (rootPackageJson) {
|
|
9402
11560
|
const declared = readPackageJsonWorkspacePatterns(join(repoPath, rootPackageJson));
|
|
9403
|
-
if (declared === null) return
|
|
11561
|
+
if (declared === null) return UNREADABLE_WORKSPACE_DECLARATION;
|
|
9404
11562
|
patterns.push(...declared);
|
|
9405
11563
|
}
|
|
9406
11564
|
if (repoFiles.includes("pnpm-workspace.yaml")) {
|
|
9407
11565
|
const declared = readPnpmWorkspacePatterns(join(repoPath, "pnpm-workspace.yaml"));
|
|
9408
|
-
if (declared === null) return
|
|
11566
|
+
if (declared === null) return UNREADABLE_WORKSPACE_DECLARATION;
|
|
9409
11567
|
patterns.push(...declared);
|
|
9410
11568
|
}
|
|
9411
|
-
if (patterns.length === 0
|
|
9412
|
-
|
|
9413
|
-
}
|
|
11569
|
+
if (patterns.length === 0) return NO_DECLARED_WORKSPACE_MANIFESTS;
|
|
11570
|
+
if (patterns.length > WORKSPACE_PATTERN_LIMIT) return UNREADABLE_WORKSPACE_DECLARATION;
|
|
9414
11571
|
const include = [];
|
|
9415
11572
|
const exclude = [];
|
|
11573
|
+
let unparsedPattern = false;
|
|
9416
11574
|
for (const pattern of patterns) {
|
|
9417
11575
|
const negated = pattern.startsWith("!");
|
|
9418
11576
|
const matcher = workspaceGlobMatcher(negated ? pattern.slice(1) : pattern);
|
|
9419
11577
|
if (negated) {
|
|
9420
|
-
if (matcher === null) return
|
|
11578
|
+
if (matcher === null) return UNREADABLE_WORKSPACE_DECLARATION;
|
|
9421
11579
|
exclude.push(matcher);
|
|
9422
11580
|
} else if (matcher !== null) {
|
|
9423
11581
|
include.push(matcher);
|
|
11582
|
+
} else {
|
|
11583
|
+
unparsedPattern = true;
|
|
9424
11584
|
}
|
|
9425
11585
|
}
|
|
9426
11586
|
const matched = repoFiles.filter((file) => {
|
|
@@ -9429,7 +11589,11 @@ function findDeclaredWorkspaceManifests(repoPath, repoFiles, rootPackageJson) {
|
|
|
9429
11589
|
const dir = file.slice(0, -"/package.json".length);
|
|
9430
11590
|
return include.some((matches) => matches(dir)) && !exclude.some((matches) => matches(dir));
|
|
9431
11591
|
}).sort();
|
|
9432
|
-
return {
|
|
11592
|
+
return {
|
|
11593
|
+
read: matched.slice(0, WORKSPACE_PACKAGE_MANIFEST_LIMIT),
|
|
11594
|
+
declared: matched.length,
|
|
11595
|
+
...unparsedPattern ? { unreadable: true } : {}
|
|
11596
|
+
};
|
|
9433
11597
|
}
|
|
9434
11598
|
function readPackageJsonWorkspacePatterns(packageJsonPath) {
|
|
9435
11599
|
if (!isRegularFile(packageJsonPath)) return [];
|
|
@@ -9478,7 +11642,7 @@ function readPnpmWorkspacePatterns(yamlPath) {
|
|
|
9478
11642
|
return patterns;
|
|
9479
11643
|
}
|
|
9480
11644
|
function stripYamlLineComment(text) {
|
|
9481
|
-
return /^\s*#/.test(text) ? "" : text
|
|
11645
|
+
return /^\s*#/.test(text) ? "" : stripTrailingHashComment(text);
|
|
9482
11646
|
}
|
|
9483
11647
|
function yamlScalar(raw) {
|
|
9484
11648
|
const single = /^'((?:[^']|'')*)'\s*(?:#.*)?$/.exec(raw);
|
|
@@ -9492,7 +11656,7 @@ var WORKSPACE_GLOB_MAX_LENGTH = 256;
|
|
|
9492
11656
|
var WORKSPACE_GLOB_MAX_SEGMENTS = 32;
|
|
9493
11657
|
function workspaceGlobMatcher(pattern) {
|
|
9494
11658
|
if (pattern.length > WORKSPACE_GLOB_MAX_LENGTH) return null;
|
|
9495
|
-
const normalised = pattern.trim().replace(/^\.\//, "")
|
|
11659
|
+
const normalised = stripTrailingSlashes(pattern.trim().replace(/^\.\//, ""));
|
|
9496
11660
|
if (normalised === "" || normalised.startsWith("/") || /(^|\/)\.\.?(\/|$)/.test(normalised) || /[[\]{}()!+@\\]/.test(normalised)) {
|
|
9497
11661
|
return null;
|
|
9498
11662
|
}
|
|
@@ -9601,7 +11765,7 @@ function countDelegatedCiSteps(root, workflows) {
|
|
|
9601
11765
|
continue;
|
|
9602
11766
|
}
|
|
9603
11767
|
if (!uses.startsWith("./") || /^\.\/\.github\/workflows\//.test(uses)) continue;
|
|
9604
|
-
const directory = uses.slice(2)
|
|
11768
|
+
const directory = stripTrailingSlashes(uses.slice(2));
|
|
9605
11769
|
if (directory.length === 0 || directory.split("/").includes("..")) continue;
|
|
9606
11770
|
if (isRegularFile(join(root, directory, "action.yml")) || isRegularFile(join(root, directory, "action.yaml"))) {
|
|
9607
11771
|
compositeActions += 1;
|
|
@@ -9621,7 +11785,7 @@ function extractWorkflowUsesValues(contents) {
|
|
|
9621
11785
|
}
|
|
9622
11786
|
const match = /^\s*(?:-\s*)?uses\s*:\s*(.*)$/u.exec(line);
|
|
9623
11787
|
if (match) {
|
|
9624
|
-
const value = (match[1] ?? "").
|
|
11788
|
+
const value = stripTrailingHashComment(match[1] ?? "").trim();
|
|
9625
11789
|
const quoted = /^(?:"(.*)"|'(.*)')$/u.exec(value);
|
|
9626
11790
|
values.push(quoted ? quoted[1] ?? quoted[2] ?? "" : value);
|
|
9627
11791
|
continue;
|
|
@@ -9639,6 +11803,263 @@ function unreadCiWorkflowReasons(includes) {
|
|
|
9639
11803
|
}
|
|
9640
11804
|
return unread;
|
|
9641
11805
|
}
|
|
11806
|
+
var BUILD_STEP_EXECUTABLES = /* @__PURE__ */ new Set([
|
|
11807
|
+
"babel",
|
|
11808
|
+
"esbuild",
|
|
11809
|
+
"ncc",
|
|
11810
|
+
"rollup",
|
|
11811
|
+
"svelte-check",
|
|
11812
|
+
"swc",
|
|
11813
|
+
"tsc",
|
|
11814
|
+
"tsup",
|
|
11815
|
+
"vue-tsc",
|
|
11816
|
+
"webpack",
|
|
11817
|
+
"webpack-cli"
|
|
11818
|
+
]);
|
|
11819
|
+
var BUILD_STEP_SUBCOMMANDS = /* @__PURE__ */ new Map([
|
|
11820
|
+
["bun", "build"],
|
|
11821
|
+
["nest", "build"],
|
|
11822
|
+
["next", "build"],
|
|
11823
|
+
["ng", "build"],
|
|
11824
|
+
["parcel", "build"],
|
|
11825
|
+
["vite", "build"]
|
|
11826
|
+
]);
|
|
11827
|
+
var BUILD_STEP_TASK_RUNNERS = /* @__PURE__ */ new Set([
|
|
11828
|
+
"bun",
|
|
11829
|
+
"gmake",
|
|
11830
|
+
"just",
|
|
11831
|
+
"lerna",
|
|
11832
|
+
"make",
|
|
11833
|
+
"npm",
|
|
11834
|
+
"nx",
|
|
11835
|
+
"pnpm",
|
|
11836
|
+
"turbo",
|
|
11837
|
+
"yarn"
|
|
11838
|
+
]);
|
|
11839
|
+
var BUILD_STEP_TARGET_NAME_PATTERN = /^(?:build|typecheck)$/u;
|
|
11840
|
+
var BUILD_STEP_TARGET_ARGUMENT_PATTERN = /^(?:[^\s:]+:)?(?:build|typecheck)$/u;
|
|
11841
|
+
var TYPESCRIPT_SOURCE_PATTERN = /\.(?:ts|tsx|mts|cts)$/u;
|
|
11842
|
+
var TYPESCRIPT_DECLARATION_PATTERN = /\.d\.(?:ts|mts|cts)$/u;
|
|
11843
|
+
var TSCONFIG_PATTERN = /(^|\/)tsconfig[^/]*\.json$/u;
|
|
11844
|
+
var BUILD_STEP_DOCKERFILE_PATTERN = /(^|\/)(?:dockerfile|containerfile)(?:\.[^/]*)?$|[^/]\.dockerfile$/iu;
|
|
11845
|
+
function settleV26BuildStepEvidence(repoPath, repoFiles, packageJson, workspaceManifests, workflows, includesWorkflow) {
|
|
11846
|
+
const nestedPackageDirs = repoFiles.filter((file) => file !== packageJson && /(^|\/)package\.json$/u.test(file)).map((file) => file.slice(0, -"package.json".length));
|
|
11847
|
+
const hasSomethingToBuild = repoFiles.some(
|
|
11848
|
+
(file) => isAuthoredProductionPath(file) && (TYPESCRIPT_SOURCE_PATTERN.test(file) && !TYPESCRIPT_DECLARATION_PATTERN.test(file) || TSCONFIG_PATTERN.test(file)) && !nestedPackageDirs.some((dir) => file.startsWith(dir))
|
|
11849
|
+
);
|
|
11850
|
+
if (!hasSomethingToBuild) return "nothing-to-build";
|
|
11851
|
+
let unsettled = false;
|
|
11852
|
+
const commandRunsStep = (command) => {
|
|
11853
|
+
const state = classifyBuildStepCommand(command);
|
|
11854
|
+
if (state === "unparseable") unsettled = true;
|
|
11855
|
+
return state === "step";
|
|
11856
|
+
};
|
|
11857
|
+
for (const manifest of [packageJson, ...workspaceManifests.read]) {
|
|
11858
|
+
const scripts = readBuildStepPackageScripts(join(repoPath, manifest));
|
|
11859
|
+
if (scripts === null) {
|
|
11860
|
+
unsettled = true;
|
|
11861
|
+
continue;
|
|
11862
|
+
}
|
|
11863
|
+
for (const [name, command] of scripts) {
|
|
11864
|
+
if (BUILD_STEP_TARGET_NAME_PATTERN.test(name) || commandRunsStep(command)) return "step-found";
|
|
11865
|
+
}
|
|
11866
|
+
}
|
|
11867
|
+
for (const workflow of workflows) {
|
|
11868
|
+
const fullPath = join(repoPath, workflow);
|
|
11869
|
+
if (!/^\.github\/workflows\//u.test(workflow) || !isRegularFile(fullPath)) {
|
|
11870
|
+
unsettled = true;
|
|
11871
|
+
continue;
|
|
11872
|
+
}
|
|
11873
|
+
if (extractCiRunCommands(readRepoText(fullPath, "utf8")).some(commandRunsStep)) {
|
|
11874
|
+
return "step-found";
|
|
11875
|
+
}
|
|
11876
|
+
}
|
|
11877
|
+
for (const file of repoFiles.filter((path) => BUILD_STEP_DOCKERFILE_PATTERN.test(path))) {
|
|
11878
|
+
const fullPath = join(repoPath, file);
|
|
11879
|
+
if (!isRegularFile(fullPath)) {
|
|
11880
|
+
unsettled = true;
|
|
11881
|
+
continue;
|
|
11882
|
+
}
|
|
11883
|
+
const run = dockerfileRunCommands(readRepoText(fullPath, "utf8"));
|
|
11884
|
+
if (run.unparseable) unsettled = true;
|
|
11885
|
+
if (run.commands.some(commandRunsStep)) return "step-found";
|
|
11886
|
+
}
|
|
11887
|
+
for (const file of repoFiles.filter(isRecipeFile)) {
|
|
11888
|
+
const fullPath = join(repoPath, file);
|
|
11889
|
+
if (!isRegularFile(fullPath)) {
|
|
11890
|
+
unsettled = true;
|
|
11891
|
+
continue;
|
|
11892
|
+
}
|
|
11893
|
+
for (const [name, target] of readRecipeTargets(readRepoText(fullPath, "utf8"))) {
|
|
11894
|
+
if (BUILD_STEP_TARGET_NAME_PATTERN.test(name)) return "step-found";
|
|
11895
|
+
if (target.commands.some((command) => commandRunsStep(command.replace(/\\$/u, "")))) {
|
|
11896
|
+
return "step-found";
|
|
11897
|
+
}
|
|
11898
|
+
}
|
|
11899
|
+
}
|
|
11900
|
+
if (workspaceManifests.declared > workspaceManifests.read.length || workspaceManifests.unreadable) {
|
|
11901
|
+
unsettled = true;
|
|
11902
|
+
}
|
|
11903
|
+
if (unreadCiWorkflowReasons(includesWorkflow).size > 0) unsettled = true;
|
|
11904
|
+
const delegated = countDelegatedCiSteps(repoPath, workflows);
|
|
11905
|
+
if (delegated.crossRepositoryReusableWorkflows + delegated.compositeActions > 0) unsettled = true;
|
|
11906
|
+
if (withheldRepoPaths().some(
|
|
11907
|
+
({ path }) => BUILD_STEP_DOCKERFILE_PATTERN.test(path) || isRecipeFile(path) || /(^|\/)package\.json$/u.test(path) && isAuthoredProductionPath(path)
|
|
11908
|
+
)) {
|
|
11909
|
+
unsettled = true;
|
|
11910
|
+
}
|
|
11911
|
+
return unsettled ? "unsettled" : "fires";
|
|
11912
|
+
}
|
|
11913
|
+
function readBuildStepPackageScripts(packageJsonPath) {
|
|
11914
|
+
if (!isRegularFile(packageJsonPath)) return null;
|
|
11915
|
+
const parsed = parseJsonObject(readRepoText(packageJsonPath, "utf8"));
|
|
11916
|
+
if (parsed === null) return null;
|
|
11917
|
+
if (parsed.scripts === void 0) return /* @__PURE__ */ new Map();
|
|
11918
|
+
if (!isRecord(parsed.scripts)) return null;
|
|
11919
|
+
const entries = Object.entries(parsed.scripts);
|
|
11920
|
+
if (!entries.every((entry) => typeof entry[1] === "string")) return null;
|
|
11921
|
+
return new Map(entries);
|
|
11922
|
+
}
|
|
11923
|
+
function dockerfileRunCommands(contents) {
|
|
11924
|
+
if (/^#\s*escape\s*=/imu.test(contents)) return { commands: [], unparseable: true };
|
|
11925
|
+
const lines = contents.split(/\r?\n/u);
|
|
11926
|
+
const commands = [];
|
|
11927
|
+
let unparseable = false;
|
|
11928
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
11929
|
+
const match = /^\s*RUN(?:\s+(.*))?$/iu.exec(lines[index] ?? "");
|
|
11930
|
+
if (!match) continue;
|
|
11931
|
+
const chunks = [match[1] ?? ""];
|
|
11932
|
+
while (endsWithLineContinuation(chunks) && index + 1 < lines.length) {
|
|
11933
|
+
index += 1;
|
|
11934
|
+
const next = lines[index] ?? "";
|
|
11935
|
+
if (/^\s*#/u.test(next)) continue;
|
|
11936
|
+
dropLineContinuation(chunks);
|
|
11937
|
+
chunks.push(" ", next.trim());
|
|
11938
|
+
}
|
|
11939
|
+
let instruction = chunks.join("");
|
|
11940
|
+
instruction = instruction.replace(/^(?:--[A-Za-z][\w-]*(?:=\S*)?\s+)*/u, "").trim();
|
|
11941
|
+
if (instruction === "" || /<<-?\s*["']?[A-Za-z_]/u.test(instruction)) {
|
|
11942
|
+
unparseable = true;
|
|
11943
|
+
continue;
|
|
11944
|
+
}
|
|
11945
|
+
if (!instruction.startsWith("[")) {
|
|
11946
|
+
commands.push(instruction);
|
|
11947
|
+
continue;
|
|
11948
|
+
}
|
|
11949
|
+
const execForm = dockerfileExecFormCommand(instruction);
|
|
11950
|
+
if (execForm === null) unparseable = true;
|
|
11951
|
+
else commands.push(execForm);
|
|
11952
|
+
}
|
|
11953
|
+
return { commands, unparseable };
|
|
11954
|
+
}
|
|
11955
|
+
function endsWithLineContinuation(chunks) {
|
|
11956
|
+
for (let c = chunks.length - 1; c >= 0; c -= 1) {
|
|
11957
|
+
const chunk = chunks[c] ?? "";
|
|
11958
|
+
for (let i = chunk.length - 1; i >= 0; i -= 1) {
|
|
11959
|
+
const character = chunk.charAt(i);
|
|
11960
|
+
if (/\s/u.test(character)) continue;
|
|
11961
|
+
return character === "\\";
|
|
11962
|
+
}
|
|
11963
|
+
}
|
|
11964
|
+
return false;
|
|
11965
|
+
}
|
|
11966
|
+
function dropLineContinuation(chunks) {
|
|
11967
|
+
while (chunks.length > 0) {
|
|
11968
|
+
const chunk = chunks[chunks.length - 1] ?? "";
|
|
11969
|
+
let i = chunk.length - 1;
|
|
11970
|
+
while (i >= 0 && /\s/u.test(chunk.charAt(i))) i -= 1;
|
|
11971
|
+
if (i < 0) {
|
|
11972
|
+
chunks.pop();
|
|
11973
|
+
continue;
|
|
11974
|
+
}
|
|
11975
|
+
chunks[chunks.length - 1] = chunk.slice(0, i);
|
|
11976
|
+
return;
|
|
11977
|
+
}
|
|
11978
|
+
}
|
|
11979
|
+
function dockerfileExecFormCommand(instruction) {
|
|
11980
|
+
let parsed;
|
|
11981
|
+
try {
|
|
11982
|
+
parsed = JSON.parse(instruction);
|
|
11983
|
+
} catch {
|
|
11984
|
+
return null;
|
|
11985
|
+
}
|
|
11986
|
+
if (!Array.isArray(parsed) || parsed.length === 0 || !parsed.every((part) => typeof part === "string")) {
|
|
11987
|
+
return null;
|
|
11988
|
+
}
|
|
11989
|
+
return parsed.map(
|
|
11990
|
+
(part) => /^[A-Za-z0-9_@%+=:,./-]+$/u.test(part) ? part : `'${part.replaceAll("'", "'\\''")}'`
|
|
11991
|
+
).join(" ");
|
|
11992
|
+
}
|
|
11993
|
+
function classifyBuildStepCommand(command) {
|
|
11994
|
+
return buildStepCommandState(command, 0);
|
|
11995
|
+
}
|
|
11996
|
+
function buildStepCommandState(command, depth) {
|
|
11997
|
+
if (depth > 4) return "unparseable";
|
|
11998
|
+
const states = splitShellCommandSegments(command).map(
|
|
11999
|
+
(segment) => buildStepSegmentState(segment, depth)
|
|
12000
|
+
);
|
|
12001
|
+
if (states.includes("step")) return "step";
|
|
12002
|
+
return states.includes("unparseable") ? "unparseable" : "none";
|
|
12003
|
+
}
|
|
12004
|
+
function buildStepSegmentState(raw, depth) {
|
|
12005
|
+
let segment = stripUnbalancedShellParentheses(raw.trim());
|
|
12006
|
+
for (; ; ) {
|
|
12007
|
+
const keyword = /^(?:if|then|else|elif|do|while|until|exec|!|\{)(?:\s+|$)/u.exec(segment);
|
|
12008
|
+
if (!keyword) break;
|
|
12009
|
+
segment = segment.slice(keyword[0].length);
|
|
12010
|
+
}
|
|
12011
|
+
if (segment === "" || /^(?:fi|done|esac|\})$/u.test(segment)) return "none";
|
|
12012
|
+
const normalized = normalizeCoverageCommandPrefixes(segment);
|
|
12013
|
+
if (normalized.splitShellOperators && splitShellCommandSegments(normalized.segment).length > 1) {
|
|
12014
|
+
return buildStepCommandState(normalized.segment, depth + 1);
|
|
12015
|
+
}
|
|
12016
|
+
if (/\b(?:npm|pnpm|yarn|bun)\s+run\s+(?:\$|["']\$|`)/u.test(normalized.segment)) {
|
|
12017
|
+
return "unparseable";
|
|
12018
|
+
}
|
|
12019
|
+
const words = strictShellWords(normalized.segment);
|
|
12020
|
+
if (words === null) return "unparseable";
|
|
12021
|
+
return words.length === 0 ? "none" : buildStepWordsState(words);
|
|
12022
|
+
}
|
|
12023
|
+
function buildStepWordsState(words) {
|
|
12024
|
+
const first = words[0] === "$(MAKE)" || words[0] === "${MAKE}" ? "make" : words[0] ?? "";
|
|
12025
|
+
if (isUnresolvedCommandWord(first)) return "unparseable";
|
|
12026
|
+
const runner = buildStepCommandName(first);
|
|
12027
|
+
if (BUILD_STEP_TASK_RUNNERS.has(runner) && words.slice(1).some((word) => BUILD_STEP_TARGET_ARGUMENT_PATTERN.test(word))) {
|
|
12028
|
+
return "step";
|
|
12029
|
+
}
|
|
12030
|
+
const command = runner === "bunx" ? words.slice(1) : runner === "bun" && words[1] === "x" ? words.slice(2) : unwrapPackageManagerExecutable(words);
|
|
12031
|
+
const executable = command[0] ?? "";
|
|
12032
|
+
if (isUnresolvedCommandWord(executable)) return "unparseable";
|
|
12033
|
+
const name = buildStepCommandName(executable);
|
|
12034
|
+
if (BUILD_STEP_EXECUTABLES.has(name)) return "step";
|
|
12035
|
+
const subcommand = BUILD_STEP_SUBCOMMANDS.get(name);
|
|
12036
|
+
return subcommand !== void 0 && command.slice(1).includes(subcommand) ? "step" : "none";
|
|
12037
|
+
}
|
|
12038
|
+
function isUnresolvedCommandWord(word) {
|
|
12039
|
+
return word.startsWith("$") || word.includes("`");
|
|
12040
|
+
}
|
|
12041
|
+
function buildStepCommandName(word) {
|
|
12042
|
+
return basename(word).replace(/\.(?:cmd|exe)$/iu, "").toLowerCase();
|
|
12043
|
+
}
|
|
12044
|
+
function stripUnbalancedShellParentheses(segment) {
|
|
12045
|
+
const opens = segment.split("(").length - 1;
|
|
12046
|
+
const closes = segment.split(")").length - 1;
|
|
12047
|
+
if (opens > closes) return segment.replace(/^\(+\s*/u, "");
|
|
12048
|
+
if (closes > opens) return stripTrailingCloseParentheses(segment);
|
|
12049
|
+
return segment;
|
|
12050
|
+
}
|
|
12051
|
+
function strictShellWords(segment) {
|
|
12052
|
+
const words = [];
|
|
12053
|
+
let offset = 0;
|
|
12054
|
+
for (; ; ) {
|
|
12055
|
+
while (offset < segment.length && /\s/u.test(segment[offset] ?? "")) offset += 1;
|
|
12056
|
+
if (offset >= segment.length) return words;
|
|
12057
|
+
const scanned = scanShellWord(segment, offset);
|
|
12058
|
+
if (!scanned) return null;
|
|
12059
|
+
if (scanned.word !== "") words.push(scanned.word);
|
|
12060
|
+
offset = scanned.end;
|
|
12061
|
+
}
|
|
12062
|
+
}
|
|
9642
12063
|
function rootOnlyPackageScriptsDisclosure(repoFiles, rootPackageJson) {
|
|
9643
12064
|
const others = repoFiles.filter(
|
|
9644
12065
|
(file) => file !== rootPackageJson && /(^|\/)package\.json$/.test(file) && isAuthoredProductionPath(file)
|
|
@@ -9662,7 +12083,7 @@ function workspacePackageScriptsDisclosure(repoFiles, rootPackageJson, workspace
|
|
|
9662
12083
|
}
|
|
9663
12084
|
function isRegularFile(path) {
|
|
9664
12085
|
try {
|
|
9665
|
-
return
|
|
12086
|
+
return lstatSync2(path).isFile();
|
|
9666
12087
|
} catch {
|
|
9667
12088
|
return false;
|
|
9668
12089
|
}
|
|
@@ -9672,6 +12093,11 @@ function fileContains(repoPath, file, pattern) {
|
|
|
9672
12093
|
if (!isRegularFile(fullPath)) return false;
|
|
9673
12094
|
return pattern.test(readRepoText(fullPath, "utf8"));
|
|
9674
12095
|
}
|
|
12096
|
+
function fileSatisfies(repoPath, file, test) {
|
|
12097
|
+
const fullPath = join(repoPath, file);
|
|
12098
|
+
if (!isRegularFile(fullPath)) return false;
|
|
12099
|
+
return test(readRepoText(fullPath, "utf8"));
|
|
12100
|
+
}
|
|
9675
12101
|
var HASH_COMMENT_SOURCE_PATTERN = /\.(?:py|rb)$/i;
|
|
9676
12102
|
function stripCodeComments(file, contents) {
|
|
9677
12103
|
const withoutComments = contents.replace(/\/\*[\s\S]*?\*\//g, (block) => block.replace(/[^\n]/g, "")).replace(/(?<!:)\/\/.*$/gm, "");
|
|
@@ -9730,7 +12156,7 @@ ${contextLines}`
|
|
|
9730
12156
|
line,
|
|
9731
12157
|
valueLength: value.length,
|
|
9732
12158
|
characterClasses: characterClasses || "other",
|
|
9733
|
-
valueFingerprint:
|
|
12159
|
+
valueFingerprint: createHash4("sha256").update(value).digest("hex"),
|
|
9734
12160
|
kind: "pem_private_key"
|
|
9735
12161
|
};
|
|
9736
12162
|
}
|
|
@@ -9751,7 +12177,7 @@ function findPemPrivateKeyAssignment(contents, file) {
|
|
|
9751
12177
|
if (!isBarePemKeyFile(file)) return null;
|
|
9752
12178
|
return scanForPemPrivateKeyMatch(BARE_PEM_PRIVATE_KEY_PATTERN, contents, lines, false);
|
|
9753
12179
|
}
|
|
9754
|
-
function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false) {
|
|
12180
|
+
function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV39Detectors = false, useV47Detectors = false, useV23PemPrivateKeyGrammar = false, setAside) {
|
|
9755
12181
|
const fullPath = join(repoPath, file);
|
|
9756
12182
|
if (!isRegularFile(fullPath)) return null;
|
|
9757
12183
|
const contents = readRepoText(fullPath, "utf8");
|
|
@@ -9764,7 +12190,12 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
|
|
|
9764
12190
|
useV47Detectors
|
|
9765
12191
|
);
|
|
9766
12192
|
const secret = findRealSecretAssignment(secretScanContents, /* @__PURE__ */ new Set(), {
|
|
9767
|
-
allowCredentialNamedDigest: useV36Detectors
|
|
12193
|
+
allowCredentialNamedDigest: useV36Detectors,
|
|
12194
|
+
...setAside ? {
|
|
12195
|
+
observeValue: (value, outcome) => {
|
|
12196
|
+
if (isPlaceholderSetAsideSecret(value, outcome)) setAside.count += 1;
|
|
12197
|
+
}
|
|
12198
|
+
} : {}
|
|
9768
12199
|
});
|
|
9769
12200
|
if (secret) return secret;
|
|
9770
12201
|
if (useV23PemPrivateKeyGrammar) {
|
|
@@ -9779,7 +12210,7 @@ function findCommittedSecretInFile(repoPath, file, useV36Detectors = false, useV
|
|
|
9779
12210
|
}
|
|
9780
12211
|
return useV36Detectors && !isTestCredentialConfiguration ? findDefaultAdministrativeCredential(secretScanContents) : null;
|
|
9781
12212
|
}
|
|
9782
|
-
function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39Detectors, useV47Detectors) {
|
|
12213
|
+
function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39Detectors, useV47Detectors, setAside) {
|
|
9783
12214
|
const fullPath = join(repoPath, file);
|
|
9784
12215
|
if (!isRegularFile(fullPath)) return null;
|
|
9785
12216
|
const contents = readRepoText(fullPath, "utf8");
|
|
@@ -9793,7 +12224,7 @@ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39De
|
|
|
9793
12224
|
true
|
|
9794
12225
|
);
|
|
9795
12226
|
const rawLines = contents.split(/\r?\n/);
|
|
9796
|
-
const classifyFor = (wanted) => (value, identifier, line) => {
|
|
12227
|
+
const classifyFor = (wanted, countSetAside = false) => (value, identifier, line) => {
|
|
9797
12228
|
const outcome = classifyV24SecretShapedValue(
|
|
9798
12229
|
value,
|
|
9799
12230
|
identifier,
|
|
@@ -9801,11 +12232,14 @@ function findV24ClassifiedSecretInFile(repoPath, file, useV36Detectors, useV39De
|
|
|
9801
12232
|
rawLines,
|
|
9802
12233
|
useV36Detectors
|
|
9803
12234
|
);
|
|
12235
|
+
if (countSetAside && setAside && isPlaceholderSetAsideSecret(value, outcome)) {
|
|
12236
|
+
setAside.count += 1;
|
|
12237
|
+
}
|
|
9804
12238
|
return outcome.classification === wanted ? outcome : V24_NO_FINDING;
|
|
9805
12239
|
};
|
|
9806
12240
|
const real = findRealSecretAssignment(secretScanContents, /* @__PURE__ */ new Set(), {
|
|
9807
12241
|
allowCredentialNamedDigest: useV36Detectors,
|
|
9808
|
-
classifyValue: classifyFor("real")
|
|
12242
|
+
classifyValue: classifyFor("real", true)
|
|
9809
12243
|
});
|
|
9810
12244
|
if (real) return { match: real, classification: "real" };
|
|
9811
12245
|
if (useV39Detectors && isEnvHistoryPath(file, useV36Detectors)) {
|
|
@@ -9997,8 +12431,9 @@ function findRealSecretAssignments(contents, excludedValueFingerprints = /* @__P
|
|
|
9997
12431
|
const value = contents.slice(valueStart, cursor).replace(/[,;)]$/, "");
|
|
9998
12432
|
nextSearchIndex = cursor;
|
|
9999
12433
|
const outcome = options.classifyValue ? options.classifyValue(value, identifier, line) : !isPlaceholderSecretValue(value) && (looksLikeSecretValue(value) || options.allowCredentialNamedDigest === true && isLikelyDigestOrHash(value) && isExplicitCredentialIdentifier(identifier)) ? V24_REAL : V24_NO_FINDING;
|
|
12434
|
+
options.observeValue?.(value, outcome);
|
|
10000
12435
|
if (outcome.classification !== "no_finding") {
|
|
10001
|
-
const valueFingerprint =
|
|
12436
|
+
const valueFingerprint = createHash4("sha256").update(value).digest("hex");
|
|
10002
12437
|
const characterClasses = [
|
|
10003
12438
|
/[a-z]/.test(value) ? "lower" : "",
|
|
10004
12439
|
/[A-Z]/.test(value) ? "upper" : "",
|
|
@@ -10055,7 +12490,7 @@ function findWeakExplicitEnvCredential(contents) {
|
|
|
10055
12490
|
line: index + 1,
|
|
10056
12491
|
valueLength: value.length,
|
|
10057
12492
|
characterClasses: characterClasses || "other",
|
|
10058
|
-
valueFingerprint:
|
|
12493
|
+
valueFingerprint: createHash4("sha256").update(value).digest("hex")
|
|
10059
12494
|
};
|
|
10060
12495
|
}
|
|
10061
12496
|
return null;
|
|
@@ -10095,7 +12530,7 @@ function findDatabaseUrlUserinfoCredential(contents) {
|
|
|
10095
12530
|
line: index + 1,
|
|
10096
12531
|
valueLength: password.length,
|
|
10097
12532
|
characterClasses: characterClasses || "other",
|
|
10098
|
-
valueFingerprint:
|
|
12533
|
+
valueFingerprint: createHash4("sha256").update(password).digest("hex")
|
|
10099
12534
|
};
|
|
10100
12535
|
}
|
|
10101
12536
|
return null;
|
|
@@ -10114,7 +12549,7 @@ function findDefaultAdministrativeCredential(contents) {
|
|
|
10114
12549
|
line: index + 1,
|
|
10115
12550
|
valueLength: value.length,
|
|
10116
12551
|
characterClasses: "lower",
|
|
10117
|
-
valueFingerprint:
|
|
12552
|
+
valueFingerprint: createHash4("sha256").update(value).digest("hex"),
|
|
10118
12553
|
kind: "default_admin"
|
|
10119
12554
|
};
|
|
10120
12555
|
}
|
|
@@ -10390,6 +12825,45 @@ function isPlaceholderSecretValue(value) {
|
|
|
10390
12825
|
const normalized = value.trim().toLowerCase();
|
|
10391
12826
|
return PLACEHOLDER_SECRET_PATTERN.test(normalized) || normalized.includes("your-") || normalized.includes("example") || ALL_CAPS_SNAKE_PLACEHOLDER_PATTERN.test(value.trim()) || isSyntheticFixtureSecretValue(normalized) || isFillerRunSecretValue(normalized);
|
|
10392
12827
|
}
|
|
12828
|
+
function isPlaceholderSetAsideSecret(value, outcome) {
|
|
12829
|
+
return outcome.classification === "no_finding" && isPlaceholderSecretValue(value) && looksLikeSecretValue(value);
|
|
12830
|
+
}
|
|
12831
|
+
var PLACEHOLDER_SET_ASIDE_PATHS_SHOWN = 3;
|
|
12832
|
+
function placeholderSetAsidePathList(files, shown) {
|
|
12833
|
+
const named = files.slice(0, shown).map((file) => file.path);
|
|
12834
|
+
const more = files.length - named.length;
|
|
12835
|
+
return more > 0 ? `${named.join(", ")} and ${more} more file${more === 1 ? "" : "s"}` : named.join(", ");
|
|
12836
|
+
}
|
|
12837
|
+
function placeholderSetAsideValues(files) {
|
|
12838
|
+
const total = files.reduce((sum, file) => sum + file.count, 0);
|
|
12839
|
+
return `${total} credential-shaped value${total === 1 ? "" : "s"}`;
|
|
12840
|
+
}
|
|
12841
|
+
function placeholderSetAsideDisclosure(files, budget = Number.POSITIVE_INFINITY) {
|
|
12842
|
+
const total = files.reduce((sum, file) => sum + file.count, 0);
|
|
12843
|
+
const lead = `${WITAN_A2_PLACEHOLDER_SET_ASIDE_PREFIX}${placeholderSetAsideValues(files)} as ${total === 1 ? "a documentation placeholder because it contains" : "documentation placeholders because they contain"} a placeholder marker such as "example", so ${total === 1 ? "it was" : "they were"} not counted against secret cleanliness or checked further`;
|
|
12844
|
+
for (let shown = Math.min(files.length, PLACEHOLDER_SET_ASIDE_PATHS_SHOWN); shown > 0; shown -= 1) {
|
|
12845
|
+
const sentence = `${lead}: ${placeholderSetAsidePathList(files, shown)}.`;
|
|
12846
|
+
if (sentence.length <= budget) return sentence;
|
|
12847
|
+
}
|
|
12848
|
+
const countOnly = `${lead}, in ${files.length} file${files.length === 1 ? "" : "s"}.`;
|
|
12849
|
+
return countOnly.length <= budget ? countOnly : "";
|
|
12850
|
+
}
|
|
12851
|
+
function placeholderSetAsideCondition(files) {
|
|
12852
|
+
const lead = `${placeholderSetAsideValues(files)} set aside as documentation placeholders, not counted`;
|
|
12853
|
+
for (let shown = Math.min(files.length, PLACEHOLDER_SET_ASIDE_PATHS_SHOWN); shown > 0; shown -= 1) {
|
|
12854
|
+
const condition = `${lead}: ${placeholderSetAsidePathList(files, shown)}`;
|
|
12855
|
+
if (condition.length <= METRIC_CONDITION_MAX_LENGTH) return condition;
|
|
12856
|
+
}
|
|
12857
|
+
return `${lead} (${files.length} file${files.length === 1 ? "" : "s"})`;
|
|
12858
|
+
}
|
|
12859
|
+
var METRIC_CONDITION_MAX_LENGTH = 240;
|
|
12860
|
+
var CRITERION_NOTES_MAX_LENGTH = 1e3;
|
|
12861
|
+
var CRITERION_DISCLOSURE_MAX_LENGTH = 1e3;
|
|
12862
|
+
function placeholderSetAsideDisclosures2(files) {
|
|
12863
|
+
if (files.length === 0) return {};
|
|
12864
|
+
const sentence = placeholderSetAsideDisclosure(files, CRITERION_DISCLOSURE_MAX_LENGTH);
|
|
12865
|
+
return sentence ? { disclosures: [sentence] } : {};
|
|
12866
|
+
}
|
|
10393
12867
|
function isFillerRunSecretValue(value) {
|
|
10394
12868
|
const candidates = [value];
|
|
10395
12869
|
const sepIdx = Math.max(value.lastIndexOf("_"), value.lastIndexOf("-"));
|
|
@@ -10566,7 +13040,7 @@ function hasSuspiciousDependencies(repoPath, manifest, useV47Detectors = false)
|
|
|
10566
13040
|
}
|
|
10567
13041
|
function readRecentCommits(repoPath) {
|
|
10568
13042
|
const historyResult = execGit(
|
|
10569
|
-
["log", "--no-show-signature",
|
|
13043
|
+
["log", "--no-show-signature", `--max-count=${RECENT_COMMIT_WINDOW}`, "--format=%H%x00%s"],
|
|
10570
13044
|
{
|
|
10571
13045
|
cwd: repoPath
|
|
10572
13046
|
}
|
|
@@ -10602,7 +13076,7 @@ function evidenceForRelative(repoPath, path, kind, label) {
|
|
|
10602
13076
|
path,
|
|
10603
13077
|
...contents ? {
|
|
10604
13078
|
line: firstMeaningfulLine(contents),
|
|
10605
|
-
contentHash:
|
|
13079
|
+
contentHash: createHash4("sha256").update(contents).digest("hex")
|
|
10606
13080
|
} : {}
|
|
10607
13081
|
};
|
|
10608
13082
|
}
|
|
@@ -10614,7 +13088,7 @@ function evidenceForRelativeAtLine(repoPath, path, kind, label, line) {
|
|
|
10614
13088
|
label,
|
|
10615
13089
|
path,
|
|
10616
13090
|
line,
|
|
10617
|
-
...contents ? { contentHash:
|
|
13091
|
+
...contents ? { contentHash: createHash4("sha256").update(contents).digest("hex") } : {}
|
|
10618
13092
|
};
|
|
10619
13093
|
}
|
|
10620
13094
|
function metric(name, label, value, max, weight, unit, description, kind, presentation) {
|
|
@@ -10680,15 +13154,6 @@ function hasEnvPathInGitHistory(repoPath, scanLimitations) {
|
|
|
10680
13154
|
// src/witan/public-scan.ts
|
|
10681
13155
|
init_esm_shims();
|
|
10682
13156
|
|
|
10683
|
-
// src/witan/history-depth.ts
|
|
10684
|
-
init_esm_shims();
|
|
10685
|
-
var SHALLOW_HISTORY_LIMITATION = "This repository is a shallow clone (git rev-parse --is-shallow-repository reports true), so its commit history is truncated. A2 (credentials and .env files in earlier commits) and B2 (pull-request merge ratio over recent commits) read history and saw only the commits present; on a full clone their results may differ. Scores are computed exactly as for any other scan. Re-scan a full clone (for actions/checkout, set fetch-depth: 0).";
|
|
10686
|
-
function detectHistoryLimitations(repoPath) {
|
|
10687
|
-
const result = execGit(["rev-parse", "--is-shallow-repository"], { cwd: repoPath });
|
|
10688
|
-
if (!result.ok || result.stdout.trim() !== "true") return void 0;
|
|
10689
|
-
return [SHALLOW_HISTORY_LIMITATION];
|
|
10690
|
-
}
|
|
10691
|
-
|
|
10692
13157
|
// src/witan/ingest.ts
|
|
10693
13158
|
init_esm_shims();
|
|
10694
13159
|
import { lstatSync as lstatSync3, opendirSync, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "fs";
|
|
@@ -10716,54 +13181,7 @@ function clampFinding(finding) {
|
|
|
10716
13181
|
// src/witan/generic-adapter.ts
|
|
10717
13182
|
init_esm_shims();
|
|
10718
13183
|
import { readFileSync } from "fs";
|
|
10719
|
-
|
|
10720
|
-
// src/witan/ingest-files.ts
|
|
10721
|
-
init_esm_shims();
|
|
10722
|
-
import { lstatSync as lstatSync2, realpathSync as realpathSync2 } from "fs";
|
|
10723
|
-
import { dirname as dirname2, isAbsolute, relative as relative2, resolve as resolve2, sep as sep2 } from "path";
|
|
10724
|
-
var MAX_INGEST_DOCUMENT_BYTES = 8 * 1024 * 1024;
|
|
10725
|
-
function isContainedPath(candidate, directory) {
|
|
10726
|
-
const relativePath = relative2(directory, candidate);
|
|
10727
|
-
return relativePath === "" || relativePath !== ".." && !relativePath.startsWith(`..${sep2}`) && !isAbsolute(relativePath);
|
|
10728
|
-
}
|
|
10729
|
-
function isMissingIngestPath(error) {
|
|
10730
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
10731
|
-
}
|
|
10732
|
-
function resolveIngestFilePath(filePath, inputDirectory = dirname2(filePath)) {
|
|
10733
|
-
const lexicalDirectory = resolve2(inputDirectory);
|
|
10734
|
-
const lexicalCandidate = resolve2(filePath);
|
|
10735
|
-
const directoryStat = lstatSync2(lexicalDirectory);
|
|
10736
|
-
if (directoryStat.isSymbolicLink()) {
|
|
10737
|
-
throw new Error(`Cejel: refusing symlinked ingest directory: ${inputDirectory}`);
|
|
10738
|
-
}
|
|
10739
|
-
if (!directoryStat.isDirectory()) {
|
|
10740
|
-
throw new Error(`Cejel: refusing non-directory ingest boundary: ${inputDirectory}`);
|
|
10741
|
-
}
|
|
10742
|
-
const candidateStat = lstatSync2(lexicalCandidate);
|
|
10743
|
-
if (candidateStat.isSymbolicLink()) {
|
|
10744
|
-
throw new Error(`Cejel: refusing symlinked ingest file: ${filePath}`);
|
|
10745
|
-
}
|
|
10746
|
-
if (!candidateStat.isFile()) {
|
|
10747
|
-
throw new Error(`Cejel: refusing non-regular ingest file: ${filePath}`);
|
|
10748
|
-
}
|
|
10749
|
-
if (candidateStat.size > MAX_INGEST_DOCUMENT_BYTES) {
|
|
10750
|
-
throw new Error(
|
|
10751
|
-
`Cejel: ingest file exceeds the ${MAX_INGEST_DOCUMENT_BYTES.toLocaleString("en-US")} byte budget: ${filePath}`
|
|
10752
|
-
);
|
|
10753
|
-
}
|
|
10754
|
-
const resolvedDirectory = realpathSync2(lexicalDirectory);
|
|
10755
|
-
const resolvedCandidate = realpathSync2(lexicalCandidate);
|
|
10756
|
-
if (!isContainedPath(resolvedCandidate, resolvedDirectory)) {
|
|
10757
|
-
throw new Error(`Cejel: refusing ingest file outside its resolved input directory: ${filePath}`);
|
|
10758
|
-
}
|
|
10759
|
-
return resolvedCandidate;
|
|
10760
|
-
}
|
|
10761
|
-
function isResolvedIngestPathContained(candidate, directory) {
|
|
10762
|
-
return isContainedPath(candidate, directory);
|
|
10763
|
-
}
|
|
10764
|
-
|
|
10765
|
-
// src/witan/generic-adapter.ts
|
|
10766
|
-
var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.0";
|
|
13184
|
+
var CEJEL_GENERIC_INGEST_CONTRACT_VERSION = "1.1";
|
|
10767
13185
|
var CEJEL_GENERIC_INGEST_CONTRACT_MAJOR = 1;
|
|
10768
13186
|
var GenericFindingV1Schema = external_exports.object({
|
|
10769
13187
|
ruleId: external_exports.string().min(1),
|
|
@@ -10781,6 +13199,17 @@ var GenericSignalDocumentV1Schema = external_exports.object({
|
|
|
10781
13199
|
tool: external_exports.string().trim().min(1),
|
|
10782
13200
|
signals: external_exports.array(GenericSignalV1Schema)
|
|
10783
13201
|
}).passthrough();
|
|
13202
|
+
var GenericIdentityV1_1Schema = external_exports.object({
|
|
13203
|
+
subject: external_exports.object({
|
|
13204
|
+
repository: external_exports.string().trim().min(1).max(2048).optional(),
|
|
13205
|
+
commit: external_exports.string().regex(/^[0-9a-fA-F]{7,64}$/, "expected a 7-64 character hexadecimal commit id").optional()
|
|
13206
|
+
}).passthrough().optional(),
|
|
13207
|
+
producer: external_exports.object({
|
|
13208
|
+
name: external_exports.string().trim().min(1).max(200).optional(),
|
|
13209
|
+
version: external_exports.string().trim().min(1).max(200).optional()
|
|
13210
|
+
}).passthrough().optional(),
|
|
13211
|
+
observedAt: external_exports.string().datetime({ offset: true }).optional()
|
|
13212
|
+
}).passthrough();
|
|
10784
13213
|
function isGenericSignalDocument(raw) {
|
|
10785
13214
|
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return false;
|
|
10786
13215
|
const doc = raw;
|
|
@@ -10808,8 +13237,10 @@ function assertSupportedContractVersion(version) {
|
|
|
10808
13237
|
function formatValidationIssues(error) {
|
|
10809
13238
|
return error.issues.map((issue) => `${issue.path.length > 0 ? issue.path.join(".") : "<document>"}: ${issue.message}`).join("; ");
|
|
10810
13239
|
}
|
|
10811
|
-
function
|
|
10812
|
-
if (!isGenericSignalDocument(raw))
|
|
13240
|
+
function parseGenericDocument(raw) {
|
|
13241
|
+
if (!isGenericSignalDocument(raw)) {
|
|
13242
|
+
return { signals: [], identity: { producers: [], subjects: [] } };
|
|
13243
|
+
}
|
|
10813
13244
|
assertSupportedContractVersion(raw.version);
|
|
10814
13245
|
const parsed = GenericSignalDocumentV1Schema.safeParse(raw);
|
|
10815
13246
|
if (!parsed.success) {
|
|
@@ -10817,7 +13248,23 @@ function parseGenericJson(raw) {
|
|
|
10817
13248
|
`Cejel: invalid generic ingest contract v${raw.version}: ${formatValidationIssues(parsed.error)}`
|
|
10818
13249
|
);
|
|
10819
13250
|
}
|
|
10820
|
-
|
|
13251
|
+
const minor = Number(/^\d+\.(\d+)$/.exec(parsed.data.version)?.[1] ?? "0");
|
|
13252
|
+
let identity = genericIdentity({ tool: parsed.data.tool });
|
|
13253
|
+
if (minor >= 1) {
|
|
13254
|
+
const identityFields = GenericIdentityV1_1Schema.safeParse(raw);
|
|
13255
|
+
if (!identityFields.success) {
|
|
13256
|
+
throw new Error(
|
|
13257
|
+
`Cejel: invalid generic ingest contract v${parsed.data.version}: ${formatValidationIssues(identityFields.error)}`
|
|
13258
|
+
);
|
|
13259
|
+
}
|
|
13260
|
+
identity = genericIdentity({
|
|
13261
|
+
tool: parsed.data.tool,
|
|
13262
|
+
...identityFields.data.subject ? { subject: identityFields.data.subject } : {},
|
|
13263
|
+
...identityFields.data.producer ? { producer: identityFields.data.producer } : {},
|
|
13264
|
+
...identityFields.data.observedAt ? { observedAt: identityFields.data.observedAt } : {}
|
|
13265
|
+
});
|
|
13266
|
+
}
|
|
13267
|
+
const signals = parsed.data.signals.map((signal) => ({
|
|
10821
13268
|
source: parsed.data.tool,
|
|
10822
13269
|
dimension: signal.dimension,
|
|
10823
13270
|
weight: signal.weight,
|
|
@@ -10828,6 +13275,7 @@ function parseGenericJson(raw) {
|
|
|
10828
13275
|
...finding.location ? { location: finding.location } : {}
|
|
10829
13276
|
}))
|
|
10830
13277
|
}));
|
|
13278
|
+
return { signals, identity };
|
|
10831
13279
|
}
|
|
10832
13280
|
|
|
10833
13281
|
// src/witan/sarif-adapter.ts
|
|
@@ -11094,11 +13542,13 @@ function sanitizeIngestSource(source) {
|
|
|
11094
13542
|
maxLength: MAX_INGEST_SOURCE_LENGTH
|
|
11095
13543
|
});
|
|
11096
13544
|
}
|
|
11097
|
-
function
|
|
13545
|
+
function readIngestDocument(filePath, options = {}) {
|
|
11098
13546
|
const resolvedFilePath = resolveIngestFilePath(filePath);
|
|
13547
|
+
const bytes = readFileSync4(resolvedFilePath);
|
|
13548
|
+
const sha256 = sha256OfBytes(bytes);
|
|
11099
13549
|
let raw;
|
|
11100
13550
|
try {
|
|
11101
|
-
raw = JSON.parse(stripBom(
|
|
13551
|
+
raw = JSON.parse(stripBom(bytes.toString("utf8")));
|
|
11102
13552
|
} catch (error) {
|
|
11103
13553
|
const message = error instanceof Error ? error.message : String(error);
|
|
11104
13554
|
throw new Error(`Cejel: could not parse ingest file as JSON: ${filePath} (${message})`);
|
|
@@ -11110,10 +13560,23 @@ function parseIngestFile(filePath, options = {}) {
|
|
|
11110
13560
|
);
|
|
11111
13561
|
}
|
|
11112
13562
|
let signals;
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
13563
|
+
let format;
|
|
13564
|
+
let identity = { producers: [], subjects: [] };
|
|
13565
|
+
if (looksLikeSarif(raw)) {
|
|
13566
|
+
signals = parseSarifJson(raw, options.extraSarifDimensionRules);
|
|
13567
|
+
format = "sarif";
|
|
13568
|
+
identity = extractSarifIdentity(raw);
|
|
13569
|
+
} else if (looksLikeScorecard(raw)) {
|
|
13570
|
+
signals = parseScorecardJson(raw);
|
|
13571
|
+
format = "scorecard";
|
|
13572
|
+
identity = extractScorecardIdentity(raw);
|
|
13573
|
+
} else if (isGenericSignalDocument(raw)) {
|
|
13574
|
+
const generic = parseGenericDocument(raw);
|
|
13575
|
+
signals = generic.signals;
|
|
13576
|
+
format = "generic";
|
|
13577
|
+
identity = generic.identity;
|
|
13578
|
+
}
|
|
13579
|
+
if (!signals || !format) {
|
|
11117
13580
|
throw new Error(
|
|
11118
13581
|
`Cejel: unrecognized ingest file format: ${filePath} \u2014 expected SARIF (a "runs" array), OpenSSF Scorecard JSON (a "checks" array), or the versioned generic Cejel external-signal shape (a "version" string + "tool" string + "signals" array). See the cejel README "Aggregate your scanners".`
|
|
11119
13582
|
);
|
|
@@ -11125,12 +13588,17 @@ function parseIngestFile(filePath, options = {}) {
|
|
|
11125
13588
|
`
|
|
11126
13589
|
);
|
|
11127
13590
|
}
|
|
11128
|
-
return
|
|
11129
|
-
|
|
11130
|
-
|
|
11131
|
-
|
|
11132
|
-
|
|
11133
|
-
|
|
13591
|
+
return {
|
|
13592
|
+
signals: signals.map((signal) => ({
|
|
13593
|
+
...signal,
|
|
13594
|
+
source: sanitizeIngestSource(signal.source),
|
|
13595
|
+
provenance: options.provenance ?? "operator_supplied",
|
|
13596
|
+
findings: signal.findings.map(clampFinding)
|
|
13597
|
+
})),
|
|
13598
|
+
sha256,
|
|
13599
|
+
format,
|
|
13600
|
+
identity
|
|
13601
|
+
};
|
|
11134
13602
|
}
|
|
11135
13603
|
function expandIngestPattern(pattern, maxMatches = Number.POSITIVE_INFINITY) {
|
|
11136
13604
|
if (!pattern.includes("*")) return [pattern];
|
|
@@ -11214,6 +13682,7 @@ function discoverIngestInputs(repoPath, maxMatches = Number.POSITIVE_INFINITY) {
|
|
|
11214
13682
|
// src/witan/public-scan.ts
|
|
11215
13683
|
var MAX_INGEST_DOCUMENTS = 128;
|
|
11216
13684
|
var MAX_RETAINED_INGEST_FINDINGS = 1e4;
|
|
13685
|
+
var MAX_INPUT_BINDING_RECORDS = 2 * MAX_INGEST_DOCUMENTS;
|
|
11217
13686
|
function scoreRepoWithPublicCejel(options) {
|
|
11218
13687
|
if (options.rubricVersion !== void 0) {
|
|
11219
13688
|
assertSelectableRubricVersion(options.rubricVersion);
|
|
@@ -11225,35 +13694,90 @@ function scoreRepoWithPublicCejel(options) {
|
|
|
11225
13694
|
...options.generatedAt ? { generatedAt: options.generatedAt } : {},
|
|
11226
13695
|
...options.rubricVersion ? { rubricVersion: options.rubricVersion } : {}
|
|
11227
13696
|
});
|
|
11228
|
-
const
|
|
13697
|
+
const scannedSubject = readScannedSubject(
|
|
13698
|
+
options.repoPath,
|
|
13699
|
+
input.repo.headSha,
|
|
13700
|
+
options.subjectRepository
|
|
13701
|
+
);
|
|
13702
|
+
const { signals: inputSignals, documents } = resolvePublicIngestInputs(options);
|
|
13703
|
+
const inputBinding = documents.length > 0 ? buildInputBinding(documents, scannedSubject, options.strictIngest ? "strict" : "record-only") : void 0;
|
|
13704
|
+
if (options.strictIngest && inputBinding) assertStrictIngestBinding(documents, inputBinding);
|
|
11229
13705
|
const scored = createWitanReport(
|
|
11230
13706
|
input,
|
|
11231
13707
|
inputSignals.length > 0 ? inputSignals : void 0,
|
|
11232
13708
|
void 0,
|
|
11233
13709
|
options.toolVersion
|
|
11234
13710
|
);
|
|
11235
|
-
const historyLimitations = detectHistoryLimitations(
|
|
11236
|
-
|
|
13711
|
+
const historyLimitations = detectHistoryLimitations(
|
|
13712
|
+
options.repoPath,
|
|
13713
|
+
usesV27FalseAssuranceClosure(scored.rubricVersion)
|
|
13714
|
+
);
|
|
13715
|
+
const withHistory = historyLimitations ? { ...scored, historyLimitations } : scored;
|
|
13716
|
+
const report = inputBinding ? { ...withHistory, inputBinding } : withHistory;
|
|
11237
13717
|
if (report.verdict !== "insufficient_source" || !isWitanNoMeasurementAbstention(report)) {
|
|
11238
|
-
return report;
|
|
13718
|
+
return assertReportVerifiable(report);
|
|
11239
13719
|
}
|
|
11240
13720
|
const contextualReason = explainNoMeasurementSourceCoverage(
|
|
11241
13721
|
options.repoPath,
|
|
11242
13722
|
report.rubricVersion
|
|
11243
13723
|
);
|
|
11244
|
-
return
|
|
11245
|
-
|
|
11246
|
-
|
|
11247
|
-
|
|
13724
|
+
return assertReportVerifiable(
|
|
13725
|
+
contextualReason ? {
|
|
13726
|
+
...report,
|
|
13727
|
+
insufficientSourceReason: contextualReason
|
|
13728
|
+
} : report
|
|
13729
|
+
);
|
|
13730
|
+
}
|
|
13731
|
+
function assertReportVerifiable(report) {
|
|
13732
|
+
const result = WitanReportSchema.safeParse(report);
|
|
13733
|
+
if (result.success) return report;
|
|
13734
|
+
const issues = result.error.issues.slice(0, 8).map((issue) => ` - ${issue.path.join(".") || "<root>"}: ${issue.message}`);
|
|
13735
|
+
throw new Error(
|
|
13736
|
+
[
|
|
13737
|
+
"Cejel: the assembled report does not satisfy the report schema `cejel verify` applies; no certificate was written.",
|
|
13738
|
+
...issues,
|
|
13739
|
+
...result.error.issues.length > issues.length ? [` ...and ${result.error.issues.length - issues.length} more.`] : []
|
|
13740
|
+
].join("\n")
|
|
13741
|
+
);
|
|
13742
|
+
}
|
|
13743
|
+
function readScannedSubject(repoPath, headSha, subjectRepository) {
|
|
13744
|
+
let repository;
|
|
13745
|
+
if (subjectRepository !== void 0) {
|
|
13746
|
+
repository = normalizeRepositoryIdentity(subjectRepository);
|
|
13747
|
+
if (repository === void 0) {
|
|
13748
|
+
throw new Error(
|
|
13749
|
+
`Cejel: --subject-repository value (${subjectRepository.length} characters, not echoed) is not a repository identity Cejel can compare. Expected a form such as github.com/owner/repo or https://github.com/owner/repo.git, with a DNS host name and a path.`
|
|
13750
|
+
);
|
|
13751
|
+
}
|
|
13752
|
+
}
|
|
13753
|
+
let commitTime;
|
|
13754
|
+
if (headSha) {
|
|
13755
|
+
const result = execGit(["show", "-s", "--format=%ct", "HEAD"], { cwd: repoPath });
|
|
13756
|
+
const seconds = result.ok ? Number(result.stdout.trim()) : Number.NaN;
|
|
13757
|
+
if (Number.isSafeInteger(seconds) && seconds >= 0) {
|
|
13758
|
+
commitTime = plainIsoInstant(seconds * 1e3);
|
|
13759
|
+
}
|
|
13760
|
+
}
|
|
13761
|
+
return {
|
|
13762
|
+
...repository !== void 0 ? { repository } : {},
|
|
13763
|
+
...headSha ? { commit: headSha.toLowerCase() } : {},
|
|
13764
|
+
...commitTime !== void 0 ? { commitTime } : {}
|
|
13765
|
+
};
|
|
11248
13766
|
}
|
|
11249
|
-
function
|
|
13767
|
+
function resolvePublicIngestInputs(options) {
|
|
11250
13768
|
const seen = /* @__PURE__ */ new Set();
|
|
11251
13769
|
const documents = [];
|
|
13770
|
+
const samePathRepeats = /* @__PURE__ */ new Map();
|
|
11252
13771
|
const addDocument = (file, provenance) => {
|
|
11253
13772
|
const resolved = resolveIngestFilePath(file);
|
|
11254
|
-
if (seen.has(resolved))
|
|
13773
|
+
if (seen.has(resolved)) {
|
|
13774
|
+
if (!samePathRepeats.has(resolved)) {
|
|
13775
|
+
samePathRepeats.set(resolved, { file: resolved, displayPath: file, provenance });
|
|
13776
|
+
}
|
|
13777
|
+
return;
|
|
13778
|
+
}
|
|
11255
13779
|
seen.add(resolved);
|
|
11256
|
-
documents.push({ file: resolved, provenance });
|
|
13780
|
+
documents.push({ file: resolved, displayPath: file, provenance });
|
|
11257
13781
|
if (documents.length > MAX_INGEST_DOCUMENTS) {
|
|
11258
13782
|
throw new Error(
|
|
11259
13783
|
`Cejel: ${documents.length} unique ingest documents exceed the ${MAX_INGEST_DOCUMENTS} ingest document budget; refusing to produce a partial certificate.`
|
|
@@ -11279,12 +13803,16 @@ function resolvePublicIngestSignals(options) {
|
|
|
11279
13803
|
}
|
|
11280
13804
|
}
|
|
11281
13805
|
const signals = [];
|
|
13806
|
+
const records = [];
|
|
13807
|
+
const readByPath = /* @__PURE__ */ new Map();
|
|
13808
|
+
const scoredDigests = /* @__PURE__ */ new Set();
|
|
11282
13809
|
let retainedFindings = 0;
|
|
11283
13810
|
for (const document of documents) {
|
|
11284
|
-
const
|
|
13811
|
+
const read = readIngestDocument(document.file, {
|
|
11285
13812
|
maxFindingCandidates: MAX_RETAINED_INGEST_FINDINGS - retainedFindings,
|
|
11286
13813
|
provenance: document.provenance
|
|
11287
13814
|
});
|
|
13815
|
+
const parsed = read.signals;
|
|
11288
13816
|
const documentFindings = parsed.reduce((count, signal) => count + signal.findings.length, 0);
|
|
11289
13817
|
retainedFindings += documentFindings;
|
|
11290
13818
|
if (retainedFindings > MAX_RETAINED_INGEST_FINDINGS) {
|
|
@@ -11293,8 +13821,30 @@ function resolvePublicIngestSignals(options) {
|
|
|
11293
13821
|
);
|
|
11294
13822
|
}
|
|
11295
13823
|
signals.push(...parsed);
|
|
13824
|
+
readByPath.set(document.file, read);
|
|
13825
|
+
records.push({
|
|
13826
|
+
sha256: read.sha256,
|
|
13827
|
+
format: read.format,
|
|
13828
|
+
provenance: document.provenance,
|
|
13829
|
+
identity: read.identity,
|
|
13830
|
+
displayPath: document.displayPath,
|
|
13831
|
+
...scoredDigests.has(read.sha256) ? { duplicate: "same-content" } : {}
|
|
13832
|
+
});
|
|
13833
|
+
scoredDigests.add(read.sha256);
|
|
13834
|
+
}
|
|
13835
|
+
for (const repeat of samePathRepeats.values()) {
|
|
13836
|
+
const read = readByPath.get(repeat.file);
|
|
13837
|
+
if (!read) continue;
|
|
13838
|
+
records.push({
|
|
13839
|
+
sha256: read.sha256,
|
|
13840
|
+
format: read.format,
|
|
13841
|
+
provenance: repeat.provenance,
|
|
13842
|
+
identity: read.identity,
|
|
13843
|
+
displayPath: repeat.displayPath,
|
|
13844
|
+
duplicate: "same-path"
|
|
13845
|
+
});
|
|
11296
13846
|
}
|
|
11297
|
-
return signals;
|
|
13847
|
+
return { signals, documents: records };
|
|
11298
13848
|
}
|
|
11299
13849
|
|
|
11300
13850
|
// src/product-identity.ts
|
|
@@ -11493,6 +14043,8 @@ function runCejelScan(options) {
|
|
|
11493
14043
|
// are third-party repositories and must opt in explicitly.
|
|
11494
14044
|
autoDiscoverIngest: true,
|
|
11495
14045
|
warnOnEmptyIngestMatch: options.warnOnEmptyIngestMatch,
|
|
14046
|
+
...options.strictIngest ? { strictIngest: true } : {},
|
|
14047
|
+
...options.subjectRepository ? { subjectRepository: options.subjectRepository } : {},
|
|
11496
14048
|
...options.toolVersion ? { toolVersion: options.toolVersion } : {}
|
|
11497
14049
|
});
|
|
11498
14050
|
const summary = buildWitanCliSummary(report);
|
|
@@ -11503,7 +14055,13 @@ export {
|
|
|
11503
14055
|
WitanReportSchema,
|
|
11504
14056
|
computeApplicableMeasuredCoverage,
|
|
11505
14057
|
formatCoverageSummary,
|
|
14058
|
+
EXTERNAL_FINDINGS_DISPLAY_LIMIT,
|
|
11506
14059
|
formatExternalSourceLine,
|
|
14060
|
+
inheritedFindingNote,
|
|
14061
|
+
importedAdjustmentSentence,
|
|
14062
|
+
inputBindingHeadline,
|
|
14063
|
+
describeInputBindingRecord,
|
|
14064
|
+
describeScannedSubject,
|
|
11507
14065
|
WITAN_RUBRIC_VERSION_V22,
|
|
11508
14066
|
assertSelectableRubricVersion,
|
|
11509
14067
|
isProspectiveRubricVersion,
|
|
@@ -11523,6 +14081,9 @@ export {
|
|
|
11523
14081
|
serializeWitanReport,
|
|
11524
14082
|
createWitanAttestation,
|
|
11525
14083
|
verifyWitanAttestationBinding,
|
|
14084
|
+
stripTerminalControls,
|
|
14085
|
+
verifyRecordedInputDigests,
|
|
14086
|
+
resolveIngestFilePath,
|
|
11526
14087
|
deriveProductIdentity,
|
|
11527
14088
|
runCejelScan
|
|
11528
14089
|
};
|