muaddib-scanner 2.10.87 → 2.10.91

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.87",
3
+ "version": "2.10.91",
4
4
  "description": "Supply-chain threat detection & response for npm & PyPI/Python",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -0,0 +1,11 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * XGBoost model trees — auto-generated by src/ml/train-xgboost.py
5
+ * 144 trees, 40 features, threshold=0.52
6
+ * CV: P=0.913 R=0.971 F1=0.941
7
+ * Holdout: P=0.999 R=0.929 F1=0.963
8
+ * DO NOT EDIT MANUALLY
9
+ */
10
+
11
+ module.exports = {"version":1,"features":["unpacked_size_bytes","score","global_risk_score","threat_density","file_count_total","package_score","max_single_points","has_tests","count_total","max_file_score","type_lifecycle_script","version_count","points_concentration","author_package_count","type_credential_regex_harvest","distinct_threat_types","count_critical","count_high","count_medium","count_low","type_suspicious_dataflow","type_env_access","type_sensitive_string","type_dangerous_call_eval","type_dangerous_call_exec","type_dangerous_call_function","type_obfuscation_detected","type_high_entropy_string","type_dynamic_require","type_dynamic_import","type_typosquat_detected","type_staged_payload","type_staged_binary_payload","type_network_require","type_sandbox_evasion","type_remote_code_load","type_suspicious_domain","type_prototype_hook","type_intent_credential_exfil","type_crypto_decipher"],"threshold":0.52,"trees":[[{"f":1,"t":2,"y":1,"n":14,"v":0},{"f":5,"t":1,"y":2,"n":11,"v":0},{"f":3,"t":1,"y":3,"n":8,"v":0},{"f":7,"t":1,"y":4,"n":7,"v":0},{"f":1,"t":1,"y":5,"n":6,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.166096},{"f":-1,"t":0,"y":0,"n":0,"v":-0.199006},{"f":-1,"t":0,"y":0,"n":0,"v":-0.19908},{"f":11,"t":1,"y":9,"n":10,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.165379},{"f":-1,"t":0,"y":0,"n":0,"v":-0.196779},{"f":11,"t":1,"y":12,"n":13,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.178149},{"f":-1,"t":0,"y":0,"n":0,"v":-0.178459},{"f":11,"t":1,"y":15,"n":22,"v":0},{"f":3,"t":1,"y":16,"n":21,"v":0},{"f":9,"t":1,"y":17,"n":20,"v":0},{"f":1,"t":4,"y":18,"n":19,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.179951},{"f":-1,"t":0,"y":0,"n":0,"v":0.199021},{"f":-1,"t":0,"y":0,"n":0,"v":-0.199171},{"f":-1,"t":0,"y":0,"n":0,"v":0.199812},{"f":-1,"t":0,"y":0,"n":0,"v":-0.199165}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.023559},{"f":-1,"t":0,"y":0,"n":0,"v":-0.179379},{"f":-1,"t":0,"y":0,"n":0,"v":0.181997},{"f":-1,"t":0,"y":0,"n":0,"v":-0.184263}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.025297},{"f":-1,"t":0,"y":0,"n":0,"v":-0.166025},{"f":-1,"t":0,"y":0,"n":0,"v":0.168281},{"f":-1,"t":0,"y":0,"n":0,"v":-0.170089}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.018951},{"f":-1,"t":0,"y":0,"n":0,"v":-0.155613},{"f":-1,"t":0,"y":0,"n":0,"v":0.157681},{"f":-1,"t":0,"y":0,"n":0,"v":-0.159127}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.019364},{"f":-1,"t":0,"y":0,"n":0,"v":-0.147067},{"f":-1,"t":0,"y":0,"n":0,"v":0.149269},{"f":-1,"t":0,"y":0,"n":0,"v":-0.15043}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.005689},{"f":-1,"t":0,"y":0,"n":0,"v":0.142388},{"f":-1,"t":0,"y":0,"n":0,"v":-0.143374}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002889},{"f":-1,"t":0,"y":0,"n":0,"v":0.136737},{"f":-1,"t":0,"y":0,"n":0,"v":-0.137571}],[{"f":1,"t":2,"y":1,"n":10,"v":0},{"f":4,"t":1,"y":2,"n":9,"v":0},{"f":3,"t":1,"y":3,"n":8,"v":0},{"f":9,"t":1,"y":4,"n":7,"v":0},{"f":7,"t":1,"y":5,"n":6,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.091257},{"f":-1,"t":0,"y":0,"n":0,"v":-0.14027},{"f":-1,"t":0,"y":0,"n":0,"v":-0.130949},{"f":-1,"t":0,"y":0,"n":0,"v":0.115779},{"f":-1,"t":0,"y":0,"n":0,"v":-0.132801},{"f":11,"t":1,"y":11,"n":18,"v":0},{"f":3,"t":1,"y":12,"n":17,"v":0},{"f":9,"t":1,"y":13,"n":16,"v":0},{"f":1,"t":4,"y":14,"n":15,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.11795},{"f":-1,"t":0,"y":0,"n":0,"v":0.131006},{"f":-1,"t":0,"y":0,"n":0,"v":-0.131716},{"f":-1,"t":0,"y":0,"n":0,"v":0.131944},{"f":-1,"t":0,"y":0,"n":0,"v":-0.131069}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.021324},{"f":-1,"t":0,"y":0,"n":0,"v":-0.135022},{"f":-1,"t":0,"y":0,"n":0,"v":0.12808},{"f":-1,"t":0,"y":0,"n":0,"v":-0.129409}],[{"f":1,"t":5,"y":1,"n":26,"v":0},{"f":1,"t":2,"y":2,"n":11,"v":0},{"f":4,"t":1,"y":3,"n":10,"v":0},{"f":7,"t":1,"y":4,"n":9,"v":0},{"f":29,"t":1,"y":5,"n":8,"v":0},{"f":2,"t":6,"y":6,"n":7,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.082086},{"f":-1,"t":0,"y":0,"n":0,"v":-0.01764},{"f":-1,"t":0,"y":0,"n":0,"v":-0.119125},{"f":-1,"t":0,"y":0,"n":0,"v":-0.12998},{"f":-1,"t":0,"y":0,"n":0,"v":-0.125232},{"f":11,"t":1,"y":12,"n":25,"v":0},{"f":9,"t":3,"y":13,"n":18,"v":0},{"f":2,"t":3,"y":14,"n":15,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.07023},{"f":6,"t":3,"y":16,"n":17,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.082875},{"f":-1,"t":0,"y":0,"n":0,"v":0.110778},{"f":15,"t":2,"y":19,"n":22,"v":0},{"f":27,"t":1,"y":20,"n":21,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.107038},{"f":-1,"t":0,"y":0,"n":0,"v":0.013351},{"f":25,"t":1,"y":23,"n":24,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.030529},{"f":-1,"t":0,"y":0,"n":0,"v":0.115054},{"f":-1,"t":0,"y":0,"n":0,"v":-0.123642},{"f":11,"t":1,"y":27,"n":36,"v":0},{"f":6,"t":6,"y":28,"n":35,"v":0},{"f":14,"t":3,"y":29,"n":34,"v":0},{"f":18,"t":4,"y":30,"n":33,"v":0},{"f":9,"t":4,"y":31,"n":32,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.118541},{"f":-1,"t":0,"y":0,"n":0,"v":0.052778},{"f":-1,"t":0,"y":0,"n":0,"v":-0.023891},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099574},{"f":-1,"t":0,"y":0,"n":0,"v":0.124699},{"f":-1,"t":0,"y":0,"n":0,"v":-0.110215}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.033674},{"f":-1,"t":0,"y":0,"n":0,"v":-0.126251},{"f":-1,"t":0,"y":0,"n":0,"v":0.121893},{"f":-1,"t":0,"y":0,"n":0,"v":-0.123543}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":2,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.026005},{"f":-1,"t":0,"y":0,"n":0,"v":-0.122481},{"f":-1,"t":0,"y":0,"n":0,"v":0.119334},{"f":-1,"t":0,"y":0,"n":0,"v":-0.120804}],[{"f":6,"t":3,"y":1,"n":12,"v":0},{"f":4,"t":1,"y":2,"n":11,"v":0},{"f":3,"t":1,"y":3,"n":10,"v":0},{"f":9,"t":1,"y":4,"n":9,"v":0},{"f":5,"t":1,"y":5,"n":8,"v":0},{"f":7,"t":1,"y":6,"n":7,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.066989},{"f":-1,"t":0,"y":0,"n":0,"v":-0.119292},{"f":-1,"t":0,"y":0,"n":0,"v":0.1009},{"f":-1,"t":0,"y":0,"n":0,"v":-0.117542},{"f":-1,"t":0,"y":0,"n":0,"v":0.112724},{"f":-1,"t":0,"y":0,"n":0,"v":-0.117537},{"f":11,"t":1,"y":13,"n":24,"v":0},{"f":6,"t":6,"y":14,"n":23,"v":0},{"f":5,"t":1,"y":15,"n":18,"v":0},{"f":3,"t":1,"y":16,"n":17,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.117232},{"f":-1,"t":0,"y":0,"n":0,"v":0.105563},{"f":14,"t":1,"y":19,"n":22,"v":0},{"f":8,"t":2,"y":20,"n":21,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.097505},{"f":-1,"t":0,"y":0,"n":0,"v":0.11452},{"f":-1,"t":0,"y":0,"n":0,"v":0.008878},{"f":-1,"t":0,"y":0,"n":0,"v":0.117101},{"f":-1,"t":0,"y":0,"n":0,"v":-0.115938}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":2,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.029886},{"f":-1,"t":0,"y":0,"n":0,"v":-0.116787},{"f":-1,"t":0,"y":0,"n":0,"v":0.11528},{"f":-1,"t":0,"y":0,"n":0,"v":-0.116945}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.021102},{"f":-1,"t":0,"y":0,"n":0,"v":0.113632},{"f":-1,"t":0,"y":0,"n":0,"v":-0.115084}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.023093},{"f":-1,"t":0,"y":0,"n":0,"v":-0.114995},{"f":-1,"t":0,"y":0,"n":0,"v":0.112018},{"f":-1,"t":0,"y":0,"n":0,"v":-0.113463}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.021435},{"f":-1,"t":0,"y":0,"n":0,"v":-0.112506},{"f":-1,"t":0,"y":0,"n":0,"v":0.110807},{"f":-1,"t":0,"y":0,"n":0,"v":-0.111999}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.021613},{"f":-1,"t":0,"y":0,"n":0,"v":-0.110627},{"f":-1,"t":0,"y":0,"n":0,"v":0.109574},{"f":-1,"t":0,"y":0,"n":0,"v":-0.110723}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.022198},{"f":-1,"t":0,"y":0,"n":0,"v":-0.108767},{"f":-1,"t":0,"y":0,"n":0,"v":0.108453},{"f":-1,"t":0,"y":0,"n":0,"v":-0.109588}],[{"f":1,"t":5,"y":1,"n":14,"v":0},{"f":10,"t":1,"y":2,"n":11,"v":0},{"f":4,"t":1,"y":3,"n":10,"v":0},{"f":3,"t":1,"y":4,"n":9,"v":0},{"f":1,"t":1,"y":5,"n":8,"v":0},{"f":7,"t":1,"y":6,"n":7,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.046418},{"f":-1,"t":0,"y":0,"n":0,"v":-0.107087},{"f":-1,"t":0,"y":0,"n":0,"v":-0.10833},{"f":-1,"t":0,"y":0,"n":0,"v":0.101423},{"f":-1,"t":0,"y":0,"n":0,"v":-0.108002},{"f":11,"t":1,"y":12,"n":13,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.086705},{"f":-1,"t":0,"y":0,"n":0,"v":-0.105347},{"f":1,"t":9,"y":15,"n":18,"v":0},{"f":5,"t":2,"y":16,"n":17,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.062312},{"f":-1,"t":0,"y":0,"n":0,"v":0.093939},{"f":18,"t":6,"y":19,"n":20,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.107482},{"f":1,"t":90,"y":21,"n":22,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.025131},{"f":-1,"t":0,"y":0,"n":0,"v":0.092991}],[{"f":1,"t":5,"y":1,"n":14,"v":0},{"f":4,"t":1,"y":2,"n":13,"v":0},{"f":5,"t":1,"y":3,"n":10,"v":0},{"f":3,"t":1,"y":4,"n":9,"v":0},{"f":1,"t":1,"y":5,"n":8,"v":0},{"f":7,"t":1,"y":6,"n":7,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.04076},{"f":-1,"t":0,"y":0,"n":0,"v":-0.105715},{"f":-1,"t":0,"y":0,"n":0,"v":-0.107229},{"f":-1,"t":0,"y":0,"n":0,"v":0.098057},{"f":11,"t":1,"y":11,"n":12,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.085153},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101203},{"f":-1,"t":0,"y":0,"n":0,"v":-0.107192},{"f":6,"t":6,"y":15,"n":20,"v":0},{"f":5,"t":2,"y":16,"n":19,"v":0},{"f":3,"t":1,"y":17,"n":18,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.094571},{"f":-1,"t":0,"y":0,"n":0,"v":0.022625},{"f":-1,"t":0,"y":0,"n":0,"v":0.087403},{"f":-1,"t":0,"y":0,"n":0,"v":0.106893}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.027722},{"f":-1,"t":0,"y":0,"n":0,"v":-0.10433},{"f":-1,"t":0,"y":0,"n":0,"v":0.105961},{"f":-1,"t":0,"y":0,"n":0,"v":-0.107632}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.024552},{"f":-1,"t":0,"y":0,"n":0,"v":-0.102988},{"f":-1,"t":0,"y":0,"n":0,"v":0.105362},{"f":-1,"t":0,"y":0,"n":0,"v":-0.106852}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":2,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.018426},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101567},{"f":-1,"t":0,"y":0,"n":0,"v":0.104741},{"f":-1,"t":0,"y":0,"n":0,"v":-0.106146}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.017335},{"f":-1,"t":0,"y":0,"n":0,"v":-0.100818},{"f":-1,"t":0,"y":0,"n":0,"v":0.104173},{"f":-1,"t":0,"y":0,"n":0,"v":-0.105523}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.018273},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099411},{"f":-1,"t":0,"y":0,"n":0,"v":0.103791},{"f":-1,"t":0,"y":0,"n":0,"v":-0.10493}],[{"f":1,"t":5,"y":1,"n":14,"v":0},{"f":4,"t":1,"y":2,"n":13,"v":0},{"f":5,"t":1,"y":3,"n":10,"v":0},{"f":3,"t":1,"y":4,"n":9,"v":0},{"f":1,"t":1,"y":5,"n":8,"v":0},{"f":7,"t":1,"y":6,"n":7,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.032564},{"f":-1,"t":0,"y":0,"n":0,"v":-0.098053},{"f":-1,"t":0,"y":0,"n":0,"v":-0.106692},{"f":-1,"t":0,"y":0,"n":0,"v":0.091536},{"f":11,"t":1,"y":11,"n":12,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.077321},{"f":-1,"t":0,"y":0,"n":0,"v":-0.093958},{"f":-1,"t":0,"y":0,"n":0,"v":-0.103987},{"f":6,"t":6,"y":15,"n":20,"v":0},{"f":5,"t":2,"y":16,"n":17,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.047188},{"f":15,"t":3,"y":18,"n":19,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.084242},{"f":-1,"t":0,"y":0,"n":0,"v":0.044127},{"f":-1,"t":0,"y":0,"n":0,"v":0.103513}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.01902},{"f":-1,"t":0,"y":0,"n":0,"v":-0.097488},{"f":-1,"t":0,"y":0,"n":0,"v":0.102764},{"f":-1,"t":0,"y":0,"n":0,"v":-0.104236}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.014549},{"f":-1,"t":0,"y":0,"n":0,"v":-0.096213},{"f":-1,"t":0,"y":0,"n":0,"v":0.102349},{"f":-1,"t":0,"y":0,"n":0,"v":-0.103801}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.016568},{"f":-1,"t":0,"y":0,"n":0,"v":-0.095081},{"f":-1,"t":0,"y":0,"n":0,"v":0.101765},{"f":-1,"t":0,"y":0,"n":0,"v":-0.103392}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":6,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.013442},{"f":-1,"t":0,"y":0,"n":0,"v":-0.093937},{"f":-1,"t":0,"y":0,"n":0,"v":0.101595},{"f":-1,"t":0,"y":0,"n":0,"v":-0.102998}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.013649},{"f":-1,"t":0,"y":0,"n":0,"v":-0.092676},{"f":-1,"t":0,"y":0,"n":0,"v":0.101264},{"f":-1,"t":0,"y":0,"n":0,"v":-0.102679}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.007743},{"f":-1,"t":0,"y":0,"n":0,"v":-0.091593},{"f":-1,"t":0,"y":0,"n":0,"v":0.101211},{"f":-1,"t":0,"y":0,"n":0,"v":-0.10238}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":2,"t":3,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.011271},{"f":-1,"t":0,"y":0,"n":0,"v":-0.090304},{"f":-1,"t":0,"y":0,"n":0,"v":0.100909},{"f":-1,"t":0,"y":0,"n":0,"v":-0.102087}],[{"f":1,"t":5,"y":1,"n":14,"v":0},{"f":4,"t":1,"y":2,"n":13,"v":0},{"f":5,"t":1,"y":3,"n":10,"v":0},{"f":3,"t":1,"y":4,"n":9,"v":0},{"f":1,"t":1,"y":5,"n":8,"v":0},{"f":7,"t":1,"y":6,"n":7,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.017879},{"f":-1,"t":0,"y":0,"n":0,"v":-0.088443},{"f":-1,"t":0,"y":0,"n":0,"v":-0.109286},{"f":-1,"t":0,"y":0,"n":0,"v":0.082224},{"f":13,"t":1,"y":11,"n":12,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.068141},{"f":-1,"t":0,"y":0,"n":0,"v":-0.083152},{"f":-1,"t":0,"y":0,"n":0,"v":-0.102207},{"f":6,"t":6,"y":15,"n":18,"v":0},{"f":2,"t":9,"y":16,"n":17,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.071793},{"f":-1,"t":0,"y":0,"n":0,"v":-0.008453},{"f":-1,"t":0,"y":0,"n":0,"v":0.101179}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.006758},{"f":-1,"t":0,"y":0,"n":0,"v":-0.088443},{"f":-1,"t":0,"y":0,"n":0,"v":0.10003},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101761}],[{"f":1,"t":5,"y":1,"n":14,"v":0},{"f":4,"t":1,"y":2,"n":13,"v":0},{"f":5,"t":1,"y":3,"n":10,"v":0},{"f":1,"t":1,"y":4,"n":7,"v":0},{"f":7,"t":1,"y":5,"n":6,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.008648},{"f":-1,"t":0,"y":0,"n":0,"v":-0.086637},{"f":3,"t":1,"y":8,"n":9,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.110782},{"f":-1,"t":0,"y":0,"n":0,"v":0.079698},{"f":19,"t":1,"y":11,"n":12,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.041031},{"f":-1,"t":0,"y":0,"n":0,"v":0.073627},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101991},{"f":1,"t":9,"y":15,"n":18,"v":0},{"f":9,"t":3,"y":16,"n":17,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.068487},{"f":-1,"t":0,"y":0,"n":0,"v":0.009985},{"f":-1,"t":0,"y":0,"n":0,"v":0.099853}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":1,"t":1,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.009683},{"f":-1,"t":0,"y":0,"n":0,"v":-0.084594},{"f":-1,"t":0,"y":0,"n":0,"v":0.099913},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101498}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":2,"t":3,"y":2,"n":5,"v":0},{"f":7,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.012524},{"f":-1,"t":0,"y":0,"n":0,"v":-0.084384},{"f":-1,"t":0,"y":0,"n":0,"v":0.099621},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101266}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.008028},{"f":-1,"t":0,"y":0,"n":0,"v":0.099348},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101081}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.00906},{"f":-1,"t":0,"y":0,"n":0,"v":0.099124},{"f":-1,"t":0,"y":0,"n":0,"v":-0.100882}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":2,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.009084},{"f":-1,"t":0,"y":0,"n":0,"v":0.098828},{"f":-1,"t":0,"y":0,"n":0,"v":-0.10069}],[{"f":1,"t":5,"y":1,"n":8,"v":0},{"f":4,"t":1,"y":2,"n":7,"v":0},{"f":5,"t":1,"y":3,"n":6,"v":0},{"f":12,"t":0.51,"y":4,"n":5,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.009047},{"f":-1,"t":0,"y":0,"n":0,"v":-0.072502},{"f":-1,"t":0,"y":0,"n":0,"v":0.042948},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101101},{"f":6,"t":6,"y":9,"n":10,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.041537},{"f":-1,"t":0,"y":0,"n":0,"v":0.099723}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.004616},{"f":-1,"t":0,"y":0,"n":0,"v":0.098232},{"f":-1,"t":0,"y":0,"n":0,"v":-0.100488}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.006863},{"f":-1,"t":0,"y":0,"n":0,"v":0.097879},{"f":-1,"t":0,"y":0,"n":0,"v":-0.100322}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.006221},{"f":-1,"t":0,"y":0,"n":0,"v":0.097599},{"f":-1,"t":0,"y":0,"n":0,"v":-0.100143}],[{"f":6,"t":6,"y":1,"n":12,"v":0},{"f":4,"t":1,"y":2,"n":11,"v":0},{"f":10,"t":1,"y":3,"n":8,"v":0},{"f":6,"t":2,"y":4,"n":7,"v":0},{"f":1,"t":1,"y":5,"n":6,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.003931},{"f":-1,"t":0,"y":0,"n":0,"v":-0.02659},{"f":-1,"t":0,"y":0,"n":0,"v":-0.071719},{"f":8,"t":2,"y":9,"n":10,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.03437},{"f":-1,"t":0,"y":0,"n":0,"v":0.071294},{"f":-1,"t":0,"y":0,"n":0,"v":-0.10067},{"f":-1,"t":0,"y":0,"n":0,"v":0.099017}],[{"f":6,"t":6,"y":1,"n":12,"v":0},{"f":4,"t":1,"y":2,"n":11,"v":0},{"f":10,"t":1,"y":3,"n":8,"v":0},{"f":12,"t":0.51,"y":4,"n":7,"v":0},{"f":7,"t":1,"y":5,"n":6,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.005971},{"f":-1,"t":0,"y":0,"n":0,"v":-0.083438},{"f":-1,"t":0,"y":0,"n":0,"v":-0.0712},{"f":8,"t":2,"y":9,"n":10,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.033137},{"f":-1,"t":0,"y":0,"n":0,"v":0.067336},{"f":-1,"t":0,"y":0,"n":0,"v":-0.101266},{"f":-1,"t":0,"y":0,"n":0,"v":0.098815}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.004883},{"f":-1,"t":0,"y":0,"n":0,"v":0.096699},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099913}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":2,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.011426},{"f":-1,"t":0,"y":0,"n":0,"v":0.097535},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099743}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.008326},{"f":-1,"t":0,"y":0,"n":0,"v":0.095696},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099579}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.005312},{"f":-1,"t":0,"y":0,"n":0,"v":0.09682},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099396}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.000559},{"f":-1,"t":0,"y":0,"n":0,"v":0.094723},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099219}],[{"f":1,"t":5,"y":1,"n":8,"v":0},{"f":4,"t":1,"y":2,"n":7,"v":0},{"f":10,"t":1,"y":3,"n":6,"v":0},{"f":9,"t":1,"y":4,"n":5,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.006157},{"f":-1,"t":0,"y":0,"n":0,"v":-0.050257},{"f":-1,"t":0,"y":0,"n":0,"v":0.033647},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099836},{"f":-1,"t":0,"y":0,"n":0,"v":0.092092}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.003115},{"f":-1,"t":0,"y":0,"n":0,"v":0.09565},{"f":-1,"t":0,"y":0,"n":0,"v":-0.098995}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":2,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.004702},{"f":-1,"t":0,"y":0,"n":0,"v":0.0953},{"f":-1,"t":0,"y":0,"n":0,"v":-0.098789}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.003084},{"f":-1,"t":0,"y":0,"n":0,"v":0.092229},{"f":-1,"t":0,"y":0,"n":0,"v":-0.098583}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001356},{"f":-1,"t":0,"y":0,"n":0,"v":0.091369},{"f":-1,"t":0,"y":0,"n":0,"v":-0.09835}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.000811},{"f":-1,"t":0,"y":0,"n":0,"v":0.093693},{"f":-1,"t":0,"y":0,"n":0,"v":-0.098111}],[{"f":6,"t":6,"y":1,"n":8,"v":0},{"f":7,"t":1,"y":2,"n":7,"v":0},{"f":9,"t":1,"y":3,"n":6,"v":0},{"f":10,"t":1,"y":4,"n":5,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.000275},{"f":-1,"t":0,"y":0,"n":0,"v":0.025253},{"f":-1,"t":0,"y":0,"n":0,"v":-0.048836},{"f":-1,"t":0,"y":0,"n":0,"v":-0.084614},{"f":-1,"t":0,"y":0,"n":0,"v":0.095222}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.004045},{"f":-1,"t":0,"y":0,"n":0,"v":0.092536},{"f":-1,"t":0,"y":0,"n":0,"v":-0.097846}],[{"f":0,"t":247,"y":1,"n":6,"v":0},{"f":3,"t":1,"y":2,"n":5,"v":0},{"f":5,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":5.8e-05},{"f":-1,"t":0,"y":0,"n":0,"v":0.083196},{"f":-1,"t":0,"y":0,"n":0,"v":0.09506},{"f":-1,"t":0,"y":0,"n":0,"v":-0.097564}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.000536},{"f":-1,"t":0,"y":0,"n":0,"v":0.091},{"f":-1,"t":0,"y":0,"n":0,"v":-0.097261}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":3,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002555},{"f":-1,"t":0,"y":0,"n":0,"v":0.094122},{"f":-1,"t":0,"y":0,"n":0,"v":-0.096948}],[{"f":3,"t":1,"y":1,"n":6,"v":0},{"f":4,"t":1,"y":2,"n":5,"v":0},{"f":5,"t":1,"y":3,"n":4,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.00078},{"f":-1,"t":0,"y":0,"n":0,"v":0.038804},{"f":-1,"t":0,"y":0,"n":0,"v":-0.094261},{"f":-1,"t":0,"y":0,"n":0,"v":0.087784}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.0058},{"f":-1,"t":0,"y":0,"n":0,"v":0.088701},{"f":-1,"t":0,"y":0,"n":0,"v":-0.096599}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":3,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001426},{"f":-1,"t":0,"y":0,"n":0,"v":0.092457},{"f":-1,"t":0,"y":0,"n":0,"v":-0.096193}],[{"f":3,"t":1,"y":1,"n":4,"v":0},{"f":5,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.003209},{"f":-1,"t":0,"y":0,"n":0,"v":0.031896},{"f":-1,"t":0,"y":0,"n":0,"v":0.085013}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":5,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001556},{"f":-1,"t":0,"y":0,"n":0,"v":0.091202},{"f":-1,"t":0,"y":0,"n":0,"v":-0.095837}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":4,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.003601},{"f":-1,"t":0,"y":0,"n":0,"v":0.090464},{"f":-1,"t":0,"y":0,"n":0,"v":-0.095378}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002674},{"f":-1,"t":0,"y":0,"n":0,"v":0.083822},{"f":-1,"t":0,"y":0,"n":0,"v":-0.094893}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001674},{"f":-1,"t":0,"y":0,"n":0,"v":0.082211},{"f":-1,"t":0,"y":0,"n":0,"v":-0.094404}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":5,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.000834},{"f":-1,"t":0,"y":0,"n":0,"v":0.088259},{"f":-1,"t":0,"y":0,"n":0,"v":-0.093893}],[{"f":3,"t":1,"y":1,"n":4,"v":0},{"f":2,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.005269},{"f":-1,"t":0,"y":0,"n":0,"v":-0.036392},{"f":-1,"t":0,"y":0,"n":0,"v":0.080925}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":3,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.000685},{"f":-1,"t":0,"y":0,"n":0,"v":0.086733},{"f":-1,"t":0,"y":0,"n":0,"v":-0.093248}],[{"f":4,"t":1,"y":1,"n":4,"v":0},{"f":3,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001125},{"f":-1,"t":0,"y":0,"n":0,"v":0.08522},{"f":-1,"t":0,"y":0,"n":0,"v":-0.099498}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":5,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001384},{"f":-1,"t":0,"y":0,"n":0,"v":0.085424},{"f":-1,"t":0,"y":0,"n":0,"v":-0.092467}],[{"f":3,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.003939},{"f":-1,"t":0,"y":0,"n":0,"v":0.076745}],[{"f":3,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.00093},{"f":-1,"t":0,"y":0,"n":0,"v":0.074932}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":2,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.000708},{"f":-1,"t":0,"y":0,"n":0,"v":0.073391},{"f":-1,"t":0,"y":0,"n":0,"v":-0.091725}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.001064},{"f":-1,"t":0,"y":0,"n":0,"v":0.057369},{"f":-1,"t":0,"y":0,"n":0,"v":-0.09104}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.000437},{"f":-1,"t":0,"y":0,"n":0,"v":0.055602},{"f":-1,"t":0,"y":0,"n":0,"v":-0.090227}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002596},{"f":-1,"t":0,"y":0,"n":0,"v":0.053598},{"f":-1,"t":0,"y":0,"n":0,"v":-0.089376}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.000134},{"f":-1,"t":0,"y":0,"n":0,"v":0.066822},{"f":-1,"t":0,"y":0,"n":0,"v":-0.088497}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":3,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.00145},{"f":-1,"t":0,"y":0,"n":0,"v":0.065197},{"f":-1,"t":0,"y":0,"n":0,"v":-0.087546}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002205},{"f":-1,"t":0,"y":0,"n":0,"v":0.037426},{"f":-1,"t":0,"y":0,"n":0,"v":-0.086515}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":2,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001147},{"f":-1,"t":0,"y":0,"n":0,"v":0.036726},{"f":-1,"t":0,"y":0,"n":0,"v":-0.08543}],[{"f":2,"t":3,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.000983},{"f":-1,"t":0,"y":0,"n":0,"v":0.040875}],[{"f":0,"t":247,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001742},{"f":-1,"t":0,"y":0,"n":0,"v":-0.084391}],[{"f":0,"t":247,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002619},{"f":-1,"t":0,"y":0,"n":0,"v":-0.083286}],[{"f":0,"t":247,"y":1,"n":4,"v":0},{"f":1,"t":1,"y":2,"n":3,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.00033},{"f":-1,"t":0,"y":0,"n":0,"v":0.032568},{"f":-1,"t":0,"y":0,"n":0,"v":-0.08209}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.003375}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001254},{"f":-1,"t":0,"y":0,"n":0,"v":0.043454}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.0005}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.001056}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.003655},{"f":-1,"t":0,"y":0,"n":0,"v":0.026255}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.000518}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.001169}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.004161},{"f":-1,"t":0,"y":0,"n":0,"v":0.008372}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.00082},{"f":-1,"t":0,"y":0,"n":0,"v":0.023861}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.00109}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.001096}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.000711}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.000883}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.002565}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.002175},{"f":-1,"t":0,"y":0,"n":0,"v":0.022166}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.000193}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.002949}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.002481},{"f":-1,"t":0,"y":0,"n":0,"v":0.020291}],[{"f":-1,"t":0,"y":0,"n":0,"v":-9.6e-05}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.002123}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.000329}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.001707}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.000378}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001883}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.001748}],[{"f":12,"t":0.17,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001974},{"f":-1,"t":0,"y":0,"n":0,"v":-0.011073}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.003072}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.002935}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":-0.00234},{"f":-1,"t":0,"y":0,"n":0,"v":0.018895}],[{"f":12,"t":0.25,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002743},{"f":-1,"t":0,"y":0,"n":0,"v":-0.01085}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.002345}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.000839}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.003219}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.002346}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001383}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001845}],[{"f":1,"t":1,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.002236},{"f":-1,"t":0,"y":0,"n":0,"v":0.018535}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.004369}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.004391}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.000625}],[{"f":12,"t":0.07,"y":1,"n":2,"v":0},{"f":-1,"t":0,"y":0,"n":0,"v":0.001467},{"f":-1,"t":0,"y":0,"n":0,"v":-0.012558}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001865}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.00056}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.00092}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001952}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.002477}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.001695}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001402}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.001508}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001024}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.003954}],[{"f":-1,"t":0,"y":0,"n":0,"v":0.001829}],[{"f":-1,"t":0,"y":0,"n":0,"v":-0.000125}]]};
@@ -2,16 +2,18 @@ const { execFileSync } = require('child_process');
2
2
  const fs = require('fs');
