npq 3.15.3 → 3.16.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
@@ -7,6 +7,16 @@ npq allows you to audit npm packages _before_ you install them
7
7
  [![Known Vulnerabilities](https://snyk.io/test/github/lirantal/npq/badge.svg)](https://snyk.io/test/github/lirantal/npq)
8
8
  [![Security Responsible Disclosure](https://img.shields.io/badge/Security-Responsible%20Disclosure-yellow)](SECURITY.md)
9
9
 
10
+ TL;DR how to use npq:
11
+
12
+ ```sh
13
+ $ npx npq install express --dry-run
14
+ ```
15
+
16
+ _What it does: the `npx` tool downloads and execute `npq` package, runs an install check for the `express` package and `--dry-run` means npq exists regardless of success/errors_.
17
+
18
+ Here's a screenshot of npq in action:
19
+
10
20
  ![npq demo screenshot](.github/npq.png)
11
21
 
12
22
  Media coverage about npq:
@@ -133,7 +143,7 @@ Note: `npq` by default will offload all commands and their arguments to the `npm
133
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
134
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.
135
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.
136
- | deprecation | Will show a warning if the package version being installed is deprecated. | Helps identify packages that are no longer maintained or recommended for use.
146
+ | deprecation | Will show a warning if the package version is deprecated on npm or if its GitHub repository has been archived. | Helps identify packages that are no longer maintained or recommended for use. Set `GITHUB_TOKEN` environment variable for higher GitHub API rate limits.
137
147
 
138
148
  ### Disabling Marshalls
139
149
 
@@ -93,6 +93,85 @@ class PackageRepoUtils {
93
93
  throw new Error(`Could not find dist-tag ${packageVersion} for package ${packageName}`)
94
94
  }
95
95
  }
96
+
97
+ /**
98
+ * Extract GitHub owner and repo from a repository URL
99
+ * @param {string} repoUrl - Repository URL (e.g., "git+https://github.com/owner/repo.git")
100
+ * @returns {{owner: string, repo: string} | null} - Owner and repo, or null if not a GitHub URL
101
+ */
102
+ extractGitHubRepoFromUrl(repoUrl) {
103
+ if (!repoUrl || typeof repoUrl !== 'string') {
104
+ return null
105
+ }
106
+
107
+ // Match GitHub URLs in various formats:
108
+ // - git+https://github.com/owner/repo.git
109
+ // - https://github.com/owner/repo.git
110
+ // - https://github.com/owner/repo
111
+ // - git://github.com/owner/repo.git
112
+ // - git@github.com:owner/repo.git
113
+ const httpsPattern = /github\.com[/:]([\w.-]+)\/([\w.-]+?)(?:\.git)?$/i
114
+ const match = repoUrl.match(httpsPattern)
115
+
116
+ if (match) {
117
+ return {
118
+ owner: match[1],
119
+ repo: match[2]
120
+ }
121
+ }
122
+
123
+ return null
124
+ }
125
+
126
+ /**
127
+ * Check if a GitHub repository is archived
128
+ * @param {string} owner - Repository owner
129
+ * @param {string} repo - Repository name
130
+ * @returns {Promise<boolean>} - True if archived, false otherwise
131
+ * @throws {Error} - On rate limit or API errors
132
+ */
133
+ async isGitHubRepoArchived(owner, repo) {
134
+ const headers = {
135
+ Accept: 'application/vnd.github.v3+json',
136
+ 'User-Agent': 'npq-package-checker'
137
+ }
138
+
139
+ // Use GITHUB_TOKEN if available for higher rate limits
140
+ if (process.env.GITHUB_TOKEN) {
141
+ headers.Authorization = `token ${process.env.GITHUB_TOKEN}`
142
+ }
143
+
144
+ const response = await fetch(`https://api.github.com/repos/${owner}/${repo}`, {
145
+ headers
146
+ })
147
+
148
+ if (response.status === 403) {
149
+ const rateLimitRemaining = response.headers.get('x-ratelimit-remaining')
150
+ if (rateLimitRemaining === '0') {
151
+ throw new Error(
152
+ 'GitHub API rate limit exceeded - deprecation marshall could not evaluate repository archive status. ' +
153
+ 'Set GITHUB_TOKEN environment variable for higher rate limits.'
154
+ )
155
+ }
156
+ throw new Error(
157
+ `GitHub API access forbidden for ${owner}/${repo} - deprecation marshall could not evaluate repository archive status`
158
+ )
159
+ }
160
+
161
+ if (response.status === 404) {
162
+ // Repository doesn't exist or is private - can't determine archive status
163
+ return false
164
+ }
165
+
166
+ if (!response.ok) {
167
+ throw new Error(
168
+ `GitHub API error (${response.status}) - deprecation marshall could not evaluate repository archive status`
169
+ )
170
+ }
171
+
172
+ const data = await response.json()
173
+ return data.archived === true
174
+ }
96
175
  }
