muaddib-scanner 1.0.9 → 1.0.11
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/.muaddib-cache/iocs.json +355 -0
- package/package.json +3 -3
- package/rapport.html +159 -0
- package/src/scanner/ast.js +45 -14
- package/src/scanner/dataflow.js +39 -11
- package/src/scanner/typosquat.js +97 -140
- package/.github/workflows/scan.yml +0 -33
- package/CONTRIBUTING.md +0 -98
- package/docs/threat-model.md +0 -116
- package/test/samples/malicious.js +0 -20
- package/tests/run-tests.js +0 -389
- package/tests/samples/ast/malicious.js +0 -20
- package/tests/samples/clean/safe.js +0 -14
- package/tests/samples/dataflow/exfiltration.js +0 -20
- package/tests/samples/edge/empty/empty.js +0 -0
- package/tests/samples/edge/invalid-syntax/broken.js +0 -5
- package/tests/samples/edge/large-file/large.js +0 -6
- package/tests/samples/edge/non-js/readme.txt +0 -3
- package/tests/samples/markers/shai-hulud.js +0 -10
- package/tests/samples/obfuscation/obfuscated.js +0 -1
- package/tests/samples/package/package.json +0 -9
- package/tests/samples/shell/malicious.sh +0 -13
- package/tests/samples/typosquat/package.json +0 -11
- package/vscode-extension/.vscode/launch.json +0 -13
- package/vscode-extension/.vscodeignore +0 -0
- package/vscode-extension/LICENSE +0 -21
- package/vscode-extension/README.md +0 -0
- package/vscode-extension/extension.js +0 -271
- package/vscode-extension/icon.png +0 -0
- package/vscode-extension/muaddib-vscode-1.0.0.vsix +0 -0
- package/vscode-extension/package.json +0 -69
- package/vscode-extension/vscode-extension/README.md +0 -44
- package/vscode-extension/vscode-extension/package.json +0 -64
package/src/scanner/dataflow.js
CHANGED
|
@@ -3,13 +3,24 @@ const path = require('path');
|
|
|
3
3
|
const acorn = require('acorn');
|
|
4
4
|
const walk = require('acorn-walk');
|
|
5
5
|
|
|
6
|
-
const EXCLUDED_DIRS = [
|
|
6
|
+
const EXCLUDED_DIRS = [
|
|
7
|
+
'test', 'tests', 'node_modules', '.git', 'src', 'vscode-extension',
|
|
8
|
+
'scripts', 'bin', 'tools', 'build', 'dist', 'fixtures', 'examples',
|
|
9
|
+
'__tests__', '__mocks__', 'benchmark', 'benchmarks', 'docs', 'doc'
|
|
10
|
+
];
|
|
7
11
|
|
|
8
12
|
async function analyzeDataFlow(targetPath) {
|
|
9
13
|
const threats = [];
|
|
10
14
|
const files = findJsFiles(targetPath);
|
|
11
15
|
|
|
12
16
|
for (const file of files) {
|
|
17
|
+
const relativePath = path.relative(targetPath, file).replace(/\\/g, '/');
|
|
18
|
+
|
|
19
|
+
// Ignorer les fichiers de dev/build/scripts
|
|
20
|
+
if (isDevFile(relativePath)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
13
24
|
const content = fs.readFileSync(file, 'utf8');
|
|
14
25
|
const fileThreats = analyzeFile(content, file, targetPath);
|
|
15
26
|
threats.push(...fileThreats);
|
|
@@ -18,6 +29,29 @@ async function analyzeDataFlow(targetPath) {
|
|
|
18
29
|
return threats;
|
|
19
30
|
}
|
|
20
31
|
|
|
32
|
+
function isDevFile(relativePath) {
|
|
33
|
+
const devPatterns = [
|
|
34
|
+
/^scripts\//,
|
|
35
|
+
/^bin\//,
|
|
36
|
+
/^tools\//,
|
|
37
|
+
/^build\//,
|
|
38
|
+
/^fixtures\//,
|
|
39
|
+
/^examples\//,
|
|
40
|
+
/^__tests__\//,
|
|
41
|
+
/^__mocks__\//,
|
|
42
|
+
/^benchmark/,
|
|
43
|
+
/^docs?\//,
|
|
44
|
+
/^compiler\//,
|
|
45
|
+
/^packages\/.*\/scripts\//,
|
|
46
|
+
/\.test\.js$/,
|
|
47
|
+
/\.spec\.js$/,
|
|
48
|
+
/test\.js$/,
|
|
49
|
+
/spec\.js$/
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
return devPatterns.some(pattern => pattern.test(relativePath));
|
|
53
|
+
}
|
|
54
|
+
|
|
21
55
|
function analyzeFile(content, filePath, basePath) {
|
|
22
56
|
const threats = [];
|
|
23
57
|
let ast;
|
|
@@ -32,15 +66,13 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
32
66
|
return threats;
|
|
33
67
|
}
|
|
34
68
|
|
|
35
|
-
const sources = [];
|
|
36
|
-
const sinks = [];
|
|
69
|
+
const sources = [];
|
|
70
|
+
const sinks = [];
|
|
37
71
|
|
|
38
72
|
walk.simple(ast, {
|
|
39
|
-
// Detecte les lectures de fichiers sensibles
|
|
40
73
|
CallExpression(node) {
|
|
41
74
|
const callName = getCallName(node);
|
|
42
75
|
|
|
43
|
-
// fs.readFileSync, fs.readFile
|
|
44
76
|
if (callName === 'readFileSync' || callName === 'readFile') {
|
|
45
77
|
const arg = node.arguments[0];
|
|
46
78
|
if (arg && isCredentialPath(arg, content)) {
|
|
@@ -52,7 +84,6 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
52
84
|
}
|
|
53
85
|
}
|
|
54
86
|
|
|
55
|
-
// Detecte les envois reseau
|
|
56
87
|
if (callName === 'request' || callName === 'fetch' || callName === 'post' || callName === 'get') {
|
|
57
88
|
sinks.push({
|
|
58
89
|
type: 'network_send',
|
|
@@ -61,7 +92,6 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
61
92
|
});
|
|
62
93
|
}
|
|
63
94
|
|
|
64
|
-
// exec avec curl/wget
|
|
65
95
|
if (callName === 'exec' || callName === 'execSync') {
|
|
66
96
|
const arg = node.arguments[0];
|
|
67
97
|
if (arg && arg.type === 'Literal' && typeof arg.value === 'string') {
|
|
@@ -76,7 +106,6 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
76
106
|
}
|
|
77
107
|
},
|
|
78
108
|
|
|
79
|
-
// Detecte les acces process.env sensibles
|
|
80
109
|
MemberExpression(node) {
|
|
81
110
|
if (
|
|
82
111
|
node.object?.object?.name === 'process' &&
|
|
@@ -94,7 +123,6 @@ function analyzeFile(content, filePath, basePath) {
|
|
|
94
123
|
}
|
|
95
124
|
});
|
|
96
125
|
|
|
97
|
-
// Si on a des sources ET des sinks = flux suspect
|
|
98
126
|
if (sources.length > 0 && sinks.length > 0) {
|
|
99
127
|
threats.push({
|
|
100
128
|
type: 'suspicious_dataflow',
|
|
@@ -126,7 +154,6 @@ function isCredentialPath(arg, content) {
|
|
|
126
154
|
val.includes('.gitconfig') ||
|
|
127
155
|
val.includes('.env');
|
|
128
156
|
}
|
|
129
|
-
// Verifie aussi les templates strings et concatenations
|
|
130
157
|
if (arg.type === 'TemplateLiteral' || arg.type === 'BinaryExpression') {
|
|
131
158
|
return content.includes('.npmrc') ||
|
|
132
159
|
content.includes('.ssh') ||
|
|
@@ -136,7 +163,8 @@ function isCredentialPath(arg, content) {
|
|
|
136
163
|
}
|
|
137
164
|
|
|
138
165
|
function isSensitiveEnv(name) {
|
|
139
|
-
const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', '
|
|
166
|
+
const sensitive = ['TOKEN', 'SECRET', 'KEY', 'PASSWORD', 'CREDENTIAL', 'AUTH', 'NPM', 'AWS', 'AZURE', 'GCP'];
|
|
167
|
+
// Ignore GITHUB — trop de faux positifs dans les scripts de release
|
|
140
168
|
return sensitive.some(s => name.toUpperCase().includes(s));
|
|
141
169
|
}
|
|
142
170
|
|
package/src/scanner/typosquat.js
CHANGED
|
@@ -7,57 +7,63 @@ const POPULAR_PACKAGES = [
|
|
|
7
7
|
'request', 'async', 'bluebird', 'underscore', 'uuid', 'debug', 'mkdirp',
|
|
8
8
|
'glob', 'minimist', 'webpack', 'babel-core', 'typescript', 'eslint',
|
|
9
9
|
'prettier', 'jest', 'mocha', 'chai', 'sinon', 'mongoose', 'sequelize',
|
|
10
|
-
'
|
|
10
|
+
'redis', 'mongodb', 'socket.io', 'express-session',
|
|
11
11
|
'body-parser', 'cookie-parser', 'cors', 'helmet', 'morgan', 'dotenv',
|
|
12
12
|
'jsonwebtoken', 'bcrypt', 'passport', 'nodemailer', 'aws-sdk', 'stripe',
|
|
13
|
-
'twilio', 'firebase', 'graphql', 'apollo-server', '
|
|
14
|
-
'gatsby', '
|
|
13
|
+
'twilio', 'firebase', 'graphql', 'apollo-server', 'nuxt',
|
|
14
|
+
'gatsby', 'angular', 'svelte', 'electron', 'puppeteer', 'cheerio',
|
|
15
15
|
'sharp', 'jimp', 'canvas', 'pdf-lib', 'exceljs', 'csv-parser', 'xml2js',
|
|
16
|
-
'yaml', '
|
|
17
|
-
'winston', 'bunyan', 'pino', 'log4js', 'ramda', '
|
|
18
|
-
'mobx', 'redux', 'zustand', 'formik', 'yup', '
|
|
16
|
+
'yaml', 'config', 'yargs', 'colors',
|
|
17
|
+
'winston', 'bunyan', 'pino', 'log4js', 'ramda', 'immutable',
|
|
18
|
+
'mobx', 'redux', 'zustand', 'formik', 'yup', 'ajv', 'validator',
|
|
19
19
|
'date-fns', 'dayjs', 'luxon', 'numeral', 'accounting', 'currency.js',
|
|
20
20
|
'lodash-es', 'core-js', 'regenerator-runtime', 'tslib', 'classnames',
|
|
21
|
-
'prop-types', 'cross-env', '
|
|
21
|
+
'prop-types', 'cross-env', 'node-fetch', 'got'
|
|
22
22
|
];
|
|
23
23
|
|
|
24
|
-
// Packages legitimes qui ressemblent a des populaires
|
|
24
|
+
// Packages legitimes courts ou qui ressemblent a des populaires
|
|
25
25
|
const WHITELIST = [
|
|
26
|
-
|
|
27
|
-
'
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'
|
|
35
|
-
'ts-node',
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
'
|
|
42
|
-
'
|
|
43
|
-
'
|
|
44
|
-
'
|
|
45
|
-
'
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
'
|
|
49
|
-
'
|
|
26
|
+
// Packages tres courts legitimes
|
|
27
|
+
'qs', 'pg', 'ms', 'ws', 'ip', 'on', 'is', 'it', 'to', 'or', 'fs', 'os',
|
|
28
|
+
'co', 'q', 'n', 'i', 'a', 'v', 'x', 'y', 'z',
|
|
29
|
+
'ejs', 'nyc', 'ini', 'joi', 'vue', 'npm', 'got', 'ora',
|
|
30
|
+
'vary', 'mime', 'send', 'etag', 'raw', 'tar', 'uid', 'cjs',
|
|
31
|
+
'rxjs', 'yarn', 'pnpm', 'next',
|
|
32
|
+
|
|
33
|
+
// Packages legitimes avec noms similaires
|
|
34
|
+
'acorn', 'acorn-walk', 'js-yaml', 'cross-env', 'node-fetch', 'node-gyp',
|
|
35
|
+
'core-js', 'lodash-es', 'date-fns', 'ts-node', 'ts-jest',
|
|
36
|
+
'css-loader', 'style-loader', 'file-loader', 'url-loader', 'babel-loader',
|
|
37
|
+
'vue-loader', 'react-dom', 'react-router', 'react-redux', 'vue-router',
|
|
38
|
+
'express-session', 'body-parser', 'cookie-parser',
|
|
39
|
+
|
|
40
|
+
// Packages Express.js communs
|
|
41
|
+
'accepts', 'array-flatten', 'content-disposition', 'content-type',
|
|
42
|
+
'depd', 'destroy', 'encodeurl', 'escape-html', 'fresh', 'merge-descriptors',
|
|
43
|
+
'methods', 'on-finished', 'parseurl', 'path-to-regexp', 'proxy-addr',
|
|
44
|
+
'range-parser', 'safe-buffer', 'safer-buffer', 'setprototypeof',
|
|
45
|
+
'statuses', 'type-is', 'unpipe', 'utils-merge',
|
|
46
|
+
|
|
47
|
+
// Packages CLI et outils legitimes
|
|
48
|
+
'jest-cli', 'prettier-2', 'prettier-1', 'eslint-cli',
|
|
49
|
+
'inquirer', 'enquirer', 'prompts',
|
|
50
|
+
'mysql2', 'pg-native', 'sqlite3', 'better-sqlite3',
|
|
51
|
+
'node-sass', 'sass', 'less',
|
|
52
|
+
'esbuild', 'rollup', 'parcel', 'vite',
|
|
53
|
+
'husky', 'lint-staged', 'commitlint',
|
|
54
|
+
'nodemon', 'pm2', 'forever', 'concurrently',
|
|
55
|
+
'lerna', 'turbo', 'nx',
|
|
56
|
+
'chalk', 'colors', 'picocolors', 'colorette',
|
|
57
|
+
'commander', 'yargs', 'meow', 'cac',
|
|
58
|
+
'execa', 'shelljs', 'cross-spawn',
|
|
59
|
+
'rimraf', 'del', 'trash-cli',
|
|
60
|
+
'globby', 'fast-glob', 'tiny-glob',
|
|
61
|
+
'chokidar', 'watchpack', 'nsfw',
|
|
62
|
+
'dotenv', 'dotenv-expand', 'env-cmd'
|
|
50
63
|
];
|
|
51
64
|
|
|
52
|
-
//
|
|
53
|
-
const
|
|
54
|
-
{ type: 'missing_char', fn: (name) => generateMissingChar(name) },
|
|
55
|
-
{ type: 'extra_char', fn: (name) => generateExtraChar(name) },
|
|
56
|
-
{ type: 'swapped_chars', fn: (name) => generateSwappedChars(name) },
|
|
57
|
-
{ type: 'wrong_char', fn: (name) => generateWrongChar(name) },
|
|
58
|
-
{ type: 'hyphen_tricks', fn: (name) => generateHyphenTricks(name) },
|
|
59
|
-
{ type: 'suffix_tricks', fn: (name) => generateSuffixTricks(name) }
|
|
60
|
-
];
|
|
65
|
+
// Seuil minimum de longueur pour eviter faux positifs
|
|
66
|
+
const MIN_PACKAGE_LENGTH = 4;
|
|
61
67
|
|
|
62
68
|
async function scanTyposquatting(targetPath) {
|
|
63
69
|
const threats = [];
|
|
@@ -97,19 +103,30 @@ async function scanTyposquatting(targetPath) {
|
|
|
97
103
|
}
|
|
98
104
|
|
|
99
105
|
function findTyposquatMatch(name) {
|
|
106
|
+
const nameLower = name.toLowerCase();
|
|
107
|
+
|
|
100
108
|
// Ignore les packages whitelistes
|
|
101
|
-
if (WHITELIST.includes(
|
|
109
|
+
if (WHITELIST.includes(nameLower)) return null;
|
|
102
110
|
|
|
103
111
|
// Ignore les packages scoped (@org/package)
|
|
104
112
|
if (name.startsWith('@')) return null;
|
|
105
113
|
|
|
114
|
+
// Ignore les packages tres courts (trop de faux positifs)
|
|
115
|
+
if (name.length < MIN_PACKAGE_LENGTH) return null;
|
|
116
|
+
|
|
117
|
+
// Ignore les packages avec suffixes legitimes courants
|
|
118
|
+
if (isLegitimateVariant(nameLower)) return null;
|
|
119
|
+
|
|
106
120
|
for (const popular of POPULAR_PACKAGES) {
|
|
107
121
|
// Ignore si c'est exactement le meme
|
|
108
|
-
if (
|
|
122
|
+
if (nameLower === popular.toLowerCase()) continue;
|
|
109
123
|
|
|
110
|
-
|
|
124
|
+
// Ignore si le package populaire est trop court
|
|
125
|
+
if (popular.length < MIN_PACKAGE_LENGTH) continue;
|
|
126
|
+
|
|
127
|
+
const distance = levenshteinDistance(nameLower, popular.toLowerCase());
|
|
111
128
|
|
|
112
|
-
// Distance de 1
|
|
129
|
+
// Distance de 1 = tres suspect (une seule lettre de difference)
|
|
113
130
|
if (distance === 1) {
|
|
114
131
|
return {
|
|
115
132
|
original: popular,
|
|
@@ -118,33 +135,53 @@ function findTyposquatMatch(name) {
|
|
|
118
135
|
};
|
|
119
136
|
}
|
|
120
137
|
|
|
121
|
-
// Distance de 2
|
|
122
|
-
if (distance === 2 && popular.length
|
|
138
|
+
// Distance de 2 seulement si le package est assez long (>= 8 chars)
|
|
139
|
+
if (distance === 2 && popular.length >= 8) {
|
|
123
140
|
return {
|
|
124
141
|
original: popular,
|
|
125
142
|
type: detectTyposquatType(name, popular),
|
|
126
143
|
distance: distance
|
|
127
144
|
};
|
|
128
145
|
}
|
|
129
|
-
|
|
130
|
-
// Verifie les tricks de suffixe
|
|
131
|
-
if (isSuffixTrick(name, popular)) {
|
|
132
|
-
return {
|
|
133
|
-
original: popular,
|
|
134
|
-
type: 'suffix_trick',
|
|
135
|
-
distance: distance
|
|
136
|
-
};
|
|
137
|
-
}
|
|
138
146
|
}
|
|
139
147
|
|
|
140
148
|
return null;
|
|
141
149
|
}
|
|
142
150
|
|
|
151
|
+
function isLegitimateVariant(name) {
|
|
152
|
+
// Suffixes legitimes qui ne sont PAS du typosquatting
|
|
153
|
+
const legitimateSuffixes = [
|
|
154
|
+
'-cli', '-core', '-utils', '-plugin', '-loader', '-webpack',
|
|
155
|
+
'-react', '-vue', '-angular', '-node', '-browser',
|
|
156
|
+
'-esm', '-cjs', '-umd',
|
|
157
|
+
'-types', '-typings',
|
|
158
|
+
'2', '3', '4', '5', // versions majeures (mysql2, etc)
|
|
159
|
+
'-v2', '-v3', '-next', '-latest', '-stable', '-lts'
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
for (const suffix of legitimateSuffixes) {
|
|
163
|
+
if (name.endsWith(suffix)) return true;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Prefixes legitimes
|
|
167
|
+
const legitimatePrefixes = [
|
|
168
|
+
'@types/', '@babel/', '@jest/', '@testing-library/',
|
|
169
|
+
'eslint-plugin-', 'eslint-config-',
|
|
170
|
+
'babel-plugin-', 'babel-preset-',
|
|
171
|
+
'webpack-plugin-', 'rollup-plugin-', 'vite-plugin-'
|
|
172
|
+
];
|
|
173
|
+
|
|
174
|
+
for (const prefix of legitimatePrefixes) {
|
|
175
|
+
if (name.startsWith(prefix)) return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
|
|
143
181
|
function detectTyposquatType(typo, original) {
|
|
144
182
|
if (typo.length === original.length - 1) return 'missing_char';
|
|
145
183
|
if (typo.length === original.length + 1) return 'extra_char';
|
|
146
184
|
if (typo.length === original.length) {
|
|
147
|
-
// Verifie si swap
|
|
148
185
|
let diffs = 0;
|
|
149
186
|
for (let i = 0; i < typo.length; i++) {
|
|
150
187
|
if (typo[i] !== original[i]) diffs++;
|
|
@@ -155,20 +192,6 @@ function detectTyposquatType(typo, original) {
|
|
|
155
192
|
return 'unknown';
|
|
156
193
|
}
|
|
157
194
|
|
|
158
|
-
function isSuffixTrick(name, popular) {
|
|
159
|
-
const suffixes = ['-js', '.js', '-node', '-npm', '-cli', '-api', '-lib', '-pkg', '-dev', '-pro'];
|
|
160
|
-
for (const suffix of suffixes) {
|
|
161
|
-
if (name === popular + suffix) return true;
|
|
162
|
-
if (name === popular.replace('-', '') + suffix) return true;
|
|
163
|
-
}
|
|
164
|
-
// Verifie aussi les prefixes
|
|
165
|
-
const prefixes = ['node-', 'npm-', 'js-', 'get-', 'the-'];
|
|
166
|
-
for (const prefix of prefixes) {
|
|
167
|
-
if (name === prefix + popular) return true;
|
|
168
|
-
}
|
|
169
|
-
return false;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
195
|
function levenshteinDistance(a, b) {
|
|
173
196
|
const matrix = [];
|
|
174
197
|
|
|
@@ -186,9 +209,9 @@ function levenshteinDistance(a, b) {
|
|
|
186
209
|
matrix[i][j] = matrix[i - 1][j - 1];
|
|
187
210
|
} else {
|
|
188
211
|
matrix[i][j] = Math.min(
|
|
189
|
-
matrix[i - 1][j - 1] + 1,
|
|
190
|
-
matrix[i][j - 1] + 1,
|
|
191
|
-
matrix[i - 1][j] + 1
|
|
212
|
+
matrix[i - 1][j - 1] + 1,
|
|
213
|
+
matrix[i][j - 1] + 1,
|
|
214
|
+
matrix[i - 1][j] + 1
|
|
192
215
|
);
|
|
193
216
|
}
|
|
194
217
|
}
|
|
@@ -197,70 +220,4 @@ function levenshteinDistance(a, b) {
|
|
|
197
220
|
return matrix[b.length][a.length];
|
|
198
221
|
}
|
|
199
222
|
|
|
200
|
-
// Generateurs pour tests (pas utilises dans le scan, mais utiles pour enrichir les IOCs)
|
|
201
|
-
function generateMissingChar(name) {
|
|
202
|
-
const results = [];
|
|
203
|
-
for (let i = 0; i < name.length; i++) {
|
|
204
|
-
results.push(name.slice(0, i) + name.slice(i + 1));
|
|
205
|
-
}
|
|
206
|
-
return results;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
function generateExtraChar(name) {
|
|
210
|
-
const results = [];
|
|
211
|
-
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789-';
|
|
212
|
-
for (let i = 0; i <= name.length; i++) {
|
|
213
|
-
for (const char of chars) {
|
|
214
|
-
results.push(name.slice(0, i) + char + name.slice(i));
|
|
215
|
-
}
|
|
216
|
-
}
|
|
217
|
-
return results;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function generateSwappedChars(name) {
|
|
221
|
-
const results = [];
|
|
222
|
-
for (let i = 0; i < name.length - 1; i++) {
|
|
223
|
-
const arr = name.split('');
|
|
224
|
-
[arr[i], arr[i + 1]] = [arr[i + 1], arr[i]];
|
|
225
|
-
results.push(arr.join(''));
|
|
226
|
-
}
|
|
227
|
-
return results;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
function generateWrongChar(name) {
|
|
231
|
-
const results = [];
|
|
232
|
-
const keyboard = {
|
|
233
|
-
'a': 'sqwz', 'b': 'vghn', 'c': 'xdfv', 'd': 'serfcx', 'e': 'wsdfr',
|
|
234
|
-
'f': 'drtgvc', 'g': 'ftyhbv', 'h': 'gyujnb', 'i': 'ujklo', 'j': 'huikmn',
|
|
235
|
-
'k': 'jiolm', 'l': 'kop', 'm': 'njk', 'n': 'bhjm', 'o': 'iklp',
|
|
236
|
-
'p': 'ol', 'q': 'wa', 'r': 'edft', 's': 'awedxz', 't': 'rfgy',
|
|
237
|
-
'u': 'yhji', 'v': 'cfgb', 'w': 'qase', 'x': 'zsdc', 'y': 'tghu', 'z': 'asx'
|
|
238
|
-
};
|
|
239
|
-
for (let i = 0; i < name.length; i++) {
|
|
240
|
-
const char = name[i].toLowerCase();
|
|
241
|
-
if (keyboard[char]) {
|
|
242
|
-
for (const replacement of keyboard[char]) {
|
|
243
|
-
results.push(name.slice(0, i) + replacement + name.slice(i + 1));
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return results;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function generateHyphenTricks(name) {
|
|
251
|
-
const results = [];
|
|
252
|
-
// Ajouter des hyphens
|
|
253
|
-
for (let i = 1; i < name.length; i++) {
|
|
254
|
-
results.push(name.slice(0, i) + '-' + name.slice(i));
|
|
255
|
-
}
|
|
256
|
-
// Retirer des hyphens
|
|
257
|
-
results.push(name.replace(/-/g, ''));
|
|
258
|
-
return results;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function generateSuffixTricks(name) {
|
|
262
|
-
const suffixes = ['-js', '.js', '-node', '-npm', '-cli'];
|
|
263
|
-
return suffixes.map(s => name + s);
|
|
264
|
-
}
|
|
265
|
-
|
|
266
223
|
module.exports = { scanTyposquatting, levenshteinDistance };
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
name: MUADDIB Security Scan
|
|
2
|
-
|
|
3
|
-
on:
|
|
4
|
-
push:
|
|
5
|
-
branches: [master, main]
|
|
6
|
-
pull_request:
|
|
7
|
-
branches: [master, main]
|
|
8
|
-
|
|
9
|
-
jobs:
|
|
10
|
-
scan:
|
|
11
|
-
runs-on: ubuntu-latest
|
|
12
|
-
permissions:
|
|
13
|
-
security-events: write
|
|
14
|
-
contents: read
|
|
15
|
-
steps:
|
|
16
|
-
- name: Checkout code
|
|
17
|
-
uses: actions/checkout@v4
|
|
18
|
-
|
|
19
|
-
- name: Setup Node.js
|
|
20
|
-
uses: actions/setup-node@v4
|
|
21
|
-
with:
|
|
22
|
-
node-version: '20'
|
|
23
|
-
|
|
24
|
-
- name: Install dependencies
|
|
25
|
-
run: npm install
|
|
26
|
-
|
|
27
|
-
- name: Run MUADDIB scan
|
|
28
|
-
run: node bin/muaddib.js scan . --sarif results.sarif || true
|
|
29
|
-
|
|
30
|
-
- name: Upload SARIF to GitHub Security
|
|
31
|
-
uses: github/codeql-action/upload-sarif@v3
|
|
32
|
-
with:
|
|
33
|
-
sarif_file: results.sarif
|
package/CONTRIBUTING.md
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
# Contributing to MUAD'DIB
|
|
2
|
-
|
|
3
|
-
Thanks for your interest in improving MUAD'DIB!
|
|
4
|
-
|
|
5
|
-
## Ways to contribute
|
|
6
|
-
|
|
7
|
-
### 1. Add new IOCs (Indicators of Compromise)
|
|
8
|
-
|
|
9
|
-
Edit `iocs/packages.yaml` and add:
|
|
10
|
-
```yaml
|
|
11
|
-
- id: MALWARE-XXX-001
|
|
12
|
-
name: "malicious-package-name"
|
|
13
|
-
version: "*"
|
|
14
|
-
severity: critical
|
|
15
|
-
confidence: high
|
|
16
|
-
source: your-name
|
|
17
|
-
description: "Short description of the threat"
|
|
18
|
-
references:
|
|
19
|
-
- https://link-to-blog-or-advisory
|
|
20
|
-
mitre: T1195.002
|
|
21
|
-
```
|
|
22
|
-
|
|
23
|
-
**Severity levels:** critical, high, medium, low
|
|
24
|
-
|
|
25
|
-
**MITRE techniques:**
|
|
26
|
-
- T1195.002 — Supply chain compromise
|
|
27
|
-
- T1552.001 — Credentials in files
|
|
28
|
-
- T1059 — Command execution
|
|
29
|
-
- T1027 — Obfuscation
|
|
30
|
-
- T1041 — Exfiltration
|
|
31
|
-
|
|
32
|
-
### 2. Add detection rules
|
|
33
|
-
|
|
34
|
-
Edit `src/rules/index.js` and add a new rule:
|
|
35
|
-
```javascript
|
|
36
|
-
my_new_rule: {
|
|
37
|
-
id: 'MUADDIB-XXX-001',
|
|
38
|
-
name: 'My New Detection',
|
|
39
|
-
severity: 'HIGH',
|
|
40
|
-
confidence: 'high',
|
|
41
|
-
description: 'What this rule detects',
|
|
42
|
-
references: ['https://...'],
|
|
43
|
-
mitre: 'T1195.002'
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### 3. Add response playbooks
|
|
48
|
-
|
|
49
|
-
Edit `src/response/playbooks.js` and add:
|
|
50
|
-
```javascript
|
|
51
|
-
case 'my_new_rule':
|
|
52
|
-
return 'Step-by-step response instructions';
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
### 4. Report false positives
|
|
56
|
-
|
|
57
|
-
Open an issue with:
|
|
58
|
-
- Package name
|
|
59
|
-
- Detection rule triggered
|
|
60
|
-
- Why it's a false positive
|
|
61
|
-
|
|
62
|
-
### 5. Report missed detections
|
|
63
|
-
|
|
64
|
-
Open an issue with:
|
|
65
|
-
- Malicious package name
|
|
66
|
-
- What it does
|
|
67
|
-
- Links to advisories
|
|
68
|
-
|
|
69
|
-
## Development
|
|
70
|
-
```bash
|
|
71
|
-
git clone https://github.com/DNSZLSK/muad-dib.git
|
|
72
|
-
cd muad-dib
|
|
73
|
-
npm install
|
|
74
|
-
npm test
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
## Testing your changes
|
|
78
|
-
```bash
|
|
79
|
-
node bin/muaddib.js scan tests/samples --explain
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
## Pull request process
|
|
83
|
-
|
|
84
|
-
1. Fork the repo
|
|
85
|
-
2. Create a branch (`git checkout -b feature/my-feature`)
|
|
86
|
-
3. Make your changes
|
|
87
|
-
4. Run tests (`npm test`)
|
|
88
|
-
5. Commit (`git commit -m "Add my feature"`)
|
|
89
|
-
6. Push (`git push origin feature/my-feature`)
|
|
90
|
-
7. Open a Pull Request
|
|
91
|
-
|
|
92
|
-
## Code of conduct
|
|
93
|
-
|
|
94
|
-
Be respectful. We're all here to make npm safer.
|
|
95
|
-
|
|
96
|
-
## Questions?
|
|
97
|
-
|
|
98
|
-
Open an issue or join our Discord: https://discord.gg/y8zxSmue
|
package/docs/threat-model.md
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
# MUAD'DIB Threat Model
|
|
2
|
-
|
|
3
|
-
## Ce que MUAD'DIB detecte
|
|
4
|
-
|
|
5
|
-
### Attaques Supply Chain npm
|
|
6
|
-
|
|
7
|
-
| Technique | Detection | Confidence |
|
|
8
|
-
|-----------|-----------|------------|
|
|
9
|
-
| Packages malveillants connus | Hash SHA256 + nom | HIGH |
|
|
10
|
-
| Shai-Hulud v1/v2/v3 | Marqueurs + fichiers + comportements | HIGH |
|
|
11
|
-
| event-stream (2018) | Nom + version | HIGH |
|
|
12
|
-
| Typosquatting | Liste de packages connus | MEDIUM |
|
|
13
|
-
| Protestware (node-ipc, colors) | Nom + version | HIGH |
|
|
14
|
-
|
|
15
|
-
### Comportements malveillants
|
|
16
|
-
|
|
17
|
-
| Technique | Detection | Confidence |
|
|
18
|
-
|-----------|-----------|------------|
|
|
19
|
-
| Vol de credentials (.npmrc, .ssh) | Analyse AST | HIGH |
|
|
20
|
-
| Exfiltration via env vars (GITHUB_TOKEN) | Analyse AST | HIGH |
|
|
21
|
-
| Execution de code distant (curl \| sh) | Pattern matching | HIGH |
|
|
22
|
-
| Reverse shell | Pattern matching | HIGH |
|
|
23
|
-
| Dead man's switch (rm -rf $HOME) | Pattern matching | HIGH |
|
|
24
|
-
| Code obfusque | Heuristiques | MEDIUM |
|
|
25
|
-
|
|
26
|
-
### Flux de donnees suspects
|
|
27
|
-
|
|
28
|
-
| Technique | Detection | Confidence |
|
|
29
|
-
|-----------|-----------|------------|
|
|
30
|
-
| Lecture credential + envoi reseau | Analyse dataflow | HIGH |
|
|
31
|
-
| Acces process.env + fetch/request | Analyse dataflow | HIGH |
|
|
32
|
-
|
|
33
|
-
## Ce que MUAD'DIB NE detecte PAS
|
|
34
|
-
|
|
35
|
-
### Limitations connues
|
|
36
|
-
|
|
37
|
-
| Technique | Raison |
|
|
38
|
-
|-----------|--------|
|
|
39
|
-
| Malware polymorphe | Pas d'analyse dynamique |
|
|
40
|
-
| Obfuscation avancee | Heuristiques limitees |
|
|
41
|
-
| Zero-day (packages inconnus) | Base IOC reactive |
|
|
42
|
-
| Attaques via binaires natifs | Pas d'analyse binaire |
|
|
43
|
-
| Backdoors subtiles | Pas de review de code semantique |
|
|
44
|
-
| Time bombs (declenchement differe) | Pas d'analyse temporelle |
|
|
45
|
-
|
|
46
|
-
### Faux negatifs potentiels
|
|
47
|
-
|
|
48
|
-
- Code malveillant dans des fichiers non-JS (WASM, binaires)
|
|
49
|
-
- Exfiltration via DNS ou autres canaux couverts
|
|
50
|
-
- Malware qui detecte l'environnement d'analyse
|
|
51
|
-
- Attaques multi-etapes avec payload distant
|
|
52
|
-
|
|
53
|
-
## Hypotheses
|
|
54
|
-
|
|
55
|
-
1. **Le code source est disponible** — MUAD'DIB analyse le code, pas les binaires
|
|
56
|
-
2. **Les IOCs sont a jour** — La detection depend de la base IOC
|
|
57
|
-
3. **L'attaquant utilise des techniques connues** — Zero-days passent a travers
|
|
58
|
-
4. **Le scan est execute avant l'installation** — Apres `npm install`, c'est trop tard si preinstall a execute
|
|
59
|
-
|
|
60
|
-
## Architecture de detection
|
|
61
|
-
```
|
|
62
|
-
┌─────────────────────────────────────────────────────────────┐
|
|
63
|
-
│ MUAD'DIB Scanner │
|
|
64
|
-
├─────────────────────────────────────────────────────────────┤
|
|
65
|
-
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
|
|
66
|
-
│ │ IOC Match │ │ AST Parse │ │ Pattern Matching │ │
|
|
67
|
-
│ │ (hashes, │ │ (acorn) │ │ (shell, scripts) │ │
|
|
68
|
-
│ │ packages) │ │ │ │ │ │
|
|
69
|
-
│ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │
|
|
70
|
-
│ │ │ │ │
|
|
71
|
-
│ v v v │
|
|
72
|
-
│ ┌─────────────────────────────────────────────────────────┐│
|
|
73
|
-
│ │ Dataflow Analysis ││
|
|
74
|
-
│ │ (credential read -> network send) ││
|
|
75
|
-
│ └─────────────────────────────────────────────────────────┘│
|
|
76
|
-
│ │ │ │ │
|
|
77
|
-
│ v v v │
|
|
78
|
-
│ ┌─────────────────────────────────────────────────────────┐│
|
|
79
|
-
│ │ Threat Enrichment ││
|
|
80
|
-
│ │ (rules, MITRE ATT&CK, playbooks) ││
|
|
81
|
-
│ └─────────────────────────────────────────────────────────┘│
|
|
82
|
-
└─────────────────────────────────────────────────────────────┘
|
|
83
|
-
```
|
|
84
|
-
|
|
85
|
-
## Mapping MITRE ATT&CK
|
|
86
|
-
|
|
87
|
-
| Technique | ID | Detection MUAD'DIB |
|
|
88
|
-
|-----------|----|--------------------|
|
|
89
|
-
| Credentials in Files | T1552.001 | AST analysis |
|
|
90
|
-
| Command and Scripting Interpreter | T1059 | Pattern matching |
|
|
91
|
-
| Supply Chain Compromise | T1195.002 | IOC matching |
|
|
92
|
-
| Obfuscated Files | T1027 | Heuristics |
|
|
93
|
-
| Exfiltration Over C2 Channel | T1041 | Dataflow analysis |
|
|
94
|
-
| Data Destruction | T1485 | Pattern matching |
|
|
95
|
-
| Ingress Tool Transfer | T1105 | Pattern matching |
|
|
96
|
-
|
|
97
|
-
## Recommandations
|
|
98
|
-
|
|
99
|
-
### Pour les utilisateurs
|
|
100
|
-
|
|
101
|
-
1. Executer `muaddib scan .` AVANT `npm install`
|
|
102
|
-
2. Mettre a jour les IOCs regulierement (`muaddib update`)
|
|
103
|
-
3. Utiliser le mode `--explain` pour comprendre les detections
|
|
104
|
-
4. Integrer dans CI/CD avec sortie SARIF
|
|
105
|
-
|
|
106
|
-
### Pour les equipes securite
|
|
107
|
-
|
|
108
|
-
1. Completer avec une analyse dynamique (sandbox)
|
|
109
|
-
2. Monitorer les nouveaux packages avant adoption
|
|
110
|
-
3. Utiliser `--sarif` pour integration SIEM
|
|
111
|
-
4. Contribuer des IOCs via PR sur le repo
|
|
112
|
-
|
|
113
|
-
## Contacts
|
|
114
|
-
|
|
115
|
-
- Repository: https://github.com/DNSZLSK/muad-dib
|
|
116
|
-
- Issues: https://github.com/DNSZLSK/muad-dib/issues
|