fix: harden provider and promotion evidence
This commit is contained in:
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user