Skip to content

Commit d8dc133

Browse files
Mitch DennyCopilot
andcommitted
feat(audit): add --include-attestations flag to output sigstore bundles
Add a new --include-attestations flag for `npm audit signatures` that includes the full sigstore attestation bundles in JSON output. This enables downstream tooling to consume and further process attestation data (e.g. for policy engines, SBOMs, or custom verification). When used with `npm audit signatures --json --include-attestations`, the JSON output includes a `verified` array containing each package's name, version, and attestation bundles. Depends on npm/pacote#457 to expose the fetched attestation bundles on the manifest's _attestations property. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 94bfef5 commit d8dc133

6 files changed

Lines changed: 103 additions & 1 deletion

File tree

docs/lib/content/commands/npm-audit.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,16 @@ The `audit signatures` command will also verify the provenance attestations of d
4242
Because provenance attestations are such a new feature, security features may be added to (or changed in) the attestation format over time.
4343
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.
4444

45+
To include the full sigstore attestation bundles in JSON output, use:
46+
47+
```bash
48+
$ npm audit signatures --json --include-attestations
49+
```
50+
51+
This adds a `verified` array to the JSON output containing the attestation
52+
bundles (DSSE envelopes, verification material, and transparency log entries)
53+
for each verified package.
54+
4555
The npm CLI supports registry signatures and signing keys provided by any registry if the following conventions are followed:
4656

4757
1. Signatures are provided in the package's `packument` in each published version within the `dist` object:

lib/commands/audit.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ class Audit extends ArboristWorkspaceCmd {
1919
'include',
2020
'foreground-scripts',
2121
'ignore-scripts',
22+
'include-attestations',
2223
...super.params,
2324
]
2425

lib/utils/verify-signatures.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ class VerifySignatures {
1717
this.invalid = []
1818
this.missing = []
1919
this.checkedPackages = new Set()
20+
this.verified = []
2021
this.auditedWithKeysCount = 0
2122
this.verifiedSignatureCount = 0
2223
this.verifiedAttestationCount = 0
@@ -60,7 +61,11 @@ class VerifySignatures {
6061
}
6162

6263
if (this.npm.config.get('json')) {
63-
output.buffer({ invalid, missing })
64+
const result = { invalid, missing }
65+
if (this.npm.config.get('include-attestations')) {
66+
result.verified = this.verified
67+
}
68+
output.buffer(result)
6469
return
6570
}
6671
const end = process.hrtime.bigint()
@@ -88,6 +93,9 @@ class VerifySignatures {
8893
} else {
8994
output.standard(`${this.verifiedAttestationCount} packages have ${verifiedBold} attestations`)
9095
}
96+
if (!this.npm.config.get('include-attestations')) {
97+
output.standard('(use --json --include-attestations to view attestation details)')
98+
}
9199
output.standard()
92100
}
93101

@@ -350,6 +358,15 @@ class VerifySignatures {
350358
// signatures, but not all packages have provenance and publish attestations.
351359
if (attestations) {
352360
this.verifiedAttestationCount += 1
361+
if (this.npm.config.get('include-attestations')) {
362+
this.verified.push({
363+
name,
364+
version,
365+
location,
366+
registry,
367+
attestations,
368+
})
369+
}
353370
}
354371
} catch (e) {
355372
if (e.code === 'EINTEGRITYSIGNATURE' || e.code === 'EATTESTATIONVERIFY') {

tap-snapshots/test/lib/commands/audit.js.test.cjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ audited 1 package in xxx
305305
1 package has a verified registry signature
306306
307307
1 package has a verified attestation
308+
(use --json --include-attestations to view attestation details)
308309
`
309310

310311
exports[`test/lib/commands/audit.js TAP audit signatures with valid signatures > must match snapshot 1`] = `

test/lib/commands/audit.js

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1850,9 +1850,71 @@ t.test('audit signatures', async t => {
18501850

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

1857+
t.test('with valid attestations --json --include-attestations', async t => {
1858+
const { npm, joinedOutput } = await loadMockNpm(t, {
1859+
prefixDir: installWithValidAttestations,
1860+
config: {
1861+
json: true,
1862+
'include-attestations': true,
1863+
},
1864+
mocks: {
1865+
pacote: t.mock('pacote', {
1866+
sigstore: { verify: async () => true },
1867+
}),
1868+
},
1869+
})
1870+
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
1871+
await manifestWithValidAttestations({ registry })
1872+
const fixture = fs.readFileSync(
1873+
path.resolve(__dirname, '../../fixtures/sigstore/valid-sigstore-attestations.json'),
1874+
'utf8'
1875+
)
1876+
registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture)
1877+
mockTUF({ npm, target: TUF_VALID_KEYS_TARGET })
1878+
1879+
await npm.exec('audit', ['signatures'])
1880+
1881+
t.notOk(process.exitCode, 'should exit successfully')
1882+
const jsonOutput = JSON.parse(joinedOutput())
1883+
t.ok(jsonOutput.verified, 'should include verified array')
1884+
t.equal(jsonOutput.verified.length, 1, 'should have one verified package')
1885+
t.equal(jsonOutput.verified[0].name, 'sigstore', 'should have correct package name')
1886+
t.equal(jsonOutput.verified[0].version, '1.0.0', 'should have correct version')
1887+
t.ok(jsonOutput.verified[0].attestations, 'should include attestations')
1888+
})
1889+
1890+
t.test('with valid attestations --json without --include-attestations', async t => {
1891+
const { npm, joinedOutput } = await loadMockNpm(t, {
1892+
prefixDir: installWithValidAttestations,
1893+
config: {
1894+
json: true,
1895+
},
1896+
mocks: {
1897+
pacote: t.mock('pacote', {
1898+
sigstore: { verify: async () => true },
1899+
}),
1900+
},
1901+
})
1902+
const registry = new MockRegistry({ tap: t, registry: npm.config.get('registry') })
1903+
await manifestWithValidAttestations({ registry })
1904+
const fixture = fs.readFileSync(
1905+
path.resolve(__dirname, '../../fixtures/sigstore/valid-sigstore-attestations.json'),
1906+
'utf8'
1907+
)
1908+
registry.nock.get('/-/npm/v1/attestations/sigstore@1.0.0').reply(200, fixture)
1909+
mockTUF({ npm, target: TUF_VALID_KEYS_TARGET })
1910+
1911+
await npm.exec('audit', ['signatures'])
1912+
1913+
t.notOk(process.exitCode, 'should exit successfully')
1914+
const jsonOutput = JSON.parse(joinedOutput())
1915+
t.notOk(jsonOutput.verified, 'should not include verified array')
1916+
})
1917+
18561918
t.test('with keyless attestations and no registry keys', async t => {
18571919
const { npm, joinedOutput } = await loadMockNpm(t, {
18581920
prefixDir: installWithValidAttestations,

workspaces/config/lib/definitions/definitions.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,17 @@ const definitions = {
946946
`,
947947
flatten,
948948
}),
949+
'include-attestations': new Definition('include-attestations', {
950+
default: false,
951+
type: Boolean,
952+
description: `
953+
When used with \`npm audit signatures --json\`, includes the full
954+
sigstore attestation bundles in the JSON output for each verified
955+
package. The bundles contain DSSE envelopes, verification material,
956+
and transparency log entries.
957+
`,
958+
flatten,
959+
}),
949960
'init-author-email': new Definition('init-author-email', {
950961
default: '',
951962
hint: '<email>',

0 commit comments

Comments
 (0)