Skip to content

Commit f92a69c

Browse files
committed
infra: fall back to Rekor integratedTime for build age
GitHub's attest-build-provenance action does not populate predicate.runDetails.metadata.finishedOn in the SLSA v1 payload, so the parser was always missing build age. Fall back to the Rekor transparency-log integratedTime (seconds since epoch) from the attestation bundle's verification material, which is signed within seconds of the build completing.
1 parent dd9ea88 commit f92a69c

1 file changed

Lines changed: 25 additions & 3 deletions

File tree

typescript/infra/src/utils/attestation.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ function extractFinishedOn(rawJson: string): Date | undefined {
6767
const parsed = JSON.parse(rawJson);
6868
const entries = Array.isArray(parsed) ? parsed : [parsed];
6969
for (const entry of entries) {
70-
const candidates: unknown[] = [
70+
// 1. SLSA provenance predicate (if present — not populated by
71+
// GitHub's attest-build-provenance, but other producers may)
72+
const statementCandidates: unknown[] = [
7173
entry?.verificationResult?.statement,
7274
entry?.statement,
7375
];
@@ -76,17 +78,37 @@ function extractFinishedOn(rawJson: string): Date | undefined {
7678
entry?.bundle?.dsseEnvelope?.payload;
7779
if (typeof dssePayload === 'string') {
7880
try {
79-
candidates.push(
81+
statementCandidates.push(
8082
JSON.parse(Buffer.from(dssePayload, 'base64').toString('utf8')),
8183
);
8284
} catch {
8385
// ignore, next candidate
8486
}
8587
}
86-
for (const c of candidates) {
88+
for (const c of statementCandidates) {
8789
const date = readFinishedOn(c);
8890
if (date) return date;
8991
}
92+
93+
// 2. Rekor transparency-log integratedTime — when the attestation
94+
// was signed (typically within seconds of build completion).
95+
const tlogEntries =
96+
entry?.attestation?.bundle?.verificationMaterial?.tlogEntries ??
97+
entry?.bundle?.verificationMaterial?.tlogEntries;
98+
if (Array.isArray(tlogEntries)) {
99+
for (const t of tlogEntries) {
100+
const raw = t?.integratedTime;
101+
const seconds =
102+
typeof raw === 'string'
103+
? Number(raw)
104+
: typeof raw === 'number'
105+
? raw
106+
: NaN;
107+
if (Number.isFinite(seconds) && seconds > 0) {
108+
return new Date(seconds * 1000);
109+
}
110+
}
111+
}
90112
}
91113
} catch {
92114
// ignore - treated as "verified but age unknown"

0 commit comments

Comments
 (0)