97
176
 
98
177
  module.exports = PackageRepoUtils
@@ -1,5 +1,31 @@
1
1
  'use strict'
2
2
 
3
+ /**
4
+ * Author Marshall - Package Publisher Security Checks
5
+ *
6
+ * This marshall performs two distinct security checks related to package publishing:
7
+ *
8
+ * 1. NEW AUTHOR CHECK (lines 65-80)
9
+ * Detects if this is the first version ever published by this user for this package.
10
+ * - Condition: No prior versions exist from this user, OR the current version is their first.
11
+ * - Only flags if the version was published recently (≤21 days ago).
12
+ * - Rationale: A brand new publisher on an established package could indicate account
13
+ * compromise or malicious takeover. However, if the "first" version is old (e.g., 10 years),
14
+ * it's not a current risk.
15
+ *
16
+ * 2. VERSION RECENCY CHECK (lines 91-106)
17
+ * Detects if the current version being installed was published very recently.
18
+ * - ≤7 days: Throws an Error (high risk)
19
+ * - ≤30 days: Throws a Warning (moderate risk)
20
+ * - Only applies if version is ≤45 days old.
21
+ * - Rationale: Very recently published versions haven't had time for community review
22
+ * and could contain undiscovered malicious code. This is independent of author history.
23
+ *
24
+ * These two checks are complementary:
25
+ * - Check 1 focuses on WHO published (author trustworthiness)
26
+ * - Check 2 focuses on WHEN it was published (version maturity)
27
+ */
28
+
3
29
  const BaseMarshall = require('./baseMarshall')
4
30
  const Warning = require('../helpers/warning')
5
31
  const { marshallCategories } = require('./constants')
@@ -18,16 +44,19 @@ class Marshall extends BaseMarshall {
18
44
  }
19
45
 
20
46
  /**
21
- * What do we check?
22
- * 1. Who is the user who published the package? in the pakument's `_npmUser` field
23
- * 2. Is this the first time we see this user publishing a package?
24
- * If so, halt and report.
25
- * 3. Start from first version to latest version and check if they published
26
- * the package before.
27
- * If they did, and the difference between this published version date and
28
- * first is more than 30 days then halt and report.
29
- * @param {*} pkg
30
- * @returns
47
+ * Validates package author and version recency for security risks.
48
+ *
49
+ * Performs two checks:
50
+ * 1. New Author Check: Flags if this is the user's first publish of this package
51
+ * AND the version was published within the last 21 days.
52
+ * 2. Version Recency Check: Flags recently published versions regardless of author:
53
+ * - Error if ≤7 days old
54
+ * - Warning if 30 days old
55
+ *
56
+ * @param {Object} pkg - Package info with packageName and packageVersion
57
+ * @returns {string} The version's publish date string if all checks pass
58
+ * @throws {Error} If security risk is detected (new author or very recent version)
59
+ * @throws {Warning} If moderate risk is detected (version 8-30 days old)
31
60
  */
