muaddib-scanner 1.3.1 → 1.4.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/package.json +1 -1
- package/src/daemon.js +28 -12
- package/src/diff.js +23 -16
- package/src/index.js +16 -8
- package/src/ioc/data/iocs.json +17578 -16018
- package/src/ioc/scraper.js +14 -12
- package/src/ioc/updater.js +8 -3
- package/src/report.js +4 -4
- package/src/response/playbooks.js +30 -0
- package/src/safe-install.js +5 -96
- package/src/sarif.js +11 -2
- package/src/scanner/ast.js +2 -12
- package/src/scanner/dataflow.js +14 -20
- package/src/scanner/dependencies.js +1 -90
- package/src/scanner/obfuscation.js +4 -26
- package/src/scanner/package.js +10 -4
- package/src/scanner/shell.js +4 -24
- package/src/scanner/typosquat.js +3 -3
- package/src/shared/constants.js +97 -0
- package/src/utils.js +79 -11
- package/src/watch.js +5 -5
- package/src/webhook.js +36 -14
- package/src/ioc/feeds.js +0 -63
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -22,12 +22,20 @@ async function startDaemon(options = {}) {
|
|
|
22
22
|
|
|
23
23
|
// Surveille le dossier courant
|
|
24
24
|
const cwd = process.cwd();
|
|
25
|
-
watchDirectory(cwd);
|
|
25
|
+
const watchers = watchDirectory(cwd);
|
|
26
|
+
|
|
27
|
+
// Cleanup function to close all watchers
|
|
28
|
+
function cleanup() {
|
|
29
|
+
for (const w of watchers) {
|
|
30
|
+
try { w.close(); } catch { /* ignore */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
26
33
|
|
|
27
34
|
// Garde le processus actif
|
|
28
35
|
process.on('SIGINT', () => {
|
|
29
36
|
console.log('\n[DAEMON] Arret...');
|
|
30
37
|
isRunning = false;
|
|
38
|
+
cleanup();
|
|
31
39
|
process.exit(0);
|
|
32
40
|
});
|
|
33
41
|
|
|
@@ -38,6 +46,7 @@ async function startDaemon(options = {}) {
|
|
|
38
46
|
}
|
|
39
47
|
|
|
40
48
|
function watchDirectory(dir) {
|
|
49
|
+
const watchers = [];
|
|
41
50
|
const nodeModulesPath = path.join(dir, 'node_modules');
|
|
42
51
|
const packageLockPath = path.join(dir, 'package-lock.json');
|
|
43
52
|
const yarnLockPath = path.join(dir, 'yarn.lock');
|
|
@@ -46,21 +55,21 @@ function watchDirectory(dir) {
|
|
|
46
55
|
|
|
47
56
|
// Surveille package-lock.json
|
|
48
57
|
if (fs.existsSync(packageLockPath)) {
|
|
49
|
-
watchFile(packageLockPath, dir);
|
|
58
|
+
watchers.push(watchFile(packageLockPath, dir));
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
// Surveille yarn.lock
|
|
53
62
|
if (fs.existsSync(yarnLockPath)) {
|
|
54
|
-
watchFile(yarnLockPath, dir);
|
|
63
|
+
watchers.push(watchFile(yarnLockPath, dir));
|
|
55
64
|
}
|
|
56
65
|
|
|
57
66
|
// Surveille node_modules
|
|
58
67
|
if (fs.existsSync(nodeModulesPath)) {
|
|
59
|
-
watchNodeModules(nodeModulesPath, dir);
|
|
68
|
+
watchers.push(watchNodeModules(nodeModulesPath, dir));
|
|
60
69
|
}
|
|
61
70
|
|
|
62
71
|
// Surveille la creation de node_modules
|
|
63
|
-
fs.watch(dir, (eventType, filename) => {
|
|
72
|
+
const dirWatcher = fs.watch(dir, (eventType, filename) => {
|
|
64
73
|
if (filename === 'node_modules' && eventType === 'rename') {
|
|
65
74
|
const nmPath = path.join(dir, 'node_modules');
|
|
66
75
|
if (fs.existsSync(nmPath)) {
|
|
@@ -73,25 +82,32 @@ function watchDirectory(dir) {
|
|
|
73
82
|
triggerScan(dir);
|
|
74
83
|
}
|
|
75
84
|
});
|
|
85
|
+
watchers.push(dirWatcher);
|
|
86
|
+
|
|
87
|
+
return watchers;
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
function watchFile(filePath, projectDir) {
|
|
79
91
|
let lastMtime = fs.statSync(filePath).mtime.getTime();
|
|
80
92
|
|
|
81
|
-
fs.watch(filePath, (eventType) => {
|
|
93
|
+
return fs.watch(filePath, (eventType) => {
|
|
82
94
|
if (eventType === 'change') {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
lastMtime
|
|
86
|
-
|
|
87
|
-
|
|
95
|
+
try {
|
|
96
|
+
const currentMtime = fs.statSync(filePath).mtime.getTime();
|
|
97
|
+
if (currentMtime !== lastMtime) {
|
|
98
|
+
lastMtime = currentMtime;
|
|
99
|
+
console.log(`[DAEMON] ${path.basename(filePath)} modifie`);
|
|
100
|
+
triggerScan(projectDir);
|
|
101
|
+
}
|
|
102
|
+
} catch {
|
|
103
|
+
// File may have been deleted between watch trigger and stat
|
|
88
104
|
}
|
|
89
105
|
}
|
|
90
106
|
});
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
function watchNodeModules(nodeModulesPath, projectDir) {
|
|
94
|
-
fs.watch(nodeModulesPath, { recursive: true }, (eventType, filename) => {
|
|
110
|
+
return fs.watch(nodeModulesPath, { recursive: true }, (eventType, filename) => {
|
|
95
111
|
if (filename && filename.includes('package.json')) {
|
|
96
112
|
console.log(`[DAEMON] Nouveau package detecte: ${filename}`);
|
|
97
113
|
triggerScan(projectDir);
|
package/src/diff.js
CHANGED
|
@@ -4,6 +4,9 @@ const path = require('path');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const os = require('os');
|
|
6
6
|
|
|
7
|
+
// Only allow safe characters in git refs (prevents command injection)
|
|
8
|
+
const SAFE_REF_REGEX = /^[a-zA-Z0-9._\-/~^@{}:]+$/;
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* Get the list of commits/tags for comparison suggestions
|
|
9
12
|
*/
|
|
@@ -61,6 +64,9 @@ function getCurrentCommit(targetPath) {
|
|
|
61
64
|
* Resolve a ref (tag, branch, commit) to a commit hash
|
|
62
65
|
*/
|
|
63
66
|
function resolveRef(targetPath, ref) {
|
|
67
|
+
if (!SAFE_REF_REGEX.test(ref)) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
64
70
|
try {
|
|
65
71
|
return execSync(`git rev-parse ${ref}`, {
|
|
66
72
|
cwd: targetPath,
|
|
@@ -92,11 +98,16 @@ function hasUncommittedChanges(targetPath) {
|
|
|
92
98
|
* Create a temporary copy of the repo at a specific commit
|
|
93
99
|
*/
|
|
94
100
|
function createTempCopyAtCommit(targetPath, commitHash) {
|
|
101
|
+
// Sanitize commitHash (should be a hex hash from resolveRef)
|
|
102
|
+
if (!SAFE_REF_REGEX.test(commitHash)) {
|
|
103
|
+
throw new Error('Invalid commit hash');
|
|
104
|
+
}
|
|
105
|
+
|
|
95
106
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'muaddib-diff-'));
|
|
96
107
|
|
|
97
108
|
try {
|
|
98
|
-
// Clone the repo to temp directory
|
|
99
|
-
execSync(`git clone --quiet "${targetPath}" "${tempDir}"`, {
|
|
109
|
+
// Clone the repo to temp directory (use -- to separate paths from options)
|
|
110
|
+
execSync(`git clone --quiet -- "${targetPath}" "${tempDir}"`, {
|
|
100
111
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
101
112
|
});
|
|
102
113
|
|
|
@@ -107,10 +118,11 @@ function createTempCopyAtCommit(targetPath, commitHash) {
|
|
|
107
118
|
});
|
|
108
119
|
|
|
109
120
|
// Install dependencies if package.json exists
|
|
121
|
+
// --ignore-scripts prevents execution of malicious preinstall/postinstall
|
|
110
122
|
const packageJsonPath = path.join(tempDir, 'package.json');
|
|
111
123
|
if (fs.existsSync(packageJsonPath)) {
|
|
112
124
|
try {
|
|
113
|
-
execSync('npm install --quiet --no-audit --no-fund', {
|
|
125
|
+
execSync('npm install --quiet --no-audit --no-fund --ignore-scripts', {
|
|
114
126
|
cwd: tempDir,
|
|
115
127
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
116
128
|
timeout: 60000
|
|
@@ -192,25 +204,20 @@ function compareThreats(oldThreats, newThreats) {
|
|
|
192
204
|
* Run scan and capture results (without console output)
|
|
193
205
|
*/
|
|
194
206
|
async function runSilentScan(targetPath, options = {}) {
|
|
195
|
-
// Capture console.log output
|
|
196
207
|
const originalLog = console.log;
|
|
197
208
|
const logs = [];
|
|
198
|
-
console.log = (...args) => logs.push(args.join(' '));
|
|
199
|
-
|
|
200
209
|
try {
|
|
210
|
+
console.log = (...args) => logs.push(args.join(' '));
|
|
201
211
|
await run(targetPath, { ...options, json: true });
|
|
212
|
+
} finally {
|
|
202
213
|
console.log = originalLog;
|
|
214
|
+
}
|
|
203
215
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return { threats: [], summary: { total: 0 } };
|
|
210
|
-
}
|
|
211
|
-
} catch (err) {
|
|
212
|
-
console.log = originalLog;
|
|
213
|
-
throw err;
|
|
216
|
+
const jsonOutput = logs.join('\n');
|
|
217
|
+
try {
|
|
218
|
+
return JSON.parse(jsonOutput);
|
|
219
|
+
} catch {
|
|
220
|
+
return { threats: [], summary: { total: 0 } };
|
|
214
221
|
}
|
|
215
222
|
}
|
|
216
223
|
|
package/src/index.js
CHANGED
|
@@ -32,7 +32,10 @@ const SEVERITY_WEIGHTS = {
|
|
|
32
32
|
|
|
33
33
|
// MEDIUM: Potential threats (suspicious patterns, light obfuscation)
|
|
34
34
|
// Moderate impact, requires investigation but not necessarily malicious
|
|
35
|
-
MEDIUM: 3
|
|
35
|
+
MEDIUM: 3,
|
|
36
|
+
|
|
37
|
+
// LOW: Informational findings, minimal impact on risk score
|
|
38
|
+
LOW: 1
|
|
36
39
|
};
|
|
37
40
|
|
|
38
41
|
// Thresholds for determining the overall risk level
|
|
@@ -52,12 +55,12 @@ function scanParanoid(targetPath) {
|
|
|
52
55
|
const threats = [];
|
|
53
56
|
|
|
54
57
|
function scanFile(filePath) {
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
try {
|
|
59
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
57
60
|
|
|
58
61
|
// Ignore URLs (they often contain patterns like .git)
|
|
59
62
|
const contentWithoutUrls = content.replace(/https?:\/\/[^\s"']+/g, '');
|
|
60
|
-
|
|
63
|
+
|
|
61
64
|
for (const [, rule] of Object.entries(PARANOID_RULES)) {
|
|
62
65
|
for (const pattern of rule.patterns) {
|
|
63
66
|
if (contentWithoutUrls.includes(pattern)) {
|
|
@@ -75,15 +78,18 @@ function scanParanoid(targetPath) {
|
|
|
75
78
|
// Ignore read errors
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
|
-
|
|
81
|
+
|
|
79
82
|
function walkDir(dir) {
|
|
80
83
|
const excluded = ['node_modules', '.git', 'test', 'tests', 'src', 'vscode-extension', '.muaddib-cache', 'data', 'iocs'];
|
|
81
84
|
try {
|
|
82
85
|
const files = fs.readdirSync(dir);
|
|
83
86
|
for (const file of files) {
|
|
84
87
|
const fullPath = path.join(dir, file);
|
|
85
|
-
|
|
86
|
-
|
|
88
|
+
// Use lstatSync to avoid following symlinks
|
|
89
|
+
const stat = fs.lstatSync(fullPath);
|
|
90
|
+
|
|
91
|
+
if (stat.isSymbolicLink()) continue;
|
|
92
|
+
|
|
87
93
|
if (stat.isDirectory()) {
|
|
88
94
|
if (!excluded.includes(file)) {
|
|
89
95
|
walkDir(fullPath);
|
|
@@ -96,7 +102,7 @@ function scanParanoid(targetPath) {
|
|
|
96
102
|
// Ignore walk errors
|
|
97
103
|
}
|
|
98
104
|
}
|
|
99
|
-
|
|
105
|
+
|
|
100
106
|
walkDir(targetPath);
|
|
101
107
|
return threats;
|
|
102
108
|
}
|
|
@@ -162,11 +168,13 @@ async function run(targetPath, options = {}) {
|
|
|
162
168
|
const criticalCount = threats.filter(t => t.severity === 'CRITICAL').length;
|
|
163
169
|
const highCount = threats.filter(t => t.severity === 'HIGH').length;
|
|
164
170
|
const mediumCount = threats.filter(t => t.severity === 'MEDIUM').length;
|
|
171
|
+
const lowCount = threats.filter(t => t.severity === 'LOW').length;
|
|
165
172
|
|
|
166
173
|
let riskScore = 0;
|
|
167
174
|
riskScore += criticalCount * SEVERITY_WEIGHTS.CRITICAL;
|
|
168
175
|
riskScore += highCount * SEVERITY_WEIGHTS.HIGH;
|
|
169
176
|
riskScore += mediumCount * SEVERITY_WEIGHTS.MEDIUM;
|
|
177
|
+
riskScore += lowCount * SEVERITY_WEIGHTS.LOW;
|
|
170
178
|
riskScore = Math.min(MAX_RISK_SCORE, riskScore);
|
|
171
179
|
|
|
172
180
|
const riskLevel = riskScore >= RISK_THRESHOLDS.CRITICAL ? 'CRITICAL'
|