[READ-ONLY] a fast, modern browser for the npm registry

test: add provenance tests (#1087)

authored by

James Garbutt and committed by
GitHub
352ec4fb 588e1a1f

+426
+426
test/unit/server/utils/provenance.spec.ts
··· 1 + import { describe, expect, it } from 'vitest' 2 + import { parseAttestationToProvenanceDetails } from '../../../../server/utils/provenance' 3 + 4 + const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1' 5 + const SLSA_PROVENANCE_V0_2 = 'https://slsa.dev/provenance/v0.2' 6 + const SIGSTORE_SEARCH_BASE = 'https://search.sigstore.dev' 7 + 8 + function encodePayload(payload: object): string { 9 + return Buffer.from(JSON.stringify(payload)).toString('base64') 10 + } 11 + 12 + describe('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 + })