npq 3.8.1 → 3.9.1

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.
Files changed (48) hide show
  1. package/.github/npq.mov +0 -0
  2. package/.github/npq.mp4 +0 -0
  3. package/.github/npq.png +0 -0
  4. package/.github/workflows/main.yml +2 -2
  5. package/README.md +18 -9
  6. package/__tests__/__fixtures__/test.marshall.js +5 -6
  7. package/__tests__/cli.test.js +110 -0
  8. package/__tests__/cliPrompt.test.js +409 -0
  9. package/__tests__/marshalls.base.test.js +1 -1
  10. package/__tests__/marshalls.classMethods.test.js +11 -14
  11. package/__tests__/marshalls.expiredDomains.test.js +2 -2
  12. package/__tests__/marshalls.provenance.test.js +181 -4
  13. package/__tests__/marshalls.signatures.test.js +0 -4
  14. package/__tests__/marshalls.snyk.test.js +294 -0
  15. package/__tests__/marshalls.tasks.test.js +48 -24
  16. package/__tests__/marshalls.typosquatting.test.js +16 -9
  17. package/__tests__/reportResults.test.js +772 -0
  18. package/__tests__/scripts.test.js +2 -2
  19. package/bin/npq-hero.js +73 -20
  20. package/bin/npq.js +113 -21
  21. package/lib/cli.js +140 -24
  22. package/lib/helpers/cliPrompt.js +97 -0
  23. package/lib/helpers/cliSpinner.js +104 -0
  24. package/lib/helpers/cliSupportHandler.js +50 -7
  25. package/lib/helpers/packageRepoUtils.js +5 -0
  26. package/lib/helpers/promiseThrottler.js +96 -0
  27. package/lib/helpers/reportResults.js +304 -0
  28. package/lib/helpers/sourcePackages.js +36 -0
  29. package/lib/marshall.js +42 -65
  30. package/lib/marshalls/age.marshall.js +3 -2
  31. package/lib/marshalls/author.marshall.js +36 -7
  32. package/lib/marshalls/baseMarshall.js +9 -17
  33. package/lib/marshalls/deprecation.marshall.js +43 -0
  34. package/lib/marshalls/downloads.marshall.js +1 -1
  35. package/lib/marshalls/expiredDomains.marshall.js +7 -5
  36. package/lib/marshalls/index.js +110 -80
  37. package/lib/marshalls/license.marshall.js +1 -1
  38. package/lib/marshalls/newbin.marshall.js +1 -2
  39. package/lib/marshalls/provenance.marshall.js +152 -33
  40. package/lib/marshalls/repo.marshall.js +6 -5
  41. package/lib/marshalls/scripts.marshall.js +1 -1
  42. package/lib/marshalls/signatures.marshall.js +26 -2
  43. package/lib/marshalls/snyk.marshall.js +61 -15
  44. package/lib/marshalls/typosquatting.marshall.js +8 -5
  45. package/lib/marshalls/version-maturity.marshall.js +1 -1
  46. package/package.json +16 -19
  47. package/scripts/postinstall.js +10 -11
  48. package/lib/cliCommons.js +0 -80
@@ -1,7 +1,7 @@
1
1
  'use strict'
2
2
 
3
3
  const BaseMarshall = require('./baseMarshall')
4
- const validator = require('validator')
4
+ const Warning = require('../helpers/warning')
5
5
  const { marshallCategories } = require('./constants')
6
6
 
7
7
  const MARSHALL_NAME = 'author'
@@ -46,7 +46,10 @@ class Marshall extends BaseMarshall {
46
46
  throw new Error('Could not determine publishing user for this package version')
47
47
  }
48
48
 
