diff --git a/test/unit/server/utils/provenance.spec.ts b/test/unit/server/utils/provenance.spec.ts new file mode 100644 index 0000000000..50e07ad879 --- /dev/null +++ b/test/unit/server/utils/provenance.spec.ts @@ -0,0 +1,426 @@ +import { describe, expect, it } from 'vitest' +import { parseAttestationToProvenanceDetails } from '../../../../server/utils/provenance' + +const SLSA_PROVENANCE_V1 = 'https://slsa.dev/provenance/v1' +const SLSA_PROVENANCE_V0_2 = 'https://slsa.dev/provenance/v0.2' +const SIGSTORE_SEARCH_BASE = 'https://search.sigstore.dev' + +function encodePayload(payload: object): string { + return Buffer.from(JSON.stringify(payload)).toString('base64') +} + +describe('parseAttestationToProvenanceDetails', () => { + it('returns null for non-object input', () => { + expect(parseAttestationToProvenanceDetails(null)).toBeNull() + expect(parseAttestationToProvenanceDetails(undefined)).toBeNull() + expect(parseAttestationToProvenanceDetails('string')).toBeNull() + }) + + it('returns null when attestations is not an array', () => { + expect(parseAttestationToProvenanceDetails({})).toBeNull() + expect(parseAttestationToProvenanceDetails({ attestations: 'not-array' })).toBeNull() + expect(parseAttestationToProvenanceDetails({ attestations: null })).toBeNull() + }) + + it('returns null when no SLSA attestation is found', () => { + expect(parseAttestationToProvenanceDetails({ attestations: [] })).toBeNull() + expect( + parseAttestationToProvenanceDetails({ + attestations: [{ predicateType: 'https://other.predicate/v1' }], + }), + ).toBeNull() + }) + + it('returns null when attestation has no dsseEnvelope', () => { + expect( + parseAttestationToProvenanceDetails({ + attestations: [{ predicateType: SLSA_PROVENANCE_V1 }], + }), + ).toBeNull() + expect( + parseAttestationToProvenanceDetails({ + attestations: [{ predicateType: SLSA_PROVENANCE_V1, bundle: {} }], + }), + ).toBeNull() + }) + + it('returns null when payload cannot be decoded', () => { + expect( + parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { dsseEnvelope: { payload: 'totally-not-base64' } }, + }, + ], + }), + ).toBeNull() + }) + + it('returns null when payload has no predicate', () => { + expect( + parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { dsseEnvelope: { payload: encodePayload({}) } }, + }, + ], + }), + ).toBeNull() + }) + + it('parses GitHub Actions v1 attestation', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + buildDefinition: { + externalParameters: { + workflow: { + repository: 'https://github.com/owner/repo', + path: '.github/workflows/publish.yml', + ref: 'refs/heads/main', + }, + }, + resolvedDependencies: [ + { + uri: 'git+https://github.com/owner/repo', + digest: { gitCommit: 'abc123def456' }, + }, + ], + }, + runDetails: { + builder: { id: 'https://github.com/actions/runner/github-hosted' }, + metadata: { invocationId: 'https://github.com/owner/repo/actions/runs/12345' }, + }, + }, + }), + }, + verificationMaterial: { + tlogEntries: [{ logIndex: '98765' }], + }, + }, + }, + ], + }) + + expect(result).toEqual({ + provider: 'github', + providerLabel: 'GitHub Actions', + buildSummaryUrl: 'https://github.com/owner/repo/actions/runs/12345', + sourceCommitUrl: 'https://github.com/owner/repo/commit/abc123def456', + sourceCommitSha: 'abc123def456', + buildFileUrl: 'https://github.com/owner/repo/blob/main/.github/workflows/publish.yml', + buildFilePath: '.github/workflows/publish.yml', + publicLedgerUrl: `${SIGSTORE_SEARCH_BASE}/?logIndex=98765`, + }) + }) + + it('parses GitLab CI attestation with project-specific runner', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + buildDefinition: { + externalParameters: { + workflow: { + repository: 'https://gitlab.com/group/project', + path: '.gitlab-ci.yml', + ref: 'refs/tags/v1.0.0', + }, + }, + resolvedDependencies: [ + { + digest: { gitCommit: 'f00f00' }, + }, + ], + }, + runDetails: { + builder: { id: 'https://gitlab.com/group/project/-/runners/12345' }, + metadata: { invocationId: 'https://gitlab.com/group/project/-/jobs/999' }, + }, + }, + }), + }, + }, + }, + ], + }) + + expect(result).toEqual({ + provider: 'gitlab', + providerLabel: 'GitLab CI', + buildSummaryUrl: 'https://gitlab.com/group/project/-/jobs/999', + sourceCommitUrl: 'https://gitlab.com/group/project/-/commit/f00f00', + sourceCommitSha: 'f00f00', + buildFileUrl: 'https://gitlab.com/group/project/-/blob/v1.0.0/.gitlab-ci.yml', + buildFilePath: '.gitlab-ci.yml', + publicLedgerUrl: undefined, + }) + }) + + it('falls back to v0.2 attestation when v1 is not available', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V0_2, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + builder: { id: 'https://github.com/actions/runner' }, + metadata: { buildInvocationId: 'https://github.com/owner/repo/actions/runs/555' }, + }, + }), + }, + verificationMaterial: { + tlogEntries: [{ logIndex: '11111' }], + }, + }, + }, + ], + }) + + expect(result).toEqual({ + provider: 'github', + providerLabel: 'GitHub Actions', + buildSummaryUrl: 'https://github.com/owner/repo/actions/runs/555', + sourceCommitUrl: undefined, + sourceCommitSha: undefined, + buildFileUrl: undefined, + buildFilePath: undefined, + publicLedgerUrl: `${SIGSTORE_SEARCH_BASE}/?logIndex=11111`, + }) + }) + + it('prefers v1 attestation over v0.2', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V0_2, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + builder: { id: 'https://github.com/actions/runner' }, + }, + }), + }, + }, + }, + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + runDetails: { + builder: { id: 'https://gitlab.com/group/project/-/runners/1' }, + }, + }, + }), + }, + }, + }, + ], + }) + + expect(result).toEqual( + expect.objectContaining({ + provider: 'gitlab', + providerLabel: 'GitLab CI', + }), + ) + }) + + it('returns unknown provider for unrecognized builder ID', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + runDetails: { + builder: { id: 'https://james-crazy-fake-ci.43081j.com/builder' }, + }, + }, + }), + }, + }, + }, + ], + }) + + expect(result).toEqual( + expect.objectContaining({ + provider: 'unknown', + providerLabel: 'CI', + }), + ) + }) + + it('returns Unknown label when builder ID is empty', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: {}, + }), + }, + }, + }, + ], + }) + + expect(result).toEqual( + expect.objectContaining({ + provider: 'unknown', + providerLabel: 'Unknown', + }), + ) + }) + + it('normalizes repository URL by removing trailing slash and .git', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + buildDefinition: { + externalParameters: { + workflow: { + repository: 'https://github.com/owner/repo.git/', + path: 'workflow.yml', + }, + }, + resolvedDependencies: [ + { + digest: { gitCommit: 'abc123' }, + }, + ], + }, + runDetails: { + builder: { id: 'https://github.com/actions/runner' }, + }, + }, + }), + }, + }, + }, + ], + }) + + expect(result?.sourceCommitUrl).toBe('https://github.com/owner/repo/commit/abc123') + expect(result?.buildFileUrl).toBe('https://github.com/owner/repo/blob/main/workflow.yml') + }) + + it('uses ref from workflow for build file URL', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + buildDefinition: { + externalParameters: { + workflow: { + repository: 'https://github.com/owner/repo', + path: 'ci.yml', + ref: 'refs/tags/v2.0.0', + }, + }, + }, + runDetails: { + builder: { id: 'https://github.com/actions/runner' }, + }, + }, + }), + }, + }, + }, + ], + }) + + expect(result?.buildFileUrl).toBe('https://github.com/owner/repo/blob/v2.0.0/ci.yml') + }) + + it('does not set buildSummaryUrl for non-URL invocation IDs', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + runDetails: { + builder: { id: 'https://github.com/actions/runner' }, + metadata: { invocationId: 'not-a-url-just-an-id' }, + }, + }, + }), + }, + }, + }, + ], + }) + + expect(result?.buildSummaryUrl).toBeUndefined() + }) + + it('generates generic commit URL for non-GitHub/GitLab repositories', () => { + const result = parseAttestationToProvenanceDetails({ + attestations: [ + { + predicateType: SLSA_PROVENANCE_V1, + bundle: { + dsseEnvelope: { + payload: encodePayload({ + predicate: { + buildDefinition: { + externalParameters: { + workflow: { + repository: 'https://bitbucket.org/owner/repo', + path: 'pipeline.yml', + }, + }, + resolvedDependencies: [ + { + digest: { gitCommit: 'abc123' }, + }, + ], + }, + runDetails: { + builder: { id: 'https://bitbucket.org/pipelines' }, + }, + }, + }), + }, + }, + }, + ], + }) + + expect(result?.sourceCommitUrl).toBe('https://bitbucket.org/owner/repo/commit/abc123') + expect(result?.buildFileUrl).toBe('https://bitbucket.org/owner/repo/blob/main/pipeline.yml') + }) +})