[READ-ONLY] a fast, modern browser for the npm registry
at main 426 lines 13 kB view raw
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})