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 +1 -1
- package/src/ml/model-trees-backup.js +11 -0
- package/src/monitor/daemon.js +216 -32
- package/src/monitor/deferred-sandbox.js +13 -0
- package/src/monitor/ingestion.js +15 -1
- package/src/monitor/state.js +55 -9
- package/src/response/playbooks.js +37 -0
- package/src/rules/index.js +58 -0
- package/src/scanner/ast-detectors/constants.js +10 -1
- package/src/scanner/ast-detectors/handle-new-expression.js +21 -0
- package/src/scanner/ast-detectors/handle-post-walk.js +22 -0
- package/src/scanner/ast-detectors/handle-variable-declarator.js +12 -0
- package/src/scanner/package.js +32 -0
- package/src/scoring.js +23 -1
package/package.json
CHANGED
|
@@ -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}]]};
|
package/src/monitor/daemon.js
CHANGED
|
@@ -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
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
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
|
|
519
|
-
//
|
|
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
|
-
//
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
//
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
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,
|
package/src/monitor/ingestion.js
CHANGED
|
@@ -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
|
-
//
|
|
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.
|
package/src/monitor/state.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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) {
|
package/src/rules/index.js
CHANGED
|
@@ -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'
|
|
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);
|
package/src/scanner/package.js
CHANGED
|
@@ -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
|
/**
|