Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions docs/lib/content/commands/npm-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ The `audit signatures` command will also verify the provenance attestations of d
Because provenance attestations are such a new feature, security features may be added to (or changed in) the attestation format over time.
To ensure that you're always able to verify attestation signatures check that you're running the latest version of the npm CLI. Please note this often means updating npm beyond the version that ships with Node.js.

To include the full sigstore attestation bundles in JSON output, use:

```bash
$ npm audit signatures --json --include-attestations
```

This adds a `verified` array to the JSON output containing the attestation
bundles (DSSE envelopes, verification material, and transparency log entries)
for each verified package.

The npm CLI supports registry signatures and signing keys provided by any registry if the following conventions are followed:

1. Signatures are provided in the package's `packument` in each published version within the `dist` object:
Expand Down
1 change: 1 addition & 0 deletions lib/commands/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class Audit extends ArboristWorkspaceCmd {
'include',
'foreground-scripts',
'ignore-scripts',
'include-attestations',
...super.params,
]

Expand Down
19 changes: 18 additions & 1 deletion lib/utils/verify-signatures.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class VerifySignatures {
this.invalid = []
this.missing = []
this.checkedPackages = new Set()
this.verified = []
this.auditedWithKeysCount = 0
this.verifiedSignatureCount = 0
this.verifiedAttestationCount = 0
Expand Down Expand Up @@ -60,7 +61,11 @@ class VerifySignatures {
}

if (this.npm.config.get('json')) {
output.buffer({ invalid, missing })
const result = { invalid, missing }
if (this.npm.config.get('include-attestations')) {
result.verified = this.verified
}
output.buffer(result)
return
}
const end = process.hrtime.bigint()
Expand Down Expand Up @@ -88,6 +93,9 @@ class VerifySignatures {
} else {
output.standard(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`)
}
if (!this.npm.config.get('include-attestations')) {
output.standard('(use --json --include-attestations to view attestation details)')
}
output.standard()
}

Expand Down Expand Up @@ -350,6 +358,15 @@ class VerifySignatures {
// signatures, but not all packages have provenance and publish attestations.
if (attestations) {
this.verifiedAttestationCount += 1
if (this.npm.config.get('include-attestations')) {
this.verified.push({
name,
version,
location,
registry,
attestations,
})
}
}
} catch (e) {
if (e.code === 'EINTEGRITYSIGNATURE' || e.code === 'EATTESTATIONVERIFY') {
Expand Down
1 change: 1 addition & 0 deletions tap-snapshots/test/lib/commands/audit.js.test.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,7 @@ audited 1 package in xxx
1 package has a verified registry signature
1 package has a verified attestation
(use --json --include-attestations to view attestation details)
`

exports[`test/lib/commands/audit.js TAP audit signatures with valid signatures > must match snapshot 1`] = `
Expand Down
62 changes: 62 additions & 0 deletions test/lib/commands/audit.js
Original file line number Diff line number Diff line change
Expand Up @@ -1850,9 +1850,71 @@ t.test('audit signatures', async t => {

t.notOk(process.exitCode, 'should exit successfully')
t.match(joinedOutput(), /1 package has a verified attestation/)
t.match(joinedOutput(), /use --json --include-attestations to view attestation details/)
t.matchSnapshot(joinedOutput())
})

t.test('with valid attestations --json --include-attestations', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: installWithValidAttestations,
config: {
json: true,
'include-attestations': true,
},
mocks: {
pacote: t.mock('pacote', {
sigstore: { verify: async () => true },
}),
},
})
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
await manifestWithValidAttestations({ registry })
const fixture = fs.readFileSync(
path.resolve(__dirname, '../../fixtures/sigstore/valid-sigstore-attestations.json'),
'utf8'
)
registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture)
mockTUF({ npm, target: TUF_VALID_KEYS_TARGET })

await npm.exec('audit', ['signatures'])

t.notOk(process.exitCode, 'should exit successfully')
const jsonOutput = JSON.parse(joinedOutput())
t.ok(jsonOutput.verified, 'should include verified array')
t.equal(jsonOutput.verified.length, 1, 'should have one verified package')
t.equal(jsonOutput.verified[0].name, 'sigstore', 'should have correct package name')
t.equal(jsonOutput.verified[0].version, '1.0.0', 'should have correct version')
t.ok(jsonOutput.verified[0].attestations, 'should include attestations')
})

t.test('with valid attestations --json without --include-attestations', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: installWithValidAttestations,
config: {
json: true,
},
mocks: {
pacote: t.mock('pacote', {
sigstore: { verify: async () => true },
}),
},
})
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
await manifestWithValidAttestations({ registry })
const fixture = fs.readFileSync(
path.resolve(__dirname, '../../fixtures/sigstore/valid-sigstore-attestations.json'),
'utf8'
)
registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture)
mockTUF({ npm, target: TUF_VALID_KEYS_TARGET })

await npm.exec('audit', ['signatures'])

t.notOk(process.exitCode, 'should exit successfully')
const jsonOutput = JSON.parse(joinedOutput())
t.notOk(jsonOutput.verified, 'should not include verified array')
})

t.test('with keyless attestations and no registry keys', async t => {
const { npm, joinedOutput } = await loadMockNpm(t, {
prefixDir: installWithValidAttestations,
Expand Down
11 changes: 11 additions & 0 deletions workspaces/config/lib/definitions/definitions.js
Original file line number Diff line number Diff line change
Expand Up @@ -946,6 +946,17 @@ const definitions = {
`,
flatten,
}),
'include-attestations': new Definition('include-attestations', {
default: false,
type: Boolean,
description: `
When used with \`npm audit signatures --json\`, includes the full
sigstore attestation bundles in the JSON output for each verified
package. The bundles contain DSSE envelopes, verification material,
and transparency log entries.
`,
flatten,
}),
'init-author-email': new Definition('init-author-email', {
default: '',
hint: '<email>',
Expand Down