32
61
  async validate(pkg) {
33
62
  // @TODO move some of these utility functions about first package version
@@ -40,7 +69,7 @@ class Marshall extends BaseMarshall {
40
69
  )
41
70
 
42
71
  // @TODO fix to work for both explicit versions (1.0.0) and also
43
- // for dis-tags (latest)
72
+ // for dist-tags (latest)
44
73
  const npmUser = pakument.versions[packageVersion]._npmUser
45
74
  if (!npmUser || !npmUser.email) {
46
75
  throw new Error('Could not determine publishing user for this package version')
@@ -88,32 +117,23 @@ class Marshall extends BaseMarshall {
88
117
  // )
89
118
  }
90
119
 
91
- const firstPublishedDateString = pakument.time[firstVersionForUser.version]
92
-
93
120
  // get date in ms
94
- const currentDate = new Date()
95
- const dateDiffInMsVersionPublished = currentDate - new Date(versionPublishedDateString)
121
+ const dateDiffInMsVersionPublished = new Date() - new Date(versionPublishedDateString)
96
122
  let dateDiffVersionPublished = 0
97
123
  if (dateDiffInMsVersionPublished > 0) {
98
124
  dateDiffVersionPublished = Math.round(dateDiffInMsVersionPublished / (1000 * 60 * 60 * 24))
99
125
  }
100
126
 
101
127
  if (dateDiffVersionPublished <= 45) {
102
- const dateDiffInMs = new Date(versionPublishedDateString) - new Date(firstPublishedDateString)
103
- let dateDiffInDays = 0
104
- if (dateDiffInMs > 0) {
105
- dateDiffInDays = Math.round(dateDiffInMs / (1000 * 60 * 60 * 24))
106
- }
107
-
108
- if (dateDiffInDays <= 7) {
128
+ if (dateDiffVersionPublished <= 7) {
109
129
  throw new Error(
110
- `The user ${npmUser.name} <${npmUser.email}> published this package for the first time only ${dateDiffInDays} days ago`
130
+ `This version was published only ${dateDiffVersionPublished} days ago by ${npmUser.name} <${npmUser.email}>`
111
131
  )
112
132
  }
113
133
 
114
- if (dateDiffInDays <= 30) {
134
+ if (dateDiffVersionPublished <= 30) {
115
135
  throw new Warning(
116
- `The user ${npmUser.name} <${npmUser.email}> published this package for the first time only ${dateDiffInDays} days ago`
136
+ `This version was published only ${dateDiffVersionPublished} days ago by ${npmUser.name} <${npmUser.email}>`
117
137
  )
118
138
  }
119
139
  }
@@ -42,6 +42,38 @@ class Marshall extends BaseMarshall {
42
42
  if (packageDeprecated) {
43
43
  throw new Error(`Package deprecated: ${packageDeprecated}`)
44
44
  }
45
+
46
+ // Check if the GitHub repository is archived
47
+ await this.checkGitHubRepoArchived(data)
48
+ }
49
+
50
+ /**
51
+ * Check if the package's GitHub repository is archived
52
+ * @param {object} data - Package data from npm registry
53
+ * @throws {Error} - If repository is archived or on API rate limit
54
+ */
55
+ async checkGitHubRepoArchived(data) {
56
+ const repoUrl = data.repository?.url
57
+
58
+ if (!repoUrl) {
59
+ return
60
+ }
61
+
62
+ const githubRepo = this.packageRepoUtils.extractGitHubRepoFromUrl(repoUrl)
63
+
64
+ if (!githubRepo) {
65
+ // @TODO: Add support for GitLab and Bitbucket repository archive detection
66
+ return
67
+ }
68
+
69
+ const isArchived = await this.packageRepoUtils.isGitHubRepoArchived(
70
+ githubRepo.owner,
71
+ githubRepo.repo
72
+ )
73
+
74
+ if (isArchived) {
75
+ throw new Error('Package repository has been archived on GitHub')
76
+ }
45
77
  }
46
78
  }
47
79
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "npq",
3
- "version": "3.15.3",
3
+ "version": "3.16.0",
4
4
  "description": "marshall your npm/npm package installs with high quality and class 🎖",
5
5
  "bin": {
6
6
  "npq": "./bin/npq.js",