muaddib-scanner 2.10.34 → 2.10.35
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/integrations/webhook.js +9 -0
- package/src/ml/llm-detective.js +76 -32
- package/src/monitor/webhook.js +1 -0
package/package.json
CHANGED
|
@@ -243,6 +243,15 @@ function formatDiscord(results) {
|
|
|
243
243
|
value: llmValue.slice(0, 1024),
|
|
244
244
|
inline: false
|
|
245
245
|
});
|
|
246
|
+
// Show investigation steps as a separate field if present (structured reasoning)
|
|
247
|
+
if (results.llm.investigation_steps && results.llm.investigation_steps.length > 0) {
|
|
248
|
+
const stepsText = results.llm.investigation_steps.map(s => `- ${s}`).join('\n');
|
|
249
|
+
fields.push({
|
|
250
|
+
name: 'Investigation Steps',
|
|
251
|
+
value: stepsText.slice(0, 1024),
|
|
252
|
+
inline: false
|
|
253
|
+
});
|
|
254
|
+
}
|
|
246
255
|
}
|
|
247
256
|
|
|
248
257
|
const titlePrefix = emoji ? `${emoji} ` : '';
|
package/src/ml/llm-detective.js
CHANGED
|
@@ -212,39 +212,81 @@ function collectSourceContext(extractedDir, scanResult) {
|
|
|
212
212
|
|
|
213
213
|
// ── Prompt construction ──
|
|
214
214
|
|
|
215
|
-
const SYSTEM_PROMPT = `You are a senior supply-chain security analyst. You receive
|
|
216
|
-
|
|
217
|
-
Your job
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
-
|
|
239
|
-
-
|
|
240
|
-
-
|
|
241
|
-
-
|
|
215
|
+
const SYSTEM_PROMPT = `You are a senior supply-chain security analyst performing the SAME investigation a human would do manually. You receive source code of a suspect package and static scanner results.
|
|
216
|
+
|
|
217
|
+
CRITICAL: The scanner findings are SIGNALS, not truth. Your job is to INDEPENDENTLY determine if this package is malicious by reading the code yourself. Many scanner findings are false positives — a CLI tool using child_process is not malware.
|
|
218
|
+
|
|
219
|
+
## YOUR INVESTIGATION METHOD
|
|
220
|
+
|
|
221
|
+
Do exactly what a human analyst would:
|
|
222
|
+
|
|
223
|
+
Step 1 — DECLARED PURPOSE: Read package.json. What does this package claim to do? Is the name/description/repo coherent?
|
|
224
|
+
|
|
225
|
+
Step 2 — CODE REALITY: Read ALL the code. Does it actually do what the description says? A "color picker" with child_process.exec is suspicious. A "CLI wrapper" with child_process.exec is normal.
|
|
226
|
+
|
|
227
|
+
Step 3 — DATA FLOW INTENT: When code accesses process.env or credentials:
|
|
228
|
+
- Is it CONFIGURING itself (reading DATABASE_URL, API_KEY for its own backend)? → BENIGN
|
|
229
|
+
- Is it COLLECTING and SENDING data to a third-party domain? → MALICIOUS
|
|
230
|
+
Follow the data: where does it GO?
|
|
231
|
+
|
|
232
|
+
Step 4 — DESTINATION CHECK: If data is sent somewhere:
|
|
233
|
+
- To the package's own documented API/backend? → BENIGN
|
|
234
|
+
- To a raw IP, ngrok/serveo tunnel, or unrelated domain? → MALICIOUS
|
|
235
|
+
- To nowhere (data is only read, never exfiltrated)? → BENIGN
|
|
236
|
+
|
|
237
|
+
Step 5 — COHERENCE: Does the complexity match the purpose?
|
|
238
|
+
- 3-file package with postinstall downloading binaries? → SUSPICIOUS
|
|
239
|
+
- Build tool with postinstall compiling native addon? → NORMAL
|
|
240
|
+
- Obfuscated code in a 10-line utility? → SUSPICIOUS
|
|
241
|
+
- Minified dist/ in a large framework? → NORMAL
|
|
242
|
+
|
|
243
|
+
## GOLDEN RULE
|
|
244
|
+
|
|
245
|
+
If sensitive data (env vars, credentials, keys) is only READ for self-configuration and never SENT to an external third-party, the package is BENIGN regardless of what the scanner says.
|
|
246
|
+
|
|
247
|
+
If sensitive data is COLLECTED and EXFILTRATED to a domain unrelated to the package's stated purpose, it is MALICIOUS.
|
|
248
|
+
|
|
249
|
+
## REFERENCE EXAMPLES
|
|
250
|
+
|
|
251
|
+
EXAMPLE 1 — TRUE MALWARE:
|
|
252
|
+
Package "slopex-cli" claims to be a "continuity patcher for OpenAI Codex". Postinstall downloads a binary from a personal GitHub repo and REPLACES the real Codex binary. The binary is not part of the described functionality — it's a trojan replacing a trusted tool.
|
|
253
|
+
→ Verdict: MALICIOUS (backdoor, confidence 0.97)
|
|
254
|
+
|
|
255
|
+
EXAMPLE 2 — FALSE POSITIVE:
|
|
256
|
+
Package "@yeaft/webchat-agent" is a "remote agent for WebChat connecting worker machines". Code uses execSync to locate the Claude CLI binary, process.env to read PATH configuration. Scanner flags "detached_credential_exfil" (CRITICAL) — but the code is just spawning a documented CLI tool and reading PATH. No data is sent to any external domain. The functionality matches the description.
|
|
257
|
+
→ Verdict: BENIGN (confidence 0.92)
|
|
258
|
+
|
|
259
|
+
EXAMPLE 3 — TRUE MALWARE:
|
|
260
|
+
Package "event-stream" (compromised via flatmap-stream dependency). Obfuscated code hidden in a nested dependency decrypts a payload targeting Bitcoin wallet data from Copay. The obfuscation has no legitimate reason — the parent package is a simple stream utility. The decrypted code specifically targets cryptocurrency credentials.
|
|
261
|
+
→ Verdict: MALICIOUS (credential_exfil, confidence 0.98)
|
|
262
|
+
|
|
263
|
+
EXAMPLE 4 — FALSE POSITIVE:
|
|
264
|
+
A web framework reads process.env.DATABASE_URL, process.env.API_KEY for configuration. It uses fetch() to call its own documented API endpoint. It uses dynamic require() to load user-configured plugins. Scanner flags env_access, dynamic_require, network_require — but all these are standard framework patterns. No data leaves the application boundary.
|
|
265
|
+
→ Verdict: BENIGN (confidence 0.95)
|
|
266
|
+
|
|
267
|
+
## KEY QUESTIONS TO ANSWER
|
|
268
|
+
|
|
269
|
+
1. "Do sensitive data (env vars, credentials) LEAVE the package to a third party?"
|
|
270
|
+
2. "Does the code do something HIDDEN that the description doesn't mention?"
|
|
271
|
+
3. "Is obfuscation justified (build tool output) or suspicious (tiny package, no build step)?"
|
|
272
|
+
4. "Does the postinstall relate to the declared functionality?"
|
|
273
|
+
5. "Could a reasonable developer have written this code for the stated purpose?"
|
|
274
|
+
|
|
275
|
+
## COMMON FALSE POSITIVE PATTERNS (do NOT flag these)
|
|
276
|
+
|
|
277
|
+
- CLI tools/wrappers using exec/spawn to run other CLI tools (their stated purpose)
|
|
278
|
+
- SDK packages reading API keys from env vars (standard configuration)
|
|
279
|
+
- Build tools with postinstall that compile native addons (node-gyp, prebuild)
|
|
280
|
+
- Packages reading process.env for feature flags, logging config, or database URLs
|
|
281
|
+
- Monorepo tooling with dynamic require for loading workspace packages
|
|
282
|
+
- Test frameworks that use eval() or vm.runInContext for sandboxed test execution
|
|
242
283
|
|
|
243
284
|
RESPOND IN STRICT JSON ONLY (nothing else):
|
|
244
285
|
{
|
|
245
286
|
"verdict": "malicious" | "benign" | "uncertain",
|
|
246
287
|
"confidence": 0.0-1.0,
|
|
247
|
-
"
|
|
288
|
+
"investigation_steps": ["Step 1: ...", "Step 2: ...", "Step 3: ..."],
|
|
289
|
+
"reasoning": "Final summary of your analysis",
|
|
248
290
|
"iocs_found": ["domain.com", "1.2.3.4"],
|
|
249
291
|
"attack_type": "credential_exfil" | "reverse_shell" | "crypto_miner" | "backdoor" | "typosquat" | "protestware" | null,
|
|
250
292
|
"recommendation": "block" | "monitor" | "safe"
|
|
@@ -275,15 +317,15 @@ function buildPrompt(name, version, ecosystem, sourceContext, threats, npmRegist
|
|
|
275
317
|
userContent += '\n';
|
|
276
318
|
}
|
|
277
319
|
|
|
278
|
-
// Static scanner findings
|
|
320
|
+
// Static scanner findings — framed as signals to challenge
|
|
279
321
|
if (threats && threats.length > 0) {
|
|
280
|
-
userContent += `## Static Scanner
|
|
322
|
+
userContent += `## Static Scanner Signals (${threats.length} total — these are SIGNALS to investigate, not confirmed threats)\n`;
|
|
281
323
|
for (const t of threats.slice(0, 30)) {
|
|
282
324
|
const loc = t.file ? ` in ${t.file}${t.line ? ':' + t.line : ''}` : '';
|
|
283
325
|
userContent += `- [${t.severity}] ${t.type}${loc}: ${t.message || ''}\n`;
|
|
284
326
|
}
|
|
285
327
|
if (threats.length > 30) {
|
|
286
|
-
userContent += `... and ${threats.length - 30} more
|
|
328
|
+
userContent += `... and ${threats.length - 30} more signals\n`;
|
|
287
329
|
}
|
|
288
330
|
userContent += '\n';
|
|
289
331
|
}
|
|
@@ -315,7 +357,7 @@ async function callAnthropicAPI(system, messages) {
|
|
|
315
357
|
|
|
316
358
|
const body = JSON.stringify({
|
|
317
359
|
model: MODEL_ID,
|
|
318
|
-
max_tokens:
|
|
360
|
+
max_tokens: 2048,
|
|
319
361
|
system,
|
|
320
362
|
messages
|
|
321
363
|
});
|
|
@@ -384,6 +426,7 @@ function parseResponse(text) {
|
|
|
384
426
|
const fallback = {
|
|
385
427
|
verdict: 'uncertain',
|
|
386
428
|
confidence: 0,
|
|
429
|
+
investigation_steps: [],
|
|
387
430
|
reasoning: 'Failed to parse LLM response',
|
|
388
431
|
iocs_found: [],
|
|
389
432
|
attack_type: null,
|
|
@@ -434,6 +477,7 @@ function parseResponse(text) {
|
|
|
434
477
|
return {
|
|
435
478
|
verdict,
|
|
436
479
|
confidence: Math.round(confidence * 1000) / 1000,
|
|
480
|
+
investigation_steps: Array.isArray(parsed.investigation_steps) ? parsed.investigation_steps.filter(x => typeof x === 'string').slice(0, 10) : [],
|
|
437
481
|
reasoning: typeof parsed.reasoning === 'string' ? parsed.reasoning : '',
|
|
438
482
|
iocs_found: Array.isArray(parsed.iocs_found) ? parsed.iocs_found.filter(x => typeof x === 'string').slice(0, 20) : [],
|
|
439
483
|
attack_type: typeof parsed.attack_type === 'string' ? parsed.attack_type : null,
|
package/src/monitor/webhook.js
CHANGED
|
@@ -379,6 +379,7 @@ function buildAlertData(name, version, ecosystem, result, sandboxResult, llmResu
|
|
|
379
379
|
webhookData.llm = {
|
|
380
380
|
verdict: llmResult.verdict,
|
|
381
381
|
confidence: llmResult.confidence,
|
|
382
|
+
investigation_steps: (llmResult.investigation_steps || []).slice(0, 5),
|
|
382
383
|
reasoning: (llmResult.reasoning || '').slice(0, 200),
|
|
383
384
|
attack_type: llmResult.attack_type || null,
|
|
384
385
|
iocs_found: (llmResult.iocs_found || []).slice(0, 5),
|