mirror of
https://github.com/AikidoSec/safe-chain.git
synced 2026-05-26 20:20:49 +00:00
Compare commits
1119 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b811289bbd | ||
|
|
40f45faea8 | ||
|
|
9453c8c0c9 | ||
|
|
2621f6f974 | ||
|
|
fd01d9f31b | ||
|
|
0414a79982 | ||
|
|
70b5e4d012 | ||
|
|
aed0aebdae | ||
|
|
6aec1bc474 | ||
|
|
f6145d5c20 | ||
|
|
ab058367f1 | ||
|
|
f2cce7b7e9 | ||
|
|
0b46c5408b | ||
|
|
07b8571758 | ||
|
|
3f0837c65a | ||
|
|
47e9ed0f6c | ||
|
|
cbbbe703d3 | ||
|
|
9d44eca1d1 | ||
|
|
b38aba43dd | ||
|
|
93c264ef84 | ||
|
|
34898980d7 | ||
|
|
a5c29d9e49 | ||
|
|
bf2d37d114 | ||
|
|
65a8075b0e | ||
|
|
a1b89a55f8 | ||
|
|
8ab5cebd4f | ||
|
|
ffe7f8de1f | ||
|
|
54db058ac7 | ||
|
|
8453012f7b | ||
|
|
e0e06431d1 | ||
|
|
6cdad3df98 | ||
|
|
d9b7aefd34 | ||
|
|
0c8de1e606 | ||
|
|
fde0003a0a | ||
|
|
c93f1920fb | ||
|
|
d812231b2f | ||
|
|
e5cd9eed91 | ||
|
|
25d966bfa9 | ||
|
|
5f0ad7ecfd | ||
|
|
6667e5d7b4 | ||
|
|
e891d1a992 | ||
|
|
26f1dfb81a | ||
|
|
7ce44b4c62 | ||
|
|
28132ba3fc | ||
|
|
55f2123f5c | ||
|
|
5f56114185 | ||
|
|
08ae1ef732 | ||
|
|
2eb32d4297 | ||
|
|
fbe094802e | ||
|
|
bd876275b3 | ||
|
|
cd5040c3be | ||
|
|
7b39239b81 | ||
|
|
369a94948a | ||
|
|
98a1ba7d10 | ||
|
|
5cf2ffe201 | ||
|
|
cb8db6c7a2 | ||
|
|
f4aa444cd8 | ||
|
|
da419a7785 | ||
|
|
00be33aa10 | ||
|
|
a0f0372e15 | ||
|
|
19d2dee5c9 | ||
|
|
cbf830a637 | ||
|
|
c8e25f3c21 | ||
|
|
fe161ba8a4 | ||
|
|
8571fc6996 | ||
|
|
f3fd003303 | ||
|
|
d0fc643f23 | ||
|
|
bf2bf24343 | ||
|
|
ebebe6d6c1 | ||
|
|
222216e22a | ||
|
|
4ef69d337f | ||
|
|
6abad2d37f | ||
|
|
ae40140199 | ||
|
|
725f7c399d | ||
|
|
dcd926f9d9 | ||
|
|
d04db58a5e | ||
|
|
9b42755502 | ||
|
|
e8fb134136 | ||
|
|
fbb856940f | ||
|
|
0a230eb64c | ||
|
|
dab616163f | ||
|
|
d81b0f5214 | ||
|
|
84346fdea7 | ||
|
|
c68fb2c7ed | ||
|
|
c22f36113c | ||
|
|
abbe0480b6 | ||
|
|
fff1422b51 | ||
|
|
88c969aee0 | ||
|
|
f56edf292b | ||
|
|
fbabd4e3c6 | ||
|
|
8dc5389ac9 | ||
|
|
a840a99f1b | ||
|
|
21b44eb4a8 | ||
|
|
b8d16c15b9 | ||
|
|
9fae225277 | ||
|
|
2930894624 | ||
|
|
3e71398430 | ||
|
|
464847a6fc | ||
|
|
33c3bec43d | ||
|
|
782af8e789 | ||
|
|
b3372cc50e | ||
|
|
7ed943d46f | ||
|
|
a68cf97f89 | ||
|
|
bafa997a70 | ||
|
|
cdb87792df | ||
|
|
6ff2ee3367 | ||
|
|
43fe715b08 | ||
|
|
0a9ab05468 | ||
|
|
8e4f036ce9 | ||
|
|
14c8abffea | ||
|
|
63b7a5ee5e | ||
|
|
f3ae77f12a | ||
|
|
7dd68cea12 | ||
|
|
50623cfc9a | ||
|
|
e54869ddd0 | ||
|
|
1076d6bea8 | ||
|
|
8dbeab8dac | ||
|
|
38a8130f4a | ||
|
|
f7324ccfc0 | ||
|
|
60732c5b6a | ||
|
|
dec9e82ee9 | ||
|
|
56a54b8683 | ||
|
|
32408c6583 | ||
|
|
f2bdd28ae6 | ||
|
|
5bbf3da576 | ||
|
|
f07d0ea888 | ||
|
|
72dc7dcf3a | ||
|
|
031c9683b1 | ||
|
|
d064d46668 | ||
|
|
1cf8fd1241 | ||
|
|
83f9f378f6 | ||
|
|
50f23d27fd | ||
|
|
e3077ebd6f | ||
|
|
9d5503aa54 | ||
|
|
2ea5362b07 | ||
|
|
df8be031cb | ||
|
|
98dcda78da | ||
|
|
ccd595fc22 | ||
|
|
94f77e1330 | ||
|
|
e5c79e5bd6 | ||
|
|
8cf41dc4a6 | ||
|
|
d7400a0bc0 | ||
|
|
eb9d0bba3e | ||
|
|
6628e1d4fd | ||
|
|
32c95dbb9d | ||
|
|
1aef941d1c | ||
|
|
b0f392522b | ||
|
|
24af6f21eb | ||
|
|
1635bee387 | ||
|
|
422963b38a | ||
|
|
a0fb8d6b3d | ||
|
|
698a12082d | ||
|
|
a6960d81e3 | ||
|
|
178b8a4423 | ||
|
|
738b1062b7 | ||
|
|
b116bc7016 | ||
|
|
f1307c6d82 | ||
|
|
2c568bb2a2 | ||
|
|
6db9f346e3 | ||
|
|
070afb9364 | ||
|
|
42102eb067 | ||
|
|
ced5e26420 | ||
|
|
f26cdab1f6 | ||
|
|
ca418de803 | ||
|
|
47ee9718d3 | ||
|
|
a5541df5ec | ||
|
|
ae63d42ae9 | ||
|
|
7994c42f8c | ||
|
|
1eb4fe05fd | ||
|
|
3f47ae890c | ||
|
|
aeb3a47cab | ||
|
|
72f3ad48cd | ||
|
|
458f7c3c42 | ||
|
|
55f5624ddd | ||
|
|
4d87285fb7 | ||
|
|
ef6a714910 | ||
|
|
299480aa83 | ||
|
|
da9e3d475e | ||
|
|
edc708f8ff | ||
|
|
841dbf9a36 | ||
|
|
1a2805ba56 | ||
|
|
0aabba668e | ||
|
|
e12ae31795 | ||
|
|
6f976f6a2b | ||
|
|
5690e55d99 | ||
|
|
308ccb3d2b | ||
|
|
2bf6ba2502 | ||
|
|
06ef0c3990 | ||
|
|
c696386825 | ||
|
|
2b1247cf36 | ||
|
|
27e77d9b0b | ||
|
|
1a811edc95 | ||
|
|
e29c11546c | ||
|
|
4564b7f607 | ||
|
|
f01d935bb1 | ||
|
|
2676170b61 | ||
|
|
55024ca1c3 | ||
|
|
4f5d9f800e | ||
|
|
1abe5932ad | ||
|
|
5bc8b39f56 | ||
|
|
136e66b1d0 | ||
|
|
8810544c7c | ||
|
|
5e63a83238 | ||
|
|
6f1299a29d | ||
|
|
2ba6aaa46e | ||
|
|
967e57ad46 | ||
|
|
99e822d509 | ||
|
|
d84270be8d | ||
|
|
aa7bbbd4e9 | ||
|
|
fd6fb456b4 | ||
|
|
2c8a1b4972 | ||
|
|
f434cd6aa2 | ||
|
|
4b21ba2709 | ||
|
|
77659efe1f | ||
|
|
706e5040ae | ||
|
|
10c078a993 | ||
|
|
faf0ba898c | ||
|
|
5b1cd7e8da | ||
|
|
f920fc61ac | ||
|
|
3a01a92f03 | ||
|
|
8133f0c970 | ||
|
|
8a4f759a78 | ||
|
|
2df8ce463c | ||
|
|
8353f353ae | ||
|
|
a53fc736e9 | ||
|
|
db31fa9f41 | ||
|
|
edf6a1694f | ||
|
|
e9db22eb50 | ||
|
|
745a831d55 | ||
|
|
8717e25b79 | ||
|
|
50a931cf4d | ||
|
|
cc0f08dc03 | ||
|
|
9f3cd1b4da | ||
|
|
de33ceab41 | ||
|
|
306c727832 | ||
|
|
7433e97c4a | ||
|
|
e6eadd9f92 | ||
|
|
33f50ba580 | ||
|
|
d83e271d3e | ||
|
|
d113ca3061 | ||
|
|
d29edc4c36 | ||
|
|
e9f941e3d0 | ||
|
|
d5744fb51e | ||
|
|
cc5a7d9a0b | ||
|
|
16c51c2720 | ||
|
|
ac09534070 | ||
|
|
07e315a382 | ||
|
|
2f4268f1af | ||
|
|
cddcec9ba5 | ||
|
|
5864b09bde | ||
|
|
a7a94d9211 | ||
|
|
cfaa8e45ad | ||
|
|
ffbdedc7cd | ||
|
|
d9e6b89918 | ||
|
|
47377711b8 | ||
|
|
527e3cd70a | ||
|
|
9494b5aae8 | ||
|
|
9749990dcc | ||
|
|
7eb93f6323 | ||
|
|
b3e5726a83 | ||
|
|
8eabdd17ba | ||
|
|
af90b20f12 | ||
|
|
4bf27ac2db | ||
|
|
7c5692f700 | ||
|
|
5dfccaac9d | ||
|
|
b3d81d2f43 | ||
|
|
9de74886b6 | ||
|
|
6c6ce796d9 | ||
|
|
c87a8ad7d9 | ||
|
|
ce05e82885 | ||
|
|
62e262785f | ||
|
|
1177d38087 | ||
|
|
e6a58ef5ae | ||
|
|
688f017d3c | ||
|
|
dc09d871ed | ||
|
|
86ae23332e | ||
|
|
5796f12fa8 | ||
|
|
87c5eddc9e | ||
|
|
8ea4463ac5 | ||
|
|
32eb81337e | ||
|
|
446f45cc28 | ||
|
|
cab1e11e95 | ||
|
|
149a28e0dc | ||
|
|
03d67d92be | ||
|
|
369167e005 | ||
|
|
bab128ab26 | ||
|
|
f1e5e7bab2 | ||
|
|
0dfa151b02 | ||
|
|
13f2ae6e22 | ||
|
|
aa461b27c3 | ||
|
|
3e90c0abd1 | ||
|
|
ad32a8d9be | ||
|
|
ff16530314 | ||
|
|
e9799e283f | ||
|
|
c765438e63 | ||
|
|
90eba0a0b6 | ||
|
|
611fe8007f | ||
|
|
e9ed6063c3 | ||
|
|
b96bbc91a4 | ||
|
|
768de61401 | ||
|
|
90a44d999a | ||
|
|
ceaf69c27d | ||
|
|
7e35d8df56 | ||
|
|
adcf609066 | ||
|
|
38b7c51985 | ||
|
|
ef05762635 | ||
|
|
adc384dd78 | ||
|
|
5ab5fee130 | ||
|
|
460be68cd3 | ||
|
|
4c29eb3549 | ||
|
|
dfac510c15 | ||
|
|
337d914124 | ||
|
|
632b3948e3 | ||
|
|
8e67f2edcd | ||
|
|
4ccdd9fef6 | ||
|
|
ca101270cc | ||
|
|
e36b7e80b4 | ||
|
|
aa6553716d | ||
|
|
57c090c3a7 | ||
|
|
a3ab80b8b4 | ||
|
|
7218d778cf | ||
|
|
a016483057 | ||
|
|
12caa6d1d4 | ||
|
|
af4bbb10fc | ||
|
|
1058630dd1 | ||
|
|
8c8a4481ee | ||
|
|
309d7df050 | ||
|
|
8e966b0609 | ||
|
|
f825f84faa | ||
|
|
1e74b8af8f | ||
|
|
b0d0110b81 | ||
|
|
c02d0785fa | ||
|
|
09730a0775 | ||
|
|
b2d94aaa16 | ||
|
|
b7a5adf670 | ||
|
|
9cde77a408 | ||
|
|
b9aade2da4 | ||
|
|
d4c496d60d | ||
|
|
a7e21bbfe2 | ||
|
|
0d8b919831 | ||
|
|
4b07619769 | ||
|
|
99cd416628 | ||
|
|
626bb0d2b9 | ||
|
|
7d55c5453b | ||
|
|
3dad1c2516 | ||
|
|
9651e05f4b | ||
|
|
da6c022ef4 | ||
|
|
c200ea56cf | ||
|
|
20fb949a23 | ||
|
|
4a7629a174 | ||
|
|
211f877384 | ||
|
|
4ebbbca432 | ||
|
|
eb00fe6f3d | ||
|
|
86e6007733 | ||
|
|
4a90bd2621 | ||
|
|
8b189443b7 | ||
|
|
9b61a325fa | ||
|
|
471ef28210 | ||
|
|
079e4893b1 | ||
|
|
fd559cfc63 | ||
|
|
0e7cce750d | ||
|
|
2784dfd34e | ||
|
|
3958fcfcef | ||
|
|
673783ceab | ||
|
|
c4941e25ed | ||
|
|
4851e582f6 | ||
|
|
6a3c7b938b | ||
|
|
2c0245b020 | ||
|
|
879b37e164 | ||
|
|
f358709ab2 | ||
|
|
05f7c8f877 | ||
|
|
6c814ff82f | ||
|
|
b6b880d21a | ||
|
|
884cb6e026 | ||
|
|
6815b62019 | ||
|
|
5898fc851a | ||
|
|
9d55afbf85 | ||
|
|
6f4eaf5234 | ||
|
|
a5d545f29b | ||
|
|
8d2655a4bf | ||
|
|
d83a381231 | ||
|
|
045fc1519b | ||
|
|
b592da7431 | ||
|
|
c38f1bcb3e | ||
|
|
f678ff8dd1 | ||
|
|
b25d405972 | ||
|
|
340e9a90a5 | ||
|
|
9a902af917 | ||
|
|
19652c49c9 | ||
|
|
31b5f73197 | ||
|
|
595f269f62 | ||
|
|
20994c1834 | ||
|
|
3573ef2bc5 | ||
|
|
6d2d943e18 | ||
|
|
0ce0a87557 | ||
|
|
4e894dd0fd | ||
|
|
6a70898e7b | ||
|
|
59f8b55bda | ||
|
|
3bfca9e296 | ||
|
|
4a63f976ae | ||
|
|
43eda4fadf | ||
|
|
6820e1e76c | ||
|
|
094d1416ca | ||
|
|
b215474271 | ||
|
|
b2a5336556 | ||
|
|
7a4b7057bc | ||
|
|
8fc3727b88 | ||
|
|
b19d67f853 | ||
|
|
17d567d0bb | ||
|
|
ffaf7b60b6 | ||
|
|
0a7b096abf | ||
|
|
504b3ca596 | ||
|
|
e8f993623b | ||
|
|
5ebbf5c6b2 | ||
|
|
1f4e50df9d | ||
|
|
66c1da0f1e | ||
|
|
4e098bcff7 | ||
|
|
4aca6ef86a | ||
|
|
d7d5bacd21 | ||
|
|
5a28d6646f | ||
|
|
10a2407b32 | ||
|
|
6bbd3f5955 | ||
|
|
efe3b24ab9 | ||
|
|
24230da4a7 | ||
|
|
eb32da49aa | ||
|
|
50f20cc30d | ||
|
|
ff4618602a | ||
|
|
d530b9a1de | ||
|
|
52a096b739 | ||
|
|
35ca2233f8 | ||
|
|
40b8638ddd | ||
|
|
a910851422 | ||
|
|
8bfbe1c77d | ||
|
|
74c57cd86a | ||
|
|
b23ba9d9c4 | ||
|
|
c510d886a9 | ||
|
|
a0e19818a0 | ||
|
|
acb4aa1a13 | ||
|
|
bc4370348f | ||
|
|
8d0dcd0068 | ||
|
|
7bfbe1376b | ||
|
|
25221b5271 | ||
|
|
c53a7347e2 | ||
|
|
39e2001d97 | ||
|
|
3b6beb7f16 | ||
|
|
bd19f477f7 | ||
|
|
b571aad6a0 | ||
|
|
53c59e35e9 | ||
|
|
e88f3f9c7c | ||
|
|
120e12fd34 | ||
|
|
5fec230181 | ||
|
|
1084abe179 | ||
|
|
bbf5f8189b | ||
|
|
9f93763b98 | ||
|
|
deb0ad5428 | ||
|
|
e3aa2e15cb | ||
|
|
41cc24d1f5 | ||
|
|
287bd7a41f | ||
|
|
6ce3791140 | ||
|
|
878e549211 | ||
|
|
28f34a8380 | ||
|
|
a1d348b768 | ||
|
|
dbc7272fb4 | ||
|
|
d2fc531c81 | ||
|
|
0925279521 | ||
|
|
66abb29cf3 | ||
|
|
b9de94f0f1 | ||
|
|
2cb891b935 | ||
|
|
a1ec035d9c | ||
|
|
148eb21430 | ||
|
|
50ed2a9a7f | ||
|
|
fb618b4b22 | ||
|
|
7dd832dd9a | ||
|
|
8c929f65e2 | ||
|
|
5de43c1bf2 | ||
|
|
3c18ad76f7 | ||
|
|
0b38fcd74e | ||
|
|
2374c76192 | ||
|
|
e6cfa65ee2 | ||
|
|
9db8a2cc24 | ||
|
|
aaa5a41af6 | ||
|
|
379cd20154 | ||
|
|
8b2ebdf49c | ||
|
|
dc14d5023f | ||
|
|
a47ea153da | ||
|
|
2068ede045 | ||
|
|
037a83e1ff | ||
|
|
dddd41e891 | ||
|
|
2c2159e512 | ||
|
|
6bb0cedf21 | ||
|
|
b060cec580 | ||
|
|
7b8a945875 | ||
|
|
3d7b1a7df5 | ||
|
|
316922e9a6 | ||
|
|
6beb962282 | ||
|
|
5e28190d87 | ||
|
|
4be1f7900d | ||
|
|
b0faf9d48d | ||
|
|
ba1eaf4afa | ||
|
|
eefcb5a2aa | ||
|
|
eb59e98785 | ||
|
|
51bcdaca47 | ||
|
|
7b2e8eef46 | ||
|
|
a99762fc28 | ||
|
|
53e47581d4 | ||
|
|
c07abe966b | ||
|
|
523ce0b6ee | ||
|
|
7e460e50e1 | ||
|
|
dc6fcb9761 | ||
|
|
917bc66fb0 | ||
|
|
dc25345b7c | ||
|
|
77408f90b6 | ||
|
|
cba1fc36af | ||
|
|
0d1283a0fc | ||
|
|
fce81d8210 | ||
|
|
9fe6dccfca | ||
|
|
bd017d02e0 | ||
|
|
67d91c171a | ||
|
|
8d5e8cc58f | ||
|
|
11bd9b3c19 | ||
|
|
7f1cbab717 | ||
|
|
c3244342e7 | ||
|
|
d96cf7d14d | ||
|
|
4210d00ac4 | ||
|
|
7b5a700655 | ||
|
|
3de53e1f8a | ||
|
|
f3b7847697 | ||
|
|
ec22421bd9 | ||
|
|
314001eb0c | ||
|
|
02c30a2544 | ||
|
|
09809d29bc | ||
|
|
fc5df6cd14 | ||
|
|
6a00b623a8 | ||
|
|
f47cd7ebc0 | ||
|
|
68180e5b44 | ||
|
|
a405a51706 | ||
|
|
7e88490bd1 | ||
|
|
3d1e4b0489 | ||
|
|
5bab03991b | ||
|
|
650dde4c84 | ||
|
|
cb9f3ee145 | ||
|
|
db2c272aea | ||
|
|
64d87ae1e1 | ||
|
|
092df57695 | ||
|
|
2b0f8d9f0d | ||
|
|
4623f3eff8 | ||
|
|
df66863ae5 | ||
|
|
a9a7a37f6a | ||
|
|
c385f9b371 | ||
|
|
2daddace31 | ||
|
|
7a9a6418a5 | ||
|
|
14bb6899d8 | ||
|
|
bb44082d51 | ||
|
|
2e212b950f | ||
|
|
9c94fadfcc | ||
|
|
dace5f3845 | ||
|
|
833fa285aa | ||
|
|
1b5814ecc2 | ||
|
|
0b28cb8fdb | ||
|
|
9444c7b4f6 | ||
|
|
40650e7912 | ||
|
|
afc68618c6 | ||
|
|
5d1807a551 | ||
|
|
23922dfb2d | ||
|
|
b84b410fd8 | ||
|
|
c51956b2db | ||
|
|
d9fe775d11 | ||
|
|
2bc6d249de | ||
|
|
091e6ec5f8 | ||
|
|
cef2194427 | ||
|
|
0931b6a5fe | ||
|
|
19aed47f02 | ||
|
|
4840b0f694 | ||
|
|
a7946377b4 | ||
|
|
9901cb8502 | ||
|
|
6e09aa85c2 | ||
|
|
b40a9dd6f5 | ||
|
|
2e9bae41f3 | ||
|
|
d0c5f35707 | ||
|
|
8aa0615293 | ||
|
|
7086cfa277 | ||
|
|
fc88120fdc | ||
|
|
85c4fcc96f | ||
|
|
19399b491b | ||
|
|
dfed1299c4 | ||
|
|
46cbb4fd28 | ||
|
|
bf674a0e5c | ||
|
|
15cc6ff7fe | ||
|
|
2dd215d620 | ||
|
|
883bae737c | ||
|
|
f097b9e66d | ||
|
|
e421414b8a | ||
|
|
57a0e88fa4 | ||
|
|
e211f531c5 | ||
|
|
22b93e91f6 | ||
|
|
d018246292 | ||
|
|
6d449d63c8 | ||
|
|
940603ae73 | ||
|
|
10a3b63a5f | ||
|
|
a9ebec14f6 | ||
|
|
47ea989bbd | ||
|
|
aadd083b9e | ||
|
|
297a264fe0 | ||
|
|
890fee83ad | ||
|
|
11bd3a2b91 | ||
|
|
cfedb6df99 | ||
|
|
b1da6af30b | ||
|
|
82416456a0 | ||
|
|
e7cf3488b7 | ||
|
|
c1a12c9573 | ||
|
|
75f8767819 | ||
|
|
6fa648d6ca | ||
|
|
3a1d9c25af | ||
|
|
7abbd4aee9 | ||
|
|
0a4c6ed5db | ||
|
|
a6c6a6663b | ||
|
|
de1fd001d6 | ||
|
|
68ed31c6ee | ||
|
|
fa7be54ee9 | ||
|
|
b64d84c252 | ||
|
|
267a5ab423 | ||
|
|
9da3411cc1 | ||
|
|
bdddf8f37e | ||
|
|
62d5af8599 | ||
|
|
9bf88dfd14 | ||
|
|
aba771e355 | ||
|
|
9518be35b4 | ||
|
|
3595e87cd6 | ||
|
|
2085aad005 | ||
|
|
019d70cc52 | ||
|
|
ac6567ba59 | ||
|
|
a578ee7213 | ||
|
|
0fd54b159b | ||
|
|
aa441e7483 | ||
|
|
b366466e11 | ||
|
|
c0076091c2 | ||
|
|
4139275b76 | ||
|
|
31a14a3f1b | ||
|
|
d782ca68af | ||
|
|
e616626c16 | ||
|
|
b7453c6700 | ||
|
|
20e63a58be | ||
|
|
795e7af23e | ||
|
|
a4f9f590a4 | ||
|
|
dc6f16a034 | ||
|
|
ce1a2a6ca6 | ||
|
|
b632e0acda | ||
|
|
998d0c4faf | ||
|
|
f9b16cf03c | ||
|
|
3002d27273 | ||
|
|
9e1bdd4a31 | ||
|
|
c4a33ca151 | ||
|
|
2d87e1b817 | ||
|
|
b60cb63fdb | ||
|
|
eddb4f3f75 | ||
|
|
292345f709 | ||
|
|
22b780ddcd | ||
|
|
34c62c5268 | ||
|
|
b6ea61170f | ||
|
|
8f80266ad3 | ||
|
|
e58e77bc63 | ||
|
|
6f583ce396 | ||
|
|
3f60ea15f7 | ||
|
|
20e9826ef0 | ||
|
|
2e57057baa | ||
|
|
a6423763e7 | ||
|
|
5a7a9dd03e | ||
|
|
c7edefd247 | ||
|
|
1361abc4e8 | ||
|
|
8852afb5fa | ||
|
|
edec6ec57c | ||
|
|
3af8b694fe | ||
|
|
ab446e081d | ||
|
|
552fd37294 | ||
|
|
8d82d4d56f | ||
|
|
3add7aa25e | ||
|
|
35ab58c440 | ||
|
|
ae9bc8a75d | ||
|
|
95d436100d | ||
|
|
20420f865e | ||
|
|
8ab4d2955a | ||
|
|
51616dda77 | ||
|
|
ec9a266164 | ||
|
|
1d00084202 | ||
|
|
bb3e50008a | ||
|
|
0fffcf2cc1 | ||
|
|
c59b8263ca | ||
|
|
161f256066 | ||
|
|
a3bff105cc | ||
|
|
f1ee6567df | ||
|
|
8c2e8c9597 | ||
|
|
832708299f | ||
|
|
97883a42c2 | ||
|
|
7f1710fb73 | ||
|
|
05f1289268 | ||
|
|
c70659b7a1 | ||
|
|
bc51c839d0 | ||
|
|
ccc8d685b2 | ||
|
|
8733f53b6b | ||
|
|
ae514d60d8 | ||
|
|
a013141118 | ||
|
|
9c149f3bb3 | ||
|
|
26157cf5a7 | ||
|
|
d863cc6920 | ||
|
|
7ddeb9025b | ||
|
|
2810a87cd0 | ||
|
|
0106767c35 | ||
|
|
bbbbe4d32a | ||
|
|
0ee5106b7a | ||
|
|
a0bbe38ee7 | ||
|
|
7ab51a992c | ||
|
|
98231b8d25 | ||
|
|
dbbe0f27bf | ||
|
|
543f10657c | ||
|
|
afbf3d94c2 | ||
|
|
a632ef9bdd | ||
|
|
2eb141caa3 | ||
|
|
430792626b | ||
|
|
b14ff4cb33 | ||
|
|
c5b4fbf238 | ||
|
|
72d6acaa7f | ||
|
|
5b479ef69e | ||
|
|
f5af26092a | ||
|
|
9c55a95eb9 | ||
|
|
4bfc315b57 | ||
|
|
da1d76e43f | ||
|
|
3140dcc071 | ||
|
|
a57c37b58d | ||
|
|
9b5b3cad22 | ||
|
|
3e6ff1ab56 | ||
|
|
13892efa70 | ||
|
|
dc6c657d41 | ||
|
|
3ceed1fc4b | ||
|
|
5c3c3399d9 | ||
|
|
023bccec11 | ||
|
|
5cb1bb935b | ||
|
|
e03bceba88 | ||
|
|
cab3a0aba3 | ||
|
|
5b6fe659c2 | ||
|
|
156522912e | ||
|
|
1d50748f32 | ||
|
|
77e9d3d843 | ||
|
|
c8df7566b5 | ||
|
|
eac173dfa3 | ||
|
|
d158e15c08 | ||
|
|
e976c28b8a | ||
|
|
fb3a8582a2 | ||
|
|
c695d0cb5d | ||
|
|
5629b640cc | ||
|
|
f6400e9822 | ||
|
|
900bf8e6ea | ||
|
|
ea75179143 | ||
|
|
0a8dacda24 | ||
|
|
faae0488c8 | ||
|
|
44ee58aa9b | ||
|
|
5834229427 | ||
|
|
9a1092199d | ||
|
|
78c8da6fae | ||
|
|
e02e36cfea | ||
|
|
f7de81645c | ||
|
|
a04bea26da | ||
|
|
f34fb3576d | ||
|
|
a0dc6536b1 | ||
|
|
72bf44cb6d | ||
|
|
ab1aa0dce9 | ||
|
|
0a0ac85542 | ||
|
|
f030b16adf | ||
|
|
0e5b9b23f1 | ||
|
|
87fcb7239a | ||
|
|
41998dff95 | ||
|
|
c6bcd6f646 | ||
|
|
59963a6f34 | ||
|
|
ddf867bf53 | ||
|
|
de27856640 | ||
|
|
4b5bef8d6a | ||
|
|
157725a25a | ||
|
|
290a630526 | ||
|
|
40523f29dd | ||
|
|
86fb69a931 | ||
|
|
06b287d4d4 | ||
|
|
7039961d4c | ||
|
|
0b3cc1c175 | ||
|
|
474d91d29a | ||
|
|
f4ff18304a | ||
|
|
4ee18973de | ||
|
|
a0e24b1722 | ||
|
|
84b8c2f2cf | ||
|
|
61c9f1a1ef | ||
|
|
59fa76a42f | ||
|
|
dc6f37b3ec | ||
|
|
752504dcc8 | ||
|
|
f64ee3bccf | ||
|
|
a9a4d76705 | ||
|
|
6b208a8730 | ||
|
|
6ae93686b7 | ||
|
|
fbd11c6d44 | ||
|
|
285906ea9d | ||
|
|
f215368c4a | ||
|
|
fdef9e0766 | ||
|
|
988507f8e1 | ||
|
|
3b905d490b | ||
|
|
bb0d06cdfc | ||
|
|
27bf768cc6 | ||
|
|
d8007f6236 | ||
|
|
ad6d9bcdd5 | ||
|
|
2cf23d5109 | ||
|
|
8bd2ace3db | ||
|
|
f2bf5869ba | ||
|
|
a3d57cbd24 | ||
|
|
6bcd3d3b8f | ||
|
|
f9d241e474 | ||
|
|
6a94271a10 | ||
|
|
9b102412af | ||
|
|
3bf7279195 | ||
|
|
76acf43128 | ||
|
|
76a1100b8c | ||
|
|
1f570a9f39 | ||
|
|
f4694ba119 | ||
|
|
d3a4f81b3c | ||
|
|
01cc0b06c0 | ||
|
|
61a53b24fd | ||
|
|
2632b5c2af | ||
|
|
a293c76ed9 | ||
|
|
e88aede939 | ||
|
|
dd2894faab | ||
|
|
032fc3847f | ||
|
|
9bd29056c6 | ||
|
|
a6956db8dc | ||
|
|
28d24bb6ea | ||
|
|
e251908cb3 | ||
|
|
f400c5576a | ||
|
|
7a39b1381b | ||
|
|
0a3028329f | ||
|
|
84cf485b31 | ||
|
|
fa4c46c23d | ||
|
|
7cff2818e4 | ||
|
|
ec4228edc1 | ||
|
|
216e16cfb1 | ||
|
|
35bd3dfb6f | ||
|
|
60dc3f6d82 | ||
|
|
3b56a0181f | ||
|
|
bded1fe660 | ||
|
|
87606def48 | ||
|
|
3cfe00e535 | ||
|
|
96860fb93d | ||
|
|
f0a3ae51db | ||
|
|
0b056e92de | ||
|
|
96d7c460fa | ||
|
|
9f0f50eb15 | ||
|
|
9c23345f1c | ||
|
|
378b0ac7c9 | ||
|
|
e4c40330f7 | ||
|
|
03312cd707 | ||
|
|
58a5e837f7 | ||
|
|
6241c56fda | ||
|
|
18f30ac66e | ||
|
|
2b6b9b6737 | ||
|
|
d789491561 | ||
|
|
fa0cc710ef | ||
|
|
497401e8e0 | ||
|
|
8db8839d90 | ||
|
|
3ea4e82acb | ||
|
|
e79fbded9e | ||
|
|
1f208d8784 | ||
|
|
86f82d6065 | ||
|
|
f7e08bbea8 | ||
|
|
2accf954ca | ||
|
|
dadb1a3fba | ||
|
|
181470d764 | ||
|
|
e65b857667 | ||
|
|
9a0b6f45bb | ||
|
|
c1eeafedf0 | ||
|
|
bffb1995bd | ||
|
|
a2fb94d0f0 | ||
|
|
3d98bb5084 | ||
|
|
27ca2153b0 | ||
|
|
548d416996 | ||
|
|
e0fbb7e18a | ||
|
|
8c872b3861 | ||
|
|
1e7cd74364 | ||
|
|
5304a7744a | ||
|
|
14c4c4997e | ||
|
|
932ea6b8f9 | ||
|
|
be6a6dccd9 | ||
|
|
aa5c74c477 | ||
|
|
855f6a417f | ||
|
|
910276deeb | ||
|
|
c3a62826d4 | ||
|
|
ad9551ca6d | ||
|
|
49d31049ac | ||
|
|
e8e7c85c62 | ||
|
|
1724e0b199 | ||
|
|
0cfce2d436 | ||
|
|
b489fe822c | ||
|
|
e164eb8b95 | ||
|
|
86a2b8c2a7 | ||
|
|
484cbcd960 | ||
|
|
29dd63d1eb | ||
|
|
4f14859351 | ||
|
|
6f962a9299 | ||
|
|
5adfb36629 | ||
|
|
c88b1a624f | ||
|
|
be5c4fb382 | ||
|
|
c2a9cc2733 | ||
|
|
b1c09c6ff1 | ||
|
|
d5dc801c00 | ||
|
|
3721ca9113 | ||
|
|
04751df30c | ||
|
|
78fd93b72a | ||
|
|
4dc14397ad | ||
|
|
df5c424a42 | ||
|
|
bae43d0dcd | ||
|
|
088c215569 | ||
|
|
efb0044419 | ||
|
|
65c9ca62de | ||
|
|
d691c614ac | ||
|
|
c9e7bd2ab4 | ||
|
|
f38a12c6d5 | ||
|
|
1755fe829c | ||
|
|
8b7784ecc0 | ||
|
|
86ce7ac45e | ||
|
|
a17e14c988 | ||
|
|
70dc89c3e8 | ||
|
|
b886bb1cfe | ||
|
|
ccd59a2f17 | ||
|
|
684edd27a2 | ||
|
|
c2e632ead2 | ||
|
|
3c109fb5fd | ||
|
|
a438175e8a | ||
|
|
57bbb06f39 | ||
|
|
f6381f5e91 | ||
|
|
8f877742d0 | ||
|
|
e25146a2d2 | ||
|
|
190607de92 | ||
|
|
5eeb68e355 | ||
|
|
ddc8218a2d | ||
|
|
c5e25f4813 | ||
|
|
0b393eeb5f | ||
|
|
c284ad7ba9 | ||
|
|
ff724154fb | ||
|
|
03070b8b6a | ||
|
|
ab3319a310 | ||
|
|
95d9cefcc9 | ||
|
|
23c8a2e324 | ||
|
|
0029a7e1c1 | ||
|
|
9dacf5cff3 | ||
|
|
598ddc17fa | ||
|
|
38d3b46939 | ||
|
|
41fda7f6ed | ||
|
|
30a347d0b3 | ||
|
|
9914c0ccb3 | ||
|
|
6b2db6dace | ||
|
|
15785fad73 | ||
|
|
f5f3b91b40 | ||
|
|
d6dda73fb9 | ||
|
|
b5988e19c1 | ||
|
|
059cba06bc | ||
|
|
f817bf887a | ||
|
|
c85802dd2e | ||
|
|
1fdb15a392 | ||
|
|
0f164d055f | ||
|
|
9a78cafbfd | ||
|
|
7a55be49f4 | ||
|
|
08c1328b52 | ||
|
|
c74c23b0ff | ||
|
|
8447d3cac5 | ||
|
|
7e72ae7d3d | ||
|
|
1b82aeb6b0 | ||
|
|
982da4aa77 | ||
|
|
fbb7e0f95f | ||
|
|
1f707c1e13 | ||
|
|
246071363a | ||
|
|
8b9ffc28ed | ||
|
|
f086aeb2be | ||
|
|
2e1ee0dfa4 | ||
|
|
f4cdf91fc9 | ||
|
|
d0f2edec0a | ||
|
|
6a69eec342 | ||
|
|
1ded3899b0 | ||
|
|
da865f855d | ||
|
|
b935f8d4f4 | ||
|
|
e123c0e019 | ||
|
|
9cec5e4bc9 | ||
|
|
05354ba2f0 | ||
|
|
3e8ce13db5 | ||
|
|
37ef3e187b | ||
|
|
fce7550609 | ||
|
|
056a1963e3 | ||
|
|
3aec473755 | ||
|
|
1f2d4e86c7 | ||
|
|
1a8d58889c | ||
|
|
b4f7d84563 | ||
|
|
24bda852d0 | ||
|
|
b567016ddd | ||
|
|
d35a4ca357 | ||
|
|
93223fe640 | ||
|
|
7ae4d3bc8d | ||
|
|
23bce71356 | ||
|
|
b794b293d1 | ||
|
|
4c76242d44 | ||
|
|
dfdce18c8d | ||
|
|
bfe5820d0f | ||
|
|
daf69964f2 | ||
|
|
ee82134c19 | ||
|
|
a2d05b0cf0 | ||
|
|
35beeb55b0 | ||
|
|
f655e8cfcb | ||
|
|
37585e8073 | ||
|
|
c50eac977b | ||
|
|
b6c31e1a5a | ||
|
|
2968960b41 | ||
|
|
f4933b08d0 | ||
|
|
d2c155afee | ||
|
|
8ed2330a3c | ||
|
|
4be412483e | ||
|
|
ea92ea0731 | ||
|
|
8aebb1b96b | ||
|
|
5eedbfb57f | ||
|
|
4fc33d2387 | ||
|
|
dc4352bffb | ||
|
|
2fa14b82f3 | ||
|
|
831621323b | ||
|
|
a2d9469761 | ||
|
|
a377fd6caa | ||
|
|
5518846e96 | ||
|
|
5e08461859 | ||
|
|
41ab4b1edb | ||
|
|
459f3a5b14 | ||
|
|
7603a29182 | ||
|
|
36213a52f1 | ||
|
|
0afea0eed6 | ||
|
|
ad7e94dac4 | ||
|
|
d5620b2d12 | ||
|
|
662b26a2d5 | ||
|
|
abc0add350 | ||
|
|
219a189993 | ||
|
|
cfce641053 | ||
|
|
79a2186c1f | ||
|
|
41e88d422e | ||
|
|
b08b4e2d4e | ||
|
|
329405e8f2 | ||
|
|
d737abd24a | ||
|
|
0318fea784 | ||
|
|
361b56a715 | ||
|
|
43dcba8802 | ||
|
|
62a9339a71 | ||
|
|
16c76de0f3 | ||
|
|
8950d528d5 | ||
|
|
240123372a | ||
|
|
28ccb55033 | ||
|
|
486a4b8f68 | ||
|
|
ea383a18de | ||
|
|
3ef4ed8bad | ||
|
|
ccaa7934ee | ||
|
|
cc4d20e380 | ||
|
|
32f5ef9b16 | ||
|
|
53bfb14fea | ||
|
|
a6980d5108 | ||
|
|
60543308f4 | ||
|
|
49fd0f5928 | ||
|
|
67304751bd | ||
|
|
bf97f089ca | ||
|
|
95663dc5f4 | ||
|
|
3b145a4695 | ||
|
|
6c08c6adce | ||
|
|
a3f91b8b55 | ||
|
|
e2afcb16e3 | ||
|
|
04cb001006 | ||
|
|
cea4507559 | ||
|
|
dc295cff80 | ||
|
|
be6895a87e | ||
|
|
1514fb44c5 | ||
|
|
e38dcc1ea8 | ||
|
|
83141d375a | ||
|
|
534aeee457 | ||
|
|
e557887da9 | ||
|
|
61d940696e | ||
|
|
f2fd82aa93 | ||
|
|
b2ce8a2abb | ||
|
|
d52a060b04 | ||
|
|
3a48c66154 | ||
|
|
28d5cb1741 | ||
|
|
644b51795a | ||
|
|
7f8bc4763d | ||
|
|
5006bc6194 | ||
|
|
ea7ee5c6b9 | ||
|
|
3675a58636 | ||
|
|
bbc111c577 | ||
|
|
5a5afc1810 | ||
|
|
528a60c166 | ||
|
|
d1c0982942 | ||
|
|
9a95385076 | ||
|
|
43a0c77a54 | ||
|
|
91473838d2 | ||
|
|
f7589160af | ||
|
|
b1a09b471c | ||
|
|
8dda3190e3 | ||
|
|
93c23ee39f | ||
|
|
7ccae17473 | ||
|
|
57ce17e7f5 | ||
|
|
3777bfa9c4 | ||
|
|
f793bb8467 | ||
|
|
d5cd59fd25 | ||
|
|
846c62e4e0 | ||
|
|
6028dd419e | ||
|
|
42c9c21470 | ||
|
|
31c6e279df | ||
|
|
08750272ba | ||
|
|
44c3bacae4 | ||
|
|
58b15caba3 | ||
|
|
f313887d99 | ||
|
|
4556e16f44 | ||
|
|
753f3cd837 | ||
|
|
45b43366d2 | ||
|
|
3d75b56ebd | ||
|
|
586b5ace33 | ||
|
|
9785f0e3d2 | ||
|
|
f163101200 | ||
|
|
4e3fe7b738 |
225 changed files with 27273 additions and 7463 deletions
152
.github/workflows/build-and-release.yml
vendored
152
.github/workflows/build-and-release.yml
vendored
|
|
@ -4,9 +4,135 @@ on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "*"
|
- "*"
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
id-token: write
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
set-version:
|
||||||
|
name: Set version number
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
runs-on: open-source-releaser
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.get_version.outputs.tag }}
|
||||||
|
steps:
|
||||||
|
- name: Set version number
|
||||||
|
id: get_version
|
||||||
|
run: |
|
||||||
|
version="${{ github.ref_name }}"
|
||||||
|
echo "tag=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
create-binaries:
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
needs: set-version
|
||||||
|
uses: ./.github/workflows/create-artifact.yml
|
||||||
|
with:
|
||||||
|
version: ${{ needs.set-version.outputs.version }}
|
||||||
|
|
||||||
|
publish-binaries:
|
||||||
|
name: Publish to GitHub release
|
||||||
|
if: github.event_name == 'push'
|
||||||
|
needs: [set-version, create-binaries]
|
||||||
|
runs-on: open-source-releaser
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Download all binary artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: binaries/
|
||||||
|
pattern: safe-chain-*
|
||||||
|
merge-multiple: false
|
||||||
|
|
||||||
|
- name: Rename binaries to include platform and architecture
|
||||||
|
run: |
|
||||||
|
mkdir release-artifacts
|
||||||
|
mv binaries/safe-chain-macos-x64/safe-chain release-artifacts/safe-chain-macos-x64
|
||||||
|
mv binaries/safe-chain-macos-arm64/safe-chain release-artifacts/safe-chain-macos-arm64
|
||||||
|
mv binaries/safe-chain-linux-x64/safe-chain release-artifacts/safe-chain-linux-x64
|
||||||
|
mv binaries/safe-chain-linux-arm64/safe-chain release-artifacts/safe-chain-linux-arm64
|
||||||
|
mv binaries/safe-chain-linuxstatic-x64/safe-chain release-artifacts/safe-chain-linuxstatic-x64
|
||||||
|
mv binaries/safe-chain-linuxstatic-arm64/safe-chain release-artifacts/safe-chain-linuxstatic-arm64
|
||||||
|
mv binaries/safe-chain-win-x64/safe-chain.exe release-artifacts/safe-chain-win-x64.exe
|
||||||
|
mv binaries/safe-chain-win-arm64/safe-chain.exe release-artifacts/safe-chain-win-arm64.exe
|
||||||
|
|
||||||
|
- name: Move install scripts and hard-code version and checksums
|
||||||
|
env:
|
||||||
|
VERSION: ${{ needs.set-version.outputs.version }}
|
||||||
|
run: |
|
||||||
|
SHA_MACOS_X64=$(sha256sum release-artifacts/safe-chain-macos-x64 | awk '{print $1}')
|
||||||
|
SHA_MACOS_ARM64=$(sha256sum release-artifacts/safe-chain-macos-arm64 | awk '{print $1}')
|
||||||
|
SHA_LINUX_X64=$(sha256sum release-artifacts/safe-chain-linux-x64 | awk '{print $1}')
|
||||||
|
SHA_LINUX_ARM64=$(sha256sum release-artifacts/safe-chain-linux-arm64 | awk '{print $1}')
|
||||||
|
SHA_LINUXSTATIC_X64=$(sha256sum release-artifacts/safe-chain-linuxstatic-x64 | awk '{print $1}')
|
||||||
|
SHA_LINUXSTATIC_ARM64=$(sha256sum release-artifacts/safe-chain-linuxstatic-arm64 | awk '{print $1}')
|
||||||
|
SHA_WIN_X64=$(sha256sum release-artifacts/safe-chain-win-x64.exe | awk '{print $1}')
|
||||||
|
SHA_WIN_ARM64=$(sha256sum release-artifacts/safe-chain-win-arm64.exe | awk '{print $1}')
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e "s/\$(fetch_latest_version)/${VERSION}/" \
|
||||||
|
-e "s|^SHA256_MACOS_X64=\"\"|SHA256_MACOS_X64=\"${SHA_MACOS_X64}\"|" \
|
||||||
|
-e "s|^SHA256_MACOS_ARM64=\"\"|SHA256_MACOS_ARM64=\"${SHA_MACOS_ARM64}\"|" \
|
||||||
|
-e "s|^SHA256_LINUX_X64=\"\"|SHA256_LINUX_X64=\"${SHA_LINUX_X64}\"|" \
|
||||||
|
-e "s|^SHA256_LINUX_ARM64=\"\"|SHA256_LINUX_ARM64=\"${SHA_LINUX_ARM64}\"|" \
|
||||||
|
-e "s|^SHA256_LINUXSTATIC_X64=\"\"|SHA256_LINUXSTATIC_X64=\"${SHA_LINUXSTATIC_X64}\"|" \
|
||||||
|
-e "s|^SHA256_LINUXSTATIC_ARM64=\"\"|SHA256_LINUXSTATIC_ARM64=\"${SHA_LINUXSTATIC_ARM64}\"|" \
|
||||||
|
-e "s|^SHA256_WIN_X64=\"\"|SHA256_WIN_X64=\"${SHA_WIN_X64}\"|" \
|
||||||
|
-e "s|^SHA256_WIN_ARM64=\"\"|SHA256_WIN_ARM64=\"${SHA_WIN_ARM64}\"|" \
|
||||||
|
install-scripts/install-safe-chain.sh > release-artifacts/install-safe-chain.sh
|
||||||
|
|
||||||
|
sed \
|
||||||
|
-e "s/\$Version = Get-LatestVersion/\$Version = \"${VERSION}\"/" \
|
||||||
|
-e "s|^\$SHA256_MACOS_X64 = \"\"|\$SHA256_MACOS_X64 = \"${SHA_MACOS_X64}\"|" \
|
||||||
|
-e "s|^\$SHA256_MACOS_ARM64 = \"\"|\$SHA256_MACOS_ARM64 = \"${SHA_MACOS_ARM64}\"|" \
|
||||||
|
-e "s|^\$SHA256_LINUX_X64 = \"\"|\$SHA256_LINUX_X64 = \"${SHA_LINUX_X64}\"|" \
|
||||||
|
-e "s|^\$SHA256_LINUX_ARM64 = \"\"|\$SHA256_LINUX_ARM64 = \"${SHA_LINUX_ARM64}\"|" \
|
||||||
|
-e "s|^\$SHA256_LINUXSTATIC_X64 = \"\"|\$SHA256_LINUXSTATIC_X64 = \"${SHA_LINUXSTATIC_X64}\"|" \
|
||||||
|
-e "s|^\$SHA256_LINUXSTATIC_ARM64 = \"\"|\$SHA256_LINUXSTATIC_ARM64 = \"${SHA_LINUXSTATIC_ARM64}\"|" \
|
||||||
|
-e "s|^\$SHA256_WIN_X64 = \"\"|\$SHA256_WIN_X64 = \"${SHA_WIN_X64}\"|" \
|
||||||
|
-e "s|^\$SHA256_WIN_ARM64 = \"\"|\$SHA256_WIN_ARM64 = \"${SHA_WIN_ARM64}\"|" \
|
||||||
|
install-scripts/install-safe-chain.ps1 > release-artifacts/install-safe-chain.ps1
|
||||||
|
|
||||||
|
cp install-scripts/uninstall-safe-chain.sh release-artifacts/uninstall-safe-chain.sh
|
||||||
|
cp install-scripts/uninstall-safe-chain.ps1 release-artifacts/uninstall-safe-chain.ps1
|
||||||
|
cp install-scripts/install-endpoint-mac.sh release-artifacts/install-endpoint-mac.sh
|
||||||
|
cp install-scripts/install-endpoint-windows.ps1 release-artifacts/install-endpoint-windows.ps1
|
||||||
|
cp install-scripts/uninstall-endpoint-mac.sh release-artifacts/uninstall-endpoint-mac.sh
|
||||||
|
cp install-scripts/uninstall-endpoint-windows.ps1 release-artifacts/uninstall-endpoint-windows.ps1
|
||||||
|
|
||||||
|
- name: Create draft release and upload assets
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
VERSION: ${{ needs.set-version.outputs.version }}
|
||||||
|
run: |
|
||||||
|
if ! gh release view "$VERSION" &>/dev/null; then
|
||||||
|
gh release create "$VERSION" --draft --title "$VERSION" --generate-notes
|
||||||
|
fi
|
||||||
|
gh release upload "$VERSION" --clobber \
|
||||||
|
release-artifacts/safe-chain-macos-x64 \
|
||||||
|
release-artifacts/safe-chain-macos-arm64 \
|
||||||
|
release-artifacts/safe-chain-linux-x64 \
|
||||||
|
release-artifacts/safe-chain-linux-arm64 \
|
||||||
|
release-artifacts/safe-chain-linuxstatic-x64 \
|
||||||
|
release-artifacts/safe-chain-linuxstatic-arm64 \
|
||||||
|
release-artifacts/safe-chain-win-x64.exe \
|
||||||
|
release-artifacts/safe-chain-win-arm64.exe \
|
||||||
|
release-artifacts/install-safe-chain.sh \
|
||||||
|
release-artifacts/install-safe-chain.ps1 \
|
||||||
|
release-artifacts/uninstall-safe-chain.sh \
|
||||||
|
release-artifacts/uninstall-safe-chain.ps1 \
|
||||||
|
release-artifacts/install-endpoint-mac.sh \
|
||||||
|
release-artifacts/install-endpoint-windows.ps1 \
|
||||||
|
release-artifacts/uninstall-endpoint-mac.sh \
|
||||||
|
release-artifacts/uninstall-endpoint-windows.ps1
|
||||||
|
|
||||||
|
publish-npm:
|
||||||
|
name: Publish to npm
|
||||||
|
if: github.event_name == 'release'
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -18,17 +144,12 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
registry-url: "https://registry.npmjs.org/"
|
registry-url: "https://registry.npmjs.org/"
|
||||||
env:
|
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
|
||||||
|
|
||||||
- name: Set version number
|
- name: Setup safe-chain
|
||||||
id: get_version
|
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
run: |
|
|
||||||
version="${{ github.ref_name }}"
|
|
||||||
echo "tag=$version" >> $GITHUB_OUTPUT
|
|
||||||
|
|
||||||
- name: Set the version in safe-chain package
|
- name: Set the version in safe-chain package
|
||||||
run: npm --no-git-tag-version version ${{ steps.get_version.outputs.tag }} --workspace=packages/safe-chain
|
run: npm --no-git-tag-version version ${{ github.event.release.tag_name }} --workspace=packages/safe-chain
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
@ -41,10 +162,15 @@ jobs:
|
||||||
cp README.md packages/safe-chain/
|
cp README.md packages/safe-chain/
|
||||||
cp LICENSE packages/safe-chain/
|
cp LICENSE packages/safe-chain/
|
||||||
cp -r docs packages/safe-chain/
|
cp -r docs packages/safe-chain/
|
||||||
|
cp npm-shrinkwrap.json packages/safe-chain/
|
||||||
|
|
||||||
- name: Publish to npm
|
- name: Publish to npm
|
||||||
run: |
|
run: |
|
||||||
echo "Publishing version ${{ steps.get_version.outputs.tag }} to NPM"
|
VERSION="${{ github.event.release.tag_name }}"
|
||||||
npm publish --workspace=packages/safe-chain --access public
|
echo "Publishing version $VERSION to NPM"
|
||||||
env:
|
if [[ "$VERSION" == *"-"* ]]; then
|
||||||
NPM_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
|
PRERELEASE_TAG=$(echo "$VERSION" | sed 's/.*-\([^-]*\)$/\1/')
|
||||||
|
npm publish --workspace=packages/safe-chain --access public --provenance --tag "$PRERELEASE_TAG"
|
||||||
|
else
|
||||||
|
npm publish --workspace=packages/safe-chain --access public --provenance
|
||||||
|
fi
|
||||||
|
|
|
||||||
82
.github/workflows/bump-endpoint.yml
vendored
Normal file
82
.github/workflows/bump-endpoint.yml
vendored
Normal file
|
|
@ -0,0 +1,82 @@
|
||||||
|
name: Bump Device Protection Automatically
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 * * * *' # every hour
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
bump-endpoint:
|
||||||
|
runs-on: open-source-releaser
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get latest safechain-internals release
|
||||||
|
id: latest
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
run: |
|
||||||
|
VERSION=$(gh api repos/AikidoSec/safechain-internals/releases/latest --jq '.tag_name')
|
||||||
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Get current version from install script
|
||||||
|
id: current
|
||||||
|
run: |
|
||||||
|
CURRENT=$(grep -oP '(?<=releases/download/)[^/]+(?=/EndpointProtection\.pkg)' install-scripts/install-endpoint-mac.sh)
|
||||||
|
echo "version=$CURRENT" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Download assets and compute checksums
|
||||||
|
if: steps.latest.outputs.version != steps.current.outputs.version
|
||||||
|
id: checksums
|
||||||
|
run: |
|
||||||
|
VERSION="${{ steps.latest.outputs.version }}"
|
||||||
|
BASE="https://github.com/AikidoSec/safechain-internals/releases/download/${VERSION}"
|
||||||
|
curl -fsSL "${BASE}/EndpointProtection.pkg" -o /tmp/EndpointProtection.pkg
|
||||||
|
curl -fsSL "${BASE}/EndpointProtection.msi" -o /tmp/EndpointProtection.msi
|
||||||
|
echo "mac=$(sha256sum /tmp/EndpointProtection.pkg | cut -d' ' -f1)" >> $GITHUB_OUTPUT
|
||||||
|
echo "win=$(sha256sum /tmp/EndpointProtection.msi | cut -d' ' -f1)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Update install scripts
|
||||||
|
if: steps.latest.outputs.version != steps.current.outputs.version
|
||||||
|
run: |
|
||||||
|
NEW="${{ steps.latest.outputs.version }}"
|
||||||
|
OLD="${{ steps.current.outputs.version }}"
|
||||||
|
MAC_SHA="${{ steps.checksums.outputs.mac }}"
|
||||||
|
WIN_SHA="${{ steps.checksums.outputs.win }}"
|
||||||
|
|
||||||
|
sed -i "s|${OLD}/EndpointProtection.pkg|${NEW}/EndpointProtection.pkg|" install-scripts/install-endpoint-mac.sh
|
||||||
|
sed -i "s|^DOWNLOAD_SHA256=\"[^\"]*\"|DOWNLOAD_SHA256=\"${MAC_SHA}\"|" install-scripts/install-endpoint-mac.sh
|
||||||
|
|
||||||
|
sed -i "s|${OLD}/EndpointProtection.msi|${NEW}/EndpointProtection.msi|" install-scripts/install-endpoint-windows.ps1
|
||||||
|
sed -i 's|^\$DownloadSha256 = "[^"]*"|\$DownloadSha256 = "'"${WIN_SHA}"'"|' install-scripts/install-endpoint-windows.ps1
|
||||||
|
|
||||||
|
- name: Open PR
|
||||||
|
if: steps.latest.outputs.version != steps.current.outputs.version
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
|
||||||
|
run: |
|
||||||
|
NEW="${{ steps.latest.outputs.version }}"
|
||||||
|
OLD="${{ steps.current.outputs.version }}"
|
||||||
|
BRANCH="bump/endpoint-${NEW}"
|
||||||
|
|
||||||
|
if git ls-remote --exit-code --heads origin "$BRANCH" &>/dev/null; then
|
||||||
|
echo "Branch $BRANCH already exists, skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
git checkout -b "$BRANCH"
|
||||||
|
git add install-scripts/install-endpoint-mac.sh install-scripts/install-endpoint-windows.ps1
|
||||||
|
git commit -m "Bump Endpoint to ${NEW}"
|
||||||
|
git push origin "$BRANCH"
|
||||||
|
PR_URL="https://github.com/${{ github.repository }}/compare/main...${BRANCH}?expand=1"
|
||||||
|
|
||||||
|
curl -s -X POST "$SLACK_WEBHOOK_URL" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"text\": \"update to ${NEW} - ${PR_URL}\"}"
|
||||||
94
.github/workflows/create-artifact.yml
vendored
Normal file
94
.github/workflows/create-artifact.yml
vendored
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
name: Create binaries
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: "Version to set in package.json"
|
||||||
|
required: false
|
||||||
|
type: string
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
create-binaries:
|
||||||
|
name: Create binary for ${{ matrix.os }}-${{ matrix.arch }}
|
||||||
|
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: macos
|
||||||
|
arch: x64
|
||||||
|
runner: macos-15-intel
|
||||||
|
target: node20-macos-x64
|
||||||
|
extension: ""
|
||||||
|
- os: macos
|
||||||
|
arch: arm64
|
||||||
|
runner: macos-latest
|
||||||
|
target: node20-macos-arm64
|
||||||
|
extension: ""
|
||||||
|
- os: linux
|
||||||
|
arch: x64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
target: node20-linux-x64
|
||||||
|
extension: ""
|
||||||
|
- os: linux
|
||||||
|
arch: arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
target: node20-linux-arm64
|
||||||
|
extension: ""
|
||||||
|
- os: linuxstatic
|
||||||
|
arch: x64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
target: node20-linuxstatic-x64
|
||||||
|
extension: ""
|
||||||
|
- os: linuxstatic
|
||||||
|
arch: arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
target: node20-linuxstatic-arm64
|
||||||
|
extension: ""
|
||||||
|
- os: win
|
||||||
|
arch: x64
|
||||||
|
runner: windows-latest
|
||||||
|
target: node20-win-x64
|
||||||
|
extension: ".exe"
|
||||||
|
- os: win
|
||||||
|
arch: arm64
|
||||||
|
runner: windows-11-arm
|
||||||
|
target: node20-win-arm64
|
||||||
|
extension: ".exe"
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "20.x"
|
||||||
|
|
||||||
|
- name: Setup safe-chain
|
||||||
|
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
|
- name: Set the version in safe-chain package
|
||||||
|
if: inputs.version != ''
|
||||||
|
env:
|
||||||
|
VERSION: ${{ inputs.version }}
|
||||||
|
shell: bash
|
||||||
|
run: npm --no-git-tag-version version $VERSION --workspace=packages/safe-chain --ignore-scripts
|
||||||
|
|
||||||
|
- name: Create binary
|
||||||
|
run: |
|
||||||
|
node build.js ${{ matrix.target }}
|
||||||
|
|
||||||
|
- name: Upload binary artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: safe-chain-${{ matrix.os }}-${{ matrix.arch }}
|
||||||
|
path: dist/*
|
||||||
68
.github/workflows/test-on-pr.yml
vendored
68
.github/workflows/test-on-pr.yml
vendored
|
|
@ -6,7 +6,12 @@ jobs:
|
||||||
unit-test:
|
unit-test:
|
||||||
name: Run unit tests and linting
|
name: Run unit tests and linting
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.os }}
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macos-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|
@ -17,20 +22,29 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|
||||||
|
- name: Setup safe-chain
|
||||||
|
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
shell: bash
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: npm ci
|
run: npm ci --ignore-scripts
|
||||||
|
|
||||||
- name: Run unit tests
|
- name: Run unit tests
|
||||||
run: npm test
|
run: npm test
|
||||||
|
|
||||||
- name: Run ESLint
|
- name: Run linting
|
||||||
run: npm run lint
|
run: npm run lint
|
||||||
|
|
||||||
|
- name: Type check
|
||||||
|
run: npm run typecheck --workspace=packages/safe-chain
|
||||||
|
|
||||||
- name: Create package tarball
|
- name: Create package tarball
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
run: npm pack --workspace=packages/safe-chain
|
run: npm pack --workspace=packages/safe-chain
|
||||||
|
|
||||||
- name: Upload package tarball
|
- name: Upload package tarball
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
|
if: matrix.os == 'ubuntu-latest'
|
||||||
with:
|
with:
|
||||||
name: safe-chain-package
|
name: safe-chain-package
|
||||||
path: aikidosec-safe-chain-*.tgz
|
path: aikidosec-safe-chain-*.tgz
|
||||||
|
|
@ -40,6 +54,46 @@ jobs:
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
# Common production setup
|
||||||
|
- node_version: "20"
|
||||||
|
npm_version: "10.2.0"
|
||||||
|
yarn_version: "4.0.0"
|
||||||
|
pnpm_version: "9.0.0"
|
||||||
|
# Current Active LTS with latest tools
|
||||||
|
- node_version: "22"
|
||||||
|
npm_version: "latest"
|
||||||
|
yarn_version: "latest"
|
||||||
|
pnpm_version: "latest"
|
||||||
|
# Legacy support (EOL April 2025)
|
||||||
|
- node_version: "18"
|
||||||
|
npm_version: "9.0.0"
|
||||||
|
yarn_version: "3.6.0"
|
||||||
|
pnpm_version: "8.0.0"
|
||||||
|
# Mixed versions (old npm, new package managers)
|
||||||
|
- node_version: "20"
|
||||||
|
npm_version: "9.0.0"
|
||||||
|
yarn_version: "latest"
|
||||||
|
pnpm_version: "10.0.0"
|
||||||
|
# Version pinning scenario
|
||||||
|
- node_version: "22"
|
||||||
|
npm_version: "10.2.0"
|
||||||
|
yarn_version: "4.0.0"
|
||||||
|
pnpm_version: "9.0.0"
|
||||||
|
# Backward compatibility testing
|
||||||
|
- node_version: "18"
|
||||||
|
npm_version: "latest"
|
||||||
|
yarn_version: "latest"
|
||||||
|
pnpm_version: "10.0.0"
|
||||||
|
# Future compatibility (becomes LTS October 2025)
|
||||||
|
- node_version: "24"
|
||||||
|
npm_version: "latest"
|
||||||
|
yarn_version: "latest"
|
||||||
|
pnpm_version: "latest"
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
@ -49,10 +103,18 @@ jobs:
|
||||||
with:
|
with:
|
||||||
node-version: "lts/*"
|
node-version: "lts/*"
|
||||||
|
|
||||||
|
- name: Setup safe-chain
|
||||||
|
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
|
||||||
- name: Install dependencies (root)
|
- name: Install dependencies (root)
|
||||||
run: npm ci
|
run: npm ci
|
||||||
|
|
||||||
- name: Run E2E tests
|
- name: Run E2E tests
|
||||||
|
env:
|
||||||
|
NODE_VERSION: ${{ matrix.node_version }}
|
||||||
|
NPM_VERSION: ${{ matrix.npm_version }}
|
||||||
|
YARN_VERSION: ${{ matrix.yarn_version }}
|
||||||
|
PNPM_VERSION: ${{ matrix.pnpm_version }}
|
||||||
run: npm run test:e2e
|
run: npm run test:e2e
|
||||||
|
|
||||||
- name: Clean up Docker resources
|
- name: Clean up Docker resources
|
||||||
|
|
|
||||||
7
.gitignore
vendored
7
.gitignore
vendored
|
|
@ -144,3 +144,10 @@ vite.config.ts.timestamp-*
|
||||||
Claude.md
|
Claude.md
|
||||||
.claude
|
.claude
|
||||||
.reference
|
.reference
|
||||||
|
|
||||||
|
# Build files
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
|
||||||
|
# Jetbrains IDEs
|
||||||
|
.idea/**
|
||||||
|
|
|
||||||
30
.oxlintrc.json
Normal file
30
.oxlintrc.json
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": [
|
||||||
|
"node",
|
||||||
|
"promise",
|
||||||
|
"eslint",
|
||||||
|
"unicorn",
|
||||||
|
"oxc",
|
||||||
|
"import"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"browser": false,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"rules": {
|
||||||
|
"eslint/no-console": "error",
|
||||||
|
"eslint/no-empty": "error",
|
||||||
|
"eslint/no-undef": "error"
|
||||||
|
},
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.spec.js"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"eslint/no-console": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
553
README.md
553
README.md
|
|
@ -1,49 +1,146 @@
|
||||||
|

|
||||||
|
|
||||||
# Aikido Safe Chain
|
# Aikido Safe Chain
|
||||||
|
|
||||||
The Aikido Safe Chain **prevents developers from installing malware** on their workstations through npm, npx, yarn, pnpm and pnpx. It's **free** to use and does not require any token.
|
[](https://www.npmjs.com/package/@aikidosec/safe-chain)
|
||||||
|
[](https://www.npmjs.com/package/@aikidosec/safe-chain)
|
||||||
|
|
||||||
The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm or pnpx from downloading or running the malware.
|
- ✅ **Block malware on developer laptops and CI/CD**
|
||||||
|
- ✅ **Supports npm and PyPI** more package managers coming
|
||||||
|
- ✅ **Blocks packages newer than 48 hours** without breaking your build
|
||||||
|
- ✅ **Tokenless, free, no build data shared**
|
||||||
|
|
||||||

|
## Need protection beyond npm & PyPI?
|
||||||
|
|
||||||
Aikido Safe Chain works on Node.js version 18 and above and supports the following package managers:
|
[Aikido Endpoint](https://www.aikido.dev/protect/endpoint-protection?utm_source=github.com&utm_medium=referral&utm_campaign=safechain) builds on Safe Chain, extending package and extension security across more ecosystems: **npm**, **PyPI**, **Maven**, **NuGet**, **VS Code**, **Open VSX** - (Cursor, Windsurf, Kiro, Vs Codium, ...), **Chrome extensions**, **Skills.sh AI skills** and more.
|
||||||
|
|
||||||
- ✅ **npm**
|
Get centralized policy management, request-and-approval workflows, and visibility across every developer workstation in your org. Powered by the same Aikido Intel feed. Deploy it manually or manage it through your MDM tool (Jamf, Fleet, or Iru).
|
||||||
- ✅ **npx**
|
|
||||||
- ✅ **yarn**
|
---
|
||||||
- ✅ **pnpm**
|
|
||||||
- ✅ **pnpx**
|
Aikido Safe Chain supports the following package managers:
|
||||||
- 🚧 **bun** Coming soon
|
|
||||||
|
- 📦 **npm**
|
||||||
|
- 📦 **npx**
|
||||||
|
- 📦 **yarn**
|
||||||
|
- 📦 **pnpm**
|
||||||
|
- 📦 **pnpx**
|
||||||
|
- 📦 **rush**
|
||||||
|
- 📦 **rushx**
|
||||||
|
- 📦 **bun**
|
||||||
|
- 📦 **bunx**
|
||||||
|
- 📦 **pip**
|
||||||
|
- 📦 **pip3**
|
||||||
|
- 📦 **uv**
|
||||||
|
- 📦 **poetry**
|
||||||
|
- 📦 **uvx**
|
||||||
|
- 📦 **pipx**
|
||||||
|
- 📦 **pdm**
|
||||||
|
|
||||||
# Usage
|
# Usage
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
Installing the Aikido Safe Chain is easy. You just need 3 simple steps:
|
Installing the Aikido Safe Chain is easy with our one-line installer.
|
||||||
|
|
||||||
|
### Unix/Linux/macOS
|
||||||
|
|
||||||
1. **Install the Aikido Safe Chain package globally** using npm:
|
|
||||||
```shell
|
```shell
|
||||||
npm install -g @aikidosec/safe-chain
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh
|
||||||
```
|
```
|
||||||
2. **Setup the shell integration** by running:
|
|
||||||
|
### Windows (PowerShell)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1" -UseBasicParsing)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Pinning to a specific version
|
||||||
|
|
||||||
|
To install a specific version instead of the latest, replace `latest` with the version number in the URL (available from version 1.3.2 onwards):
|
||||||
|
|
||||||
|
**Unix/Linux/macOS:**
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
safe-chain setup
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/x.x.x/install-safe-chain.sh | sh
|
||||||
```
|
```
|
||||||
3. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
|
||||||
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm and pnpx are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
**Windows (PowerShell):**
|
||||||
4. **Verify the installation** by running:
|
|
||||||
|
```powershell
|
||||||
|
iex (iwr "https://github.com/AikidoSec/safe-chain/releases/download/x.x.x/install-safe-chain.ps1" -UseBasicParsing)
|
||||||
|
```
|
||||||
|
|
||||||
|
You can find all available versions on the [releases page](https://github.com/AikidoSec/safe-chain/releases).
|
||||||
|
|
||||||
|
### Verify the installation
|
||||||
|
|
||||||
|
1. **❗Restart your terminal** to start using the Aikido Safe Chain.
|
||||||
|
- This step is crucial as it ensures that the shell aliases for npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, pip, pip3, poetry, uv, uvx, pipx and pdm are loaded correctly. If you do not restart your terminal, the aliases will not be available.
|
||||||
|
|
||||||
|
2. **Verify the installation** by running the verification command:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm safe-chain-verify
|
||||||
|
pnpm safe-chain-verify
|
||||||
|
pip safe-chain-verify
|
||||||
|
uv safe-chain-verify
|
||||||
|
|
||||||
|
# Any other supported package manager: {packagemanager} safe-chain-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
- The output should display "OK: Safe-chain works!" confirming that Aikido Safe Chain is properly installed and running.
|
||||||
|
|
||||||
|
3. **(Optional) Test malware blocking** by attempting to install a test package:
|
||||||
|
|
||||||
|
For JavaScript/Node.js:
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install safe-chain-test
|
npm install safe-chain-test
|
||||||
```
|
```
|
||||||
- The output should show that Aikido Safe Chain is blocking the installation of this package as it is flagged as malware.
|
|
||||||
|
|
||||||
When running `npm`, `npx`, `yarn`, `pnpm` or `pnpx` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. If any malware is detected, it will prompt you to exit the command.
|
For Python:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
pip3 install safe-chain-pi-test
|
||||||
|
```
|
||||||
|
|
||||||
|
- The output should show that Aikido Safe Chain is blocking the installation of these test packages as they are flagged as malware.
|
||||||
|
|
||||||
|
When running `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `rush`, `rushx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `uvx`, `poetry`, `pipx` and `pdm` commands, the Aikido Safe Chain will automatically check for malware in the packages you are trying to install. It also intercepts Python module invocations for pip when available (e.g., `python -m pip install ...`, `python3 -m pip download ...`). If any malware is detected, it will prompt you to exit the command.
|
||||||
|
|
||||||
|
You can check the installed version by running:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
safe-chain --version
|
||||||
|
```
|
||||||
|
|
||||||
## How it works
|
## How it works
|
||||||
|
|
||||||
The Aikido Safe Chain works by intercepting the npm, npx, yarn, pnpm and pnpx commands and verifying the packages against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**.
|
### Malware Blocking
|
||||||
|
|
||||||
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm and pnpx commands. It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which perform malware checks before executing the original commands. We currently support:
|
The Aikido Safe Chain works by running a lightweight proxy server that intercepts package downloads from the npm registry and PyPI. When you run npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, pip, pip3, uv, uvx, poetry, pipx or pdm commands, all package downloads are routed through this local proxy, which verifies packages in real-time against **[Aikido Intel - Open Sources Threat Intelligence](https://intel.aikido.dev/?tab=malware)**. If malware is detected in any package (including deep dependencies), the proxy blocks the download before the malicious code reaches your machine.
|
||||||
|
|
||||||
|
### Minimum package age
|
||||||
|
|
||||||
|
Safe Chain applies minimum package age checks to supported ecosystems.
|
||||||
|
|
||||||
|
Current enforcement differs by ecosystem:
|
||||||
|
|
||||||
|
- npm-based package managers:
|
||||||
|
- during normal package resolution, Safe Chain suppresses versions that are newer than the configured minimum age from the package metadata returned by the registry
|
||||||
|
- for direct package download requests that bypass that metadata flow, Safe Chain can block the request itself using a cached list of newly released packages
|
||||||
|
- Python package managers:
|
||||||
|
- during package resolution, Safe Chain suppresses too-young files and releases from PyPI metadata responses
|
||||||
|
- for direct package download requests that bypass that metadata flow, Safe Chain can block the request itself using a cached list of newly released packages
|
||||||
|
|
||||||
|
By default, the minimum package age is 48 hours. This provides an additional security layer during the critical period when newly published packages are most vulnerable to containing undetected threats. You can configure this threshold or bypass this protection entirely - see the [Minimum Package Age Configuration](#minimum-package-age) section below.
|
||||||
|
|
||||||
|
### Shell Integration
|
||||||
|
|
||||||
|
The Aikido Safe Chain integrates with your shell to provide a seamless experience when using npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, and Python package managers (pip, uv, uvx, poetry, pipx, pdm). It sets up aliases for these commands so that they are wrapped by the Aikido Safe Chain commands, which manage the proxy server before executing the original commands. We currently support:
|
||||||
|
|
||||||
- ✅ **Bash**
|
- ✅ **Bash**
|
||||||
- ✅ **Zsh**
|
- ✅ **Zsh**
|
||||||
|
|
@ -51,37 +148,419 @@ The Aikido Safe Chain integrates with your shell to provide a seamless experienc
|
||||||
- ✅ **PowerShell**
|
- ✅ **PowerShell**
|
||||||
- ✅ **PowerShell Core**
|
- ✅ **PowerShell Core**
|
||||||
|
|
||||||
More information about the shell integration can be found in the [shell integration documentation](docs/shell-integration.md).
|
More information about the shell integration can be found in the [shell integration documentation](https://github.com/AikidoSec/safe-chain/blob/main/docs/shell-integration.md).
|
||||||
|
|
||||||
## Uninstallation
|
## Uninstallation
|
||||||
|
|
||||||
To uninstall the Aikido Safe Chain, you can run the following command:
|
To uninstall the Aikido Safe Chain, use our one-line uninstaller:
|
||||||
|
|
||||||
|
### Unix/Linux/macOS
|
||||||
|
|
||||||
1. **Remove all aliases from your shell** by running:
|
|
||||||
```shell
|
```shell
|
||||||
safe-chain teardown
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/uninstall-safe-chain.sh | sh
|
||||||
```
|
```
|
||||||
2. **Uninstall the Aikido Safe Chain package** using npm:
|
|
||||||
```shell
|
### Windows (PowerShell)
|
||||||
npm uninstall -g @aikidosec/safe-chain
|
|
||||||
|
```powershell
|
||||||
|
iex (iwr "https://github.com/AikidoSec/safe-chain/releases/latest/download/uninstall-safe-chain.ps1" -UseBasicParsing)
|
||||||
```
|
```
|
||||||
3. **❗Restart your terminal** to remove the aliases.
|
|
||||||
|
**❗Restart your terminal** after uninstalling to ensure all aliases are removed.
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
## Malware Action
|
## Logging
|
||||||
|
|
||||||
You can control how Aikido Safe Chain responds when malware is detected using the `--safe-chain-malware-action` flag:
|
You can control the output from Aikido Safe Chain using the `--safe-chain-logging` flag or the `SAFE_CHAIN_LOGGING` environment variable.
|
||||||
|
|
||||||
- `--safe-chain-malware-action=block` (**default**) - Automatically blocks installation and exits with an error when malware is detected
|
### Configuration Options
|
||||||
- `--safe-chain-malware-action=prompt` - Prompts the user to decide whether to continue despite the malware detection
|
|
||||||
|
|
||||||
Example usage:
|
You can set the logging level through multiple sources (in order of priority):
|
||||||
|
|
||||||
|
1. **CLI Argument** (highest priority):
|
||||||
|
- `--safe-chain-logging=silent` - Suppresses all Aikido Safe Chain output except when malware is blocked. The package manager output is written to stdout as normal, and Safe Chain only writes a short message if it has blocked malware and causes the process to exit.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
npm install suspicious-package --safe-chain-malware-action=prompt
|
npm install express --safe-chain-logging=silent
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--safe-chain-logging=verbose` - Enables detailed diagnostic output from Aikido Safe Chain. Useful for troubleshooting issues or understanding what Safe Chain is doing behind the scenes.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install express --safe-chain-logging=verbose
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Environment Variable**:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export SAFE_CHAIN_LOGGING=verbose
|
||||||
|
npm install express
|
||||||
|
```
|
||||||
|
|
||||||
|
Valid values: `silent`, `normal`, `verbose`
|
||||||
|
|
||||||
|
This is useful for setting a default logging level for all package manager commands in your terminal session or CI/CD environment.
|
||||||
|
|
||||||
|
## Minimum Package Age
|
||||||
|
|
||||||
|
You can configure how long packages must exist before Safe Chain allows their installation. By default, packages must be at least 48 hours old before they can be installed.
|
||||||
|
|
||||||
|
For npm-based package managers, this check currently has two enforcement modes:
|
||||||
|
|
||||||
|
- Safe Chain suppresses too-young versions from package metadata during normal dependency resolution.
|
||||||
|
- Safe Chain blocks direct package download requests when they are matched against the cached newly released packages list.
|
||||||
|
|
||||||
|
For Python package managers, this check currently has two enforcement modes:
|
||||||
|
|
||||||
|
- Safe Chain suppresses too-young files and releases from PyPI metadata during dependency resolution.
|
||||||
|
- Safe Chain blocks direct package download requests when they are matched against the cached newly released packages list.
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
You can set the minimum package age through multiple sources (in order of priority):
|
||||||
|
|
||||||
|
1. **CLI Argument** (highest priority):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install express --safe-chain-minimum-package-age-hours=48
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Environment Variable**:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS=48
|
||||||
|
npm install express
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Config File** (`~/.safe-chain/config.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"minimumPackageAgeHours": 48
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Excluding Packages
|
||||||
|
|
||||||
|
Exclude trusted packages from minimum age filtering via environment variable or config file (both are merged). Use `@scope/*` to trust all packages from an organization:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS="@aikidosec/*"
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"npm": {
|
||||||
|
"minimumPackageAgeExclusions": ["@aikidosec/*"]
|
||||||
|
},
|
||||||
|
"pip": {
|
||||||
|
"minimumPackageAgeExclusions": ["requests"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Registries
|
||||||
|
|
||||||
|
Configure Safe Chain to scan packages from custom or private registries.
|
||||||
|
|
||||||
|
Supported ecosystems:
|
||||||
|
|
||||||
|
- Node.js
|
||||||
|
- Python
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
You can set custom registries through environment variable or config file. Both sources are merged together.
|
||||||
|
|
||||||
|
1. **Environment Variable** (comma-separated):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export SAFE_CHAIN_NPM_CUSTOM_REGISTRIES="npm.company.com,registry.internal.net"
|
||||||
|
export SAFE_CHAIN_PIP_CUSTOM_REGISTRIES="pip.company.com,registry.internal.net"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Config File** (`~/.safe-chain/config.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"npm": {
|
||||||
|
"customRegistries": ["npm.company.com", "registry.internal.net"]
|
||||||
|
},
|
||||||
|
"pip": {
|
||||||
|
"customRegistries": ["pip.company.com", "registry.internal.net"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## PYPI Configuration File
|
||||||
|
|
||||||
|
If you rely on a `pip.conf` file for pip configuration you must point pip at it explicitly via the `PIP_CONFIG_FILE` environment variable so Safe Chain can merge it.
|
||||||
|
|
||||||
|
Safe Chain runs pip behind its MITM proxy and writes a temporary pip configuration file to inject its certificate and proxy settings. When `PIP_CONFIG_FILE` is set, Safe Chain merges its settings into a copy of your file (your original file is never modified) so your `index-url`, credentials, and other options are preserved. When `PIP_CONFIG_FILE` is not set, pip's user-level config (e.g. `~/.config/pip/pip.conf`) might be overridden by Safe Chain's temporary file and your settings will not be picked up.
|
||||||
|
|
||||||
|
## Malware List Base URL
|
||||||
|
|
||||||
|
Configure Safe Chain to fetch malware databases and new packages lists from a custom mirror URL. This allows you to host your own copy of the Aikido malware database.
|
||||||
|
|
||||||
|
### Configuration Options
|
||||||
|
|
||||||
|
You can set the malware list base URL through multiple sources (in order of priority):
|
||||||
|
|
||||||
|
1. **CLI Argument** (highest priority):
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npm install express --safe-chain-malware-list-base-url=https://your-mirror.com
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Environment Variable**:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
export SAFE_CHAIN_MALWARE_LIST_BASE_URL=https://your-mirror.com
|
||||||
|
npm install express
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Config File** (`~/.safe-chain/config.json`):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"malwareListBaseUrl": "https://your-mirror.com"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The base URL should point to a server that mirrors the structure of `https://malware-list.aikido.dev/`, including the following paths:
|
||||||
|
- `/malware_predictions.json` (JavaScript ecosystem malware database)
|
||||||
|
- `/malware_pypi.json` (Python ecosystem malware database)
|
||||||
|
- `/releases/npm.json` (JavaScript new packages list)
|
||||||
|
- `/releases/pypi.json` (Python new packages list)
|
||||||
|
|
||||||
|
## Custom Install Directory
|
||||||
|
|
||||||
|
By default, Safe Chain installs itself into `~/.safe-chain`. You can change this by passing an explicit install directory to the installer. This is useful for system-wide installations (e.g. inside a Docker image) or when you need to avoid conflicts with other tools.
|
||||||
|
|
||||||
|
When set, all Safe Chain data (binary, shims, scripts, config) is placed under the custom directory instead of `~/.safe-chain`.
|
||||||
|
|
||||||
|
### Unix/Linux/macOS
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --install-dir /usr/local/.safe-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -InstallDir 'C:\ProgramData\safe-chain'"
|
||||||
```
|
```
|
||||||
|
|
||||||
# Usage in CI/CD
|
# Usage in CI/CD
|
||||||
|
|
||||||
🚧 Support for CI/CD environments is coming soon...
|
You can protect your CI/CD pipelines from malicious packages by integrating Aikido Safe Chain into your build process. This ensures that any packages installed during your automated builds are checked for malware before installation.
|
||||||
|
|
||||||
|
## Installation for CI/CD
|
||||||
|
|
||||||
|
Use the `--ci` flag to automatically configure Aikido Safe Chain for CI/CD environments. This sets up executable shims in the PATH instead of shell aliases.
|
||||||
|
|
||||||
|
### Unix/Linux/macOS (GitHub Actions, Azure Pipelines, etc.)
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows (Azure Pipelines, etc.)
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
iex "& { $(iwr 'https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.ps1' -UseBasicParsing) } -ci"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
|
||||||
|
- ✅ **GitHub Actions**
|
||||||
|
- ✅ **Azure Pipelines**
|
||||||
|
- ✅ **CircleCI**
|
||||||
|
- ✅ **Jenkins**
|
||||||
|
- ✅ **Bitbucket Pipelines**
|
||||||
|
- ✅ **GitLab Pipelines**
|
||||||
|
|
||||||
|
## GitHub Actions Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "22"
|
||||||
|
cache: "npm"
|
||||||
|
|
||||||
|
- name: Install safe-chain
|
||||||
|
run: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
```
|
||||||
|
|
||||||
|
## Azure DevOps Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
- task: NodeTool@0
|
||||||
|
inputs:
|
||||||
|
versionSpec: "22.x"
|
||||||
|
displayName: "Install Node.js"
|
||||||
|
|
||||||
|
- script: curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
displayName: "Install safe-chain"
|
||||||
|
|
||||||
|
- script: npm ci
|
||||||
|
displayName: "Install dependencies"
|
||||||
|
```
|
||||||
|
|
||||||
|
## CircleCI Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
version: 2.1
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
docker:
|
||||||
|
- image: cimg/node:lts
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: |
|
||||||
|
curl -fsSL https://raw.githubusercontent.com/AikidoSec/safe-chain/main/install-scripts/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
- run: npm ci
|
||||||
|
workflows:
|
||||||
|
build_and_test:
|
||||||
|
jobs:
|
||||||
|
- build
|
||||||
|
```
|
||||||
|
|
||||||
|
## Jenkins Example
|
||||||
|
|
||||||
|
Note: This assumes Node.js and npm are installed on the Jenkins agent.
|
||||||
|
|
||||||
|
```groovy
|
||||||
|
pipeline {
|
||||||
|
agent any
|
||||||
|
|
||||||
|
environment {
|
||||||
|
// Jenkins does not automatically persist PATH updates from setup-ci,
|
||||||
|
// so add the shims + binary directory explicitly for all stages.
|
||||||
|
// If you installed into a custom directory, replace ~/.safe-chain with that path here.
|
||||||
|
PATH = "${env.HOME}/.safe-chain/shims:${env.HOME}/.safe-chain/bin:${env.PATH}"
|
||||||
|
}
|
||||||
|
|
||||||
|
stages {
|
||||||
|
stage('Install safe-chain') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# Install Safe Chain for CI
|
||||||
|
curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Install project dependencies etc...') {
|
||||||
|
steps {
|
||||||
|
sh '''
|
||||||
|
set -euo pipefail
|
||||||
|
npm ci
|
||||||
|
'''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Bitbucket Pipelines Example
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
image: node:22
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- step:
|
||||||
|
name: Install
|
||||||
|
script:
|
||||||
|
- curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
- export PATH=~/.safe-chain/shims:~/.safe-chain/bin:$PATH
|
||||||
|
- npm ci
|
||||||
|
```
|
||||||
|
|
||||||
|
After setup, all subsequent package manager commands in your CI pipeline will automatically be protected by Aikido Safe Chain's malware detection.
|
||||||
|
|
||||||
|
## GitLab Pipelines Example
|
||||||
|
|
||||||
|
To add safe-chain in GitLab pipelines, you need to install it in the image running the pipeline. This can be done by:
|
||||||
|
|
||||||
|
1. Define a dockerfile to run your build
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
FROM node:lts
|
||||||
|
|
||||||
|
# Install safe-chain
|
||||||
|
RUN curl -fsSL https://github.com/AikidoSec/safe-chain/releases/latest/download/install-safe-chain.sh | sh -s -- --ci
|
||||||
|
|
||||||
|
# Add safe-chain to PATH (update paths if you used a custom install dir)
|
||||||
|
ENV PATH="/root/.safe-chain/shims:/root/.safe-chain/bin:${PATH}"
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Build the Docker image in your CI pipeline
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
build-image:
|
||||||
|
stage: build-image
|
||||||
|
image: docker:latest
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
script:
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
- docker build -t $CI_REGISTRY_IMAGE:latest .
|
||||||
|
- docker push $CI_REGISTRY_IMAGE:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Use the image in your pipeline:
|
||||||
|
```yaml
|
||||||
|
npm-ci:
|
||||||
|
stage: install
|
||||||
|
image: $CI_REGISTRY_IMAGE:latest
|
||||||
|
script:
|
||||||
|
- npm ci
|
||||||
|
```
|
||||||
|
|
||||||
|
The full pipeline for this example looks like this:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
stages:
|
||||||
|
- build-image
|
||||||
|
- install
|
||||||
|
|
||||||
|
build-image:
|
||||||
|
stage: build-image
|
||||||
|
image: docker:latest
|
||||||
|
services:
|
||||||
|
- docker:dind
|
||||||
|
script:
|
||||||
|
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||||
|
- docker build -t $CI_REGISTRY_IMAGE:latest .
|
||||||
|
- docker push $CI_REGISTRY_IMAGE:latest
|
||||||
|
|
||||||
|
npm-ci:
|
||||||
|
stage: install
|
||||||
|
image: $CI_REGISTRY_IMAGE:latest
|
||||||
|
script:
|
||||||
|
- npm ci
|
||||||
|
```
|
||||||
|
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
Having issues? See the [Troubleshooting Guide](./docs/troubleshooting) for help with common problems.
|
||||||
|
|
||||||
|
# Report Issues
|
||||||
|
|
||||||
|
If you encounter problems:
|
||||||
|
|
||||||
|
1. Visit [GitHub Issues](https://github.com/AikidoSec/safe-chain/issues)
|
||||||
|
2. Include:
|
||||||
|
* Operating system and version
|
||||||
|
* Shell type and version
|
||||||
|
* `safe-chain --version` output
|
||||||
|
* Output from verification commands
|
||||||
|
* Verbose logs of the failing command (add the `--safe-chain-logging=verbose` argument)
|
||||||
|
|
|
||||||
139
build.js
Normal file
139
build.js
Normal file
|
|
@ -0,0 +1,139 @@
|
||||||
|
import { build } from "esbuild";
|
||||||
|
import { mkdir, cp, rm, readFile, writeFile, stat } from "node:fs/promises";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import { resolve } from "node:path";
|
||||||
|
|
||||||
|
const target = process.argv[2];
|
||||||
|
if (!target) {
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Usage: node build.js <target>");
|
||||||
|
// eslint-disable-next-line no-console
|
||||||
|
console.error("Example: node build.js node22-macos-arm64");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
(async function main() {
|
||||||
|
const startBuildTime = performance.now();
|
||||||
|
|
||||||
|
await clearOutputFolder();
|
||||||
|
console.log("- Cleared output folder ✅")
|
||||||
|
|
||||||
|
// Esbuild creates a single safe-chain.cjs with all dependencies included
|
||||||
|
await bundleSafeChain();
|
||||||
|
console.log("- Bundled safe-chain into safe-chain.cjs (es-build) ✅")
|
||||||
|
|
||||||
|
// Copy assets that need to be included in the binary
|
||||||
|
// - All shell scripts that are used to setup safe-chain
|
||||||
|
// - Certifi because it contains static root certs for Python
|
||||||
|
// - Package.json for its metadata (package name, version, ...)
|
||||||
|
await copyShellScripts();
|
||||||
|
await copyCertifi();
|
||||||
|
await copyAndModifyPackageJson();
|
||||||
|
console.log("- Copied auxiliary resources (shell, package.json,...) ✅")
|
||||||
|
|
||||||
|
// Creates a single binary with safe-chain.cjs and the copied assets
|
||||||
|
await buildSafeChainBinary(target);
|
||||||
|
console.log(`- Built safe-chain binary for ${target} (pkg) ✅`)
|
||||||
|
|
||||||
|
|
||||||
|
const totalBuildTime = (performance.now() - startBuildTime)/1000;
|
||||||
|
const totalSizeInMb = (await stat("./dist/safe-chain" + (process.platform === "win32" ? ".exe" : ""))).size / (1024*1024);
|
||||||
|
console.log(`🏁 Finished build in ${totalBuildTime.toFixed(2)}s, total build size: ${totalSizeInMb.toFixed(2)}MB`);
|
||||||
|
})();
|
||||||
|
|
||||||
|
async function clearOutputFolder() {
|
||||||
|
await rm("./build", { recursive: true, force: true });
|
||||||
|
await mkdir("./build");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function bundleSafeChain() {
|
||||||
|
await build({
|
||||||
|
entryPoints: ["./packages/safe-chain/bin/safe-chain.js"],
|
||||||
|
bundle: true,
|
||||||
|
platform: "node",
|
||||||
|
target: "node24",
|
||||||
|
outfile: "./build/bin/safe-chain.cjs",
|
||||||
|
external: ["certifi"],
|
||||||
|
});
|
||||||
|
|
||||||
|
let bundledContent = await readFile("./build/bin/safe-chain.cjs", "utf-8");
|
||||||
|
|
||||||
|
await writeFile("./build/bin/safe-chain.cjs", bundledContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyShellScripts() {
|
||||||
|
await mkdir("./build/bin/startup-scripts", { recursive: true });
|
||||||
|
await cp(
|
||||||
|
"./packages/safe-chain/src/shell-integration/startup-scripts/",
|
||||||
|
"./build/bin/startup-scripts",
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
|
await mkdir("./build/bin/path-wrappers", { recursive: true });
|
||||||
|
await cp(
|
||||||
|
"./packages/safe-chain/src/shell-integration/path-wrappers/",
|
||||||
|
"./build/bin/path-wrappers",
|
||||||
|
{ recursive: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyCertifi() {
|
||||||
|
await mkdir("./build/node_modules/certifi", { recursive: true });
|
||||||
|
await cp("./node_modules/certifi/", "./build/node_modules/certifi", {
|
||||||
|
recursive: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
async function copyAndModifyPackageJson() {
|
||||||
|
const packageJsonContent = await readFile(
|
||||||
|
"./packages/safe-chain/package.json",
|
||||||
|
"utf-8"
|
||||||
|
);
|
||||||
|
const packageJson = JSON.parse(packageJsonContent);
|
||||||
|
|
||||||
|
delete packageJson.main;
|
||||||
|
delete packageJson.scripts;
|
||||||
|
delete packageJson.exports;
|
||||||
|
delete packageJson.dependencies;
|
||||||
|
delete packageJson.devDependencies;
|
||||||
|
|
||||||
|
packageJson.bin = {
|
||||||
|
"safe-chain": "bin/safe-chain.cjs",
|
||||||
|
};
|
||||||
|
packageJson.type = "commonjs";
|
||||||
|
packageJson.pkg = {
|
||||||
|
outputPath: "dist",
|
||||||
|
assets: [
|
||||||
|
"node_modules/certifi/**/*",
|
||||||
|
"bin/startup-scripts/**/*",
|
||||||
|
"bin/path-wrappers/**/*",
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeFile("./build/package.json", JSON.stringify(packageJson, null, 2));
|
||||||
|
|
||||||
|
return packageJson;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildSafeChainBinary(target) {
|
||||||
|
return new Promise((promiseResolve, reject) => {
|
||||||
|
// Use .cmd on Windows, resolve to absolute path for cross-platform compatibility
|
||||||
|
const pkgBin = process.platform === "win32"
|
||||||
|
? resolve("node_modules/.bin/pkg.cmd")
|
||||||
|
: resolve("node_modules/.bin/pkg");
|
||||||
|
|
||||||
|
let pkgArgs = [];
|
||||||
|
|
||||||
|
pkgArgs = pkgArgs.concat(["./build/package.json", "-t", target]);
|
||||||
|
const pkg = spawn(pkgBin, pkgArgs, {
|
||||||
|
stdio: "inherit",
|
||||||
|
shell: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
pkg.on("close", (code) => {
|
||||||
|
if (code !== 0) {
|
||||||
|
reject(new Error(`pkg process exited with code ${code}`));
|
||||||
|
} else {
|
||||||
|
promiseResolve();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
25
docs/Release.md
Normal file
25
docs/Release.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# Release Guide
|
||||||
|
|
||||||
|
## Steps
|
||||||
|
|
||||||
|
### 1. Create and push a version tag
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag 1.0.0
|
||||||
|
git push origin 1.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
This triggers the build pipeline, which compiles binaries for all platforms and creates a draft GitHub release.
|
||||||
|
|
||||||
|
### 2. Wait for artifacts to build
|
||||||
|
|
||||||
|
Monitor the [Actions tab](https://github.com/AikidoSec/safe-chain/actions) until the `Create Release` workflow completes.
|
||||||
|
|
||||||
|
### 3. Publish the GitHub release
|
||||||
|
|
||||||
|
1. Go to the [Releases page](https://github.com/AikidoSec/safe-chain/releases)
|
||||||
|
2. Open the draft release created for your tag
|
||||||
|
3. Add release notes
|
||||||
|
4. Click **Publish release**
|
||||||
|
|
||||||
|
Publishing the release automatically triggers an npm publish. Pre-release versions (e.g. `1.0.0-beta`) are published to npm under a tag matching the pre-release identifier (e.g. `beta`). Stable versions are published to the `latest` tag.
|
||||||
151
docs/banner.svg
Normal file
151
docs/banner.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 73 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
BIN
docs/safe-package-manager-demo.png
Normal file
BIN
docs/safe-package-manager-demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
|
|
@ -2,21 +2,21 @@
|
||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`) with Aikido's security scanning functionality. This is achieved by adding shell aliases that redirect these commands to their Aikido-wrapped equivalents.
|
The shell integration automatically wraps common package manager commands (`npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `rush`, `rushx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `uvx`, `poetry`, `pipx`) with Aikido's security scanning functionality. It also intercepts Python module invocations for pip when available: `python -m pip`, `python -m pip3`, `python3 -m pip`, `python3 -m pip3`. This is achieved by sourcing startup scripts that define shell functions to wrap these commands with their Aikido-protected equivalents.
|
||||||
|
|
||||||
## Supported Shells
|
## Supported Shells
|
||||||
|
|
||||||
Aikido Safe Chain supports integration with the following shells.
|
Aikido Safe Chain supports integration with the following shells.
|
||||||
|
|
||||||
| Shell | Startup File | Alias Format |
|
| Shell | Startup File |
|
||||||
| ---------------------- | ---------------------------- | -------------------------- |
|
| ---------------------- | ---------------------------- |
|
||||||
| **Bash** | `~/.bashrc` | `alias npm='aikido-npm'` |
|
| **Bash** | `~/.bashrc` |
|
||||||
| **Zsh** | `~/.zshrc` | `alias npm='aikido-npm'` |
|
| **Zsh** | `~/.zshrc` |
|
||||||
| **Fish** | `~/.config/fish/config.fish` | `alias npm "aikido-npm"` |
|
| **Fish** | `~/.config/fish/config.fish` |
|
||||||
| **PowerShell Core** | `$PROFILE` | `Set-Alias npm aikido-npm` |
|
| **PowerShell Core** | `$PROFILE` |
|
||||||
| **Windows PowerShell** | `$PROFILE` | `Set-Alias npm aikido-npm` |
|
| **Windows PowerShell** | `$PROFILE` |
|
||||||
|
|
||||||
## Commands
|
## Setup Commands
|
||||||
|
|
||||||
### Setup Shell Integration
|
### Setup Shell Integration
|
||||||
|
|
||||||
|
|
@ -26,10 +26,12 @@ safe-chain setup
|
||||||
|
|
||||||
This command:
|
This command:
|
||||||
|
|
||||||
|
- Copies necessary startup scripts to Safe Chain's installation directory (`~/.safe-chain/scripts`)
|
||||||
- Detects all supported shells on your system
|
- Detects all supported shells on your system
|
||||||
- Adds aliases for `npm`, `npx`, `yarn`, `pnpm` and `pnpx` to each shell's startup file
|
- Sources each shell's startup file to add Safe Chain functions for `npm`, `npx`, `yarn`, `pnpm`, `pnpx`, `rush`, `rushx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `uvx`, `poetry` and `pipx`
|
||||||
|
- Adds lightweight interceptors so `python -m pip[...]` and `python3 -m pip[...]` route through Safe Chain when invoked by name
|
||||||
|
|
||||||
❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the aliases are loaded correctly.
|
❗ After running this command, **you must restart your terminal** for the changes to take effect. This ensures that the startup scripts are sourced correctly.
|
||||||
|
|
||||||
### Remove Shell Integration
|
### Remove Shell Integration
|
||||||
|
|
||||||
|
|
@ -40,13 +42,13 @@ safe-chain teardown
|
||||||
This command:
|
This command:
|
||||||
|
|
||||||
- Detects all supported shells on your system
|
- Detects all supported shells on your system
|
||||||
- Removes Aikido aliases from each shell's startup file
|
- Removes the Safe Chain scripts from each shell's startup file, restoring the original commands
|
||||||
|
|
||||||
❗ After running this command, **you must restart your terminal** to restore the original commands.
|
❗ After running this command, **you must restart your terminal** to restore the original commands.
|
||||||
|
|
||||||
## File Locations
|
## File Locations
|
||||||
|
|
||||||
The system modifies the following files based on your shell configuration:
|
The system modifies the following files to source Safe Chain startup scripts:
|
||||||
|
|
||||||
### Unix/Linux/macOS
|
### Unix/Linux/macOS
|
||||||
|
|
||||||
|
|
@ -64,45 +66,84 @@ The system modifies the following files based on your shell configuration:
|
||||||
|
|
||||||
### Common Issues
|
### Common Issues
|
||||||
|
|
||||||
**Aliases not working after setup:**
|
**Shell functions not working after setup:**
|
||||||
|
|
||||||
- Make sure to restart your terminal
|
- Make sure to restart your terminal
|
||||||
- Check that the startup file was actually modified
|
- Check that the startup file was modified to source Safe Chain scripts
|
||||||
|
- Check the sourced file exists at `~/.safe-chain/scripts/`
|
||||||
- Verify your shell is reading the correct startup file
|
- Verify your shell is reading the correct startup file
|
||||||
|
|
||||||
**Getting 'command not found: aikido-npm' error:**
|
**Getting 'command not found: aikido-npm' error:**
|
||||||
|
|
||||||
This means the aliases are working but the Aikido commands aren't installed or available in your PATH:
|
This means the shell functions are working but the Aikido commands aren't installed or available in your PATH:
|
||||||
|
|
||||||
- Make sure Aikido Safe Chain is properly installed on your system
|
- Make sure Aikido Safe Chain is properly installed on your system
|
||||||
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` and `aikido-pnpx` commands exist
|
- Verify the `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm`, `aikido-pnpx`, `aikido-rush`, `aikido-rushx`, `aikido-bun`, `aikido-bunx`, `aikido-pip`, `aikido-pip3`, `aikido-uv`, `aikido-uvx`, `aikido-poetry` and `aikido-pipx` commands exist
|
||||||
- Check that these commands are in your system's PATH
|
- Check that these commands are in your system's PATH
|
||||||
|
|
||||||
### Manual Verification
|
### Manual Verification
|
||||||
|
|
||||||
To verify the integration is working, follow these steps:
|
To verify the integration is working, follow these steps:
|
||||||
|
|
||||||
1. **Check if aliases were added to your shell startup file:**
|
1. **Check if startup scripts were sourced in your shell startup file:**
|
||||||
|
|
||||||
- **For Bash**: Open `~/.bashrc` in your text editor
|
- **For Bash**: Open `~/.bashrc` in your text editor
|
||||||
- **For Zsh**: Open `~/.zshrc` in your text editor
|
- **For Zsh**: Open `~/.zshrc` in your text editor
|
||||||
- **For Fish**: Open `~/.config/fish/config.fish` in your text editor
|
- **For Fish**: Open `~/.config/fish/config.fish` in your text editor
|
||||||
- **For PowerShell**: Open your PowerShell profile file (run `$PROFILE` in PowerShell to see the path)
|
- **For PowerShell**: Open your PowerShell profile file (run `$PROFILE` in PowerShell to see the path)
|
||||||
|
|
||||||
Look for lines like:
|
Look for lines that source the Safe Chain startup scripts from `~/.safe-chain/scripts/`
|
||||||
|
|
||||||
- `alias npm='aikido-npm'` (Bash/Zsh)
|
2. **Test that shell functions are active in your terminal:**
|
||||||
- `alias npm "aikido-npm"` (Fish)
|
|
||||||
- `Set-Alias npm aikido-npm` (PowerShell)
|
|
||||||
|
|
||||||
2. **Test that aliases are active in your terminal:**
|
|
||||||
|
|
||||||
After restarting your terminal, run these commands:
|
After restarting your terminal, run these commands:
|
||||||
|
|
||||||
- `which npm` - Should show the path to `aikido-npm` instead of the original npm
|
|
||||||
- `npm --version` - Should show output from the Aikido-wrapped version
|
- `npm --version` - Should show output from the Aikido-wrapped version
|
||||||
- `type npm` - Alternative way to check what command `npm` resolves to
|
- `type npm` - Should show that `npm` is a function
|
||||||
|
|
||||||
3. **If you need to remove aliases manually:**
|
3. **If you need to remove the integration manually:**
|
||||||
|
|
||||||
Edit the same startup file from step 1 and delete any lines containing `aikido-npm`, `aikido-npx`, `aikido-yarn`, `aikido-pnpm` or `aikido-pnpx`.
|
Edit the same startup file from step 1 and delete any lines that source Safe Chain scripts from `~/.safe-chain/scripts/`.
|
||||||
|
|
||||||
|
## Manual Setup
|
||||||
|
|
||||||
|
For advanced users who prefer manual configuration, you can create wrapper functions directly in your shell's startup file. Shell functions take precedence over commands in PATH, so defining an `npm` function will intercept all `npm` calls:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example for Bash/Zsh
|
||||||
|
npm() {
|
||||||
|
if command -v aikido-npm > /dev/null 2>&1; then
|
||||||
|
aikido-npm "$@"
|
||||||
|
else
|
||||||
|
echo "Warning: safe-chain is not installed. npm will run without protection."
|
||||||
|
command npm "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Repeat this pattern for `npx`, `yarn`, `pnpm`, `pnpx`, `rush`, `rushx`, `bun`, `bunx`, `pip`, `pip3`, `uv`, `uvx`, `poetry` and `pipx` using their respective `aikido-*` commands. After adding these functions, restart your terminal to apply the changes.
|
||||||
|
|
||||||
|
To intercept Python module invocations for pip without altering Python itself, you can add small forwarding functions:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Example for Bash/Zsh
|
||||||
|
python() {
|
||||||
|
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
||||||
|
local mod="$2"; shift 2
|
||||||
|
if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi
|
||||||
|
else
|
||||||
|
command python "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
python3() {
|
||||||
|
if [[ "$1" == "-m" && "$2" == pip* ]]; then
|
||||||
|
local mod="$2"; shift 2
|
||||||
|
if [[ "$mod" == "pip3" ]]; then aikido-pip3 "$@"; else aikido-pip "$@"; fi
|
||||||
|
else
|
||||||
|
command python3 "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Limitations: these only apply when invoking `python`/`python3` by name. Absolute paths (e.g., `/usr/bin/python -m pip`) bypass shell functions.
|
||||||
|
|
|
||||||
298
docs/troubleshooting.md
Normal file
298
docs/troubleshooting.md
Normal file
|
|
@ -0,0 +1,298 @@
|
||||||
|
# Troubleshooting
|
||||||
|
|
||||||
|
This guide helps you diagnose and resolve common issues with Aikido Safe Chain.
|
||||||
|
|
||||||
|
## Verification & Diagnostics
|
||||||
|
|
||||||
|
**Check Installation**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check version
|
||||||
|
safe-chain --version
|
||||||
|
```
|
||||||
|
|
||||||
|
**Verify Shell Integration**
|
||||||
|
|
||||||
|
Run the verification command for your package manager:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm safe-chain-verify
|
||||||
|
pnpm safe-chain-verify
|
||||||
|
```
|
||||||
|
|
||||||
|
```
|
||||||
|
Expected output: `OK: Safe-chain works!`
|
||||||
|
```
|
||||||
|
|
||||||
|
**Test Malware Blocking**
|
||||||
|
|
||||||
|
Verify that malware detection is working:
|
||||||
|
```
|
||||||
|
npm install safe-chain-test
|
||||||
|
```
|
||||||
|
|
||||||
|
These test packages are flagged as malware and should be blocked by Safe Chain.
|
||||||
|
|
||||||
|
**If the test package installs successfully instead of being blocked**, see Malware Not Being Blocked below.
|
||||||
|
|
||||||
|
## Logging Options
|
||||||
|
|
||||||
|
Use logging flags or environment variables to get more information:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Verbose mode - detailed diagnostic output for troubleshooting
|
||||||
|
npm install express --safe-chain-logging=verbose
|
||||||
|
|
||||||
|
# Or set it globally for all commands in your session
|
||||||
|
export SAFE_CHAIN_LOGGING=verbose
|
||||||
|
npm install express
|
||||||
|
|
||||||
|
# Silent mode - suppress all output except malware blocking
|
||||||
|
npm install express --safe-chain-logging=silent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Common Issues
|
||||||
|
|
||||||
|
### Malware Not Being Blocked
|
||||||
|
|
||||||
|
**Symptom:** Test malware packages (like `safe-chain-test`) install successfully when they should be blocked
|
||||||
|
|
||||||
|
**Most Common Cause:** The package is cached in your package manager's local store
|
||||||
|
|
||||||
|
Safe-chain blocks malicious packages by intercepting network requests to package registries using its proxy.
|
||||||
|
|
||||||
|
When a package is already cached locally, the package manager skips downloading it from the registry, which bypasses the proxy.
|
||||||
|
|
||||||
|
**Resolution Steps**
|
||||||
|
|
||||||
|
1) Clear your package manager's cache
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# For npm
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
|
# For pnpm
|
||||||
|
pnpm store prune
|
||||||
|
|
||||||
|
# For yarn (classic)
|
||||||
|
yarn cache clean
|
||||||
|
|
||||||
|
# For yarn (berry/v2+)
|
||||||
|
yarn cache clean --all
|
||||||
|
|
||||||
|
# For bun
|
||||||
|
bun pm cache rm
|
||||||
|
```
|
||||||
|
|
||||||
|
2) Clean local installation artifacts:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Remove node_modules if you want a completely fresh install
|
||||||
|
rm -rf node_modules
|
||||||
|
```
|
||||||
|
|
||||||
|
3) Re-test malware blocking:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install safe-chain-test # Should be blocked
|
||||||
|
```
|
||||||
|
|
||||||
|
### Shell Aliases Not Working After Installation
|
||||||
|
|
||||||
|
**Symptom:** Running `npm` shows regular npm instead of safe-chain wrapped version
|
||||||
|
|
||||||
|
**First step:** Restart your terminal (most common fix)
|
||||||
|
|
||||||
|
**Verify it's working:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
type npm
|
||||||
|
```
|
||||||
|
|
||||||
|
Should show: `npm is a function`
|
||||||
|
|
||||||
|
**If still not working:**
|
||||||
|
|
||||||
|
Check that your startup file sources safe-chain scripts from `~/.safe-chain/scripts/`:
|
||||||
|
|
||||||
|
* Bash: `~/.bashrc`
|
||||||
|
* Zsh: `~/.zshrc`
|
||||||
|
* Fish: `~/.config/fish/config.fish`
|
||||||
|
* PowerShell: `$PROFILE`
|
||||||
|
|
||||||
|
### "Command Not Found: safe-chain"
|
||||||
|
|
||||||
|
**Symptom:** Binary not found in PATH
|
||||||
|
|
||||||
|
**First step:** Restart your terminal
|
||||||
|
|
||||||
|
**Check PATH:**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
echo $PATH
|
||||||
|
```
|
||||||
|
|
||||||
|
Should include `~/.safe-chain/bin`
|
||||||
|
|
||||||
|
**If persists:** Re-run the installation script
|
||||||
|
|
||||||
|
### PowerShell Execution Policy Blocks Scripts (Windows)
|
||||||
|
|
||||||
|
**Symptom:** When opening PowerShell, you see an error like:
|
||||||
|
|
||||||
|
```
|
||||||
|
. : File C:\Users\<username>\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 cannot be loaded because
|
||||||
|
running scripts is disabled on this system.
|
||||||
|
CategoryInfo : SecurityError: (:) [], PSSecurityException
|
||||||
|
FullyQualifiedErrorId : UnauthorizedAccess
|
||||||
|
```
|
||||||
|
|
||||||
|
**Cause:** Windows PowerShell's default execution policy (`Restricted`) blocks all script execution, including safe-chain's initialization script that's sourced from your PowerShell profile.
|
||||||
|
|
||||||
|
**Resolution**
|
||||||
|
|
||||||
|
1) Set the execution policy to allow local scripts
|
||||||
|
|
||||||
|
Open PowerShell as Administrator and run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned
|
||||||
|
```
|
||||||
|
|
||||||
|
This allows:
|
||||||
|
|
||||||
|
* Local scripts (like safe-chain's) to run without signing
|
||||||
|
* Downloaded scripts to run only if signed by a trusted publisher
|
||||||
|
|
||||||
|
2) Restart PowerShell and verify the error is resolved.
|
||||||
|
|
||||||
|
> [!IMPORTANT]
|
||||||
|
> `RemoteSigned` is Microsoft's recommended execution policy for client computers. It provides a good balance between security and usability.
|
||||||
|
|
||||||
|
### Shell Aliases Persist After Uninstallation
|
||||||
|
|
||||||
|
**Symptom:** safe-chain commands still active after running uninstall script
|
||||||
|
|
||||||
|
**Steps**
|
||||||
|
|
||||||
|
1. Run `safe-chain teardown` (if binary still exists)
|
||||||
|
2. Restart your terminal
|
||||||
|
3. If still present, manually edit shell config files:
|
||||||
|
* Bash: `~/.bashrc`
|
||||||
|
* Zsh: `~/.zshrc`
|
||||||
|
* Fish: `~/.config/fish/config.fish`
|
||||||
|
* PowerShell: `$PROFILE`
|
||||||
|
4. Remove lines that source scripts from `~/.safe-chain/scripts/`
|
||||||
|
5. Restart terminal again
|
||||||
|
|
||||||
|
## Manual Verification Steps
|
||||||
|
|
||||||
|
### Check Installation Status
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check installation location (helps identify if installed via npm or as standalone binary)
|
||||||
|
which safe-chain
|
||||||
|
|
||||||
|
# Verify binary exists
|
||||||
|
ls ~/.safe-chain/bin/safe-chain
|
||||||
|
|
||||||
|
# Check version
|
||||||
|
safe-chain --version
|
||||||
|
|
||||||
|
# Test shell integration
|
||||||
|
type npm
|
||||||
|
type pip
|
||||||
|
```
|
||||||
|
|
||||||
|
**Expected `which` output:**
|
||||||
|
|
||||||
|
* Standalone binary (correct): `~/.safe-chain/bin/safe-chain` or `/Users/<username>/.safe-chain/bin/safe-chain`
|
||||||
|
* npm global (outdated): path containing `node_modules` or nvm version paths
|
||||||
|
|
||||||
|
If `which` shows an npm installation, see Check for Conflicting Installations.
|
||||||
|
|
||||||
|
### Check Shell Integration
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Which shell you're using
|
||||||
|
echo $SHELL
|
||||||
|
|
||||||
|
# Check if startup file sources safe-chain
|
||||||
|
# For Bash:
|
||||||
|
grep safe-chain ~/.bashrc
|
||||||
|
|
||||||
|
# For Zsh:
|
||||||
|
grep safe-chain ~/.zshrc
|
||||||
|
|
||||||
|
# For Fish:
|
||||||
|
grep safe-chain ~/.config/fish/config.fish
|
||||||
|
|
||||||
|
# Verify scripts exist
|
||||||
|
ls ~/.safe-chain/scripts/
|
||||||
|
```
|
||||||
|
|
||||||
|
### Check for Conflicting Installations
|
||||||
|
|
||||||
|
> **Note:** The install/uninstall scripts automatically detect and remove conflicting installations, but you can manually check:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check npm global
|
||||||
|
npm list -g @aikidosec/safe-chain
|
||||||
|
|
||||||
|
# Check Volta
|
||||||
|
volta list safe-chain
|
||||||
|
|
||||||
|
# Check nvm (all versions)
|
||||||
|
for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do
|
||||||
|
nvm exec "$version" npm list -g @aikidosec/safe-chain 2>/dev/null && echo "Found in $version"
|
||||||
|
done
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Cleanup
|
||||||
|
|
||||||
|
> **Note:** The install and uninstall scripts automatically handle these cleanup steps. Use these manual commands only if automatic cleanup fails.
|
||||||
|
|
||||||
|
#### Remove npm Global Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm uninstall -g @aikidosec/safe-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Remove Volta Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
volta uninstall @aikidosec/safe-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Remove nvm Installations (All Versions)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Automated approach
|
||||||
|
for version in $(nvm list | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+'); do
|
||||||
|
nvm exec "$version" npm uninstall -g @aikidosec/safe-chain
|
||||||
|
done
|
||||||
|
|
||||||
|
# Or manual per version
|
||||||
|
nvm use <version>
|
||||||
|
npm uninstall -g @aikidosec/safe-chain
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Clean Shell Configuration Files
|
||||||
|
|
||||||
|
Manually remove safe-chain entries from:
|
||||||
|
|
||||||
|
* Bash: `~/.bashrc`
|
||||||
|
* Zsh: `~/.zshrc`
|
||||||
|
* Fish: `~/.config/fish/config.fish`
|
||||||
|
* PowerShell: `$PROFILE`
|
||||||
|
|
||||||
|
Look for and remove:
|
||||||
|
|
||||||
|
* Lines sourcing from `~/.safe-chain/scripts/`
|
||||||
|
* Any safe-chain related function definitions
|
||||||
|
|
||||||
|
#### Remove Installation Directory
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rm -rf ~/.safe-chain
|
||||||
|
```
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
import js from "@eslint/js";
|
|
||||||
import { defineConfig, globalIgnores } from "@eslint/config-helpers";
|
|
||||||
import globals from "globals";
|
|
||||||
import importPlugin from "eslint-plugin-import";
|
|
||||||
|
|
||||||
export default defineConfig([
|
|
||||||
{
|
|
||||||
files: ["**/*.{js,mjs,cjs,ts}"],
|
|
||||||
plugins: { js },
|
|
||||||
extends: ["js/recommended"],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files: ["**/*.{js,mjs,cjs,ts}"],
|
|
||||||
languageOptions: { globals: globals.node },
|
|
||||||
},
|
|
||||||
importPlugin.flatConfigs.recommended,
|
|
||||||
{
|
|
||||||
files: ["**/*.{js,mjs,cjs}"],
|
|
||||||
languageOptions: {
|
|
||||||
ecmaVersion: "latest",
|
|
||||||
sourceType: "module",
|
|
||||||
},
|
|
||||||
rules: {},
|
|
||||||
},
|
|
||||||
globalIgnores(['test/e2e', 'node_modules']),
|
|
||||||
]);
|
|
||||||
133
install-scripts/install-endpoint-mac.sh
Executable file
133
install-scripts/install-endpoint-mac.sh
Executable file
|
|
@ -0,0 +1,133 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Downloads and installs Aikido Endpoint Protection on macOS
|
||||||
|
#
|
||||||
|
# Usage: curl -fsSL <url> | sudo sh -s -- --token <TOKEN>
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
INSTALL_URL="https://github.com/AikidoSec/safechain-internals/releases/download/v1.5.6/EndpointProtection.pkg"
|
||||||
|
DOWNLOAD_SHA256="345b26168b3090de5268c48d923cdf115cc617c39c37d44cc40fb9150409a6ba"
|
||||||
|
TOKEN_FILE="/tmp/aikido_endpoint_token.txt"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
info() {
|
||||||
|
printf "${GREEN}[INFO]${NC} %s\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download file
|
||||||
|
download() {
|
||||||
|
url="$1"
|
||||||
|
dest="$2"
|
||||||
|
|
||||||
|
if command -v curl >/dev/null 2>&1; then
|
||||||
|
curl -fsSL "$url" -o "$dest" || error "Failed to download from $url"
|
||||||
|
elif command -v wget >/dev/null 2>&1; then
|
||||||
|
wget -q "$url" -O "$dest" || error "Failed to download from $url"
|
||||||
|
else
|
||||||
|
error "Neither curl nor wget found. Please install one of them."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify SHA256 checksum
|
||||||
|
verify_checksum() {
|
||||||
|
file="$1"
|
||||||
|
expected="$2"
|
||||||
|
|
||||||
|
actual=$(shasum -a 256 "$file" | awk '{ print $1 }')
|
||||||
|
|
||||||
|
if [ "$actual" != "$expected" ]; then
|
||||||
|
error "Checksum verification failed. Expected: $expected, Got: $actual"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Checksum verified successfully."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup temporary files
|
||||||
|
cleanup() {
|
||||||
|
if [ -f "$PKG_FILE" ]; then
|
||||||
|
rm -f "$PKG_FILE"
|
||||||
|
fi
|
||||||
|
if [ -f "$TOKEN_FILE" ]; then
|
||||||
|
rm -f "$TOKEN_FILE"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command-line arguments
|
||||||
|
parse_arguments() {
|
||||||
|
TOKEN=""
|
||||||
|
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--token)
|
||||||
|
if [ -z "${2:-}" ]; then
|
||||||
|
error "--token requires a value"
|
||||||
|
fi
|
||||||
|
TOKEN="$2"
|
||||||
|
shift 2
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation
|
||||||
|
main() {
|
||||||
|
parse_arguments "$@"
|
||||||
|
|
||||||
|
# 1. Check if we're running on macOS
|
||||||
|
if [ "$(uname -s)" != "Darwin" ]; then
|
||||||
|
error "This script is only supported on macOS."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're running as root
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
error "Root privileges required. Please re-run with sudo, e.g.: curl -fsSL <url> | sudo sh -s -- --token <TOKEN>"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if token is provided via command argument
|
||||||
|
if [ -z "$TOKEN" ]; then
|
||||||
|
error "Token is required. Pass it with --token <TOKEN> or enter it when prompted."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Validate token to prevent injection
|
||||||
|
case "$TOKEN" in
|
||||||
|
*[\"\'\;\`\$\ ]*)
|
||||||
|
error "Invalid token format. Token must not contain quotes, semicolons, backticks, dollar signs, or whitespace."
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# 2. Download and verify checksum
|
||||||
|
PKG_FILE=$(mktemp /tmp/AikidoEndpoint.XXXXXX.pkg)
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
info "Downloading Aikido Endpoint Protection..."
|
||||||
|
download "$INSTALL_URL" "$PKG_FILE"
|
||||||
|
|
||||||
|
info "Verifying checksum..."
|
||||||
|
verify_checksum "$PKG_FILE" "$DOWNLOAD_SHA256"
|
||||||
|
|
||||||
|
# 3. Write token to file for the installer
|
||||||
|
printf "%s" "$TOKEN" > "$TOKEN_FILE"
|
||||||
|
|
||||||
|
# 4. Install the package
|
||||||
|
info "Installing Aikido Endpoint Protection..."
|
||||||
|
installer -pkg "$PKG_FILE" -target /
|
||||||
|
|
||||||
|
info "Aikido Endpoint Protection installed successfully!"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
100
install-scripts/install-endpoint-windows.ps1
Normal file
100
install-scripts/install-endpoint-windows.ps1
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Downloads and installs Aikido Endpoint Protection on Windows
|
||||||
|
#
|
||||||
|
# Usage: iex "& { $(iwr '<url>' -UseBasicParsing) } -token <TOKEN>"
|
||||||
|
|
||||||
|
param(
|
||||||
|
[string]$token
|
||||||
|
)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
$InstallUrl = "https://github.com/AikidoSec/safechain-internals/releases/download/v1.5.6/EndpointProtection.msi"
|
||||||
|
$DownloadSha256 = "70382b65036c6a4f0fc64e221ab3e74b06ec23bce54f93616a1e59abaac5442d"
|
||||||
|
|
||||||
|
# Ensure TLS 1.2 is enabled for downloads
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
function Write-Info {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[INFO] $Message" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Error-Custom {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[ERROR] $Message" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if running as Administrator
|
||||||
|
function Test-Administrator {
|
||||||
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||||
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation
|
||||||
|
function Install-Endpoint {
|
||||||
|
# 1. Check if we're running as Administrator
|
||||||
|
if (-not (Test-Administrator)) {
|
||||||
|
Write-Error-Custom "Administrator privileges required. Please run this script in an elevated terminal (Run as Administrator)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if token is provided, prompt if not
|
||||||
|
if ([string]::IsNullOrWhiteSpace($token)) {
|
||||||
|
$token = Read-Host "Enter your Aikido endpoint token"
|
||||||
|
if ([string]::IsNullOrWhiteSpace($token)) {
|
||||||
|
Write-Error-Custom "Token is required. Pass it with -token <TOKEN> or enter it when prompted."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Validate token to prevent command/property injection via msiexec
|
||||||
|
if ($token -match '[";`$\s]') {
|
||||||
|
Write-Error-Custom "Invalid token format. Token must not contain quotes, semicolons, backticks, dollar signs, or whitespace."
|
||||||
|
}
|
||||||
|
|
||||||
|
# 2. Download the .msi
|
||||||
|
$msiFile = Join-Path $env:TEMP "AikidoEndpoint-$([System.Guid]::NewGuid().ToString('N')).msi"
|
||||||
|
|
||||||
|
Write-Info "Downloading Aikido Endpoint Protection..."
|
||||||
|
try {
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
Invoke-WebRequest -Uri $InstallUrl -OutFile $msiFile -UseBasicParsing
|
||||||
|
$ProgressPreference = 'Continue'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Failed to download from $InstallUrl : $_"
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Verify SHA256 checksum
|
||||||
|
Write-Info "Verifying checksum..."
|
||||||
|
$actualHash = (Get-FileHash -Path $msiFile -Algorithm SHA256).Hash.ToLower()
|
||||||
|
if ($actualHash -ne $DownloadSha256) {
|
||||||
|
Write-Error-Custom "Checksum verification failed. Expected: $DownloadSha256, Got: $actualHash"
|
||||||
|
}
|
||||||
|
Write-Info "Checksum verified successfully."
|
||||||
|
|
||||||
|
# 3. Install the package with token passed as MSI property
|
||||||
|
Write-Info "Installing Aikido Endpoint Protection..."
|
||||||
|
$process = Start-Process -FilePath "msiexec" -ArgumentList "/i", "`"$msiFile`"", "/qn", "/norestart", "AIKIDO_TOKEN=$token" -Wait -PassThru
|
||||||
|
if ($process.ExitCode -ne 0) {
|
||||||
|
Write-Error-Custom "MSI installer failed (exit code: $($process.ExitCode))."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Aikido Endpoint Protection installed successfully!"
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
# Cleanup
|
||||||
|
if (Test-Path $msiFile) {
|
||||||
|
Remove-Item -Path $msiFile -Force -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run installation
|
||||||
|
try {
|
||||||
|
Install-Endpoint
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Installation failed: $_"
|
||||||
|
}
|
||||||
381
install-scripts/install-safe-chain.ps1
Normal file
381
install-scripts/install-safe-chain.ps1
Normal file
|
|
@ -0,0 +1,381 @@
|
||||||
|
# Downloads and installs safe-chain for Windows
|
||||||
|
#
|
||||||
|
# Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md
|
||||||
|
|
||||||
|
param(
|
||||||
|
[switch]$ci,
|
||||||
|
[switch]$includepython,
|
||||||
|
[string]$InstallDir
|
||||||
|
)
|
||||||
|
|
||||||
|
# Validates and normalizes the requested install directory.
|
||||||
|
# Rejects non-absolute, root, PATH-like, and traversal-containing paths.
|
||||||
|
function Test-InstallDir {
|
||||||
|
param([string]$Dir)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Dir)) {
|
||||||
|
return @{ Ok = $true; Normalized = $null }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not [System.IO.Path]::IsPathRooted($Dir)) {
|
||||||
|
return @{ Ok = $false; Reason = "-InstallDir must be an absolute path, got: $Dir" }
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($Dir.Contains([System.IO.Path]::PathSeparator)) {
|
||||||
|
return @{ Ok = $false; Reason = "-InstallDir must not contain the PATH separator ($([System.IO.Path]::PathSeparator))" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$inputSegments = $Dir.Split([char[]]@('\', '/'), [System.StringSplitOptions]::RemoveEmptyEntries)
|
||||||
|
if ($inputSegments -contains "..") {
|
||||||
|
return @{ Ok = $false; Reason = "-InstallDir must not contain path traversal segments" }
|
||||||
|
}
|
||||||
|
|
||||||
|
$normalized = [System.IO.Path]::GetFullPath($Dir)
|
||||||
|
$root = [System.IO.Path]::GetPathRoot($normalized)
|
||||||
|
if ($normalized.TrimEnd('\', '/') -eq $root.TrimEnd('\', '/')) {
|
||||||
|
return @{ Ok = $false; Reason = "-InstallDir cannot be a root or drive-root directory" }
|
||||||
|
}
|
||||||
|
|
||||||
|
return @{ Ok = $true; Normalized = $normalized }
|
||||||
|
}
|
||||||
|
|
||||||
|
$Version = $env:SAFE_CHAIN_VERSION # Will be fetched from latest release if not set
|
||||||
|
$SafeChainBase = if ($InstallDir) { $InstallDir } else { Join-Path $HOME ".safe-chain" }
|
||||||
|
|
||||||
|
$installDirValidation = Test-InstallDir -Dir $SafeChainBase
|
||||||
|
if (-not $installDirValidation.Ok) {
|
||||||
|
Write-Host "[ERROR] $($installDirValidation.Reason)" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
$SafeChainBase = $installDirValidation.Normalized
|
||||||
|
$InstallDir = Join-Path $SafeChainBase "bin"
|
||||||
|
$RepoUrl = "https://github.com/AikidoSec/safe-chain"
|
||||||
|
|
||||||
|
# SHA256 checksums for release binaries.
|
||||||
|
# Empty in source; populated by the release pipeline.
|
||||||
|
# When empty (running from main), checksum verification is skipped.
|
||||||
|
# Non-Windows hashes are unused today (PS script is Windows-only) but baked in
|
||||||
|
# for future cross-platform support.
|
||||||
|
$SHA256_MACOS_X64 = ""
|
||||||
|
$SHA256_MACOS_ARM64 = ""
|
||||||
|
$SHA256_LINUX_X64 = ""
|
||||||
|
$SHA256_LINUX_ARM64 = ""
|
||||||
|
$SHA256_LINUXSTATIC_X64 = ""
|
||||||
|
$SHA256_LINUXSTATIC_ARM64 = ""
|
||||||
|
$SHA256_WIN_X64 = ""
|
||||||
|
$SHA256_WIN_ARM64 = ""
|
||||||
|
|
||||||
|
# Ensure TLS 1.2 is enabled for downloads
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
function Write-Info {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[INFO] $Message" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Warn {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Error-Custom {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[ERROR] $Message" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get currently installed version of safe-chain
|
||||||
|
function Get-InstalledVersion {
|
||||||
|
# Check if safe-chain command exists
|
||||||
|
if (-not (Get-Command safe-chain -ErrorAction SilentlyContinue)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Execute safe-chain -v and capture output
|
||||||
|
$output = & safe-chain -v 2>&1
|
||||||
|
|
||||||
|
# Extract version from "Current safe-chain version: X.Y.Z" output
|
||||||
|
if ($output -match "Current safe-chain version:\s*(.+)") {
|
||||||
|
return $matches[1].Trim()
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the requested version is already installed
|
||||||
|
function Test-VersionInstalled {
|
||||||
|
param([string]$RequestedVersion)
|
||||||
|
|
||||||
|
$installedVersion = Get-InstalledVersion
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($installedVersion)) {
|
||||||
|
return $false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Strip leading 'v' from versions if present for comparison
|
||||||
|
$requestedClean = $RequestedVersion -replace '^v', ''
|
||||||
|
$installedClean = $installedVersion -replace '^v', ''
|
||||||
|
|
||||||
|
return $requestedClean -eq $installedClean
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch latest release version tag from GitHub
|
||||||
|
function Get-LatestVersion {
|
||||||
|
try {
|
||||||
|
$response = Invoke-RestMethod -Uri "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" -UseBasicParsing
|
||||||
|
$latestVersion = $response.tag_name
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($latestVersion)) {
|
||||||
|
Write-Error-Custom "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable."
|
||||||
|
}
|
||||||
|
|
||||||
|
return $latestVersion
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Failed to fetch latest version from GitHub API: $($_.Exception.Message). Please set SAFE_CHAIN_VERSION environment variable."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
function Get-Architecture {
|
||||||
|
$arch = $env:PROCESSOR_ARCHITECTURE
|
||||||
|
switch ($arch) {
|
||||||
|
"AMD64" { return "x64" }
|
||||||
|
"ARM64" { return "arm64" }
|
||||||
|
default { Write-Error-Custom "Unsupported architecture: $arch" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Emits the deprecation warning for SAFE_CHAIN_VERSION and prints the version-pinned install command.
|
||||||
|
# Returns immediately when no version was provided through the environment.
|
||||||
|
function Write-VersionDeprecationWarning {
|
||||||
|
if ([string]::IsNullOrWhiteSpace($env:SAFE_CHAIN_VERSION)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
||||||
|
Write-Warn ""
|
||||||
|
Write-Warn "Please use direct download URLs for version pinning instead:"
|
||||||
|
Write-Warn ""
|
||||||
|
if ($ci) {
|
||||||
|
Write-Warn " iex `"& { `$(iwr 'https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1' -UseBasicParsing) } -ci`""
|
||||||
|
} else {
|
||||||
|
Write-Warn " iex (iwr `"https://github.com/AikidoSec/safe-chain/releases/download/$env:SAFE_CHAIN_VERSION/install-safe-chain.ps1`" -UseBasicParsing)"
|
||||||
|
}
|
||||||
|
Write-Warn ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Builds the Windows release binary filename for the detected architecture.
|
||||||
|
# Centralizes binary name generation for the download step.
|
||||||
|
function Get-BinaryName {
|
||||||
|
param([string]$Architecture)
|
||||||
|
|
||||||
|
return "safe-chain-win-$Architecture.exe"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the expected SHA256 for the given OS+arch, or empty if not baked in.
|
||||||
|
function Get-ExpectedSha256 {
|
||||||
|
param([string]$Os, [string]$Architecture)
|
||||||
|
switch ("$Os-$Architecture") {
|
||||||
|
"macos-x64" { return $SHA256_MACOS_X64 }
|
||||||
|
"macos-arm64" { return $SHA256_MACOS_ARM64 }
|
||||||
|
"linux-x64" { return $SHA256_LINUX_X64 }
|
||||||
|
"linux-arm64" { return $SHA256_LINUX_ARM64 }
|
||||||
|
"linuxstatic-x64" { return $SHA256_LINUXSTATIC_X64 }
|
||||||
|
"linuxstatic-arm64" { return $SHA256_LINUXSTATIC_ARM64 }
|
||||||
|
"win-x64" { return $SHA256_WIN_X64 }
|
||||||
|
"win-arm64" { return $SHA256_WIN_ARM64 }
|
||||||
|
default { return "" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Test-Checksum {
|
||||||
|
param([string]$File, [string]$Expected)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Expected)) { return }
|
||||||
|
|
||||||
|
$actual = (Get-FileHash -Path $File -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||||
|
$expectedLower = $Expected.ToLowerInvariant()
|
||||||
|
|
||||||
|
if ($actual -ne $expectedLower) {
|
||||||
|
Remove-Item -Path $File -Force -ErrorAction SilentlyContinue
|
||||||
|
Write-Error-Custom "Checksum verification failed. Expected: $expectedLower, Got: $actual"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Checksum verified."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs safe-chain setup or setup-ci after the binary is installed.
|
||||||
|
# Temporarily appends the install directory to PATH and downgrades setup failures to warnings.
|
||||||
|
function Invoke-SafeChainSetup {
|
||||||
|
param(
|
||||||
|
[string]$BinaryPath,
|
||||||
|
[string]$InstallDirectory
|
||||||
|
)
|
||||||
|
|
||||||
|
$setupCmd = if ($ci) { "setup-ci" } else { "setup" }
|
||||||
|
|
||||||
|
Write-Info "Running safe-chain $setupCmd..."
|
||||||
|
try {
|
||||||
|
$env:Path = "$env:Path;$InstallDirectory"
|
||||||
|
& $BinaryPath $setupCmd
|
||||||
|
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warn "safe-chain was installed but setup encountered issues."
|
||||||
|
Write-Warn "You can run 'safe-chain $setupCmd' manually later."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warn "safe-chain was installed but setup encountered issues: $_"
|
||||||
|
Write-Warn "You can run 'safe-chain $setupCmd' manually later."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall npm global package if present
|
||||||
|
function Remove-NpmInstallation {
|
||||||
|
# Check if npm is available
|
||||||
|
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if safe-chain is installed as an npm global package
|
||||||
|
npm list -g @aikidosec/safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Detected npm global installation of @aikidosec/safe-chain"
|
||||||
|
Write-Info "Uninstalling npm version before installing binary version..."
|
||||||
|
|
||||||
|
npm uninstall -g @aikidosec/safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Successfully uninstalled npm version"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warn "Failed to uninstall npm version automatically"
|
||||||
|
Write-Warn "Please run: npm uninstall -g @aikidosec/safe-chain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall Volta-managed package if present
|
||||||
|
function Remove-VoltaInstallation {
|
||||||
|
# Check if Volta is available
|
||||||
|
if (-not (Get-Command volta -ErrorAction SilentlyContinue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Volta manages global packages in its own directory
|
||||||
|
# Check if safe-chain is installed via Volta
|
||||||
|
volta list safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Detected Volta installation of @aikidosec/safe-chain"
|
||||||
|
Write-Info "Uninstalling Volta version before installing binary version..."
|
||||||
|
|
||||||
|
volta uninstall @aikidosec/safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Successfully uninstalled Volta version"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warn "Failed to uninstall Volta version automatically"
|
||||||
|
Write-Warn "Please run: volta uninstall @aikidosec/safe-chain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation
|
||||||
|
function Install-SafeChain {
|
||||||
|
Write-VersionDeprecationWarning
|
||||||
|
|
||||||
|
# Fetch latest version if VERSION is not set
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Version)) {
|
||||||
|
Write-Info "Fetching latest release version..."
|
||||||
|
$Version = Get-LatestVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the requested version is already installed
|
||||||
|
if (Test-VersionInstalled -RequestedVersion $Version) {
|
||||||
|
Write-Info "safe-chain $Version is already installed"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Build installation message
|
||||||
|
$installMsg = "Installing safe-chain $Version"
|
||||||
|
if ($ci) {
|
||||||
|
$installMsg += " in ci"
|
||||||
|
}
|
||||||
|
if ($includepython) {
|
||||||
|
Write-Warn "-includepython is deprecated and ignored. Python ecosystem is now included by default."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info $installMsg
|
||||||
|
|
||||||
|
# Check for existing safe-chain installation through npm or volta
|
||||||
|
Remove-NpmInstallation
|
||||||
|
Remove-VoltaInstallation
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
$arch = Get-Architecture
|
||||||
|
$binaryName = Get-BinaryName -Architecture $arch
|
||||||
|
|
||||||
|
Write-Info "Detected architecture: $arch"
|
||||||
|
|
||||||
|
# Create installation directory
|
||||||
|
if (-not (Test-Path $InstallDir)) {
|
||||||
|
Write-Info "Creating installation directory: $InstallDir"
|
||||||
|
try {
|
||||||
|
New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Failed to create directory $InstallDir : $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download binary
|
||||||
|
$downloadUrl = "$RepoUrl/releases/download/$Version/$binaryName"
|
||||||
|
$tempFile = Join-Path $InstallDir $binaryName
|
||||||
|
|
||||||
|
Write-Info "Downloading from: $downloadUrl"
|
||||||
|
|
||||||
|
try {
|
||||||
|
# Download with progress suppressed for cleaner output
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
Invoke-WebRequest -Uri $downloadUrl -OutFile $tempFile -UseBasicParsing
|
||||||
|
$ProgressPreference = 'Continue'
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Failed to download from $downloadUrl : $_"
|
||||||
|
}
|
||||||
|
|
||||||
|
$expectedSha = Get-ExpectedSha256 -Os "win" -Architecture $arch
|
||||||
|
Test-Checksum -File $tempFile -Expected $expectedSha
|
||||||
|
|
||||||
|
# Rename to final location
|
||||||
|
$finalFile = Join-Path $InstallDir "safe-chain.exe"
|
||||||
|
try {
|
||||||
|
# Remove existing file if present (Move-Item -Force doesn't overwrite)
|
||||||
|
if (Test-Path $finalFile) {
|
||||||
|
Remove-Item -Path $finalFile -Force
|
||||||
|
}
|
||||||
|
Move-Item -Path $tempFile -Destination $finalFile -Force
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Failed to move binary to $finalFile : $_"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Binary installed to: $finalFile"
|
||||||
|
|
||||||
|
Invoke-SafeChainSetup -BinaryPath $finalFile -InstallDirectory $InstallDir
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run installation
|
||||||
|
try {
|
||||||
|
Install-SafeChain
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Installation failed: $_"
|
||||||
|
}
|
||||||
509
install-scripts/install-safe-chain.sh
Executable file
509
install-scripts/install-safe-chain.sh
Executable file
|
|
@ -0,0 +1,509 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Downloads and installs safe-chain, depending on the operating system and architecture
|
||||||
|
#
|
||||||
|
# Usage with "curl -fsSL {url} | sh" --> See README.md
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Validates a user-provided install dir and exits on unsafe values.
|
||||||
|
# Rejects relative paths, root paths, PATH separators, and traversal segments.
|
||||||
|
validate_install_dir() {
|
||||||
|
dir="$1"
|
||||||
|
|
||||||
|
if [ -z "$dir" ]; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$dir" in
|
||||||
|
/*) ;;
|
||||||
|
*)
|
||||||
|
printf '[ERROR] --install-dir must be an absolute path, got: %s\n' "$dir" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$dir" in
|
||||||
|
*:*)
|
||||||
|
printf '[ERROR] --install-dir must not contain the PATH separator (:)\n' >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ "$dir" = "/" ]; then
|
||||||
|
printf '[ERROR] --install-dir cannot be a root or drive-root directory\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
old_ifs=$IFS
|
||||||
|
IFS='/'
|
||||||
|
set -- $dir
|
||||||
|
IFS=$old_ifs
|
||||||
|
|
||||||
|
for segment in "$@"; do
|
||||||
|
if [ "$segment" = ".." ]; then
|
||||||
|
printf '[ERROR] --install-dir must not contain path traversal segments\n' >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
VERSION="${SAFE_CHAIN_VERSION:-}" # Will be fetched from latest release if not set
|
||||||
|
SAFE_CHAIN_BASE="${HOME}/.safe-chain"
|
||||||
|
|
||||||
|
INSTALL_DIR="${SAFE_CHAIN_BASE}/bin"
|
||||||
|
REPO_URL="https://github.com/AikidoSec/safe-chain"
|
||||||
|
|
||||||
|
# SHA256 checksums for release binaries.
|
||||||
|
# Empty in source; populated by the release pipeline via sed.
|
||||||
|
# When empty (running from main), checksum verification is skipped.
|
||||||
|
SHA256_MACOS_X64=""
|
||||||
|
SHA256_MACOS_ARM64=""
|
||||||
|
SHA256_LINUX_X64=""
|
||||||
|
SHA256_LINUX_ARM64=""
|
||||||
|
SHA256_LINUXSTATIC_X64=""
|
||||||
|
SHA256_LINUXSTATIC_ARM64=""
|
||||||
|
SHA256_WIN_X64=""
|
||||||
|
SHA256_WIN_ARM64=""
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
info() {
|
||||||
|
printf "${GREEN}[INFO]${NC} %s\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
printf "${YELLOW}[WARN]${NC} %s\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect OS
|
||||||
|
# For legacy versions (when SAFE_CHAIN_VERSION is set), use 'linux' instead of 'linuxstatic'
|
||||||
|
detect_os() {
|
||||||
|
case "$(uname -s)" in
|
||||||
|
Linux*)
|
||||||
|
if [ -n "$SAFE_CHAIN_VERSION" ]; then
|
||||||
|
echo "linux"
|
||||||
|
else
|
||||||
|
echo "linuxstatic"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
Darwin*) echo "macos" ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) echo "win" ;;
|
||||||
|
*) error "Unsupported operating system: $(uname -s)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Detect architecture
|
||||||
|
detect_arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64|amd64) echo "x64" ;;
|
||||||
|
aarch64|arm64) echo "arm64" ;;
|
||||||
|
*) error "Unsupported architecture: $(uname -m)" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if command exists
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Get currently installed version of safe-chain
|
||||||
|
get_installed_version() {
|
||||||
|
if ! command_exists safe-chain; then
|
||||||
|
echo ""
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Extract version from "Current safe-chain version: X.Y.Z" output
|
||||||
|
installed_version=$(safe-chain -v 2>/dev/null | grep "Current safe-chain version:" | sed -E 's/.*: (.*)/\1/')
|
||||||
|
echo "$installed_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if the requested version is already installed
|
||||||
|
is_version_installed() {
|
||||||
|
requested_version="$1"
|
||||||
|
installed_version=$(get_installed_version)
|
||||||
|
|
||||||
|
if [ -z "$installed_version" ]; then
|
||||||
|
return 1 # Not installed
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Strip leading 'v' from versions if present for comparison
|
||||||
|
requested_clean=$(echo "$requested_version" | sed 's/^v//')
|
||||||
|
installed_clean=$(echo "$installed_version" | sed 's/^v//')
|
||||||
|
|
||||||
|
if [ "$requested_clean" = "$installed_clean" ]; then
|
||||||
|
return 0 # Same version installed
|
||||||
|
else
|
||||||
|
return 1 # Different version installed
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Fetch latest release version tag from GitHub
|
||||||
|
fetch_latest_version() {
|
||||||
|
# Try using GitHub API to get the latest release tag
|
||||||
|
if command_exists curl; then
|
||||||
|
latest_version=$(curl -fsSL "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
elif command_exists wget; then
|
||||||
|
latest_version=$(wget -qO- "https://api.github.com/repos/AikidoSec/safe-chain/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
else
|
||||||
|
error "Neither curl nor wget found. Please install one of them or set SAFE_CHAIN_VERSION environment variable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$latest_version" ]; then
|
||||||
|
error "Failed to fetch latest version from GitHub API. Please set SAFE_CHAIN_VERSION environment variable."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$latest_version"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the expected SHA256 for the detected platform, or empty if the
|
||||||
|
# release pipeline has not baked one in (i.e. running the source from main).
|
||||||
|
get_expected_sha256() {
|
||||||
|
os="$1"; arch="$2"
|
||||||
|
case "${os}-${arch}" in
|
||||||
|
macos-x64) echo "$SHA256_MACOS_X64" ;;
|
||||||
|
macos-arm64) echo "$SHA256_MACOS_ARM64" ;;
|
||||||
|
linux-x64) echo "$SHA256_LINUX_X64" ;;
|
||||||
|
linux-arm64) echo "$SHA256_LINUX_ARM64" ;;
|
||||||
|
linuxstatic-x64) echo "$SHA256_LINUXSTATIC_X64" ;;
|
||||||
|
linuxstatic-arm64) echo "$SHA256_LINUXSTATIC_ARM64" ;;
|
||||||
|
win-x64) echo "$SHA256_WIN_X64" ;;
|
||||||
|
win-arm64) echo "$SHA256_WIN_ARM64" ;;
|
||||||
|
*) echo "" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
compute_sha256() {
|
||||||
|
file="$1"
|
||||||
|
if command_exists sha256sum; then
|
||||||
|
sha256sum "$file" | awk '{print $1}'
|
||||||
|
elif command_exists shasum; then
|
||||||
|
shasum -a 256 "$file" | awk '{print $1}'
|
||||||
|
else
|
||||||
|
echo ""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verifies the downloaded binary against the expected hash baked in by the release pipeline.
|
||||||
|
# No-op when no expected hash is set (running the script from main).
|
||||||
|
verify_checksum() {
|
||||||
|
file="$1"; expected="$2"
|
||||||
|
|
||||||
|
if [ -z "$expected" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
actual=$(compute_sha256 "$file")
|
||||||
|
if [ -z "$actual" ]; then
|
||||||
|
rm -f "$file"
|
||||||
|
error "Cannot verify checksum: neither sha256sum nor shasum is available. Install one and re-run."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$actual" != "$expected" ]; then
|
||||||
|
rm -f "$file"
|
||||||
|
error "Checksum verification failed. Expected: $expected, Got: $actual"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Checksum verified."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Download file
|
||||||
|
download() {
|
||||||
|
url="$1"
|
||||||
|
dest="$2"
|
||||||
|
|
||||||
|
if command_exists curl; then
|
||||||
|
curl -fsSL "$url" -o "$dest" || error "Failed to download from $url"
|
||||||
|
elif command_exists wget; then
|
||||||
|
wget -q "$url" -O "$dest" || error "Failed to download from $url"
|
||||||
|
else
|
||||||
|
error "Neither curl nor wget found. Please install one of them."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Prints the deprecation warning for SAFE_CHAIN_VERSION and the replacement install command.
|
||||||
|
# Returns immediately when no version was pinned through the environment.
|
||||||
|
warn_deprecated_version_env() {
|
||||||
|
if [ -z "$SAFE_CHAIN_VERSION" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
warn "SAFE_CHAIN_VERSION environment variable is deprecated."
|
||||||
|
warn ""
|
||||||
|
warn "Please use direct download URLs for version pinning instead:"
|
||||||
|
warn ""
|
||||||
|
if [ "$USE_CI_SETUP" = "true" ]; then
|
||||||
|
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh -s -- --ci"
|
||||||
|
else
|
||||||
|
warn " curl -fsSL https://github.com/AikidoSec/safe-chain/releases/download/${SAFE_CHAIN_VERSION}/install-safe-chain.sh | sh"
|
||||||
|
fi
|
||||||
|
warn ""
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ensures VERSION is populated before installation continues.
|
||||||
|
# Fetches the latest release only when no explicit version was provided.
|
||||||
|
ensure_version() {
|
||||||
|
if [ -n "$VERSION" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Fetching latest release version..."
|
||||||
|
VERSION=$(fetch_latest_version)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Constructs platform-specific binary filename to match GitHub release asset naming convention.
|
||||||
|
get_binary_name() {
|
||||||
|
os="$1"
|
||||||
|
arch="$2"
|
||||||
|
|
||||||
|
if [ "$os" = "win" ]; then
|
||||||
|
printf 'safe-chain-%s-%s.exe\n' "$os" "$arch"
|
||||||
|
else
|
||||||
|
printf 'safe-chain-%s-%s\n' "$os" "$arch"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the final installation path for the downloaded safe-chain binary.
|
||||||
|
# Uses INSTALL_DIR and the platform-specific executable name.
|
||||||
|
get_final_binary_path() {
|
||||||
|
os="$1"
|
||||||
|
|
||||||
|
if [ "$os" = "win" ]; then
|
||||||
|
printf '%s/safe-chain.exe\n' "$INSTALL_DIR"
|
||||||
|
else
|
||||||
|
printf '%s/safe-chain\n' "$INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
run_setup_command() {
|
||||||
|
final_file="$1"
|
||||||
|
|
||||||
|
setup_cmd="setup"
|
||||||
|
if [ "$USE_CI_SETUP" = "true" ]; then
|
||||||
|
setup_cmd="setup-ci"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Running safe-chain $setup_cmd..."
|
||||||
|
if ! "$final_file" "$setup_cmd"; then
|
||||||
|
warn "safe-chain was installed but setup encountered issues."
|
||||||
|
warn "You can run 'safe-chain $setup_cmd' manually later."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall npm global package if present
|
||||||
|
remove_npm_installation() {
|
||||||
|
if ! command_exists npm; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if safe-chain is installed as an npm global package
|
||||||
|
if npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Detected npm global installation of @aikidosec/safe-chain"
|
||||||
|
info "Uninstalling npm version before installing binary version..."
|
||||||
|
|
||||||
|
if npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Successfully uninstalled npm version"
|
||||||
|
else
|
||||||
|
warn "Failed to uninstall npm version automatically"
|
||||||
|
warn "Please run: npm uninstall -g @aikidosec/safe-chain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall Volta-managed package if present
|
||||||
|
remove_volta_installation() {
|
||||||
|
if ! command_exists volta; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Volta manages global packages in its own directory
|
||||||
|
# Check if safe-chain is installed via Volta
|
||||||
|
if volta list safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Detected Volta installation of @aikidosec/safe-chain"
|
||||||
|
info "Uninstalling Volta version before installing binary version..."
|
||||||
|
|
||||||
|
if volta uninstall @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Successfully uninstalled Volta version"
|
||||||
|
else
|
||||||
|
warn "Failed to uninstall Volta version automatically"
|
||||||
|
warn "Please run: volta uninstall @aikidosec/safe-chain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall nvm-managed package if present across all Node versions
|
||||||
|
remove_nvm_installation() {
|
||||||
|
# This script is run in sh shell for greatest compatibility.
|
||||||
|
# Because nvm is usually setup in bash/zsh/fish startup scripts, we need to source it.
|
||||||
|
# Otherwise it won't be available in sh.
|
||||||
|
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
# Source nvm to make it available in this script
|
||||||
|
. "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
|
||||||
|
elif [ -s "$NVM_DIR/nvm.sh" ]; then
|
||||||
|
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if nvm is now available
|
||||||
|
if ! command_exists nvm; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
nvm_versions=$(nvm list 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "")
|
||||||
|
|
||||||
|
if [ -z "$nvm_versions" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Track if we found any installations
|
||||||
|
found_installation=false
|
||||||
|
uninstall_failed=false
|
||||||
|
current_version=$(nvm current 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Check each version for safe-chain installation
|
||||||
|
for version in $nvm_versions; do
|
||||||
|
# Check if this version has safe-chain installed
|
||||||
|
# Use nvm exec to run npm list in the context of that Node version
|
||||||
|
if nvm exec "$version" npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
if [ "$found_installation" = false ]; then
|
||||||
|
info "Detected nvm installation(s) of @aikidosec/safe-chain"
|
||||||
|
info "Uninstalling from all Node versions..."
|
||||||
|
found_installation=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
info " Removing from Node $version..."
|
||||||
|
if nvm exec "$version" npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info " Successfully uninstalled from Node $version"
|
||||||
|
else
|
||||||
|
warn " Failed to uninstall from Node $version"
|
||||||
|
uninstall_failed=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Restore original Node version if it was set
|
||||||
|
if [ -n "$current_version" ] && [ "$current_version" != "none" ] && [ "$current_version" != "system" ]; then
|
||||||
|
nvm use "$current_version" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If any uninstall failed, error out instead of continuing
|
||||||
|
if [ "$uninstall_failed" = true ]; then
|
||||||
|
error "Failed to uninstall @aikidosec/safe-chain from all nvm Node versions. Please uninstall manually and try again."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse command-line arguments
|
||||||
|
parse_arguments() {
|
||||||
|
while [ $# -gt 0 ]; do
|
||||||
|
case "$1" in
|
||||||
|
--ci)
|
||||||
|
USE_CI_SETUP=true
|
||||||
|
;;
|
||||||
|
--install-dir)
|
||||||
|
shift
|
||||||
|
if [ $# -eq 0 ]; then
|
||||||
|
error "Missing value for --install-dir"
|
||||||
|
fi
|
||||||
|
if [ -z "$1" ]; then
|
||||||
|
error "--install-dir must not be empty"
|
||||||
|
fi
|
||||||
|
SAFE_CHAIN_BASE="$1"
|
||||||
|
;;
|
||||||
|
--install-dir=*)
|
||||||
|
SAFE_CHAIN_BASE="${1#--install-dir=}"
|
||||||
|
if [ -z "$SAFE_CHAIN_BASE" ]; then
|
||||||
|
error "--install-dir must not be empty"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
--include-python)
|
||||||
|
warn "--include-python is deprecated and ignored. Python ecosystem is now included by default."
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
error "Unknown argument: $1"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
|
||||||
|
validate_install_dir "${SAFE_CHAIN_BASE}"
|
||||||
|
INSTALL_DIR="${SAFE_CHAIN_BASE}/bin"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main installation
|
||||||
|
main() {
|
||||||
|
# Initialize argument flags
|
||||||
|
USE_CI_SETUP=false
|
||||||
|
|
||||||
|
# Parse command-line arguments
|
||||||
|
parse_arguments "$@"
|
||||||
|
|
||||||
|
warn_deprecated_version_env
|
||||||
|
|
||||||
|
ensure_version
|
||||||
|
|
||||||
|
# Check if the requested version is already installed
|
||||||
|
if is_version_installed "$VERSION"; then
|
||||||
|
info "safe-chain ${VERSION} is already installed"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Build installation message
|
||||||
|
INSTALL_MSG="Installing safe-chain ${VERSION}"
|
||||||
|
if [ "$USE_CI_SETUP" = "true" ]; then
|
||||||
|
INSTALL_MSG="${INSTALL_MSG} in ci"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "$INSTALL_MSG"
|
||||||
|
|
||||||
|
# Check for existing safe-chain installation through nvm, volta, or npm
|
||||||
|
remove_npm_installation
|
||||||
|
remove_volta_installation
|
||||||
|
remove_nvm_installation
|
||||||
|
|
||||||
|
# Detect platform
|
||||||
|
OS=$(detect_os)
|
||||||
|
ARCH=$(detect_arch)
|
||||||
|
BINARY_NAME=$(get_binary_name "$OS" "$ARCH")
|
||||||
|
|
||||||
|
info "Detected platform: ${OS}-${ARCH}"
|
||||||
|
|
||||||
|
# Create installation directory
|
||||||
|
if [ ! -d "$INSTALL_DIR" ]; then
|
||||||
|
info "Creating installation directory: $INSTALL_DIR"
|
||||||
|
mkdir -p "$INSTALL_DIR" || error "Failed to create directory $INSTALL_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Download binary
|
||||||
|
DOWNLOAD_URL="${REPO_URL}/releases/download/${VERSION}/${BINARY_NAME}"
|
||||||
|
TEMP_FILE="${INSTALL_DIR}/${BINARY_NAME}"
|
||||||
|
|
||||||
|
info "Downloading from: $DOWNLOAD_URL"
|
||||||
|
download "$DOWNLOAD_URL" "$TEMP_FILE"
|
||||||
|
|
||||||
|
EXPECTED_SHA256=$(get_expected_sha256 "$OS" "$ARCH")
|
||||||
|
verify_checksum "$TEMP_FILE" "$EXPECTED_SHA256"
|
||||||
|
|
||||||
|
# Rename and make executable
|
||||||
|
FINAL_FILE=$(get_final_binary_path "$OS")
|
||||||
|
mv "$TEMP_FILE" "$FINAL_FILE" || error "Failed to move binary to $FINAL_FILE"
|
||||||
|
if [ "$OS" != "win" ]; then
|
||||||
|
chmod +x "$FINAL_FILE" || error "Failed to make binary executable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Binary installed to: $FINAL_FILE"
|
||||||
|
|
||||||
|
run_setup_command "$FINAL_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
50
install-scripts/uninstall-endpoint-mac.sh
Executable file
50
install-scripts/uninstall-endpoint-mac.sh
Executable file
|
|
@ -0,0 +1,50 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Uninstalls Aikido Endpoint Protection on macOS
|
||||||
|
#
|
||||||
|
# Usage: curl -fsSL <url> | sudo sh
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
UNINSTALL_SCRIPT="/Applications/Aikido Endpoint Protection.app/Contents/Resources/scripts/uninstall"
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
info() {
|
||||||
|
printf "${GREEN}[INFO]${NC} %s\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main uninstallation
|
||||||
|
main() {
|
||||||
|
# Check if we're running on macOS
|
||||||
|
if [ "$(uname -s)" != "Darwin" ]; then
|
||||||
|
error "This script is only supported on macOS."
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if we're running as root
|
||||||
|
if [ "$(id -u)" -ne 0 ]; then
|
||||||
|
error "Root privileges required. Please re-run with sudo, e.g.: curl -fsSL <url> | sudo sh"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if the uninstall script exists
|
||||||
|
if [ ! -f "$UNINSTALL_SCRIPT" ]; then
|
||||||
|
error "Aikido Endpoint Protection does not appear to be installed (uninstall script not found)."
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Uninstalling Aikido Endpoint Protection..."
|
||||||
|
"$UNINSTALL_SCRIPT"
|
||||||
|
|
||||||
|
info "Aikido Endpoint Protection uninstalled successfully!"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
59
install-scripts/uninstall-endpoint-windows.ps1
Normal file
59
install-scripts/uninstall-endpoint-windows.ps1
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Uninstalls Aikido Endpoint Protection endpoint on Windows
|
||||||
|
#
|
||||||
|
# Usage: iex (iwr '<url>' -UseBasicParsing)
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
$AppName = "Aikido Endpoint Protection"
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
function Write-Info {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[INFO] $Message" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Error-Custom {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[ERROR] $Message" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if running as Administrator
|
||||||
|
function Test-Administrator {
|
||||||
|
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
|
||||||
|
return $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main uninstallation
|
||||||
|
function Uninstall-Endpoint {
|
||||||
|
# Check if we're running as Administrator
|
||||||
|
if (-not (Test-Administrator)) {
|
||||||
|
Write-Error-Custom "Administrator privileges required. Please run this script in an elevated terminal (Run as Administrator)."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Find the installed product
|
||||||
|
Write-Info "Looking for Aikido Endpoint Protection installation..."
|
||||||
|
$app = Get-WmiObject -Class Win32_Product -Filter "Name='$AppName'"
|
||||||
|
|
||||||
|
if (-not $app) {
|
||||||
|
Write-Error-Custom "Aikido Endpoint Protection does not appear to be installed."
|
||||||
|
}
|
||||||
|
|
||||||
|
$productCode = $app.IdentifyingNumber
|
||||||
|
|
||||||
|
Write-Info "Uninstalling Aikido Endpoint Protection..."
|
||||||
|
$process = Start-Process -FilePath "msiexec" -ArgumentList "/x", $productCode, "/qn", "/norestart" -Wait -PassThru
|
||||||
|
if ($process.ExitCode -ne 0) {
|
||||||
|
Write-Error-Custom "Uninstall failed (exit code: $($process.ExitCode))."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Aikido Endpoint Protection uninstalled successfully!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run uninstallation
|
||||||
|
try {
|
||||||
|
Uninstall-Endpoint
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Uninstallation failed: $_"
|
||||||
|
}
|
||||||
249
install-scripts/uninstall-safe-chain.ps1
Normal file
249
install-scripts/uninstall-safe-chain.ps1
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
# Uninstalls safe-chain from Windows
|
||||||
|
#
|
||||||
|
# Usage with "iex (iwr {url} -UseBasicParsing)" --> See README.md
|
||||||
|
|
||||||
|
# Use HOME on Unix, USERPROFILE on Windows (PowerShell Core is cross-platform)
|
||||||
|
$HomeDir = if ($env:HOME) { $env:HOME } else { $env:USERPROFILE }
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
function Write-Info {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[INFO] $Message" -ForegroundColor Green
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Warn {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
|
||||||
|
function Write-Error-Custom {
|
||||||
|
param([string]$Message)
|
||||||
|
Write-Host "[ERROR] $Message" -ForegroundColor Red
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Derives the safe-chain base install directory from a resolved binary path.
|
||||||
|
# Rejects wrapper scripts and paths that do not match the packaged bin layout.
|
||||||
|
function Get-InstallDirFromBinaryPath {
|
||||||
|
param([string]$BinaryPath)
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($BinaryPath)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$resolvedPath = (Resolve-Path -LiteralPath $BinaryPath -ErrorAction Stop).Path
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
$resolvedPath = [System.IO.Path]::GetFullPath($BinaryPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
$fileName = [System.IO.Path]::GetFileName($resolvedPath)
|
||||||
|
if (($fileName -ne "safe-chain") -and ($fileName -ne "safe-chain.exe")) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($resolvedPath -match '\.(js|cjs|mjs|cmd|ps1)$') {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$binDir = Split-Path -Parent $resolvedPath
|
||||||
|
if ((Split-Path -Leaf $binDir) -ne "bin") {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Split-Path -Parent $binDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the first safe-chain command found on PATH, if any.
|
||||||
|
# Used as the starting point for install-dir discovery.
|
||||||
|
function Get-SafeChainCommand {
|
||||||
|
return Get-Command safe-chain -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the safe-chain command path only when it points to a valid packaged binary install.
|
||||||
|
# Prevents teardown from invoking arbitrary wrappers or scripts from PATH.
|
||||||
|
function Get-ValidatedSafeChainCommandPath {
|
||||||
|
$command = Get-SafeChainCommand
|
||||||
|
if (-not $command -or [string]::IsNullOrWhiteSpace($command.Path)) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
$installDir = Get-InstallDirFromBinaryPath -BinaryPath $command.Path
|
||||||
|
if (-not $installDir) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $command.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
# Invokes the validated safe-chain binary with get-install-dir and returns the reported base directory.
|
||||||
|
# Safely returns $null when the command is unavailable or the lookup fails.
|
||||||
|
function Get-ReportedInstallDir {
|
||||||
|
$safeChainPath = Get-ValidatedSafeChainCommandPath
|
||||||
|
if (-not $safeChainPath) {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$reportedInstallDir = & $safeChainPath get-install-dir 2>$null | Select-Object -First 1
|
||||||
|
if ($reportedInstallDir) {
|
||||||
|
$reportedInstallDir = $reportedInstallDir.Trim()
|
||||||
|
}
|
||||||
|
if ($reportedInstallDir) {
|
||||||
|
return $reportedInstallDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
return $null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determines the safe-chain base install directory for uninstall.
|
||||||
|
# Prefers the binary-reported location, then derives it from PATH, then falls back to the default home-dir layout.
|
||||||
|
function Get-SafeChainInstallDir {
|
||||||
|
$reportedInstallDir = Get-ReportedInstallDir
|
||||||
|
if ($reportedInstallDir) {
|
||||||
|
return $reportedInstallDir
|
||||||
|
}
|
||||||
|
|
||||||
|
$command = Get-SafeChainCommand
|
||||||
|
if ($command -and $command.Path) {
|
||||||
|
$discoveredInstallDir = Get-InstallDirFromBinaryPath -BinaryPath $command.Path
|
||||||
|
if ($discoveredInstallDir) {
|
||||||
|
return $discoveredInstallDir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (Join-Path $HomeDir ".safe-chain")
|
||||||
|
}
|
||||||
|
|
||||||
|
# Finds the installed safe-chain binary inside the resolved install directory.
|
||||||
|
# Falls back to a validated safe-chain command when the expected file is missing.
|
||||||
|
function Find-SafeChainBinary {
|
||||||
|
param([string]$DotSafeChain)
|
||||||
|
|
||||||
|
$safeChainExe = Join-Path $DotSafeChain "bin/safe-chain.exe"
|
||||||
|
$safeChainBin = Join-Path $DotSafeChain "bin/safe-chain"
|
||||||
|
|
||||||
|
if (Test-Path $safeChainExe) {
|
||||||
|
return $safeChainExe
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path $safeChainBin) {
|
||||||
|
return $safeChainBin
|
||||||
|
}
|
||||||
|
|
||||||
|
return Get-ValidatedSafeChainCommandPath
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs safe-chain teardown before removing the installation directory.
|
||||||
|
# Converts teardown failures into warnings so uninstall can still complete.
|
||||||
|
function Invoke-SafeChainTeardown {
|
||||||
|
param([string]$SafeChainPath)
|
||||||
|
|
||||||
|
if (-not $SafeChainPath) {
|
||||||
|
Write-Warn "safe-chain command not found. Proceeding with uninstallation."
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "Running safe-chain teardown..."
|
||||||
|
try {
|
||||||
|
& $SafeChainPath teardown
|
||||||
|
if ($LASTEXITCODE -ne 0) {
|
||||||
|
Write-Warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warn "safe-chain teardown encountered issues: $_"
|
||||||
|
Write-Warn "Continuing with uninstallation..."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall npm global package if present
|
||||||
|
function Remove-NpmInstallation {
|
||||||
|
# Check if npm is available
|
||||||
|
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if safe-chain is installed as an npm global package
|
||||||
|
npm list -g @aikidosec/safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Detected npm global installation of @aikidosec/safe-chain"
|
||||||
|
Write-Info "Uninstalling npm version before installing binary version..."
|
||||||
|
|
||||||
|
npm uninstall -g @aikidosec/safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Successfully uninstalled npm version"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warn "Failed to uninstall npm version automatically"
|
||||||
|
Write-Warn "Please run: npm uninstall -g @aikidosec/safe-chain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall Volta-managed package if present
|
||||||
|
function Remove-VoltaInstallation {
|
||||||
|
# Check if Volta is available
|
||||||
|
if (-not (Get-Command volta -ErrorAction SilentlyContinue)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
# Volta manages global packages in its own directory
|
||||||
|
# Check if safe-chain is installed via Volta
|
||||||
|
volta list safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Detected Volta installation of @aikidosec/safe-chain"
|
||||||
|
Write-Info "Uninstalling Volta version before installing binary version..."
|
||||||
|
|
||||||
|
volta uninstall @aikidosec/safe-chain 2>&1 | Out-Null
|
||||||
|
if ($LASTEXITCODE -eq 0) {
|
||||||
|
Write-Info "Successfully uninstalled Volta version"
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Warn "Failed to uninstall Volta version automatically"
|
||||||
|
Write-Warn "Please run: volta uninstall @aikidosec/safe-chain"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main uninstallation
|
||||||
|
function Uninstall-SafeChain {
|
||||||
|
Write-Info "Uninstalling safe-chain..."
|
||||||
|
$DotSafeChain = Get-SafeChainInstallDir
|
||||||
|
$safeChainPath = Find-SafeChainBinary -DotSafeChain $DotSafeChain
|
||||||
|
Invoke-SafeChainTeardown -SafeChainPath $safeChainPath
|
||||||
|
|
||||||
|
# Remove npm and Volta installations
|
||||||
|
Remove-NpmInstallation
|
||||||
|
Remove-VoltaInstallation
|
||||||
|
|
||||||
|
# Remove .safe-chain directory
|
||||||
|
if (Test-Path $DotSafeChain) {
|
||||||
|
Write-Info "Removing installation directory: $DotSafeChain"
|
||||||
|
try {
|
||||||
|
Remove-Item -Path $DotSafeChain -Recurse -Force
|
||||||
|
Write-Info "Successfully removed installation directory"
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Failed to remove $DotSafeChain : $_"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Write-Info "Installation directory $DotSafeChain does not exist. Nothing to remove."
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Info "safe-chain has been uninstalled successfully!"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Run uninstallation
|
||||||
|
try {
|
||||||
|
Uninstall-SafeChain
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Error-Custom "Uninstallation failed: $_"
|
||||||
|
}
|
||||||
312
install-scripts/uninstall-safe-chain.sh
Executable file
312
install-scripts/uninstall-safe-chain.sh
Executable file
|
|
@ -0,0 +1,312 @@
|
||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
# Downloads and installs safe-chain, depending on the operating system and architecture
|
||||||
|
#
|
||||||
|
# Usage with "curl -fsSL {url} | sh" --> See README.md
|
||||||
|
|
||||||
|
set -e # Exit on error
|
||||||
|
|
||||||
|
# Configuration
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Helper functions
|
||||||
|
info() {
|
||||||
|
printf "${GREEN}[INFO]${NC} %s\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
warn() {
|
||||||
|
printf "${YELLOW}[WARN]${NC} %s\n" "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
error() {
|
||||||
|
printf "${RED}[ERROR]${NC} %s\n" "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check if command exists
|
||||||
|
command_exists() {
|
||||||
|
command -v "$1" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Resolves a path to its canonical filesystem location when possible.
|
||||||
|
# Follows symlinks so binary validation can inspect the real installed path.
|
||||||
|
resolve_path() {
|
||||||
|
target="$1"
|
||||||
|
|
||||||
|
while [ -L "$target" ]; do
|
||||||
|
link_target=$(readlink "$target" 2>/dev/null || echo "")
|
||||||
|
if [ -z "$link_target" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$link_target" in
|
||||||
|
/*) target="$link_target" ;;
|
||||||
|
*)
|
||||||
|
target="$(dirname "$target")/$link_target"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
target_dir=$(dirname "$target")
|
||||||
|
target_name=$(basename "$target")
|
||||||
|
|
||||||
|
if cd "$target_dir" 2>/dev/null; then
|
||||||
|
printf '%s/%s\n' "$(pwd -P)" "$target_name"
|
||||||
|
else
|
||||||
|
printf '%s\n' "$target"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Derives the safe-chain base install directory from a packaged binary path.
|
||||||
|
# Rejects wrapper scripts and paths that do not match the expected bin layout.
|
||||||
|
derive_install_dir_from_binary() {
|
||||||
|
binary_path="$1"
|
||||||
|
|
||||||
|
if [ -z "$binary_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
resolved_path=$(resolve_path "$binary_path")
|
||||||
|
binary_name=$(basename "$resolved_path")
|
||||||
|
case "$binary_name" in
|
||||||
|
safe-chain|safe-chain.exe) ;;
|
||||||
|
*) return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$resolved_path" in
|
||||||
|
*.js|*.cjs|*.mjs|*.cmd|*.ps1) return 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
binary_dir=$(dirname "$resolved_path")
|
||||||
|
if [ "$(basename "$binary_dir")" != "bin" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
dirname "$binary_dir"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Determines the installed safe-chain base directory for uninstall.
|
||||||
|
# Prefers the binary-reported location, then infers it from PATH, then falls back to ~/.safe-chain.
|
||||||
|
get_install_dir() {
|
||||||
|
reported_install_dir=$(get_reported_install_dir || true)
|
||||||
|
if [ -n "$reported_install_dir" ]; then
|
||||||
|
printf '%s\n' "$reported_install_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command_path=$(get_safe_chain_command_path || true)
|
||||||
|
install_dir=$(derive_install_dir_from_binary "$command_path" || true)
|
||||||
|
if [ -n "$install_dir" ]; then
|
||||||
|
printf '%s\n' "$install_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "${HOME}/.safe-chain"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the current safe-chain command path from PATH.
|
||||||
|
# Fails when safe-chain is not currently resolvable.
|
||||||
|
get_safe_chain_command_path() {
|
||||||
|
if ! command_exists safe-chain; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
command -v safe-chain
|
||||||
|
}
|
||||||
|
|
||||||
|
# Returns the safe-chain command path only when it resolves to a valid packaged binary install.
|
||||||
|
# Prevents the uninstaller from invoking arbitrary PATH entries.
|
||||||
|
get_validated_safe_chain_command_path() {
|
||||||
|
command_path=$(get_safe_chain_command_path || true)
|
||||||
|
if [ -z "$command_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_dir=$(derive_install_dir_from_binary "$command_path" || true)
|
||||||
|
if [ -z "$install_dir" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf '%s\n' "$command_path"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Asks the validated safe-chain binary for its install directory via get-install-dir.
|
||||||
|
# Returns nothing if the command is unavailable or the lookup fails.
|
||||||
|
get_reported_install_dir() {
|
||||||
|
safe_chain_path=$(get_validated_safe_chain_command_path || true)
|
||||||
|
if [ -z "$safe_chain_path" ]; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
install_dir=$("$safe_chain_path" get-install-dir 2>/dev/null || true)
|
||||||
|
if [ -n "$install_dir" ]; then
|
||||||
|
printf '%s\n' "$install_dir"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Locates the installed safe-chain binary to use for teardown.
|
||||||
|
# Checks the discovered install dir first, then falls back to a validated PATH entry.
|
||||||
|
find_installed_safe_chain_binary() {
|
||||||
|
dot_safe_chain="$1"
|
||||||
|
|
||||||
|
safe_chain_location="$dot_safe_chain/bin/safe-chain"
|
||||||
|
if [ -x "$safe_chain_location" ]; then
|
||||||
|
printf '%s\n' "$safe_chain_location"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
command_path=$(get_validated_safe_chain_command_path || true)
|
||||||
|
if [ -n "$command_path" ]; then
|
||||||
|
printf '%s\n' "$command_path"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Runs safe-chain teardown before removing files.
|
||||||
|
# Continues with uninstall even if teardown is unavailable or fails.
|
||||||
|
run_safe_chain_teardown() {
|
||||||
|
safe_chain_command="$1"
|
||||||
|
|
||||||
|
if [ -z "$safe_chain_command" ]; then
|
||||||
|
warn "safe-chain command not found. Proceeding with uninstallation."
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
info "Running safe-chain teardown..."
|
||||||
|
"$safe_chain_command" teardown || warn "safe-chain teardown encountered issues, continuing with uninstallation..."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall npm global package if present
|
||||||
|
remove_npm_installation() {
|
||||||
|
if ! command_exists npm; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if safe-chain is installed as an npm global package
|
||||||
|
if npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Detected npm global installation of @aikidosec/safe-chain"
|
||||||
|
info "Uninstalling npm version before installing binary version..."
|
||||||
|
|
||||||
|
if npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Successfully uninstalled npm version"
|
||||||
|
else
|
||||||
|
warn "Failed to uninstall npm version automatically"
|
||||||
|
warn "Please run: npm uninstall -g @aikidosec/safe-chain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall Volta-managed package if present
|
||||||
|
remove_volta_installation() {
|
||||||
|
if ! command_exists volta; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Volta manages global packages in its own directory
|
||||||
|
# Check if safe-chain is installed via Volta
|
||||||
|
if volta list safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Detected Volta installation of @aikidosec/safe-chain"
|
||||||
|
info "Uninstalling Volta version before installing binary version..."
|
||||||
|
|
||||||
|
if volta uninstall @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info "Successfully uninstalled Volta version"
|
||||||
|
else
|
||||||
|
warn "Failed to uninstall Volta version automatically"
|
||||||
|
warn "Please run: volta uninstall @aikidosec/safe-chain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Check and uninstall nvm-managed package if present across all Node versions
|
||||||
|
remove_nvm_installation() {
|
||||||
|
# This script is run in sh shell for greatest compatibility.
|
||||||
|
# Because nvm is usually setup in bash/zsh/fish startup scripts, we need to source it.
|
||||||
|
# Otherwise it won't be available in sh.
|
||||||
|
if [ -s "$HOME/.nvm/nvm.sh" ]; then
|
||||||
|
# Source nvm to make it available in this script
|
||||||
|
. "$HOME/.nvm/nvm.sh" >/dev/null 2>&1
|
||||||
|
elif [ -s "$NVM_DIR/nvm.sh" ]; then
|
||||||
|
. "$NVM_DIR/nvm.sh" >/dev/null 2>&1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if nvm is now available
|
||||||
|
if ! command_exists nvm; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get list of installed Node versions
|
||||||
|
nvm_versions=$(nvm list 2>/dev/null | grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' || echo "")
|
||||||
|
|
||||||
|
if [ -z "$nvm_versions" ]; then
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Track if we found any installations
|
||||||
|
found_installation=false
|
||||||
|
uninstall_failed=false
|
||||||
|
current_version=$(nvm current 2>/dev/null || echo "")
|
||||||
|
|
||||||
|
# Check each version for safe-chain installation
|
||||||
|
for version in $nvm_versions; do
|
||||||
|
# Check if this version has safe-chain installed
|
||||||
|
# Use nvm exec to run npm list in the context of that Node version
|
||||||
|
if nvm exec "$version" npm list -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
if [ "$found_installation" = false ]; then
|
||||||
|
info "Detected nvm installation(s) of @aikidosec/safe-chain"
|
||||||
|
info "Uninstalling from all Node versions..."
|
||||||
|
found_installation=true
|
||||||
|
fi
|
||||||
|
|
||||||
|
info " Removing from Node $version..."
|
||||||
|
if nvm exec "$version" npm uninstall -g @aikidosec/safe-chain >/dev/null 2>&1; then
|
||||||
|
info " Successfully uninstalled from Node $version"
|
||||||
|
else
|
||||||
|
warn " Failed to uninstall from Node $version"
|
||||||
|
uninstall_failed=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Restore original Node version if it was set
|
||||||
|
if [ -n "$current_version" ] && [ "$current_version" != "none" ] && [ "$current_version" != "system" ]; then
|
||||||
|
nvm use "$current_version" >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Show warning if any uninstall failed (but don't error out during uninstall)
|
||||||
|
if [ "$uninstall_failed" = true ]; then
|
||||||
|
warn "Failed to uninstall @aikidosec/safe-chain from some nvm Node versions"
|
||||||
|
warn "You may need to manually run: nvm exec <version> npm uninstall -g @aikidosec/safe-chain"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Main uninstallation
|
||||||
|
main() {
|
||||||
|
DOT_SAFE_CHAIN=$(get_install_dir)
|
||||||
|
SAFE_CHAIN_COMMAND=$(find_installed_safe_chain_binary "$DOT_SAFE_CHAIN" || true)
|
||||||
|
run_safe_chain_teardown "$SAFE_CHAIN_COMMAND"
|
||||||
|
|
||||||
|
# Check for existing safe-chain installation through nvm, volta, or npm
|
||||||
|
remove_npm_installation
|
||||||
|
remove_volta_installation
|
||||||
|
remove_nvm_installation
|
||||||
|
|
||||||
|
# Remove install dir recursively if it exists
|
||||||
|
if [ -d "$DOT_SAFE_CHAIN" ]; then
|
||||||
|
info "Removing installation directory $DOT_SAFE_CHAIN"
|
||||||
|
rm -rf "$DOT_SAFE_CHAIN" || error "Failed to remove $DOT_SAFE_CHAIN"
|
||||||
|
else
|
||||||
|
info "Installation directory $DOT_SAFE_CHAIN does not exist. Nothing to remove."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
3183
npm-shrinkwrap.json
generated
Normal file
3183
npm-shrinkwrap.json
generated
Normal file
File diff suppressed because it is too large
Load diff
6013
package-lock.json
generated
6013
package-lock.json
generated
File diff suppressed because it is too large
Load diff
16
package.json
16
package.json
|
|
@ -7,9 +7,10 @@
|
||||||
"test/e2e"
|
"test/e2e"
|
||||||
],
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "npm run test --workspace=packages/safe-chain --workspace=packages/safe-chain-bun",
|
"test": "npm run test --workspace=packages/safe-chain",
|
||||||
"test:e2e": "npm run test --workspace=test/e2e",
|
"test:e2e": "npm run test --workspace=test/e2e",
|
||||||
"lint": "npm run lint --workspace=packages/safe-chain"
|
"lint": "npm run lint --workspace=packages/safe-chain",
|
||||||
|
"typecheck": "npm run typecheck --workspace=packages/safe-chain"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|
@ -18,13 +19,8 @@
|
||||||
"author": "Aikido Security",
|
"author": "Aikido Security",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.26.0",
|
"oxlint": "^1.22.0",
|
||||||
"eslint": "^9.26.0",
|
"esbuild": "^0.27.0",
|
||||||
"eslint-plugin-import": "^2.31.0",
|
"@yao-pkg/pkg": "6.10.1"
|
||||||
"globals": "^16.1.0",
|
|
||||||
"typescript-eslint": "^8.32.0"
|
|
||||||
},
|
|
||||||
"overrides": {
|
|
||||||
"brace-expansion@<=2.0.2": "2.0.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
{
|
|
||||||
"name": "@aikidosec/safe-chain-bun",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"type": "module",
|
|
||||||
"main": "src/index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'"
|
|
||||||
},
|
|
||||||
"exports": {
|
|
||||||
".": {
|
|
||||||
"bun": "./src/index.js",
|
|
||||||
"default": "./src/index.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"keywords": ["bun", "security", "scanner", "malware", "aikido"],
|
|
||||||
"author": "Aikido Security",
|
|
||||||
"license": "AGPL-3.0-or-later",
|
|
||||||
"description": "Aikido Security Scanner for Bun package manager - detects malware and security threats during package installation",
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "git+https://github.com/AikidoSec/safe-chain.git",
|
|
||||||
"directory": "packages/safe-chain-bun"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@aikidosec/safe-chain": "file:../safe-chain"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"bun": ">=1.2.21"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
import { auditChanges } from "@aikidosec/safe-chain/scanning";
|
|
||||||
|
|
||||||
// Bun Security Scanner for Safe-Chain
|
|
||||||
// This is the entry point for Bun's native security scanner integration
|
|
||||||
|
|
||||||
export const scanner = {
|
|
||||||
version: "1", // Our scanner is using version 1 of the bun security scanner API.
|
|
||||||
|
|
||||||
async scan({ packages }) {
|
|
||||||
const advisories = [];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const changes = packages.map((pkg) => ({
|
|
||||||
name: pkg.name,
|
|
||||||
version: pkg.version,
|
|
||||||
type: "add",
|
|
||||||
}));
|
|
||||||
|
|
||||||
const audit = await auditChanges(changes);
|
|
||||||
|
|
||||||
if (!audit.isAllowed) {
|
|
||||||
for (const change of audit.disallowedChanges) {
|
|
||||||
advisories.push({
|
|
||||||
level: "fatal", // Fatal will block the installation process, this is what we want for packages that contain malware.
|
|
||||||
package: change.name,
|
|
||||||
url: null,
|
|
||||||
description: `Package ${change.name}@${change.version} contains known security threats (${change.reason}). Installation blocked by Safe-Chain.`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.warn(`Safe-Chain security scan failed: ${error.message}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return advisories;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
@ -1,140 +0,0 @@
|
||||||
import assert from "node:assert/strict";
|
|
||||||
import { describe, it, mock } from "node:test";
|
|
||||||
|
|
||||||
describe("Bun Scanner", async () => {
|
|
||||||
const mockAuditChanges = mock.fn();
|
|
||||||
|
|
||||||
// Mock the scanning module
|
|
||||||
mock.module("@aikidosec/safe-chain/scanning", {
|
|
||||||
namedExports: {
|
|
||||||
auditChanges: mockAuditChanges,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const { scanner } = await import("./index.js");
|
|
||||||
|
|
||||||
it("should export scanner object with version", () => {
|
|
||||||
assert.strictEqual(scanner.version, "1");
|
|
||||||
assert.strictEqual(typeof scanner.scan, "function");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return empty advisories for clean packages", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }],
|
|
||||||
disallowedChanges: [],
|
|
||||||
isAllowed: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const packages = [{ name: "express", version: "4.18.2" }];
|
|
||||||
const result = await scanner.scan({ packages });
|
|
||||||
|
|
||||||
assert.deepEqual(result, []);
|
|
||||||
assert.strictEqual(mockAuditChanges.mock.callCount(), 1);
|
|
||||||
assert.deepEqual(mockAuditChanges.mock.calls[0].arguments[0], [
|
|
||||||
{ name: "express", version: "4.18.2", type: "add" },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return fatal advisory for malware packages", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [],
|
|
||||||
disallowedChanges: [
|
|
||||||
{
|
|
||||||
name: "malicious-pkg",
|
|
||||||
version: "1.0.0",
|
|
||||||
type: "add",
|
|
||||||
reason: "MALWARE",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isAllowed: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const packages = [{ name: "malicious-pkg", version: "1.0.0" }];
|
|
||||||
const result = await scanner.scan({ packages });
|
|
||||||
|
|
||||||
assert.strictEqual(result.length, 1);
|
|
||||||
assert.deepEqual(result[0], {
|
|
||||||
level: "fatal",
|
|
||||||
package: "malicious-pkg",
|
|
||||||
url: null,
|
|
||||||
description: "Package malicious-pkg@1.0.0 contains known security threats (MALWARE). Installation blocked by Safe-Chain.",
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiple packages with mixed results", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [{ name: "express", version: "4.18.2", type: "add" }],
|
|
||||||
disallowedChanges: [
|
|
||||||
{
|
|
||||||
name: "malicious-pkg",
|
|
||||||
version: "1.0.0",
|
|
||||||
type: "add",
|
|
||||||
reason: "MALWARE",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "another-bad-pkg",
|
|
||||||
version: "2.1.0",
|
|
||||||
type: "add",
|
|
||||||
reason: "MALWARE",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
isAllowed: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const packages = [
|
|
||||||
{ name: "express", version: "4.18.2" },
|
|
||||||
{ name: "malicious-pkg", version: "1.0.0" },
|
|
||||||
{ name: "another-bad-pkg", version: "2.1.0" },
|
|
||||||
];
|
|
||||||
const result = await scanner.scan({ packages });
|
|
||||||
|
|
||||||
assert.strictEqual(result.length, 2);
|
|
||||||
assert.strictEqual(result[0].package, "malicious-pkg");
|
|
||||||
assert.strictEqual(result[0].level, "fatal");
|
|
||||||
assert.strictEqual(result[1].package, "another-bad-pkg");
|
|
||||||
assert.strictEqual(result[1].level, "fatal");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle empty package list", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [],
|
|
||||||
disallowedChanges: [],
|
|
||||||
isAllowed: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const result = await scanner.scan({ packages: [] });
|
|
||||||
|
|
||||||
assert.deepEqual(result, []);
|
|
||||||
assert.deepEqual(
|
|
||||||
mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1]
|
|
||||||
.arguments[0],
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should convert Bun package format to safe-chain format correctly", async () => {
|
|
||||||
mockAuditChanges.mock.mockImplementation(() => ({
|
|
||||||
allowedChanges: [],
|
|
||||||
disallowedChanges: [],
|
|
||||||
isAllowed: true,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const bunPackages = [
|
|
||||||
{ name: "lodash", version: "4.17.21" },
|
|
||||||
{ name: "@types/node", version: "20.0.0" },
|
|
||||||
];
|
|
||||||
|
|
||||||
await scanner.scan({ packages: bunPackages });
|
|
||||||
|
|
||||||
const expectedChanges = [
|
|
||||||
{ name: "lodash", version: "4.17.21", type: "add" },
|
|
||||||
{ name: "@types/node", version: "20.0.0", type: "add" },
|
|
||||||
];
|
|
||||||
|
|
||||||
assert.deepEqual(
|
|
||||||
mockAuditChanges.mock.calls[mockAuditChanges.mock.callCount() - 1]
|
|
||||||
.arguments[0],
|
|
||||||
expectedChanges
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
14
packages/safe-chain/bin/aikido-bun.js
Executable file
14
packages/safe-chain/bin/aikido-bun.js
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
|
const packageManagerName = "bun";
|
||||||
|
initializePackageManager(packageManagerName);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
14
packages/safe-chain/bin/aikido-bunx.js
Executable file
14
packages/safe-chain/bin/aikido-bunx.js
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
|
const packageManagerName = "bunx";
|
||||||
|
initializePackageManager(packageManagerName);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "npm";
|
const packageManagerName = "npm";
|
||||||
initializePackageManager(packageManagerName, process.versions.node);
|
initializePackageManager(packageManagerName);
|
||||||
await main(process.argv.slice(2));
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "npx";
|
const packageManagerName = "npx";
|
||||||
initializePackageManager(packageManagerName, process.versions.node);
|
initializePackageManager(packageManagerName);
|
||||||
await main(process.argv.slice(2));
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
|
||||||
13
packages/safe-chain/bin/aikido-pdm.js
Executable file
13
packages/safe-chain/bin/aikido-pdm.js
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
initializePackageManager("pdm");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
17
packages/safe-chain/bin/aikido-pip.js
Executable file
17
packages/safe-chain/bin/aikido-pip.js
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
import { PIP_PACKAGE_MANAGER, PIP_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP_COMMAND, args: process.argv.slice(2) });
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Pass through only user-supplied pip args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
17
packages/safe-chain/bin/aikido-pip3.js
Executable file
17
packages/safe-chain/bin/aikido-pip3.js
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
import { PIP_PACKAGE_MANAGER, PIP3_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PIP3_COMMAND, args: process.argv.slice(2) });
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Pass through only user-supplied pip args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
16
packages/safe-chain/bin/aikido-pipx.js
Executable file
16
packages/safe-chain/bin/aikido-pipx.js
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
initializePackageManager("pipx");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Pass through only user-supplied pipx args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "pnpm";
|
const packageManagerName = "pnpm";
|
||||||
initializePackageManager(packageManagerName, process.versions.node);
|
initializePackageManager(packageManagerName);
|
||||||
await main(process.argv.slice(2));
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "pnpx";
|
const packageManagerName = "pnpx";
|
||||||
initializePackageManager(packageManagerName, process.versions.node);
|
initializePackageManager(packageManagerName);
|
||||||
await main(process.argv.slice(2));
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
|
||||||
13
packages/safe-chain/bin/aikido-poetry.js
Executable file
13
packages/safe-chain/bin/aikido-poetry.js
Executable file
|
|
@ -0,0 +1,13 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
initializePackageManager("poetry");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
19
packages/safe-chain/bin/aikido-python.js
Executable file
19
packages/safe-chain/bin/aikido-python.js
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { PIP_PACKAGE_MANAGER, PYTHON_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
// Strip nodejs and wrapper script from args
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON_COMMAND, args: argv });
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(argv);
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
19
packages/safe-chain/bin/aikido-python3.js
Executable file
19
packages/safe-chain/bin/aikido-python3.js
Executable file
|
|
@ -0,0 +1,19 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { PIP_PACKAGE_MANAGER, PYTHON3_COMMAND } from "../src/packagemanager/pip/pipSettings.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
// Strip nodejs and wrapper script from args
|
||||||
|
let argv = process.argv.slice(2);
|
||||||
|
|
||||||
|
initializePackageManager(PIP_PACKAGE_MANAGER, { tool: PYTHON3_COMMAND, args: argv });
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(argv);
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
14
packages/safe-chain/bin/aikido-rush.js
Executable file
14
packages/safe-chain/bin/aikido-rush.js
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
|
const packageManagerName = "rush";
|
||||||
|
initializePackageManager(packageManagerName);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
14
packages/safe-chain/bin/aikido-rushx.js
Executable file
14
packages/safe-chain/bin/aikido-rushx.js
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
|
const packageManagerName = "rushx";
|
||||||
|
initializePackageManager(packageManagerName);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
16
packages/safe-chain/bin/aikido-uv.js
Executable file
16
packages/safe-chain/bin/aikido-uv.js
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
initializePackageManager("uv");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Pass through only user-supplied uv args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
16
packages/safe-chain/bin/aikido-uvx.js
Executable file
16
packages/safe-chain/bin/aikido-uvx.js
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_PY } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
// Set eco system
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
|
||||||
|
initializePackageManager("uvx");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
// Pass through only user-supplied uvx args
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
@ -2,7 +2,13 @@
|
||||||
|
|
||||||
import { main } from "../src/main.js";
|
import { main } from "../src/main.js";
|
||||||
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { setEcoSystem, ECOSYSTEM_JS } from "../src/config/settings.js";
|
||||||
|
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
const packageManagerName = "yarn";
|
const packageManagerName = "yarn";
|
||||||
initializePackageManager(packageManagerName, process.versions.node);
|
initializePackageManager(packageManagerName);
|
||||||
await main(process.argv.slice(2));
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(process.argv.slice(2));
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,42 @@
|
||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Strip PKG_EXECPATH from the environment so any child process safe-chain
|
||||||
|
// spawns (npm, uv, pip, …) doesn't inherit it. If it leaks into a subsequent
|
||||||
|
// safe-chain invocation (e.g. via a shim) the yao-pkg bootstrap would treat
|
||||||
|
// argv[1] as a script path and fail with MODULE_NOT_FOUND.
|
||||||
|
delete process.env.PKG_EXECPATH;
|
||||||
|
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import { ui } from "../src/environment/userInteraction.js";
|
import { ui } from "../src/environment/userInteraction.js";
|
||||||
import { setup } from "../src/shell-integration/setup.js";
|
import { setup } from "../src/shell-integration/setup.js";
|
||||||
import { teardown } from "../src/shell-integration/teardown.js";
|
import {
|
||||||
|
teardown,
|
||||||
|
teardownDirectories,
|
||||||
|
} from "../src/shell-integration/teardown.js";
|
||||||
|
import { setupCi } from "../src/shell-integration/setup-ci.js";
|
||||||
|
import { initializeCliArguments } from "../src/config/cliArguments.js";
|
||||||
|
import { setEcoSystem } from "../src/config/settings.js";
|
||||||
|
import { initializePackageManager } from "../src/packagemanager/currentPackageManager.js";
|
||||||
|
import { main } from "../src/main.js";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import fs from "fs";
|
||||||
|
import { knownAikidoTools, getPackageManagerList } from "../src/shell-integration/helpers.js";
|
||||||
|
import { getInstalledSafeChainDir } from "../src/installLocation.js";
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
// This checks the current file's dirname in a way that's compatible with:
|
||||||
|
// - Modulejs (import.meta.url)
|
||||||
|
// - ES modules (__dirname)
|
||||||
|
// This is needed because safe-chain's npm package is built using ES modules,
|
||||||
|
// but building the binaries requires commonjs.
|
||||||
|
let dirname;
|
||||||
|
if (import.meta.url) {
|
||||||
|
const filename = fileURLToPath(import.meta.url);
|
||||||
|
dirname = path.dirname(filename);
|
||||||
|
} else {
|
||||||
|
dirname = __dirname;
|
||||||
|
}
|
||||||
|
|
||||||
if (process.argv.length < 3) {
|
if (process.argv.length < 3) {
|
||||||
ui.writeError("No command provided. Please provide a command to execute.");
|
ui.writeError("No command provided. Please provide a command to execute.");
|
||||||
|
|
@ -12,17 +45,50 @@ if (process.argv.length < 3) {
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initializeCliArguments(process.argv);
|
||||||
|
|
||||||
const command = process.argv[2];
|
const command = process.argv[2];
|
||||||
|
|
||||||
if (command === "help" || command === "--help" || command === "-h") {
|
const tool = knownAikidoTools.find((tool) => tool.tool === command);
|
||||||
|
|
||||||
|
if (tool) {
|
||||||
|
const args = process.argv.slice(3);
|
||||||
|
|
||||||
|
setEcoSystem(tool.ecoSystem);
|
||||||
|
|
||||||
|
// Provide tool context to PM (pip uses this; others ignore)
|
||||||
|
const toolContext = { tool: tool.tool, args };
|
||||||
|
initializePackageManager(tool.internalPackageManagerName, toolContext);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
var exitCode = await main(args);
|
||||||
|
process.exit(exitCode);
|
||||||
|
})();
|
||||||
|
} else if (command === "help" || command === "--help" || command === "-h") {
|
||||||
writeHelp();
|
writeHelp();
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
} else if (command === "setup") {
|
||||||
|
|
||||||
if (command === "setup") {
|
|
||||||
setup();
|
setup();
|
||||||
} else if (command === "teardown") {
|
} else if (command === "teardown") {
|
||||||
teardown();
|
teardown();
|
||||||
|
teardownDirectories();
|
||||||
|
} else if (command === "setup-ci") {
|
||||||
|
setupCi();
|
||||||
|
} else if (command === "get-install-dir") {
|
||||||
|
const installDir = getInstalledSafeChainDir();
|
||||||
|
if (!installDir) {
|
||||||
|
ui.writeError(
|
||||||
|
"Install directory is only available for packaged safe-chain binaries.",
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.writeInformation(installDir);
|
||||||
|
process.exit(0);
|
||||||
|
} else if (command === "--version" || command === "-v" || command === "-v") {
|
||||||
|
(async () => {
|
||||||
|
ui.writeInformation(`Current safe-chain version: ${await getVersion()}`);
|
||||||
|
})();
|
||||||
} else {
|
} else {
|
||||||
ui.writeError(`Unknown command: ${command}.`);
|
ui.writeError(`Unknown command: ${command}.`);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
|
|
@ -34,24 +100,54 @@ if (command === "setup") {
|
||||||
|
|
||||||
function writeHelp() {
|
function writeHelp() {
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>")
|
chalk.bold("Usage: ") + chalk.cyan("safe-chain <command>"),
|
||||||
);
|
);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
`Available commands: ${chalk.cyan("setup")}, ${chalk.cyan(
|
||||||
"teardown"
|
"teardown",
|
||||||
)}, ${chalk.cyan("help")}`
|
)}, ${chalk.cyan("setup-ci")}, ${chalk.cyan("get-install-dir")}, ${chalk.cyan("help")}, ${chalk.cyan(
|
||||||
|
"--version",
|
||||||
|
)}`,
|
||||||
);
|
);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan(
|
`- ${chalk.cyan(
|
||||||
"safe-chain setup"
|
"safe-chain setup",
|
||||||
)}: This will setup your shell to wrap safe-chain around npm, npx, yarn, pnpm and pnpx.`
|
)}: This will setup your shell to wrap safe-chain around ${getPackageManagerList()}.`,
|
||||||
);
|
);
|
||||||
ui.writeInformation(
|
ui.writeInformation(
|
||||||
`- ${chalk.cyan(
|
`- ${chalk.cyan(
|
||||||
"safe-chain teardown"
|
"safe-chain teardown",
|
||||||
)}: This will remove safe-chain aliases from your shell configuration.`
|
)}: This will remove safe-chain aliases from your shell configuration.`,
|
||||||
|
);
|
||||||
|
ui.writeInformation(
|
||||||
|
`- ${chalk.cyan(
|
||||||
|
"safe-chain setup-ci",
|
||||||
|
)}: This will setup safe-chain for CI environments by creating shims and modifying the PATH.`,
|
||||||
|
);
|
||||||
|
ui.writeInformation(
|
||||||
|
`- ${chalk.cyan(
|
||||||
|
"safe-chain get-install-dir",
|
||||||
|
)}: Print the install directory for packaged safe-chain binaries.`,
|
||||||
|
);
|
||||||
|
ui.writeInformation(
|
||||||
|
`- ${chalk.cyan("safe-chain --version")} (or ${chalk.cyan(
|
||||||
|
"-v",
|
||||||
|
)}): Display the current version of safe-chain.`,
|
||||||
);
|
);
|
||||||
ui.emptyLine();
|
ui.emptyLine();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getVersion() {
|
||||||
|
const packageJsonPath = path.join(dirname, "..", "package.json");
|
||||||
|
|
||||||
|
const data = await fs.promises.readFile(packageJsonPath);
|
||||||
|
const json = JSON.parse(data.toString("utf8"));
|
||||||
|
|
||||||
|
if (json && json.version) {
|
||||||
|
return json.version;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "0.0.0";
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,8 @@
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
|
"test": "node --test --experimental-test-module-mocks 'src/**/*.spec.js'",
|
||||||
"test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
|
"test:watch": "node --test --watch --experimental-test-module-mocks 'src/**/*.spec.js'",
|
||||||
"lint": "eslint ."
|
"lint": "oxlint --deny-warnings",
|
||||||
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"aikido-npm": "bin/aikido-npm.js",
|
"aikido-npm": "bin/aikido-npm.js",
|
||||||
|
|
@ -12,6 +13,19 @@
|
||||||
"aikido-yarn": "bin/aikido-yarn.js",
|
"aikido-yarn": "bin/aikido-yarn.js",
|
||||||
"aikido-pnpm": "bin/aikido-pnpm.js",
|
"aikido-pnpm": "bin/aikido-pnpm.js",
|
||||||
"aikido-pnpx": "bin/aikido-pnpx.js",
|
"aikido-pnpx": "bin/aikido-pnpx.js",
|
||||||
|
"aikido-rush": "bin/aikido-rush.js",
|
||||||
|
"aikido-rushx": "bin/aikido-rushx.js",
|
||||||
|
"aikido-bun": "bin/aikido-bun.js",
|
||||||
|
"aikido-bunx": "bin/aikido-bunx.js",
|
||||||
|
"aikido-uv": "bin/aikido-uv.js",
|
||||||
|
"aikido-uvx": "bin/aikido-uvx.js",
|
||||||
|
"aikido-pip": "bin/aikido-pip.js",
|
||||||
|
"aikido-pip3": "bin/aikido-pip3.js",
|
||||||
|
"aikido-python": "bin/aikido-python.js",
|
||||||
|
"aikido-python3": "bin/aikido-python3.js",
|
||||||
|
"aikido-poetry": "bin/aikido-poetry.js",
|
||||||
|
"aikido-pipx": "bin/aikido-pipx.js",
|
||||||
|
"aikido-pdm": "bin/aikido-pdm.js",
|
||||||
"safe-chain": "bin/safe-chain.js"
|
"safe-chain": "bin/safe-chain.js"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|
@ -26,14 +40,26 @@
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "Aikido Security",
|
"author": "Aikido Security",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), and [pnpx](https://pnpm.io/cli/dlx) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, or pnpx from downloading or running the malware.",
|
"description": "The Aikido Safe Chain wraps around the [npm cli](https://github.com/npm/cli), [npx](https://github.com/npm/cli/blob/latest/docs/content/commands/npx.md), [yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/), [pnpx](https://pnpm.io/cli/dlx), [rush](https://rushjs.io/), [rushx](https://rushjs.io/pages/commands/rushx/), [bun](https://bun.sh/), [bunx](https://bun.sh/docs/cli/bunx), [uv](https://docs.astral.sh/uv/) (Python), [pip](https://pip.pypa.io/), and [pdm](https://pdm-project.org/) to provide extra checks before installing new packages. This tool will detect when a package contains malware and prompt you to exit, preventing npm, npx, yarn, pnpm, pnpx, rush, rushx, bun, bunx, uv, uvx, pip/pip3, or pdm from downloading or running the malware.",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@inquirer/prompts": "^7.4.1",
|
"certifi": "14.5.15",
|
||||||
"abbrev": "^3.0.1",
|
"chalk": "5.4.1",
|
||||||
"chalk": "^5.4.1",
|
"https-proxy-agent": "7.0.6",
|
||||||
"npm-registry-fetch": "^18.0.2",
|
"ini": "6.0.0",
|
||||||
"ora": "^8.2.0",
|
"make-fetch-happen": "15.0.3",
|
||||||
"semver": "^7.7.2"
|
"node-forge": "1.3.2",
|
||||||
|
"npm-registry-fetch": "19.1.1",
|
||||||
|
"semver": "7.7.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/ini": "^4.1.1",
|
||||||
|
"@types/make-fetch-happen": "^10.0.4",
|
||||||
|
"@types/node": "^18.19.130",
|
||||||
|
"@types/node-forge": "^1.3.14",
|
||||||
|
"@types/npm-registry-fetch": "^8.0.9",
|
||||||
|
"@types/semver": "^7.7.1",
|
||||||
|
"esbuild": "^0.27.0",
|
||||||
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"main": "src/main.js",
|
"main": "src/main.js",
|
||||||
"bugs": {
|
"bugs": {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,56 @@
|
||||||
const malwareDatabaseUrl =
|
import fetch from "make-fetch-happen";
|
||||||
"https://malware-list.aikido.dev/malware_predictions.json";
|
import {
|
||||||
|
getEcoSystem,
|
||||||
|
ECOSYSTEM_JS,
|
||||||
|
ECOSYSTEM_PY,
|
||||||
|
getMalwareListBaseUrl,
|
||||||
|
} from "../config/settings.js";
|
||||||
|
import { ui } from "../environment/userInteraction.js";
|
||||||
|
|
||||||
|
const malwareDatabasePaths = {
|
||||||
|
[ECOSYSTEM_JS]: "malware_predictions.json",
|
||||||
|
[ECOSYSTEM_PY]: "malware_pypi.json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const newPackagesListPaths = {
|
||||||
|
[ECOSYSTEM_JS]: "releases/npm.json",
|
||||||
|
[ECOSYSTEM_PY]: "releases/pypi.json",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_FETCH_RETRY_ATTEMPTS = 4;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} MalwarePackage
|
||||||
|
* @property {string} package_name
|
||||||
|
* @property {string} version
|
||||||
|
* @property {string} reason
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} NewPackageEntry
|
||||||
|
* @property {string} [source]
|
||||||
|
* @property {string} package_name
|
||||||
|
* @property {string} version
|
||||||
|
* @property {number} released_on - Unix timestamp (seconds)
|
||||||
|
* @property {number} scraped_on - Unix timestamp (seconds)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<{malwareDatabase: MalwarePackage[], version: string | undefined}>}
|
||||||
|
*/
|
||||||
export async function fetchMalwareDatabase() {
|
export async function fetchMalwareDatabase() {
|
||||||
|
return retry(async () => {
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
const baseUrl = getMalwareListBaseUrl();
|
||||||
|
const path = malwareDatabasePaths[
|
||||||
|
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
||||||
|
];
|
||||||
|
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
||||||
const response = await fetch(malwareDatabaseUrl);
|
const response = await fetch(malwareDatabaseUrl);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Error fetching malware database: ${response.statusText}`);
|
throw new Error(
|
||||||
|
`Error fetching ${ecosystem} malware database: ${response.statusText}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
@ -13,19 +59,129 @@ export async function fetchMalwareDatabase() {
|
||||||
malwareDatabase: malwareDatabase,
|
malwareDatabase: malwareDatabase,
|
||||||
version: response.headers.get("etag") || undefined,
|
version: response.headers.get("etag") || undefined,
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (/** @type {any} */ error) {
|
||||||
throw new Error(`Error parsing malware database: ${error.message}`);
|
throw new Error(`Error parsing malware database: ${error.message}`);
|
||||||
}
|
}
|
||||||
|
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<string | undefined>}
|
||||||
|
*/
|
||||||
export async function fetchMalwareDatabaseVersion() {
|
export async function fetchMalwareDatabaseVersion() {
|
||||||
|
return retry(async () => {
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
const baseUrl = getMalwareListBaseUrl();
|
||||||
|
const path = malwareDatabasePaths[
|
||||||
|
/** @type {keyof typeof malwareDatabasePaths} */ (ecosystem)
|
||||||
|
];
|
||||||
|
const malwareDatabaseUrl = `${baseUrl}/${path}`;
|
||||||
const response = await fetch(malwareDatabaseUrl, {
|
const response = await fetch(malwareDatabaseUrl, {
|
||||||
method: "HEAD",
|
method: "HEAD",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Error fetching malware database version: ${response.statusText}`
|
`Error fetching ${ecosystem} malware database version: ${response.statusText}`
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return response.headers.get("etag") || undefined;
|
return response.headers.get("etag") || undefined;
|
||||||
|
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<{newPackagesList: NewPackageEntry[], version: string | undefined}>}
|
||||||
|
*/
|
||||||
|
export async function fetchNewPackagesList() {
|
||||||
|
return retry(async () => {
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
const baseUrl = getMalwareListBaseUrl();
|
||||||
|
const path = newPackagesListPaths[/** @type {keyof typeof newPackagesListPaths} */ (ecosystem)];
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return { newPackagesList: [], version: undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${baseUrl}/${path}`;
|
||||||
|
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Error fetching ${ecosystem} new packages list: ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newPackagesList = await response.json();
|
||||||
|
return {
|
||||||
|
newPackagesList,
|
||||||
|
version: response.headers.get("etag") || undefined,
|
||||||
|
};
|
||||||
|
} catch (/** @type {any} */ error) {
|
||||||
|
throw new Error(`Error parsing new packages list: ${error.message}`);
|
||||||
|
}
|
||||||
|
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<string | undefined>}
|
||||||
|
*/
|
||||||
|
export async function fetchNewPackagesListVersion() {
|
||||||
|
return retry(async () => {
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
const baseUrl = getMalwareListBaseUrl();
|
||||||
|
const path = newPackagesListPaths[/** @type {keyof typeof newPackagesListPaths} */ (ecosystem)];
|
||||||
|
|
||||||
|
if (!path) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = `${baseUrl}/${path}`;
|
||||||
|
|
||||||
|
const response = await fetch(url, { method: "HEAD" });
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(
|
||||||
|
`Error fetching ${ecosystem} new packages list version: ${response.statusText}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response.headers.get("etag") || undefined;
|
||||||
|
}, DEFAULT_FETCH_RETRY_ATTEMPTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retries an asynchronous function multiple times until it succeeds or exhausts all attempts.
|
||||||
|
*
|
||||||
|
* @template T
|
||||||
|
* @param {() => Promise<T>} func - The asynchronous function to retry
|
||||||
|
* @param {number} attempts - The number of attempts
|
||||||
|
* @returns {Promise<T>} The return value of the function if successful
|
||||||
|
* @throws {Error} The last error encountered if all retry attempts fail
|
||||||
|
*/
|
||||||
|
async function retry(func, attempts) {
|
||||||
|
let lastError;
|
||||||
|
|
||||||
|
for (let i = 0; i < attempts; i++) {
|
||||||
|
try {
|
||||||
|
return await func();
|
||||||
|
} catch (error) {
|
||||||
|
ui.writeVerbose(
|
||||||
|
"An error occurred while trying to download Aikido data",
|
||||||
|
error
|
||||||
|
);
|
||||||
|
lastError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < attempts - 1) {
|
||||||
|
// When this is not the last try, back-off exponentially:
|
||||||
|
// 1st attempt - 500ms delay
|
||||||
|
// 2nd attempt - 1000ms delay
|
||||||
|
// 3rd attempt - 2000ms delay
|
||||||
|
// 4th attempt - 4000ms delay
|
||||||
|
// ...
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, i) * 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw lastError;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
231
packages/safe-chain/src/api/aikido.spec.js
Normal file
231
packages/safe-chain/src/api/aikido.spec.js
Normal file
|
|
@ -0,0 +1,231 @@
|
||||||
|
import { describe, it, mock, beforeEach } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("aikido API", async () => {
|
||||||
|
const mockFetch = mock.fn();
|
||||||
|
let ecosystem = "js";
|
||||||
|
|
||||||
|
mock.module("make-fetch-happen", {
|
||||||
|
defaultExport: mockFetch,
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module("../environment/userInteraction.js", {
|
||||||
|
namedExports: {
|
||||||
|
ui: {
|
||||||
|
writeVerbose: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module("../config/settings.js", {
|
||||||
|
namedExports: {
|
||||||
|
getEcoSystem: () => ecosystem,
|
||||||
|
ECOSYSTEM_JS: "js",
|
||||||
|
ECOSYSTEM_PY: "py",
|
||||||
|
getMalwareListBaseUrl: () => "https://malware-list.aikido.dev",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
fetchMalwareDatabase,
|
||||||
|
fetchMalwareDatabaseVersion,
|
||||||
|
fetchNewPackagesList,
|
||||||
|
fetchNewPackagesListVersion,
|
||||||
|
} = await import("./aikido.js");
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockFetch.mock.resetCalls();
|
||||||
|
ecosystem = "js";
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchMalwareDatabase", () => {
|
||||||
|
it("should succeed immediately when fetch succeeds on first try", async () => {
|
||||||
|
const malwareData = [
|
||||||
|
{ package_name: "malicious-pkg", version: "1.0.0", reason: "test" },
|
||||||
|
];
|
||||||
|
mockFetch.mock.mockImplementationOnce(() => ({
|
||||||
|
ok: true,
|
||||||
|
json: async () => malwareData,
|
||||||
|
headers: { get: () => '"etag-123"' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await fetchMalwareDatabase();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 1);
|
||||||
|
assert.deepStrictEqual(result.malwareDatabase, malwareData);
|
||||||
|
assert.strictEqual(result.version, '"etag-123"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error after exhausting all retries", async () => {
|
||||||
|
mockFetch.mock.mockImplementation(() => {
|
||||||
|
throw new Error("Network error");
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(() => fetchMalwareDatabase(), {
|
||||||
|
message: "Network error",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed after failing 3 times and succeeding on 4th attempt", async () => {
|
||||||
|
const malwareData = [
|
||||||
|
{ package_name: "bad-pkg", version: "2.0.0", reason: "malware" },
|
||||||
|
];
|
||||||
|
let callCount = 0;
|
||||||
|
mockFetch.mock.mockImplementation(() => {
|
||||||
|
callCount++;
|
||||||
|
if (callCount < 4) {
|
||||||
|
throw new Error("Network error");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
json: async () => malwareData,
|
||||||
|
headers: { get: () => '"etag-456"' },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchMalwareDatabase();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 4);
|
||||||
|
assert.deepStrictEqual(result.malwareDatabase, malwareData);
|
||||||
|
assert.strictEqual(result.version, '"etag-456"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchMalwareDatabaseVersion", () => {
|
||||||
|
it("should succeed immediately when fetch succeeds on first try", async () => {
|
||||||
|
mockFetch.mock.mockImplementationOnce(() => ({
|
||||||
|
ok: true,
|
||||||
|
headers: { get: () => '"version-etag"' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await fetchMalwareDatabaseVersion();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(result, '"version-etag"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error after exhausting all retries", async () => {
|
||||||
|
mockFetch.mock.mockImplementation(() => {
|
||||||
|
throw new Error("Connection refused");
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(() => fetchMalwareDatabaseVersion(), {
|
||||||
|
message: "Connection refused",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should succeed after failing 3 times and succeeding on 4th attempt", async () => {
|
||||||
|
let callCount = 0;
|
||||||
|
mockFetch.mock.mockImplementation(() => {
|
||||||
|
callCount++;
|
||||||
|
if (callCount < 4) {
|
||||||
|
throw new Error("Timeout");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
headers: { get: () => '"final-etag"' },
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchMalwareDatabaseVersion();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 4);
|
||||||
|
assert.strictEqual(result, '"final-etag"');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchNewPackagesList", () => {
|
||||||
|
it("should succeed immediately when fetch succeeds on first try", async () => {
|
||||||
|
const releases = [
|
||||||
|
{
|
||||||
|
package_name: "fresh-pkg",
|
||||||
|
version: "1.0.0",
|
||||||
|
released_on: 123,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
mockFetch.mock.mockImplementationOnce(() => ({
|
||||||
|
ok: true,
|
||||||
|
json: async () => releases,
|
||||||
|
headers: { get: () => '"etag-new-packages"' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await fetchNewPackagesList();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(
|
||||||
|
mockFetch.mock.calls[0].arguments[0],
|
||||||
|
"https://malware-list.aikido.dev/releases/npm.json"
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(result.newPackagesList, releases);
|
||||||
|
assert.strictEqual(result.version, '"etag-new-packages"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error after exhausting all retries", async () => {
|
||||||
|
mockFetch.mock.mockImplementation(() => {
|
||||||
|
throw new Error("Network error");
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(() => fetchNewPackagesList(), {
|
||||||
|
message: "Network error",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return an empty list without fetching for unsupported ecosystems", async () => {
|
||||||
|
ecosystem = "ruby";
|
||||||
|
|
||||||
|
const result = await fetchNewPackagesList();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 0);
|
||||||
|
assert.deepStrictEqual(result.newPackagesList, []);
|
||||||
|
assert.strictEqual(result.version, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined version without fetching for unsupported ecosystems", async () => {
|
||||||
|
ecosystem = "ruby";
|
||||||
|
|
||||||
|
const result = await fetchNewPackagesListVersion();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 0);
|
||||||
|
assert.strictEqual(result, undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetchNewPackagesListVersion", () => {
|
||||||
|
it("should succeed immediately when fetch succeeds on first try", async () => {
|
||||||
|
mockFetch.mock.mockImplementationOnce(() => ({
|
||||||
|
ok: true,
|
||||||
|
headers: { get: () => '"new-packages-etag"' },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await fetchNewPackagesListVersion();
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 1);
|
||||||
|
assert.strictEqual(
|
||||||
|
mockFetch.mock.calls[0].arguments[0],
|
||||||
|
"https://malware-list.aikido.dev/releases/npm.json"
|
||||||
|
);
|
||||||
|
assert.deepStrictEqual(mockFetch.mock.calls[0].arguments[1], {
|
||||||
|
method: "HEAD",
|
||||||
|
});
|
||||||
|
assert.strictEqual(result, '"new-packages-etag"');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw error after exhausting all retries", async () => {
|
||||||
|
mockFetch.mock.mockImplementation(() => {
|
||||||
|
throw new Error("Connection refused");
|
||||||
|
});
|
||||||
|
|
||||||
|
await assert.rejects(() => fetchNewPackagesListVersion(), {
|
||||||
|
message: "Connection refused",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(mockFetch.mock.calls.length, 4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
import * as semver from "semver";
|
import * as semver from "semver";
|
||||||
import * as npmFetch from "npm-registry-fetch";
|
import * as npmFetch from "npm-registry-fetch";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} packageName
|
||||||
|
* @param {string | null} [versionRange]
|
||||||
|
* @returns {Promise<string | null>}
|
||||||
|
*/
|
||||||
export async function resolvePackageVersion(packageName, versionRange) {
|
export async function resolvePackageVersion(packageName, versionRange) {
|
||||||
if (!versionRange) {
|
if (!versionRange) {
|
||||||
versionRange = "latest";
|
versionRange = "latest";
|
||||||
|
|
@ -11,7 +16,10 @@ export async function resolvePackageVersion(packageName, versionRange) {
|
||||||
return versionRange;
|
return versionRange;
|
||||||
}
|
}
|
||||||
|
|
||||||
const packageInfo = await getPackageInfo(packageName);
|
const packageInfo = (
|
||||||
|
/** @type {{"dist-tags"?: Record<string, string>, versions?: Record<string, unknown>} | null} */
|
||||||
|
await getPackageInfo(packageName)
|
||||||
|
);
|
||||||
if (!packageInfo) {
|
if (!packageInfo) {
|
||||||
// It is possible that no version is found (could be a private package, or a package that doesn't exist)
|
// It is possible that no version is found (could be a private package, or a package that doesn't exist)
|
||||||
// In this case, we return null to indicate that we couldn't resolve the version
|
// In this case, we return null to indicate that we couldn't resolve the version
|
||||||
|
|
@ -19,12 +27,16 @@ export async function resolvePackageVersion(packageName, versionRange) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const distTags = packageInfo["dist-tags"];
|
const distTags = packageInfo["dist-tags"];
|
||||||
if (distTags && distTags[versionRange]) {
|
if (distTags && isDistTags(distTags) && distTags[versionRange]) {
|
||||||
// If the version range is a dist-tag, return the version associated with that tag
|
// If the version range is a dist-tag, return the version associated with that tag
|
||||||
// e.g., "latest", "next", etc.
|
// e.g., "latest", "next", etc.
|
||||||
return distTags[versionRange];
|
return distTags[versionRange];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!packageInfo.versions) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// If the version range is not a dist-tag, we need to resolve the highest version matching the range.
|
// If the version range is not a dist-tag, we need to resolve the highest version matching the range.
|
||||||
// This is useful for ranges like "^1.0.0" or "~2.3.4".
|
// This is useful for ranges like "^1.0.0" or "~2.3.4".
|
||||||
const availableVersions = Object.keys(packageInfo.versions);
|
const availableVersions = Object.keys(packageInfo.versions);
|
||||||
|
|
@ -37,6 +49,19 @@ export async function resolvePackageVersion(packageName, versionRange) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {unknown} distTags
|
||||||
|
* @returns {distTags is Record<string, string>}
|
||||||
|
*/
|
||||||
|
function isDistTags(distTags) {
|
||||||
|
return typeof distTags === "object";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} packageName
|
||||||
|
* @returns {Promise<Record<string, unknown> | null>}
|
||||||
|
*/
|
||||||
async function getPackageInfo(packageName) {
|
async function getPackageInfo(packageName) {
|
||||||
try {
|
try {
|
||||||
return await npmFetch.json(packageName);
|
return await npmFetch.json(packageName);
|
||||||
|
|
|
||||||
211
packages/safe-chain/src/api/npmApi.spec.js
Normal file
211
packages/safe-chain/src/api/npmApi.spec.js
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
import { describe, it, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("resolvePackageVersion", async () => {
|
||||||
|
const mockNpmFetchJson = mock.fn();
|
||||||
|
|
||||||
|
mock.module("npm-registry-fetch", {
|
||||||
|
namedExports: {
|
||||||
|
json: mockNpmFetchJson,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { resolvePackageVersion } = await import("./npmApi.js");
|
||||||
|
|
||||||
|
it("should return the version if it is already a fixed version", async () => {
|
||||||
|
const result = await resolvePackageVersion("express", "4.17.1");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "4.17.1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use 'latest' as default version range when not provided", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "4.18.2",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"4.18.2": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "4.18.2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve dist-tag versions", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "4.18.2",
|
||||||
|
next: "5.0.0-beta.1",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"4.18.2": {},
|
||||||
|
"5.0.0-beta.1": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express", "next");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "5.0.0-beta.1");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve version ranges using semver", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "4.18.2",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"4.16.0": {},
|
||||||
|
"4.17.0": {},
|
||||||
|
"4.17.1": {},
|
||||||
|
"4.18.0": {},
|
||||||
|
"4.18.2": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express", "^4.17.0");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "4.18.2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve tilde ranges correctly", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "4.18.2",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"4.17.0": {},
|
||||||
|
"4.17.1": {},
|
||||||
|
"4.17.3": {},
|
||||||
|
"4.18.0": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express", "~4.17.0");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "4.17.3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null if package info cannot be fetched", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => {
|
||||||
|
throw new Error("Package not found");
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("non-existent-package", "latest");
|
||||||
|
|
||||||
|
assert.strictEqual(result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null if no versions match the range", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "1.0.0",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"1.0.0": {},
|
||||||
|
"1.1.0": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express", "^5.0.0");
|
||||||
|
|
||||||
|
assert.strictEqual(result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null if dist-tag does not exist", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "4.18.2",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"4.18.2": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express", "nonexistent-tag");
|
||||||
|
|
||||||
|
assert.strictEqual(result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null if package info has no versions property (retracted package)", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
_id: "zenn",
|
||||||
|
name: "zenn",
|
||||||
|
time: {
|
||||||
|
modified: "2021-04-20T16:20:56.084Z",
|
||||||
|
created: "2017-07-10T19:48:07.891Z",
|
||||||
|
unpublished: {
|
||||||
|
time: "2021-04-20T16:20:56.084Z",
|
||||||
|
versions: [
|
||||||
|
"0.9.0",
|
||||||
|
"0.9.1",
|
||||||
|
"0.9.2",
|
||||||
|
"0.9.3",
|
||||||
|
"0.9.4",
|
||||||
|
"0.9.5",
|
||||||
|
"0.9.6",
|
||||||
|
"0.9.8",
|
||||||
|
"0.9.9",
|
||||||
|
"0.9.10",
|
||||||
|
"0.9.11",
|
||||||
|
"0.9.12",
|
||||||
|
"0.9.13",
|
||||||
|
"0.9.14",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("zenn", "^0.9.0");
|
||||||
|
|
||||||
|
assert.strictEqual(result, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return dist-tag version even if versions property is missing", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "4.18.2",
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express", "latest");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "4.18.2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle scoped packages", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "1.2.3",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"1.2.3": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("@scope/package", "latest");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "1.2.3");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle complex version ranges", async () => {
|
||||||
|
mockNpmFetchJson.mock.mockImplementationOnce(() => ({
|
||||||
|
"dist-tags": {
|
||||||
|
latest: "2.5.0",
|
||||||
|
},
|
||||||
|
versions: {
|
||||||
|
"1.0.0": {},
|
||||||
|
"2.0.0": {},
|
||||||
|
"2.3.0": {},
|
||||||
|
"2.4.0": {},
|
||||||
|
"2.5.0": {},
|
||||||
|
"3.0.0": {},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await resolvePackageVersion("express", ">=2.0.0 <3.0.0");
|
||||||
|
|
||||||
|
assert.strictEqual(result, "2.5.0");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,12 +1,27 @@
|
||||||
|
import { ui } from "../environment/userInteraction.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {{loggingLevel: string | undefined, skipMinimumPackageAge: boolean | undefined, minimumPackageAgeHours: string | undefined, malwareListBaseUrl: string | undefined}}
|
||||||
|
*/
|
||||||
const state = {
|
const state = {
|
||||||
malwareAction: undefined,
|
loggingLevel: undefined,
|
||||||
|
skipMinimumPackageAge: undefined,
|
||||||
|
minimumPackageAgeHours: undefined,
|
||||||
|
malwareListBaseUrl: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
const SAFE_CHAIN_ARG_PREFIX = "--safe-chain-";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
export function initializeCliArguments(args) {
|
export function initializeCliArguments(args) {
|
||||||
// Reset state on each call
|
// Reset state on each call
|
||||||
state.malwareAction = undefined;
|
state.loggingLevel = undefined;
|
||||||
|
state.skipMinimumPackageAge = undefined;
|
||||||
|
state.minimumPackageAgeHours = undefined;
|
||||||
|
state.malwareListBaseUrl = undefined;
|
||||||
|
|
||||||
const safeChainArgs = [];
|
const safeChainArgs = [];
|
||||||
const remainingArgs = [];
|
const remainingArgs = [];
|
||||||
|
|
@ -19,21 +34,19 @@ export function initializeCliArguments(args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setMalwareAction(safeChainArgs);
|
setLoggingLevel(safeChainArgs);
|
||||||
|
setSkipMinimumPackageAge(safeChainArgs);
|
||||||
|
setMinimumPackageAgeHours(safeChainArgs);
|
||||||
|
setMalwareListBaseUrl(safeChainArgs);
|
||||||
|
checkDeprecatedPythonFlag(args);
|
||||||
return remainingArgs;
|
return remainingArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setMalwareAction(args) {
|
/**
|
||||||
const safeChainMalwareActionArg = SAFE_CHAIN_ARG_PREFIX + "malware-action=";
|
* @param {string[]} args
|
||||||
|
* @param {string} prefix
|
||||||
const action = getLastArgEqualsValue(args, safeChainMalwareActionArg);
|
* @returns {string | undefined}
|
||||||
if (!action) {
|
*/
|
||||||
return;
|
|
||||||
}
|
|
||||||
state.malwareAction = action.toLowerCase();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLastArgEqualsValue(args, prefix) {
|
function getLastArgEqualsValue(args, prefix) {
|
||||||
for (var i = args.length - 1; i >= 0; i--) {
|
for (var i = args.length - 1; i >= 0; i--) {
|
||||||
const arg = args[i];
|
const arg = args[i];
|
||||||
|
|
@ -45,6 +58,104 @@ function getLastArgEqualsValue(args, prefix) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMalwareAction() {
|
/**
|
||||||
return state.malwareAction;
|
* @param {string[]} args
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function setLoggingLevel(args) {
|
||||||
|
const safeChainLoggingArg = SAFE_CHAIN_ARG_PREFIX + "logging=";
|
||||||
|
|
||||||
|
const level = getLastArgEqualsValue(args, safeChainLoggingArg);
|
||||||
|
if (!level) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.loggingLevel = level.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLoggingLevel() {
|
||||||
|
return state.loggingLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function setSkipMinimumPackageAge(args) {
|
||||||
|
const flagName = SAFE_CHAIN_ARG_PREFIX + "skip-minimum-package-age";
|
||||||
|
|
||||||
|
if (hasFlagArg(args, flagName)) {
|
||||||
|
state.skipMinimumPackageAge = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getSkipMinimumPackageAge() {
|
||||||
|
return state.skipMinimumPackageAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function setMinimumPackageAgeHours(args) {
|
||||||
|
const argName = SAFE_CHAIN_ARG_PREFIX + "minimum-package-age-hours=";
|
||||||
|
|
||||||
|
const value = getLastArgEqualsValue(args, argName);
|
||||||
|
if (value) {
|
||||||
|
state.minimumPackageAgeHours = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getMinimumPackageAgeHours() {
|
||||||
|
return state.minimumPackageAgeHours;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
function setMalwareListBaseUrl(args) {
|
||||||
|
const argName = SAFE_CHAIN_ARG_PREFIX + "malware-list-base-url=";
|
||||||
|
|
||||||
|
const value = getLastArgEqualsValue(args, argName);
|
||||||
|
if (value) {
|
||||||
|
state.malwareListBaseUrl = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getMalwareListBaseUrl() {
|
||||||
|
return state.malwareListBaseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @param {string} flagName
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
function hasFlagArg(args, flagName) {
|
||||||
|
for (const arg of args) {
|
||||||
|
if (arg.toLowerCase() === flagName.toLowerCase()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits a deprecation warning for legacy --include-python flag
|
||||||
|
*
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
|
export function checkDeprecatedPythonFlag(args) {
|
||||||
|
if (hasFlagArg(args, "--include-python")) {
|
||||||
|
ui.writeWarning(
|
||||||
|
"--include-python is deprecated and ignored. Python tooling is included by default."
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
import { describe, it } from "node:test";
|
import { describe, it } from "node:test";
|
||||||
import assert from "node:assert";
|
import assert from "node:assert";
|
||||||
import { initializeCliArguments, getMalwareAction } from "./cliArguments.js";
|
import {
|
||||||
|
initializeCliArguments,
|
||||||
|
getLoggingLevel,
|
||||||
|
getSkipMinimumPackageAge,
|
||||||
|
getMinimumPackageAgeHours,
|
||||||
|
} from "./cliArguments.js";
|
||||||
|
import { ui } from "../environment/userInteraction.js";
|
||||||
|
|
||||||
describe("initializeCliArguments", () => {
|
describe("initializeCliArguments", () => {
|
||||||
it("should return all args when no safe-chain args are present", () => {
|
it("should return all args when no safe-chain args are present", () => {
|
||||||
|
|
@ -57,52 +63,249 @@ describe("initializeCliArguments", () => {
|
||||||
assert.deepEqual(result, ["install", "my--safe-chain-package", "--save"]);
|
assert.deepEqual(result, ["install", "my--safe-chain-package", "--save"]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should not set malwareAction when no safe-chain arguments are passed", () => {
|
it("should not set loggingLevel when no logging argument is passed", () => {
|
||||||
const args = ["install", "express", "--save"];
|
const args = ["install", "express", "--save"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getLoggingLevel(), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse logging=silent and set state", () => {
|
||||||
|
const args = ["--safe-chain-logging=silent", "install", "package"];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install", "package"]);
|
||||||
|
assert.strictEqual(getLoggingLevel(), "silent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse logging=normal and set state", () => {
|
||||||
|
const args = ["--safe-chain-logging=normal", "install", "package"];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install", "package"]);
|
||||||
|
assert.strictEqual(getLoggingLevel(), "normal");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle multiple logging args, using the last one", () => {
|
||||||
|
const args = [
|
||||||
|
"--safe-chain-logging=normal",
|
||||||
|
"--safe-chain-logging=silent",
|
||||||
|
"install",
|
||||||
|
];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install"]);
|
||||||
|
assert.strictEqual(getLoggingLevel(), "silent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle logging level case-insensitively", () => {
|
||||||
|
const args = ["--safe-chain-logging=SILENT", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getLoggingLevel(), "silent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should capture invalid logging level as-is (lowercased)", () => {
|
||||||
|
const args = ["--safe-chain-logging=invalid", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getLoggingLevel(), "invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle logging with other safe-chain args", () => {
|
||||||
|
const args = [
|
||||||
|
"--safe-chain-debug",
|
||||||
|
"--safe-chain-logging=silent",
|
||||||
|
"--safe-chain-malware-action=block",
|
||||||
|
"install",
|
||||||
|
];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install"]);
|
||||||
|
assert.strictEqual(getLoggingLevel(), "silent");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should not set skipMinimumPackageAge when flag is absent", () => {
|
||||||
|
const args = ["install", "express", "--save"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getSkipMinimumPackageAge(), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set skipMinimumPackageAge to true when flag is present", () => {
|
||||||
|
const args = ["--safe-chain-skip-minimum-package-age", "install", "lodash"];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install", "lodash"]);
|
||||||
|
assert.strictEqual(getSkipMinimumPackageAge(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle skip-minimum-package-age flag case-insensitively", () => {
|
||||||
|
const args = ["--SAFE-CHAIN-SKIP-MINIMUM-PACKAGE-AGE", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getSkipMinimumPackageAge(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter out skip-minimum-package-age flag from returned args", () => {
|
||||||
|
const args = [
|
||||||
|
"install",
|
||||||
|
"--safe-chain-skip-minimum-package-age",
|
||||||
|
"express",
|
||||||
|
"--save",
|
||||||
|
];
|
||||||
const result = initializeCliArguments(args);
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
assert.deepEqual(result, ["install", "express", "--save"]);
|
assert.deepEqual(result, ["install", "express", "--save"]);
|
||||||
assert.strictEqual(getMalwareAction(), undefined);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse malware-action=block and set state", () => {
|
it("should handle skip-minimum-package-age with other safe-chain arguments", () => {
|
||||||
const args = ["--safe-chain-malware-action=block", "install", "package"];
|
|
||||||
const result = initializeCliArguments(args);
|
|
||||||
|
|
||||||
assert.deepEqual(result, ["install", "package"]);
|
|
||||||
assert.strictEqual(getMalwareAction(), "block");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse malware-action=prompt and set state", () => {
|
|
||||||
const args = ["--safe-chain-malware-action=prompt", "install", "package"];
|
|
||||||
const result = initializeCliArguments(args);
|
|
||||||
|
|
||||||
assert.deepEqual(result, ["install", "package"]);
|
|
||||||
assert.strictEqual(getMalwareAction(), "prompt");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should handle multiple malware-action args, using the last valid one", () => {
|
|
||||||
const args = [
|
const args = [
|
||||||
"--safe-chain-malware-action=block",
|
"--safe-chain-logging=verbose",
|
||||||
"--safe-chain-malware-action=prompt",
|
"--safe-chain-skip-minimum-package-age",
|
||||||
"install",
|
"install",
|
||||||
|
"lodash",
|
||||||
];
|
];
|
||||||
const result = initializeCliArguments(args);
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
assert.deepEqual(result, ["install"]);
|
assert.deepEqual(result, ["install", "lodash"]);
|
||||||
assert.strictEqual(getMalwareAction(), "prompt");
|
assert.strictEqual(getLoggingLevel(), "verbose");
|
||||||
|
assert.strictEqual(getSkipMinimumPackageAge(), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle malware-action with other safe-chain args", () => {
|
it("should handle skip-minimum-package-age flag in different positions", () => {
|
||||||
|
const args = ["install", "lodash", "--safe-chain-skip-minimum-package-age"];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install", "lodash"]);
|
||||||
|
assert.strictEqual(getSkipMinimumPackageAge(), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined when no minimum-package-age-hours argument is passed", () => {
|
||||||
|
const args = ["install", "express", "--save"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse minimum-package-age-hours value and set state", () => {
|
||||||
const args = [
|
const args = [
|
||||||
"--safe-chain-debug",
|
"--safe-chain-minimum-package-age-hours=48",
|
||||||
"--safe-chain-malware-action=block",
|
|
||||||
"--safe-chain-verbose",
|
|
||||||
"install",
|
"install",
|
||||||
|
"lodash",
|
||||||
];
|
];
|
||||||
const result = initializeCliArguments(args);
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
assert.deepEqual(result, ["install"]);
|
assert.deepEqual(result, ["install", "lodash"]);
|
||||||
assert.strictEqual(getMalwareAction(), "block");
|
assert.strictEqual(getMinimumPackageAgeHours(), "48");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle minimum-package-age-hours with zero value", () => {
|
||||||
|
const args = ["--safe-chain-minimum-package-age-hours=0", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), "0");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle minimum-package-age-hours with decimal values", () => {
|
||||||
|
const args = ["--safe-chain-minimum-package-age-hours=1.5", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), "1.5");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle minimum-package-age-hours case-insensitively", () => {
|
||||||
|
const args = ["--SAFE-CHAIN-MINIMUM-PACKAGE-AGE-HOURS=72", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), "72");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use the last minimum-package-age-hours argument when multiple are provided", () => {
|
||||||
|
const args = [
|
||||||
|
"--safe-chain-minimum-package-age-hours=12",
|
||||||
|
"--safe-chain-minimum-package-age-hours=36",
|
||||||
|
"install",
|
||||||
|
];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), "36");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter out minimum-package-age-hours argument from returned args", () => {
|
||||||
|
const args = [
|
||||||
|
"install",
|
||||||
|
"--safe-chain-minimum-package-age-hours=48",
|
||||||
|
"express",
|
||||||
|
"--save",
|
||||||
|
];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install", "express", "--save"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle minimum-package-age-hours with other safe-chain arguments", () => {
|
||||||
|
const args = [
|
||||||
|
"--safe-chain-logging=verbose",
|
||||||
|
"--safe-chain-minimum-package-age-hours=96",
|
||||||
|
"install",
|
||||||
|
"lodash",
|
||||||
|
];
|
||||||
|
const result = initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.deepEqual(result, ["install", "lodash"]);
|
||||||
|
assert.strictEqual(getLoggingLevel(), "verbose");
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), "96");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle non-numeric values without validation (validation in settings.js)", () => {
|
||||||
|
const args = ["--safe-chain-minimum-package-age-hours=invalid", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
// cliArguments.js just captures the value; validation is in settings.js
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), "invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle negative values as strings (validation in settings.js)", () => {
|
||||||
|
const args = ["--safe-chain-minimum-package-age-hours=-24", "install"];
|
||||||
|
initializeCliArguments(args);
|
||||||
|
|
||||||
|
assert.strictEqual(getMinimumPackageAgeHours(), "-24");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn on deprecated --include-python for setup", () => {
|
||||||
|
const warnings = [];
|
||||||
|
const originalWriteWarning = ui.writeWarning;
|
||||||
|
ui.writeWarning = (msg, ..._rest) => {
|
||||||
|
warnings.push(String(msg));
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const argv = ["node", "safe-chain", "setup", "--include-python"];
|
||||||
|
initializeCliArguments(argv);
|
||||||
|
assert.ok(
|
||||||
|
warnings.some((m) => m.includes("--include-python is deprecated")),
|
||||||
|
"Expected a deprecation warning for --include-python in setup"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
ui.writeWarning = originalWriteWarning;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should warn on deprecated --include-python for setup-ci", () => {
|
||||||
|
const warnings = [];
|
||||||
|
const originalWriteWarning = ui.writeWarning;
|
||||||
|
ui.writeWarning = (msg, ..._rest) => {
|
||||||
|
warnings.push(String(msg));
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const argv = ["node", "safe-chain", "setup-ci", "--include-python"];
|
||||||
|
initializeCliArguments(argv);
|
||||||
|
assert.ok(
|
||||||
|
warnings.some((m) => m.includes("--include-python is deprecated")),
|
||||||
|
"Expected a deprecation warning for --include-python in setup-ci"
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
ui.writeWarning = originalWriteWarning;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,176 @@ import fs from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { ui } from "../environment/userInteraction.js";
|
import { ui } from "../environment/userInteraction.js";
|
||||||
|
import { getEcoSystem } from "./settings.js";
|
||||||
|
import { getSafeChainBaseDir } from "./safeChainDir.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} SafeChainConfig
|
||||||
|
*
|
||||||
|
* We cannot trust the input and should add the necessary validations
|
||||||
|
* @property {unknown | Number} scanTimeout
|
||||||
|
* @property {unknown | Number} minimumPackageAgeHours
|
||||||
|
* @property {unknown | string} malwareListBaseUrl
|
||||||
|
* @property {unknown | SafeChainRegistryConfiguration} npm
|
||||||
|
* @property {unknown | SafeChainRegistryConfiguration} pip
|
||||||
|
*
|
||||||
|
* @typedef {Object} SafeChainRegistryConfiguration
|
||||||
|
* We cannot trust the input and should add the necessary validations.
|
||||||
|
* @property {unknown | string[]} customRegistries
|
||||||
|
* @property {unknown | string[]} minimumPackageAgeExclusions
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {number}
|
||||||
|
*/
|
||||||
export function getScanTimeout() {
|
export function getScanTimeout() {
|
||||||
const config = readConfigFile();
|
const config = readConfigFile();
|
||||||
return (
|
|
||||||
parseInt(process.env.AIKIDO_SCAN_TIMEOUT_MS) || config.scanTimeout || 10000 // Default to 10 seconds
|
if (process.env.AIKIDO_SCAN_TIMEOUT_MS) {
|
||||||
);
|
const scanTimeout = validateTimeout(process.env.AIKIDO_SCAN_TIMEOUT_MS);
|
||||||
|
if (scanTimeout != null) {
|
||||||
|
return scanTimeout;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.scanTimeout) {
|
||||||
|
const scanTimeout = validateTimeout(config.scanTimeout);
|
||||||
|
if (scanTimeout != null) {
|
||||||
|
return scanTimeout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 10000; // Default to 10 seconds
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {any} value
|
||||||
|
* @returns {number?}
|
||||||
|
*/
|
||||||
|
function validateTimeout(value) {
|
||||||
|
const timeout = Number(value);
|
||||||
|
if (!Number.isNaN(timeout) && timeout > 0) {
|
||||||
|
return timeout;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {any} value
|
||||||
|
* @returns {number | undefined}
|
||||||
|
*/
|
||||||
|
function validateMinimumPackageAgeHours(value) {
|
||||||
|
const hours = Number(value);
|
||||||
|
if (!Number.isNaN(hours)) {
|
||||||
|
return hours;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the minimum package age in hours from config file only
|
||||||
|
* @returns {number | undefined}
|
||||||
|
*/
|
||||||
|
export function getMinimumPackageAgeHours() {
|
||||||
|
const config = readConfigFile();
|
||||||
|
if (config.minimumPackageAgeHours !== undefined) {
|
||||||
|
const validated = validateMinimumPackageAgeHours(
|
||||||
|
config.minimumPackageAgeHours
|
||||||
|
);
|
||||||
|
if (validated !== undefined) {
|
||||||
|
return validated;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the malware list base URL from config file only
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getMalwareListBaseUrl() {
|
||||||
|
const config = readConfigFile();
|
||||||
|
if (config.malwareListBaseUrl && typeof config.malwareListBaseUrl === "string") {
|
||||||
|
return config.malwareListBaseUrl;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getNpmCustomRegistries() {
|
||||||
|
const config = readConfigFile();
|
||||||
|
|
||||||
|
if (!config || !config.npm) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript needs help understanding that config.npm exists and has customRegistries
|
||||||
|
const npmConfig = /** @type {SafeChainRegistryConfiguration} */ (config.npm);
|
||||||
|
const customRegistries = npmConfig.customRegistries;
|
||||||
|
|
||||||
|
if (!Array.isArray(customRegistries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return customRegistries.filter((item) => typeof item === "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from the config file (format parsing only, no validation)
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getPipCustomRegistries() {
|
||||||
|
const config = readConfigFile();
|
||||||
|
|
||||||
|
if (!config || !config.pip) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// TypeScript needs help understanding that config.pip exists and has customRegistries
|
||||||
|
const pipConfig = /** @type {SafeChainRegistryConfiguration} */ (config.pip);
|
||||||
|
const customRegistries = pipConfig.customRegistries;
|
||||||
|
|
||||||
|
if (!Array.isArray(customRegistries)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return customRegistries.filter((item) => typeof item === "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the minimum package age exclusions from the config file for the current ecosystem
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getMinimumPackageAgeExclusions() {
|
||||||
|
const config = readConfigFile();
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
const registryConfig = ecosystem === "py" ? config.pip : config.npm;
|
||||||
|
|
||||||
|
if (!config || !registryConfig) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const typedRegistryConfig =
|
||||||
|
/** @type {SafeChainRegistryConfiguration} */ (registryConfig);
|
||||||
|
const exclusions = typedRegistryConfig.minimumPackageAgeExclusions;
|
||||||
|
|
||||||
|
if (!Array.isArray(exclusions)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return exclusions.filter((item) => typeof item === "string");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {import("../api/aikido.js").MalwarePackage[]} data
|
||||||
|
* @param {string | number} version
|
||||||
|
*
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
export function writeDatabaseToLocalCache(data, version) {
|
export function writeDatabaseToLocalCache(data, version) {
|
||||||
try {
|
try {
|
||||||
const databasePath = getDatabasePath();
|
const databasePath = getDatabasePath();
|
||||||
|
|
@ -24,6 +186,9 @@ export function writeDatabaseToLocalCache(data, version) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {{malwareDatabase: import("../api/aikido.js").MalwarePackage[] | null, version: string | null}}
|
||||||
|
*/
|
||||||
export function readDatabaseFromLocalCache() {
|
export function readDatabaseFromLocalCache() {
|
||||||
try {
|
try {
|
||||||
const databasePath = getDatabasePath();
|
const databasePath = getDatabasePath();
|
||||||
|
|
@ -55,31 +220,102 @@ export function readDatabaseFromLocalCache() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {SafeChainConfig}
|
||||||
|
*/
|
||||||
function readConfigFile() {
|
function readConfigFile() {
|
||||||
|
/** @type {SafeChainConfig} */
|
||||||
|
const emptyConfig = {
|
||||||
|
scanTimeout: undefined,
|
||||||
|
minimumPackageAgeHours: undefined,
|
||||||
|
malwareListBaseUrl: undefined,
|
||||||
|
npm: {
|
||||||
|
customRegistries: undefined,
|
||||||
|
},
|
||||||
|
pip: {
|
||||||
|
customRegistries: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const configFilePath = getConfigFilePath();
|
const configFilePath = getConfigFilePath();
|
||||||
|
|
||||||
if (!fs.existsSync(configFilePath)) {
|
if (!fs.existsSync(configFilePath)) {
|
||||||
return {};
|
return emptyConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
const data = fs.readFileSync(configFilePath, "utf8");
|
const data = fs.readFileSync(configFilePath, "utf8");
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
|
} catch {
|
||||||
|
return emptyConfig;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function getDatabasePath() {
|
function getDatabasePath() {
|
||||||
const aikidoDir = getAikidoDirectory();
|
const aikidoDir = getAikidoDirectory();
|
||||||
return path.join(aikidoDir, "malwareDatabase.json");
|
const ecosystem = getEcoSystem();
|
||||||
|
return path.join(aikidoDir, `malwareDatabase_${ecosystem}.json`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDatabaseVersionPath() {
|
function getDatabaseVersionPath() {
|
||||||
const aikidoDir = getAikidoDirectory();
|
const aikidoDir = getAikidoDirectory();
|
||||||
return path.join(aikidoDir, "version.txt");
|
const ecosystem = getEcoSystem();
|
||||||
|
return path.join(aikidoDir, `version_${ecosystem}.txt`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getNewPackagesListPath() {
|
||||||
|
const safeChainDir = getSafeChainDirectory();
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
return path.join(safeChainDir, `newPackagesList_${ecosystem}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getNewPackagesListVersionPath() {
|
||||||
|
const safeChainDir = getSafeChainDirectory();
|
||||||
|
const ecosystem = getEcoSystem();
|
||||||
|
return path.join(safeChainDir, `newPackagesList_version_${ecosystem}.txt`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function getConfigFilePath() {
|
function getConfigFilePath() {
|
||||||
return path.join(getAikidoDirectory(), "config.json");
|
const primaryPath = path.join(getSafeChainDirectory(), "config.json");
|
||||||
|
if (fs.existsSync(primaryPath)) {
|
||||||
|
return primaryPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const legacyPath = path.join(getAikidoDirectory(), "config.json");
|
||||||
|
if (fs.existsSync(legacyPath)) {
|
||||||
|
return legacyPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return primaryPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getSafeChainDirectory() {
|
||||||
|
const safeChainDir = getSafeChainBaseDir();
|
||||||
|
|
||||||
|
if (!fs.existsSync(safeChainDir)) {
|
||||||
|
fs.mkdirSync(safeChainDir, { recursive: true });
|
||||||
|
}
|
||||||
|
return safeChainDir;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function getAikidoDirectory() {
|
function getAikidoDirectory() {
|
||||||
const homeDir = os.homedir();
|
const homeDir = os.homedir();
|
||||||
const aikidoDir = path.join(homeDir, ".aikido");
|
const aikidoDir = path.join(homeDir, ".aikido");
|
||||||
|
|
|
||||||
380
packages/safe-chain/src/config/configFile.spec.js
Normal file
380
packages/safe-chain/src/config/configFile.spec.js
Normal file
|
|
@ -0,0 +1,380 @@
|
||||||
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const safeChainConfigPath = path.join(os.homedir(), ".safe-chain", "config.json");
|
||||||
|
const aikidoConfigPath = path.join(os.homedir(), ".aikido", "config.json");
|
||||||
|
|
||||||
|
/** @type {Map<string, string>} */
|
||||||
|
let mockFiles = new Map();
|
||||||
|
mock.module("fs", {
|
||||||
|
namedExports: {
|
||||||
|
existsSync: (filePath) => mockFiles.has(filePath),
|
||||||
|
readFileSync: (filePath) => {
|
||||||
|
if (!mockFiles.has(filePath)) {
|
||||||
|
throw new Error(`ENOENT: no such file: ${filePath}`);
|
||||||
|
}
|
||||||
|
return mockFiles.get(filePath);
|
||||||
|
},
|
||||||
|
writeFileSync: (filePath, content) => mockFiles.set(filePath, content),
|
||||||
|
mkdirSync: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to set config content at the primary (~/.safe-chain/) location.
|
||||||
|
* @param {string} content
|
||||||
|
*/
|
||||||
|
function setConfigContent(content) {
|
||||||
|
mockFiles.set(safeChainConfigPath, content);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getScanTimeout", async () => {
|
||||||
|
let originalEnv;
|
||||||
|
|
||||||
|
const { getScanTimeout } = await import("./configFile.js");
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
// Save original environment
|
||||||
|
originalEnv = process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
// Restore original environment
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||||
|
}
|
||||||
|
|
||||||
|
mockFiles.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return default timeout of 10000ms when no config or env var is set", () => {
|
||||||
|
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return timeout from config file when set", () => {
|
||||||
|
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 5000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prioritize environment variable over config file", () => {
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = "20000";
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 20000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle invalid environment variable and fall back to config", () => {
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = "invalid";
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: 7000 }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 7000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore zero and negative values and fall back to default", () => {
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = "0";
|
||||||
|
|
||||||
|
let timeout = getScanTimeout();
|
||||||
|
assert.strictEqual(timeout, 10000);
|
||||||
|
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = "-5000";
|
||||||
|
|
||||||
|
timeout = getScanTimeout();
|
||||||
|
assert.strictEqual(timeout, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore textual non-numeric values in environment variable and fall back to config", () => {
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = "fast";
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: 8000 }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 8000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore textual non-numeric values in config file and fall back to default", () => {
|
||||||
|
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: "slow" }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore textual non-numeric values in both env and config, fall back to default", () => {
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = "quick";
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: "medium" }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 10000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore mixed alphanumeric strings in environment variable", () => {
|
||||||
|
process.env.AIKIDO_SCAN_TIMEOUT_MS = "5000ms";
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: 6000 }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 6000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should ignore mixed alphanumeric strings in config file", () => {
|
||||||
|
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: "3000ms" }));
|
||||||
|
|
||||||
|
const timeout = getScanTimeout();
|
||||||
|
|
||||||
|
assert.strictEqual(timeout, 10000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getMinimumPackageAgeHours", async () => {
|
||||||
|
const { getMinimumPackageAgeHours } = await import("./configFile.js");
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockFiles.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when config file doesn't exist", () => {
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null when config file exists but minimumPackageAgeHours is not set", () => {
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return value from config file when set to valid number", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: 48 }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, 48);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle string numbers in config file", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: "72" }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, 72);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle decimal values", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: 1.5 }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, 1.5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return null for non-numeric strings", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: "invalid" }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return undefined for values with units suffix", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: "48h" }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle malformed JSON and return null", () => {
|
||||||
|
setConfigContent("{ invalid json");
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 0 when minimumPackageAgeHours is set to 0", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: 0 }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return 0 when minimumPackageAgeHours is set to string '0'", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: "0" }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle negative numeric values", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: -24 }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, -24);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle negative string values", () => {
|
||||||
|
setConfigContent(JSON.stringify({ minimumPackageAgeHours: "-48" }));
|
||||||
|
|
||||||
|
const hours = getMinimumPackageAgeHours();
|
||||||
|
|
||||||
|
assert.strictEqual(hours, -48);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const { getNpmCustomRegistries, getPipCustomRegistries } = await import(
|
||||||
|
"./configFile.js"
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const { packageManager, getCustomRegistries } of [
|
||||||
|
{
|
||||||
|
packageManager: "npm",
|
||||||
|
getCustomRegistries: getNpmCustomRegistries,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageManager: "pip",
|
||||||
|
getCustomRegistries: getPipCustomRegistries,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
{
|
||||||
|
describe(getCustomRegistries.name, async () => {
|
||||||
|
afterEach(() => {
|
||||||
|
mockFiles.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when config file doesn't exist", () => {
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should return empty array when ${packageManager} config is not set`, () => {
|
||||||
|
setConfigContent(JSON.stringify({ scanTimeout: 5000 }));
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when customRegistries is not an array", () => {
|
||||||
|
setConfigContent(JSON.stringify({
|
||||||
|
[packageManager]: { customRegistries: "not-an-array" },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return array of custom registries when set", () => {
|
||||||
|
setConfigContent(JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [`${packageManager}.company.com`, "registry.internal.net"],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter out non-string values", () => {
|
||||||
|
setConfigContent(JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
123,
|
||||||
|
null,
|
||||||
|
"registry.internal.net",
|
||||||
|
undefined,
|
||||||
|
{},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for empty customRegistries array", () => {
|
||||||
|
setConfigContent(JSON.stringify({
|
||||||
|
[packageManager]: { customRegistries: [] },
|
||||||
|
}));
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle malformed JSON and return empty array", () => {
|
||||||
|
setConfigContent("{ invalid json");
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("config file location fallback", async () => {
|
||||||
|
const { getScanTimeout } = await import("./configFile.js");
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockFiles.clear();
|
||||||
|
delete process.env.AIKIDO_SCAN_TIMEOUT_MS;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read config from ~/.safe-chain/config.json when it exists", () => {
|
||||||
|
mockFiles.set(safeChainConfigPath, JSON.stringify({ scanTimeout: 3000 }));
|
||||||
|
|
||||||
|
assert.strictEqual(getScanTimeout(), 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to ~/.aikido/config.json when primary does not exist", () => {
|
||||||
|
mockFiles.set(aikidoConfigPath, JSON.stringify({ scanTimeout: 4000 }));
|
||||||
|
|
||||||
|
assert.strictEqual(getScanTimeout(), 4000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prefer ~/.safe-chain/config.json when both exist", () => {
|
||||||
|
mockFiles.set(safeChainConfigPath, JSON.stringify({ scanTimeout: 3000 }));
|
||||||
|
mockFiles.set(aikidoConfigPath, JSON.stringify({ scanTimeout: 4000 }));
|
||||||
|
|
||||||
|
assert.strictEqual(getScanTimeout(), 3000);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return default when neither config file exists", () => {
|
||||||
|
assert.strictEqual(getScanTimeout(), 10000);
|
||||||
|
});
|
||||||
|
});
|
||||||
57
packages/safe-chain/src/config/environmentVariables.js
Normal file
57
packages/safe-chain/src/config/environmentVariables.js
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Gets the minimum package age in hours from environment variable
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getMinimumPackageAgeHours() {
|
||||||
|
return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_HOURS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from environment variable
|
||||||
|
* Expected format: comma-separated list of registry domains
|
||||||
|
* Example: "npm.company.com,registry.internal.net"
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getNpmCustomRegistries() {
|
||||||
|
return process.env.SAFE_CHAIN_NPM_CUSTOM_REGISTRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom pip registries from environment variable
|
||||||
|
* Expected format: comma-separated list of registry domains
|
||||||
|
* Example: "pip.company.com,registry.internal.net"
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getPipCustomRegistries() {
|
||||||
|
return process.env.SAFE_CHAIN_PIP_CUSTOM_REGISTRIES;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the logging level from environment variable
|
||||||
|
* Valid values: "silent", "normal", "verbose"
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getLoggingLevel() {
|
||||||
|
return process.env.SAFE_CHAIN_LOGGING;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the minimum package age exclusions from environment variable
|
||||||
|
* Expected format: comma-separated list of package names
|
||||||
|
* Example: "react,@aikidosec/safe-chain,lodash"
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getMinimumPackageAgeExclusions() {
|
||||||
|
return process.env.SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS ||
|
||||||
|
process.env.SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the malware list base URL from environment variable
|
||||||
|
* Expected format: full URL without trailing slash
|
||||||
|
* Example: "https://malware-list.aikido.dev"
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getMalwareListBaseUrl() {
|
||||||
|
return process.env.SAFE_CHAIN_MALWARE_LIST_BASE_URL;
|
||||||
|
}
|
||||||
71
packages/safe-chain/src/config/safeChainDir.js
Normal file
71
packages/safe-chain/src/config/safeChainDir.js
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
import os from "os";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { getInstalledSafeChainDir } from "../installLocation.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getSafeChainBaseDir() {
|
||||||
|
return getInstalledSafeChainDir() ?? path.join(os.homedir(), ".safe-chain");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getBinDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "bin");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getShimsDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "shims");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getScriptsDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "scripts");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getCertsDir() {
|
||||||
|
return path.join(getSafeChainBaseDir(), "certs");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves the directory of the calling module.
|
||||||
|
* Falls back to __dirname when import.meta.url is unavailable (pkg CJS binary).
|
||||||
|
* @param {string | undefined} moduleUrl
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function resolveModuleDir(moduleUrl) {
|
||||||
|
if (moduleUrl) {
|
||||||
|
return path.dirname(fileURLToPath(moduleUrl));
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
return __dirname;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | undefined} moduleUrl
|
||||||
|
* @param {string} fileName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getStartupScriptSourcePath(moduleUrl, fileName) {
|
||||||
|
return path.join(resolveModuleDir(moduleUrl), "startup-scripts", fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | undefined} moduleUrl
|
||||||
|
* @param {string} fileName
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getPathWrapperTemplatePath(moduleUrl, fileName) {
|
||||||
|
return path.join(resolveModuleDir(moduleUrl), "path-wrappers", "templates", fileName);
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,247 @@
|
||||||
import * as cliArguments from "./cliArguments.js";
|
import * as cliArguments from "./cliArguments.js";
|
||||||
|
import * as configFile from "./configFile.js";
|
||||||
|
import * as environmentVariables from "./environmentVariables.js";
|
||||||
|
import { ui } from "../environment/userInteraction.js";
|
||||||
|
|
||||||
export function getMalwareAction() {
|
export const LOGGING_SILENT = "silent";
|
||||||
const action = cliArguments.getMalwareAction();
|
export const LOGGING_NORMAL = "normal";
|
||||||
|
export const LOGGING_VERBOSE = "verbose";
|
||||||
|
|
||||||
if (action === MALWARE_ACTION_PROMPT) {
|
export function getLoggingLevel() {
|
||||||
return MALWARE_ACTION_PROMPT;
|
// Priority 1: CLI argument
|
||||||
|
const cliLevel = cliArguments.getLoggingLevel();
|
||||||
|
if (cliLevel === LOGGING_SILENT || cliLevel === LOGGING_VERBOSE) {
|
||||||
|
return cliLevel;
|
||||||
|
}
|
||||||
|
if (cliLevel) {
|
||||||
|
// CLI arg was set but invalid, default to normal for backwards compatibility.
|
||||||
|
return LOGGING_NORMAL;
|
||||||
}
|
}
|
||||||
|
|
||||||
return MALWARE_ACTION_BLOCK;
|
// Priority 2: Environment variable
|
||||||
|
const envLevel = environmentVariables.getLoggingLevel()?.toLowerCase();
|
||||||
|
if (envLevel === LOGGING_SILENT || envLevel === LOGGING_VERBOSE) {
|
||||||
|
return envLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MALWARE_ACTION_BLOCK = "block";
|
return LOGGING_NORMAL;
|
||||||
export const MALWARE_ACTION_PROMPT = "prompt";
|
}
|
||||||
|
|
||||||
|
export const ECOSYSTEM_JS = "js";
|
||||||
|
export const ECOSYSTEM_PY = "py";
|
||||||
|
|
||||||
|
// Default to JavaScript ecosystem
|
||||||
|
const ecosystemSettings = {
|
||||||
|
ecoSystem: ECOSYSTEM_JS,
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns {string} - The current ecosystem setting (ECOSYSTEM_JS or ECOSYSTEM_PY) */
|
||||||
|
export function getEcoSystem() {
|
||||||
|
return ecosystemSettings.ecoSystem;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {string} setting - The ecosystem to set (ECOSYSTEM_JS or ECOSYSTEM_PY)
|
||||||
|
*/
|
||||||
|
export function setEcoSystem(setting) {
|
||||||
|
ecosystemSettings.ecoSystem = setting;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultMinimumPackageAge = 48;
|
||||||
|
/** @returns {number} */
|
||||||
|
export function getMinimumPackageAgeHours() {
|
||||||
|
// Priority 1: CLI argument
|
||||||
|
const cliValue = validateMinimumPackageAgeHours(
|
||||||
|
cliArguments.getMinimumPackageAgeHours()
|
||||||
|
);
|
||||||
|
if (cliValue !== undefined) {
|
||||||
|
return cliValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Environment variable
|
||||||
|
const envValue = validateMinimumPackageAgeHours(
|
||||||
|
environmentVariables.getMinimumPackageAgeHours()
|
||||||
|
);
|
||||||
|
if (envValue !== undefined) {
|
||||||
|
return envValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Config file
|
||||||
|
const configValue = configFile.getMinimumPackageAgeHours();
|
||||||
|
if (configValue !== undefined) {
|
||||||
|
return configValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultMinimumPackageAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string | undefined} value
|
||||||
|
* @returns {number | undefined}
|
||||||
|
*/
|
||||||
|
function validateMinimumPackageAgeHours(value) {
|
||||||
|
if (!value) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numericValue = Number(value);
|
||||||
|
if (Number.isNaN(numericValue)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numericValue >= 0) {
|
||||||
|
return numericValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSkipMinimumPackageAge = false;
|
||||||
|
export function skipMinimumPackageAge() {
|
||||||
|
const cliValue = cliArguments.getSkipMinimumPackageAge();
|
||||||
|
|
||||||
|
if (cliValue === true) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultSkipMinimumPackageAge;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes a registry URL by removing protocol if present
|
||||||
|
* @param {string} registry
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function normalizeRegistry(registry) {
|
||||||
|
// Remove protocol (http://, https://) if present
|
||||||
|
return registry.replace(/^https?:\/\//, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses comma-separated registries from environment variable
|
||||||
|
* @param {string | undefined} envValue
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function parseRegistriesFromEnv(envValue) {
|
||||||
|
if (!envValue || typeof envValue !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split by comma and trim whitespace
|
||||||
|
return envValue
|
||||||
|
.split(",")
|
||||||
|
.map((registry) => registry.trim())
|
||||||
|
.filter((registry) => registry.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from both environment variable and config file (merged)
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getNpmCustomRegistries() {
|
||||||
|
const envRegistries = parseRegistriesFromEnv(
|
||||||
|
environmentVariables.getNpmCustomRegistries()
|
||||||
|
);
|
||||||
|
const configRegistries = configFile.getNpmCustomRegistries();
|
||||||
|
|
||||||
|
// Merge both sources and remove duplicates
|
||||||
|
const allRegistries = [...envRegistries, ...configRegistries];
|
||||||
|
const uniqueRegistries = [...new Set(allRegistries)];
|
||||||
|
|
||||||
|
// Normalize each registry (remove protocol if any)
|
||||||
|
return uniqueRegistries.map(normalizeRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the custom npm registries from both environment variable and config file (merged)
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getPipCustomRegistries() {
|
||||||
|
const envRegistries = parseRegistriesFromEnv(
|
||||||
|
environmentVariables.getPipCustomRegistries()
|
||||||
|
);
|
||||||
|
const configRegistries = configFile.getPipCustomRegistries();
|
||||||
|
|
||||||
|
// Merge both sources and remove duplicates
|
||||||
|
const allRegistries = [...envRegistries, ...configRegistries];
|
||||||
|
const uniqueRegistries = [...new Set(allRegistries)];
|
||||||
|
|
||||||
|
// Normalize each registry (remove protocol if any)
|
||||||
|
return uniqueRegistries.map(normalizeRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parses comma-separated exclusions from environment variable
|
||||||
|
* @param {string | undefined} envValue
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
function parseExclusionsFromEnv(envValue) {
|
||||||
|
if (!envValue || typeof envValue !== "string") {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return envValue
|
||||||
|
.split(",")
|
||||||
|
.map((exclusion) => exclusion.trim())
|
||||||
|
.filter((exclusion) => exclusion.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the minimum package age exclusions from both environment variable and config file (merged)
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
export function getMinimumPackageAgeExclusions() {
|
||||||
|
const envExclusions = parseExclusionsFromEnv(
|
||||||
|
environmentVariables.getMinimumPackageAgeExclusions()
|
||||||
|
);
|
||||||
|
const configExclusions = configFile.getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
// Merge both sources and remove duplicates
|
||||||
|
const allExclusions = [...envExclusions, ...configExclusions];
|
||||||
|
return [...new Set(allExclusions)];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the malware list base URL with priority: CLI argument > environment variable > config file > default
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
export function getMalwareListBaseUrl() {
|
||||||
|
// Priority 1: CLI argument
|
||||||
|
const cliValue = cliArguments.getMalwareListBaseUrl();
|
||||||
|
if (cliValue) {
|
||||||
|
const url = removeTrailingSlashes(cliValue);
|
||||||
|
ui.writeVerbose(`Fetching malware lists from ${url} as defined by CLI argument --safe-chain-malware-list-base-url`);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Environment variable
|
||||||
|
const envValue = environmentVariables.getMalwareListBaseUrl();
|
||||||
|
if (envValue) {
|
||||||
|
const url = removeTrailingSlashes(envValue);
|
||||||
|
ui.writeVerbose(`Fetching malware lists from ${url} as defined by environment variable SAFE_CHAIN_MALWARE_LIST_BASE_URL`);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Config file
|
||||||
|
const configValue = configFile.getMalwareListBaseUrl();
|
||||||
|
if (configValue) {
|
||||||
|
const url = removeTrailingSlashes(configValue);
|
||||||
|
ui.writeVerbose(`Fetching malware lists from ${url} as defined by config file (malwareListBaseUrl)`);
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default
|
||||||
|
return removeTrailingSlashes("https://malware-list.aikido.dev");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes trailing slashes from a URL-like string.
|
||||||
|
* @param {string} value
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
function removeTrailingSlashes(value) {
|
||||||
|
if (!value || typeof value !== "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.replace(/\/+$/, "");
|
||||||
|
}
|
||||||
|
|
|
||||||
647
packages/safe-chain/src/config/settings.spec.js
Normal file
647
packages/safe-chain/src/config/settings.spec.js
Normal file
|
|
@ -0,0 +1,647 @@
|
||||||
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
let configFileContent = undefined;
|
||||||
|
mock.module("fs", {
|
||||||
|
namedExports: {
|
||||||
|
existsSync: () => configFileContent !== undefined,
|
||||||
|
readFileSync: () => configFileContent,
|
||||||
|
writeFileSync: (content) => (configFileContent = content),
|
||||||
|
mkdirSync: () => {},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
getNpmCustomRegistries,
|
||||||
|
getPipCustomRegistries,
|
||||||
|
getMinimumPackageAgeExclusions,
|
||||||
|
getMalwareListBaseUrl,
|
||||||
|
setEcoSystem,
|
||||||
|
ECOSYSTEM_JS,
|
||||||
|
ECOSYSTEM_PY,
|
||||||
|
getLoggingLevel,
|
||||||
|
LOGGING_SILENT,
|
||||||
|
LOGGING_NORMAL,
|
||||||
|
LOGGING_VERBOSE,
|
||||||
|
} = await import("./settings.js");
|
||||||
|
const { initializeCliArguments } = await import("./cliArguments.js");
|
||||||
|
|
||||||
|
for (const { packageManager, getCustomRegistries, envVarName } of [
|
||||||
|
{
|
||||||
|
packageManager: "npm",
|
||||||
|
getCustomRegistries: getNpmCustomRegistries,
|
||||||
|
envVarName: "SAFE_CHAIN_NPM_CUSTOM_REGISTRIES",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
packageManager: "pip",
|
||||||
|
getCustomRegistries: getPipCustomRegistries,
|
||||||
|
envVarName: "SAFE_CHAIN_PIP_CUSTOM_REGISTRIES",
|
||||||
|
},
|
||||||
|
]) {
|
||||||
|
describe(getCustomRegistries.name, async () => {
|
||||||
|
let originalEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env[envVarName];
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env[envVarName] = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
}
|
||||||
|
configFileContent = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when no registries configured", () => {
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return registries without protocol", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should strip https:// protocol from registries", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [
|
||||||
|
`https://${packageManager}.company.com`,
|
||||||
|
"https://registry.internal.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should strip http:// protocol from registries", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [
|
||||||
|
`http://${packageManager}.company.com`,
|
||||||
|
"http://registry.internal.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed protocols and no protocol", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [
|
||||||
|
`https://${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
"http://private.registry.io",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"registry.internal.net",
|
||||||
|
"private.registry.io",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve registry path after stripping protocol", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [
|
||||||
|
`https://${packageManager}.company.com/custom/path`,
|
||||||
|
`registry.internal.net/${packageManager}`,
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com/custom/path`,
|
||||||
|
`registry.internal.net/${packageManager}`,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse comma-separated registries from environment variable", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] = "env1.registry.com,env2.registry.net";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim whitespace from environment variable registries", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] = " env1.registry.com , env2.registry.net ";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge environment variable and config file registries", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] = "env1.registry.com";
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: ["config1.registry.net"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"config1.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove duplicate registries when merging env and config", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[
|
||||||
|
envVarName
|
||||||
|
] = `${packageManager}.company.com,env.registry.com`;
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
[packageManager]: {
|
||||||
|
customRegistries: [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"config.registry.net",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
`${packageManager}.company.com`,
|
||||||
|
"env.registry.com",
|
||||||
|
"config.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should normalize protocols from environment variable registries", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] =
|
||||||
|
"https://env1.registry.com,http://env2.registry.net";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty strings in comma-separated list", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] = "env1.registry.com,,env2.registry.net,";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, [
|
||||||
|
"env1.registry.com",
|
||||||
|
"env2.registry.net",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle single registry in environment variable", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] = "single.registry.com";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, ["single.registry.com"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for empty environment variable", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] = "";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for whitespace-only environment variable", () => {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
process.env[envVarName] = " , , ";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const registries = getCustomRegistries();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(registries, []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("getLoggingLevel", () => {
|
||||||
|
let originalEnv;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env.SAFE_CHAIN_LOGGING;
|
||||||
|
delete process.env.SAFE_CHAIN_LOGGING;
|
||||||
|
// Reset CLI arguments state
|
||||||
|
initializeCliArguments([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env.SAFE_CHAIN_LOGGING;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return normal by default when nothing is configured", () => {
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_NORMAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return silent from environment variable", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "silent";
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_SILENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return verbose from environment variable", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "verbose";
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_VERBOSE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle uppercase environment variable values", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "VERBOSE";
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_VERBOSE);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle mixed case environment variable values", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "Silent";
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_SILENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return normal for invalid environment variable values", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "invalid";
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_NORMAL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prioritize CLI argument over environment variable", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "verbose";
|
||||||
|
initializeCliArguments(["--safe-chain-logging=silent"]);
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_SILENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should use environment variable when CLI argument is not set", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "silent";
|
||||||
|
initializeCliArguments(["install", "express"]);
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_SILENT);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return normal when CLI argument is invalid (even if env var is valid)", () => {
|
||||||
|
process.env.SAFE_CHAIN_LOGGING = "verbose";
|
||||||
|
initializeCliArguments(["--safe-chain-logging=invalid"]);
|
||||||
|
|
||||||
|
const level = getLoggingLevel();
|
||||||
|
|
||||||
|
assert.strictEqual(level, LOGGING_NORMAL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getMinimumPackageAgeExclusions", () => {
|
||||||
|
let originalEnv;
|
||||||
|
let originalLegacyEnv;
|
||||||
|
const envVarName = "SAFE_CHAIN_MINIMUM_PACKAGE_AGE_EXCLUSIONS";
|
||||||
|
const legacyEnvVarName = "SAFE_CHAIN_NPM_MINIMUM_PACKAGE_AGE_EXCLUSIONS";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env[envVarName];
|
||||||
|
originalLegacyEnv = process.env[legacyEnvVarName];
|
||||||
|
delete process.env[envVarName];
|
||||||
|
delete process.env[legacyEnvVarName];
|
||||||
|
setEcoSystem(ECOSYSTEM_JS);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env[envVarName] = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
}
|
||||||
|
if (originalLegacyEnv !== undefined) {
|
||||||
|
process.env[legacyEnvVarName] = originalLegacyEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env[legacyEnvVarName];
|
||||||
|
}
|
||||||
|
configFileContent = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array when no exclusions configured", () => {
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return exclusions from config file", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
minimumPackageAgeExclusions: ["react", "@aikidosec/safe-chain"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["react", "@aikidosec/safe-chain"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse comma-separated exclusions from environment variable", () => {
|
||||||
|
process.env[envVarName] = "lodash,express,@types/node";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["lodash", "express", "@types/node"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should merge environment variable and config file exclusions", () => {
|
||||||
|
process.env[envVarName] = "lodash";
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
minimumPackageAgeExclusions: ["react"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should remove duplicate exclusions when merging", () => {
|
||||||
|
process.env[envVarName] = "lodash,react";
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
minimumPackageAgeExclusions: ["react", "express"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["lodash", "react", "express"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim whitespace from environment variable exclusions", () => {
|
||||||
|
process.env[envVarName] = " lodash , react ";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle scoped packages", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
minimumPackageAgeExclusions: ["@babel/core", "@types/react"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["@babel/core", "@types/react"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should handle empty strings in comma-separated list", () => {
|
||||||
|
process.env[envVarName] = "lodash,,react,";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for empty environment variable", () => {
|
||||||
|
process.env[envVarName] = "";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty array for whitespace-only environment variable", () => {
|
||||||
|
process.env[envVarName] = " , , ";
|
||||||
|
configFileContent = undefined;
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should filter non-string values from config file", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
npm: {
|
||||||
|
minimumPackageAgeExclusions: ["react", 123, null, "lodash", undefined],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["react", "lodash"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fall back to the legacy npm environment variable", () => {
|
||||||
|
process.env[legacyEnvVarName] = "lodash,react";
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["lodash", "react"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should read exclusions from the python config when the current ecosystem is py", () => {
|
||||||
|
setEcoSystem(ECOSYSTEM_PY);
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
pip: {
|
||||||
|
minimumPackageAgeExclusions: ["requests", "urllib3"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const exclusions = getMinimumPackageAgeExclusions();
|
||||||
|
|
||||||
|
assert.deepStrictEqual(exclusions, ["requests", "urllib3"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getMalwareListBaseUrl", () => {
|
||||||
|
let originalEnv;
|
||||||
|
const envVarName = "SAFE_CHAIN_MALWARE_LIST_BASE_URL";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
originalEnv = process.env[envVarName];
|
||||||
|
delete process.env[envVarName];
|
||||||
|
// Reset CLI arguments state
|
||||||
|
initializeCliArguments([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
if (originalEnv !== undefined) {
|
||||||
|
process.env[envVarName] = originalEnv;
|
||||||
|
} else {
|
||||||
|
delete process.env[envVarName];
|
||||||
|
}
|
||||||
|
configFileContent = undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return default URL when nothing is configured", () => {
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://malware-list.aikido.dev");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim trailing slash from CLI argument", () => {
|
||||||
|
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com/"]);
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://cli-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim trailing slash from environment variable", () => {
|
||||||
|
process.env[envVarName] = "https://env-mirror.com/";
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://env-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should trim trailing slash from config file value", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
malwareListBaseUrl: "https://config-mirror.com/",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://config-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return CLI argument value with highest priority", () => {
|
||||||
|
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com"]);
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://cli-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return environment variable value when no CLI argument", () => {
|
||||||
|
process.env[envVarName] = "https://env-mirror.com";
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://env-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return config file value when no CLI or env", () => {
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
malwareListBaseUrl: "https://config-mirror.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://config-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prioritize CLI over environment variable", () => {
|
||||||
|
process.env[envVarName] = "https://env-mirror.com";
|
||||||
|
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com"]);
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://cli-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prioritize environment variable over config file", () => {
|
||||||
|
process.env[envVarName] = "https://env-mirror.com";
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
malwareListBaseUrl: "https://config-mirror.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://env-mirror.com");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should prioritize CLI over config file", () => {
|
||||||
|
initializeCliArguments(["--safe-chain-malware-list-base-url=https://cli-mirror.com"]);
|
||||||
|
configFileContent = JSON.stringify({
|
||||||
|
malwareListBaseUrl: "https://config-mirror.com",
|
||||||
|
});
|
||||||
|
|
||||||
|
const url = getMalwareListBaseUrl();
|
||||||
|
|
||||||
|
assert.strictEqual(url, "https://cli-mirror.com");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,79 +1,122 @@
|
||||||
|
// oxlint-disable no-console
|
||||||
import chalk from "chalk";
|
import chalk from "chalk";
|
||||||
import ora from "ora";
|
|
||||||
import { confirm as inquirerConfirm } from "@inquirer/prompts";
|
|
||||||
import { isCi } from "./environment.js";
|
import { isCi } from "./environment.js";
|
||||||
|
import {
|
||||||
|
getLoggingLevel,
|
||||||
|
LOGGING_SILENT,
|
||||||
|
LOGGING_VERBOSE,
|
||||||
|
} from "../config/settings.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {{ bufferOutput: boolean, bufferedMessages:(() => void)[]}}
|
||||||
|
*/
|
||||||
|
const state = {
|
||||||
|
bufferOutput: false,
|
||||||
|
bufferedMessages: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
function isSilentMode() {
|
||||||
|
return getLoggingLevel() === LOGGING_SILENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isVerboseMode() {
|
||||||
|
return getLoggingLevel() === LOGGING_VERBOSE;
|
||||||
|
}
|
||||||
|
|
||||||
function emptyLine() {
|
function emptyLine() {
|
||||||
|
if (isSilentMode()) return;
|
||||||
|
|
||||||
writeInformation("");
|
writeInformation("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @param {...any} optionalParams
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
function writeInformation(message, ...optionalParams) {
|
function writeInformation(message, ...optionalParams) {
|
||||||
console.log(message, ...optionalParams);
|
if (isSilentMode()) return;
|
||||||
|
|
||||||
|
writeOrBuffer(() => console.log(message, ...optionalParams));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @param {...any} optionalParams
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
function writeWarning(message, ...optionalParams) {
|
function writeWarning(message, ...optionalParams) {
|
||||||
|
if (isSilentMode()) return;
|
||||||
|
|
||||||
if (!isCi()) {
|
if (!isCi()) {
|
||||||
message = chalk.yellow(message);
|
message = chalk.yellow(message);
|
||||||
}
|
}
|
||||||
console.warn(message, ...optionalParams);
|
writeOrBuffer(() => console.warn(message, ...optionalParams));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} message
|
||||||
|
* @param {...any} optionalParams
|
||||||
|
* @returns {void}
|
||||||
|
*/
|
||||||
function writeError(message, ...optionalParams) {
|
function writeError(message, ...optionalParams) {
|
||||||
if (!isCi()) {
|
if (!isCi()) {
|
||||||
message = chalk.red(message);
|
message = chalk.red(message);
|
||||||
}
|
}
|
||||||
console.error(message, ...optionalParams);
|
writeOrBuffer(() => console.error(message, ...optionalParams));
|
||||||
}
|
}
|
||||||
|
|
||||||
function startProcess(message) {
|
function writeExitWithoutInstallingMaliciousPackages() {
|
||||||
if (isCi()) {
|
let message = "Safe-chain: Exiting without installing malicious packages.";
|
||||||
return {
|
if (!isCi()) {
|
||||||
succeed: (message) => {
|
message = chalk.red(message);
|
||||||
writeInformation(message);
|
}
|
||||||
},
|
writeOrBuffer(() => console.error(message));
|
||||||
fail: (message) => {
|
}
|
||||||
writeError(message);
|
|
||||||
},
|
/**
|
||||||
stop: () => {},
|
* @param {string} message
|
||||||
setText: (message) => {
|
* @param {...any} optionalParams
|
||||||
writeInformation(message);
|
* @returns {void}
|
||||||
},
|
*/
|
||||||
};
|
function writeVerbose(message, ...optionalParams) {
|
||||||
|
if (!isVerboseMode()) return;
|
||||||
|
|
||||||
|
writeOrBuffer(() => console.log(message, ...optionalParams));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {() => void} messageFunction
|
||||||
|
*/
|
||||||
|
function writeOrBuffer(messageFunction) {
|
||||||
|
if (state.bufferOutput) {
|
||||||
|
state.bufferedMessages.push(messageFunction);
|
||||||
} else {
|
} else {
|
||||||
const spinner = ora(message).start();
|
messageFunction();
|
||||||
return {
|
|
||||||
succeed: (message) => {
|
|
||||||
spinner.succeed(message);
|
|
||||||
},
|
|
||||||
fail: (message) => {
|
|
||||||
spinner.fail(message);
|
|
||||||
},
|
|
||||||
stop: () => {
|
|
||||||
spinner.stop();
|
|
||||||
},
|
|
||||||
setText: (message) => {
|
|
||||||
spinner.text = message;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function confirm(config) {
|
function startBufferingLogs() {
|
||||||
if (isCi()) {
|
state.bufferOutput = true;
|
||||||
return Promise.resolve(config.default);
|
state.bufferedMessages = [];
|
||||||
} else {
|
|
||||||
return inquirerConfirm({
|
|
||||||
message: config.message,
|
|
||||||
default: config.default,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function writeBufferedLogsAndStopBuffering() {
|
||||||
|
state.bufferOutput = false;
|
||||||
|
for (const log of state.bufferedMessages) {
|
||||||
|
log();
|
||||||
|
}
|
||||||
|
state.bufferedMessages = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ui = {
|
export const ui = {
|
||||||
|
writeVerbose,
|
||||||
writeInformation,
|
writeInformation,
|
||||||
writeWarning,
|
writeWarning,
|
||||||
writeError,
|
writeError,
|
||||||
|
writeExitWithoutInstallingMaliciousPackages,
|
||||||
emptyLine,
|
emptyLine,
|
||||||
startProcess,
|
startBufferingLogs,
|
||||||
confirm,
|
writeBufferedLogsAndStopBuffering,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
42
packages/safe-chain/src/installLocation.js
Normal file
42
packages/safe-chain/src/installLocation.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
/** @type {NodeJS.Process & { pkg?: unknown }} */
|
||||||
|
const processWithPkg = process;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} executablePath
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function deriveInstallDirFromExecutablePath(executablePath) {
|
||||||
|
if (!executablePath) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathLibrary = executablePath.includes("\\") ? path.win32 : path.posix;
|
||||||
|
const executableDir = pathLibrary.dirname(executablePath);
|
||||||
|
if (pathLibrary.basename(executableDir) !== "bin") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathLibrary.dirname(executableDir);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the install directory for a packaged safe-chain binary.
|
||||||
|
* Custom installation directories only apply to packaged binary installs.
|
||||||
|
* For npm/global/dev-script executions this intentionally returns undefined,
|
||||||
|
* which causes callers to fall back to the default ~/.safe-chain layout.
|
||||||
|
*
|
||||||
|
* @param {{ isPackaged?: boolean, executablePath?: string }} [options]
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
|
export function getInstalledSafeChainDir(options = {}) {
|
||||||
|
const isPackaged = options.isPackaged ?? Boolean(processWithPkg.pkg);
|
||||||
|
if (!isPackaged) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deriveInstallDirFromExecutablePath(
|
||||||
|
options.executablePath ?? process.execPath,
|
||||||
|
);
|
||||||
|
}
|
||||||
51
packages/safe-chain/src/installLocation.spec.js
Normal file
51
packages/safe-chain/src/installLocation.spec.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { describe, it } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import {
|
||||||
|
deriveInstallDirFromExecutablePath,
|
||||||
|
getInstalledSafeChainDir,
|
||||||
|
} from "./installLocation.js";
|
||||||
|
|
||||||
|
describe("deriveInstallDirFromExecutablePath", () => {
|
||||||
|
it("derives the install dir from a Unix binary path", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
deriveInstallDirFromExecutablePath("/usr/local/.safe-chain/bin/safe-chain"),
|
||||||
|
"/usr/local/.safe-chain",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("derives the install dir from a Windows binary path", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
deriveInstallDirFromExecutablePath("C:\\ProgramData\\safe-chain\\bin\\safe-chain.exe"),
|
||||||
|
"C:\\ProgramData\\safe-chain",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns undefined when the executable is not inside a bin directory", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
deriveInstallDirFromExecutablePath("/usr/local/.safe-chain/safe-chain"),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getInstalledSafeChainDir", () => {
|
||||||
|
it("returns undefined for non-packaged executions", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getInstalledSafeChainDir({
|
||||||
|
isPackaged: false,
|
||||||
|
executablePath: "/usr/local/.safe-chain/bin/safe-chain",
|
||||||
|
}),
|
||||||
|
undefined,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the install dir for packaged executions", () => {
|
||||||
|
assert.strictEqual(
|
||||||
|
getInstalledSafeChainDir({
|
||||||
|
isPackaged: true,
|
||||||
|
executablePath: "/usr/local/.safe-chain/bin/safe-chain",
|
||||||
|
}),
|
||||||
|
"/usr/local/.safe-chain",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,19 +4,120 @@ import { scanCommand, shouldScanCommand } from "./scanning/index.js";
|
||||||
import { ui } from "./environment/userInteraction.js";
|
import { ui } from "./environment/userInteraction.js";
|
||||||
import { getPackageManager } from "./packagemanager/currentPackageManager.js";
|
import { getPackageManager } from "./packagemanager/currentPackageManager.js";
|
||||||
import { initializeCliArguments } from "./config/cliArguments.js";
|
import { initializeCliArguments } from "./config/cliArguments.js";
|
||||||
|
import { createSafeChainProxy } from "./registryProxy/registryProxy.js";
|
||||||
|
import chalk from "chalk";
|
||||||
|
import { getAuditStats } from "./scanning/audit/index.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<number>}
|
||||||
|
*/
|
||||||
export async function main(args) {
|
export async function main(args) {
|
||||||
|
if (isSafeChainVerify(args)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
process.on("SIGINT", handleProcessTermination);
|
||||||
|
process.on("SIGTERM", handleProcessTermination);
|
||||||
|
|
||||||
|
const proxy = createSafeChainProxy();
|
||||||
|
await proxy.startServer();
|
||||||
|
|
||||||
|
// Global error handlers to log unhandled errors
|
||||||
|
process.on("uncaughtException", (error) => {
|
||||||
|
ui.writeError(`Safe-chain: Uncaught exception: ${error.message}`);
|
||||||
|
ui.writeVerbose(`Stack trace: ${error.stack}`);
|
||||||
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on("unhandledRejection", (reason) => {
|
||||||
|
ui.writeError(`Safe-chain: Unhandled promise rejection: ${reason}`);
|
||||||
|
if (reason instanceof Error) {
|
||||||
|
ui.writeVerbose(`Stack trace: ${reason.stack}`);
|
||||||
|
}
|
||||||
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This parses all the --safe-chain arguments and removes them from the args array
|
// This parses all the --safe-chain arguments and removes them from the args array
|
||||||
args = initializeCliArguments(args);
|
args = initializeCliArguments(args);
|
||||||
|
|
||||||
if (shouldScanCommand(args)) {
|
if (shouldScanCommand(args)) {
|
||||||
await scanCommand(args);
|
const commandScanResult = await scanCommand(args);
|
||||||
|
|
||||||
|
// Returning the exit code back to the caller allows the promise
|
||||||
|
// to be awaited in the bin files and return the correct exit code
|
||||||
|
if (commandScanResult !== 0) {
|
||||||
|
return commandScanResult;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
ui.writeError("Failed to check for malicious packages:", error.message);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = getPackageManager().runCommand(args);
|
// Buffer logs during package manager execution, this avoids interleaving
|
||||||
process.exit(result.status);
|
// of logs from the package manager and safe-chain
|
||||||
|
// Not doing this could cause bugs to disappear when cursor movement codes
|
||||||
|
// are written by the package manager while safe-chain is writing logs
|
||||||
|
ui.startBufferingLogs();
|
||||||
|
const packageManagerResult = await getPackageManager().runCommand(args);
|
||||||
|
|
||||||
|
// Write all buffered logs
|
||||||
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
|
||||||
|
if (proxy.hasBlockedMaliciousPackages()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy.hasBlockedMinimumAgeRequests()) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auditStats = getAuditStats();
|
||||||
|
if (auditStats.totalPackages > 0) {
|
||||||
|
ui.writeVerbose(
|
||||||
|
`${chalk.green("✔")} Safe-chain: Scanned ${
|
||||||
|
auditStats.totalPackages
|
||||||
|
} packages, no malware found.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (proxy.hasSuppressedVersions()) {
|
||||||
|
ui.writeInformation(
|
||||||
|
`${chalk.yellow(
|
||||||
|
"ℹ",
|
||||||
|
)} Safe-chain: Some package versions were suppressed during package metadata resolution due to minimum package age.`,
|
||||||
|
);
|
||||||
|
ui.writeInformation(
|
||||||
|
` To disable this check, use: ${chalk.cyan(
|
||||||
|
"--safe-chain-skip-minimum-package-age",
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returning the exit code back to the caller allows the promise
|
||||||
|
// to be awaited in the bin files and return the correct exit code
|
||||||
|
return packageManagerResult.status;
|
||||||
|
} catch (/** @type any */ error) {
|
||||||
|
ui.writeError("Failed to check for malicious packages:", error.message);
|
||||||
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
|
||||||
|
// Returning the exit code back to the caller allows the promise
|
||||||
|
// to be awaited in the bin files and return the correct exit code
|
||||||
|
return 1;
|
||||||
|
} finally {
|
||||||
|
await proxy.stopServer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleProcessTermination() {
|
||||||
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param {string[]} args */
|
||||||
|
function isSafeChainVerify(args) {
|
||||||
|
const safeChainCheckCommand = "safe-chain-verify";
|
||||||
|
if (args.length > 0 && args[0] === safeChainCheckCommand) {
|
||||||
|
ui.writeInformation("OK: Safe-chain works!");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import { ui } from "../../environment/userInteraction.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Centralized logging for package-manager command launch failures.
|
||||||
|
*
|
||||||
|
* @param {any} error - Error thrown by safeSpawn while preparing/running the command.
|
||||||
|
* @param {string} command - Command name that failed to execute.
|
||||||
|
* @returns {{status: number}}
|
||||||
|
*/
|
||||||
|
export function reportCommandExecutionFailure(error, command) {
|
||||||
|
const message = typeof error?.message === "string" ? error.message : "Unknown error";
|
||||||
|
ui.writeError(`Error executing command: ${message}`);
|
||||||
|
|
||||||
|
ui.writeError(`Is '${command}' installed and available on your system?`);
|
||||||
|
|
||||||
|
return { status: typeof error?.status === "number" ? error.status : 1 };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,59 @@
|
||||||
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("reportCommandExecutionFailure", () => {
|
||||||
|
let errorLines;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
errorLines = [];
|
||||||
|
|
||||||
|
mock.module("../../environment/userInteraction.js", {
|
||||||
|
namedExports: {
|
||||||
|
ui: {
|
||||||
|
writeError: (...args) => {
|
||||||
|
errorLines.push(args.join(" "));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("reports command errors while preserving exit status", async () => {
|
||||||
|
const { reportCommandExecutionFailure } = await import("./commandErrors.js");
|
||||||
|
|
||||||
|
const result = reportCommandExecutionFailure(
|
||||||
|
{
|
||||||
|
status: 127,
|
||||||
|
message: "Command failed: command -v bun",
|
||||||
|
},
|
||||||
|
"bun",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, { status: 127 });
|
||||||
|
assert.deepStrictEqual(errorLines, [
|
||||||
|
"Error executing command: Command failed: command -v bun",
|
||||||
|
"Is 'bun' installed and available on your system?",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to exit code 1 when status is missing", async () => {
|
||||||
|
const { reportCommandExecutionFailure } = await import("./commandErrors.js");
|
||||||
|
|
||||||
|
const result = reportCommandExecutionFailure(
|
||||||
|
{
|
||||||
|
message: "Network error",
|
||||||
|
},
|
||||||
|
"npm",
|
||||||
|
);
|
||||||
|
|
||||||
|
assert.deepStrictEqual(result, { status: 1 });
|
||||||
|
assert.deepStrictEqual(errorLines, [
|
||||||
|
"Error executing command: Network error",
|
||||||
|
"Is 'npm' installed and available on your system?",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @param {...string} commandArgs
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
export function matchesCommand(args, ...commandArgs) {
|
export function matchesCommand(args, ...commandArgs) {
|
||||||
if (args.length < commandArgs.length) {
|
if (args.length < commandArgs.length) {
|
||||||
return false;
|
return false;
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
|
export function createBunPackageManager() {
|
||||||
|
return {
|
||||||
|
runCommand: (args) => runBunCommand("bun", args),
|
||||||
|
|
||||||
|
// For bun, we use the proxy-only approach to block package downloads,
|
||||||
|
// so we don't need to analyze commands.
|
||||||
|
isSupportedCommand: () => false,
|
||||||
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
|
export function createBunxPackageManager() {
|
||||||
|
return {
|
||||||
|
runCommand: (args) => runBunCommand("bunx", args),
|
||||||
|
|
||||||
|
// For bunx, we use the proxy-only approach to block package downloads,
|
||||||
|
// so we don't need to analyze commands.
|
||||||
|
isSupportedCommand: () => false,
|
||||||
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} command
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<{status: number}>}
|
||||||
|
*/
|
||||||
|
async function runBunCommand(command, args) {
|
||||||
|
try {
|
||||||
|
const result = await safeSpawn(command, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
|
});
|
||||||
|
return { status: result.status };
|
||||||
|
} catch (/** @type any */ error) {
|
||||||
|
return reportCommandExecutionFailure(error, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
import {
|
||||||
|
createBunPackageManager,
|
||||||
|
createBunxPackageManager,
|
||||||
|
} from "./bun/createBunPackageManager.js";
|
||||||
import { createNpmPackageManager } from "./npm/createPackageManager.js";
|
import { createNpmPackageManager } from "./npm/createPackageManager.js";
|
||||||
import { createNpxPackageManager } from "./npx/createPackageManager.js";
|
import { createNpxPackageManager } from "./npx/createPackageManager.js";
|
||||||
import {
|
import {
|
||||||
|
|
@ -5,14 +9,45 @@ import {
|
||||||
createPnpxPackageManager,
|
createPnpxPackageManager,
|
||||||
} from "./pnpm/createPackageManager.js";
|
} from "./pnpm/createPackageManager.js";
|
||||||
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
import { createYarnPackageManager } from "./yarn/createPackageManager.js";
|
||||||
|
import { createPipPackageManager } from "./pip/createPackageManager.js";
|
||||||
|
import { createUvPackageManager } from "./uv/createUvPackageManager.js";
|
||||||
|
import { createPoetryPackageManager } from "./poetry/createPoetryPackageManager.js";
|
||||||
|
import { createPipXPackageManager } from "./pipx/createPipXPackageManager.js";
|
||||||
|
import { createPdmPackageManager } from "./pdm/createPdmPackageManager.js";
|
||||||
|
import { createRushPackageManager } from "./rush/createRushPackageManager.js";
|
||||||
|
import { createRushxPackageManager } from "./rushx/createRushxPackageManager.js";
|
||||||
|
import { createUvxPackageManager } from "./uvx/createUvxPackageManager.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {{packageManagerName: PackageManager | null}}
|
||||||
|
*/
|
||||||
const state = {
|
const state = {
|
||||||
packageManagerName: null,
|
packageManagerName: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export function initializePackageManager(packageManagerName, version) {
|
/**
|
||||||
|
* @typedef {Object} GetDependencyUpdatesResult
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} version
|
||||||
|
* @property {string} type
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PackageManager
|
||||||
|
* @property {(args: string[]) => Promise<{ status: number }>} runCommand
|
||||||
|
* @property {(args: string[]) => boolean} isSupportedCommand
|
||||||
|
* @property {(args: string[]) => Promise<GetDependencyUpdatesResult[]> | GetDependencyUpdatesResult[]} getDependencyUpdatesForCommand
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} packageManagerName
|
||||||
|
* @param {{ tool: string, args: string[] }} [context] - Optional tool context for package managers like pip
|
||||||
|
*
|
||||||
|
* @return {PackageManager}
|
||||||
|
*/
|
||||||
|
export function initializePackageManager(packageManagerName, context) {
|
||||||
if (packageManagerName === "npm") {
|
if (packageManagerName === "npm") {
|
||||||
state.packageManagerName = createNpmPackageManager(version);
|
state.packageManagerName = createNpmPackageManager();
|
||||||
} else if (packageManagerName === "npx") {
|
} else if (packageManagerName === "npx") {
|
||||||
state.packageManagerName = createNpxPackageManager();
|
state.packageManagerName = createNpxPackageManager();
|
||||||
} else if (packageManagerName === "yarn") {
|
} else if (packageManagerName === "yarn") {
|
||||||
|
|
@ -21,6 +56,26 @@ export function initializePackageManager(packageManagerName, version) {
|
||||||
state.packageManagerName = createPnpmPackageManager();
|
state.packageManagerName = createPnpmPackageManager();
|
||||||
} else if (packageManagerName === "pnpx") {
|
} else if (packageManagerName === "pnpx") {
|
||||||
state.packageManagerName = createPnpxPackageManager();
|
state.packageManagerName = createPnpxPackageManager();
|
||||||
|
} else if (packageManagerName === "bun") {
|
||||||
|
state.packageManagerName = createBunPackageManager();
|
||||||
|
} else if (packageManagerName === "bunx") {
|
||||||
|
state.packageManagerName = createBunxPackageManager();
|
||||||
|
} else if (packageManagerName === "pip") {
|
||||||
|
state.packageManagerName = createPipPackageManager(context);
|
||||||
|
} else if (packageManagerName === "uv") {
|
||||||
|
state.packageManagerName = createUvPackageManager();
|
||||||
|
} else if (packageManagerName === "uvx") {
|
||||||
|
state.packageManagerName = createUvxPackageManager();
|
||||||
|
} else if (packageManagerName === "poetry") {
|
||||||
|
state.packageManagerName = createPoetryPackageManager();
|
||||||
|
} else if (packageManagerName === "pipx") {
|
||||||
|
state.packageManagerName = createPipXPackageManager();
|
||||||
|
} else if (packageManagerName === "pdm") {
|
||||||
|
state.packageManagerName = createPdmPackageManager();
|
||||||
|
} else if (packageManagerName === "rush") {
|
||||||
|
state.packageManagerName = createRushPackageManager();
|
||||||
|
} else if (packageManagerName === "rushx") {
|
||||||
|
state.packageManagerName = createRushxPackageManager();
|
||||||
} else {
|
} else {
|
||||||
throw new Error("Unsupported package manager: " + packageManagerName);
|
throw new Error("Unsupported package manager: " + packageManagerName);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,66 @@
|
||||||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||||
import { dryRunScanner } from "./dependencyScanner/dryRunScanner.js";
|
|
||||||
import { nullScanner } from "./dependencyScanner/nullScanner.js";
|
import { nullScanner } from "./dependencyScanner/nullScanner.js";
|
||||||
import { runNpm } from "./runNpmCommand.js";
|
import { runNpm } from "./runNpmCommand.js";
|
||||||
import {
|
import {
|
||||||
getNpmCommandForArgs,
|
getNpmCommandForArgs,
|
||||||
npmInstallCommand,
|
npmInstallCommand,
|
||||||
npmCiCommand,
|
|
||||||
npmInstallTestCommand,
|
|
||||||
npmInstallCiTestCommand,
|
|
||||||
npmUpdateCommand,
|
npmUpdateCommand,
|
||||||
npmAuditCommand,
|
|
||||||
npmExecCommand,
|
npmExecCommand,
|
||||||
} from "./utils/npmCommands.js";
|
} from "./utils/npmCommands.js";
|
||||||
|
|
||||||
export function createNpmPackageManager(version) {
|
/**
|
||||||
const supportedScanners =
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
getMajorVersion(version) >= 22
|
*/
|
||||||
? npm22AndAboveSupportedScanners
|
export function createNpmPackageManager() {
|
||||||
: npm21AndBelowSupportedScanners;
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function isSupportedCommand(args) {
|
function isSupportedCommand(args) {
|
||||||
const scanner = findDependencyScannerForCommand(supportedScanners, args);
|
const scanner = findDependencyScannerForCommand(
|
||||||
|
commandScannerMapping,
|
||||||
|
args
|
||||||
|
);
|
||||||
return scanner.shouldScan(args);
|
return scanner.shouldScan(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*
|
||||||
|
* @returns {ReturnType<import("../currentPackageManager.js").PackageManager["getDependencyUpdatesForCommand"]>}
|
||||||
|
*/
|
||||||
function getDependencyUpdatesForCommand(args) {
|
function getDependencyUpdatesForCommand(args) {
|
||||||
const scanner = findDependencyScannerForCommand(supportedScanners, args);
|
const scanner = findDependencyScannerForCommand(
|
||||||
|
commandScannerMapping,
|
||||||
|
args
|
||||||
|
);
|
||||||
return scanner.scan(args);
|
return scanner.scan(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getWarningMessage: () => warnForLimitedSupport(version),
|
|
||||||
runCommand: runNpm,
|
runCommand: runNpm,
|
||||||
isSupportedCommand,
|
isSupportedCommand,
|
||||||
getDependencyUpdatesForCommand,
|
getDependencyUpdatesForCommand,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const npm22AndAboveSupportedScanners = {
|
/**
|
||||||
[npmInstallCommand]: dryRunScanner(),
|
* @type {Record<string, import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner>}
|
||||||
[npmUpdateCommand]: dryRunScanner(),
|
*/
|
||||||
[npmCiCommand]: dryRunScanner(),
|
const commandScannerMapping = {
|
||||||
[npmAuditCommand]: dryRunScanner({
|
|
||||||
skipScanWhen: (args) => !args.includes("fix"),
|
|
||||||
}),
|
|
||||||
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
|
||||||
|
|
||||||
// Running dry-run on install-test and install-ci-test will install & run tests.
|
|
||||||
// We only want to know if there are changes in the dependencies.
|
|
||||||
// So we run change the dry-run command to only check the install.
|
|
||||||
[npmInstallTestCommand]: dryRunScanner({ dryRunCommand: npmInstallCommand }),
|
|
||||||
[npmInstallCiTestCommand]: dryRunScanner({ dryRunCommand: npmCiCommand }),
|
|
||||||
};
|
|
||||||
|
|
||||||
const npm21AndBelowSupportedScanners = {
|
|
||||||
[npmInstallCommand]: commandArgumentScanner(),
|
[npmInstallCommand]: commandArgumentScanner(),
|
||||||
[npmUpdateCommand]: commandArgumentScanner(),
|
[npmUpdateCommand]: commandArgumentScanner(),
|
||||||
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
[npmExecCommand]: commandArgumentScanner({ ignoreDryRun: true }), // exec command doesn't support dry-run
|
||||||
};
|
};
|
||||||
|
|
||||||
function warnForLimitedSupport(version) {
|
/**
|
||||||
if (getMajorVersion(version) >= 22) {
|
*
|
||||||
return null;
|
* @param {Record<string, import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner>} scanners
|
||||||
}
|
* @param {string[]} args
|
||||||
|
*
|
||||||
return `Aikido-npm will only scan the arguments of the install command for Node.js version prior to version 22.
|
* @returns {import("./dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
|
||||||
Please update your Node.js version to 22 or higher for full coverage. Current version: v${version}`;
|
*/
|
||||||
}
|
|
||||||
|
|
||||||
function getMajorVersion(version) {
|
|
||||||
return parseInt(version.split(".")[0]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function findDependencyScannerForCommand(scanners, args) {
|
function findDependencyScannerForCommand(scanners, args) {
|
||||||
const command = getNpmCommandForArgs(args);
|
const command = getNpmCommandForArgs(args);
|
||||||
if (!command) {
|
if (!command) {
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,29 @@ import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||||
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
|
import { parsePackagesFromInstallArgs } from "../parsing/parsePackagesFromInstallArgs.js";
|
||||||
import { hasDryRunArg } from "../utils/npmCommands.js";
|
import { hasDryRunArg } from "../utils/npmCommands.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ScanResult
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} version
|
||||||
|
* @property {string} type
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} ScannerOptions
|
||||||
|
* @property {boolean} [ignoreDryRun]
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} CommandArgumentScanner
|
||||||
|
* @property {(args: string[]) => Promise<ScanResult[]> | ScanResult[]} scan
|
||||||
|
* @property {(args: string[]) => boolean} shouldScan
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ScannerOptions} [opts]
|
||||||
|
*
|
||||||
|
* @returns {CommandArgumentScanner}
|
||||||
|
*/
|
||||||
export function commandArgumentScanner(opts) {
|
export function commandArgumentScanner(opts) {
|
||||||
const ignoreDryRun = opts?.ignoreDryRun ?? false;
|
const ignoreDryRun = opts?.ignoreDryRun ?? false;
|
||||||
|
|
||||||
|
|
@ -10,14 +33,28 @@ export function commandArgumentScanner(opts) {
|
||||||
shouldScan: (args) => shouldScanDependencies(args, ignoreDryRun),
|
shouldScan: (args) => shouldScanDependencies(args, ignoreDryRun),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<ScanResult[]>}
|
||||||
|
*/
|
||||||
function scanDependencies(args) {
|
function scanDependencies(args) {
|
||||||
return checkChangesFromArgs(args);
|
return checkChangesFromArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @param {boolean} ignoreDryRun
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function shouldScanDependencies(args, ignoreDryRun) {
|
function shouldScanDependencies(args, ignoreDryRun) {
|
||||||
return ignoreDryRun || !hasDryRunArg(args);
|
return ignoreDryRun || !hasDryRunArg(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<ScanResult[]>}
|
||||||
|
*/
|
||||||
export async function checkChangesFromArgs(args) {
|
export async function checkChangesFromArgs(args) {
|
||||||
const changes = [];
|
const changes = [];
|
||||||
const packageUpdates = parsePackagesFromInstallArgs(args);
|
const packageUpdates = parsePackagesFromInstallArgs(args);
|
||||||
|
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
import { ui } from "../../../environment/userInteraction.js";
|
|
||||||
import { parseDryRunOutput } from "../parsing/parseNpmInstallDryRunOutput.js";
|
|
||||||
import { dryRunNpmCommandAndOutput } from "../runNpmCommand.js";
|
|
||||||
import { hasDryRunArg } from "../utils/npmCommands.js";
|
|
||||||
|
|
||||||
export function dryRunScanner(scannerOptions) {
|
|
||||||
return {
|
|
||||||
scan: (args) => scanDependencies(scannerOptions, args),
|
|
||||||
shouldScan: (args) => shouldScanDependencies(scannerOptions, args),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
function scanDependencies(scannerOptions, args) {
|
|
||||||
let dryRunArgs = args;
|
|
||||||
|
|
||||||
if (scannerOptions?.dryRunCommand) {
|
|
||||||
// Replace the first argument with the dryRunCommand (eg: "install" instead of "install-test")
|
|
||||||
dryRunArgs = [scannerOptions.dryRunCommand, ...args.slice(1)];
|
|
||||||
}
|
|
||||||
|
|
||||||
return checkChangesWithDryRun(dryRunArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldScanDependencies(scannerOptions, args) {
|
|
||||||
if (hasDryRunArg(args)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (scannerOptions?.skipScanWhen && scannerOptions.skipScanWhen(args)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkChangesWithDryRun(args) {
|
|
||||||
const dryRunOutput = dryRunNpmCommandAndOutput(args);
|
|
||||||
|
|
||||||
// Dry-run can return a non-zero status code in some cases
|
|
||||||
// e.g., when running "npm audit fix --dry-run", it returns exit code 1
|
|
||||||
// when there are vulnurabilities that can be fixed.
|
|
||||||
if (dryRunOutput.status !== 0 && !dryRunOutput.output) {
|
|
||||||
ui.writeError("Detecting changes failed.");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const parsedOutput = parseDryRunOutput(dryRunOutput.output);
|
|
||||||
|
|
||||||
// reverse the array to have the top-level packages first
|
|
||||||
return parsedOutput.reverse();
|
|
||||||
}
|
|
||||||
|
|
@ -1,3 +1,6 @@
|
||||||
|
/**
|
||||||
|
* @returns {import("./commandArgumentScanner.js").CommandArgumentScanner}
|
||||||
|
*/
|
||||||
export function nullScanner() {
|
export function nullScanner() {
|
||||||
return {
|
return {
|
||||||
scan: () => [],
|
scan: () => [],
|
||||||
|
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
export function parseDryRunOutput(output) {
|
|
||||||
const lines = output.split(/\r?\n/);
|
|
||||||
const packageChanges = [];
|
|
||||||
|
|
||||||
for (const line of lines) {
|
|
||||||
if (line.startsWith("add ")) {
|
|
||||||
packageChanges.push(parseAdd(line));
|
|
||||||
} else if (line.startsWith("remove ")) {
|
|
||||||
packageChanges.push(parseRemove(line));
|
|
||||||
} else if (line.startsWith("change ")) {
|
|
||||||
packageChanges.push(parseChange(line));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return packageChanges;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseAdd(line) {
|
|
||||||
const splitLine = getLineParts(line);
|
|
||||||
const packageName = splitLine[1];
|
|
||||||
const packageVersion = splitLine[splitLine.length - 1];
|
|
||||||
return addedPackage(packageName, packageVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addedPackage(name, version) {
|
|
||||||
return { type: "add", name, version };
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseRemove(line) {
|
|
||||||
const splitLine = getLineParts(line);
|
|
||||||
const packageName = splitLine[1];
|
|
||||||
const packageVersion = splitLine[splitLine.length - 1];
|
|
||||||
return removedPackage(packageName, packageVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function removedPackage(name, version) {
|
|
||||||
return { type: "remove", name, version };
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseChange(line) {
|
|
||||||
const splitLine = getLineParts(line);
|
|
||||||
const packageName = splitLine[1];
|
|
||||||
const packageVersion = splitLine[splitLine.length - 1];
|
|
||||||
const oldVersion = splitLine[2];
|
|
||||||
return changedPackage(packageName, packageVersion, oldVersion);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getLineParts(line) {
|
|
||||||
return line
|
|
||||||
.split(" ")
|
|
||||||
.map((part) => part.trim())
|
|
||||||
.filter((part) => part !== "");
|
|
||||||
}
|
|
||||||
|
|
||||||
function changedPackage(name, version, oldVersion) {
|
|
||||||
return { type: "change", name, version, oldVersion };
|
|
||||||
}
|
|
||||||
|
|
@ -1,134 +0,0 @@
|
||||||
import { describe, it } from "node:test";
|
|
||||||
import assert from "node:assert";
|
|
||||||
import { parseDryRunOutput } from "./parseNpmInstallDryRunOutput.js";
|
|
||||||
|
|
||||||
describe("parseNpmInstallDryRunOutput", () => {
|
|
||||||
it("should parse added packages", () => {
|
|
||||||
const output = `
|
|
||||||
add @jest/transform 29.7.0
|
|
||||||
add @jest/test-result 29.7.0
|
|
||||||
add @jest/reporters 29.7.0
|
|
||||||
add @jest/console 29.7.0
|
|
||||||
add jest-cli 29.7.0
|
|
||||||
add import-local 3.2.0
|
|
||||||
add @jest/types 29.6.3
|
|
||||||
add @jest/core 29.7.0
|
|
||||||
add jest 29.7.0
|
|
||||||
|
|
||||||
added 267 packages in 831ms
|
|
||||||
|
|
||||||
32 packages are looking for funding
|
|
||||||
run \`npm fund\` for details`;
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{ name: "@jest/transform", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "@jest/test-result", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "@jest/reporters", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "@jest/console", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "jest-cli", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "import-local", version: "3.2.0", type: "add" },
|
|
||||||
{ name: "@jest/types", version: "29.6.3", type: "add" },
|
|
||||||
{ name: "@jest/core", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "jest", version: "29.7.0", type: "add" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = parseDryRunOutput(output);
|
|
||||||
|
|
||||||
assert.deepEqual(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse removed packages", () => {
|
|
||||||
const output = `
|
|
||||||
remove react 19.1.0
|
|
||||||
|
|
||||||
removed 1 package in 115ms`;
|
|
||||||
|
|
||||||
const expected = [{ name: "react", version: "19.1.0", type: "remove" }];
|
|
||||||
|
|
||||||
const result = parseDryRunOutput(output);
|
|
||||||
|
|
||||||
assert.deepEqual(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse changed packages", () => {
|
|
||||||
const output = `
|
|
||||||
change react 19.0.0 => 19.1.0
|
|
||||||
|
|
||||||
changed 1 package in 204ms`;
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{
|
|
||||||
name: "react",
|
|
||||||
version: "19.1.0",
|
|
||||||
oldVersion: "19.0.0",
|
|
||||||
type: "change",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = parseDryRunOutput(output);
|
|
||||||
|
|
||||||
assert.deepEqual(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should parse mixed package changes", () => {
|
|
||||||
const output = `
|
|
||||||
add @jest/transform 29.7.0
|
|
||||||
add @jest/test-result 29.7.0
|
|
||||||
add @jest/reporters 29.7.0
|
|
||||||
add @jest/console 29.7.0
|
|
||||||
add jest-cli 29.7.0
|
|
||||||
add import-local 3.2.0
|
|
||||||
add @jest/types 29.6.3
|
|
||||||
add @jest/core 29.7.0
|
|
||||||
add jest 29.7.0
|
|
||||||
remove react 19.1.0
|
|
||||||
change lodash 4.17.0 => 4.18.0
|
|
||||||
|
|
||||||
removed 1 package in 115ms`;
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{ name: "@jest/transform", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "@jest/test-result", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "@jest/reporters", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "@jest/console", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "jest-cli", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "import-local", version: "3.2.0", type: "add" },
|
|
||||||
{ name: "@jest/types", version: "29.6.3", type: "add" },
|
|
||||||
{ name: "@jest/core", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "jest", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "react", version: "19.1.0", type: "remove" },
|
|
||||||
{
|
|
||||||
name: "lodash",
|
|
||||||
version: "4.18.0",
|
|
||||||
oldVersion: "4.17.0",
|
|
||||||
type: "change",
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = parseDryRunOutput(output);
|
|
||||||
|
|
||||||
assert.deepEqual(result, expected);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should work with npm v22.0.0", () => {
|
|
||||||
const output = `
|
|
||||||
add @jest/types 29.6.3
|
|
||||||
add @jest/core 29.7.0
|
|
||||||
add jest 29.7.0
|
|
||||||
|
|
||||||
added 257 packages in 791ms
|
|
||||||
|
|
||||||
44 packages are looking for funding
|
|
||||||
run \`npm fund\` for details`;
|
|
||||||
|
|
||||||
const expected = [
|
|
||||||
{ name: "@jest/types", version: "29.6.3", type: "add" },
|
|
||||||
{ name: "@jest/core", version: "29.7.0", type: "add" },
|
|
||||||
{ name: "jest", version: "29.7.0", type: "add" },
|
|
||||||
];
|
|
||||||
|
|
||||||
const result = parseDryRunOutput(output);
|
|
||||||
|
|
||||||
assert.deepEqual(result, expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,4 +1,21 @@
|
||||||
|
/**
|
||||||
|
* @typedef {Object} PackageDetail
|
||||||
|
* @property {string} name
|
||||||
|
* @property {string} version
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} NpmOption
|
||||||
|
* @property {string} name
|
||||||
|
* @property {number} numberOfParameters
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {PackageDetail[]}
|
||||||
|
*/
|
||||||
export function parsePackagesFromInstallArgs(args) {
|
export function parsePackagesFromInstallArgs(args) {
|
||||||
|
/** @type {{name: string, version: string | null}[]} */
|
||||||
const changes = [];
|
const changes = [];
|
||||||
let defaultTag = "latest";
|
let defaultTag = "latest";
|
||||||
|
|
||||||
|
|
@ -32,9 +49,13 @@ export function parsePackagesFromInstallArgs(args) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return changes;
|
return /** @type {PackageDetail[]} */ (changes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {NpmOption | undefined}
|
||||||
|
*/
|
||||||
function getNpmOption(arg) {
|
function getNpmOption(arg) {
|
||||||
if (isNpmOptionWithParameter(arg)) {
|
if (isNpmOptionWithParameter(arg)) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -54,6 +75,10 @@ function getNpmOption(arg) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function isNpmOptionWithParameter(arg) {
|
function isNpmOptionWithParameter(arg) {
|
||||||
const optionsWithParameters = [
|
const optionsWithParameters = [
|
||||||
"--access",
|
"--access",
|
||||||
|
|
@ -81,6 +106,10 @@ function isNpmOptionWithParameter(arg) {
|
||||||
return optionsWithParameters.includes(arg);
|
return optionsWithParameters.includes(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {{name: string, version: string | null}}
|
||||||
|
*/
|
||||||
function parsePackagename(arg) {
|
function parsePackagename(arg) {
|
||||||
arg = removeAlias(arg);
|
arg = removeAlias(arg);
|
||||||
const lastAtIndex = arg.lastIndexOf("@");
|
const lastAtIndex = arg.lastIndexOf("@");
|
||||||
|
|
@ -102,6 +131,10 @@ function parsePackagename(arg) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function removeAlias(arg) {
|
function removeAlias(arg) {
|
||||||
const aliasIndex = arg.indexOf("@npm:");
|
const aliasIndex = arg.indexOf("@npm:");
|
||||||
if (aliasIndex !== -1) {
|
if (aliasIndex !== -1) {
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,20 @@
|
||||||
import { execSync } from "child_process";
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
import { ui } from "../../environment/userInteraction.js";
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||||
|
|
||||||
export function runNpm(args) {
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*
|
||||||
|
* @returns {Promise<{status: number}>}
|
||||||
|
*/
|
||||||
|
export async function runNpm(args) {
|
||||||
try {
|
try {
|
||||||
const npmCommand = `npm ${args.join(" ")}`;
|
const result = await safeSpawn("npm", args, {
|
||||||
execSync(npmCommand, { stdio: "inherit" });
|
stdio: "inherit",
|
||||||
} catch (error) {
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
if (error.status) {
|
});
|
||||||
return { status: error.status };
|
return { status: result.status };
|
||||||
} else {
|
} catch (/** @type any */ error) {
|
||||||
ui.writeError("Error executing command:", error.message);
|
return reportCommandExecutionFailure(error, "npm");
|
||||||
return { status: 1 };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { status: 0 };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function dryRunNpmCommandAndOutput(args) {
|
|
||||||
try {
|
|
||||||
const npmCommand = `npm ${args.join(" ")} --dry-run`;
|
|
||||||
const output = execSync(npmCommand, { stdio: "pipe" });
|
|
||||||
return { status: 0, output: output.toString() };
|
|
||||||
} catch (error) {
|
|
||||||
if (error.status) {
|
|
||||||
const output = error.stdout ? error.stdout.toString() : "";
|
|
||||||
return { status: error.status, output };
|
|
||||||
} else {
|
|
||||||
ui.writeError("Error executing command:", error.message);
|
|
||||||
return { status: 1 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
// This was ran with the abbrev package to generate the abbrevs object below
|
||||||
|
// console.log(abbrev(commands.concat(Object.keys(aliases))));
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
|
export const abbrevs = {
|
||||||
|
ac: "access",
|
||||||
|
acc: "access",
|
||||||
|
acce: "access",
|
||||||
|
acces: "access",
|
||||||
|
access: "access",
|
||||||
|
add: "add",
|
||||||
|
"add-": "add-user",
|
||||||
|
"add-u": "add-user",
|
||||||
|
"add-us": "add-user",
|
||||||
|
"add-use": "add-user",
|
||||||
|
"add-user": "add-user",
|
||||||
|
addu: "adduser",
|
||||||
|
addus: "adduser",
|
||||||
|
adduse: "adduser",
|
||||||
|
adduser: "adduser",
|
||||||
|
aud: "audit",
|
||||||
|
audi: "audit",
|
||||||
|
audit: "audit",
|
||||||
|
aut: "author",
|
||||||
|
auth: "author",
|
||||||
|
autho: "author",
|
||||||
|
author: "author",
|
||||||
|
b: "bugs",
|
||||||
|
bu: "bugs",
|
||||||
|
bug: "bugs",
|
||||||
|
bugs: "bugs",
|
||||||
|
c: "c",
|
||||||
|
ca: "cache",
|
||||||
|
cac: "cache",
|
||||||
|
cach: "cache",
|
||||||
|
cache: "cache",
|
||||||
|
ci: "ci",
|
||||||
|
cit: "cit",
|
||||||
|
"clean-install": "clean-install",
|
||||||
|
"clean-install-": "clean-install-test",
|
||||||
|
"clean-install-t": "clean-install-test",
|
||||||
|
"clean-install-te": "clean-install-test",
|
||||||
|
"clean-install-tes": "clean-install-test",
|
||||||
|
"clean-install-test": "clean-install-test",
|
||||||
|
com: "completion",
|
||||||
|
comp: "completion",
|
||||||
|
compl: "completion",
|
||||||
|
comple: "completion",
|
||||||
|
complet: "completion",
|
||||||
|
completi: "completion",
|
||||||
|
completio: "completion",
|
||||||
|
completion: "completion",
|
||||||
|
con: "config",
|
||||||
|
conf: "config",
|
||||||
|
confi: "config",
|
||||||
|
config: "config",
|
||||||
|
cr: "create",
|
||||||
|
cre: "create",
|
||||||
|
crea: "create",
|
||||||
|
creat: "create",
|
||||||
|
create: "create",
|
||||||
|
dd: "ddp",
|
||||||
|
ddp: "ddp",
|
||||||
|
ded: "dedupe",
|
||||||
|
dedu: "dedupe",
|
||||||
|
dedup: "dedupe",
|
||||||
|
dedupe: "dedupe",
|
||||||
|
dep: "deprecate",
|
||||||
|
depr: "deprecate",
|
||||||
|
depre: "deprecate",
|
||||||
|
deprec: "deprecate",
|
||||||
|
depreca: "deprecate",
|
||||||
|
deprecat: "deprecate",
|
||||||
|
deprecate: "deprecate",
|
||||||
|
dif: "diff",
|
||||||
|
diff: "diff",
|
||||||
|
"dist-tag": "dist-tag",
|
||||||
|
"dist-tags": "dist-tags",
|
||||||
|
docs: "docs",
|
||||||
|
doct: "doctor",
|
||||||
|
docto: "doctor",
|
||||||
|
doctor: "doctor",
|
||||||
|
ed: "edit",
|
||||||
|
edi: "edit",
|
||||||
|
edit: "edit",
|
||||||
|
exe: "exec",
|
||||||
|
exec: "exec",
|
||||||
|
expla: "explain",
|
||||||
|
explai: "explain",
|
||||||
|
explain: "explain",
|
||||||
|
explo: "explore",
|
||||||
|
explor: "explore",
|
||||||
|
explore: "explore",
|
||||||
|
find: "find",
|
||||||
|
"find-": "find-dupes",
|
||||||
|
"find-d": "find-dupes",
|
||||||
|
"find-du": "find-dupes",
|
||||||
|
"find-dup": "find-dupes",
|
||||||
|
"find-dupe": "find-dupes",
|
||||||
|
"find-dupes": "find-dupes",
|
||||||
|
fu: "fund",
|
||||||
|
fun: "fund",
|
||||||
|
fund: "fund",
|
||||||
|
g: "get",
|
||||||
|
ge: "get",
|
||||||
|
get: "get",
|
||||||
|
help: "help",
|
||||||
|
"help-": "help-search",
|
||||||
|
"help-s": "help-search",
|
||||||
|
"help-se": "help-search",
|
||||||
|
"help-sea": "help-search",
|
||||||
|
"help-sear": "help-search",
|
||||||
|
"help-searc": "help-search",
|
||||||
|
"help-search": "help-search",
|
||||||
|
hl: "hlep",
|
||||||
|
hle: "hlep",
|
||||||
|
hlep: "hlep",
|
||||||
|
ho: "home",
|
||||||
|
hom: "home",
|
||||||
|
home: "home",
|
||||||
|
i: "i",
|
||||||
|
ic: "ic",
|
||||||
|
in: "in",
|
||||||
|
inf: "info",
|
||||||
|
info: "info",
|
||||||
|
ini: "init",
|
||||||
|
init: "init",
|
||||||
|
inn: "innit",
|
||||||
|
inni: "innit",
|
||||||
|
innit: "innit",
|
||||||
|
ins: "ins",
|
||||||
|
inst: "inst",
|
||||||
|
insta: "insta",
|
||||||
|
instal: "instal",
|
||||||
|
install: "install",
|
||||||
|
"install-ci": "install-ci-test",
|
||||||
|
"install-ci-": "install-ci-test",
|
||||||
|
"install-ci-t": "install-ci-test",
|
||||||
|
"install-ci-te": "install-ci-test",
|
||||||
|
"install-ci-tes": "install-ci-test",
|
||||||
|
"install-ci-test": "install-ci-test",
|
||||||
|
"install-cl": "install-clean",
|
||||||
|
"install-cle": "install-clean",
|
||||||
|
"install-clea": "install-clean",
|
||||||
|
"install-clean": "install-clean",
|
||||||
|
"install-t": "install-test",
|
||||||
|
"install-te": "install-test",
|
||||||
|
"install-tes": "install-test",
|
||||||
|
"install-test": "install-test",
|
||||||
|
isnt: "isnt",
|
||||||
|
isnta: "isnta",
|
||||||
|
isntal: "isntal",
|
||||||
|
isntall: "isntall",
|
||||||
|
"isntall-": "isntall-clean",
|
||||||
|
"isntall-c": "isntall-clean",
|
||||||
|
"isntall-cl": "isntall-clean",
|
||||||
|
"isntall-cle": "isntall-clean",
|
||||||
|
"isntall-clea": "isntall-clean",
|
||||||
|
"isntall-clean": "isntall-clean",
|
||||||
|
iss: "issues",
|
||||||
|
issu: "issues",
|
||||||
|
issue: "issues",
|
||||||
|
issues: "issues",
|
||||||
|
it: "it",
|
||||||
|
la: "la",
|
||||||
|
lin: "link",
|
||||||
|
link: "link",
|
||||||
|
lis: "list",
|
||||||
|
list: "list",
|
||||||
|
ll: "ll",
|
||||||
|
ln: "ln",
|
||||||
|
logi: "login",
|
||||||
|
login: "login",
|
||||||
|
logo: "logout",
|
||||||
|
logou: "logout",
|
||||||
|
logout: "logout",
|
||||||
|
ls: "ls",
|
||||||
|
og: "ogr",
|
||||||
|
ogr: "ogr",
|
||||||
|
or: "org",
|
||||||
|
org: "org",
|
||||||
|
ou: "outdated",
|
||||||
|
out: "outdated",
|
||||||
|
outd: "outdated",
|
||||||
|
outda: "outdated",
|
||||||
|
outdat: "outdated",
|
||||||
|
outdate: "outdated",
|
||||||
|
outdated: "outdated",
|
||||||
|
ow: "owner",
|
||||||
|
own: "owner",
|
||||||
|
owne: "owner",
|
||||||
|
owner: "owner",
|
||||||
|
pa: "pack",
|
||||||
|
pac: "pack",
|
||||||
|
pack: "pack",
|
||||||
|
pi: "ping",
|
||||||
|
pin: "ping",
|
||||||
|
ping: "ping",
|
||||||
|
pk: "pkg",
|
||||||
|
pkg: "pkg",
|
||||||
|
pre: "prefix",
|
||||||
|
pref: "prefix",
|
||||||
|
prefi: "prefix",
|
||||||
|
prefix: "prefix",
|
||||||
|
pro: "profile",
|
||||||
|
prof: "profile",
|
||||||
|
profi: "profile",
|
||||||
|
profil: "profile",
|
||||||
|
profile: "profile",
|
||||||
|
pru: "prune",
|
||||||
|
prun: "prune",
|
||||||
|
prune: "prune",
|
||||||
|
pu: "publish",
|
||||||
|
pub: "publish",
|
||||||
|
publ: "publish",
|
||||||
|
publi: "publish",
|
||||||
|
publis: "publish",
|
||||||
|
publish: "publish",
|
||||||
|
q: "query",
|
||||||
|
qu: "query",
|
||||||
|
que: "query",
|
||||||
|
quer: "query",
|
||||||
|
query: "query",
|
||||||
|
r: "r",
|
||||||
|
rb: "rb",
|
||||||
|
reb: "rebuild",
|
||||||
|
rebu: "rebuild",
|
||||||
|
rebui: "rebuild",
|
||||||
|
rebuil: "rebuild",
|
||||||
|
rebuild: "rebuild",
|
||||||
|
rem: "remove",
|
||||||
|
remo: "remove",
|
||||||
|
remov: "remove",
|
||||||
|
remove: "remove",
|
||||||
|
rep: "repo",
|
||||||
|
repo: "repo",
|
||||||
|
res: "restart",
|
||||||
|
rest: "restart",
|
||||||
|
resta: "restart",
|
||||||
|
restar: "restart",
|
||||||
|
restart: "restart",
|
||||||
|
rm: "rm",
|
||||||
|
ro: "root",
|
||||||
|
roo: "root",
|
||||||
|
root: "root",
|
||||||
|
rum: "rum",
|
||||||
|
run: "run",
|
||||||
|
"run-": "run-script",
|
||||||
|
"run-s": "run-script",
|
||||||
|
"run-sc": "run-script",
|
||||||
|
"run-scr": "run-script",
|
||||||
|
"run-scri": "run-script",
|
||||||
|
"run-scrip": "run-script",
|
||||||
|
"run-script": "run-script",
|
||||||
|
s: "s",
|
||||||
|
sb: "sbom",
|
||||||
|
sbo: "sbom",
|
||||||
|
sbom: "sbom",
|
||||||
|
se: "se",
|
||||||
|
sea: "search",
|
||||||
|
sear: "search",
|
||||||
|
searc: "search",
|
||||||
|
search: "search",
|
||||||
|
set: "set",
|
||||||
|
sho: "show",
|
||||||
|
show: "show",
|
||||||
|
shr: "shrinkwrap",
|
||||||
|
shri: "shrinkwrap",
|
||||||
|
shrin: "shrinkwrap",
|
||||||
|
shrink: "shrinkwrap",
|
||||||
|
shrinkw: "shrinkwrap",
|
||||||
|
shrinkwr: "shrinkwrap",
|
||||||
|
shrinkwra: "shrinkwrap",
|
||||||
|
shrinkwrap: "shrinkwrap",
|
||||||
|
si: "sit",
|
||||||
|
sit: "sit",
|
||||||
|
star: "star",
|
||||||
|
stars: "stars",
|
||||||
|
start: "start",
|
||||||
|
sto: "stop",
|
||||||
|
stop: "stop",
|
||||||
|
t: "t",
|
||||||
|
tea: "team",
|
||||||
|
team: "team",
|
||||||
|
tes: "test",
|
||||||
|
test: "test",
|
||||||
|
to: "token",
|
||||||
|
tok: "token",
|
||||||
|
toke: "token",
|
||||||
|
token: "token",
|
||||||
|
ts: "tst",
|
||||||
|
tst: "tst",
|
||||||
|
ud: "udpate",
|
||||||
|
udp: "udpate",
|
||||||
|
udpa: "udpate",
|
||||||
|
udpat: "udpate",
|
||||||
|
udpate: "udpate",
|
||||||
|
un: "un",
|
||||||
|
und: "undeprecate",
|
||||||
|
unde: "undeprecate",
|
||||||
|
undep: "undeprecate",
|
||||||
|
undepr: "undeprecate",
|
||||||
|
undepre: "undeprecate",
|
||||||
|
undeprec: "undeprecate",
|
||||||
|
undepreca: "undeprecate",
|
||||||
|
undeprecat: "undeprecate",
|
||||||
|
undeprecate: "undeprecate",
|
||||||
|
uni: "uninstall",
|
||||||
|
unin: "uninstall",
|
||||||
|
unins: "uninstall",
|
||||||
|
uninst: "uninstall",
|
||||||
|
uninsta: "uninstall",
|
||||||
|
uninstal: "uninstall",
|
||||||
|
uninstall: "uninstall",
|
||||||
|
unl: "unlink",
|
||||||
|
unli: "unlink",
|
||||||
|
unlin: "unlink",
|
||||||
|
unlink: "unlink",
|
||||||
|
unp: "unpublish",
|
||||||
|
unpu: "unpublish",
|
||||||
|
unpub: "unpublish",
|
||||||
|
unpubl: "unpublish",
|
||||||
|
unpubli: "unpublish",
|
||||||
|
unpublis: "unpublish",
|
||||||
|
unpublish: "unpublish",
|
||||||
|
uns: "unstar",
|
||||||
|
unst: "unstar",
|
||||||
|
unsta: "unstar",
|
||||||
|
unstar: "unstar",
|
||||||
|
up: "up",
|
||||||
|
upd: "update",
|
||||||
|
upda: "update",
|
||||||
|
updat: "update",
|
||||||
|
update: "update",
|
||||||
|
upg: "upgrade",
|
||||||
|
upgr: "upgrade",
|
||||||
|
upgra: "upgrade",
|
||||||
|
upgrad: "upgrade",
|
||||||
|
upgrade: "upgrade",
|
||||||
|
ur: "urn",
|
||||||
|
urn: "urn",
|
||||||
|
v: "v",
|
||||||
|
veri: "verison",
|
||||||
|
veris: "verison",
|
||||||
|
veriso: "verison",
|
||||||
|
verison: "verison",
|
||||||
|
vers: "version",
|
||||||
|
versi: "version",
|
||||||
|
versio: "version",
|
||||||
|
version: "version",
|
||||||
|
vi: "view",
|
||||||
|
vie: "view",
|
||||||
|
view: "view",
|
||||||
|
who: "whoami",
|
||||||
|
whoa: "whoami",
|
||||||
|
whoam: "whoami",
|
||||||
|
whoami: "whoami",
|
||||||
|
why: "why",
|
||||||
|
x: "x",
|
||||||
|
};
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
// Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js
|
// Based on https://github.com/npm/cli/blob/latest/lib/utils/cmd-list.js
|
||||||
|
|
||||||
import abbrev from "abbrev";
|
import { abbrevs } from "./abbrevs-generated.js";
|
||||||
|
|
||||||
const commands = [
|
const commands = [
|
||||||
"access",
|
"access",
|
||||||
|
|
@ -73,6 +73,7 @@ const commands = [
|
||||||
];
|
];
|
||||||
|
|
||||||
// These must resolve to an entry in commands
|
// These must resolve to an entry in commands
|
||||||
|
/** @type {Record<string, string>} */
|
||||||
const aliases = {
|
const aliases = {
|
||||||
// aliases
|
// aliases
|
||||||
author: "owner",
|
author: "owner",
|
||||||
|
|
@ -138,6 +139,10 @@ const aliases = {
|
||||||
"add-user": "adduser",
|
"add-user": "adduser",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} c
|
||||||
|
* @returns {string | undefined}
|
||||||
|
*/
|
||||||
export function deref(c) {
|
export function deref(c) {
|
||||||
if (!c) {
|
if (!c) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -158,8 +163,6 @@ export function deref(c) {
|
||||||
return aliases[c];
|
return aliases[c];
|
||||||
}
|
}
|
||||||
|
|
||||||
const abbrevs = abbrev(commands.concat(Object.keys(aliases)));
|
|
||||||
|
|
||||||
// first deref the abbrev, if there is one
|
// first deref the abbrev, if there is one
|
||||||
// then resolve any aliases
|
// then resolve any aliases
|
||||||
// so `npm install-cl` will resolve to `install-clean` then to `ci`
|
// so `npm install-cl` will resolve to `install-clean` then to `ci`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,9 @@
|
||||||
import { deref } from "./cmd-list.js";
|
import { deref } from "./cmd-list.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {string | null}
|
||||||
|
*/
|
||||||
export function getNpmCommandForArgs(args) {
|
export function getNpmCommandForArgs(args) {
|
||||||
if (args.length === 0) {
|
if (args.length === 0) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -13,6 +17,10 @@ export function getNpmCommandForArgs(args) {
|
||||||
return argCommand;
|
return argCommand;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
export function hasDryRunArg(args) {
|
export function hasDryRunArg(args) {
|
||||||
return args.some((arg) => arg === "--dry-run");
|
return args.some((arg) => arg === "--dry-run");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
import { commandArgumentScanner } from "./dependencyScanner/commandArgumentScanner.js";
|
||||||
import { runNpx } from "./runNpxCommand.js";
|
import { runNpx } from "./runNpxCommand.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
export function createNpxPackageManager() {
|
export function createNpxPackageManager() {
|
||||||
const scanner = commandArgumentScanner();
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
getWarningMessage: () => null,
|
|
||||||
runCommand: runNpx,
|
runCommand: runNpx,
|
||||||
isSupportedCommand: (args) => scanner.shouldScan(args),
|
isSupportedCommand: (args) => scanner.shouldScan(args),
|
||||||
getDependencyUpdatesForCommand: (args) => scanner.scan(args),
|
getDependencyUpdatesForCommand: (args) => scanner.scan(args),
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,28 @@
|
||||||
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||||
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
|
||||||
|
*/
|
||||||
export function commandArgumentScanner() {
|
export function commandArgumentScanner() {
|
||||||
return {
|
return {
|
||||||
scan: (args) => scanDependencies(args),
|
scan: (args) => scanDependencies(args),
|
||||||
shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run
|
shouldScan: () => true, // all npx commands need to be scanned, npx doesn't have dry-run
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
|
||||||
|
*/
|
||||||
function scanDependencies(args) {
|
function scanDependencies(args) {
|
||||||
return checkChangesFromArgs(args);
|
return checkChangesFromArgs(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
|
||||||
|
*/
|
||||||
export async function checkChangesFromArgs(args) {
|
export async function checkChangesFromArgs(args) {
|
||||||
const changes = [];
|
const changes = [];
|
||||||
const packageUpdates = parsePackagesFromArguments(args);
|
const packageUpdates = parsePackagesFromArguments(args);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,8 @@
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*
|
||||||
|
* @returns {{name: string, version: string}[]}
|
||||||
|
*/
|
||||||
export function parsePackagesFromArguments(args) {
|
export function parsePackagesFromArguments(args) {
|
||||||
let defaultTag = "latest";
|
let defaultTag = "latest";
|
||||||
|
|
||||||
|
|
@ -21,6 +26,10 @@ export function parsePackagesFromArguments(args) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {{name: string, numberOfParameters: number} | undefined}
|
||||||
|
*/
|
||||||
function getOption(arg) {
|
function getOption(arg) {
|
||||||
if (isOptionWithParameter(arg)) {
|
if (isOptionWithParameter(arg)) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -41,6 +50,10 @@ function getOption(arg) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function isOptionWithParameter(arg) {
|
function isOptionWithParameter(arg) {
|
||||||
const optionsWithParameters = [
|
const optionsWithParameters = [
|
||||||
"--access",
|
"--access",
|
||||||
|
|
@ -68,6 +81,11 @@ function isOptionWithParameter(arg) {
|
||||||
return optionsWithParameters.includes(arg);
|
return optionsWithParameters.includes(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @param {string} defaultTag
|
||||||
|
* @returns {{name: string, version: string}}
|
||||||
|
*/
|
||||||
function parsePackagename(arg, defaultTag) {
|
function parsePackagename(arg, defaultTag) {
|
||||||
// format can be --package=name@version
|
// format can be --package=name@version
|
||||||
// in that case, we need to remove the --package= part
|
// in that case, we need to remove the --package= part
|
||||||
|
|
@ -97,6 +115,10 @@ function parsePackagename(arg, defaultTag) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function removeAlias(arg) {
|
function removeAlias(arg) {
|
||||||
// removes the alias.
|
// removes the alias.
|
||||||
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
import { execSync } from "child_process";
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
import { ui } from "../../environment/userInteraction.js";
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||||
|
|
||||||
export function runNpx(args) {
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*
|
||||||
|
* @returns {Promise<{status: number}>}
|
||||||
|
*/
|
||||||
|
export async function runNpx(args) {
|
||||||
try {
|
try {
|
||||||
const npxCommand = `npx ${args.join(" ")}`;
|
const result = await safeSpawn("npx", args, {
|
||||||
execSync(npxCommand, { stdio: "inherit" });
|
stdio: "inherit",
|
||||||
} catch (error) {
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
if (error.status) {
|
});
|
||||||
return { status: error.status };
|
return { status: result.status };
|
||||||
} else {
|
} catch (/** @type any */ error) {
|
||||||
ui.writeError("Error executing command:", error.message);
|
return reportCommandExecutionFailure(error, "npx");
|
||||||
return { status: 1 };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { status: 0 };
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
import { ui } from "../../environment/userInteraction.js";
|
||||||
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
||||||
|
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
|
export function createPdmPackageManager() {
|
||||||
|
return {
|
||||||
|
runCommand: (args) => runPdmCommand(args),
|
||||||
|
|
||||||
|
// MITM only approach for PDM
|
||||||
|
isSupportedCommand: () => false,
|
||||||
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets CA bundle environment variables used by PDM and Python libraries.
|
||||||
|
* PDM uses httpx (via unearth) which respects SSL_CERT_FILE through Python's ssl module.
|
||||||
|
*
|
||||||
|
* @param {NodeJS.ProcessEnv} env - Environment object to modify
|
||||||
|
* @param {string} combinedCaPath - Path to the combined CA bundle
|
||||||
|
*/
|
||||||
|
function setPdmCaBundleEnvironmentVariables(env, combinedCaPath) {
|
||||||
|
// SSL_CERT_FILE: Used by Python SSL libraries and httpx (which PDM uses)
|
||||||
|
if (env.SSL_CERT_FILE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.SSL_CERT_FILE = combinedCaPath;
|
||||||
|
|
||||||
|
// REQUESTS_CA_BUNDLE: Used by the requests library (PDM plugins may use it)
|
||||||
|
if (env.REQUESTS_CA_BUNDLE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
||||||
|
|
||||||
|
// PIP_CERT: PDM may use pip internally
|
||||||
|
if (env.PIP_CERT) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.PIP_CERT = combinedCaPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a pdm command with safe-chain's certificate bundle and proxy configuration.
|
||||||
|
*
|
||||||
|
* PDM respects standard HTTP_PROXY/HTTPS_PROXY environment variables through
|
||||||
|
* httpx which it uses for package downloads.
|
||||||
|
*
|
||||||
|
* @param {string[]} args - Command line arguments to pass to pdm
|
||||||
|
* @returns {Promise<{status: number}>} Exit status of the pdm command
|
||||||
|
*/
|
||||||
|
async function runPdmCommand(args) {
|
||||||
|
try {
|
||||||
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
|
const combinedCaPath = getCombinedCaBundlePath();
|
||||||
|
setPdmCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
|
const result = await safeSpawn("pdm", args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: result.status };
|
||||||
|
} catch (/** @type any */ error) {
|
||||||
|
return reportCommandExecutionFailure(error, "pdm");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { createPdmPackageManager } from "./createPdmPackageManager.js";
|
||||||
|
|
||||||
|
test("createPdmPackageManager", async (t) => {
|
||||||
|
await t.test("should create package manager with required interface", () => {
|
||||||
|
const pm = createPdmPackageManager();
|
||||||
|
|
||||||
|
assert.ok(pm);
|
||||||
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { runPip } from "./runPipCommand.js";
|
||||||
|
import { PIP_COMMAND } from "./pipSettings.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {{ tool: string, args: string[] }} [context] - Optional context with tool name and args
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
|
export function createPipPackageManager(context) {
|
||||||
|
const tool = context?.tool || PIP_COMMAND;
|
||||||
|
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*/
|
||||||
|
runCommand: (args) => {
|
||||||
|
// Args from main.js are already stripped of --safe-chain-* flags
|
||||||
|
// We just pass the tool (e.g. "python3") and the args (e.g. ["-m", "pip", "install", ...])
|
||||||
|
return runPip(tool, args);
|
||||||
|
},
|
||||||
|
// For pip, rely solely on MITM proxy to detect/deny downloads from known registries.
|
||||||
|
isSupportedCommand: () => false,
|
||||||
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,57 @@
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { createPipPackageManager } from "./createPackageManager.js";
|
||||||
|
|
||||||
|
test("createPipPackageManager", async (t) => {
|
||||||
|
await t.test("should create package manager with required interface", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
|
assert.ok(pm);
|
||||||
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should accept pip3 as command parameter", () => {
|
||||||
|
const pm = createPipPackageManager("pip3");
|
||||||
|
assert.ok(pm);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should support install, download, and wheel commands", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
// MITM-only approach, pip does not scan args
|
||||||
|
assert.strictEqual(pm.isSupportedCommand(["install", "requests"]), false);
|
||||||
|
assert.strictEqual(pm.isSupportedCommand(["download", "requests"]), false);
|
||||||
|
assert.strictEqual(pm.isSupportedCommand(["wheel", "requests"]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should not support uninstall and info commands", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
|
assert.strictEqual(pm.isSupportedCommand(["uninstall", "requests"]), false);
|
||||||
|
assert.strictEqual(pm.isSupportedCommand(["list"]), false);
|
||||||
|
assert.strictEqual(pm.isSupportedCommand(["show", "requests"]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should extract packages from install command", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
const result = pm.getDependencyUpdatesForCommand(["install", "requests==2.28.0"]);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.strictEqual(result.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should return empty array for unsupported commands", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
|
const result = pm.getDependencyUpdatesForCommand(["uninstall", "requests"]);
|
||||||
|
assert.ok(Array.isArray(result));
|
||||||
|
assert.strictEqual(result.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
await t.test("should handle empty args gracefully", () => {
|
||||||
|
const pm = createPipPackageManager();
|
||||||
|
|
||||||
|
assert.strictEqual(pm.isSupportedCommand([]), false);
|
||||||
|
assert.deepStrictEqual(pm.getDependencyUpdatesForCommand([]), []);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
export const PIP_PACKAGE_MANAGER = "pip";
|
||||||
|
|
||||||
|
export const PIP_COMMAND = "pip";
|
||||||
|
export const PIP3_COMMAND = "pip3";
|
||||||
|
export const PYTHON_COMMAND = "python";
|
||||||
|
export const PYTHON3_COMMAND = "python3";
|
||||||
209
packages/safe-chain/src/packagemanager/pip/runPipCommand.js
Normal file
209
packages/safe-chain/src/packagemanager/pip/runPipCommand.js
Normal file
|
|
@ -0,0 +1,209 @@
|
||||||
|
import { ui } from "../../environment/userInteraction.js";
|
||||||
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
||||||
|
import { PIP_COMMAND, PIP3_COMMAND, PYTHON_COMMAND, PYTHON3_COMMAND } from "./pipSettings.js";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import fsSync from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import ini from "ini";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if this pip invocation should bypass safe-chain and spawn directly.
|
||||||
|
* Returns true if the tool is python/python3 but NOT being run with -m pip/pip3.
|
||||||
|
* @param {string} command - The command executable
|
||||||
|
* @param {string[]} args - The arguments
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export function shouldBypassSafeChain(command, args) {
|
||||||
|
if (command === PYTHON_COMMAND || command === PYTHON3_COMMAND) {
|
||||||
|
// Check if args start with -m pip
|
||||||
|
if (args.length >= 2 && args[0] === "-m" && (args[1] === PIP_COMMAND || args[1] === PIP3_COMMAND)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets fallback CA bundle environment variables used by Python libraries.
|
||||||
|
* These are applied in addition to the PIP_CONFIG_FILE to ensure all Python
|
||||||
|
* network libraries respect the combined CA bundle, even if they don't read pip's config.
|
||||||
|
*
|
||||||
|
* @param {NodeJS.ProcessEnv} env - Environment object to modify
|
||||||
|
* @param {string} combinedCaPath - Path to the combined CA bundle
|
||||||
|
*/
|
||||||
|
function setFallbackCaBundleEnvironmentVariables(env, combinedCaPath) {
|
||||||
|
// REQUESTS_CA_BUNDLE: Used by the popular 'requests' library
|
||||||
|
if (env.REQUESTS_CA_BUNDLE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.REQUESTS_CA_BUNDLE = combinedCaPath;
|
||||||
|
|
||||||
|
// SSL_CERT_FILE: Used by some Python SSL libraries and urllib
|
||||||
|
if (env.SSL_CERT_FILE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.SSL_CERT_FILE = combinedCaPath;
|
||||||
|
|
||||||
|
// PIP_CERT: Pip's own environment variable for certificate verification
|
||||||
|
if (env.PIP_CERT) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
env.PIP_CERT = combinedCaPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a pip command with safe-chain's certificate bundle and proxy configuration.
|
||||||
|
*
|
||||||
|
* Creates a temporary pip config file to configure:
|
||||||
|
* - Cert bundle for HTTPS verification
|
||||||
|
* - Proxy settings
|
||||||
|
*
|
||||||
|
* If the user has an existing PIP_CONFIG_FILE, a new temporary config is created that merges
|
||||||
|
* their settings with safe-chain's, leaving the original file unchanged.
|
||||||
|
*
|
||||||
|
* Special handling for commands that modify config/cache/state: PIP_CONFIG_FILE is NOT overridden to allow
|
||||||
|
* users to read/write persistent config. Only CA environment variables are set for these commands.
|
||||||
|
*
|
||||||
|
* @param {string} command - The pip command executable (e.g., 'pip3' or 'python3')
|
||||||
|
* @param {string[]} args - Command line arguments to pass to pip
|
||||||
|
* @returns {Promise<{status: number}>} Exit status of the pip command
|
||||||
|
*/
|
||||||
|
export async function runPip(command, args) {
|
||||||
|
// Check if we should bypass safe-chain (python/python3 without -m pip)
|
||||||
|
if (shouldBypassSafeChain(command, args)) {
|
||||||
|
ui.writeVerbose(`Safe-chain: Bypassing safe-chain for non-pip invocation: ${command} ${args.join(" ")}`);
|
||||||
|
// Spawn the ORIGINAL command with ORIGINAL args
|
||||||
|
return new Promise((_resolve) => {
|
||||||
|
const proc = spawn(command, args, { stdio: "inherit" });
|
||||||
|
proc.on("exit", (/** @type {number | null} */ code) => {
|
||||||
|
ui.writeVerbose(`${command} ${args.join(" ")} exited with status ${code}`);
|
||||||
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
process.exit(code ?? 0);
|
||||||
|
});
|
||||||
|
proc.on("error", (/** @type {Error} */ err) => {
|
||||||
|
ui.writeError(`Error executing command: ${err.message}`);
|
||||||
|
ui.writeBufferedLogsAndStopBuffering();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
|
// Always provide Python with a complete CA bundle (Safe Chain CA + Mozilla + Node built-in roots + user certs)
|
||||||
|
// so that any network request made by pip, including those outside explicit CLI args,
|
||||||
|
// validates correctly under both MITM'd and tunneled HTTPS.
|
||||||
|
const combinedCaPath = getCombinedCaBundlePath();
|
||||||
|
|
||||||
|
// Commands that need access to persistent config/cache/state files
|
||||||
|
// These should not have PIP_CONFIG_FILE overridden as it would prevent them from
|
||||||
|
// reading/writing to the user's actual pip configuration and cache directories
|
||||||
|
const configRelatedCommands = ['config', 'cache', 'debug', 'completion'];
|
||||||
|
const isConfigRelatedCommand = args.length > 0 && configRelatedCommands.includes(args[0]);
|
||||||
|
|
||||||
|
// https://pip.pypa.io/en/stable/topics/https-certificates/ explains that the 'cert' param (which we're providing via INI file)
|
||||||
|
// will tell pip to use the provided CA bundle for HTTPS verification.
|
||||||
|
|
||||||
|
// Proxy settings: GLOBAL_AGENT_HTTP_PROXY is our safe-chain proxy (if active),
|
||||||
|
// otherwise fall back to user-defined HTTPS_PROXY or HTTP_PROXY environment variables
|
||||||
|
const proxy = env.GLOBAL_AGENT_HTTP_PROXY || env.HTTPS_PROXY || env.HTTP_PROXY || '';
|
||||||
|
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const pipConfigPath = path.join(tmpDir, `safe-chain-pip-${Date.now()}.ini`);
|
||||||
|
let cleanupConfigPath = null; // Track temp file for cleanup
|
||||||
|
|
||||||
|
if (isConfigRelatedCommand) {
|
||||||
|
ui.writeVerbose(`Safe-chain: Skipping PIP_CONFIG_FILE override for 'pip ${args[0]}' command to allow persistent config/cache access.`);
|
||||||
|
|
||||||
|
// Still set the fallback CA bundle environment variables to avoid edge cases where a
|
||||||
|
// plugin or extension triggers a network call during config introspection
|
||||||
|
// This can do no harm
|
||||||
|
setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
|
const result = await safeSpawn(command, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: result.status };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: Setting PIP_CONFIG_FILE overrides all pip config levels (Global/User/Site) per pip's loading order
|
||||||
|
if (!env.PIP_CONFIG_FILE) {
|
||||||
|
/** @type {{ global: { cert: string, proxy?: string } }} */
|
||||||
|
const configObj = { global: { cert: combinedCaPath } };
|
||||||
|
if (proxy) {
|
||||||
|
configObj.global.proxy = proxy;
|
||||||
|
}
|
||||||
|
const pipConfig = ini.stringify(configObj);
|
||||||
|
await fs.writeFile(pipConfigPath, pipConfig);
|
||||||
|
env.PIP_CONFIG_FILE = pipConfigPath;
|
||||||
|
cleanupConfigPath = pipConfigPath;
|
||||||
|
|
||||||
|
} else if (fsSync.existsSync(env.PIP_CONFIG_FILE)) {
|
||||||
|
ui.writeVerbose("Safe-chain: Merging user provided PIP_CONFIG_FILE with safe-chain certificate and proxy settings.");
|
||||||
|
const userConfig = env.PIP_CONFIG_FILE;
|
||||||
|
|
||||||
|
// Read the existing config without modifying it
|
||||||
|
let content = await fs.readFile(userConfig, "utf-8");
|
||||||
|
const parsed = ini.parse(content);
|
||||||
|
|
||||||
|
// Ensure [global] section exists
|
||||||
|
parsed.global = parsed.global || {};
|
||||||
|
|
||||||
|
// Cert
|
||||||
|
if (typeof parsed.global.cert !== "undefined") {
|
||||||
|
ui.writeWarning("Safe-chain: User defined cert found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
|
||||||
|
}
|
||||||
|
parsed.global.cert = combinedCaPath;
|
||||||
|
|
||||||
|
// Proxy
|
||||||
|
if (typeof parsed.global.proxy !== "undefined") {
|
||||||
|
ui.writeWarning("Safe-chain: User defined proxy found in PIP_CONFIG_FILE. It will be overwritten in the temporary config.");
|
||||||
|
}
|
||||||
|
if (proxy) {
|
||||||
|
parsed.global.proxy = proxy;
|
||||||
|
}
|
||||||
|
|
||||||
|
const updated = ini.stringify(parsed);
|
||||||
|
|
||||||
|
// Save to a new temp file to avoid overwriting user's original config
|
||||||
|
await fs.writeFile(pipConfigPath, updated, "utf-8");
|
||||||
|
env.PIP_CONFIG_FILE = pipConfigPath;
|
||||||
|
cleanupConfigPath = pipConfigPath;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// The user provided PIP_CONFIG_FILE does not exist on disk
|
||||||
|
// PIP will handle this as an error and inform the user
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set fallback CA bundle environment variables for Python libraries that don't read pip config
|
||||||
|
setFallbackCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
|
const result = await safeSpawn(command, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cleanup temporary config file if we created one
|
||||||
|
if (cleanupConfigPath) {
|
||||||
|
try {
|
||||||
|
await fs.unlink(cleanupConfigPath);
|
||||||
|
} catch {
|
||||||
|
// Ignore cleanup errors - the file may have already been deleted or is inaccessible
|
||||||
|
// Temp files in os.tmpdir() may eventually be cleaned by the OS, but timing varies by platform
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status: result.status };
|
||||||
|
} catch (/** @type any */ error) {
|
||||||
|
return reportCommandExecutionFailure(error, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
419
packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js
Normal file
419
packages/safe-chain/src/packagemanager/pip/runPipCommand.spec.js
Normal file
|
|
@ -0,0 +1,419 @@
|
||||||
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import fs from "node:fs/promises";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import ini from "ini";
|
||||||
|
|
||||||
|
describe("runPipCommand environment variable handling", () => {
|
||||||
|
let runPip;
|
||||||
|
let shouldBypassSafeChain;
|
||||||
|
let capturedArgs = null;
|
||||||
|
let customEnv = null;
|
||||||
|
let capturedConfigContent = null; // Capture config file content before cleanup
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
capturedArgs = null;
|
||||||
|
capturedConfigContent = null;
|
||||||
|
|
||||||
|
// Mock safeSpawn to capture args and config file content before cleanup
|
||||||
|
mock.module("../../utils/safeSpawn.js", {
|
||||||
|
namedExports: {
|
||||||
|
safeSpawn: async (command, args, options) => {
|
||||||
|
capturedArgs = { command, args, options };
|
||||||
|
// Capture the config file content before the function cleans it up
|
||||||
|
if (options.env.PIP_CONFIG_FILE) {
|
||||||
|
try {
|
||||||
|
capturedConfigContent = await fs.readFile(options.env.PIP_CONFIG_FILE, "utf-8");
|
||||||
|
} catch {
|
||||||
|
// Ignore if file doesn't exist or can't be read
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { status: 0 };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock proxy env merge, allow custom env override
|
||||||
|
mock.module("../../registryProxy/registryProxy.js", {
|
||||||
|
namedExports: {
|
||||||
|
mergeSafeChainProxyEnvironmentVariables: (env) => ({
|
||||||
|
...env,
|
||||||
|
...customEnv,
|
||||||
|
// Force deterministic proxy for tests regardless of ambient env
|
||||||
|
GLOBAL_AGENT_HTTP_PROXY: "http://localhost:8080",
|
||||||
|
HTTPS_PROXY: "http://localhost:8080",
|
||||||
|
HTTP_PROXY: "",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Mock certBundle to return a test combined bundle path
|
||||||
|
mock.module("../../registryProxy/certBundle.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mod = await import("./runPipCommand.js");
|
||||||
|
runPip = mod.runPip;
|
||||||
|
shouldBypassSafeChain = mod.shouldBypassSafeChain;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT set PIP_CONFIG_FILE for 'pip config' commands to allow persistent config access", async () => {
|
||||||
|
const res = await runPip("pip3", ["config", "set", "global.index-url", "https://test.pypi.org/simple"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
|
||||||
|
// PIP_CONFIG_FILE should NOT be set for config commands
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||||
|
undefined,
|
||||||
|
"PIP_CONFIG_FILE should NOT be set for pip config commands"
|
||||||
|
);
|
||||||
|
|
||||||
|
// But CA environment variables should still be set
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.REQUESTS_CA_BUNDLE,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"REQUESTS_CA_BUNDLE should still be set"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.SSL_CERT_FILE,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"SSL_CERT_FILE should still be set"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CERT,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"PIP_CERT should still be set"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT set PIP_CONFIG_FILE for 'pip config get' commands", async () => {
|
||||||
|
const res = await runPip("pip3", ["config", "get", "global.index-url"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||||
|
undefined,
|
||||||
|
"PIP_CONFIG_FILE should NOT be set for pip config get"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT set PIP_CONFIG_FILE for 'pip config list' commands", async () => {
|
||||||
|
const res = await runPip("pip3", ["config", "list"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||||
|
undefined,
|
||||||
|
"PIP_CONFIG_FILE should NOT be set for pip config list"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT set PIP_CONFIG_FILE for 'pip cache' commands", async () => {
|
||||||
|
const res = await runPip("pip3", ["cache", "dir"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||||
|
undefined,
|
||||||
|
"PIP_CONFIG_FILE should NOT be set for pip cache commands"
|
||||||
|
);
|
||||||
|
|
||||||
|
// CA env vars should still be set
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.SSL_CERT_FILE,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"SSL_CERT_FILE should still be set"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT set PIP_CONFIG_FILE for 'pip debug' commands", async () => {
|
||||||
|
const res = await runPip("pip3", ["debug"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||||
|
undefined,
|
||||||
|
"PIP_CONFIG_FILE should NOT be set for pip debug"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT set PIP_CONFIG_FILE for 'pip completion' commands", async () => {
|
||||||
|
const res = await runPip("pip3", ["completion", "--bash"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CONFIG_FILE,
|
||||||
|
undefined,
|
||||||
|
"PIP_CONFIG_FILE should NOT be set for pip completion"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set PIP_CERT env var and create config file", async () => {
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
// Check PIP_CERT env var
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.PIP_CERT,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"PIP_CERT should be set to combined bundle path"
|
||||||
|
);
|
||||||
|
// Check PIP_CONFIG_FILE env var exists and is a non-empty string
|
||||||
|
const configPath = capturedArgs.options.env.PIP_CONFIG_FILE;
|
||||||
|
assert.ok(configPath, "PIP_CONFIG_FILE should be set");
|
||||||
|
assert.strictEqual(typeof configPath, "string", "PIP_CONFIG_FILE should be a string");
|
||||||
|
assert.ok(configPath.length > 0, "PIP_CONFIG_FILE should be a non-empty path");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set REQUESTS_CA_BUNDLE and SSL_CERT_FILE for default PyPI (no explicit index)", async () => {
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
|
||||||
|
assert.ok(capturedArgs, "safeSpawn should have been called");
|
||||||
|
|
||||||
|
// Check environment variables are set
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.REQUESTS_CA_BUNDLE,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"REQUESTS_CA_BUNDLE should be set to combined bundle path"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.SSL_CERT_FILE,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"SSL_CERT_FILE should be set to combined bundle path"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should set CA environment variables even for external/test PyPI mirror (covers non-CLI traffic)", async () => {
|
||||||
|
const res = await runPip("pip3", [
|
||||||
|
"install",
|
||||||
|
"certifi",
|
||||||
|
"--index-url",
|
||||||
|
"https://test.pypi.org/simple",
|
||||||
|
]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
// Env vars should be set unconditionally
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.REQUESTS_CA_BUNDLE,
|
||||||
|
"/tmp/test-combined-ca.pem"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.SSL_CERT_FILE,
|
||||||
|
"/tmp/test-combined-ca.pem"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should still set CA env vars for PyPI even with user --cert flag", async () => {
|
||||||
|
// For default PyPI, we still set env vars; pip CLI --cert takes precedence
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
|
||||||
|
// Environment variables still set (pip CLI --cert takes precedence)
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.REQUESTS_CA_BUNDLE,
|
||||||
|
"/tmp/test-combined-ca.pem"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.SSL_CERT_FILE,
|
||||||
|
"/tmp/test-combined-ca.pem"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should preserve HTTPS_PROXY from proxy merge", async () => {
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
|
||||||
|
assert.strictEqual(
|
||||||
|
capturedArgs.options.env.HTTPS_PROXY,
|
||||||
|
"http://localhost:8080",
|
||||||
|
"HTTPS_PROXY should be set by proxy merge"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create a new temp config when existing config exists (original file untouched)", async () => {
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
|
||||||
|
const initial = "[global]\nindex-url = https://example.com/simple\n";
|
||||||
|
await fs.writeFile(userCfgPath, initial, "utf-8");
|
||||||
|
|
||||||
|
customEnv = { PIP_CONFIG_FILE: userCfgPath };
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
|
||||||
|
assert.notStrictEqual(newCfgPath, userCfgPath, "should point to a new temp config file");
|
||||||
|
|
||||||
|
// Original file unchanged
|
||||||
|
const originalContent = await fs.readFile(userCfgPath, "utf-8");
|
||||||
|
const originalParsed = ini.parse(originalContent);
|
||||||
|
assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert");
|
||||||
|
|
||||||
|
// New file has merged settings (read from captured content before cleanup)
|
||||||
|
assert.ok(capturedConfigContent, "config content should have been captured");
|
||||||
|
const newParsed = ini.parse(capturedConfigContent);
|
||||||
|
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "new config should include cert");
|
||||||
|
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "new config should include proxy from env");
|
||||||
|
assert.strictEqual(newParsed.global["index-url"], "https://example.com/simple", "index-url should be preserved");
|
||||||
|
customEnv = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create new config with proxy set from env (ini-validated)", async () => {
|
||||||
|
// No PIP_CONFIG_FILE in env => creation path
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
|
||||||
|
assert.ok(capturedConfigContent, "config content should have been captured");
|
||||||
|
const parsed = ini.parse(capturedConfigContent);
|
||||||
|
assert.ok(parsed.global, "[global] should exist after creation");
|
||||||
|
assert.strictEqual(
|
||||||
|
parsed.global.proxy,
|
||||||
|
"http://localhost:8080",
|
||||||
|
"proxy should be set from merged env"
|
||||||
|
);
|
||||||
|
assert.strictEqual(
|
||||||
|
parsed.global.cert,
|
||||||
|
"/tmp/test-combined-ca.pem",
|
||||||
|
"cert should be set during creation"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create new temp config adding cert but preserving existing proxy (original file unchanged)", async () => {
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
|
||||||
|
const initial = "[global]\nproxy = http://original:9999\n";
|
||||||
|
await fs.writeFile(userCfgPath, initial, "utf-8");
|
||||||
|
|
||||||
|
customEnv = { PIP_CONFIG_FILE: userCfgPath };
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
|
||||||
|
assert.notStrictEqual(newCfgPath, userCfgPath, "should use a new temp config file");
|
||||||
|
|
||||||
|
// Original file unchanged
|
||||||
|
const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8"));
|
||||||
|
assert.strictEqual(originalParsed.global.cert, undefined, "original file should not gain cert");
|
||||||
|
assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy remains");
|
||||||
|
|
||||||
|
// New file: cert and proxy always overwritten (read from captured content)
|
||||||
|
assert.ok(capturedConfigContent, "config content should have been captured");
|
||||||
|
const newParsed = ini.parse(capturedConfigContent);
|
||||||
|
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
|
||||||
|
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
|
||||||
|
customEnv = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create new temp config preserving existing cert and proxy while leaving original file unchanged", async () => {
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const cfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
|
||||||
|
const initialIni = [
|
||||||
|
"[global]",
|
||||||
|
"cert = /path/to/existing.pem",
|
||||||
|
"proxy = http://original:9999",
|
||||||
|
""
|
||||||
|
].join("\n");
|
||||||
|
await fs.writeFile(cfgPath, initialIni, "utf-8");
|
||||||
|
|
||||||
|
customEnv = { PIP_CONFIG_FILE: cfgPath };
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0, "execution should succeed");
|
||||||
|
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
|
||||||
|
assert.notStrictEqual(newCfgPath, cfgPath, "should use a newly generated temp config file");
|
||||||
|
|
||||||
|
// Original file stays untouched
|
||||||
|
const originalContent = await fs.readFile(cfgPath, "utf-8");
|
||||||
|
const originalParsed = ini.parse(originalContent);
|
||||||
|
assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert preserved");
|
||||||
|
assert.strictEqual(originalParsed.global.proxy, "http://original:9999", "original proxy preserved");
|
||||||
|
|
||||||
|
// New temp config: cert and proxy always overwritten (read from captured content)
|
||||||
|
assert.ok(capturedConfigContent, "config content should have been captured");
|
||||||
|
const newParsed = ini.parse(capturedConfigContent);
|
||||||
|
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
|
||||||
|
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
|
||||||
|
customEnv = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should create new temp config preserving existing cert and adding missing proxy", async () => {
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const userCfgPath = path.join(tmpDir, `safe-chain-test-pip-${Date.now()}.ini`);
|
||||||
|
const initial = "[global]\ncert = /path/to/existing.pem\n";
|
||||||
|
await fs.writeFile(userCfgPath, initial, "utf-8");
|
||||||
|
|
||||||
|
customEnv = { PIP_CONFIG_FILE: userCfgPath };
|
||||||
|
const res = await runPip("pip3", ["install", "requests"]);
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
const newCfgPath = capturedArgs.options.env.PIP_CONFIG_FILE;
|
||||||
|
assert.notStrictEqual(newCfgPath, userCfgPath, "should produce a new temp config file");
|
||||||
|
|
||||||
|
// Original remains unchanged
|
||||||
|
const originalParsed = ini.parse(await fs.readFile(userCfgPath, "utf-8"));
|
||||||
|
assert.strictEqual(originalParsed.global.cert, "/path/to/existing.pem", "original cert unchanged");
|
||||||
|
assert.strictEqual(originalParsed.global.proxy, undefined, "original proxy still missing");
|
||||||
|
|
||||||
|
// New file: cert and proxy always overwritten (read from captured content)
|
||||||
|
assert.ok(capturedConfigContent, "config content should have been captured");
|
||||||
|
const newParsed = ini.parse(capturedConfigContent);
|
||||||
|
assert.strictEqual(newParsed.global.cert, "/tmp/test-combined-ca.pem", "cert always overwritten in temp config");
|
||||||
|
assert.strictEqual(newParsed.global.proxy, "http://localhost:8080", "proxy always overwritten in temp config");
|
||||||
|
customEnv = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should log warnings when cert and proxy are already set in user config file", async () => {
|
||||||
|
const tmpDir = os.tmpdir();
|
||||||
|
const cfgPath = path.join(tmpDir, `safe-chain-test-pip-warn-${Date.now()}.ini`);
|
||||||
|
const initialIni = [
|
||||||
|
"[global]",
|
||||||
|
"cert = /user/cert.pem",
|
||||||
|
"proxy = http://user-proxy:9999",
|
||||||
|
""
|
||||||
|
].join("\n");
|
||||||
|
await fs.writeFile(cfgPath, initialIni, "utf-8");
|
||||||
|
|
||||||
|
customEnv = { PIP_CONFIG_FILE: cfgPath };
|
||||||
|
|
||||||
|
// Capture stdout/stderr
|
||||||
|
let output = "";
|
||||||
|
const originalWrite = process.stdout.write;
|
||||||
|
const originalError = process.stderr.write;
|
||||||
|
process.stdout.write = (chunk, ...args) => { output += chunk; return originalWrite.apply(process.stdout, [chunk, ...args]); };
|
||||||
|
process.stderr.write = (chunk, ...args) => { output += chunk; return originalError.apply(process.stderr, [chunk, ...args]); };
|
||||||
|
|
||||||
|
await runPip("pip3", ["install", "requests"]);
|
||||||
|
|
||||||
|
process.stdout.write = originalWrite;
|
||||||
|
process.stderr.write = originalError;
|
||||||
|
|
||||||
|
assert.ok(output.includes("cert found in PIP_CONFIG_FILE"), "Should warn about cert overwrite in output");
|
||||||
|
assert.ok(output.includes("proxy found in PIP_CONFIG_FILE"), "Should warn about proxy overwrite in output");
|
||||||
|
customEnv = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should bypass safe-chain for python correctly", async () => {
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python", []), true);
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python3", []), true);
|
||||||
|
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python", ["--version"]), true);
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python3", ["--version"]), true);
|
||||||
|
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "http.server"]), true);
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "http.server"]), true);
|
||||||
|
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip"]), false);
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip"]), false);
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python", ["-m", "pip3"]), false);
|
||||||
|
assert.strictEqual(shouldBypassSafeChain("python3", ["-m", "pip3"]), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { runPipX } from "./runPipXCommand.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
|
export function createPipXPackageManager() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
*/
|
||||||
|
runCommand: (args) => {
|
||||||
|
return runPipX("pipx", args);
|
||||||
|
},
|
||||||
|
// MITM only
|
||||||
|
isSupportedCommand: () => false,
|
||||||
|
getDependencyUpdatesForCommand: () => [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { test } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
import { createPipXPackageManager } from "./createPipXPackageManager.js";
|
||||||
|
|
||||||
|
test("createPipXPackageManager", async (t) => {
|
||||||
|
await t.test("should create package manager with required interface", () => {
|
||||||
|
const pm = createPipXPackageManager();
|
||||||
|
|
||||||
|
assert.ok(pm);
|
||||||
|
assert.strictEqual(typeof pm.runCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.isSupportedCommand, "function");
|
||||||
|
assert.strictEqual(typeof pm.getDependencyUpdatesForCommand, "function");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
import { ui } from "../../environment/userInteraction.js";
|
||||||
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
|
import { getCombinedCaBundlePath } from "../../registryProxy/certBundle.js";
|
||||||
|
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets CA bundle environment variables used by Python libraries and pipx.
|
||||||
|
*
|
||||||
|
* @param {NodeJS.ProcessEnv} env - Env object
|
||||||
|
* @param {string} combinedCaPath - Path to the combined CA bundle
|
||||||
|
* @return {NodeJS.ProcessEnv} Modified environment object
|
||||||
|
*/
|
||||||
|
function getPipXCaBundleEnvironmentVariables(env, combinedCaPath) {
|
||||||
|
let retVal = { ...env };
|
||||||
|
|
||||||
|
if (env.SSL_CERT_FILE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined SSL_CERT_FILE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
retVal.SSL_CERT_FILE = combinedCaPath;
|
||||||
|
|
||||||
|
if (env.REQUESTS_CA_BUNDLE) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined REQUESTS_CA_BUNDLE found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
retVal.REQUESTS_CA_BUNDLE = combinedCaPath;
|
||||||
|
|
||||||
|
if (env.PIP_CERT) {
|
||||||
|
ui.writeWarning("Safe-chain: User defined PIP_CERT found in environment. It will be overwritten.");
|
||||||
|
}
|
||||||
|
retVal.PIP_CERT = combinedCaPath;
|
||||||
|
return retVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs a pipx command with safe-chain's certificate bundle and proxy configuration.
|
||||||
|
*
|
||||||
|
* @param {string} command - The command to execute
|
||||||
|
* @param {string[]} args - Command line arguments
|
||||||
|
* @returns {Promise<{status: number}>} Exit status of the command
|
||||||
|
*/
|
||||||
|
export async function runPipX(command, args) {
|
||||||
|
try {
|
||||||
|
const env = mergeSafeChainProxyEnvironmentVariables(process.env);
|
||||||
|
|
||||||
|
const combinedCaPath = getCombinedCaBundlePath();
|
||||||
|
const modifiedEnv = getPipXCaBundleEnvironmentVariables(env, combinedCaPath);
|
||||||
|
|
||||||
|
// Note: pipx uses HTTPS_PROXY and HTTP_PROXY environment variables for proxy configuration
|
||||||
|
// These are already set by mergeSafeChainProxyEnvironmentVariables
|
||||||
|
|
||||||
|
const result = await safeSpawn(command, args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env: modifiedEnv,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { status: result.status };
|
||||||
|
} catch (/** @type any */ error) {
|
||||||
|
return reportCommandExecutionFailure(error, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,80 @@
|
||||||
|
import { describe, it, beforeEach, afterEach, mock } from "node:test";
|
||||||
|
import assert from "node:assert";
|
||||||
|
|
||||||
|
describe("runPipXCommand", () => {
|
||||||
|
let runPipX;
|
||||||
|
let safeSpawnMock;
|
||||||
|
let warnMock;
|
||||||
|
let errorMock;
|
||||||
|
let mergeCalls;
|
||||||
|
let mergedEnvReturn;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
mergeCalls = [];
|
||||||
|
mergedEnvReturn = {
|
||||||
|
HTTPS_PROXY: "http://localhost:8080",
|
||||||
|
HTTP_PROXY: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
safeSpawnMock = mock.fn(async () => ({ status: 0 }));
|
||||||
|
warnMock = mock.fn();
|
||||||
|
errorMock = mock.fn();
|
||||||
|
|
||||||
|
mock.module("../../environment/userInteraction.js", {
|
||||||
|
namedExports: {
|
||||||
|
ui: {
|
||||||
|
writeWarning: warnMock,
|
||||||
|
writeError: errorMock,
|
||||||
|
writeInfo: () => {},
|
||||||
|
writeVerbose: () => {},
|
||||||
|
writeSuccess: () => {},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module("../../registryProxy/registryProxy.js", {
|
||||||
|
namedExports: {
|
||||||
|
mergeSafeChainProxyEnvironmentVariables: (env) => {
|
||||||
|
mergeCalls.push(env);
|
||||||
|
return { ...env, ...mergedEnvReturn };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module("../../registryProxy/certBundle.js", {
|
||||||
|
namedExports: {
|
||||||
|
getCombinedCaBundlePath: () => "/tmp/test-combined-ca.pem",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mock.module("../../utils/safeSpawn.js", {
|
||||||
|
namedExports: {
|
||||||
|
safeSpawn: safeSpawnMock,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const mod = await import("./runPipXCommand.js");
|
||||||
|
runPipX = mod.runPipX;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mock.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets CA env vars and proxies before spawning", async () => {
|
||||||
|
const res = await runPipX("pipx", ["install", "ruff"]);
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 0);
|
||||||
|
assert.strictEqual(safeSpawnMock.mock.calls.length, 1, "safeSpawn should be called once");
|
||||||
|
|
||||||
|
const [, , options] = safeSpawnMock.mock.calls[0].arguments;
|
||||||
|
const env = options.env;
|
||||||
|
|
||||||
|
assert.strictEqual(env.SSL_CERT_FILE, "/tmp/test-combined-ca.pem");
|
||||||
|
assert.strictEqual(env.REQUESTS_CA_BUNDLE, "/tmp/test-combined-ca.pem");
|
||||||
|
assert.strictEqual(env.PIP_CERT, "/tmp/test-combined-ca.pem");
|
||||||
|
assert.strictEqual(env.HTTPS_PROXY, "http://localhost:8080");
|
||||||
|
assert.strictEqual(env.HTTP_PROXY, "");
|
||||||
|
assert.ok(mergeCalls.length >= 1, "proxy merge should be invoked");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -4,9 +4,11 @@ import { runPnpmCommand } from "./runPnpmCommand.js";
|
||||||
|
|
||||||
const scanner = commandArgumentScanner();
|
const scanner = commandArgumentScanner();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
export function createPnpmPackageManager() {
|
export function createPnpmPackageManager() {
|
||||||
return {
|
return {
|
||||||
getWarningMessage: () => null,
|
|
||||||
runCommand: (args) => runPnpmCommand(args, "pnpm"),
|
runCommand: (args) => runPnpmCommand(args, "pnpm"),
|
||||||
isSupportedCommand: (args) =>
|
isSupportedCommand: (args) =>
|
||||||
matchesCommand(args, "add") ||
|
matchesCommand(args, "add") ||
|
||||||
|
|
@ -24,9 +26,11 @@ export function createPnpmPackageManager() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../currentPackageManager.js").PackageManager}
|
||||||
|
*/
|
||||||
export function createPnpxPackageManager() {
|
export function createPnpxPackageManager() {
|
||||||
return {
|
return {
|
||||||
getWarningMessage: () => null,
|
|
||||||
runCommand: (args) => runPnpmCommand(args, "pnpx"),
|
runCommand: (args) => runPnpmCommand(args, "pnpx"),
|
||||||
isSupportedCommand: () => true,
|
isSupportedCommand: () => true,
|
||||||
getDependencyUpdatesForCommand: (args) =>
|
getDependencyUpdatesForCommand: (args) =>
|
||||||
|
|
@ -34,6 +38,11 @@ export function createPnpxPackageManager() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @param {boolean} isPnpx
|
||||||
|
* @returns {ReturnType<import("../currentPackageManager.js").PackageManager["getDependencyUpdatesForCommand"]>}
|
||||||
|
*/
|
||||||
function getDependencyUpdatesForCommand(args, isPnpx) {
|
function getDependencyUpdatesForCommand(args, isPnpx) {
|
||||||
if (isPnpx) {
|
if (isPnpx) {
|
||||||
return scanner.scan(args);
|
return scanner.scan(args);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
import { resolvePackageVersion } from "../../../api/npmApi.js";
|
||||||
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
import { parsePackagesFromArguments } from "../parsing/parsePackagesFromArguments.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {import("../../npm/dependencyScanner/commandArgumentScanner.js").CommandArgumentScanner}
|
||||||
|
*/
|
||||||
export function commandArgumentScanner() {
|
export function commandArgumentScanner() {
|
||||||
return {
|
return {
|
||||||
scan: (args) => scanDependencies(args),
|
scan: (args) => scanDependencies(args),
|
||||||
|
|
@ -8,6 +11,10 @@ export function commandArgumentScanner() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {Promise<import("../../npm/dependencyScanner/commandArgumentScanner.js").ScanResult[]>}
|
||||||
|
*/
|
||||||
async function scanDependencies(args) {
|
async function scanDependencies(args) {
|
||||||
const changes = [];
|
const changes = [];
|
||||||
const packageUpdates = parsePackagesFromArguments(args);
|
const packageUpdates = parsePackagesFromArguments(args);
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,7 @@
|
||||||
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @returns {{name: string, version: string}[]}
|
||||||
|
*/
|
||||||
export function parsePackagesFromArguments(args) {
|
export function parsePackagesFromArguments(args) {
|
||||||
const changes = [];
|
const changes = [];
|
||||||
let defaultTag = "latest";
|
let defaultTag = "latest";
|
||||||
|
|
@ -22,6 +26,10 @@ export function parsePackagesFromArguments(args) {
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {{name: string, numberOfParameters: number} | undefined}
|
||||||
|
*/
|
||||||
function getOption(arg) {
|
function getOption(arg) {
|
||||||
if (isOptionWithParameter(arg)) {
|
if (isOptionWithParameter(arg)) {
|
||||||
return {
|
return {
|
||||||
|
|
@ -42,12 +50,21 @@ function getOption(arg) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
function isOptionWithParameter(arg) {
|
function isOptionWithParameter(arg) {
|
||||||
const optionsWithParameters = ["--C", "--dir"];
|
const optionsWithParameters = ["--C", "--dir"];
|
||||||
|
|
||||||
return optionsWithParameters.includes(arg);
|
return optionsWithParameters.includes(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @param {string} defaultTag
|
||||||
|
* @returns {{name: string, version: string}}
|
||||||
|
*/
|
||||||
function parsePackagename(arg, defaultTag) {
|
function parsePackagename(arg, defaultTag) {
|
||||||
// format can be --package=name@version
|
// format can be --package=name@version
|
||||||
// in that case, we need to remove the --package= part
|
// in that case, we need to remove the --package= part
|
||||||
|
|
@ -77,6 +94,10 @@ function parsePackagename(arg, defaultTag) {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {string} arg
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
function removeAlias(arg) {
|
function removeAlias(arg) {
|
||||||
// removes the alias.
|
// removes the alias.
|
||||||
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
// Eg.: server@npm:http-server@latest becomes http-server@latest
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,32 @@
|
||||||
import { spawnSync } from "child_process";
|
import { mergeSafeChainProxyEnvironmentVariables } from "../../registryProxy/registryProxy.js";
|
||||||
import { ui } from "../../environment/userInteraction.js";
|
import { safeSpawn } from "../../utils/safeSpawn.js";
|
||||||
|
import { reportCommandExecutionFailure } from "../_shared/commandErrors.js";
|
||||||
|
|
||||||
export function runPnpmCommand(args, toolName = "pnpm") {
|
/**
|
||||||
|
* @param {string[]} args
|
||||||
|
* @param {string} [toolName]
|
||||||
|
* @returns {Promise<{status: number}>}
|
||||||
|
*/
|
||||||
|
export async function runPnpmCommand(args, toolName = "pnpm") {
|
||||||
try {
|
try {
|
||||||
let result;
|
let result;
|
||||||
|
|
||||||
if (toolName === "pnpm") {
|
if (toolName === "pnpm") {
|
||||||
result = spawnSync("pnpm", args, { stdio: "inherit" });
|
result = await safeSpawn("pnpm", args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
|
});
|
||||||
} else if (toolName === "pnpx") {
|
} else if (toolName === "pnpx") {
|
||||||
result = spawnSync("pnpx", args, { stdio: "inherit" });
|
result = await safeSpawn("pnpx", args, {
|
||||||
|
stdio: "inherit",
|
||||||
|
env: mergeSafeChainProxyEnvironmentVariables(process.env),
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
|
throw new Error(`Unsupported tool name for aikido-pnpm: ${toolName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (result.status !== null) {
|
|
||||||
return { status: result.status };
|
return { status: result.status };
|
||||||
|
} catch (/** @type any */ error) {
|
||||||
|
const target = toolName === "pnpm" ? "pnpm" : "pnpx";
|
||||||
|
return reportCommandExecutionFailure(error, target);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
|
||||||
ui.writeError("Error executing command:", error.message);
|
|
||||||
return { status: 1 };
|
|
||||||
}
|
|
||||||
return { status: 0 };
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue