forked from
npmx.dev/npmx.dev
[READ-ONLY]
a fast, modern browser for the npm registry
1import { describe, expect, it } from 'vitest'
2import { parseAttestationToProvenanceDetails } from '../../../../server/utils/provenance'
3
4const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1'
5const SLSA_PROVENANCE_V0_2 = 'https://slsa.dev/provenance/v0.2'
6const SIGSTORE_SEARCH_BASE = 'https://search.sigstore.dev'
7
8function encodePayload(payload: object): string {
9 return Buffer.from(JSON.stringify(payload)).toString('base64')
10}
11
12describe('parseAttestationToProvenanceDetails', () => {
13 it('returns null for non-object input', () => {
14 expect(parseAttestationToProvenanceDetails(null)).toBeNull()
15 expect(parseAttestationToProvenanceDetails(undefined)).toBeNull()
16 expect(parseAttestationToProvenanceDetails('string')).toBeNull()
17 })
18
19 it('returns null when attestations is not an array', () => {
20 expect(parseAttestationToProvenanceDetails({})).toBeNull()
21 expect(parseAttestationToProvenanceDetails({ attestations: 'not-array' })).toBeNull()
22 expect(parseAttestationToProvenanceDetails({ attestations: null })).toBeNull()
23 })
24
25 it('returns null when no SLSA attestation is found', () => {
26 expect(parseAttestationToProvenanceDetails({ attestations: [] })).toBeNull()
27 expect(
28 parseAttestationToProvenanceDetails({
29 attestations: [{ predicateType: 'https://other.predicate/v1' }],
30 }),
31 ).toBeNull()
32 })
33
34 it('returns null when attestation has no dsseEnvelope', () => {
35 expect(
36 parseAttestationToProvenanceDetails({
37 attestations: [{ predicateType: SLSA_PROVENANCE_V1 }],
38 }),
39 ).toBeNull()
40 expect(
41 parseAttestationToProvenanceDetails({
42 attestations: [{ predicateType: SLSA_PROVENANCE_V1, bundle: {} }],
43 }),
44 ).toBeNull()
45 })
46
47 it('returns null when payload cannot be decoded', () => {
48 expect(
49 parseAttestationToProvenanceDetails({
50 attestations: [
51 {
52 predicateType: SLSA_PROVENANCE_V1,
53 bundle: { dsseEnvelope: { payload: 'totally-not-base64' } },
54 },
55 ],
56 }),
57 ).toBeNull()
58 })
59
60 it('returns null when payload has no predicate', () => {
61 expect(
62 parseAttestationToProvenanceDetails({
63 attestations: [
64 {
65 predicateType: SLSA_PROVENANCE_V1,
66 bundle: { dsseEnvelope: { payload: encodePayload({}) } },
67 },
68 ],
69 }),
70 ).toBeNull()
71 })
72
73 it('parses GitHub Actions v1 attestation', () => {
74 const result = parseAttestationToProvenanceDetails({
75 attestations: [
76 {
77 predicateType: SLSA_PROVENANCE_V1,
78 bundle: {
79 dsseEnvelope: {
80 payload: encodePayload({
81 predicate: {
82 buildDefinition: {
83 externalParameters: {
84 workflow: {
85 repository: 'https://github.com/owner/repo',
86 path: '.github/workflows/publish.yml',
87 ref: 'refs/heads/main',
88 },
89 },
90 resolvedDependencies: [
91 {
92 uri: 'git+https://github.com/owner/repo',
93 digest: { gitCommit: 'abc123def456' },
94 },
95 ],
96 },
97 runDetails: {
98 builder: { id: 'https://github.com/actions/runner/github-hosted' },
99 metadata: { invocationId: 'https://github.com/owner/repo/actions/runs/12345' },
100 },
101 },
102 }),
103 },
104 verificationMaterial: {
105 tlogEntries: [{ logIndex: '98765' }],
106 },
107 },
108 },
109 ],
110 })
111
112 expect(result).toEqual({
113 provider: 'github',
114 providerLabel: 'GitHub Actions',
115 buildSummaryUrl: 'https://github.com/owner/repo/actions/runs/12345',
116 sourceCommitUrl: 'https://github.com/owner/repo/commit/abc123def456',
117 sourceCommitSha: 'abc123def456',
118 buildFileUrl: 'https://github.com/owner/repo/blob/main/.github/workflows/publish.yml',
119 buildFilePath: '.github/workflows/publish.yml',
120 publicLedgerUrl: `${SIGSTORE_SEARCH_BASE}/?logIndex=98765`,
121 })
122 })
123
124 it('parses GitLab CI attestation with project-specific runner', () => {
125 const result = parseAttestationToProvenanceDetails({
126 attestations: [
127 {
128 predicateType: SLSA_PROVENANCE_V1,
129 bundle: {
130 dsseEnvelope: {
131 payload: encodePayload({
132 predicate: {
133 buildDefinition: {
134 externalParameters: {
135 workflow: {
136 repository: 'https://gitlab.com/group/project',
137 path: '.gitlab-ci.yml',
138 ref: 'refs/tags/v1.0.0',
139 },
140 },
141 resolvedDependencies: [
142 {
143 digest: { gitCommit: 'f00f00' },
144 },
145 ],
146 },
147 runDetails: {
148 builder: { id: 'https://gitlab.com/group/project/-/runners/12345' },
149 metadata: { invocationId: 'https://gitlab.com/group/project/-/jobs/999' },
150 },
151 },
152 }),
153 },
154 },
155 },
156 ],
157 })
158
159 expect(result).toEqual({
160 provider: 'gitlab',
161 providerLabel: 'GitLab CI',
162 buildSummaryUrl: 'https://gitlab.com/group/project/-/jobs/999',
163 sourceCommitUrl: 'https://gitlab.com/group/project/-/commit/f00f00',
164 sourceCommitSha: 'f00f00',
165 buildFileUrl: 'https://gitlab.com/group/project/-/blob/v1.0.0/.gitlab-ci.yml',
166 buildFilePath: '.gitlab-ci.yml',
167 publicLedgerUrl: undefined,
168 })
169 })
170
171 it('falls back to v0.2 attestation when v1 is not available', () => {
172 const result = parseAttestationToProvenanceDetails({
173 attestations: [
174 {
175 predicateType: SLSA_PROVENANCE_V0_2,
176 bundle: {
177 dsseEnvelope: {
178 payload: encodePayload({
179 predicate: {
180 builder: { id: 'https://github.com/actions/runner' },
181 metadata: { buildInvocationId: 'https://github.com/owner/repo/actions/runs/555' },
182 },
183 }),
184 },
185 verificationMaterial: {
186 tlogEntries: [{ logIndex: '11111' }],
187 },
188 },
189 },
190 ],
191 })
192
193 expect(result).toEqual({
194 provider: 'github',
195 providerLabel: 'GitHub Actions',
196 buildSummaryUrl: 'https://github.com/owner/repo/actions/runs/555',
197 sourceCommitUrl: undefined,
198 sourceCommitSha: undefined,
199 buildFileUrl: undefined,
200 buildFilePath: undefined,
201 publicLedgerUrl: `${SIGSTORE_SEARCH_BASE}/?logIndex=11111`,
202 })
203 })
204
205 it('prefers v1 attestation over v0.2', () => {
206 const result = parseAttestationToProvenanceDetails({
207 attestations: [
208 {
209 predicateType: SLSA_PROVENANCE_V0_2,
210 bundle: {
211 dsseEnvelope: {
212 payload: encodePayload({
213 predicate: {
214 builder: { id: 'https://github.com/actions/runner' },
215 },
216 }),
217 },
218 },
219 },
220 {
221 predicateType: SLSA_PROVENANCE_V1,
222 bundle: {
223 dsseEnvelope: {
224 payload: encodePayload({
225 predicate: {
226 runDetails: {
227 builder: { id: 'https://gitlab.com/group/project/-/runners/1' },
228 },
229 },
230 }),
231 },
232 },
233 },
234 ],
235 })
236
237 expect(result).toEqual(
238 expect.objectContaining({
239 provider: 'gitlab',
240 providerLabel: 'GitLab CI',
241 }),
242 )
243 })
244
245 it('returns unknown provider for unrecognized builder ID', () => {
246 const result = parseAttestationToProvenanceDetails({
247 attestations: [
248 {
249 predicateType: SLSA_PROVENANCE_V1,
250 bundle: {
251 dsseEnvelope: {
252 payload: encodePayload({
253 predicate: {
254 runDetails: {
255 builder: { id: 'https://james-crazy-fake-ci.43081j.com/builder' },
256 },
257 },
258 }),
259 },
260 },
261 },
262 ],
263 })
264
265 expect(result).toEqual(
266 expect.objectContaining({
267 provider: 'unknown',
268 providerLabel: 'CI',
269 }),
270 )
271 })
272
273 it('returns Unknown label when builder ID is empty', () => {
274 const result = parseAttestationToProvenanceDetails({
275 attestations: [
276 {
277 predicateType: SLSA_PROVENANCE_V1,
278 bundle: {
279 dsseEnvelope: {
280 payload: encodePayload({
281 predicate: {},
282 }),
283 },
284 },
285 },
286 ],
287 })
288
289 expect(result).toEqual(
290 expect.objectContaining({
291 provider: 'unknown',
292 providerLabel: 'Unknown',
293 }),
294 )
295 })
296
297 it('normalizes repository URL by removing trailing slash and .git', () => {
298 const result = parseAttestationToProvenanceDetails({
299 attestations: [
300 {
301 predicateType: SLSA_PROVENANCE_V1,
302 bundle: {
303 dsseEnvelope: {
304 payload: encodePayload({
305 predicate: {
306 buildDefinition: {
307 externalParameters: {
308 workflow: {
309 repository: 'https://github.com/owner/repo.git/',
310 path: 'workflow.yml',
311 },
312 },
313 resolvedDependencies: [
314 {
315 digest: { gitCommit: 'abc123' },
316 },
317 ],
318 },
319 runDetails: {
320 builder: { id: 'https://github.com/actions/runner' },
321 },
322 },
323 }),
324 },
325 },
326 },
327 ],
328 })
329
330 expect(result?.sourceCommitUrl).toBe('https://github.com/owner/repo/commit/abc123')
331 expect(result?.buildFileUrl).toBe('https://github.com/owner/repo/blob/main/workflow.yml')
332 })
333
334 it('uses ref from workflow for build file URL', () => {
335 const result = parseAttestationToProvenanceDetails({
336 attestations: [
337 {
338 predicateType: SLSA_PROVENANCE_V1,
339 bundle: {
340 dsseEnvelope: {
341 payload: encodePayload({
342 predicate: {
343 buildDefinition: {
344 externalParameters: {
345 workflow: {
346 repository: 'https://github.com/owner/repo',
347 path: 'ci.yml',
348 ref: 'refs/tags/v2.0.0',
349 },
350 },
351 },
352 runDetails: {
353 builder: { id: 'https://github.com/actions/runner' },
354 },
355 },
356 }),
357 },
358 },
359 },
360 ],
361 })
362
363 expect(result?.buildFileUrl).toBe('https://github.com/owner/repo/blob/v2.0.0/ci.yml')
364 })
365
366 it('does not set buildSummaryUrl for non-URL invocation IDs', () => {
367 const result = parseAttestationToProvenanceDetails({
368 attestations: [
369 {
370 predicateType: SLSA_PROVENANCE_V1,
371 bundle: {
372 dsseEnvelope: {
373 payload: encodePayload({
374 predicate: {
375 runDetails: {
376 builder: { id: 'https://github.com/actions/runner' },
377 metadata: { invocationId: 'not-a-url-just-an-id' },
378 },
379 },
380 }),
381 },
382 },
383 },
384 ],
385 })
386
387 expect(result?.buildSummaryUrl).toBeUndefined()
388 })
389
390 it('generates generic commit URL for non-GitHub/GitLab repositories', () => {
391 const result = parseAttestationToProvenanceDetails({
392 attestations: [
393 {
394 predicateType: SLSA_PROVENANCE_V1,
395 bundle: {
396 dsseEnvelope: {
397 payload: encodePayload({
398 predicate: {
399 buildDefinition: {
400 externalParameters: {
401 workflow: {
402 repository: 'https://bitbucket.org/owner/repo',
403 path: 'pipeline.yml',
404 },
405 },
406 resolvedDependencies: [
407 {
408 digest: { gitCommit: 'abc123' },
409 },
410 ],
411 },
412 runDetails: {
413 builder: { id: 'https://bitbucket.org/pipelines' },
414 },
415 },
416 }),
417 },
418 },
419 },
420 ],
421 })
422
423 expect(result?.sourceCommitUrl).toBe('https://bitbucket.org/owner/repo/commit/abc123')
424 expect(result?.buildFileUrl).toBe('https://bitbucket.org/owner/repo/blob/main/pipeline.yml')
425 })
426})