diff --git a/workspaces/libnpmpublish/lib/provenance.js b/workspaces/libnpmpublish/lib/provenance.js index d11d210478b65..1eb870da5f24f 100644 --- a/workspaces/libnpmpublish/lib/provenance.js +++ b/workspaces/libnpmpublish/lib/provenance.js @@ -4,39 +4,40 @@ const INTOTO_PAYLOAD_TYPE = 'application/vnd.in-toto+json' const INTOTO_STATEMENT_TYPE = 'https://in-toto.io/Statement/v0.1' const SLSA_PREDICATE_TYPE = 'https://slsa.dev/provenance/v0.2' -const BUILDER_ID_PREFIX = 'https://github.com/npm/cli' +const BUILDER_ID = 'https://github.com/actions/runner' const BUILD_TYPE_PREFIX = 'https://github.com/npm/cli/gha' -const BUILD_TYPE_VERSION = 'v1' +const BUILD_TYPE_VERSION = 'v2' const generateProvenance = async (subject, opts) => { const { env } = process + /* istanbul ignore next - not covering missing env var case */ + const [workflowPath] = (env.GITHUB_WORKFLOW_REF || '') + .replace(env.GITHUB_REPOSITORY + '/', '') + .split('@') const payload = { _type: INTOTO_STATEMENT_TYPE, subject, predicateType: SLSA_PREDICATE_TYPE, predicate: { - buildType: `${BUILD_TYPE_PREFIX}@${BUILD_TYPE_VERSION}`, - builder: { id: `${BUILDER_ID_PREFIX}@${opts.npmVersion}` }, + buildType: `${BUILD_TYPE_PREFIX}/${BUILD_TYPE_VERSION}`, + builder: { id: BUILDER_ID }, invocation: { configSource: { uri: `git+${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}@${env.GITHUB_REF}`, digest: { sha1: env.GITHUB_SHA, }, - entryPoint: env.GITHUB_WORKFLOW_REF, + entryPoint: workflowPath, }, parameters: {}, environment: { - GITHUB_ACTOR_ID: env.GITHUB_ACTOR_ID, GITHUB_EVENT_NAME: env.GITHUB_EVENT_NAME, GITHUB_REF: env.GITHUB_REF, - GITHUB_REF_TYPE: env.GITHUB_REF_TYPE, GITHUB_REPOSITORY: env.GITHUB_REPOSITORY, GITHUB_REPOSITORY_ID: env.GITHUB_REPOSITORY_ID, GITHUB_REPOSITORY_OWNER_ID: env.GITHUB_REPOSITORY_OWNER_ID, GITHUB_RUN_ATTEMPT: env.GITHUB_RUN_ATTEMPT, GITHUB_RUN_ID: env.GITHUB_RUN_ID, - GITHUB_RUN_NUMBER: env.GITHUB_RUN_NUMBER, GITHUB_SHA: env.GITHUB_SHA, GITHUB_WORKFLOW_REF: env.GITHUB_WORKFLOW_REF, GITHUB_WORKFLOW_SHA: env.GITHUB_WORKFLOW_SHA, @@ -53,7 +54,7 @@ const generateProvenance = async (subject, opts) => { }, materials: [ { - uri: `git+${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}`, + uri: `git+${env.GITHUB_SERVER_URL}/${env.GITHUB_REPOSITORY}@${env.GITHUB_REF}`, digest: { sha1: env.GITHUB_SHA, }, diff --git a/workspaces/libnpmpublish/test/publish.js b/workspaces/libnpmpublish/test/publish.js index d64952c189403..587ce81153ebe 100644 --- a/workspaces/libnpmpublish/test/publish.js +++ b/workspaces/libnpmpublish/test/publish.js @@ -599,8 +599,17 @@ t.test('other error code', async t => { }) t.test('publish existing package with provenance in gha', async t => { + // Environment variables const oidcURL = 'https://mock.oidc' const requestToken = 'decafbad' + const workflowPath = '.github/workflows/publish.yml' + const repository = 'github/foo' + const serverUrl = 'https://github.com' + const ref = 'refs/heads/main' + const sha = 'deadbeef' + const runID = '123456' + const runAttempt = '1' + // Set-up GHA environment variables mockGlobals(t, { 'process.env': { @@ -608,8 +617,29 @@ t.test('publish existing package with provenance in gha', async t => { GITHUB_ACTIONS: true, ACTIONS_ID_TOKEN_REQUEST_URL: oidcURL, ACTIONS_ID_TOKEN_REQUEST_TOKEN: requestToken, + GITHUB_WORKFLOW_REF: `${repository}/${workflowPath}@${ref}`, + GITHUB_REPOSITORY: repository, + GITHUB_SERVER_URL: serverUrl, + GITHUB_REF: ref, + GITHUB_SHA: sha, + GITHUB_RUN_ID: runID, + GITHUB_RUN_ATTEMPT: runAttempt, }, }) + + const expectedSubject = { + name: 'pkg:npm/%40npmcli/libnpmpublish-test@1.0.0', + digest: { + sha512: integrity.sha512[0].hexDigest(), + }, + } + + const expectedConfigSource = { + uri: `git+${serverUrl}/${repository}@${ref}`, + digest: { sha1: sha }, + entryPoint: workflowPath, + } + const { publish } = t.mock('..', { 'ci-info': t.mock('ci-info') }) const registry = new MockRegistry({ tap: t, @@ -732,7 +762,24 @@ t.test('publish existing package with provenance in gha', async t => { registry.getVisibility({ spec, visibility: { public: true } }) registry.nock.put(`/${spec.escapedName}`, body => { - return t.match(body, packument, 'posted packument matches expectations') + const bundleAttachment = body._attachments['@npmcli/libnpmpublish-test-1.0.0.sigstore'] + const bundle = JSON.parse(bundleAttachment.data) + const provenance = JSON.parse(Buffer.from(bundle.dsseEnvelope.payload, 'base64').toString()) + + t.hasStrict(body, packument, 'posted packument matches expectations') + t.hasStrict(provenance.subject[0], + expectedSubject, + 'provenance subject matches expectations') + t.hasStrict(provenance.predicate.buildType, + 'https://github.com/npm/cli/gha/v2', + 'buildType matches expectations') + t.hasStrict(provenance.predicate.builder.id, + 'https://github.com/actions/runner', + 'builder id matches expectations') + t.hasStrict(provenance.predicate.invocation.configSource, + expectedConfigSource, + 'configSource matches expectations') + return true }).reply(201, {}) const ret = await publish(manifest, tarData, {