3
3
  const path = require('path');
4
4
  const os = require('os');
5
+ const v8 = require('v8');
5
6
  const { isDockerAvailable, SANDBOX_CONCURRENCY_MAX } = require('../sandbox/index.js');
6
7
  const { setVerboseMode, isSandboxEnabled, isCanaryEnabled, isLlmDetectiveEnabled, getLlmDetectiveMode, DOWNLOADS_CACHE_TTL } = require('./classify.js');
7
- const { loadState, saveState, loadDailyStats, saveDailyStats, purgeTarballCache, getParisHour, atomicWriteFileSync, saveNpmSeq } = require('./state.js');
8
+ const { loadState, saveState, loadDailyStats, saveDailyStats, purgeTarballCache, getParisHour, atomicWriteFileSync, saveNpmSeq, ALERTS_FILE } = require('./state.js');
8
9
  const { isTemporalEnabled, isTemporalAstEnabled, isTemporalPublishEnabled, isTemporalMaintainerEnabled } = require('./temporal.js');
9
10
  const { pendingGrouped, flushScopeGroup, sendDailyReport, DAILY_REPORT_HOUR, alertedPackageRules } = require('./webhook.js');
10
11
  const { poll } = require('./ingestion.js');
11
12
  const { processQueue, ensureWorkers, drainWorkers, getTargetConcurrency, setTargetConcurrency, getActiveWorkers, SCAN_CONCURRENCY } = require('./queue.js');
