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/publish-anomaly.js
CHANGED
|
@@ -1,206 +1,206 @@
|
|
|
1
|
-
const { fetchPackageMetadata } = require('./temporal-analysis.js');
|
|
2
|
-
|
|
3
|
-
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
4
|
-
const MS_PER_HOUR = 60 * 60 * 1000;
|
|
5
|
-
const BURST_WINDOW_MS = 24 * MS_PER_HOUR; // 24h
|
|
6
|
-
const BURST_MIN_VERSIONS = 3;
|
|
7
|
-
const RAPID_WINDOW_MS = MS_PER_HOUR; // 1h
|
|
8
|
-
const RAPID_MIN_VERSIONS = 2;
|
|
9
|
-
const DORMANT_THRESHOLD_MS = 180 * MS_PER_DAY; // 6 months
|
|
10
|
-
const MIN_VERSIONS_FOR_ANALYSIS = 3;
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Analyze the publish frequency of an npm package.
|
|
14
|
-
* @param {object} metadata - Full registry metadata from fetchPackageMetadata()
|
|
15
|
-
* @returns {{ totalVersions: number, avgIntervalDays: number, stdDevDays: number, lastPublishedAt: string|null, publishHistory: Array<{version: string, date: string}> }}
|
|
16
|
-
*/
|
|
17
|
-
function analyzePublishFrequency(metadata) {
|
|
18
|
-
const time = metadata && metadata.time;
|
|
19
|
-
if (!time || typeof time !== 'object') {
|
|
20
|
-
return {
|
|
21
|
-
totalVersions: 0,
|
|
22
|
-
avgIntervalDays: 0,
|
|
23
|
-
stdDevDays: 0,
|
|
24
|
-
lastPublishedAt: null,
|
|
25
|
-
publishHistory: []
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const versions = metadata.versions || {};
|
|
30
|
-
const entries = [];
|
|
31
|
-
for (const [version, publishedAt] of Object.entries(time)) {
|
|
32
|
-
if (version === 'created' || version === 'modified') continue;
|
|
33
|
-
if (!versions[version]) continue;
|
|
34
|
-
entries.push({ version, date: publishedAt });
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// Sort chronologically (oldest first)
|
|
38
|
-
entries.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
39
|
-
|
|
40
|
-
if (entries.length === 0) {
|
|
41
|
-
return {
|
|
42
|
-
totalVersions: 0,
|
|
43
|
-
avgIntervalDays: 0,
|
|
44
|
-
stdDevDays: 0,
|
|
45
|
-
lastPublishedAt: null,
|
|
46
|
-
publishHistory: []
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Calculate intervals between consecutive publications
|
|
51
|
-
const intervals = [];
|
|
52
|
-
for (let i = 1; i < entries.length; i++) {
|
|
53
|
-
const diffMs = new Date(entries[i].date) - new Date(entries[i - 1].date);
|
|
54
|
-
intervals.push(diffMs / MS_PER_DAY);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
let avgIntervalDays = 0;
|
|
58
|
-
let stdDevDays = 0;
|
|
59
|
-
|
|
60
|
-
if (intervals.length > 0) {
|
|
61
|
-
avgIntervalDays = intervals.reduce((sum, d) => sum + d, 0) / intervals.length;
|
|
62
|
-
|
|
63
|
-
const variance = intervals.reduce((sum, d) => sum + (d - avgIntervalDays) ** 2, 0) / intervals.length;
|
|
64
|
-
stdDevDays = Math.sqrt(variance);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
return {
|
|
68
|
-
totalVersions: entries.length,
|
|
69
|
-
avgIntervalDays: Math.round(avgIntervalDays * 100) / 100,
|
|
70
|
-
stdDevDays: Math.round(stdDevDays * 100) / 100,
|
|
71
|
-
lastPublishedAt: entries[entries.length - 1].date,
|
|
72
|
-
publishHistory: entries
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Detect publish frequency anomalies for an npm package.
|
|
78
|
-
* @param {string} packageName - npm package name
|
|
79
|
-
* @returns {Promise<object>} Detection result with suspicious flag, findings, and stats
|
|
80
|
-
*/
|
|
81
|
-
async function detectPublishAnomaly(packageName) {
|
|
82
|
-
let metadata;
|
|
83
|
-
try {
|
|
84
|
-
metadata = await fetchPackageMetadata(packageName);
|
|
85
|
-
} catch {
|
|
86
|
-
return {
|
|
87
|
-
packageName,
|
|
88
|
-
suspicious: false,
|
|
89
|
-
anomalies: [],
|
|
90
|
-
stats: { totalVersions: 0, avgIntervalDays: 0, stdDevDays: 0, lastPublishedAt: null, publishHistory: [] }
|
|
91
|
-
};
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (!metadata || !metadata.time || !metadata.versions) {
|
|
95
|
-
return {
|
|
96
|
-
packageName,
|
|
97
|
-
suspicious: false,
|
|
98
|
-
anomalies: [],
|
|
99
|
-
stats: { totalVersions: 0, avgIntervalDays: 0, stdDevDays: 0, lastPublishedAt: null, publishHistory: [] }
|
|
100
|
-
};
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const stats = analyzePublishFrequency(metadata);
|
|
104
|
-
|
|
105
|
-
if (stats.totalVersions < MIN_VERSIONS_FOR_ANALYSIS) {
|
|
106
|
-
return {
|
|
107
|
-
packageName,
|
|
108
|
-
suspicious: false,
|
|
109
|
-
anomalies: [],
|
|
110
|
-
stats
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const findings = [];
|
|
115
|
-
const history = stats.publishHistory;
|
|
116
|
-
|
|
117
|
-
// --- BURST: 3+ versions published within a 24h window ---
|
|
118
|
-
for (let i = 0; i < history.length; i++) {
|
|
119
|
-
const windowStart = new Date(history[i].date).getTime();
|
|
120
|
-
const windowEnd = windowStart + BURST_WINDOW_MS;
|
|
121
|
-
const inWindow = [];
|
|
122
|
-
for (let j = i; j < history.length; j++) {
|
|
123
|
-
const t = new Date(history[j].date).getTime();
|
|
124
|
-
if (t <= windowEnd) {
|
|
125
|
-
inWindow.push(history[j]);
|
|
126
|
-
} else {
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
if (inWindow.length >= BURST_MIN_VERSIONS) {
|
|
131
|
-
const spanMs = new Date(inWindow[inWindow.length - 1].date) - new Date(inWindow[0].date);
|
|
132
|
-
const spanHours = Math.round(spanMs / MS_PER_HOUR * 10) / 10;
|
|
133
|
-
findings.push({
|
|
134
|
-
type: 'publish_burst',
|
|
135
|
-
severity: 'HIGH',
|
|
136
|
-
description: `${inWindow.length} versions published in ${spanHours} hours (avg interval: ${stats.avgIntervalDays} days)`,
|
|
137
|
-
versions: inWindow.map(e => e.version)
|
|
138
|
-
});
|
|
139
|
-
break; // report only the first (largest) burst window
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// --- DORMANT_SPIKE: 6+ months without publication, then new version ---
|
|
144
|
-
if (history.length >= 2) {
|
|
145
|
-
const lastDate = new Date(history[history.length - 1].date).getTime();
|
|
146
|
-
const prevDate = new Date(history[history.length - 2].date).getTime();
|
|
147
|
-
const gapMs = lastDate - prevDate;
|
|
148
|
-
|
|
149
|
-
if (gapMs >= DORMANT_THRESHOLD_MS) {
|
|
150
|
-
const gapDays = Math.round(gapMs / MS_PER_DAY);
|
|
151
|
-
findings.push({
|
|
152
|
-
type: 'dormant_spike',
|
|
153
|
-
severity: 'HIGH',
|
|
154
|
-
description: `Package dormant for ${gapDays} days, then new version published (avg interval: ${stats.avgIntervalDays} days)`,
|
|
155
|
-
versions: [history[history.length - 2].version, history[history.length - 1].version]
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
// --- RAPID_SUCCESSION: 2+ versions in less than 1 hour ---
|
|
161
|
-
for (let i = 0; i < history.length; i++) {
|
|
162
|
-
const windowStart = new Date(history[i].date).getTime();
|
|
163
|
-
const windowEnd = windowStart + RAPID_WINDOW_MS;
|
|
164
|
-
const inWindow = [];
|
|
165
|
-
for (let j = i; j < history.length; j++) {
|
|
166
|
-
const t = new Date(history[j].date).getTime();
|
|
167
|
-
if (t <= windowEnd) {
|
|
168
|
-
inWindow.push(history[j]);
|
|
169
|
-
} else {
|
|
170
|
-
break;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
if (inWindow.length >= RAPID_MIN_VERSIONS) {
|
|
174
|
-
const spanMs = new Date(inWindow[inWindow.length - 1].date) - new Date(inWindow[0].date);
|
|
175
|
-
const spanMinutes = Math.round(spanMs / 60000);
|
|
176
|
-
findings.push({
|
|
177
|
-
type: 'rapid_succession',
|
|
178
|
-
severity: 'MEDIUM',
|
|
179
|
-
description: `${inWindow.length} versions published within ${spanMinutes} minutes`,
|
|
180
|
-
versions: inWindow.map(e => e.version)
|
|
181
|
-
});
|
|
182
|
-
break; // report only the first rapid window
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
return {
|
|
187
|
-
packageName,
|
|
188
|
-
suspicious: findings.length > 0,
|
|
189
|
-
anomalies: findings,
|
|
190
|
-
stats
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
module.exports = {
|
|
195
|
-
analyzePublishFrequency,
|
|
196
|
-
detectPublishAnomaly,
|
|
197
|
-
// Exported for testing
|
|
198
|
-
MS_PER_DAY,
|
|
199
|
-
MS_PER_HOUR,
|
|
200
|
-
BURST_WINDOW_MS,
|
|
201
|
-
BURST_MIN_VERSIONS,
|
|
202
|
-
RAPID_WINDOW_MS,
|
|
203
|
-
RAPID_MIN_VERSIONS,
|
|
204
|
-
DORMANT_THRESHOLD_MS,
|
|
205
|
-
MIN_VERSIONS_FOR_ANALYSIS
|
|
206
|
-
};
|
|
1
|
+
const { fetchPackageMetadata } = require('./temporal-analysis.js');
|
|
2
|
+
|
|
3
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000;
|
|
4
|
+
const MS_PER_HOUR = 60 * 60 * 1000;
|
|
5
|
+
const BURST_WINDOW_MS = 24 * MS_PER_HOUR; // 24h
|
|
6
|
+
const BURST_MIN_VERSIONS = 3;
|
|
7
|
+
const RAPID_WINDOW_MS = MS_PER_HOUR; // 1h
|
|
8
|
+
const RAPID_MIN_VERSIONS = 2;
|
|
9
|
+
const DORMANT_THRESHOLD_MS = 180 * MS_PER_DAY; // 6 months
|
|
10
|
+
const MIN_VERSIONS_FOR_ANALYSIS = 3;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Analyze the publish frequency of an npm package.
|
|
14
|
+
* @param {object} metadata - Full registry metadata from fetchPackageMetadata()
|
|
15
|
+
* @returns {{ totalVersions: number, avgIntervalDays: number, stdDevDays: number, lastPublishedAt: string|null, publishHistory: Array<{version: string, date: string}> }}
|
|
16
|
+
*/
|
|
17
|
+
function analyzePublishFrequency(metadata) {
|
|
18
|
+
const time = metadata && metadata.time;
|
|
19
|
+
if (!time || typeof time !== 'object') {
|
|
20
|
+
return {
|
|
21
|
+
totalVersions: 0,
|
|
22
|
+
avgIntervalDays: 0,
|
|
23
|
+
stdDevDays: 0,
|
|
24
|
+
lastPublishedAt: null,
|
|
25
|
+
publishHistory: []
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const versions = metadata.versions || {};
|
|
30
|
+
const entries = [];
|
|
31
|
+
for (const [version, publishedAt] of Object.entries(time)) {
|
|
32
|
+
if (version === 'created' || version === 'modified') continue;
|
|
33
|
+
if (!versions[version]) continue;
|
|
34
|
+
entries.push({ version, date: publishedAt });
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Sort chronologically (oldest first)
|
|
38
|
+
entries.sort((a, b) => new Date(a.date) - new Date(b.date));
|
|
39
|
+
|
|
40
|
+
if (entries.length === 0) {
|
|
41
|
+
return {
|
|
42
|
+
totalVersions: 0,
|
|
43
|
+
avgIntervalDays: 0,
|
|
44
|
+
stdDevDays: 0,
|
|
45
|
+
lastPublishedAt: null,
|
|
46
|
+
publishHistory: []
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Calculate intervals between consecutive publications
|
|
51
|
+
const intervals = [];
|
|
52
|
+
for (let i = 1; i < entries.length; i++) {
|
|
53
|
+
const diffMs = new Date(entries[i].date) - new Date(entries[i - 1].date);
|
|
54
|
+
intervals.push(diffMs / MS_PER_DAY);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let avgIntervalDays = 0;
|
|
58
|
+
let stdDevDays = 0;
|
|
59
|
+
|
|
60
|
+
if (intervals.length > 0) {
|
|
61
|
+
avgIntervalDays = intervals.reduce((sum, d) => sum + d, 0) / intervals.length;
|
|
62
|
+
|
|
63
|
+
const variance = intervals.reduce((sum, d) => sum + (d - avgIntervalDays) ** 2, 0) / intervals.length;
|
|
64
|
+
stdDevDays = Math.sqrt(variance);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
totalVersions: entries.length,
|
|
69
|
+
avgIntervalDays: Math.round(avgIntervalDays * 100) / 100,
|
|
70
|
+
stdDevDays: Math.round(stdDevDays * 100) / 100,
|
|
71
|
+
lastPublishedAt: entries[entries.length - 1].date,
|
|
72
|
+
publishHistory: entries
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Detect publish frequency anomalies for an npm package.
|
|
78
|
+
* @param {string} packageName - npm package name
|
|
79
|
+
* @returns {Promise<object>} Detection result with suspicious flag, findings, and stats
|
|
80
|
+
*/
|
|
81
|
+
async function detectPublishAnomaly(packageName) {
|
|
82
|
+
let metadata;
|
|
83
|
+
try {
|
|
84
|
+
metadata = await fetchPackageMetadata(packageName);
|
|
85
|
+
} catch {
|
|
86
|
+
return {
|
|
87
|
+
packageName,
|
|
88
|
+
suspicious: false,
|
|
89
|
+
anomalies: [],
|
|
90
|
+
stats: { totalVersions: 0, avgIntervalDays: 0, stdDevDays: 0, lastPublishedAt: null, publishHistory: [] }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!metadata || !metadata.time || !metadata.versions) {
|
|
95
|
+
return {
|
|
96
|
+
packageName,
|
|
97
|
+
suspicious: false,
|
|
98
|
+
anomalies: [],
|
|
99
|
+
stats: { totalVersions: 0, avgIntervalDays: 0, stdDevDays: 0, lastPublishedAt: null, publishHistory: [] }
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const stats = analyzePublishFrequency(metadata);
|
|
104
|
+
|
|
105
|
+
if (stats.totalVersions < MIN_VERSIONS_FOR_ANALYSIS) {
|
|
106
|
+
return {
|
|
107
|
+
packageName,
|
|
108
|
+
suspicious: false,
|
|
109
|
+
anomalies: [],
|
|
110
|
+
stats
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const findings = [];
|
|
115
|
+
const history = stats.publishHistory;
|
|
116
|
+
|
|
117
|
+
// --- BURST: 3+ versions published within a 24h window ---
|
|
118
|
+
for (let i = 0; i < history.length; i++) {
|
|
119
|
+
const windowStart = new Date(history[i].date).getTime();
|
|
120
|
+
const windowEnd = windowStart + BURST_WINDOW_MS;
|
|
121
|
+
const inWindow = [];
|
|
122
|
+
for (let j = i; j < history.length; j++) {
|
|
123
|
+
const t = new Date(history[j].date).getTime();
|
|
124
|
+
if (t <= windowEnd) {
|
|
125
|
+
inWindow.push(history[j]);
|
|
126
|
+
} else {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (inWindow.length >= BURST_MIN_VERSIONS) {
|
|
131
|
+
const spanMs = new Date(inWindow[inWindow.length - 1].date) - new Date(inWindow[0].date);
|
|
132
|
+
const spanHours = Math.round(spanMs / MS_PER_HOUR * 10) / 10;
|
|
133
|
+
findings.push({
|
|
134
|
+
type: 'publish_burst',
|
|
135
|
+
severity: 'HIGH',
|
|
136
|
+
description: `${inWindow.length} versions published in ${spanHours} hours (avg interval: ${stats.avgIntervalDays} days)`,
|
|
137
|
+
versions: inWindow.map(e => e.version)
|
|
138
|
+
});
|
|
139
|
+
break; // report only the first (largest) burst window
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- DORMANT_SPIKE: 6+ months without publication, then new version ---
|
|
144
|
+
if (history.length >= 2) {
|
|
145
|
+
const lastDate = new Date(history[history.length - 1].date).getTime();
|
|
146
|
+
const prevDate = new Date(history[history.length - 2].date).getTime();
|
|
147
|
+
const gapMs = lastDate - prevDate;
|
|
148
|
+
|
|
149
|
+
if (gapMs >= DORMANT_THRESHOLD_MS) {
|
|
150
|
+
const gapDays = Math.round(gapMs / MS_PER_DAY);
|
|
151
|
+
findings.push({
|
|
152
|
+
type: 'dormant_spike',
|
|
153
|
+
severity: 'HIGH',
|
|
154
|
+
description: `Package dormant for ${gapDays} days, then new version published (avg interval: ${stats.avgIntervalDays} days)`,
|
|
155
|
+
versions: [history[history.length - 2].version, history[history.length - 1].version]
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// --- RAPID_SUCCESSION: 2+ versions in less than 1 hour ---
|
|
161
|
+
for (let i = 0; i < history.length; i++) {
|
|
162
|
+
const windowStart = new Date(history[i].date).getTime();
|
|
163
|
+
const windowEnd = windowStart + RAPID_WINDOW_MS;
|
|
164
|
+
const inWindow = [];
|
|
165
|
+
for (let j = i; j < history.length; j++) {
|
|
166
|
+
const t = new Date(history[j].date).getTime();
|
|
167
|
+
if (t <= windowEnd) {
|
|
168
|
+
inWindow.push(history[j]);
|
|
169
|
+
} else {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (inWindow.length >= RAPID_MIN_VERSIONS) {
|
|
174
|
+
const spanMs = new Date(inWindow[inWindow.length - 1].date) - new Date(inWindow[0].date);
|
|
175
|
+
const spanMinutes = Math.round(spanMs / 60000);
|
|
176
|
+
findings.push({
|
|
177
|
+
type: 'rapid_succession',
|
|
178
|
+
severity: 'MEDIUM',
|
|
179
|
+
description: `${inWindow.length} versions published within ${spanMinutes} minutes`,
|
|
180
|
+
versions: inWindow.map(e => e.version)
|
|
181
|
+
});
|
|
182
|
+
break; // report only the first rapid window
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return {
|
|
187
|
+
packageName,
|
|
188
|
+
suspicious: findings.length > 0,
|
|
189
|
+
anomalies: findings,
|
|
190
|
+
stats
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
analyzePublishFrequency,
|
|
196
|
+
detectPublishAnomaly,
|
|
197
|
+
// Exported for testing
|
|
198
|
+
MS_PER_DAY,
|
|
199
|
+
MS_PER_HOUR,
|
|
200
|
+
BURST_WINDOW_MS,
|
|
201
|
+
BURST_MIN_VERSIONS,
|
|
202
|
+
RAPID_WINDOW_MS,
|
|
203
|
+
RAPID_MIN_VERSIONS,
|
|
204
|
+
DORMANT_THRESHOLD_MS,
|
|
205
|
+
MIN_VERSIONS_FOR_ANALYSIS
|
|
206
|
+
};
|