npq 3.17.0 → 3.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -139,7 +139,7 @@ Note: `npq` by default will offload all commands and their arguments to the `npm
139
139
  | license | Will show a warning if a package has been found without a license field | Checks the latest version for a license
140
140
  | expired domains | Will show a warning if a package has been found with one of its maintainers having an email address that includes an expired domain | Checks a dependency version for a maintainer with an expired domain
141
141
  | signatures | Will compare the package's signature as it shows on the registry's pakument with the keys published on the npmjs.com registry
142
- | provenance | Will verify the package's attestations of provenance metadata for the published package
142
+ | provenance | Will verify the package's attestations of provenance metadata for the published package, and **error** on [provenance regression](docs/feature/provenance.md) (an older semver had registry provenance metadata but the version you install does not)
143
143
  | version-maturity | Will show a warning if the specific version being installed was published less than 7 days ago | Helps identify recently published versions that may not have been reviewed by the community yet
144
144
  | newBin | Will show a warning if the package version being installed introduces a new command-line binary (via the `bin` field in `package.json`) that was not present in its previous version. | Helps identify potentially unexpected new executables being added to your `node_modules/.bin/` directory.
145
145
  | typosquatting | Will show a warning if the package name is similar to a popular package name, which could indicate a potential typosquatting attack. | Helps identify packages that may be trying to trick users into installing them by mimicking popular package names.
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- // const semver = require('semver')
3
+ const semver = require('semver')
4
4
  const BaseMarshall = require('./baseMarshall')
5
5
  const NpmRegistry = require('../helpers/npmRegistry')
6
6
  const Warning = require('../helpers/warning')
@@ -8,6 +8,62 @@ const { marshallCategories } = require('./constants')
8
8
 
9
9
  const MARSHALL_NAME = 'provenance'
10
10
 
11
+ /**
12
+ * Newest semver older than installedVersion whose packument entry includes dist.attestations.
13
+ * Uses only packageInfo already returned by getPackageInfo (no extra HTTP).
14
+ */
15
+ function findNewestPriorVersionWithDistAttestations(packageInfo, installedVersion) {
16
+ if (!packageInfo?.versions || !semver.valid(installedVersion)) {
17
+ return null
18
+ }
19
+
20
+ const older = Object.keys(packageInfo.versions)
21
+ .filter((v) => semver.valid(v) && semver.lt(v, installedVersion))
22
+ .sort(semver.rcompare)
23
+
24
+ for (const v of older) {
25
+ if (packageInfo.versions[v]?.dist?.attestations) {
26
+ return v
27
+ }
28
+ }
29
+
30
+ return null
31
+ }
32
+
33
+ /**
34
+ * Whether this error means the resolved version did not yield verifiable provenance,
35
+ * as opposed to infrastructure failures (manifest fetch, keys, attestations URL fetch).
36
+ */
37
+ function shouldConsiderProvenanceRegression(error) {
38
+ const msg = error && typeof error.message === 'string' ? error.message : ''
39
+ if (!msg) {
40
+ return false
41
+ }
42
+
43
+ if (msg.includes('Package has no attestations to verify')) {
44
+ return true
45
+ }
46
+
47
+ // Malformed checkpoint (#329) is handled in validate() before this runs.
48
+ if (error.code === 'EATTESTATIONVERIFY') {
49
+ return true
50
+ }
51
+
52
+ if (
53
+ error.code === 'EMISSINGSIGNATUREKEY' ||
54
+ error.code === 'EEXPIREDSIGNATUREKEY' ||
55
+ error.code === 'EATTESTATIONSUBJECT'
56
+ ) {
57
+ return true
58
+ }
59
+
60
+ if (msg.includes('failed to verify attestation')) {
61
+ return true
62
+ }
63
+
64
+ return false
65
+ }
66
+
11
67
  class Marshall extends BaseMarshall {
12
68
  constructor(options) {
13
69
  super(options)
@@ -19,6 +75,19 @@ class Marshall extends BaseMarshall {
19
75
  return 'Verifying package provenance'
20
76
  }
21
77
 
78
+ throwIfProvenanceRegression(validationMetadata) {
79
+ const prior = findNewestPriorVersionWithDistAttestations(
80
+ validationMetadata.packageInfo,
81
+ validationMetadata.version
82
+ )
83
+
84
+ if (prior) {
85
+ throw new Error(
86
+ `Provenance regression detected: published version ${prior} includes npm provenance metadata, but ${validationMetadata.name}@${validationMetadata.version} does not (or it could not be verified).`
87
+ )
88
+ }
89
+ }
90
+
22
91
  validate(pkg) {
23
92
  const validationMetadata = {}
24
93
  const npmRegistry = new NpmRegistry({
@@ -56,6 +125,7 @@ class Marshall extends BaseMarshall {
56
125
  })
57
126
  .then((metadata) => {
58
127
  if (!metadata || !metadata._attestations) {
128
+ this.throwIfProvenanceRegression(validationMetadata)
59
129
  throw new Warning('the package was published without any attestations')
60
130
  }
61
131
 
@@ -67,114 +137,42 @@ class Marshall extends BaseMarshall {
67
137
  // See: https://github.com/lirantal/npq/issues/329
68
138
  if (error.code === 'EATTESTATIONVERIFY' && error.message.includes('malformed checkpoint')) {
69
139
  return []
70
- } else {
71
- if (error.message.includes('Package has no attestations to verify')) {
72
- throw new Warning(
73
- `Unable to verify provenance: the package was published without any attestations`
74
- )
75
- } else {
76
- this.debug(
77
- '\nUnable to verify provenance for package %s@%s: %s',
78
- validationMetadata.name,
79
- validationMetadata.version,
80
- error.message
81
- )
82
- throw new Warning(`Unable to verify provenance`)
83
- }
84
140
  }
85
- })
86
- }
87
-
88
- /*
89
- async checkProvenanceRegression(validationMetadata) {
90
- const { packageInfo, name, version } = validationMetadata
91
-
92
- if (!packageInfo || !packageInfo.versions) {
93
- return // Cannot check regression without version information
94
- }
95
-
96
- const allVersions = Object.keys(packageInfo.versions)
97
- const validVersions = allVersions.filter(v => semver.valid(v))
98
-
99
- if (validVersions.length === 0) {
100
- return // No valid versions to compare
101
- }
102
141
 
103
- // Get all versions older than the target version
104
- const olderVersions = validVersions
105
- .filter(v => semver.lt(v, version))
106
- .sort(semver.rcompare) // Sort descending, newest first
107
-
108
- if (olderVersions.length === 0) {
109
- return // No older versions to check
110
- }
111
-
112
- // Check if any older versions had provenance
113
- let hasProvenanceRegressionDetected = false
114
- const versionWithProvenance = []
115
-
116
- // NOTE: Going back all versions and fetching their manifest to check provenance
117
- // is going to be very slow. We can limit this function to go back just 2-3 versions
118
- // and then stop there
119
- for (const olderVersion of olderVersions) {
120
- try {
121
- const keys = await this.fetchRegistryKeys()
122
- const metadata = await pacote.manifest(`${name}@${olderVersion}`, {
123
- verifyAttestations: true,
124
- registry: 'https://registry.npmjs.org',
125
- '//registry.npmjs.org/:_keys': keys
126
- })
127
-
128
- if (metadata && metadata._attestations) {
129
- versionWithProvenance.push(olderVersion)
130
- hasProvenanceRegressionDetected = true
142
+ if (
143
+ error instanceof Error &&
144
+ error.message === 'Unable to find version or dist-tag for package'
145
+ ) {
146
+ throw error
131
147
  }
132
- } catch (error) {
133
- // Ignore errors for older versions as they might not have provenance
134
- // We only care if they successfully had provenance before
135
- continue
136
- }
137
- }
138
-
139
- // If older versions had provenance, check if current version lacks it
140
- if (hasProvenanceRegressionDetected) {
141
- try {
142
- const keys = await this.fetchRegistryKeys()
143
- const currentMetadata = await pacote.manifest(`${name}@${version}`, {
144
- verifyAttestations: true,
145
- registry: 'https://registry.npmjs.org',
146
- '//registry.npmjs.org/:_keys': keys
147
- })
148
148
 
149
- if (!currentMetadata || !currentMetadata._attestations) {
150
- const latestVersionWithProvenance = versionWithProvenance[0] // First is the newest
151
- throw new Error(
152
- `Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
153
- )
154
- }
155
- } catch (error) {
156
- // If it's our custom error, re-throw it
157
- if (error.message.includes('Provenance regression detected')) {
149
+ if (
150
+ error instanceof Warning &&
151
+ (error.message.includes('Error fetching registry keys') ||
152
+ error.message === 'the package was published without any attestations')
153
+ ) {
158
154
  throw error
159
155
  }
160
-
161
- // If it's a verification error, the current version lacks valid provenance
162
- if (error.code === 'EATTESTATIONVERIFY' || error.message.includes('attestations')) {
163
- const latestVersionWithProvenance = versionWithProvenance[0]
164
- throw new Error(
165
- `Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
156
+
157
+ if (shouldConsiderProvenanceRegression(error)) {
158
+ this.throwIfProvenanceRegression(validationMetadata)
159
+ }
160
+
161
+ if (error.message.includes('Package has no attestations to verify')) {
162
+ throw new Warning(
163
+ `Unable to verify provenance: the package was published without any attestations`
166
164
  )
167
165
  }
168
-
169
- // For other errors, treat as if current version lacks provenance
170
- const latestVersionWithProvenance = versionWithProvenance[0]
171
- throw new Error(
172
- `Provenance regression detected: Previous version ${latestVersionWithProvenance} had provenance attestations, but version ${version} does not. This represents a security downgrade.`
166
+
167
+ this.debug(
168
+ '\nUnable to verify provenance for package %s@%s: %s',
169
+ validationMetadata.name,
170
+ validationMetadata.version,
171
+ error.message
173
172
  )
174
- }
175
- }
173
+ throw new Warning(`Unable to verify provenance`)
174
+ })
176
175
  }
177
- */
178
176
 
179
177
  fetchRegistryKeys() {
180
178
  const registryHost = 'https://registry.npmjs.org'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npq",
3
- "version": "3.17.0",
3
+ "version": "3.18.0",
4
4
  "description": "marshall your npm/npm package installs with high quality and class 🎖",
5
5
  "bin": {
6
6
  "npq": "./bin/npq.js",