muaddib-scanner 1.3.1 → 1.4.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.
- package/bin/muaddib.js +9 -1
- package/package.json +1 -1
- package/src/daemon.js +44 -26
- package/src/diff.js +23 -16
- package/src/hooks-init.js +10 -1
- package/src/index.js +17 -8
- package/src/ioc/data/iocs.json +17578 -16018
- package/src/ioc/scraper.js +30 -18
- package/src/ioc/updater.js +57 -17
- package/src/ioc/yaml-loader.js +45 -29
- package/src/report.js +4 -4
- package/src/response/playbooks.js +30 -15
- package/src/rules/index.js +170 -0
- package/src/safe-install.js +8 -99
- package/src/sarif.js +11 -2
- package/src/scanner/ast.js +9 -14
- package/src/scanner/dataflow.js +14 -20
- package/src/scanner/dependencies.js +12 -102
- package/src/scanner/github-actions.js +15 -2
- package/src/scanner/hash.js +3 -78
- package/src/scanner/obfuscation.js +10 -27
- package/src/scanner/package.js +28 -8
- package/src/scanner/shell.js +4 -24
- package/src/scanner/typosquat.js +13 -7
- package/src/shared/constants.js +97 -0
- package/src/utils.js +80 -11
- package/src/watch.js +11 -6
- package/src/webhook.js +36 -14
- package/src/ioc/feeds.js +0 -63
package/bin/muaddib.js
CHANGED
|
@@ -8,7 +8,7 @@ const { runScraper } = require('../src/ioc/scraper.js');
|
|
|
8
8
|
const { safeInstall } = require('../src/safe-install.js');
|
|
9
9
|
const { buildSandboxImage, runSandbox } = require('../src/sandbox.js');
|
|
10
10
|
const { diff, showRefs } = require('../src/diff.js');
|
|
11
|
-
const { initHooks } = require('../src/hooks-init.js');
|
|
11
|
+
const { initHooks, removeHooks } = require('../src/hooks-init.js');
|
|
12
12
|
|
|
13
13
|
const args = process.argv.slice(2);
|
|
14
14
|
const command = args[0];
|
|
@@ -238,6 +238,7 @@ const helpText = `
|
|
|
238
238
|
muaddib watch [path] Watch in real-time
|
|
239
239
|
muaddib daemon [options] Start daemon
|
|
240
240
|
muaddib init-hooks [options] Setup git pre-commit hooks
|
|
241
|
+
muaddib remove-hooks [path] Remove MUAD'DIB git hooks
|
|
241
242
|
muaddib update Update IOCs
|
|
242
243
|
muaddib scrape Scrape new IOCs
|
|
243
244
|
muaddib sandbox <pkg> Analyze in isolated Docker container
|
|
@@ -385,6 +386,13 @@ if (!command || command === '--help' || command === '-h') {
|
|
|
385
386
|
console.error('[ERROR]', err.message);
|
|
386
387
|
process.exit(1);
|
|
387
388
|
});
|
|
389
|
+
} else if (command === 'remove-hooks') {
|
|
390
|
+
removeHooks(target).then(success => {
|
|
391
|
+
process.exit(success ? 0 : 1);
|
|
392
|
+
}).catch(err => {
|
|
393
|
+
console.error('[ERROR]', err.message);
|
|
394
|
+
process.exit(1);
|
|
395
|
+
});
|
|
388
396
|
} else if (command === 'help') {
|
|
389
397
|
console.log(helpText);
|
|
390
398
|
process.exit(0);
|
package/package.json
CHANGED
package/src/daemon.js
CHANGED
|
@@ -22,22 +22,30 @@ 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
26
|
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
27
|
+
// Cleanup function to close all watchers
|
|
28
|
+
function cleanup() {
|
|
29
|
+
for (const w of watchers) {
|
|
30
|
+
try { w.close(); } catch { /* ignore */ }
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Keep process alive until SIGINT
|
|
35
|
+
await new Promise((resolve) => {
|
|
36
|
+
process.on('SIGINT', () => {
|
|
37
|
+
console.log('\n[DAEMON] Arret...');
|
|
38
|
+
isRunning = false;
|
|
39
|
+
cleanup();
|
|
40
|
+
resolve();
|
|
41
|
+
});
|
|
32
42
|
});
|
|
33
43
|
|
|
34
|
-
|
|
35
|
-
while (isRunning) {
|
|
36
|
-
await sleep(1000);
|
|
37
|
-
}
|
|
44
|
+
process.exit(0);
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
function watchDirectory(dir) {
|
|
48
|
+
const watchers = [];
|
|
41
49
|
const nodeModulesPath = path.join(dir, 'node_modules');
|
|
42
50
|
const packageLockPath = path.join(dir, 'package-lock.json');
|
|
43
51
|
const yarnLockPath = path.join(dir, 'yarn.lock');
|
|
@@ -46,21 +54,23 @@ function watchDirectory(dir) {
|
|
|
46
54
|
|
|
47
55
|
// Surveille package-lock.json
|
|
48
56
|
if (fs.existsSync(packageLockPath)) {
|
|
49
|
-
watchFile(packageLockPath, dir);
|
|
57
|
+
const w = watchFile(packageLockPath, dir);
|
|
58
|
+
if (w) watchers.push(w);
|
|
50
59
|
}
|
|
51
60
|
|
|
52
61
|
// Surveille yarn.lock
|
|
53
62
|
if (fs.existsSync(yarnLockPath)) {
|
|
54
|
-
watchFile(yarnLockPath, dir);
|
|
63
|
+
const w = watchFile(yarnLockPath, dir);
|
|
64
|
+
if (w) watchers.push(w);
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
// Surveille node_modules
|
|
58
68
|
if (fs.existsSync(nodeModulesPath)) {
|
|
59
|
-
watchNodeModules(nodeModulesPath, dir);
|
|
69
|
+
watchers.push(watchNodeModules(nodeModulesPath, dir));
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
// Surveille la creation de node_modules
|
|
63
|
-
fs.watch(dir, (eventType, filename) => {
|
|
73
|
+
const dirWatcher = fs.watch(dir, (eventType, filename) => {
|
|
64
74
|
if (filename === 'node_modules' && eventType === 'rename') {
|
|
65
75
|
const nmPath = path.join(dir, 'node_modules');
|
|
66
76
|
if (fs.existsSync(nmPath)) {
|
|
@@ -73,25 +83,37 @@ function watchDirectory(dir) {
|
|
|
73
83
|
triggerScan(dir);
|
|
74
84
|
}
|
|
75
85
|
});
|
|
86
|
+
watchers.push(dirWatcher);
|
|
87
|
+
|
|
88
|
+
return watchers;
|
|
76
89
|
}
|
|
77
90
|
|
|
78
91
|
function watchFile(filePath, projectDir) {
|
|
79
|
-
let lastMtime
|
|
92
|
+
let lastMtime;
|
|
93
|
+
try {
|
|
94
|
+
lastMtime = fs.statSync(filePath).mtime.getTime();
|
|
95
|
+
} catch {
|
|
96
|
+
return null; // File deleted between existsSync and statSync
|
|
97
|
+
}
|
|
80
98
|
|
|
81
|
-
fs.watch(filePath, (eventType) => {
|
|
99
|
+
return fs.watch(filePath, (eventType) => {
|
|
82
100
|
if (eventType === 'change') {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
lastMtime
|
|
86
|
-
|
|
87
|
-
|
|
101
|
+
try {
|
|
102
|
+
const currentMtime = fs.statSync(filePath).mtime.getTime();
|
|
103
|
+
if (currentMtime !== lastMtime) {
|
|
104
|
+
lastMtime = currentMtime;
|
|
105
|
+
console.log(`[DAEMON] ${path.basename(filePath)} modifie`);
|
|
106
|
+
triggerScan(projectDir);
|
|
107
|
+
}
|
|
108
|
+
} catch {
|
|
109
|
+
// File may have been deleted between watch trigger and stat
|
|
88
110
|
}
|
|
89
111
|
}
|
|
90
112
|
});
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
function watchNodeModules(nodeModulesPath, projectDir) {
|
|
94
|
-
fs.watch(nodeModulesPath, { recursive: true }, (eventType, filename) => {
|
|
116
|
+
return fs.watch(nodeModulesPath, { recursive: true }, (eventType, filename) => {
|
|
95
117
|
if (filename && filename.includes('package.json')) {
|
|
96
118
|
console.log(`[DAEMON] Nouveau package detecte: ${filename}`);
|
|
97
119
|
triggerScan(projectDir);
|
|
@@ -133,8 +155,4 @@ function triggerScan(dir) {
|
|
|
133
155
|
}, 3000);
|
|
134
156
|
}
|
|
135
157
|
|
|
136
|
-
function sleep(ms) {
|
|
137
|
-
return new Promise(resolve => setTimeout(resolve, ms));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
158
|
module.exports = { startDaemon };
|
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/hooks-init.js
CHANGED
|
@@ -2,6 +2,15 @@ const { execSync } = require('child_process');
|
|
|
2
2
|
const fs = require('fs');
|
|
3
3
|
const path = require('path');
|
|
4
4
|
|
|
5
|
+
// Read version from package.json for pre-commit config
|
|
6
|
+
const PKG_VERSION = (() => {
|
|
7
|
+
try {
|
|
8
|
+
return 'v' + JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json'), 'utf8')).version;
|
|
9
|
+
} catch {
|
|
10
|
+
return 'v1.0.0';
|
|
11
|
+
}
|
|
12
|
+
})();
|
|
13
|
+
|
|
5
14
|
/**
|
|
6
15
|
* Detect which hook system is available
|
|
7
16
|
*/
|
|
@@ -131,7 +140,7 @@ async function initPreCommit(targetPath, mode) {
|
|
|
131
140
|
const hookId = mode === 'diff' ? 'muaddib-diff' : 'muaddib-scan';
|
|
132
141
|
const muaddibConfig = `
|
|
133
142
|
- repo: https://github.com/DNSZLSK/muad-dib
|
|
134
|
-
rev:
|
|
143
|
+
rev: ${PKG_VERSION}
|
|
135
144
|
hooks:
|
|
136
145
|
- id: ${hookId}
|
|
137
146
|
`;
|
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'
|
|
@@ -184,6 +192,7 @@ async function run(targetPath, options = {}) {
|
|
|
184
192
|
critical: criticalCount,
|
|
185
193
|
high: highCount,
|
|
186
194
|
medium: mediumCount,
|
|
195
|
+
low: lowCount,
|
|
187
196
|
riskScore: riskScore,
|
|
188
197
|
riskLevel: riskLevel
|
|
189
198
|
}
|