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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "muaddib-scanner",
3
- "version": "2.10.34",
3
+ "version": "2.10.35",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -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} ` : '';
@@ -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 the COMPLETE source code of a suspect npm/PyPI package and the results of a static threat scanner.
216
-
217
- Your job: determine if this package is MALICIOUS or LEGITIMATE.
218
-
219
- METHODICAL ANALYSIS:
220
- 1. Read ALL the code, not just flagged files
221
- 2. Look for exfiltration patterns: process.env -> HTTP/DNS/WebSocket to an external domain
222
- 3. Look for persistence patterns: writes to ~/.bashrc, ~/.npmrc, crontab, systemd
223
- 4. Look for obfuscation patterns: eval(atob(...)), Buffer.from(...,'base64'), String.fromCharCode chains
224
- 5. Look for reverse shell patterns: child_process.exec + /bin/sh + net.Socket
225
- 6. Check coherence: does the README match the code? Are declared dependencies actually used?
226
- 7. Check lifecycle scripts: what does postinstall/preinstall actually do?
227
-
228
- LEGITIMATE PATTERNS (DO NOT FLAG):
229
- - CLI tools using child_process for documented commands
230
- - Bundlers/transpilers doing dynamic require/import
231
- - Web frameworks accessing process.env for configuration
232
- - Build tools downloading native binaries from their own CDN/GitHub releases
233
- - Packages with >1000 weekly downloads AND an active GitHub repo with stars
234
-
235
- MALICIOUS PATTERNS:
236
- - Code executed at postinstall unrelated to the described functionality
237
- - Exfiltration of process.env, ~/.npmrc, ~/.ssh, ~/.aws to an external domain
238
- - Obfuscation with no reason (a 10-line package doesn't need minification)
239
- - Suspicious domains in code (raw IPs, recent domains, ngrok, serveo, etc.)
240
- - Empty or copied README (typosquatting signal)
241
- - Package created recently (<7 days) with 0 downloads and dangerous code
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
- "reasoning": "Detailed explanation of your analysis",
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 Findings (${threats.length} total)\n`;
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 findings\n`;
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: 1024,
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,
@@ -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),