fix: harden provider and promotion evidence

This commit is contained in:
DongHyeonka
2026-08-02 16:28:24 +09:00
parent 42ffb79997
commit 30ceac23c1
29 changed files with 3961 additions and 1076 deletions
+125
View File
@@ -7,6 +7,131 @@ const timestamp = z.iso.datetime();
const sha256 = z.string().regex(/^[a-f0-9]{64}$/u);
const jsonObject = z.record(z.string(), z.json());
const canonicalTimestamp = z
.string()
.regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/u)
.refine((value) => new Date(value).toISOString() === value, {
message: "must be a canonical ISO-8601 UTC timestamp",
});
const safeRepositoryPath = z
.string()
.min(1)
.max(1_024)
.refine(
(value) =>
!value.startsWith("-") &&
!value.startsWith("/") &&
!value.includes("\\") &&
!value.split("/").some((segment) => segment === "" || segment === "." || segment === "..") &&
![...value].some((character) => {
const codePoint = character.codePointAt(0)!;
return codePoint <= 0x1f || codePoint === 0x7f;
}),
{ message: "must be a safe canonical repository-relative path" },
);
const assessmentInputRowSchema = z
.object({
path: safeRepositoryPath,
bytes: z.int().nonnegative().max(268_435_456),
sha256,
})
.strict();
const assessmentStatusSchema = z.enum(["PASS", "FAIL"]);
function addCanonicalInputIssues(
rows: readonly Readonly<{ path: string }>[],
pathPrefix: "policyInputs" | "evidenceInputs",
context: z.RefinementCtx,
): void {
const paths = rows.map(({ path }) => path);
const canonical = [...paths].sort((left, right) =>
left < right ? -1 : left > right ? 1 : 0,
);
if (JSON.stringify(paths) !== JSON.stringify(canonical)) {
context.addIssue({
code: "custom",
path: [pathPrefix],
message: "must be in canonical ASCII path order",
});
}
if (new Set(paths).size !== paths.length) {
context.addIssue({
code: "custom",
path: [pathPrefix],
message: "must not contain duplicate paths",
});
}
}
export const localEvidenceAssessmentArtifactSchema = z
.object({
schemaVersion: z.literal(1),
artifactType: z.literal("local-evidence-assessment"),
generatedAt: canonicalTimestamp,
status: assessmentStatusSchema,
verifier: z
.object({
id: nonEmptyString,
version: nonEmptyString,
sourceSha256: sha256,
})
.strict(),
source: z
.object({
revision: z.string().regex(/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u),
sourceSetSha256: sha256,
})
.strict(),
candidate: z
.object({ distSha256: sha256, lockfileSha256: sha256, sbomSha256: sha256 })
.strict(),
policyInputs: z.array(assessmentInputRowSchema).min(1).max(256),
evidenceInputs: z.array(assessmentInputRowSchema).min(1).max(4_096),
checks: z
.object({
release: assessmentStatusSchema,
supplyChain: assessmentStatusSchema,
dependencyPolicy: assessmentStatusSchema,
licensePolicy: assessmentStatusSchema,
vulnerabilityPolicy: assessmentStatusSchema,
secretScan: assessmentStatusSchema,
})
.strict(),
failures: z.array(z.string()),
})
.strict()
.superRefine((assessment, context) => {
addCanonicalInputIssues(assessment.policyInputs, "policyInputs", context);
addCanonicalInputIssues(assessment.evidenceInputs, "evidenceInputs", context);
const failedChecks = Object.values(assessment.checks).filter(
(status) => status === "FAIL",
);
if (
assessment.status === "PASS" &&
(failedChecks.length > 0 || assessment.failures.length > 0)
) {
context.addIssue({
code: "custom",
path: ["status"],
message: "PASS requires all six checks PASS and no failures",
});
}
if (
assessment.status === "FAIL" &&
(failedChecks.length === 0 || assessment.failures.length === 0)
) {
context.addIssue({
code: "custom",
path: ["status"],
message: "FAIL requires a failed check and a failure diagnostic",
});
}
});
export type LocalEvidenceAssessment = z.infer<
typeof localEvidenceAssessmentArtifactSchema
>;
export const moduleInventoryArtifactSchema = z
.object({
schemaVersion: z.literal(1),