npq 3.21.2 → 3.21.4
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
|
@@ -146,7 +146,7 @@ Note: `npq` by default will offload all commands and their arguments to the `npm
|
|
|
146
146
|
| scripts | Will show a warning if a package has a pre/post install script which could potentially be malicious
|
|
147
147
|
| snyk | Will show a warning if a package has been found with vulnerabilities in Snyk's database | For Snyk to work you need to either have the `snyk` npm package installed with a valid API token, or make the token available in the `SNYK_TOKEN` environment variable, and npq will use it
|
|
148
148
|
| license | Will show a warning if a package has been found without a license field | Checks the latest version for a license
|
|
149
|
-
| expired domains |
|
|
149
|
+
| expired domains | Normalizes and checks complete maintainer email hosts, warning only when a simple two-label domain does not resolve but its public top-level domain does | Multipart and otherwise inconclusive DNS failures are reported as not evaluated; non-resolution is not proof that a domain is expired or available
|
|
150
150
|
| signatures | Will compare the package's signature as it shows on the registry's packument with signing keys published by the selected registry | Reports `not evaluated` if the selected registry does not expose signing keys
|
|
151
151
|
| 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) | Reports `not evaluated` if the selected registry does not expose signing keys or attestations
|
|
152
152
|
| 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
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { isIP } = require('node:net')
|
|
4
|
+
const { domainToASCII } = require('node:url')
|
|
5
|
+
|
|
6
|
+
const SPECIAL_USE_SUFFIXES = new Set([
|
|
7
|
+
'alt',
|
|
8
|
+
'corp',
|
|
9
|
+
'example',
|
|
10
|
+
'home',
|
|
11
|
+
'internal',
|
|
12
|
+
'invalid',
|
|
13
|
+
'local',
|
|
14
|
+
'localhost',
|
|
15
|
+
'mail',
|
|
16
|
+
'onion',
|
|
17
|
+
'test'
|
|
18
|
+
])
|
|
19
|
+
const RESERVED_HOSTS = new Set([
|
|
20
|
+
'example.com',
|
|
21
|
+
'example.net',
|
|
22
|
+
'example.org',
|
|
23
|
+
'home.arpa',
|
|
24
|
+
'in-addr.arpa',
|
|
25
|
+
'ip6.arpa'
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
function hasReservedSuffix(hostname, suffixes) {
|
|
29
|
+
return [...suffixes].some((suffix) => hostname === suffix || hostname.endsWith(`.${suffix}`))
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizeMaintainerDomain(maintainerEmail) {
|
|
33
|
+
const atIndex = typeof maintainerEmail === 'string' ? maintainerEmail.lastIndexOf('@') : -1
|
|
34
|
+
const hasOneAtSign = atIndex > 0 && maintainerEmail.indexOf('@') === atIndex
|
|
35
|
+
const emailDomain = atIndex > 0 ? maintainerEmail.slice(atIndex + 1).trim() : ''
|
|
36
|
+
|
|
37
|
+
if (!hasOneAtSign || !emailDomain || /\s/.test(emailDomain)) {
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const hostname = emailDomain.replace(/\.$/, '').toLowerCase()
|
|
42
|
+
const bracketedIp = hostname.match(/^\[(.*)\]$/)
|
|
43
|
+
if (isIP(bracketedIp ? bracketedIp[1] : hostname)) {
|
|
44
|
+
return null
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const asciiHostname = domainToASCII(hostname).toLowerCase()
|
|
48
|
+
const labels = asciiHostname.split('.')
|
|
49
|
+
const labelsAreValid = labels.every(
|
|
50
|
+
(label) =>
|
|
51
|
+
label.length > 0 &&
|
|
52
|
+
label.length <= 63 &&
|
|
53
|
+
/^[a-z0-9-]+$/.test(label) &&
|
|
54
|
+
!label.startsWith('-') &&
|
|
55
|
+
!label.endsWith('-')
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
if (
|
|
59
|
+
!asciiHostname ||
|
|
60
|
+
isIP(asciiHostname) ||
|
|
61
|
+
asciiHostname.length > 253 ||
|
|
62
|
+
labels.length < 2 ||
|
|
63
|
+
!labelsAreValid ||
|
|
64
|
+
hasReservedSuffix(asciiHostname, SPECIAL_USE_SUFFIXES) ||
|
|
65
|
+
hasReservedSuffix(asciiHostname, RESERVED_HOSTS)
|
|
66
|
+
) {
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return asciiHostname
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function isSimpleMaintainerDomain(domain) {
|
|
74
|
+
return domain.split('.').length === 2
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
module.exports = { isSimpleMaintainerDomain, normalizeMaintainerDomain }
|
|
@@ -3,6 +3,11 @@
|
|
|
3
3
|
const BaseMarshall = require('./baseMarshall')
|
|
4
4
|
const { marshallCategories } = require('./constants')
|
|
5
5
|
const NotEvaluated = require('../helpers/notEvaluated')
|
|
6
|
+
const Warning = require('../helpers/warning')
|
|
7
|
+
const {
|
|
8
|
+
isSimpleMaintainerDomain,
|
|
9
|
+
normalizeMaintainerDomain
|
|
10
|
+
} = require('../helpers/maintainerDomain')
|
|
6
11
|
const { Resolver } = require('dns/promises')
|
|
7
12
|
|
|
8
13
|
const dns = new Resolver()
|
|
@@ -32,44 +37,104 @@ class Marshall extends BaseMarshall {
|
|
|
32
37
|
throw new NotEvaluated('no maintainers information available for the package')
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
const emailDomains =
|
|
40
|
+
const emailDomains = new Map()
|
|
36
41
|
let invalidMaintainers = 0
|
|
37
42
|
for (const maintainerInfo of maintainersAccounts) {
|
|
38
|
-
const
|
|
39
|
-
const atIndex = typeof maintainerEmail === 'string' ? maintainerEmail.lastIndexOf('@') : -1
|
|
40
|
-
const emailDomain = atIndex > 0 ? maintainerEmail.slice(atIndex + 1).trim() : ''
|
|
43
|
+
const emailDomain = normalizeMaintainerDomain(maintainerInfo && maintainerInfo.email)
|
|
41
44
|
|
|
42
|
-
if (!emailDomain
|
|
45
|
+
if (!emailDomain) {
|
|
43
46
|
invalidMaintainers += 1
|
|
44
47
|
continue
|
|
45
48
|
}
|
|
46
49
|
|
|
47
|
-
emailDomains.
|
|
50
|
+
emailDomains.set(emailDomain, (emailDomains.get(emailDomain) || 0) + 1)
|
|
48
51
|
}
|
|
49
52
|
|
|
50
|
-
if (emailDomains.
|
|
53
|
+
if (emailDomains.size === 0) {
|
|
51
54
|
throw new NotEvaluated('no valid maintainer email domains are available for the package')
|
|
52
55
|
}
|
|
53
56
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
57
|
+
const domains = [...emailDomains.keys()].sort()
|
|
58
|
+
const resolutionResults = await Promise.allSettled(
|
|
59
|
+
domains.map((emailDomain) => this.dnsResolver.resolve(emailDomain, 'NS'))
|
|
60
|
+
)
|
|
61
|
+
const simpleMissingDomains = []
|
|
62
|
+
const incompleteDomains = []
|
|
63
|
+
|
|
64
|
+
resolutionResults.forEach((result, index) => {
|
|
65
|
+
if (result.status === 'fulfilled') {
|
|
66
|
+
return
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const domain = domains[index]
|
|
70
|
+
const errorCode = result.reason && result.reason.code
|
|
71
|
+
this.debug(
|
|
72
|
+
'\nUnable to resolve maintainer email domain %s with code %s',
|
|
73
|
+
domain,
|
|
74
|
+
errorCode || '<unknown>'
|
|
58
75
|
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
76
|
+
|
|
77
|
+
if (errorCode === 'ENOTFOUND' && isSimpleMaintainerDomain(domain)) {
|
|
78
|
+
simpleMissingDomains.push(domain)
|
|
79
|
+
} else {
|
|
80
|
+
incompleteDomains.push(domain)
|
|
81
|
+
}
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const topLevelDomains = [
|
|
85
|
+
...new Set(simpleMissingDomains.map((domain) => domain.split('.')[1]))
|
|
86
|
+
].sort()
|
|
87
|
+
const topLevelResults = await Promise.allSettled(
|
|
88
|
+
topLevelDomains.map((domain) => this.dnsResolver.resolve(domain, 'NS'))
|
|
89
|
+
)
|
|
90
|
+
const publicTopLevelDomains = new Set()
|
|
91
|
+
|
|
92
|
+
topLevelResults.forEach((result, index) => {
|
|
93
|
+
const domain = topLevelDomains[index]
|
|
94
|
+
if (result.status === 'fulfilled') {
|
|
95
|
+
publicTopLevelDomains.add(domain)
|
|
96
|
+
return
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
this.debug(
|
|
100
|
+
'\nUnable to verify public top-level domain %s with code %s',
|
|
101
|
+
domain,
|
|
102
|
+
(result.reason && result.reason.code) || '<unknown>'
|
|
64
103
|
)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
const suspectedDomains = simpleMissingDomains.filter((domain) => {
|
|
107
|
+
const isPublic = publicTopLevelDomains.has(domain.split('.')[1])
|
|
108
|
+
if (!isPublic) {
|
|
109
|
+
incompleteDomains.push(domain)
|
|
110
|
+
}
|
|
111
|
+
return isPublic
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
const incompleteCount =
|
|
115
|
+
invalidMaintainers +
|
|
116
|
+
incompleteDomains.reduce((count, domain) => count + emailDomains.get(domain), 0)
|
|
117
|
+
if (suspectedDomains.length > 0) {
|
|
118
|
+
const domainNoun = suspectedDomains.length === 1 ? 'domain' : 'domains'
|
|
119
|
+
const resolveVerb = suspectedDomains.length === 1 ? 'does' : 'do'
|
|
120
|
+
let message = `Maintainer ${domainNoun} ${suspectedDomains.join(', ')} ${resolveVerb} not resolve in public DNS and may warrant investigation.`
|
|
121
|
+
|
|
122
|
+
if (incompleteCount > 0) {
|
|
123
|
+
const recordNoun = incompleteCount === 1 ? 'record' : 'records'
|
|
124
|
+
message += ` ${incompleteCount} other maintainer ${recordNoun} could not be evaluated.`
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
throw new Warning(message)
|
|
65
128
|
}
|
|
66
129
|
|
|
67
|
-
if (
|
|
68
|
-
const
|
|
69
|
-
throw new NotEvaluated(
|
|
130
|
+
if (incompleteCount > 0) {
|
|
131
|
+
const recordNoun = incompleteCount === 1 ? 'record' : 'records'
|
|
132
|
+
throw new NotEvaluated(
|
|
133
|
+
`${incompleteCount} maintainer ${recordNoun} could not be evaluated because DNS or email data was incomplete`
|
|
134
|
+
)
|
|
70
135
|
}
|
|
71
136
|
|
|
72
|
-
return resolutionResults
|
|
137
|
+
return resolutionResults.map((result) => result.value)
|
|
73
138
|
}
|
|
74
139
|
}
|
|
75
140
|
|