12
13
  const { computeTarget, ADJUST_INTERVAL_MS, BASE_CONCURRENCY, resetDeltas } = require('./adaptive-concurrency.js');
13
14
  const { startHealthcheck } = require('./healthcheck.js');
14
- const { startDeferredWorker, stopDeferredWorker, persistDeferredQueue, restoreDeferredQueue } = require('./deferred-sandbox.js');
15
+ const { startDeferredWorker, stopDeferredWorker, persistDeferredQueue, restoreDeferredQueue, clearDeferredQueue } = require('./deferred-sandbox.js');
16
+ const { clearMetadataCache } = require('../scanner/temporal-analysis.js');
15
17
 
16
18
  const POLL_INTERVAL = 60_000;
17
19
  const PROCESS_LOOP_INTERVAL = 2_000; // Queue check interval when empty
@@ -21,9 +23,49 @@ const QUEUE_STATE_FILE = path.join(__dirname, '..', '..', 'data', 'queue-state.j
21
23
  const QUEUE_STATE_MAX_AGE_MS = 24 * 60 * 60 * 1000; // 24h expiry
22
24
  const MAX_QUEUE_PERSIST_SIZE = 200_000; // Don't persist if queue > 200K items (OOM guard)
23
25
  const MAX_RESTORE_QUEUE_SIZE = 100_000; // Cap restored queue at 100K items
24
- // MAX_SCAN_QUEUE removed: backpressure no longer skips polling.
25
- // Queue grows unbounded in memory (entries are ~300B, 100K = 30MB on 12GB VPS).
26
- // Adaptive concurrency adjusts processing speed to match ingestion rate.
26
+
27
+ // ─── Memory pressure circuit breaker ───
28
+ // Graduated response based on V8 heap usage against heap_size_limit.
29
+ // Threat model: when GC thrashing starts (>90% heap limit), throughput drops to 0
30
+ // and the queue grows unbounded because ingestion continues. Without a circuit
31
+ // breaker, the only recovery is OOM kill or manual restart.
32
+ //
33
+ // Denominator: v8.getHeapStatistics().heap_size_limit (NOT process.memoryUsage().heapTotal).
34
+ // V8 dynamically adjusts heapTotal so heapUsed/heapTotal is structurally 70-85%
35
+ // even when actual usage is 0.1% of the --max-old-space-size limit. heap_size_limit
36
+ // reflects the actual V8 ceiling (~3264MB with --max-old-space-size=3072).
37
+ //
38
+ // Levels:
39
+ // NONE (<75%) — normal operation
40
+ // ELEVATED (75%) — log warning, reduce concurrency target
41
+ // HIGH (85%) — prune caches, stop spawning new workers
42
+ // CRITICAL (90%) — stop ingestion, clear scanner caches, force GC
43
+ // EMERGENCY (95%) — truncate queue to most recent N items, clear deferred queue
44
+ //
45
+ // The key insight from the 2026-04-13 incident: emergency prune at 85% only cleared
46
+ // ~4MB of auxiliary caches (recentlyScanned, downloadsCache, alertedPackageRules) on a
47
+ // 3571MB heap. The real memory was held by N concurrent scan workers retaining AST trees,
48
+ // scan results, and extracted file references. Stopping worker spawning is the only way
49
+ // to let running scans finish and release their memory.
50
+ const MEMORY_PRESSURE_LEVELS = {
51
+ NONE: 0,
52
+ ELEVATED: 1,
53
+ HIGH: 2,
54
+ CRITICAL: 3,
55
+ EMERGENCY: 4
56
+ };
57
+ const MEMORY_THRESHOLD_ELEVATED = 0.75;
58
+ const MEMORY_THRESHOLD_HIGH = 0.85;
59
+ const MEMORY_THRESHOLD_CRITICAL = 0.90;
60
+ const MEMORY_THRESHOLD_EMERGENCY = 0.95;
61
+ // When truncating queue under EMERGENCY, keep the N most recent items.
62
+ // These are the newest packages — most likely to still be on npm for re-scan.
63
+ const EMERGENCY_QUEUE_KEEP = 500;
64
+ // Memory check interval adapts: 5min under NONE/ELEVATED, 15s under HIGH+.
65
+ // Fast checks are critical because at 50 pkg/min ingestion, 5min = 250 new items.
66
+ const MEMORY_LOG_INTERVAL_NORMAL = 300_000; // 5 minutes
67
+ const MEMORY_LOG_INTERVAL_PRESSURE = 15_000; // 15 seconds
68
+ let _memoryPressureLevel = MEMORY_PRESSURE_LEVELS.NONE;
27
69
 
28
70
  function sleep(ms) {
29
71
  return new Promise((resolve) => setTimeout(resolve, ms));
@@ -246,6 +288,48 @@ function checkDiskSpace() {
246
288
  const MAX_RECENTLY_SCANNED = 50_000;
247
289
  const MAX_ALERTED_PACKAGES = 5_000;
248
290
 
291
+ /**
292
+ * Compute current memory pressure level from V8 heap usage.
293
+ * Returns one of MEMORY_PRESSURE_LEVELS and updates the module-level _memoryPressureLevel.
294
+ * Cheap call (~0.1ms) — safe to run every 2s in the main loop.
295
+ *
296
+ * IMPORTANT: Uses v8.getHeapStatistics().heap_size_limit as the denominator,
297
+ * NOT process.memoryUsage().heapTotal. V8 adjusts heapTotal dynamically so
298
+ * heapUsed/heapTotal is structurally 70-85% even when actual usage is 0.1%
299
+ * of the --max-old-space-size limit. This caused the initial v2.10.88 circuit
300
+ * breaker to trigger at ELEVATED/HIGH permanently in normal operation.
301
+ *
302
+ * heap_size_limit reflects the actual V8 ceiling:
303
+ * - With --max-old-space-size=3072: ~3264MB (3072 + new space overhead)
304
+ * - Without the flag: ~4288MB (V8 default on 64-bit)
305
+ */
306
+ function computeMemoryPressure() {
307
+ const mem = process.memoryUsage();
308
+ const heapLimit = v8.getHeapStatistics().heap_size_limit;
309
+ const ratio = heapLimit > 0 ? mem.heapUsed / heapLimit : 0;
310
+
311
+ if (ratio >= MEMORY_THRESHOLD_EMERGENCY) {
312
+ _memoryPressureLevel = MEMORY_PRESSURE_LEVELS.EMERGENCY;
313
+ } else if (ratio >= MEMORY_THRESHOLD_CRITICAL) {
314
+ _memoryPressureLevel = MEMORY_PRESSURE_LEVELS.CRITICAL;
315
+ } else if (ratio >= MEMORY_THRESHOLD_HIGH) {
316
+ _memoryPressureLevel = MEMORY_PRESSURE_LEVELS.HIGH;
317
+ } else if (ratio >= MEMORY_THRESHOLD_ELEVATED) {
318
+ _memoryPressureLevel = MEMORY_PRESSURE_LEVELS.ELEVATED;
319
+ } else {
320
+ _memoryPressureLevel = MEMORY_PRESSURE_LEVELS.NONE;
321
+ }
322
+ return { level: _memoryPressureLevel, mem, ratio };
323
+ }
324
+
325
+ /**
326
+ * Get the current memory pressure level.
327
+ * Used by ingestion.js to decide whether to skip polling.
328
+ */
329
+ function getMemoryPressureLevel() {
330
+ return _memoryPressureLevel;
331
+ }
332
+
249
333
  /**
250
334
  * Prune in-memory caches to prevent unbounded growth between daily resets.
251
335
  * Called hourly from the main loop. Targets:
@@ -284,6 +368,76 @@ function pruneMemoryCaches(recentlyScanned, downloadsCache, alertedPackageRules)
284
368
  }
285
369
  }
286
370
 
371
+ /**
372
+ * Graduated memory pressure response. Called from the main loop when
373
+ * computeMemoryPressure() detects a level >= HIGH.
374
+ *
375
+ * The key principle: clearing caches alone is futile when the real memory is held
376
+ * by N concurrent scan workers retaining AST trees, scan results, and extracted
377
+ * file references. The only effective response is to STOP creating new work and
378
+ * let running scans finish/timeout and release their memory.
379
+ *
380
+ * Level actions (cumulative — higher levels include lower-level actions):
381
+ * HIGH (85%): clear auxiliary caches (recentlyScanned, downloadsCache, etc.)
382
+ * CRITICAL (90%): clear scanner caches (temporal metadata), force GC, log loudly
383
+ * EMERGENCY (95%): truncate queue to EMERGENCY_QUEUE_KEEP, clear deferred queue
384
+ *
385
+ * Worker spawning is gated separately in the main loop (ensureWorkers skipped at HIGH+).
386
+ * Ingestion is gated in ingestion.js via getMemoryPressureLevel() (skipped at CRITICAL+).
387
+ */
388
+ function handleMemoryPressure(level, ratio, recentlyScanned, downloadsCache, scanQueue) {
389
+ const pct = (ratio * 100).toFixed(0);
390
+
391
+ // HIGH (85%+): clear auxiliary caches — same as old emergency prune
392
+ if (level >= MEMORY_PRESSURE_LEVELS.HIGH) {
393
+ console.error(`[MONITOR] MEMORY PRESSURE HIGH: heap at ${pct}% — pruning caches, stopping new workers`);
394
+ recentlyScanned.clear();
395
+ downloadsCache.clear();
396
+ alertedPackageRules.clear();
397
+ }
398
+
399
+ // CRITICAL (90%+): clear scanner caches, force GC
400
+ if (level >= MEMORY_PRESSURE_LEVELS.CRITICAL) {
401
+ console.error(`[MONITOR] MEMORY PRESSURE CRITICAL: heap at ${pct}% — stopping ingestion, clearing scanner caches`);
402
+ // temporal-analysis._metadataCache (200 entries × full npm registry metadata)
403
+ try { clearMetadataCache(); } catch {}
404
+ // pendingGrouped webhook buffers
405
+ for (const [scope, group] of pendingGrouped) {
406
+ clearTimeout(group.timer);
407
+ }
408
+ pendingGrouped.clear();
409
+ // Force GC if available (requires --expose-gc)
410
+ if (global.gc) {
411
+ global.gc();
412
+ console.log('[MONITOR] Forced garbage collection');
413
+ }
414
+ }
415
+
416
+ // EMERGENCY (95%+): queue truncation + deferred queue clear
417
+ if (level >= MEMORY_PRESSURE_LEVELS.EMERGENCY) {
418
+ const queueBefore = scanQueue.length;
419
+ if (queueBefore > EMERGENCY_QUEUE_KEEP) {
420
+ // Keep the LAST N items (most recently added = newest packages).
421
+ // These are the packages most likely to still exist on npm for re-scan later.
422
+ // Dropped items are public packages — they'll appear again on republish or
423
+ // can be re-fetched from the registry if needed.
424
+ const dropped = queueBefore - EMERGENCY_QUEUE_KEEP;
425
+ // splice from the front: older items were pushed first
426
+ scanQueue.splice(0, dropped);
427
+ console.error(`[MONITOR] MEMORY EMERGENCY: heap at ${pct}% — truncated queue ${queueBefore} → ${scanQueue.length} (dropped ${dropped} oldest items)`);
428
+ }
429
+ // Clear deferred sandbox queue (holds full staticResult objects)
430
+ const deferredDropped = clearDeferredQueue();
431
+ if (deferredDropped > 0) {
432
+ console.error(`[MONITOR] MEMORY EMERGENCY: cleared ${deferredDropped} deferred sandbox items`);
433
+ }
434
+ // Second GC pass after freeing queue + deferred references
435
+ if (global.gc) {
436
+ global.gc();
437
+ }
438
+ }
439
+ }
440
+
287
441
  function reportStats(stats) {
288
442
  const avg = stats.scanned > 0 ? (stats.totalTimeMs / stats.scanned / 1000).toFixed(1) : '0.0';
289
443
  const { t1, t1a, t1b, t2, t3 } = stats.suspectByTier;
@@ -339,6 +493,15 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
339
493
  ╚════════════════════════════════════════════╝
340
494
  `);
341
495
 
496
+ // Note: alerts file migrated from .json to .jsonl in v2.10.89
497
+ const oldAlertsJson = ALERTS_FILE.replace('.jsonl', '.json');
498
+ if (fs.existsSync(oldAlertsJson)) {
499
+ try {
500
+ const sizeMB = (fs.statSync(oldAlertsJson).size / 1024 / 1024).toFixed(0);
501
+ console.log(`[MONITOR] Legacy ${path.basename(oldAlertsJson)} found (${sizeMB}MB). Safe to archive — alerts now use JSONL.`);
502
+ } catch {}
503
+ }
504
+
342
505
  // Check sandbox availability
343
506
  if (isSandboxEnabled()) {
344
507
  sandboxAvailableRef.value = isDockerAvailable();
@@ -410,6 +573,8 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
410
573
  console.log('[MONITOR] npm changes stream enabled (replicate.npmjs.com) with RSS fallback');
411
574
  console.log(`[MONITOR] Scan concurrency: adaptive ${BASE_CONCURRENCY}→${getTargetConcurrency()} (base MUADDIB_SCAN_CONCURRENCY=${BASE_CONCURRENCY}, max MUADDIB_MAX_CONCURRENCY)`);
412
575
  console.log(`[MONITOR] Sandbox concurrency: ${SANDBOX_CONCURRENCY_MAX} (MUADDIB_SANDBOX_CONCURRENCY to override)`);
576
+ const heapLimitMB = (v8.getHeapStatistics().heap_size_limit / 1024 / 1024).toFixed(0);
577
+ console.log(`[MONITOR] Memory circuit breaker: heap limit ${heapLimitMB}MB, thresholds HIGH=${(MEMORY_THRESHOLD_HIGH * 100).toFixed(0)}% CRITICAL=${(MEMORY_THRESHOLD_CRITICAL * 100).toFixed(0)}% EMERGENCY=${(MEMORY_THRESHOLD_EMERGENCY * 100).toFixed(0)}%, GC=${typeof global.gc === 'function' ? 'available' : 'unavailable (start with --expose-gc)'}`);
413
578
  console.log(`[MONITOR] Polling every ${POLL_INTERVAL / 1000}s (decoupled from processing). Ctrl+C to stop.\n`);
414
579
 
415
580
  let running = true;
@@ -515,8 +680,8 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
515
680
  // ─── Decoupled polling ───
516
681
  // Poll runs on its own interval, independent of processing.
517
682
  // This ensures new packages are ingested even while a large batch is being scanned.
518
- // Backpressure removed: polling ALWAYS runs. Queue grows unbounded in memory
519
- // (entries ~300B, 100K = 30MB). Adaptive concurrency adjusts scan throughput.
683
+ // Backpressure: poll() skips when queue >= 30K or memory pressure >= CRITICAL (90%).
684
+ // Adaptive concurrency adjusts scan throughput to match ingestion rate.
520
685
  let pollInProgress = false;
521
686
  pollIntervalHandle = setInterval(async () => {
522
687
  if (!running || pollInProgress) return;
@@ -549,33 +714,40 @@ async function startMonitor(options, stats, dailyAlerts, recentlyScanned, downlo
549
714
  // Non-blocking: ensureWorkers spawns fire-and-forget background workers.
550
715
  // This loop tops up workers every 2s AND runs housekeeping (memory, daily report)
551
716
  // without being blocked by long-running scans.
552
- const MEMORY_LOG_INTERVAL = 300_000; // 5 minutes
553
- const MEMORY_PRESSURE_THRESHOLD = 0.85; // 85% heap usage triggers emergency prune
554
717
  let lastMemoryLogTime = Date.now();
555
718
 
556
719
  while (running) {
557
- // Top up workers (non-blocking spawns missing workers as background promises)
558
- ensureWorkers(scanQueue, stats, dailyAlerts, recentlyScanned, downloadsCache, sandboxAvailableRef.value);
559
-
560
- // ─── Memory watchdog (every 5 min) ───
561
- if (Date.now() - lastMemoryLogTime >= MEMORY_LOG_INTERVAL) {
562
- const mem = process.memoryUsage();
563
- const heapUsedMB = (mem.heapUsed / 1024 / 1024).toFixed(0);
564
- const heapTotalMB = (mem.heapTotal / 1024 / 1024).toFixed(0);
565
- const rssMB = (mem.rss / 1024 / 1024).toFixed(0);
566
- console.log(`[MONITOR] MEMORY: heap=${heapUsedMB}MB/${heapTotalMB}MB, rss=${rssMB}MB, queue=${scanQueue.length}, dedup=${recentlyScanned.size}, downloads=${downloadsCache.size}, alerts=${alertedPackageRules.size}`);
567
-
568
- // Emergency prune under memory pressure
569
- if (mem.heapUsed / mem.heapTotal > MEMORY_PRESSURE_THRESHOLD) {
570
- console.error(`[MONITOR] MEMORY PRESSURE: heap at ${((mem.heapUsed / mem.heapTotal) * 100).toFixed(0)}% — emergency prune`);
571
- recentlyScanned.clear();
572
- downloadsCache.clear();
573
- alertedPackageRules.clear();
574
- // Force GC if available (requires --expose-gc)
575
- if (global.gc) {
576
- global.gc();
577
- console.log('[MONITOR] Forced garbage collection');
578
- }
720
+ // ─── Memory circuit breaker (every iteration) ───
721
+ // computeMemoryPressure() is cheap (~0.1ms). Running every 2s ensures fast
722
+ // reaction to memory spikes — the 2026-04-13 incident showed that checking
723
+ // every 5min is too slow (250 packages ingested between checks).
724
+ const { level: pressureLevel, mem: currentMem, ratio: heapRatio } = computeMemoryPressure();
725
+
726
+ // Top up workers ONLY when memory pressure is below HIGH.
727
+ // At HIGH+, existing workers continue (they'll finish or timeout) but no new
728
+ // ones are spawned. This is the core mechanism: let running scans release their
729
+ // memory (AST trees, scan results, extracted files) before starting new ones.
730
+ if (pressureLevel < MEMORY_PRESSURE_LEVELS.HIGH) {
731
+ ensureWorkers(scanQueue, stats, dailyAlerts, recentlyScanned, downloadsCache, sandboxAvailableRef.value);
732
+ }
733
+
734
+ // ─── Memory watchdog (adaptive interval) ───
735
+ // Log every 5min normally, every 15s under pressure.
736
+ const memLogInterval = pressureLevel >= MEMORY_PRESSURE_LEVELS.HIGH
737
+ ? MEMORY_LOG_INTERVAL_PRESSURE
738
+ : MEMORY_LOG_INTERVAL_NORMAL;
739
+
740
+ if (Date.now() - lastMemoryLogTime >= memLogInterval) {
741
+ const heapUsedMB = (currentMem.heapUsed / 1024 / 1024).toFixed(0);
742
+ const heapLimitMB = (v8.getHeapStatistics().heap_size_limit / 1024 / 1024).toFixed(0);
743
+ const rssMB = (currentMem.rss / 1024 / 1024).toFixed(0);
744
+ const pctUsed = (heapRatio * 100).toFixed(0);
745
+ const levelName = Object.keys(MEMORY_PRESSURE_LEVELS).find(k => MEMORY_PRESSURE_LEVELS[k] === pressureLevel) || 'UNKNOWN';
746
+ console.log(`[MONITOR] MEMORY: heap=${heapUsedMB}MB/${heapLimitMB}MB (${pctUsed}%), rss=${rssMB}MB, queue=${scanQueue.length}, dedup=${recentlyScanned.size}, downloads=${downloadsCache.size}, alerts=${alertedPackageRules.size}, pressure=${levelName}`);
747
+
748
+ // Graduated response at HIGH+
749
+ if (pressureLevel >= MEMORY_PRESSURE_LEVELS.HIGH) {
750
+ handleMemoryPressure(pressureLevel, heapRatio, recentlyScanned, downloadsCache, scanQueue);
579
751
  }
580
752
  lastMemoryLogTime = Date.now();
581
753
  }
@@ -632,5 +804,17 @@ module.exports = {
632
804
  MAX_RESTORE_QUEUE_SIZE,
633
805
  pruneMemoryCaches,
634
806
  MAX_RECENTLY_SCANNED,
635
- MAX_ALERTED_PACKAGES
807
+ MAX_ALERTED_PACKAGES,
808
+ // Memory circuit breaker
809
+ computeMemoryPressure,
810
+ getMemoryPressureLevel,
811
+ handleMemoryPressure,
812
+ MEMORY_PRESSURE_LEVELS,
813
+ MEMORY_THRESHOLD_ELEVATED,
814
+ MEMORY_THRESHOLD_HIGH,
815
+ MEMORY_THRESHOLD_CRITICAL,
816
+ MEMORY_THRESHOLD_EMERGENCY,
817
+ EMERGENCY_QUEUE_KEEP,
818
+ MEMORY_LOG_INTERVAL_NORMAL,
819
+ MEMORY_LOG_INTERVAL_PRESSURE
636
820
  };
@@ -419,6 +419,18 @@ function isDeferredSlotBusy() {
419
419
  return _deferredSlotBusy;
420
420
  }
421
421
 
422
+ /**
423
+ * Emergency clear: drop all deferred items and free their staticResult references.
424
+ * Called by daemon.js memory circuit breaker at EMERGENCY level.
425
+ * Returns the count of items dropped for logging.
426
+ */
427
+ function clearDeferredQueue() {
428
+ const count = _deferredQueue.length;
429
+ _deferredQueue.length = 0;
430
+ _deferredSeen.clear();
431
+ return count;
432
+ }
433
+
422
434
  module.exports = {
423
435
  enqueueDeferred,
424
436
  getDeferredQueue,
@@ -431,6 +443,7 @@ module.exports = {
431
443
  buildDeferredFollowUpEmbed,
432
444
  pruneExpired,
433
445
  isDeferredSlotBusy,
446
+ clearDeferredQueue,
434
447
  _resetDeferredQueue,
435
448
  DEFERRED_QUEUE_MAX,
436
449
  DEFERRED_TTL_MS,
@@ -650,7 +650,21 @@ async function pollPyPI(state, scanQueue) {
650
650
  const SOFT_BACKPRESSURE_THRESHOLD = 30_000;
651
651
 
652
652
  async function poll(state, scanQueue, stats) {
653
- // Soft backpressure: skip poll when queue is very deep.
653
+ // Memory-based backpressure: skip poll when heap is at CRITICAL+ (90%+).
654
+ // This is the primary defense against the 2026-04-13 death spiral where
655
+ // ingestion continued at 50 pkg/min while processing was at 0 throughput.
656
+ // Safe because: CouchDB seq is NOT advanced — next poll resumes from same point.
657
+ try {
658
+ const { getMemoryPressureLevel } = require('./daemon.js');
659
+ const pressureLevel = getMemoryPressureLevel();
660
+ // CRITICAL=3, EMERGENCY=4
661
+ if (pressureLevel >= 3) {
662
+ console.log(`[MONITOR] MEMORY BACKPRESSURE: skipping poll (pressure level ${pressureLevel} >= CRITICAL) — seq not advanced, 0 packages lost`);
663
+ return;
664
+ }
665
+ } catch { /* daemon.js not loaded yet (initial poll) — proceed normally */ }
666
+
667
+ // Queue-depth backpressure: skip poll when queue is very deep.
654
668
  // Safe because: CouchDB seq is NOT advanced (stays in memory only, persisted
655
669
  // by daemon.js AFTER poll returns) — next poll resumes from the same point.
656
670
  // Combined with adaptive concurrency: workers scale up → queue drains → poll resumes.
@@ -10,13 +10,17 @@ const { sanitizePackageName } = require('../shared/download.js');
10
10
  // --- File path constants ---
11
11
 
12
12
  const STATE_FILE = path.join(__dirname, '..', '..', 'data', 'monitor-state.json');
13
- const ALERTS_FILE = path.join(__dirname, '..', '..', 'data', 'monitor-alerts.json');
13
+ const ALERTS_FILE = path.join(__dirname, '..', '..', 'data', 'monitor-alerts.jsonl');
14
14
  const DETECTIONS_FILE = path.join(__dirname, '..', '..', 'data', 'detections.json');
15
15
  const SCAN_STATS_FILE = path.join(__dirname, '..', '..', 'data', 'scan-stats.json');
16
16
  const LAST_DAILY_REPORT_FILE = path.join(__dirname, '..', '..', 'data', 'last-daily-report.json');
17
17
  const DAILY_STATS_FILE = path.join(__dirname, '..', '..', 'data', 'daily-stats.json');
18
18
  const TEMPORAL_DETECTIONS_FILE = path.join(__dirname, '..', '..', 'data', 'temporal-detections.json');
19
19
 
20
+ // --- Alerts/detections persistence limits ---
21
+ const ALERTS_MAX_SIZE = 100 * 1024 * 1024; // 100MB rotation threshold (matches ml-training.jsonl)
22
+ const MAX_DETECTIONS = 10_000; // Cap detections array — oldest entries discarded
23
+
20
24
  // Local log persistence directories (parallel to Discord webhooks for offline analysis)
21
25
  // Primary: logs/ relative to project root. Fallback: /tmp/ if primary is read-only (EROFS/EACCES).
22
26
  const PRIMARY_DAILY_REPORTS_DIR = path.join(__dirname, '..', '..', 'logs', 'daily-reports');
@@ -436,6 +440,27 @@ function purgeTarballCache() {
436
440
 
437
441
  // --- Temporal detections ---
438
442
 
443
+ /**
444
+ * Trim temporal findings to essential fields only.
445
+ * Production findings arrive as { type, data: { suspicious, message, score, findings: [...], ... } }
446
+ * with the data object containing full AST diffs, metadata snapshots, etc (~80KB each).
447
+ * This retains only type, severity, suspicious, message, and score for persistence.
448
+ */
449
+ function trimTemporalFindings(findings) {
450
+ return findings.map(f => {
451
+ const trimmed = { type: f.type };
452
+ if (f.severity) trimmed.severity = f.severity;
453
+ if (f.message) trimmed.message = f.message;
454
+ if (f.data) {
455
+ if (f.data.suspicious !== undefined) trimmed.suspicious = f.data.suspicious;
456
+ if (f.data.message) trimmed.message = trimmed.message || f.data.message;
457
+ if (f.data.score !== undefined) trimmed.score = f.data.score;
458
+ if (f.data.severity) trimmed.severity = trimmed.severity || f.data.severity;
459
+ }
460
+ return trimmed;
461
+ });
462
+ }
463
+
439
464
  /**
440
465
  * Append a temporal detection to the temporal detections file.
441
466
  * @param {string} name - Package name
@@ -452,7 +477,7 @@ function appendTemporalDetection(name, version, findings) {
452
477
  detections.push({
453
478
  name,
454
479
  version,
455
- findings,
480
+ findings: trimTemporalFindings(findings),
456
481
  timestamp: new Date().toISOString()
457
482
  });
458
483
  // Keep last 1000 entries
@@ -520,7 +545,21 @@ function saveState(state, stats) {
520
545
  }
521
546
  }
522
547
 
523
- // --- Alerts persistence ---
548
+ // --- Alerts persistence (JSONL append-only) ---
549
+
550
+ function maybeRotateAlerts() {
551
+ try {
552
+ if (!fs.existsSync(ALERTS_FILE)) return;
553
+ const stat = fs.statSync(ALERTS_FILE);
554
+ if (stat.size < ALERTS_MAX_SIZE) return;
555
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
556
+ const rotatedName = ALERTS_FILE.replace('.jsonl', `-${timestamp}.jsonl`);
557
+ fs.renameSync(ALERTS_FILE, rotatedName);
558
+ console.log(`[MONITOR] Rotated alerts -> ${path.basename(rotatedName)} (${(stat.size / 1024 / 1024).toFixed(1)}MB)`);
559
+ } catch (err) {
560
+ console.error(`[MONITOR] Alerts rotation failed: ${err.message}`);
561
+ }
562
+ }
524
563
 
525
564
  function appendAlert(alert) {
526
565
  try {
@@ -528,13 +567,14 @@ function appendAlert(alert) {
528
567
  if (!fs.existsSync(dir)) {
529
568
  fs.mkdirSync(dir, { recursive: true });
530
569
  }
531
- let alerts = [];
532
- try {
533
- alerts = JSON.parse(fs.readFileSync(ALERTS_FILE, 'utf8'));
534
- } catch {}
535
- alerts.push(alert);
536
- atomicWriteFileSync(ALERTS_FILE, JSON.stringify(alerts, null, 2));
570
+ maybeRotateAlerts();
571
+ const line = JSON.stringify(alert) + '\n';
572
+ fs.appendFileSync(ALERTS_FILE, line, 'utf8');
537
573
  } catch (err) {
574
+ if (err.code === 'EROFS' || err.code === 'EACCES' || err.code === 'EPERM') {
575
+ console.warn(`[MONITOR] Permission denied writing alerts: ${err.code}`);
576
+ return;
577
+ }
538
578
  console.error(`[MONITOR] Failed to save alert: ${err.message}`);
539
579
  }
540
580
  }
@@ -573,6 +613,10 @@ function appendDetection(name, version, ecosystem, findings, severity) {
573
613
  advisory_at: null,
574
614
  lead_time_hours: null
575
615
  });
616
+ // Cap at MAX_DETECTIONS — discard oldest entries
617
+ if (data.detections.length > MAX_DETECTIONS) {
618
+ data.detections = data.detections.slice(-MAX_DETECTIONS);
619
+ }
576
620
  atomicWriteFileSync(DETECTIONS_FILE, JSON.stringify(data, null, 2));
577
621
  } catch (err) {
578
622
  console.error(`[MONITOR] Failed to save detection: ${err.message}`);
@@ -846,6 +890,8 @@ module.exports = {
846
890
  TARBALL_CACHE_HIGH_RISK_RETENTION_DAYS,
847
891
  TARBALL_CACHE_MAX_SIZE_BYTES,
848
892
  DAILY_STATS_PERSIST_INTERVAL,
893
+ ALERTS_MAX_SIZE,
894
+ MAX_DETECTIONS,
849
895
 
850
896
  // Mutable state getters/setters
851
897
  getScanMemoryCache,
@@ -844,6 +844,43 @@ const PLAYBOOKS = {
844
844
  trusted_new_dependency:
845
845
  'HAUTE: Package populaire (TRUSTED) a ajoute une nouvelle dependance connue. ' +
846
846
  'Verifier le changelog et la legitimite de l\'ajout. Pas de blocage immediat mais surveillance renforcee.',
847
+
848
+ // v2.10.89: Security review findings (apr-2026)
849
+ curl_env_exfil:
850
+ 'CRITIQUE: curl/wget exfiltre les variables d\'environnement ou des donnees systeme via lifecycle script. ' +
851
+ 'Machine potentiellement compromise si deja installe. Rotation immediate de TOUS les secrets ' +
852
+ '(npm tokens, AWS keys, GitHub tokens, etc.). Verifier les connexions sortantes recentes.',
853
+
854
+ function_constructor_require:
855
+ 'CRITIQUE: new Function.constructor("require", code) — execution de code dynamique avec acces au require reel. ' +
856
+ 'Le code telecharge probablement un payload depuis un C2 (jsonkeeper.com, npoint.io, etc.) et l\'execute. ' +
857
+ 'Isoler la machine. Supprimer le package. Regenerer TOUS les secrets.',
858
+
859
+ process_variable_shadow:
860
+ 'ELEVE: Le global process est shadow par une variable locale pour cacher des URLs C2 dans process.env. ' +
861
+ 'Technique d\'evasion de la campagne "Robert King" (npoint.io/jsonkeeper.com). ' +
862
+ 'Verifier les URLs dans les valeurs de la fausse variable process. Supprimer le package.',
863
+
864
+ newsletter_auto_follow:
865
+ 'ELEVE: Fork Baileys malveillant qui force l\'abonnement a des channels WhatsApp via la session authentifiee. ' +
866
+ 'Les JIDs des channels sont hardcodes ou telecharges depuis un C2 (cdn.malvintech.sbs). ' +
867
+ 'Desinstaller immediatement. Verifier les channels WhatsApp auxquels la session est abonnee. ' +
868
+ 'Revoquer la session WhatsApp Web si compromise.',
869
+
870
+ version_99_preinstall:
871
+ 'ELEVE: Version >= 99.x.x avec hook lifecycle — indicateur de dependency confusion. ' +
872
+ 'La version elevee force npm a resoudre vers le package public au lieu du package interne prive. ' +
873
+ 'NE PAS installer. Configurer un registry scope pour les packages internes. ' +
874
+ 'Signaler le package sur npm comme dependency confusion.',
875
+
876
+ lifecycle_newsletter_hijack:
877
+ 'CRITIQUE: Lifecycle hook + newsletter auto-follow WhatsApp — channel hijack a l\'installation. ' +
878
+ 'Desinstaller immediatement. Revoquer la session WhatsApp Web. ' +
879
+ 'Verifier les channels auxquels la session est abonnee.',
880
+
881
+ lifecycle_env_exfil:
882
+ 'CRITIQUE: Lifecycle hook + exfiltration env via curl/wget a l\'installation. ' +
883
+ 'Machine compromise si deja installe. Rotation immediate de TOUS les secrets.',
847
884
  };
848
885
 
849
886
  function getPlaybook(threatType) {
@@ -2218,6 +2218,64 @@ const RULES = {
2218
2218
  ],
2219
2219
  mitre: 'T1195.002'
2220
2220
  },
2221
+ // v2.10.89: Security review findings (apr-2026) — 5 new rules from 14K tarball review
2222
+ curl_env_exfil: {
2223
+ id: 'MUADDIB-PKG-018',
2224
+ name: 'Curl/Wget Environment Exfiltration',
2225
+ severity: 'CRITICAL',
2226
+ confidence: 'high',
2227
+ description: 'curl/wget combine avec base64 ou env dans un lifecycle script — exfiltration de credentials a l\'installation. Pattern: curl -d $(env|base64) URL dans preinstall/postinstall.',
2228
+ references: [
2229
+ 'https://attack.mitre.org/techniques/T1041/',
2230
+ 'https://blog.phylum.io/npm-dependency-confusion-attacks'
2231
+ ],
2232
+ mitre: 'T1041'
2233
+ },
2234
+ function_constructor_require: {
2235
+ id: 'MUADDIB-AST-086',
2236
+ name: 'Function Constructor Require Evasion',
2237
+ severity: 'CRITICAL',
2238
+ confidence: 'high',
2239
+ description: 'new Function.constructor("require", code) — execution de code dynamique via le constructeur Function avec acces au require reel. Technique d\'evasion: contourne la detection de eval/require en passant par le prototype de Function.',
2240
+ references: [
2241
+ 'https://attack.mitre.org/techniques/T1059/007/'
2242
+ ],
2243
+ mitre: 'T1059.007'
2244
+ },
2245
+ process_variable_shadow: {
2246
+ id: 'MUADDIB-AST-087',
2247
+ name: 'Process Variable Shadowing',
2248
+ severity: 'HIGH',
2249
+ confidence: 'high',
2250
+ description: 'Le global process est shadow par une variable locale (const process = {...}). Technique d\'evasion: cache les URLs C2 dans un faux process.env pour contourner la detection de domaines suspects. Campagne "Robert King" (npoint.io/jsonkeeper.com).',
2251
+ references: [
2252
+ 'https://attack.mitre.org/techniques/T1036/'
2253
+ ],
2254
+ mitre: 'T1036'
2255
+ },
2256
+ newsletter_auto_follow: {
2257
+ id: 'MUADDIB-AST-088',
2258
+ name: 'Baileys Newsletter Auto-Follow Hijack',
2259
+ severity: 'HIGH',
2260
+ confidence: 'high',
2261
+ description: 'Pattern de detournement WhatsApp Baileys: newsletter + FOLLOW/QueryIds ou AUTO_FOLLOW_CHANNELS dans le meme fichier. Force l\'abonnement a des channels WhatsApp via la session authentifiee de la victime sans consentement.',
2262
+ references: [
2263
+ 'https://attack.mitre.org/techniques/T1496/'
2264
+ ],
2265
+ mitre: 'T1496'
2266
+ },
2267
+ version_99_preinstall: {
2268
+ id: 'MUADDIB-PKG-019',
2269
+ name: 'Dependency Confusion Version Indicator',
2270
+ severity: 'HIGH',
2271
+ confidence: 'high',
2272
+ description: 'Version >= 99.x.x avec hook lifecycle (preinstall/postinstall). Indicateur fort de dependency confusion: la version elevee force la resolution npm vers le package public malveillant au lieu du package interne prive.',
2273
+ references: [
2274
+ 'https://medium.com/@alex.birsan/dependency-confusion-4a5d60fec610',
2275
+ 'https://attack.mitre.org/techniques/T1195.002/'
2276
+ ],
2277
+ mitre: 'T1195.002'
2278
+ },
2221
2279
  // Trusted dependency diff detections (monitor-only)
2222
2280
  trusted_new_unknown_dependency: {
2223
2281
  id: 'MUADDIB-TRUSTED-001',
@@ -158,7 +158,16 @@ const SUSPICIOUS_DOMAINS_HIGH = [
158
158
  'scan.aquasecurtiy.org', // Trivy exfil C2 (typosquat of aquasecurity)
159
159
  'api.telegram.org', // Telegram bot exfiltration (crypto typosquats)
160
160
  'checkmarx.zone', // Checkmarx/LiteLLM exfil C2
161
- '45.148.10.212', '83.142.209.11' // TeamPCP C2 IPs
161
+ '45.148.10.212', '83.142.209.11', // TeamPCP C2 IPs
162
+ // Security review apr-2026 findings
163
+ 'cdn.malvintech.sbs', // Baileys V3 C2 (newsletter JID dynamic loading)
164
+ '173.211.46.220', // RCE trojan campaign (react-emits, buffer-util-extend)
165
+ '144.31.107.231', // Strapi/Guardarian targeted attack C2
166
+ 'cchubber-telemetry.asmirkhan087.workers.dev', // Claude Code credential stealer
167
+ 'minhdong.site', // Facebook credential proxy (fca-mmtat)
168
+ 'ltidi.storage.googleapis.com', // KuCoin dependency confusion payload
169
+ 'jsonkeeper.com', // Robert King campaign C2 dead drop
170
+ 'npoint.io' // Robert King campaign C2 dead drop
162
171
  ];
163
172
 
164
173
  // Suspicious tunnel/proxy domains (MEDIUM severity)
@@ -23,6 +23,27 @@ function handleNewExpression(node, ctx) {
23
23
  }
24
24
  }
25
25
 
26
+ // v2.10.89: new Function.constructor("require", code) — RCE evasion
27
+ // Catches: chai-as-inserted (score 10→CRITICAL), cookie-parseflow (score 10→CRITICAL)
28
+ // Pattern: new Function.constructor("require", s); handler(require) — passes real require to dynamic code
29
+ if (node.callee.type === 'MemberExpression' &&
30
+ node.callee.property?.type === 'Identifier' && node.callee.property.name === 'constructor' &&
31
+ node.callee.object?.type === 'Identifier' && node.callee.object.name === 'Function' &&
32
+ node.arguments.length >= 1) {
33
+ const hasRequireArg = node.arguments.some(arg =>
34
+ arg.type === 'Literal' && typeof arg.value === 'string' && arg.value === 'require'
35
+ );
36
+ if (hasRequireArg) {
37
+ ctx.hasDynamicExec = true;
38
+ ctx.threats.push({
39
+ type: 'function_constructor_require',
40
+ severity: 'CRITICAL',
41
+ message: 'new Function.constructor("require", ...) — dynamic code execution bypassing require() detection. Attacker passes real require to execute arbitrary code.',
42
+ file: ctx.relFile
43
+ });
44
+ }
45
+ }
46
+
26
47
  // Batch 1: new vm.Script(code) — dynamic code compilation via vm module
27
48
  if (node.callee.type === 'MemberExpression' &&
28
49
  node.callee.property?.type === 'Identifier' && node.callee.property.name === 'Script' &&
@@ -488,6 +488,28 @@ function handlePostWalk(ctx) {
488
488
  });
489
489
  }
490
490
  }
491
+
492
+ // v2.10.89: Baileys newsletter auto-follow hijack
493
+ // Catches: Baileys V4 (budetzz score 35→HIGH, archeron-dev score 35→HIGH), all Baileys variants
494
+ // Pattern: newsletter + (FOLLOW|QueryIds|AUTO_FOLLOW_CHANNELS) in same file
495
+ // Uses source code regex since these are string patterns, not AST node types
496
+ const src = ctx._sourceCode || '';
497
+ if (/\bnewsletter\b/i.test(src) &&
498
+ (/\bFOLLOW\b/.test(src) || /\bQueryIds\b/.test(src) || /\bAUTO_FOLLOW_CHANNELS\b/.test(src))) {
499
+ // Only fire if the file also has WhatsApp/Baileys context to avoid FP on email newsletter code
500
+ const hasBaileysContext = /\b(baileys|whatsapp|WAProto|WA\.|SignalProtocol|libsignal|newsletterWMex)\b/i.test(src);
501
+ if (hasBaileysContext) {
502
+ const hasDelay = ctx.hasTimerDelayedPayload || /\bsetTimeout\b/.test(src) || /\bsetInterval\b/.test(src);
503
+ ctx.threats.push({
504
+ type: 'newsletter_auto_follow',
505
+ severity: hasDelay ? 'CRITICAL' : 'HIGH',
506
+ message: hasDelay
507
+ ? 'Baileys newsletter auto-follow with timer delay — forces WhatsApp channel subscription via authenticated session. Timer evasion pattern.'
508
+ : 'Baileys newsletter auto-follow pattern — forces WhatsApp channel subscription via authenticated session without user consent.',
509
+ file: ctx.relFile
510
+ });
511
+ }
512
+ }
491
513
  }
492
514
 
493
515
 
@@ -78,6 +78,18 @@ function handleVariableDeclarator(node, ctx) {
78
78
  ctx.varSource.set(node.id.name, source);
79
79
  }
80
80
 
81
+ // v2.10.89: Detect process variable shadowing — evasion technique
82
+ // Catches: Robert King campaign (process shadow to hide C2 URLs in fake process.env)
83
+ // Pattern: const process = { env: { DEV_API_KEY: "https://evil.com/..." } }
84
+ if (node.id.name === 'process' && node.init?.type === 'ObjectExpression') {
85
+ ctx.threats.push({
86
+ type: 'process_variable_shadow',
87
+ severity: 'HIGH',
88
+ message: 'Global "process" shadowed with local ObjectExpression — evasion technique to hide C2 URLs in fake process.env.',
89
+ file: ctx.relFile
90
+ });
91
+ }
92
+
81
93
  // Track dynamic require vars + module aliases
82
94
  if (node.init?.type === 'CallExpression') {
83
95
  const initCallName = getCallName(node.init);
@@ -116,6 +116,22 @@ async function scanPackageJson(targetPath) {
116
116
  });
117
117
  }
118
118
 
119
+ // v2.10.89: curl/wget + env/base64 exfiltration in lifecycle scripts
120
+ // Catches: apache-arrow-14 (score 9→CRITICAL), @signals-notebook (score 9→CRITICAL)
121
+ // Pattern: curl -d $(env|base64) URL, curl -X POST URL?env=$(env|base64 -w0)
122
+ if (['preinstall', 'install', 'postinstall'].includes(scriptName) &&
123
+ /\b(curl|wget)\b/.test(scriptContent) &&
124
+ (/\$\(.*\b(env|id|whoami|uname|hostname)\b/.test(scriptContent) ||
125
+ (/\bbase64\b/.test(scriptContent) && !/\|\s*(sh|bash)\b/.test(scriptContent)))) {
126
+ // Exclude curl|sh which is already caught by lifecycle_shell_pipe
127
+ threats.push({
128
+ type: 'curl_env_exfil',
129
+ severity: 'CRITICAL',
130
+ message: `Critical: "${scriptName}" uses curl/wget with env/base64 exfiltration — credential theft via lifecycle script.`,
131
+ file: 'package.json'
132
+ });
133
+ }
134
+
119
135
  // Detect Bun runtime evasion in lifecycle scripts (Shai-Hulud 2.0)
120
136
  if (/\bbun\s+(run|exec|install|x)\b/.test(scriptContent) || /\bbunx\s+/.test(scriptContent)) {
121
137
  threats.push({
@@ -147,6 +163,22 @@ async function scanPackageJson(targetPath) {
147
163
  }
148
164
  }
149
165
 
166
+ // v2.10.89: Dependency confusion indicator — version >= 99 with install hooks
167
+ // Catches: @corpweb-ui/wmkt-library, @toprank/partner, @adac-fahrzeugplattform/ui
168
+ const versionStr = pkg.version || '';
169
+ const majorVersion = parseInt(versionStr.split('.')[0], 10);
170
+ if (majorVersion >= 99) {
171
+ const hasInstallHook = ['preinstall', 'install', 'postinstall'].some(s => scripts[s]);
172
+ if (hasInstallHook) {
173
+ threats.push({
174
+ type: 'version_99_preinstall',
175
+ severity: 'HIGH',
176
+ message: `Version ${versionStr} (major >= 99) with lifecycle hook — dependency confusion attack pattern.`,
177
+ file: 'package.json'
178
+ });
179
+ }
180
+ }
181
+
150
182
  // Check non-lifecycle scripts (test, start, etc.) for network exfil commands
151
183
  const NETWORK_SCRIPT_PATTERN = /\bcurl\b|\bwget\b|\bnc\s+-|\bncat\b|\bpowershell\b|\bnslookup\b/;
152
184
  for (const [scriptName, scriptContent] of Object.entries(scripts)) {
package/src/scoring.js CHANGED
@@ -115,7 +115,10 @@ const PACKAGE_LEVEL_TYPES = new Set([
115
115
  // Blue Team v8: package-level boost signals
116
116
  'isolated_suspicious_file', 'deep_suspicious_file',
117
117
  // Blue Team v8b: phantom lifecycle scripts
118
- 'lifecycle_missing_script'
118
+ 'lifecycle_missing_script',
119
+ // v2.10.89: Security review compounds
120
+ 'lifecycle_newsletter_hijack', 'lifecycle_env_exfil',
121
+ 'curl_env_exfil', 'version_99_preinstall'
119
122
  ]);
120
123
 
121
124
  /**
@@ -248,6 +251,8 @@ const DIST_EXEMPT_TYPES = new Set([
248
251
  // Kept in REACHABILITY_EXEMPT_TYPES (lifecycle invocation is valid).
249
252
  'node_modules_write', // writeFile to node_modules/ (worm propagation)
250
253
  'npm_publish_worm', // exec("npm publish") (worm propagation)
254
+ 'curl_env_exfil', // curl/wget env exfil in lifecycle (always malicious)
255
+ 'function_constructor_require', // new Function.constructor("require") (always malicious)
251
256
  // Dangerous shell commands in dist/ are real threats, never bundler output
252
257
  'dangerous_exec',
253
258
  // Compound scoring rules — co-occurrence signals, never FP
@@ -394,6 +399,23 @@ const SCORING_COMPOUNDS = [
394
399
  // Only obfuscation_detected + env_access must be in the same file (lifecycle_script is package-level)
395
400
  sameFileTypes: ['obfuscation_detected', 'env_access']
396
401
  },
402
+ // v2.10.89: Security review compounds
403
+ {
404
+ type: 'lifecycle_newsletter_hijack',
405
+ requires: ['lifecycle_script', 'newsletter_auto_follow'],
406
+ severity: 'CRITICAL',
407
+ message: 'Lifecycle hook + newsletter auto-follow — WhatsApp Baileys channel hijack via install-time hook (scoring compound).',
408
+ fileFrom: 'newsletter_auto_follow'
409
+ // No sameFile: lifecycle is package-level, newsletter_auto_follow is file-level
410
+ },
411
+ {
412
+ type: 'lifecycle_env_exfil',
413
+ requires: ['lifecycle_script', 'curl_env_exfil'],
414
+ severity: 'CRITICAL',
415
+ message: 'Lifecycle hook + curl/wget env exfiltration — install-time credential theft (scoring compound).',
416
+ fileFrom: 'curl_env_exfil'
417
+ // No sameFile: both are package-level
418
+ },
397
419
  ];
398
420
 
399
421
  /**