muaddib-scanner 2.4.4 → 2.4.5
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/LICENSE +20 -20
- package/iocs/builtin.yaml +131 -131
- package/iocs/hashes.yaml +214 -214
- package/iocs/packages.yaml +276 -276
- package/package.json +2 -3
- package/src/canary-tokens.js +184 -184
- package/src/ioc/bootstrap.js +181 -181
- package/src/ioc/yaml-loader.js +223 -223
- package/src/maintainer-change.js +224 -224
- package/src/output-formatter.js +192 -192
- package/src/publish-anomaly.js +206 -206
- package/src/report.js +230 -230
- package/src/sarif.js +96 -96
- package/src/scanner/ai-config.js +183 -183
- package/src/scanner/ast-detectors.js +40 -17
- package/src/scanner/ast.js +1 -0
- package/src/scanner/dataflow.js +14 -2
- package/src/scanner/dependencies.js +223 -223
- package/src/scanner/entropy.js +7 -0
- package/src/scanner/hash.js +118 -118
- package/src/scanner/npm-registry.js +128 -128
- package/src/scanner/python.js +442 -442
- package/src/scoring.js +3 -1
- package/src/shared/analyze-helper.js +49 -49
- package/src/temporal-analysis.js +260 -260
- package/src/temporal-runner.js +139 -139
- package/src/utils.js +327 -327
- package/src/watch.js +55 -55
package/src/maintainer-change.js
CHANGED
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
const { fetchPackageMetadata, getLatestVersions } = require('./temporal-analysis.js');
|
|
2
|
-
|
|
3
|
-
// Patterns that indicate generic/suspicious maintainer names
|
|
4
|
-
const GENERIC_NAME_PATTERNS = [
|
|
5
|
-
/^npm-user-\w+$/i,
|
|
6
|
-
/^user\d+$/i,
|
|
7
|
-
/^test$/i,
|
|
8
|
-
/^admin$/i,
|
|
9
|
-
/^root$/i,
|
|
10
|
-
/^default$/i,
|
|
11
|
-
/^temp$/i,
|
|
12
|
-
/^tmp$/i,
|
|
13
|
-
/^owner$/i,
|
|
14
|
-
/^maintainer$/i
|
|
15
|
-
];
|
|
16
|
-
|
|
17
|
-
const MIN_NAME_LENGTH = 3;
|
|
18
|
-
const DIGIT_RATIO_THRESHOLD = 0.5;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Extract current maintainers from package metadata.
|
|
22
|
-
* @param {object} metadata - Full registry metadata from fetchPackageMetadata()
|
|
23
|
-
* @returns {{ current: Array<{name: string, email: string}>, count: number }}
|
|
24
|
-
*/
|
|
25
|
-
function getMaintainersHistory(metadata) {
|
|
26
|
-
if (!metadata || !metadata.maintainers || !Array.isArray(metadata.maintainers)) {
|
|
27
|
-
return { current: [], count: 0 };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const current = metadata.maintainers.map(m => ({
|
|
31
|
-
name: m.name || '',
|
|
32
|
-
email: m.email || ''
|
|
33
|
-
}));
|
|
34
|
-
|
|
35
|
-
return { current, count: current.length };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Evaluate the risk level of a maintainer based on their name.
|
|
40
|
-
* @param {{ name: string, email: string }} maintainer
|
|
41
|
-
* @returns {{ riskLevel: string, reasons: string[] }}
|
|
42
|
-
*/
|
|
43
|
-
function analyzeMaintainerRisk(maintainer) {
|
|
44
|
-
const reasons = [];
|
|
45
|
-
const name = (maintainer && maintainer.name) || '';
|
|
46
|
-
|
|
47
|
-
if (!name) {
|
|
48
|
-
return { riskLevel: 'HIGH', reasons: ['Empty maintainer name'] };
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// Check generic name patterns
|
|
52
|
-
for (const pattern of GENERIC_NAME_PATTERNS) {
|
|
53
|
-
if (pattern.test(name)) {
|
|
54
|
-
reasons.push(`Generic name pattern: "${name}"`);
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// Check very short name
|
|
60
|
-
if (name.length < MIN_NAME_LENGTH) {
|
|
61
|
-
reasons.push(`Very short name (${name.length} chars)`);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Check digit ratio
|
|
65
|
-
const digitCount = (name.match(/\d/g) || []).length;
|
|
66
|
-
const digitRatio = digitCount / name.length;
|
|
67
|
-
if (digitRatio >= DIGIT_RATIO_THRESHOLD && name.length >= MIN_NAME_LENGTH) {
|
|
68
|
-
reasons.push(`High digit ratio (${(digitRatio * 100).toFixed(0)}% digits)`);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
if (reasons.length > 0) {
|
|
72
|
-
return { riskLevel: 'HIGH', reasons };
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return { riskLevel: 'LOW', reasons: [] };
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* Get maintainers associated with a specific version from metadata.
|
|
80
|
-
* Uses _npmUser (who published) and maintainers list from the version entry.
|
|
81
|
-
* @param {object} versionData - metadata.versions[version]
|
|
82
|
-
* @returns {{ publisher: {name: string, email: string}|null, maintainers: Array<{name: string, email: string}> }}
|
|
83
|
-
*/
|
|
84
|
-
function getVersionMaintainers(versionData) {
|
|
85
|
-
if (!versionData) return { publisher: null, maintainers: [] };
|
|
86
|
-
|
|
87
|
-
const publisher = versionData._npmUser
|
|
88
|
-
? { name: versionData._npmUser.name || '', email: versionData._npmUser.email || '' }
|
|
89
|
-
: null;
|
|
90
|
-
|
|
91
|
-
const maintainers = Array.isArray(versionData.maintainers)
|
|
92
|
-
? versionData.maintainers.map(m => ({ name: m.name || '', email: m.email || '' }))
|
|
93
|
-
: [];
|
|
94
|
-
|
|
95
|
-
return { publisher, maintainers };
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Detect maintainer changes between two versions.
|
|
100
|
-
* @param {string} packageName - npm package name
|
|
101
|
-
* @returns {Promise<object>} Detection result
|
|
102
|
-
*/
|
|
103
|
-
async function detectMaintainerChange(packageName) {
|
|
104
|
-
const metadata = await fetchPackageMetadata(packageName);
|
|
105
|
-
const maintainersInfo = getMaintainersHistory(metadata);
|
|
106
|
-
const latest = getLatestVersions(metadata, 2);
|
|
107
|
-
|
|
108
|
-
if (latest.length < 2) {
|
|
109
|
-
return {
|
|
110
|
-
packageName,
|
|
111
|
-
suspicious: false,
|
|
112
|
-
findings: [],
|
|
113
|
-
maintainers: maintainersInfo
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const [newestEntry, previousEntry] = latest;
|
|
118
|
-
const versions = metadata.versions || {};
|
|
119
|
-
const newestData = versions[newestEntry.version];
|
|
120
|
-
const previousData = versions[previousEntry.version];
|
|
121
|
-
|
|
122
|
-
const newestMaint = getVersionMaintainers(newestData);
|
|
123
|
-
const previousMaint = getVersionMaintainers(previousData);
|
|
124
|
-
|
|
125
|
-
const findings = [];
|
|
126
|
-
|
|
127
|
-
// Build name sets for comparison
|
|
128
|
-
const previousNames = new Set(previousMaint.maintainers.map(m => m.name.toLowerCase()));
|
|
129
|
-
const currentNames = new Set(newestMaint.maintainers.map(m => m.name.toLowerCase()));
|
|
130
|
-
|
|
131
|
-
// Detect NEW_MAINTAINER: maintainers in newest that weren't in previous
|
|
132
|
-
for (const m of newestMaint.maintainers) {
|
|
133
|
-
if (m.name && !previousNames.has(m.name.toLowerCase())) {
|
|
134
|
-
const risk = analyzeMaintainerRisk(m);
|
|
135
|
-
findings.push({
|
|
136
|
-
type: 'new_maintainer',
|
|
137
|
-
severity: risk.riskLevel === 'HIGH' ? 'CRITICAL' : 'HIGH',
|
|
138
|
-
maintainer: m,
|
|
139
|
-
riskAssessment: risk,
|
|
140
|
-
description: `New maintainer '${m.name}' added between v${previousEntry.version} and v${newestEntry.version}`
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Detect SUSPICIOUS_MAINTAINER: current maintainers with HIGH risk names
|
|
146
|
-
for (const m of maintainersInfo.current) {
|
|
147
|
-
const risk = analyzeMaintainerRisk(m);
|
|
148
|
-
if (risk.riskLevel === 'HIGH') {
|
|
149
|
-
// Avoid duplicate if already reported as new_maintainer
|
|
150
|
-
const alreadyReported = findings.some(
|
|
151
|
-
f => f.type === 'new_maintainer' && f.maintainer.name === m.name
|
|
152
|
-
);
|
|
153
|
-
if (!alreadyReported) {
|
|
154
|
-
findings.push({
|
|
155
|
-
type: 'suspicious_maintainer',
|
|
156
|
-
severity: 'HIGH',
|
|
157
|
-
maintainer: m,
|
|
158
|
-
riskAssessment: risk,
|
|
159
|
-
description: `Suspicious maintainer '${m.name}': ${risk.reasons.join(', ')}`
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Detect SOLE_MAINTAINER_CHANGE: the only maintainer changed
|
|
166
|
-
if (previousMaint.maintainers.length === 1 && newestMaint.maintainers.length === 1) {
|
|
167
|
-
const prevName = previousMaint.maintainers[0].name.toLowerCase();
|
|
168
|
-
const newName = newestMaint.maintainers[0].name.toLowerCase();
|
|
169
|
-
if (prevName && newName && prevName !== newName) {
|
|
170
|
-
const risk = analyzeMaintainerRisk(newestMaint.maintainers[0]);
|
|
171
|
-
// Avoid duplicate if already reported as new_maintainer
|
|
172
|
-
const alreadyReported = findings.some(
|
|
173
|
-
f => f.type === 'new_maintainer' && f.maintainer.name.toLowerCase() === newName
|
|
174
|
-
);
|
|
175
|
-
if (!alreadyReported) {
|
|
176
|
-
findings.push({
|
|
177
|
-
type: 'sole_maintainer_change',
|
|
178
|
-
severity: risk.riskLevel === 'HIGH' ? 'CRITICAL' : 'HIGH',
|
|
179
|
-
maintainer: newestMaint.maintainers[0],
|
|
180
|
-
previousMaintainer: previousMaint.maintainers[0],
|
|
181
|
-
riskAssessment: risk,
|
|
182
|
-
description: `Sole maintainer changed from '${previousMaint.maintainers[0].name}' to '${newestMaint.maintainers[0].name}'`
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Detect publisher change (different _npmUser between versions)
|
|
189
|
-
if (newestMaint.publisher && previousMaint.publisher) {
|
|
190
|
-
const prevPublisher = previousMaint.publisher.name.toLowerCase();
|
|
191
|
-
const newPublisher = newestMaint.publisher.name.toLowerCase();
|
|
192
|
-
if (prevPublisher && newPublisher && prevPublisher !== newPublisher) {
|
|
193
|
-
// Check if the new publisher is in the previous maintainers list
|
|
194
|
-
if (!previousNames.has(newPublisher)) {
|
|
195
|
-
const risk = analyzeMaintainerRisk(newestMaint.publisher);
|
|
196
|
-
findings.push({
|
|
197
|
-
type: 'new_publisher',
|
|
198
|
-
severity: risk.riskLevel === 'HIGH' ? 'CRITICAL' : 'HIGH',
|
|
199
|
-
maintainer: newestMaint.publisher,
|
|
200
|
-
previousPublisher: previousMaint.publisher,
|
|
201
|
-
riskAssessment: risk,
|
|
202
|
-
description: `New publisher '${newestMaint.publisher.name}' (previously '${previousMaint.publisher.name}')`
|
|
203
|
-
});
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return {
|
|
209
|
-
packageName,
|
|
210
|
-
suspicious: findings.length > 0,
|
|
211
|
-
findings,
|
|
212
|
-
maintainers: maintainersInfo
|
|
213
|
-
};
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
module.exports = {
|
|
217
|
-
getMaintainersHistory,
|
|
218
|
-
analyzeMaintainerRisk,
|
|
219
|
-
detectMaintainerChange,
|
|
220
|
-
getVersionMaintainers,
|
|
221
|
-
GENERIC_NAME_PATTERNS,
|
|
222
|
-
MIN_NAME_LENGTH,
|
|
223
|
-
DIGIT_RATIO_THRESHOLD
|
|
224
|
-
};
|
|
1
|
+
const { fetchPackageMetadata, getLatestVersions } = require('./temporal-analysis.js');
|
|
2
|
+
|
|
3
|
+
// Patterns that indicate generic/suspicious maintainer names
|
|
4
|
+
const GENERIC_NAME_PATTERNS = [
|
|
5
|
+
/^npm-user-\w+$/i,
|
|
6
|
+
/^user\d+$/i,
|
|
7
|
+
/^test$/i,
|
|
8
|
+
/^admin$/i,
|
|
9
|
+
/^root$/i,
|
|
10
|
+
/^default$/i,
|
|
11
|
+
/^temp$/i,
|
|
12
|
+
/^tmp$/i,
|
|
13
|
+
/^owner$/i,
|
|
14
|
+
/^maintainer$/i
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
const MIN_NAME_LENGTH = 3;
|
|
18
|
+
const DIGIT_RATIO_THRESHOLD = 0.5;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Extract current maintainers from package metadata.
|
|
22
|
+
* @param {object} metadata - Full registry metadata from fetchPackageMetadata()
|
|
23
|
+
* @returns {{ current: Array<{name: string, email: string}>, count: number }}
|
|
24
|
+
*/
|
|
25
|
+
function getMaintainersHistory(metadata) {
|
|
26
|
+
if (!metadata || !metadata.maintainers || !Array.isArray(metadata.maintainers)) {
|
|
27
|
+
return { current: [], count: 0 };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const current = metadata.maintainers.map(m => ({
|
|
31
|
+
name: m.name || '',
|
|
32
|
+
email: m.email || ''
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
return { current, count: current.length };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Evaluate the risk level of a maintainer based on their name.
|
|
40
|
+
* @param {{ name: string, email: string }} maintainer
|
|
41
|
+
* @returns {{ riskLevel: string, reasons: string[] }}
|
|
42
|
+
*/
|
|
43
|
+
function analyzeMaintainerRisk(maintainer) {
|
|
44
|
+
const reasons = [];
|
|
45
|
+
const name = (maintainer && maintainer.name) || '';
|
|
46
|
+
|
|
47
|
+
if (!name) {
|
|
48
|
+
return { riskLevel: 'HIGH', reasons: ['Empty maintainer name'] };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Check generic name patterns
|
|
52
|
+
for (const pattern of GENERIC_NAME_PATTERNS) {
|
|
53
|
+
if (pattern.test(name)) {
|
|
54
|
+
reasons.push(`Generic name pattern: "${name}"`);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check very short name
|
|
60
|
+
if (name.length < MIN_NAME_LENGTH) {
|
|
61
|
+
reasons.push(`Very short name (${name.length} chars)`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Check digit ratio
|
|
65
|
+
const digitCount = (name.match(/\d/g) || []).length;
|
|
66
|
+
const digitRatio = digitCount / name.length;
|
|
67
|
+
if (digitRatio >= DIGIT_RATIO_THRESHOLD && name.length >= MIN_NAME_LENGTH) {
|
|
68
|
+
reasons.push(`High digit ratio (${(digitRatio * 100).toFixed(0)}% digits)`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (reasons.length > 0) {
|
|
72
|
+
return { riskLevel: 'HIGH', reasons };
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return { riskLevel: 'LOW', reasons: [] };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Get maintainers associated with a specific version from metadata.
|
|
80
|
+
* Uses _npmUser (who published) and maintainers list from the version entry.
|
|
81
|
+
* @param {object} versionData - metadata.versions[version]
|
|
82
|
+
* @returns {{ publisher: {name: string, email: string}|null, maintainers: Array<{name: string, email: string}> }}
|
|
83
|
+
*/
|
|
84
|
+
function getVersionMaintainers(versionData) {
|
|
85
|
+
if (!versionData) return { publisher: null, maintainers: [] };
|
|
86
|
+
|
|
87
|
+
const publisher = versionData._npmUser
|
|
88
|
+
? { name: versionData._npmUser.name || '', email: versionData._npmUser.email || '' }
|
|
89
|
+
: null;
|
|
90
|
+
|
|
91
|
+
const maintainers = Array.isArray(versionData.maintainers)
|
|
92
|
+
? versionData.maintainers.map(m => ({ name: m.name || '', email: m.email || '' }))
|
|
93
|
+
: [];
|
|
94
|
+
|
|
95
|
+
return { publisher, maintainers };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Detect maintainer changes between two versions.
|
|
100
|
+
* @param {string} packageName - npm package name
|
|
101
|
+
* @returns {Promise<object>} Detection result
|
|
102
|
+
*/
|
|
103
|
+
async function detectMaintainerChange(packageName) {
|
|
104
|
+
const metadata = await fetchPackageMetadata(packageName);
|
|
105
|
+
const maintainersInfo = getMaintainersHistory(metadata);
|
|
106
|
+
const latest = getLatestVersions(metadata, 2);
|
|
107
|
+
|
|
108
|
+
if (latest.length < 2) {
|
|
109
|
+
return {
|
|
110
|
+
packageName,
|
|
111
|
+
suspicious: false,
|
|
112
|
+
findings: [],
|
|
113
|
+
maintainers: maintainersInfo
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const [newestEntry, previousEntry] = latest;
|
|
118
|
+
const versions = metadata.versions || {};
|
|
119
|
+
const newestData = versions[newestEntry.version];
|
|
120
|
+
const previousData = versions[previousEntry.version];
|
|
121
|
+
|
|
122
|
+
const newestMaint = getVersionMaintainers(newestData);
|
|
123
|
+
const previousMaint = getVersionMaintainers(previousData);
|
|
124
|
+
|
|
125
|
+
const findings = [];
|
|
126
|
+
|
|
127
|
+
// Build name sets for comparison
|
|
128
|
+
const previousNames = new Set(previousMaint.maintainers.map(m => m.name.toLowerCase()));
|
|
129
|
+
const currentNames = new Set(newestMaint.maintainers.map(m => m.name.toLowerCase()));
|
|
130
|
+
|
|
131
|
+
// Detect NEW_MAINTAINER: maintainers in newest that weren't in previous
|
|
132
|
+
for (const m of newestMaint.maintainers) {
|
|
133
|
+
if (m.name && !previousNames.has(m.name.toLowerCase())) {
|
|
134
|
+
const risk = analyzeMaintainerRisk(m);
|
|
135
|
+
findings.push({
|
|
136
|
+
type: 'new_maintainer',
|
|
137
|
+
severity: risk.riskLevel === 'HIGH' ? 'CRITICAL' : 'HIGH',
|
|
138
|
+
maintainer: m,
|
|
139
|
+
riskAssessment: risk,
|
|
140
|
+
description: `New maintainer '${m.name}' added between v${previousEntry.version} and v${newestEntry.version}`
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Detect SUSPICIOUS_MAINTAINER: current maintainers with HIGH risk names
|
|
146
|
+
for (const m of maintainersInfo.current) {
|
|
147
|
+
const risk = analyzeMaintainerRisk(m);
|
|
148
|
+
if (risk.riskLevel === 'HIGH') {
|
|
149
|
+
// Avoid duplicate if already reported as new_maintainer
|
|
150
|
+
const alreadyReported = findings.some(
|
|
151
|
+
f => f.type === 'new_maintainer' && f.maintainer.name === m.name
|
|
152
|
+
);
|
|
153
|
+
if (!alreadyReported) {
|
|
154
|
+
findings.push({
|
|
155
|
+
type: 'suspicious_maintainer',
|
|
156
|
+
severity: 'HIGH',
|
|
157
|
+
maintainer: m,
|
|
158
|
+
riskAssessment: risk,
|
|
159
|
+
description: `Suspicious maintainer '${m.name}': ${risk.reasons.join(', ')}`
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Detect SOLE_MAINTAINER_CHANGE: the only maintainer changed
|
|
166
|
+
if (previousMaint.maintainers.length === 1 && newestMaint.maintainers.length === 1) {
|
|
167
|
+
const prevName = previousMaint.maintainers[0].name.toLowerCase();
|
|
168
|
+
const newName = newestMaint.maintainers[0].name.toLowerCase();
|
|
169
|
+
if (prevName && newName && prevName !== newName) {
|
|
170
|
+
const risk = analyzeMaintainerRisk(newestMaint.maintainers[0]);
|
|
171
|
+
// Avoid duplicate if already reported as new_maintainer
|
|
172
|
+
const alreadyReported = findings.some(
|
|
173
|
+
f => f.type === 'new_maintainer' && f.maintainer.name.toLowerCase() === newName
|
|
174
|
+
);
|
|
175
|
+
if (!alreadyReported) {
|
|
176
|
+
findings.push({
|
|
177
|
+
type: 'sole_maintainer_change',
|
|
178
|
+
severity: risk.riskLevel === 'HIGH' ? 'CRITICAL' : 'HIGH',
|
|
179
|
+
maintainer: newestMaint.maintainers[0],
|
|
180
|
+
previousMaintainer: previousMaint.maintainers[0],
|
|
181
|
+
riskAssessment: risk,
|
|
182
|
+
description: `Sole maintainer changed from '${previousMaint.maintainers[0].name}' to '${newestMaint.maintainers[0].name}'`
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Detect publisher change (different _npmUser between versions)
|
|
189
|
+
if (newestMaint.publisher && previousMaint.publisher) {
|
|
190
|
+
const prevPublisher = previousMaint.publisher.name.toLowerCase();
|
|
191
|
+
const newPublisher = newestMaint.publisher.name.toLowerCase();
|
|
192
|
+
if (prevPublisher && newPublisher && prevPublisher !== newPublisher) {
|
|
193
|
+
// Check if the new publisher is in the previous maintainers list
|
|
194
|
+
if (!previousNames.has(newPublisher)) {
|
|
195
|
+
const risk = analyzeMaintainerRisk(newestMaint.publisher);
|
|
196
|
+
findings.push({
|
|
197
|
+
type: 'new_publisher',
|
|
198
|
+
severity: risk.riskLevel === 'HIGH' ? 'CRITICAL' : 'HIGH',
|
|
199
|
+
maintainer: newestMaint.publisher,
|
|
200
|
+
previousPublisher: previousMaint.publisher,
|
|
201
|
+
riskAssessment: risk,
|
|
202
|
+
description: `New publisher '${newestMaint.publisher.name}' (previously '${previousMaint.publisher.name}')`
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
packageName,
|
|
210
|
+
suspicious: findings.length > 0,
|
|
211
|
+
findings,
|
|
212
|
+
maintainers: maintainersInfo
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = {
|
|
217
|
+
getMaintainersHistory,
|
|
218
|
+
analyzeMaintainerRisk,
|
|
219
|
+
detectMaintainerChange,
|
|
220
|
+
getVersionMaintainers,
|
|
221
|
+
GENERIC_NAME_PATTERNS,
|
|
222
|
+
MIN_NAME_LENGTH,
|
|
223
|
+
DIGIT_RATIO_THRESHOLD
|
|
224
|
+
};
|