-
Notifications
You must be signed in to change notification settings - Fork 0
fix(acquisition): reject future deployment evidence timestamps #484
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
81b446d
6650429
c9c974e
c893c57
b9ad490
e29a503
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ const EXPECTED_OIDC_ISSUER = "https://token.actions.githubusercontent.com"; | |
| const shaPattern = /^[0-9a-f]{40}$/; | ||
| const digestPattern = /^[0-9a-f]{64}$/; | ||
| const tagPattern = /^v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/; | ||
| const isoCalendarPrefixPattern = /^(\d{4})-(\d{2})-(\d{2})T/; | ||
|
|
||
| function isObject(value) { | ||
| return Boolean(value) && typeof value === "object" && !Array.isArray(value); | ||
|
|
@@ -21,6 +22,26 @@ function canonicalIdentity(value, pattern) { | |
| return typeof value === "string" && value === value.trim() && pattern.test(value); | ||
| } | ||
|
|
||
| function isValidNonFutureTimestamp(value, nowMilliseconds) { | ||
| if (typeof value !== "string" || value !== value.trim()) { | ||
| return false; | ||
| } | ||
| const calendar = value.match(isoCalendarPrefixPattern); | ||
| const timestampMilliseconds = Date.parse(value); | ||
| if (!calendar || !Number.isFinite(timestampMilliseconds) || timestampMilliseconds > nowMilliseconds) { | ||
| return false; | ||
| } | ||
| const year = Number(calendar[1]); | ||
| const month = Number(calendar[2]); | ||
| const day = Number(calendar[3]); | ||
| const leapYear = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0); | ||
| const daysPerMonth = [31, leapYear ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; | ||
| return month >= 1 | ||
| && month <= 12 | ||
| && day >= 1 | ||
| && day <= daysPerMonth[month - 1]; | ||
| } | ||
|
|
||
| function failure(code, detail) { | ||
| return { code, detail }; | ||
| } | ||
|
|
@@ -65,6 +86,7 @@ function validBundle(value) { | |
|
|
||
| export function evaluateAcquisitionDeploymentEvidence(input = {}) { | ||
| const failures = []; | ||
| const nowMilliseconds = Date.now(); | ||
| const expectedTag = text(input.expectedTag); | ||
| const deployment = input.deploymentEvidence; | ||
| const governance = input.governanceEvidence; | ||
|
|
@@ -111,6 +133,12 @@ export function evaluateAcquisitionDeploymentEvidence(input = {}) { | |
| "deployment_schema_invalid", | ||
| "Deployment evidence schemaVersion must be 1.", | ||
| ); | ||
| add( | ||
| failures, | ||
| isValidNonFutureTimestamp(deployment.generatedAt, nowMilliseconds), | ||
| "deployment_generated_at_invalid", | ||
| "Deployment evidence generatedAt must be a valid non-future timestamp.", | ||
| ); | ||
| add( | ||
| failures, | ||
| deployment.source?.repository === EXPECTED_REPOSITORY, | ||
|
|
@@ -149,7 +177,7 @@ export function evaluateAcquisitionDeploymentEvidence(input = {}) { | |
| ); | ||
| add( | ||
| failures, | ||
| Number(deployment.deployment?.trafficPercentage) === 100, | ||
| deployment.deployment?.trafficPercentage === 100, | ||
| "deployment_traffic_not_full", | ||
| "Deployment evidence must prove exactly 100% active traffic.", | ||
| ); | ||
|
|
@@ -162,6 +190,18 @@ export function evaluateAcquisitionDeploymentEvidence(input = {}) { | |
| "deployment_workflow_url_invalid", | ||
| "Deployment evidence must contain the trusted repository workflow-run URL.", | ||
| ); | ||
| add( | ||
| failures, | ||
| isValidNonFutureTimestamp(deployment.deployment?.deployedAt, nowMilliseconds), | ||
| "deployment_deployed_at_invalid", | ||
| "Deployment deployedAt must be a valid non-future timestamp.", | ||
| ); | ||
| add( | ||
| failures, | ||
| isValidNonFutureTimestamp(deployment.deployment?.deploymentCreatedAt, nowMilliseconds), | ||
| "deployment_created_at_invalid", | ||
| "Deployment deploymentCreatedAt must be a valid non-future timestamp.", | ||
| ); | ||
| add( | ||
| failures, | ||
| deployment.validation?.immutableRelease === true, | ||
|
|
@@ -180,6 +220,18 @@ export function evaluateAcquisitionDeploymentEvidence(input = {}) { | |
| "deployment_smoke_failed", | ||
| "Deployment validation must prove successful post-deployment smoke checks.", | ||
| ); | ||
| add( | ||
| failures, | ||
| isValidNonFutureTimestamp(deployment.validation?.kpiExecutedAt, nowMilliseconds), | ||
| "deployment_kpi_timestamp_invalid", | ||
| "Deployment KPI execution time must be a valid non-future timestamp.", | ||
| ); | ||
| add( | ||
| failures, | ||
| isValidNonFutureTimestamp(deployment.validation?.smokeTimestamp, nowMilliseconds), | ||
| "deployment_smoke_timestamp_invalid", | ||
| "Deployment smoke time must be a valid non-future timestamp.", | ||
| ); | ||
|
Comment on lines
220
to
+234
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📝 Info: New timestamp gates require builder fields present The evaluator now hard-requires (Refers to this code) Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| } | ||
|
|
||
| if (isObject(governance)) { | ||
|
|
@@ -209,8 +261,8 @@ export function evaluateAcquisitionDeploymentEvidence(input = {}) { | |
| ); | ||
| add( | ||
| failures, | ||
| Number.isSafeInteger(Number(governance.reviewer_count)) | ||
| && Number(governance.reviewer_count) > 0 | ||
| Number.isSafeInteger(governance.reviewer_count) | ||
| && governance.reviewer_count > 0 | ||
| && Array.isArray(governance.reviewers) | ||
| && governance.reviewers.length > 0, | ||
| "governance_reviewer_missing", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { evaluateAcquisitionDeploymentEvidence } from "../scripts/lib/acquisition-deployment-evidence.mjs"; | ||
|
|
||
| const repository = "ContextualWisdomLab/noema"; | ||
| const releaseTag = "v0.1.0"; | ||
| const commitSha = "a".repeat(40); | ||
| const deploymentEvidenceSha256 = "b".repeat(64); | ||
|
|
||
| function fixture() { | ||
| return { | ||
| expectedTag: releaseTag, | ||
| deploymentEvidence: { | ||
| schemaVersion: 1, | ||
| generatedAt: "2026-08-04T00:00:00.000Z", | ||
| source: { | ||
| repository, | ||
| releaseTag, | ||
| releaseRef: `refs/tags/${releaseTag}`, | ||
| commitSha, | ||
| }, | ||
| deployment: { | ||
| environment: "production", | ||
| workerName: "noema", | ||
| trafficPercentage: 100, | ||
| workflowRunUrl: `https://github.com/${repository}/actions/runs/123`, | ||
| deployedAt: "2026-08-04T00:00:01.000Z", | ||
| deploymentCreatedAt: "2026-08-04T00:00:02.000Z", | ||
| }, | ||
| validation: { | ||
| immutableRelease: true, | ||
| strictKpi: true, | ||
| smokePassed: true, | ||
| kpiExecutedAt: "2026-08-03T23:59:50.000Z", | ||
| smokeTimestamp: "2026-08-04T00:00:04.000Z", | ||
| }, | ||
| }, | ||
| deploymentEvidenceSha256, | ||
| governanceEvidence: { | ||
| schema_version: 1, | ||
| repository, | ||
| environment: "production", | ||
| status: "PASS", | ||
| reviewer_count: 1, | ||
| reviewers: [{ type: "Team", id: 42, identifier: "production-approvers" }], | ||
| checks: [{ name: "reviewed", pass: true, detail: "reviewed" }], | ||
| failures: [], | ||
| }, | ||
| attestationBundle: { | ||
| mediaType: "application/vnd.dev.sigstore.bundle.v0.3+json", | ||
| verificationMaterial: { tlogEntries: [{}] }, | ||
| dsseEnvelope: { payload: "ZXZpZGVuY2U=", signatures: [{ sig: "c2ln" }] }, | ||
| }, | ||
| verificationReceipt: { | ||
| schemaVersion: 1, | ||
| verified: true, | ||
| repository, | ||
| releaseTag, | ||
| commitSha, | ||
| deploymentEvidenceSha256, | ||
| signerWorkflow: `${repository}/.github/workflows/cd.yml`, | ||
| predicateType: "https://contextualwisdomlab.org/attestations/noema-deployment/v1", | ||
| oidcIssuer: "https://token.actions.githubusercontent.com", | ||
| denySelfHostedRunners: true, | ||
| workflowRunUrl: `https://github.com/${repository}/actions/runs/123`, | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| function codes(input: ReturnType<typeof fixture>) { | ||
| return evaluateAcquisitionDeploymentEvidence(input).failures.map((entry) => entry.code); | ||
| } | ||
|
|
||
| describe("acquisition deployment temporal evidence integrity", () => { | ||
| it.each([ | ||
| ["generatedAt", (input: ReturnType<typeof fixture>) => { | ||
| input.deploymentEvidence.generatedAt = "2999-01-01T00:00:00.000Z"; | ||
| }, "deployment_generated_at_invalid"], | ||
| ["deployedAt", (input: ReturnType<typeof fixture>) => { | ||
| input.deploymentEvidence.deployment.deployedAt = "2999-01-01T00:00:00.000Z"; | ||
| }, "deployment_deployed_at_invalid"], | ||
| ["deploymentCreatedAt", (input: ReturnType<typeof fixture>) => { | ||
| input.deploymentEvidence.deployment.deploymentCreatedAt = "2999-01-01T00:00:00.000Z"; | ||
| }, "deployment_created_at_invalid"], | ||
| ["kpiExecutedAt", (input: ReturnType<typeof fixture>) => { | ||
| input.deploymentEvidence.validation.kpiExecutedAt = "2999-01-01T00:00:00.000Z"; | ||
| }, "deployment_kpi_timestamp_invalid"], | ||
| ["smokeTimestamp", (input: ReturnType<typeof fixture>) => { | ||
| input.deploymentEvidence.validation.smokeTimestamp = "2999-01-01T00:00:00.000Z"; | ||
| }, "deployment_smoke_timestamp_invalid"], | ||
| ])("rejects future-dated %s retained acquisition evidence", (_label, mutate, expectedCode) => { | ||
| const input = fixture(); | ||
| mutate(input); | ||
|
|
||
| const result = evaluateAcquisitionDeploymentEvidence(input); | ||
|
|
||
| expect(result.pass).toBe(false); | ||
| expect(codes(input)).toContain(expectedCode); | ||
| }); | ||
|
|
||
| it("does not coerce string traffic percentage into deployment authority", () => { | ||
| const input = fixture(); | ||
| (input.deploymentEvidence.deployment as { trafficPercentage: number | string }).trafficPercentage = "100"; | ||
|
|
||
| const result = evaluateAcquisitionDeploymentEvidence(input); | ||
|
|
||
| expect(result.pass).toBe(false); | ||
| expect(codes(input)).toContain("deployment_traffic_not_full"); | ||
| }); | ||
|
|
||
| it("does not coerce string reviewer counts into governance authority", () => { | ||
| const input = fixture(); | ||
| (input.governanceEvidence as { reviewer_count: number | string }).reviewer_count = "1"; | ||
|
|
||
| const result = evaluateAcquisitionDeploymentEvidence(input); | ||
|
|
||
| expect(result.pass).toBe(false); | ||
| expect(codes(input)).toContain("governance_reviewer_missing"); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { describe, expect, it } from "vitest"; | ||
| import { buildDeploymentEvidence } from "../scripts/deployment-evidence.mjs"; | ||
|
|
||
| const repository = "ContextualWisdomLab/noema"; | ||
| const commitSha = "a".repeat(40); | ||
| const workerVersionId = "v1-abc123"; | ||
|
|
||
| function validInput() { | ||
| return { | ||
| identity: { | ||
| repository, | ||
| releaseTag: "v0.1.0", | ||
| commitSha, | ||
| environment: "production", | ||
| workflowRunUrl: `${repository}/actions/runs/123`, | ||
| generatedAt: "2026-08-04T00:00:00.000Z", | ||
| }, | ||
| releaseView: { | ||
| isImmutable: true, | ||
| tagName: "v0.1.0", | ||
| url: `https://github.com/${repository}/releases/tag/v0.1.0`, | ||
| }, | ||
| releaseEvidence: { | ||
| schemaVersion: 1, | ||
| source: { | ||
| repository, | ||
| commitSha, | ||
| ref: "refs/tags/v0.1.0", | ||
| version: "0.1.0", | ||
| }, | ||
| }, | ||
| wranglerOutput: [{ | ||
| type: "deploy", | ||
| worker_name: "noema", | ||
| version_id: workerVersionId, | ||
| targets: ["https://noema.example.workers.dev"], | ||
| timestamp: "2026-08-04T00:00:01.000Z", | ||
| }], | ||
| beforeDeployments: [], | ||
| afterDeployments: [{ | ||
| id: "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", | ||
| created_on: "2026-08-04T00:00:02.000Z", | ||
| versions: [{ version_id: workerVersionId, percentage: 100 }], | ||
| }], | ||
| smokeEvidence: { | ||
| passed: true, | ||
| timestamp: "2026-08-04T00:00:04Z", | ||
| noema_exchange_url: "https://noema.example.workers.dev/exchange", | ||
| }, | ||
| kpiEvidence: { | ||
| status: "PASS", | ||
| strict: true, | ||
| requireWindowDays: 30, | ||
| executedAt: "2026-08-03T23:59:50.000Z", | ||
| }, | ||
| digests: { | ||
| releaseEvidenceSha256: "1".repeat(64), | ||
| smokeEvidenceSha256: "2".repeat(64), | ||
| kpiEvidenceSha256: "3".repeat(64), | ||
| }, | ||
| }; | ||
| } | ||
|
|
||
| describe("deployment authority types", () => { | ||
| it("rejects string-coerced 100 percent deployment status", () => { | ||
| const input = validInput(); | ||
| (input.afterDeployments[0].versions[0] as { percentage: number | string }).percentage = "100"; | ||
|
|
||
| expect(() => buildDeploymentEvidence(input)).toThrow("100%"); | ||
| }); | ||
|
|
||
| it("rejects a string-coerced KPI window", () => { | ||
| const input = validInput(); | ||
| (input.kpiEvidence as { requireWindowDays: number | string }).requireWindowDays = "30"; | ||
|
|
||
| expect(() => buildDeploymentEvidence(input)).toThrow("KPI evidence"); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📝 Info: Coercion removal aligns with numeric producers
The stricter checks (
match.percentage !== 100,Number.isSafeInteger(requireWindowDays),trafficPercentage === 100, safe-integerreviewer_count) reject strings. The producers all emit numbers:scripts/kpi-gate.mjs:44writes a numeric window,scripts/lib/production-environment-governance.mjs:159setsreviewer_countfromreviewers.length, and the builder emitstrafficPercentage: 100. Legitimate evidence stays accepted.Was this helpful? React with 👍 or 👎 to provide feedback.