49
- if (!validator.isEmail(npmUser.email)) {
49
+ // Agree with Colin on keeping email regex simple: https://colinhacks.com/essays/reasonable-email-regex
50
+ const emailRegex =
51
+ /^(?!\.)(?!.*\.\.)([a-z0-9_'+\-\.]*)[a-z0-9_'+\-]@([a-z0-9][a-z0-9\-]*\.)+[a-z]{2,}$/i
52
+ if (!emailRegex.test(npmUser.email)) {
50
53
  throw new Error('The publishing user has no valid email address')
51
54
  }
52
55
 
@@ -60,9 +63,29 @@ class Marshall extends BaseMarshall {
60
63
  }
61
64
 
62
65
  if (!firstVersionForUser || firstVersionForUser.version === packageVersion) {
63
- throw new Error(
64
- `This is first version the user ${npmUser.name} <${npmUser.email}> published this package`
65
- )
66
+ // Only throw the error if also the `packageVersion` was published less than 21 days ago:
67
+ if (versionPublishedDateString) {
68
+ const dateDiffInMs = new Date() - new Date(versionPublishedDateString)
69
+ let dateDiffInDays = 0
70
+
71
+ if (dateDiffInMs > 0) {
72
+ dateDiffInDays = Math.round(dateDiffInMs / (1000 * 60 * 60 * 24))
73
+ }
74
+
75
+ if (dateDiffInDays <= 21) {
76
+ throw new Error(
77
+ `The user ${npmUser.name} <${npmUser.email}> published this package for the first time only ${dateDiffInDays} days ago`
78
+ )
79
+ }
80
+ }
81
+
82
+ // otherwise, there's no point in throwing an error
83
+ // because this version already exists for a while. for e.g: package `ncp` latest version
84
+ // is from 10 years ago which was the first version published by the user, but that's
85
+ // hardly a risk at this point being 10 years old, so we don't throw the following error:
86
+ // throw new Error(
87
+ // `This is the first version the user ${npmUser.name} <${npmUser.email}> published this package`
88
+ // )
66
89
  }
67
90
 
68
91
  const firstPublishedDateString = pakument.time[firstVersionForUser.version]
@@ -82,9 +105,15 @@ class Marshall extends BaseMarshall {
82
105
  dateDiffInDays = Math.round(dateDiffInMs / (1000 * 60 * 60 * 24))
83
106
  }
84
107
 
85
- if (dateDiffInDays <= 30) {
108
+ if (dateDiffInDays <= 7) {
86
109
  throw new Error(
87
- `The user ${npmUser.name} <${npmUser.email}> published this package for the first time only ${dateDiffInDays} days prior to this version.`
110
+ `The user ${npmUser.name} <${npmUser.email}> published this package for the first time only ${dateDiffInDays} days ago`
111
+ )
112
+ }
113
+
114
+ if (dateDiffInDays <= 30) {
115
+ throw new Warning(
116
+ `The user ${npmUser.name} <${npmUser.email}> published this package for the first time only ${dateDiffInDays} days ago`
88
117
  )
89
118
  }
90
119
  }
@@ -1,38 +1,40 @@
1
1
  'use strict'
2
2
 
3
+ const util = require('node:util')
3
4
  const Warning = require('../helpers/warning')
4
5
  const { marshallCategories } = require('./constants')
5
6
 
6
7
  class BaseMarshall {
7
8
  constructor(options) {
9
+ this.debug = util.debuglog('npq')
8
10
  this.packageRepoUtils = options.packageRepoUtils
9
11
  this.categoryId = marshallCategories.PackageHealth.id
10
12
  }
11
13
 
12
- init(ctx, task) {
14
+ init(ctx) {
13
15
  this.ctx = ctx
14
- this.task = task
15
16
 
16
17
  ctx.marshalls[this.name] = {
17
18
  status: null,
18
19
  errors: [],
19
20
  warnings: [],
20
- data: {}
21
+ data: {},
22
+ marshall: this.name,
23
+ categoryId: this.categoryId
21
24
  }
22
25
  }
23
26
 
24
- run(ctx, task) {
27
+ run(ctx) {
25
28
  const tasks = ctx.pkgs.reduce((prevPkg, currPkg) => {
26
- return prevPkg.concat(this.checkPackage(currPkg, ctx, task))
29
+ return prevPkg.concat(this.checkPackage(currPkg, ctx))
27
30
  }, [])
28
31
 
29
32
  return Promise.all(tasks)
30
33
  }
31
34
 
32
- checkPackage(pkg, ctx, task) {
35
+ checkPackage(pkg, ctx) {
33
36
  return this.validate(pkg)
34
37
  .then((data) => {
35
- task.output = `querying ${pkg.packageString}...`
36
38
  ctx.marshalls[this.name].data[pkg.packageString] = data
37
39
 
38
40
  // not explicitly required, but a task can return its results
@@ -65,16 +67,6 @@ class BaseMarshall {
65
67
  message: msg.message
66
68
  })
67
69
  }
68
-
69
- handleMessages() {
70
- const errors = this.ctx.marshalls[this.name].errors
71
- const warnings = this.ctx.marshalls[this.name].warnings
72
- if ((errors && errors.length) || (warnings && warnings.length)) {
73
- throw new Error()
74
- }
75
-
76
- return Promise.resolve()
77
- }
78
70
  }
79
71
 
80
72
  module.exports = BaseMarshall
@@ -0,0 +1,43 @@
1
+ 'use strict'
2
+
3
+ const BaseMarshall = require('./baseMarshall')
4
+ const { marshallCategories } = require('./constants')
5
+
6
+ const MARSHALL_NAME = 'deprecation'
7
+
8
+ class Marshall extends BaseMarshall {
9
+ constructor(options) {
10
+ super(options)
11
+ this.name = MARSHALL_NAME
12
+ this.categoryId = marshallCategories.PackageHealth.id
13
+ }
14
+
15
+ title() {
16
+ return 'Checking package for deprecation flag'
17
+ }
18
+
19
+ validate(pkg) {
20
+ return this.packageRepoUtils.getPackageInfo(pkg.packageName).then((data) => {
21
+ const packageVersion =
22
+ pkg.packageVersion === 'latest'
23
+ ? data['dist-tags'] && data['dist-tags'].latest
24
+ : this.packageRepoUtils.parsePackageVersion(pkg.packageVersion).version
25
+
26
+ if (!packageVersion) {
27
+ return true
28
+ }
29
+
30
+ const packageDeprecated =
31
+ data &&
32
+ data.versions &&
33
+ data.versions[packageVersion] &&
34
+ data.versions[packageVersion].deprecated
35
+
36
+ if (packageDeprecated) {
37
+ throw new Error(`Package deprecated: ${packageDeprecated}`)
38
+ }
39
+ })
40
+ }
41
+ }
42
+
43
+ module.exports = Marshall
@@ -12,7 +12,7 @@ class Marshall extends BaseMarshall {
12
12
  constructor(options) {
13
13
  super(options)
14
14
  this.name = MARSHALL_NAME
15
- this.categoryId = marshallCategories.MalwareDetection.id
15
+ this.categoryId = marshallCategories.PackageHealth.id
16
16
  }
17
17
 
18
18
  title() {
@@ -2,15 +2,17 @@
2
2
 
3
3
  const BaseMarshall = require('./baseMarshall')
4
4
  const { marshallCategories } = require('./constants')
5
- const dns = require('dns').promises
5
+ const { Resolver } = require('dns/promises')
6
6
 
7
+ const dns = new Resolver()
8
+ dns.setServers(['1.1.1.1', '8.8.8.8'])
7
9
  const MARSHALL_NAME = 'maintainers_expired_emails'
8
10
 
9
11
  class Marshall extends BaseMarshall {
10
12
  constructor(options) {
11
13
  super(options)
12
14
  this.name = MARSHALL_NAME
13
- this.categoryId = marshallCategories.MalwareDetection.id
15
+ this.categoryId = marshallCategories.PackageHealth.id
14
16
  }
15
17
 
16
18
  title() {
@@ -30,16 +32,16 @@ class Marshall extends BaseMarshall {
30
32
  for (const maintainerInfo of maintainersAccounts) {
31
33
  const maintainerEmail = maintainerInfo.email
32
34
  const emailDomain = maintainerEmail.split('@')[1]
33
- testEmails.push(dns.resolve(emailDomain))
35
+ testEmails.push(dns.resolve(emailDomain, 'NS'))
34
36
  }
35
37
 
36
38
  return Promise.all(testEmails)
37
39
  })
38
40
  .catch((error) => {
39
41
  const emailHostname = error.hostname ? error.hostname : '<unknown>'
42
+ this.debug('\nDetected error resolving domain for maintainer e-mail: %s', emailHostname)
40
43
  throw new Error(
41
- 'Unable to resolve domain for maintainer e-mail, could be an expired account: ' +
42
- emailHostname
44
+ 'Detected expired domain can be abused for account takeover: ' + emailHostname
43
45
  )
44
46
  })
45
47
  }
@@ -1,24 +1,28 @@
1
1
  'use strict'
2
2
 
3
- const { Listr } = require('listr2')
4
- const { glob } = require('glob')
5
- const { marshallCategories } = require('./constants')
3
+ const fs = require('node:fs')
4
+ const path = require('node:path')
6
5
 
7
6
  class Marshalls {
8
7
  static async collectMarshalls() {
9
- const files = await glob(this.GLOB_MARSHALLS, {
10
- cwd: __dirname,
11
- absolute: true
12
- })
8
+ const files = await fs.promises.readdir(__dirname)
9
+
10
+ const matchingFiles = files
11
+ .filter((file) => file.endsWith('.marshall.js'))
12
+ .map((file) => path.resolve(__dirname, file))
13
13
 
14
- return files
14
+ return matchingFiles
15
15
  }
16
16
 
17
- static async buildMarshallTasks(marshalls, config) {
17
+ static async buildMarshallTasks(marshalls, options) {
18
18
  if (!marshalls || !Array.isArray(marshalls) || marshalls.length === 0) {
19
19
  return Promise.reject(new Error('unable to collect marshalls, or no marshalls found'))
20
20
  }
21
21
 
22
+ const config = {
23
+ packageRepoUtils: options.packageRepoUtils
24
+ }
25
+
22
26
  const marshallTasks = marshalls.reduce((prev, curr) => {
23
27
  // eslint-disable-next-line security/detect-non-literal-require
24
28
  const Marshall = require(curr)
@@ -27,79 +31,114 @@ class Marshalls {
27
31
  return prev.concat({
28
32
  title: marshall.title(),
29
33
  categoryId: marshall.categoryId,
30
- enabled: (ctx) => marshall.isEnabled(ctx),
31
- task: async (ctx, task) => {
32
- marshall.init(ctx, task)
33
- await marshall.run(ctx, task)
34
- const messages = marshall.handleMessages()
35
- return messages
34
+ execute: async (options) => {
35
+ const ctx = {
36
+ pkgs: options.pkgs,
37
+ marshalls: {}
38
+ }
39
+ marshall.init(ctx)
40
+ await marshall.run(ctx)
41
+ return ctx
36
42
  }
37
43
  })
38
44
  }, [])
39
45
 
40
- const allMarshallTasksByCategories = Object.keys(marshallCategories).map((categoryId) => {
41
- const category = marshallCategories[categoryId]
42
- const filteredMarshallTasks = marshallTasks.filter((marshall) => {
43
- return marshall.categoryId === category.id
44
- })
45
-
46
- const marshallsInCategory = {
47
- title: category.title,
48
- enabled: true,
49
- task: () => {
50
- return new Listr(filteredMarshallTasks, {
51
- exitOnError: false,
52
- concurrent: true,
53
- rendererOptions: { collapseSubtasks: false }
54
- })
55
- }
56
- }
57
- return marshallsInCategory
58
- })
59
-
60
- return new Listr(allMarshallTasksByCategories, {
61
- exitOnError: false,
62
- concurrent: true,
63
- rendererOptions: { collapseSubtasks: false }
64
- })
46
+ return marshallTasks
65
47
  }
66
48
 
67
- static tasks(options) {
68
- // console.log(options);
69
- // process.exit(1);
70
- return Marshalls.warmUpPackagesCache(options)
71
- .then((packagesDataList) => {
72
- // handle error in case we get just one package and it is not found
73
- if (packagesDataList && packagesDataList.length === 1) {
74
- if (packagesDataList[0].error && packagesDataList[0].error === 'Not found') {
75
- throw new Error(`Package not found: ${options.pkgs[0].packageName}`)
49
+ /**
50
+ * Executes security and health audits on npm packages using registered marshalls
51
+ *
52
+ * @param {Object} options - Configuration object containing packages and utilities
53
+ * @param {Array<Object>} options.pkgs - Array of package objects with packageName, packageVersion, and packageString properties
54
+ * @param {PackageRepoUtils} options.packageRepoUtils - Instance for fetching package information from registry
55
+ * @param {Object} [progressManager] - Optional progress manager for displaying audit progress updates
56
+ * @returns {Promise<Array<Object>>} Array of marshall results containing audit findings for each package
57
+ */
58
+ static async tasks(options, progressManager) {
59
+ const marshallResults = []
60
+
61
+ // @TODO handle throw error because fetch didn't work due to network issues
62
+ // currently, errors here with fetch fail like this:
63
+ // TypeError: fetch failed
64
+ // at node:internal/deps/undici/undici:13502:13
65
+ // at process.processTicksAndRejections (node:internal/process/task_queues:105:5)
66
+ // at async Promise.all (index 0)
67
+ // [cause]: Error: getaddrinfo ENOTFOUND registry.npmjs.org
68
+ // at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:120:26) {
69
+ // errno: -3008,
70
+ // code: 'ENOTFOUND',
71
+ // syscall: 'getaddrinfo',
72
+ // hostname: 'registry.npmjs.org'
73
+ // }
74
+ // }
75
+ const packagesDataList = await Marshalls.warmUpPackagesCache(options)
76
+
77
+ // handle error in case we get just one package and it is not found
78
+ // if (packagesDataList && packagesDataList.length === 1) {
79
+ // if (packagesDataList[0].error && packagesDataList[0].error === 'Not found') {
80
+ // }
81
+ // }
82
+
83
+ // handle error in case we get more than one package and at least one is not found
84
+ // in which we case we simply remove the package from the `options` array
85
+ // which lists objects (each have a property of packageName)
86
+ if (packagesDataList && packagesDataList.length > 0) {
87
+ options.pkgs = options.pkgs.filter((pkg, index) => {
88
+ if (packagesDataList[index].error && packagesDataList[index].error === 'Not found') {
89
+ const errorResult = {
90
+ ['not_found']: {
91
+ marshall: 'not_found',
92
+ status: null,
93
+ errors: [{ pkg: options.pkgs[index].packageName, message: 'Package not found' }],
94
+ warnings: [],
95
+ data: {},
96
+ categoryId: 'PackageHealth'
97
+ }
76
98
  }
77
- }
78
99
 
79
- // handle error in case we get more than one package and at least one is not found
80
- // in which we case we simply remove the package from the `options` array
81
- // which lists objects (each have a property of packageName)
82
- if (packagesDataList && packagesDataList.length > 1) {
83
- options.pkgs = options.pkgs.filter((pkg, index) => {
84
- return !packagesDataList[index].error || packagesDataList[index].error !== 'Not found'
85
- })
100
+ marshallResults.push(errorResult)
101
+ return false
86
102
  }
103
+
104
+ return true
87
105
  })
88
- .then(() => Marshalls.collectMarshalls())
89
- .then((marshalls) => {
90
- return Marshalls.buildMarshallTasks(marshalls, {
91
- packageRepoUtils: options.packageRepoUtils
92
- })
93
- })
94
- .then((tasks) => {
95
- return Marshalls.runTasks(tasks, options)
96
- })
97
- .catch((err) => {
98
- // console.error('Error:', err.message)
99
- // avoid implementing a custom error message for a not found package
100
- // because the package manager will yield its own error message
101
- return { error: true, message: err.message }
106
+ }
107
+
108
+ const marshalls = await Marshalls.collectMarshalls()
109
+
110
+ if (progressManager && options.pkgs && options.pkgs.length > 0) {
111
+ const packageList = options.pkgs.map((pkg) => pkg.packageString).join(', ')
112
+ progressManager.update(`Auditing packages: ${packageList}`)
113
+ }
114
+
115
+ const packagesToProcess = options.pkgs
116
+ if (packagesToProcess.length > 0) {
117
+ const marshallTasks = await Marshalls.buildMarshallTasks(marshalls, {
118
+ packageRepoUtils: options.packageRepoUtils
102
119
  })
120
+
121
+ for (const marshall of marshallTasks) {
122
+ try {
123
+ const res = await marshall.execute({
124
+ pkgs: options.pkgs,
125
+ marshalls: {}
126
+ })
127
+ marshallResults.push(res.marshalls)
128
+ } catch (error) {
129
+ console.error(`Error running task ${marshall.title}:`, error)
130
+ }
131
+ }
132
+ }
133
+
134
+ return marshallResults
135
+
136
+ // .catch((err) => {
137
+ // // console.error('Error:', err.message)
138
+ // // avoid implementing a custom error message for a not found package
139
+ // // because the package manager will yield its own error message
140
+ // return { error: true, message: err.message }
141
+ // })
103
142
  }
104
143
 
105
144
  static warmUpPackagesCache(options) {
@@ -112,15 +151,6 @@ class Marshalls {
112
151
 
113
152
  return Promise.all(fetchPackagesInfoPromises)
114
153
  }
115
-
116
- static runTasks(tasks, options) {
117
- return tasks.run({
118
- pkgs: options.pkgs,
119
- marshalls: {}
120
- })
121
- }
122
154
  }
123
155
 
124
- Marshalls.GLOB_MARSHALLS = '*.marshall.js'
125
-
126
156
  module.exports = Marshalls
@@ -9,7 +9,7 @@ class Marshall extends BaseMarshall {
9
9
  constructor(options) {
10
10
  super(options)
11
11
  this.name = MARSHALL_NAME
12
- this.categoryId = marshallCategories.PackageHealth.id
12
+ this.categoryId = marshallCategories.SupplyChainSecurity.id
13
13
  }
14
14
 
15
15
  title() {
@@ -11,8 +11,7 @@ class NewBinMarshall extends BaseMarshall {
11
11
  constructor(options) {
12
12
  super(options)
13
13
  this.name = MARSHALL_NAME
14
- // Decide on a category, e.g., PackageHealth or a new one if appropriate
15
- this.categoryId = marshallCategories.PackageHealth.id
14
+ this.categoryId = marshallCategories.SupplyChainSecurity.id
16
15
  }
17
16
 
18
17
  title() {