muaddib-scanner 2.10.68 → 2.10.69

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.68",
3
+ "version": "2.10.69",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -359,23 +359,36 @@ def filter_leaky_features(X: pd.DataFrame, y: np.ndarray,
359
359
  return X_filtered, retained
360
360
 
361
361
 
362
- def source_discrimination_gate(X: pd.DataFrame, y: np.ndarray,
363
- active_features: list,
364
- max_accuracy: float = 0.65) -> bool:
362
+ def source_discrimination_diagnostic(X: pd.DataFrame, y: np.ndarray,
363
+ active_features: list):
365
364
  """
366
- Step 2c: Hard gate verify that retained behavioral features cannot
367
- trivially distinguish data source (monitor vs Datadog).
365
+ Step 2c: Source discrimination diagnostic (LOG-ONLY, non-blocking).
366
+
367
+ DESIGN NOTE: This test cannot function as a hard gate when source labels
368
+ are perfectly confounded with class labels (all negatives = monitor,
369
+ all positives = Datadog). In that case, legitimate behavioral features
370
+ (score, count_critical, type_*) will dominate the discriminator because
371
+ malware genuinely behaves differently from clean packages — this is
372
+ signal, not leak.
368
373
 
369
- Since all negatives come from monitor and all positives from Datadog,
370
- y IS the source label. A shallow classifier that achieves accuracy > 65%
371
- on the retained features indicates residual source-identity leaks.
374
+ A true source discrimination test would require either:
375
+ (a) positives re-scanned through our own pipeline, or
376
+ (b) negatives and positives from the SAME source.
372
377
 
373
- Returns: True if gate passes (accuracy <= max_accuracy), False if fails.
374
- Prints SHAP top 10 of the discriminator to identify offending features.
378
+ This diagnostic still serves a purpose: it flags NON-BEHAVIORAL features
379
+ that shouldn't appear in the top discriminators. If metadata features
380
+ (unpacked_size_bytes, file_count_total, etc.) appear despite being
381
+ excluded in Step 2a, something is wrong.
382
+
383
+ The real validation happens in shadow deployment on live production data.
375
384
  """
376
385
  print("\n" + "=" * 60)
377
- print(f"[Step 2c/8] Source discrimination gate (threshold={max_accuracy:.0%})...")
386
+ print("[Step 2c/8] Source discrimination diagnostic (log-only)...")
378
387
  print("=" * 60)
388
+ print(" NOTE: source=Datadog correlates 100% with label=malicious.")
389
+ print(" This diagnostic checks for non-behavioral features in the")
390
+ print(" top discriminators, NOT for overall accuracy (which will")
391
+ print(" always be high due to the source/label confound).")
379
392
 
380
393
  X_active = X[active_features]
381
394
 
@@ -385,7 +398,6 @@ def source_discrimination_gate(X: pd.DataFrame, y: np.ndarray,
385
398
  )
386
399
 
387
400
  # Shallow model — depth=3, 50 rounds, no class weighting
388
- # (we want to detect ANY discriminability, not optimize for one class)
389
401
  params = {
390
402
  'objective': 'binary:logistic',
391
403
  'eval_metric': 'logloss',
@@ -407,35 +419,52 @@ def source_discrimination_gate(X: pd.DataFrame, y: np.ndarray,
407
419
  p = precision_score(y_te, preds, zero_division=0)
408
420
  r = recall_score(y_te, preds, zero_division=0)
409
421
 
410
- print(f" Discrimination accuracy: {accuracy:.3f} (P={p:.3f} R={r:.3f})")
422
+ print(f"\n Discrimination accuracy: {accuracy:.3f} (P={p:.3f} R={r:.3f})")
423
+ print(f" (Expected to be high due to source/label confound)")
411
424
 
412
- # SHAP analysis to identify which features drive discrimination
425
+ # SHAP analysis the diagnostic value is in WHICH features dominate
413
426
  explainer = shap.TreeExplainer(model)
414
427
  shap_values = explainer.shap_values(X_te)
415
428
  mean_abs_shap = np.abs(shap_values).mean(axis=0)
416
429
  importance = sorted(zip(active_features, mean_abs_shap),
417
430
  key=lambda x: x[1], reverse=True)
418
431
 
419
- print(f"\n Top 10 features driving source discrimination:")
432
+ # Known behavioral features that SHOULD dominate (malware scores higher)
433
+ EXPECTED_BEHAVIORAL = {
434
+ 'score', 'global_risk_score', 'max_file_score', 'package_score',
435
+ 'count_total', 'count_critical', 'count_high', 'count_medium',
436
+ 'count_low', 'distinct_threat_types', 'severity_ratio_high',
437
+ 'max_single_points', 'points_concentration', 'file_count_with_threats',
438
+ 'file_score_mean', 'file_score_max', 'threat_density',
439
+ }
440
+ # Features that should NOT appear (already excluded, but sanity check)
441
+ EXCLUDED_CHECK = {
442
+ 'unpacked_size_bytes', 'file_count_total', 'has_tests',
443
+ 'dep_count', 'dev_dep_count', 'reputation_factor',
444
+ 'package_age_days', 'weekly_downloads', 'version_count',
445
+ 'author_package_count', 'has_repository', 'readme_size',
446
+ }
447
+
448
+ print(f"\n Top 10 features driving discrimination:")
449
+ has_leak = False
420
450
  for i, (name, val) in enumerate(importance[:10]):
421
- flag = ""
422
- # Flag non-behavioral features that shouldn't be discriminative
423
- if name in ('unpacked_size_bytes', 'file_count_total', 'has_tests',
424
- 'dep_count', 'dev_dep_count', 'reputation_factor'):
425
- flag = " *** NON-BEHAVIORAL"
451
+ if name in EXCLUDED_CHECK:
452
+ flag = " *** LEAK should have been excluded in Step 2a!"
453
+ has_leak = True
454
+ elif name in EXPECTED_BEHAVIORAL:
455
+ flag = " (expected — behavioral)"
456
+ elif name.startswith('type_') or name.startswith('has_'):
457
+ flag = " (behavioral signal)"
458
+ else:
459
+ flag = ""
426
460
  print(f" {i + 1:2d}. {name:40s} {val:.6f}{flag}")
427
461
 
428
- if accuracy <= max_accuracy:
429
- print(f"\n [GATE PASS] Accuracy {accuracy:.3f} <= {max_accuracy:.3f}")
430
- print(f" Behavioral features do not trivially encode source identity.")
431
- return True
462
+ if has_leak:
463
+ print(f"\n [WARNING] Non-behavioral features found in top discriminators!")
464
+ print(f" Check EXCLUDED_METADATA some metadata features leaked through.")
432
465
  else:
433
- print(f"\n [GATE FAIL] Accuracy {accuracy:.3f} > {max_accuracy:.3f}")
434
- print(f" Retained features still encode source identity.")
435
- print(f" Offending features (exclude and re-run):")
436
- for name, val in importance[:5]:
437
- print(f" - {name} (SHAP={val:.6f})")
438
- return False
466
+ print(f"\n [OK] Top discriminators are all behavioral features.")
467
+ print(f" No metadata/source-proxy leak detected.")
439
468
 
440
469
 
441
470
  def split_data(X: pd.DataFrame, y: np.ndarray) -> tuple:
@@ -836,18 +865,15 @@ def main():
836
865
  else:
837
866
  active_features = list(remaining_features)
838
867
 
839
- # Step 2c: Source discrimination gate — HARD STOP if features encode source
868
+ # Step 2c: Source discrimination diagnostic (log-only).
869
+ # NOT a hard gate — source label is 100% confounded with class label
870
+ # (all positives = Datadog, all negatives = monitor), so behavioral
871
+ # features will always dominate the discriminator. The diagnostic
872
+ # checks that no METADATA features leaked through Step 2a.
840
873
  if not args.skip_gate:
841
- gate_pass = source_discrimination_gate(X, y, active_features)
842
- if not gate_pass:
843
- print("\n" + "=" * 60)
844
- print("ABORTED: Source discrimination gate failed.")
845
- print("The retained features still encode source identity.")
846
- print("Add offending features to EXCLUDED_METADATA and re-run.")
847
- print("=" * 60)
848
- sys.exit(1)
874
+ source_discrimination_diagnostic(X, y, active_features)
849
875
  else:
850
- print("\n [Step 2c] Source discrimination gate SKIPPED (--skip-gate)")
876
+ print("\n [Step 2c] Source discrimination diagnostic SKIPPED (--skip-gate)")
851
877
 
852
878
  # Class imbalance weight
853
879
  n_neg = stats['n_neg']