Compare commits
755 Commits
v0.21.0
...
feature/be
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3fe592926d | ||
|
|
5bcf3069c6 | ||
|
|
6546130518 | ||
|
|
07108cfa8d | ||
|
|
de945c358b | ||
|
|
e5c7b85f82 | ||
|
|
6e06cc3396 | ||
|
|
7e3b5b13b4 | ||
|
|
91ba53476c | ||
|
|
442425a4a5 | ||
|
|
71fe278e33 | ||
|
|
468af25887 | ||
|
|
d437de1ccf | ||
|
|
8c56f40131 | ||
|
|
2bf4f869ab | ||
|
|
dd698afa7e | ||
|
|
5db3f7046c | ||
|
|
b59a788101 | ||
|
|
e7460f7e50 | ||
|
|
1e1727faa1 | ||
|
|
0c87c350e5 | ||
|
|
03101012b9 | ||
|
|
5f855ea779 | ||
|
|
a74d10d138 | ||
|
|
515a95aaf1 | ||
|
|
1bcb4de6f8 | ||
|
|
07764db43e | ||
|
|
54f32c03d0 | ||
|
|
c983250327 | ||
|
|
2308f372d7 | ||
|
|
d68001b949 | ||
|
|
a599623ea9 | ||
|
|
96f0be19a4 | ||
|
|
0f0a442d74 | ||
|
|
c1470eaac0 | ||
|
|
2123fbca77 | ||
|
|
a8cd4bf34c | ||
|
|
02911109ef | ||
|
|
2bad9fec53 | ||
|
|
54ce6f677c | ||
|
|
26a75f5fe3 | ||
|
|
ad7704c1df | ||
|
|
877fee487b | ||
|
|
330ccae82f | ||
|
|
0a5bb296a9 | ||
|
|
437a35bd47 | ||
|
|
612d3655fa | ||
|
|
38cdc5d9d0 | ||
|
|
816124634b | ||
|
|
2b2f3c876b | ||
|
|
20f2624653 | ||
|
|
6509bb5d1b | ||
|
|
e8724c5edc | ||
|
|
2c284bdd49 | ||
|
|
db1e77ceb3 | ||
|
|
df5e69236a | ||
|
|
a3259b042d | ||
|
|
f5e7c2bdfc | ||
|
|
0859ab31ab | ||
|
|
c02219cc92 | ||
|
|
d73b3aee5c | ||
|
|
80eb91e9a1 | ||
|
|
aa6c751007 | ||
|
|
1af786e7c8 | ||
|
|
c46c1976a2 | ||
|
|
3b3ea83ecd | ||
|
|
5980a8081c | ||
|
|
55f64f8050 | ||
|
|
983ae34147 | ||
|
|
4232c0a8ee | ||
|
|
402a8b3105 | ||
|
|
f46bb838ca | ||
|
|
3d0179a119 | ||
|
|
557b33dc73 | ||
|
|
2a1652d0b1 | ||
|
|
f0fdf9b752 | ||
|
|
973efd6412 | ||
|
|
028342c63a | ||
|
|
eb9b907ba3 | ||
|
|
aee0eeef82 | ||
|
|
c977cf6190 | ||
|
|
28bc73bb1a | ||
|
|
19719693b0 | ||
|
|
a243066691 | ||
|
|
741a59c333 | ||
|
|
5642a37c44 | ||
|
|
1726a19cb6 | ||
|
|
40090cda23 | ||
|
|
9945fac150 | ||
|
|
9c416599f8 | ||
|
|
abf88ab4cb | ||
|
|
34903cdd49 | ||
|
|
98c720987d | ||
|
|
1bd7eab223 | ||
|
|
080e17d85a | ||
|
|
a059edf60d | ||
|
|
0a3b64ba5c | ||
|
|
8ee0d0403a | ||
|
|
9dab9186e5 | ||
|
|
c63e4a3d6b | ||
|
|
0e8ff1bc2a | ||
|
|
683967bbfc | ||
|
|
15947616a9 | ||
|
|
813985a903 | ||
|
|
bd48c17aab | ||
|
|
8239a94938 | ||
|
|
fb8d80f6a3 | ||
|
|
8090c12556 | ||
|
|
0e0d42c9fd | ||
|
|
14b48f23b6 | ||
|
|
0c0adf0e5a | ||
|
|
135edd208c | ||
|
|
81a083a634 | ||
|
|
149a2071c3 | ||
|
|
027a1b1f18 | ||
|
|
7adf39a6a0 | ||
|
|
5408ebc95b | ||
|
|
92a90bb8a1 | ||
|
|
6391532b2d | ||
|
|
a161163508 | ||
|
|
5b6bf945d9 | ||
|
|
877a32f180 | ||
|
|
1fe8a79ea3 | ||
|
|
7c8e8c001c | ||
|
|
29c56ab283 | ||
|
|
0391f2b3e3 | ||
|
|
942f585dd1 | ||
|
|
3005db6943 | ||
|
|
f3c33dc81b | ||
|
|
44e2bdec95 | ||
|
|
d71fc0b95f | ||
|
|
f295788ac1 | ||
|
|
c19aa55fd7 | ||
|
|
ea3d93253f | ||
|
|
114dca89c6 | ||
|
|
c7932fa1d9 | ||
|
|
f0ffc27ca7 | ||
|
|
4dfcf70c08 | ||
|
|
71b34061d9 | ||
|
|
368130b07a | ||
|
|
85216ba6e0 | ||
|
|
06aacdee98 | ||
|
|
ef44ae40ec | ||
|
|
26ea2e9da1 | ||
|
|
b90da3740c | ||
|
|
83b361ae57 | ||
|
|
0ae1dc998a | ||
|
|
44f475778f | ||
|
|
7bd3a73bcf | ||
|
|
48f6b7a12b | ||
|
|
122e1fc20b | ||
|
|
850550c5da | ||
|
|
3b4fa064d6 | ||
|
|
78a9231c8a | ||
|
|
e88a4c7982 | ||
|
|
9c056faec7 | ||
|
|
e865fa2b8b | ||
|
|
e1bc648dfc | ||
|
|
9d8d97e556 | ||
|
|
9dc55675ca | ||
|
|
30c9d735aa | ||
|
|
e49ea7061a | ||
|
|
5c50d8b314 | ||
|
|
00ba5b3650 | ||
|
|
af95c1bdb3 | ||
|
|
01e3d910f1 | ||
|
|
1230694f55 | ||
|
|
77f15a225f | ||
|
|
d75abb80d1 | ||
|
|
42bc897610 | ||
|
|
b15f7c3fbc | ||
|
|
bb99dacecd | ||
|
|
4b925418f2 | ||
|
|
9e82efd23a | ||
|
|
8f7c10440c | ||
|
|
a439e1d467 | ||
|
|
718a957ad9 | ||
|
|
059ff9c6b4 | ||
|
|
062b86642d | ||
|
|
a5724aecf9 | ||
|
|
53dccbe82b | ||
|
|
8d6645415a | ||
|
|
4cfcc9aa02 | ||
|
|
5d384e4afa | ||
|
|
5bf25fdebc | ||
|
|
253d1ddd29 | ||
|
|
5eab41b559 | ||
|
|
a076bb3265 | ||
|
|
9c85d9e737 | ||
|
|
1de4ce6729 | ||
|
|
8e0f88e8bd | ||
|
|
36460a884e | ||
|
|
585ae9494d | ||
|
|
ed9d6fe5d8 | ||
|
|
f0147b1315 | ||
|
|
615e5a95f5 | ||
|
|
5b85d18217 | ||
|
|
f05c24dd66 | ||
|
|
fd11279aa3 | ||
|
|
59282952b0 | ||
|
|
8742c76d52 | ||
|
|
9c0193e812 | ||
|
|
64465e1cd9 | ||
|
|
580e20d573 | ||
|
|
bb496daae3 | ||
|
|
4cd568b0e5 | ||
|
|
efd70cd651 | ||
|
|
3d4a63b515 | ||
|
|
42cec9e8c3 | ||
|
|
73565e0e0d | ||
|
|
6dddc5db43 | ||
|
|
ef90d1c0d7 | ||
|
|
0354f5cecf | ||
|
|
2d923246a9 | ||
|
|
241c0d1b35 | ||
|
|
a9767baa69 | ||
|
|
79f0080c80 | ||
|
|
bfa6fc0920 | ||
|
|
c70c87386e | ||
|
|
a5c6eb95c6 | ||
|
|
f5ab2cddd8 | ||
|
|
47d306b44b | ||
|
|
5e73ba7bd0 | ||
|
|
32a30434b1 | ||
|
|
138426311f | ||
|
|
a8ef9dd6ce | ||
|
|
b48794df14 | ||
|
|
85a80568b2 | ||
|
|
fc0e31df56 | ||
|
|
cb4ae8367c | ||
|
|
de020d9901 | ||
|
|
0634357ee9 | ||
|
|
9753a13001 | ||
|
|
d0deef1537 | ||
|
|
4603b57224 | ||
|
|
bb64ca64e2 | ||
|
|
ce4a9c5626 | ||
|
|
b45861090d | ||
|
|
4a3f655a49 | ||
|
|
29e069ac94 | ||
|
|
625fcf8e5c | ||
|
|
2b8ed06c3c | ||
|
|
34d73ad6ed | ||
|
|
e06a8cb676 | ||
|
|
5ba8cd60c8 | ||
|
|
29985714a3 | ||
|
|
64c9d7adbe | ||
|
|
8d56760c64 | ||
|
|
087ae9cc0d | ||
|
|
35b003ae5e | ||
|
|
cab3c68508 | ||
|
|
b6558d4165 | ||
|
|
64cbe5a74d | ||
|
|
1d3e60b4f8 | ||
|
|
07e6ad2d09 | ||
|
|
1911003db5 | ||
|
|
543388b5a4 | ||
|
|
e2774cccf7 | ||
|
|
bf4dd17792 | ||
|
|
4abc29406f | ||
|
|
b75f92a88b | ||
|
|
237a3a4d80 | ||
|
|
3e926298f2 | ||
|
|
e84df69cb6 | ||
|
|
0a43a76a4a | ||
|
|
c852838644 | ||
|
|
9740ddb813 | ||
|
|
5abd01f61c | ||
|
|
e40a241d62 | ||
|
|
a72e587d29 | ||
|
|
976ae0272b | ||
|
|
ccd3081d09 | ||
|
|
844c800cd9 | ||
|
|
ecf314b2e5 | ||
|
|
a78529e218 | ||
|
|
e32f3dfb57 | ||
|
|
e6c4e46dd8 | ||
|
|
f40fca844f | ||
|
|
c7daa4ac46 | ||
|
|
0a4ac41242 | ||
|
|
3336aae2a0 | ||
|
|
1fe69c2a15 | ||
|
|
846eedeab0 | ||
|
|
37c7c4aeb8 | ||
|
|
548a2b6851 | ||
|
|
c64890b5a0 | ||
|
|
664b440d70 | ||
|
|
c929dfbe4a | ||
|
|
20e724f19c | ||
|
|
a6deff77a7 | ||
|
|
8702d7b76d | ||
|
|
c9f4e42735 | ||
|
|
86023788aa | ||
|
|
5a2b6fec9d | ||
|
|
d90dc5af98 | ||
|
|
1d62a3da5f | ||
|
|
f237fa595a | ||
|
|
07ce79b439 | ||
|
|
77511b0994 | ||
|
|
246b83c72d | ||
|
|
a7e4e12f32 | ||
|
|
91c1fa9d0f | ||
|
|
5a2698123e | ||
|
|
752e4dbd66 | ||
|
|
f2769eca1a | ||
|
|
e779041039 | ||
|
|
6c6c3f3373 | ||
|
|
59adf32861 | ||
|
|
55204289ec | ||
|
|
95bf0b496d | ||
|
|
583633c74b | ||
|
|
c822ba7582 | ||
|
|
a5daaa5e8c | ||
|
|
6967c73eaf | ||
|
|
602b0b0e2e | ||
|
|
49b3e4e537 | ||
|
|
ca477c48d4 | ||
|
|
7d986f2821 | ||
|
|
849c3513bb | ||
|
|
a707d8e67e | ||
|
|
3cacecde5a | ||
|
|
4bdc771cd4 | ||
|
|
f13d95df0f | ||
|
|
73aecc60e8 | ||
|
|
6fc4409513 | ||
|
|
9ed698b236 | ||
|
|
69736503ac | ||
|
|
5b8941554b | ||
|
|
0bb7826ad5 | ||
|
|
bae55fb876 | ||
|
|
97255f84e6 | ||
|
|
174f1fe511 | ||
|
|
53fc2f1e78 | ||
|
|
ef5e2e2ea2 | ||
|
|
b2c40345f8 | ||
|
|
a38de8518f | ||
|
|
a98e37b8b4 | ||
|
|
441864be95 | ||
|
|
2c9c791ae5 | ||
|
|
ea3e8e8371 | ||
|
|
c5dc4a9d71 | ||
|
|
3b3ae29414 | ||
|
|
551532d41b | ||
|
|
20537d7bd9 | ||
|
|
66b37b5a98 | ||
|
|
9d4b6e5b43 | ||
|
|
f335b3f03f | ||
|
|
52f759cc00 | ||
|
|
cc3cb1da4b | ||
|
|
2c608bf684 | ||
|
|
a855ed0cf6 | ||
|
|
ad7e97e7df | ||
|
|
a2fea2b368 | ||
|
|
c428a5be57 | ||
|
|
22769977e3 | ||
|
|
50fb6659da | ||
|
|
e4f2606ea2 | ||
|
|
af5cdf48cf | ||
|
|
1940f7f55d | ||
|
|
c785c5165d | ||
|
|
eaf981f635 | ||
|
|
4284bcf0b6 | ||
|
|
586f7cfc98 | ||
|
|
15e9efeeae | ||
|
|
cd8bb2f501 | ||
|
|
fa42e79af3 | ||
|
|
859ddaef1f | ||
|
|
3b247cdd73 | ||
|
|
00aab022f5 | ||
|
|
a40764d7da | ||
|
|
87b3db7019 | ||
|
|
ded533d690 | ||
|
|
fc4ceafa20 | ||
|
|
5b02eebfe5 | ||
|
|
338c9a3eef | ||
|
|
68d21fc20b | ||
|
|
ea9ebdfdf2 | ||
|
|
1d09c793f6 | ||
|
|
856fd4097b | ||
|
|
bb14ae73cc | ||
|
|
44450ff88a | ||
|
|
3a80e032f4 | ||
|
|
6e2d89372f | ||
|
|
5bf7b54496 | ||
|
|
0bdcb2a091 | ||
|
|
b988179685 | ||
|
|
cbfe80809e | ||
|
|
9f826f764c | ||
|
|
262a805317 | ||
|
|
ec25165e54 | ||
|
|
7b34e2ecea | ||
|
|
ec9b8ac925 | ||
|
|
431d88c47c | ||
|
|
e08e1861d6 | ||
|
|
64d2d4d423 | ||
|
|
9f233a0128 | ||
|
|
6939c792bd | ||
|
|
853940b74a | ||
|
|
5aa8940af2 | ||
|
|
cd3f2a90b4 | ||
|
|
bf89c2603d | ||
|
|
19b388d865 | ||
|
|
25e40f164d | ||
|
|
5505f66c41 | ||
|
|
9a07619b89 | ||
|
|
faf2041a82 | ||
|
|
460834f8f3 | ||
|
|
75ae77a6bf | ||
|
|
73f2134caf | ||
|
|
c5efc30f43 | ||
|
|
3099d74b28 | ||
|
|
fcc9309f2e | ||
|
|
e581a9e7e7 | ||
|
|
ac72e6c3ac | ||
|
|
db824152ef | ||
|
|
1de29fe6fc | ||
|
|
ac2026159e | ||
|
|
cfb28055cf | ||
|
|
a2d8970b22 | ||
|
|
abadf9878a | ||
|
|
87590ac4e8 | ||
|
|
999a81dce7 | ||
|
|
031457406a | ||
|
|
3d9d183b77 | ||
|
|
379c664b5c | ||
|
|
4d8f09e279 | ||
|
|
8a0e91ac3b | ||
|
|
3bc798bc9d | ||
|
|
8b4e0afd43 | ||
|
|
c7c4fc8915 | ||
|
|
41c0252cf1 | ||
|
|
4c375ad86f | ||
|
|
459a8fef42 | ||
|
|
00a18704e8 | ||
|
|
dc9bbacc27 | ||
|
|
4da4e1a0d4 | ||
|
|
3318b4af80 | ||
|
|
c1aaa48ecb | ||
|
|
f82a892405 | ||
|
|
287e85d232 | ||
|
|
fa6fbc8ce9 | ||
|
|
61418fa9dd | ||
|
|
0df1126aa9 | ||
|
|
1c72469ad6 | ||
|
|
338f864f60 | ||
|
|
8b0011f6c6 | ||
|
|
e6a044c532 | ||
|
|
bb1e59ea93 | ||
|
|
b761d7d4f7 | ||
|
|
418fb7d17c | ||
|
|
5084483984 | ||
|
|
3c96810aa1 | ||
|
|
dcd1ec7e95 | ||
|
|
4f222b6308 | ||
|
|
071ae38d35 | ||
|
|
3385800f41 | ||
|
|
4fe538b37e | ||
|
|
2bdf4f8286 | ||
|
|
a96366957e | ||
|
|
c44642241c | ||
|
|
b5bf505ab9 | ||
|
|
51f59e5972 | ||
|
|
65d02e754e | ||
|
|
816c0595e1 | ||
|
|
9496001811 | ||
|
|
ec1b79c2b7 | ||
|
|
bab79f2349 | ||
|
|
edd7405313 | ||
|
|
79800871fa | ||
|
|
67dd87d3a9 | ||
|
|
dfc2beb8f3 | ||
|
|
5e5eae7422 | ||
|
|
78f216eaef | ||
|
|
34d5cca972 | ||
|
|
5d771381a1 | ||
|
|
95a65069c0 | ||
|
|
1e4b2d1d03 | ||
|
|
81f1dce887 | ||
|
|
3570c05805 | ||
|
|
b66cc34e1c | ||
|
|
5bafd92edf | ||
|
|
6e4294dce1 | ||
|
|
82b1c85b7c | ||
|
|
41ecb7122f | ||
|
|
2fa7608b9b | ||
|
|
285ee2cdda | ||
|
|
72598ed2ce | ||
|
|
8670cdfd2b | ||
|
|
f8e8440388 | ||
|
|
ab4dee5fcd | ||
|
|
04e87e87d5 | ||
|
|
cc96435db1 | ||
|
|
53af0a6866 | ||
|
|
3577ce6c56 | ||
|
|
0ce35f2d64 | ||
|
|
0e556433f7 | ||
|
|
4b170b69e0 | ||
|
|
fd58f9d99a | ||
|
|
f33ab83b7c | ||
|
|
6777f6e8ff | ||
|
|
1096b00b94 | ||
|
|
6180d53a93 | ||
|
|
fca1139c81 | ||
|
|
847b10322a | ||
|
|
59251c8f27 | ||
|
|
58b087bc63 | ||
|
|
8ab926dc8b | ||
|
|
85f258d9f6 | ||
|
|
042c5ec6e5 | ||
|
|
05d19c0471 | ||
|
|
48af524313 | ||
|
|
bad97102e1 | ||
|
|
98a4efcd82 | ||
|
|
f631dfc628 | ||
|
|
eb5b74cbe3 | ||
|
|
1785ccc39f | ||
|
|
4b896c2e3c | ||
|
|
88a9cdb0ff | ||
|
|
354ff0068a | ||
|
|
0c419d8f85 | ||
|
|
26be592f4d | ||
|
|
fb9b6cae76 | ||
|
|
5bb9b2a6fb | ||
|
|
593694a4b4 | ||
|
|
b207993299 | ||
|
|
a807288052 | ||
|
|
49b956f916 | ||
|
|
53227de55c | ||
|
|
58921556a1 | ||
|
|
442164cc5c | ||
|
|
8414004d8f | ||
|
|
7932188dae | ||
|
|
d4081d954f | ||
|
|
2e85a341c8 | ||
|
|
2969eb58e4 | ||
|
|
9d6ecd8f73 | ||
|
|
0c2a9d0ee8 | ||
|
|
c71e6fef30 | ||
|
|
3186676f94 | ||
|
|
b108f11bb4 | ||
|
|
d56e8a0f7f | ||
|
|
b76c1d7efc | ||
|
|
cbb2f42a2b | ||
|
|
fd056c05a7 | ||
|
|
2f76b4eadc | ||
|
|
fde59a94ae | ||
|
|
7409862140 | ||
|
|
065ac87815 | ||
|
|
d6d810f1a2 | ||
|
|
05c71988c0 | ||
|
|
3e32610ea1 | ||
|
|
be502b7533 | ||
|
|
4e81a982aa | ||
|
|
c977c6f9a4 | ||
|
|
7416229ba3 | ||
|
|
9000c1f4ba | ||
|
|
7423e64bc5 | ||
|
|
1d5f46980d | ||
|
|
e09efa42a8 | ||
|
|
e99be20bae | ||
|
|
6ce858e52e | ||
|
|
f41bd485e3 | ||
|
|
2fc5b10d3d | ||
|
|
f3d69b0116 | ||
|
|
13c5f8356c | ||
|
|
95c3adfa61 | ||
|
|
ef71f66029 | ||
|
|
317bff326b | ||
|
|
542d4ff3ee | ||
|
|
82a55da026 | ||
|
|
0535f50d89 | ||
|
|
fc5cb0eb88 | ||
|
|
524d363e27 | ||
|
|
e2ebdb37f0 | ||
|
|
539dd1bff4 | ||
|
|
f8ec567a35 | ||
|
|
c758c9d3ab | ||
|
|
bfe535d36a | ||
|
|
aaf52475ee | ||
|
|
424dc43652 | ||
|
|
cd35f6d8c7 | ||
|
|
85b0bb1f5e | ||
|
|
b0001e4d50 | ||
|
|
a77b6c5d3e | ||
|
|
3414c7c941 | ||
|
|
332872c7f5 | ||
|
|
c499c57296 | ||
|
|
912bb7c577 | ||
|
|
36d561bbb8 | ||
|
|
fccb1f06ac | ||
|
|
cf46ff0a3b | ||
|
|
6a37a906ce | ||
|
|
0f823956c6 | ||
|
|
703108051a | ||
|
|
795486e5b2 | ||
|
|
799ca8c5f9 | ||
|
|
9cc7393e7b | ||
|
|
791e812c3c | ||
|
|
187c3aea68 | ||
|
|
d7de28a040 | ||
|
|
d1baf6f1b0 | ||
|
|
3201830405 | ||
|
|
728a55f1d8 | ||
|
|
d3ef8d83b3 | ||
|
|
c4e8d6c8ae | ||
|
|
698ad86d17 | ||
|
|
2240c4c629 | ||
|
|
65b82a8e08 | ||
|
|
8032fb5b41 | ||
|
|
56fde3cbe1 | ||
|
|
bccbb708f1 | ||
|
|
80b1ed7fab | ||
|
|
e68035fe30 | ||
|
|
80ecb7de7f | ||
|
|
75cd0a4d9c | ||
|
|
2824a731f5 | ||
|
|
2dbb00036d | ||
|
|
0ad0c2f2c4 | ||
|
|
104f0eb6ee | ||
|
|
c144bb2b97 | ||
|
|
f50b05519b | ||
|
|
ca3c1085ac | ||
|
|
4cee4f01f3 | ||
|
|
82e2134333 | ||
|
|
6add11f1d2 | ||
|
|
744b6aeff5 | ||
|
|
92310a8b3e | ||
|
|
d74ea47e2c | ||
|
|
c665f62700 | ||
|
|
37471141e8 | ||
|
|
81497beb4b | ||
|
|
2d40f34ff0 | ||
|
|
801760add1 | ||
|
|
4ebf8d23fe | ||
|
|
77a7368c5d | ||
|
|
51a01c4f7b | ||
|
|
13d31dd922 | ||
|
|
c9bb303a7d | ||
|
|
6ebfd417e3 | ||
|
|
b527470e75 | ||
|
|
89b4d88eb1 | ||
|
|
a69f698440 | ||
|
|
ee224adcf1 | ||
|
|
5bbae48b6b | ||
|
|
abcfd62b21 | ||
|
|
10d952a22e | ||
|
|
635caf0f9a | ||
|
|
2266a8d051 | ||
|
|
b292a1b793 | ||
|
|
bf398a1cb2 | ||
|
|
e7c98e5526 | ||
|
|
99ff0a34e3 | ||
|
|
c42b7f5a5b | ||
|
|
ed89295012 | ||
|
|
834907cb5d | ||
|
|
e295a1f64c | ||
|
|
7cec4d7979 | ||
|
|
132bbbd657 | ||
|
|
833220f1cb | ||
|
|
e1e422bfc6 | ||
|
|
e4b6ce62cd | ||
|
|
396d01595e | ||
|
|
6a13e648ea | ||
|
|
5fa0cff274 | ||
|
|
bcb2748f89 | ||
|
|
e68a6039b9 | ||
|
|
0199f93994 | ||
|
|
f2cf5c3508 | ||
|
|
1d39756713 | ||
|
|
71455ef88f | ||
|
|
99b8ed875e | ||
|
|
8242666678 | ||
|
|
5aade0456e | ||
|
|
479f56f3e8 | ||
|
|
8c7a55eaa2 | ||
|
|
924b8227b5 | ||
|
|
c3fa29d13c | ||
|
|
e5dab58b42 | ||
|
|
22496a44a8 | ||
|
|
87e6762611 | ||
|
|
ddc79865bc | ||
|
|
6ee185c538 | ||
|
|
367943b543 | ||
|
|
08e7eb7525 | ||
|
|
35ca99866a | ||
|
|
2f83526966 | ||
|
|
5a58404e1b | ||
|
|
8ea907066b | ||
|
|
ffe5d951e0 | ||
|
|
e5af7d98d1 | ||
|
|
27c252600a | ||
|
|
c32cce2a88 | ||
|
|
c01c6c6225 | ||
|
|
a66659476d | ||
|
|
7a8b0343e4 | ||
|
|
cc3077d709 | ||
|
|
d1362a7fba | ||
|
|
4e9e1919a8 | ||
|
|
f19f53ed9a | ||
|
|
f062dc206e | ||
|
|
a97cb334a2 | ||
|
|
cf52a943b5 | ||
|
|
46d0ecc4fb | ||
|
|
348c5e5405 | ||
|
|
25dbe82360 | ||
|
|
fc404da455 | ||
|
|
ed27fb0da9 | ||
|
|
afbd50b43f | ||
|
|
ad2d30b525 | ||
|
|
a570a3327f | ||
|
|
0fd00575a2 | ||
|
|
a3d1ae3742 | ||
|
|
6f408f62ba | ||
|
|
e92e7edd70 | ||
|
|
4e4c4581ea | ||
|
|
3f12ca05a3 | ||
|
|
a681d6aa30 | ||
|
|
3632d0d88c | ||
|
|
a1a9ab2ece | ||
|
|
9c203914dd | ||
|
|
6cfe8ca9f2 | ||
|
|
938b170d98 | ||
|
|
9d6d2cbe53 | ||
|
|
136dd7ef62 | ||
|
|
f0c754cc52 | ||
|
|
28be62dee0 | ||
|
|
49bfbf3f76 | ||
|
|
2f90d936bf | ||
|
|
4a60400af9 | ||
|
|
18d0c235fa | ||
|
|
fe8225753b | ||
|
|
273fb3cf21 | ||
|
|
e3b6693402 | ||
|
|
ac915f14c7 | ||
|
|
5ee52dd4d6 | ||
|
|
b5fd5d5774 | ||
|
|
ae4f5936b3 | ||
|
|
5017fdf4c1 | ||
|
|
f0eda7c93c | ||
|
|
f60a99d0bd | ||
|
|
1440b2722e | ||
|
|
3b92700b5b | ||
|
|
5c0a543669 | ||
|
|
317b695efb | ||
|
|
077e3c1d2b | ||
|
|
b5c5ab0bc3 | ||
|
|
a6188bf2f1 | ||
|
|
16752f4bb1 | ||
|
|
a75dd2dcdd | ||
|
|
63e79664cc | ||
|
|
005b7bdf5b | ||
|
|
0f143af5bc | ||
|
|
76fb800922 | ||
|
|
58f5295652 | ||
|
|
0917a1ae95 |
@@ -47,7 +47,7 @@ services:
|
|||||||
# - <PATH_TO_YOUR_CADDYFILE>:/import/Caddyfile:ro
|
# - <PATH_TO_YOUR_CADDYFILE>:/import/Caddyfile:ro
|
||||||
# - <PATH_TO_YOUR_SITES_DIR>:/import/sites:ro # If your Caddyfile imports other files
|
# - <PATH_TO_YOUR_SITES_DIR>:/import/sites:ro # If your Caddyfile imports other files
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
|
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ services:
|
|||||||
- playwright_caddy_config:/config
|
- playwright_caddy_config:/config
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
|
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "curl", "-sf", "http://localhost:8080/api/v1/health"]
|
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 12
|
retries: 12
|
||||||
|
|||||||
@@ -48,11 +48,12 @@ services:
|
|||||||
tmpfs:
|
tmpfs:
|
||||||
# True tmpfs for E2E test data - fresh on every run, in-memory only
|
# True tmpfs for E2E test data - fresh on every run, in-memory only
|
||||||
# mode=1777 allows any user to write (container runs as non-root)
|
# mode=1777 allows any user to write (container runs as non-root)
|
||||||
- /app/data:size=100M,mode=1777
|
# 256M gives headroom for the backup service's 100MB disk-space check
|
||||||
|
- /app/data:size=256M,mode=1777
|
||||||
volumes:
|
volumes:
|
||||||
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
|
- /var/run/docker.sock:/var/run/docker.sock:ro # For container discovery in tests
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
|
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ services:
|
|||||||
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
|
# - ./my-existing-Caddyfile:/import/Caddyfile:ro
|
||||||
# - ./sites:/import/sites:ro # If your Caddyfile imports other files
|
# - ./sites:/import/sites:ro # If your Caddyfile imports other files
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/api/v1/health || exit 1"]
|
test: ["CMD-SHELL", "wget -qO /dev/null http://localhost:8080/api/v1/health || exit 1"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
timeout: 10s
|
timeout: 10s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -303,6 +303,19 @@ ACQUIS_EOF
|
|||||||
# Also handle case where it might be without trailing slash
|
# Also handle case where it might be without trailing slash
|
||||||
sed -i 's|log_dir: /var/log$|log_dir: /var/log/crowdsec|g' "$CS_CONFIG_DIR/config.yaml"
|
sed -i 's|log_dir: /var/log$|log_dir: /var/log/crowdsec|g' "$CS_CONFIG_DIR/config.yaml"
|
||||||
|
|
||||||
|
# Redirect CrowdSec LAPI database to persistent volume
|
||||||
|
# Default path /var/lib/crowdsec/data/crowdsec.db is ephemeral (not volume-mounted),
|
||||||
|
# so it is destroyed on every container rebuild. The bouncer API key (stored on the
|
||||||
|
# persistent volume at /app/data/crowdsec/) survives rebuilds but the LAPI database
|
||||||
|
# that validates it does not — causing perpetual key rejection.
|
||||||
|
# Redirecting db_path to the volume-mounted CS_DATA_DIR fixes this.
|
||||||
|
sed -i "s|db_path: /var/lib/crowdsec/data/crowdsec.db|db_path: ${CS_DATA_DIR}/crowdsec.db|g" "$CS_CONFIG_DIR/config.yaml"
|
||||||
|
if grep -q "db_path:.*${CS_DATA_DIR}" "$CS_CONFIG_DIR/config.yaml"; then
|
||||||
|
echo "✓ CrowdSec LAPI database redirected to persistent volume: ${CS_DATA_DIR}/crowdsec.db"
|
||||||
|
else
|
||||||
|
echo "⚠️ WARNING: Could not verify LAPI db_path redirect — bouncer keys may not survive rebuilds"
|
||||||
|
fi
|
||||||
|
|
||||||
# Verify LAPI configuration was applied correctly
|
# Verify LAPI configuration was applied correctly
|
||||||
if grep -q "listen_uri:.*:8085" "$CS_CONFIG_DIR/config.yaml"; then
|
if grep -q "listen_uri:.*:8085" "$CS_CONFIG_DIR/config.yaml"; then
|
||||||
echo "✓ CrowdSec LAPI configured for port 8085"
|
echo "✓ CrowdSec LAPI configured for port 8085"
|
||||||
@@ -310,10 +323,11 @@ ACQUIS_EOF
|
|||||||
echo "✗ WARNING: LAPI port configuration may be incorrect"
|
echo "✗ WARNING: LAPI port configuration may be incorrect"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Update hub index to ensure CrowdSec can start
|
# Always refresh hub index on startup (stale index causes hash mismatch errors on collection install)
|
||||||
if [ ! -f "/etc/crowdsec/hub/.index.json" ]; then
|
echo "Updating CrowdSec hub index..."
|
||||||
echo "Updating CrowdSec hub index..."
|
if ! timeout 60s cscli hub update 2>&1; then
|
||||||
timeout 60s cscli hub update 2>/dev/null || echo "⚠️ Hub update timed out or failed, continuing..."
|
echo "⚠️ Hub index update failed (network issue?). Collections may fail to install."
|
||||||
|
echo " CrowdSec will still start with whatever index is cached."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Ensure local machine is registered (auto-heal for volume/config mismatch)
|
# Ensure local machine is registered (auto-heal for volume/config mismatch)
|
||||||
@@ -321,12 +335,11 @@ ACQUIS_EOF
|
|||||||
echo "Registering local machine..."
|
echo "Registering local machine..."
|
||||||
cscli machines add -a --force 2>/dev/null || echo "Warning: Machine registration may have failed"
|
cscli machines add -a --force 2>/dev/null || echo "Warning: Machine registration may have failed"
|
||||||
|
|
||||||
# Install hub items (parsers, scenarios, collections) if local mode enabled
|
# Always ensure required collections are present (idempotent — already-installed items are skipped).
|
||||||
if [ "$SECURITY_CROWDSEC_MODE" = "local" ]; then
|
# Collections are just config files with zero runtime cost when CrowdSec is disabled.
|
||||||
echo "Installing CrowdSec hub items..."
|
echo "Ensuring CrowdSec hub items are installed..."
|
||||||
if [ -x /usr/local/bin/install_hub_items.sh ]; then
|
if [ -x /usr/local/bin/install_hub_items.sh ]; then
|
||||||
/usr/local/bin/install_hub_items.sh 2>/dev/null || echo "Warning: Some hub items may not have installed"
|
/usr/local/bin/install_hub_items.sh || echo "⚠️ Some hub items may not have installed. CrowdSec can still start."
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fix ownership AFTER cscli commands (they run as root and create root-owned files)
|
# Fix ownership AFTER cscli commands (they run as root and create root-owned files)
|
||||||
@@ -365,7 +378,7 @@ echo "Caddy started (PID: $CADDY_PID)"
|
|||||||
echo "Waiting for Caddy admin API..."
|
echo "Waiting for Caddy admin API..."
|
||||||
i=1
|
i=1
|
||||||
while [ "$i" -le 30 ]; do
|
while [ "$i" -le 30 ]; do
|
||||||
if curl -sf http://127.0.0.1:2019/config/ > /dev/null 2>&1; then
|
if wget -qO /dev/null http://127.0.0.1:2019/config/ 2>/dev/null; then
|
||||||
echo "Caddy is ready!"
|
echo "Caddy is ready!"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -9,13 +9,12 @@
|
|||||||
.git/
|
.git/
|
||||||
.gitignore
|
.gitignore
|
||||||
.github/
|
.github/
|
||||||
.pre-commit-config.yaml
|
|
||||||
codecov.yml
|
codecov.yml
|
||||||
.goreleaser.yaml
|
.goreleaser.yaml
|
||||||
.sourcery.yml
|
.sourcery.yml
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Python (pre-commit, tooling)
|
# Python (tooling)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
__pycache__/
|
__pycache__/
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
|
|||||||
12
.github/agents/Backend_Dev.agent.md
vendored
12
.github/agents/Backend_Dev.agent.md
vendored
File diff suppressed because one or more lines are too long
3
.github/agents/DevOps.agent.md
vendored
3
.github/agents/DevOps.agent.md
vendored
File diff suppressed because one or more lines are too long
3
.github/agents/Doc_Writer.agent.md
vendored
3
.github/agents/Doc_Writer.agent.md
vendored
File diff suppressed because one or more lines are too long
5
.github/agents/Frontend_Dev.agent.md
vendored
5
.github/agents/Frontend_Dev.agent.md
vendored
File diff suppressed because one or more lines are too long
50
.github/agents/Management.agent.md
vendored
50
.github/agents/Management.agent.md
vendored
File diff suppressed because one or more lines are too long
13
.github/agents/Planning.agent.md
vendored
13
.github/agents/Planning.agent.md
vendored
File diff suppressed because one or more lines are too long
3
.github/agents/Playwright_Dev.agent.md
vendored
3
.github/agents/Playwright_Dev.agent.md
vendored
File diff suppressed because one or more lines are too long
4
.github/agents/QA_Security.agent.md
vendored
4
.github/agents/QA_Security.agent.md
vendored
File diff suppressed because one or more lines are too long
2
.github/agents/Supervisor.agent.md
vendored
2
.github/agents/Supervisor.agent.md
vendored
File diff suppressed because one or more lines are too long
@@ -126,11 +126,11 @@ graph TB
|
|||||||
| **HTTP Framework** | Gin | Latest | Routing, middleware, HTTP handling |
|
| **HTTP Framework** | Gin | Latest | Routing, middleware, HTTP handling |
|
||||||
| **Database** | SQLite | 3.x | Embedded database |
|
| **Database** | SQLite | 3.x | Embedded database |
|
||||||
| **ORM** | GORM | Latest | Database abstraction layer |
|
| **ORM** | GORM | Latest | Database abstraction layer |
|
||||||
| **Reverse Proxy** | Caddy Server | 2.11.0-beta.2 | Embedded HTTP/HTTPS proxy |
|
| **Reverse Proxy** | Caddy Server | 2.11.2 | Embedded HTTP/HTTPS proxy |
|
||||||
| **WebSocket** | gorilla/websocket | Latest | Real-time log streaming |
|
| **WebSocket** | gorilla/websocket | Latest | Real-time log streaming |
|
||||||
| **Crypto** | golang.org/x/crypto | Latest | Password hashing, encryption |
|
| **Crypto** | golang.org/x/crypto | Latest | Password hashing, encryption |
|
||||||
| **Metrics** | Prometheus Client | Latest | Application metrics |
|
| **Metrics** | Prometheus Client | Latest | Application metrics |
|
||||||
| **Notifications** | Shoutrrr | Latest | Multi-platform alerts |
|
| **Notifications** | Notify | Latest | Multi-platform alerts |
|
||||||
| **Docker Client** | Docker SDK | Latest | Container discovery |
|
| **Docker Client** | Docker SDK | Latest | Container discovery |
|
||||||
| **Logging** | Logrus + Lumberjack | Latest | Structured logging with rotation |
|
| **Logging** | Logrus + Lumberjack | Latest | Structured logging with rotation |
|
||||||
|
|
||||||
@@ -1263,8 +1263,8 @@ docker exec charon /app/scripts/restore-backup.sh \
|
|||||||
- Future: Dynamic plugin loading for custom providers
|
- Future: Dynamic plugin loading for custom providers
|
||||||
|
|
||||||
2. **Notification Channels:**
|
2. **Notification Channels:**
|
||||||
- Shoutrrr provides 40+ channels (Discord, Slack, Email, etc.)
|
- Notify provides multi-platform channels (Discord, Slack, Gotify, etc.)
|
||||||
- Custom channels via Shoutrrr service URLs
|
- Provider-based configuration with per-channel feature flags
|
||||||
|
|
||||||
3. **Authentication Providers:**
|
3. **Authentication Providers:**
|
||||||
- Current: Local database authentication
|
- Current: Local database authentication
|
||||||
|
|||||||
14
.github/instructions/copilot-instructions.md
vendored
14
.github/instructions/copilot-instructions.md
vendored
@@ -67,7 +67,7 @@ Before proposing ANY code change or fix, you must build a mental map of the feat
|
|||||||
|
|
||||||
- **Run**: `cd backend && go run ./cmd/api`.
|
- **Run**: `cd backend && go run ./cmd/api`.
|
||||||
- **Test**: `go test ./...`.
|
- **Test**: `go test ./...`.
|
||||||
- **Static Analysis (BLOCKING)**: Fast linters run automatically on every commit via pre-commit hooks.
|
- **Static Analysis (BLOCKING)**: Fast linters run automatically on every commit via lefthook pre-commit-phase hooks.
|
||||||
- **Staticcheck errors MUST be fixed** - commits are BLOCKED until resolved
|
- **Staticcheck errors MUST be fixed** - commits are BLOCKED until resolved
|
||||||
- Manual run: `make lint-fast` or VS Code task "Lint: Staticcheck (Fast)"
|
- Manual run: `make lint-fast` or VS Code task "Lint: Staticcheck (Fast)"
|
||||||
- Staticcheck-only: `make lint-staticcheck-only`
|
- Staticcheck-only: `make lint-staticcheck-only`
|
||||||
@@ -79,7 +79,7 @@ Before proposing ANY code change or fix, you must build a mental map of the feat
|
|||||||
- **Security**: Sanitize all file paths using `filepath.Clean`. Use `fmt.Errorf("context: %w", err)` for error wrapping.
|
- **Security**: Sanitize all file paths using `filepath.Clean`. Use `fmt.Errorf("context: %w", err)` for error wrapping.
|
||||||
- **Graceful Shutdown**: Long-running work must respect `server.Run(ctx)`.
|
- **Graceful Shutdown**: Long-running work must respect `server.Run(ctx)`.
|
||||||
|
|
||||||
### Troubleshooting Pre-Commit Staticcheck Failures
|
### Troubleshooting Lefthook Staticcheck Failures
|
||||||
|
|
||||||
**Common Issues:**
|
**Common Issues:**
|
||||||
|
|
||||||
@@ -175,7 +175,7 @@ Before marking an implementation task as complete, perform the following in orde
|
|||||||
- **Exclusions**: Skip this gate for docs-only (`**/*.md`) or frontend-only (`frontend/**`) changes
|
- **Exclusions**: Skip this gate for docs-only (`**/*.md`) or frontend-only (`frontend/**`) changes
|
||||||
- **Run One Of**:
|
- **Run One Of**:
|
||||||
- VS Code task: `Lint: GORM Security Scan`
|
- VS Code task: `Lint: GORM Security Scan`
|
||||||
- Pre-commit: `pre-commit run --hook-stage manual gorm-security-scan --all-files`
|
- Lefthook: `lefthook run pre-commit` (includes gorm-security-scan)
|
||||||
- Direct: `./scripts/scan-gorm-security.sh --check`
|
- Direct: `./scripts/scan-gorm-security.sh --check`
|
||||||
- **Gate Enforcement**: DoD is process-blocking until scanner reports zero
|
- **Gate Enforcement**: DoD is process-blocking until scanner reports zero
|
||||||
CRITICAL/HIGH findings, even while automation remains in manual stage
|
CRITICAL/HIGH findings, even while automation remains in manual stage
|
||||||
@@ -189,15 +189,15 @@ Before marking an implementation task as complete, perform the following in orde
|
|||||||
- **Expected Behavior**: Report may warn (non-blocking rollout), but artifact generation is mandatory.
|
- **Expected Behavior**: Report may warn (non-blocking rollout), but artifact generation is mandatory.
|
||||||
|
|
||||||
3. **Security Scans** (MANDATORY - Zero Tolerance):
|
3. **Security Scans** (MANDATORY - Zero Tolerance):
|
||||||
- **CodeQL Go Scan**: Run VS Code task "Security: CodeQL Go Scan (CI-Aligned)" OR `pre-commit run codeql-go-scan --all-files`
|
- **CodeQL Go Scan**: Run VS Code task "Security: CodeQL Go Scan (CI-Aligned)" OR `lefthook run pre-commit`
|
||||||
- Must use `security-and-quality` suite (CI-aligned)
|
- Must use `security-and-quality` suite (CI-aligned)
|
||||||
- **Zero high/critical (error-level) findings allowed**
|
- **Zero high/critical (error-level) findings allowed**
|
||||||
- Medium/low findings should be documented and triaged
|
- Medium/low findings should be documented and triaged
|
||||||
- **CodeQL JS Scan**: Run VS Code task "Security: CodeQL JS Scan (CI-Aligned)" OR `pre-commit run codeql-js-scan --all-files`
|
- **CodeQL JS Scan**: Run VS Code task "Security: CodeQL JS Scan (CI-Aligned)" OR `lefthook run pre-commit`
|
||||||
- Must use `security-and-quality` suite (CI-aligned)
|
- Must use `security-and-quality` suite (CI-aligned)
|
||||||
- **Zero high/critical (error-level) findings allowed**
|
- **Zero high/critical (error-level) findings allowed**
|
||||||
- Medium/low findings should be documented and triaged
|
- Medium/low findings should be documented and triaged
|
||||||
- **Validate Findings**: Run `pre-commit run codeql-check-findings --all-files` to check for HIGH/CRITICAL issues
|
- **Validate Findings**: Run `lefthook run pre-commit` to check for HIGH/CRITICAL issues
|
||||||
- **Trivy Container Scan**: Run VS Code task "Security: Trivy Scan" for container/dependency vulnerabilities
|
- **Trivy Container Scan**: Run VS Code task "Security: Trivy Scan" for container/dependency vulnerabilities
|
||||||
- **Results Viewing**:
|
- **Results Viewing**:
|
||||||
- Primary: VS Code SARIF Viewer extension (`MS-SarifVSCode.sarif-viewer`)
|
- Primary: VS Code SARIF Viewer extension (`MS-SarifVSCode.sarif-viewer`)
|
||||||
@@ -210,7 +210,7 @@ Before marking an implementation task as complete, perform the following in orde
|
|||||||
- Database creation: `--threads=0 --overwrite`
|
- Database creation: `--threads=0 --overwrite`
|
||||||
- Analysis: `--sarif-add-baseline-file-info`
|
- Analysis: `--sarif-add-baseline-file-info`
|
||||||
|
|
||||||
4. **Pre-Commit Triage**: Run `pre-commit run --all-files`.
|
4. **Lefthook Triage**: Run `lefthook run pre-commit`.
|
||||||
- If errors occur, **fix them immediately**.
|
- If errors occur, **fix them immediately**.
|
||||||
- If logic errors occur, analyze and propose a fix.
|
- If logic errors occur, analyze and propose a fix.
|
||||||
- Do not output code that violates pre-commit standards.
|
- Do not output code that violates pre-commit standards.
|
||||||
|
|||||||
2
.github/instructions/go.instructions.md
vendored
2
.github/instructions/go.instructions.md
vendored
@@ -353,7 +353,7 @@ Follow idiomatic Go practices and community standards when writing Go code. Thes
|
|||||||
### Development Practices
|
### Development Practices
|
||||||
|
|
||||||
- Run tests before committing
|
- Run tests before committing
|
||||||
- Use pre-commit hooks for formatting and linting
|
- Use lefthook pre-commit-phase hooks for formatting and linting
|
||||||
- Keep commits focused and atomic
|
- Keep commits focused and atomic
|
||||||
- Write meaningful commit messages
|
- Write meaningful commit messages
|
||||||
- Review diffs before committing
|
- Review diffs before committing
|
||||||
|
|||||||
204
.github/instructions/security.md.instructions.md
vendored
Normal file
204
.github/instructions/security.md.instructions.md
vendored
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
---
|
||||||
|
applyTo: SECURITY.md
|
||||||
|
---
|
||||||
|
|
||||||
|
# Instructions: Maintaining `SECURITY.md`
|
||||||
|
|
||||||
|
`SECURITY.md` is the project's living security record. It serves two audiences simultaneously: users who need to know what risks exist right now, and the broader community who need confidence that vulnerabilities are being tracked and remediated with discipline. Treat it like a changelog, but for security events — every known issue gets an entry, every resolved issue keeps its entry.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
`SECURITY.md` must always contain the following top-level sections, in this order:
|
||||||
|
|
||||||
|
1. A brief project security policy preamble (responsible disclosure contact, response SLA)
|
||||||
|
2. **`## Known Vulnerabilities`** — active, unpatched issues
|
||||||
|
3. **`## Patched Vulnerabilities`** — resolved issues, retained permanently for audit trail
|
||||||
|
|
||||||
|
No other top-level sections are required. Do not collapse or remove sections even when they are empty — use the explicit empty-state placeholder defined below.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 1: Known Vulnerabilities
|
||||||
|
|
||||||
|
This section lists every vulnerability that is currently unpatched or only partially mitigated. Entries must be sorted with the highest severity first, then by discovery date descending within the same severity tier.
|
||||||
|
|
||||||
|
### Entry Format
|
||||||
|
|
||||||
|
Each entry is an H3 heading followed by a structured block:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### [SEVERITY] CVE-XXXX-XXXXX · Short Title
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|--------------|-------|
|
||||||
|
| **ID** | CVE-XXXX-XXXXX (or `CHARON-YYYY-NNN` if no CVE assigned yet) |
|
||||||
|
| **Severity** | Critical / High / Medium / Low · CVSS v3.1 score if known (e.g. `8.1 · High`) |
|
||||||
|
| **Status** | Investigating / Fix In Progress / Awaiting Upstream / Mitigated (partial) |
|
||||||
|
|
||||||
|
**What**
|
||||||
|
One to three sentences describing the vulnerability class and its impact.
|
||||||
|
Be specific: name the weakness type (e.g. SQL injection, path traversal, SSRF).
|
||||||
|
|
||||||
|
**Who**
|
||||||
|
- Discovered by: [Reporter name or handle, or "Internal audit", or "Automated scan (tool name)"]
|
||||||
|
- Reported: YYYY-MM-DD
|
||||||
|
- Affects: [User roles, API consumers, unauthenticated users, etc.]
|
||||||
|
|
||||||
|
**Where**
|
||||||
|
- Component: [Module or service name]
|
||||||
|
- File(s): `path/to/affected/file.go`, `path/to/other/file.ts`
|
||||||
|
- Versions affected: `>= X.Y.Z` (or "all versions" / "prior to X.Y.Z")
|
||||||
|
|
||||||
|
**When**
|
||||||
|
- Discovered: YYYY-MM-DD
|
||||||
|
- Disclosed (if public): YYYY-MM-DD (or "Not yet publicly disclosed")
|
||||||
|
- Target fix: YYYY-MM-DD (or sprint/milestone reference)
|
||||||
|
|
||||||
|
**How**
|
||||||
|
A concise technical description of the attack vector, prerequisites, and exploitation
|
||||||
|
method. Omit proof-of-concept code. Reference CVE advisories or upstream issue
|
||||||
|
trackers where appropriate.
|
||||||
|
|
||||||
|
**Planned Remediation**
|
||||||
|
Describe the fix strategy: library upgrade, logic refactor, config change, etc.
|
||||||
|
If a workaround is available in the meantime, document it here.
|
||||||
|
Link to the tracking issue: [#NNN](https://github.com/owner/repo/issues/NNN)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty State
|
||||||
|
|
||||||
|
When there are no known vulnerabilities:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Known Vulnerabilities
|
||||||
|
|
||||||
|
No known unpatched vulnerabilities at this time.
|
||||||
|
Last reviewed: YYYY-MM-DD
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Section 2: Patched Vulnerabilities
|
||||||
|
|
||||||
|
This section is a permanent, append-only ledger. Entries are never deleted. Sort newest-patched first. This section builds community trust by demonstrating that issues are resolved promptly and transparently.
|
||||||
|
|
||||||
|
### Entry Format
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### ✅ [SEVERITY] CVE-XXXX-XXXXX · Short Title
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|--------------|-------|
|
||||||
|
| **ID** | CVE-XXXX-XXXXX (or internal ID) |
|
||||||
|
| **Severity** | Critical / High / Medium / Low · CVSS v3.1 score |
|
||||||
|
| **Patched** | YYYY-MM-DD in `vX.Y.Z` |
|
||||||
|
|
||||||
|
**What**
|
||||||
|
Same description carried over from the Known Vulnerabilities entry.
|
||||||
|
|
||||||
|
**Who**
|
||||||
|
- Discovered by: [Reporter or method]
|
||||||
|
- Reported: YYYY-MM-DD
|
||||||
|
|
||||||
|
**Where**
|
||||||
|
- Component: [Module or service name]
|
||||||
|
- File(s): `path/to/affected/file.go`
|
||||||
|
- Versions affected: `< X.Y.Z`
|
||||||
|
|
||||||
|
**When**
|
||||||
|
- Discovered: YYYY-MM-DD
|
||||||
|
- Patched: YYYY-MM-DD
|
||||||
|
- Time to patch: N days
|
||||||
|
|
||||||
|
**How**
|
||||||
|
Same technical description as the original entry.
|
||||||
|
|
||||||
|
**Resolution**
|
||||||
|
Describe exactly what was changed to fix the issue.
|
||||||
|
- Commit: [`abc1234`](https://github.com/owner/repo/commit/abc1234)
|
||||||
|
- PR: [#NNN](https://github.com/owner/repo/pull/NNN)
|
||||||
|
- Release: [`vX.Y.Z`](https://github.com/owner/repo/releases/tag/vX.Y.Z)
|
||||||
|
|
||||||
|
**Credit**
|
||||||
|
[Optional] Thank the reporter if they consented to attribution.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Empty State
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Patched Vulnerabilities
|
||||||
|
|
||||||
|
No patched vulnerabilities on record yet.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lifecycle: Moving an Entry from Known → Patched
|
||||||
|
|
||||||
|
When a fix ships:
|
||||||
|
|
||||||
|
1. Remove the entry from `## Known Vulnerabilities` entirely.
|
||||||
|
2. Add a new entry to the **top** of `## Patched Vulnerabilities` using the patched format above.
|
||||||
|
3. Carry forward all original fields verbatim — do not rewrite the history of the issue.
|
||||||
|
4. Add the `**Resolution**` and `**Credit**` blocks with patch details.
|
||||||
|
5. Update the `Last reviewed` date on the Known Vulnerabilities section if it is now empty.
|
||||||
|
|
||||||
|
Do not edit or backfill existing Patched entries once they are committed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Severity Classification
|
||||||
|
|
||||||
|
Use the following definitions consistently:
|
||||||
|
|
||||||
|
| Severity | CVSS Range | Meaning |
|
||||||
|
|----------|------------|---------|
|
||||||
|
| **Critical** | 9.0–10.0 | Remote code execution, auth bypass, full data exposure |
|
||||||
|
| **High** | 7.0–8.9 | Significant data exposure, privilege escalation, DoS |
|
||||||
|
| **Medium** | 4.0–6.9 | Limited data exposure, requires user interaction or auth |
|
||||||
|
| **Low** | 0.1–3.9 | Minimal impact, difficult to exploit, defense-in-depth |
|
||||||
|
|
||||||
|
When a CVE CVSS score is not yet available, assign a preliminary severity based on these definitions and note it as `(preliminary)` until confirmed.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Internal IDs
|
||||||
|
|
||||||
|
If a vulnerability has no CVE assigned, use the format `CHARON-YYYY-NNN` where `YYYY` is the year and `NNN` is a zero-padded sequence number starting at `001` for each year. Example: `CHARON-2025-003`. Assign a CVE ID in the entry retroactively if one is issued later, and add the internal ID as an alias in parentheses.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Responsible Disclosure Preamble
|
||||||
|
|
||||||
|
The preamble at the top of `SECURITY.md` (before the vulnerability sections) must include:
|
||||||
|
|
||||||
|
- The preferred contact method for reporting vulnerabilities (e.g. a GitHub private advisory link, a security email address, or both)
|
||||||
|
- An acknowledgment-first response commitment: confirm receipt within 48 hours, even if the full investigation takes longer
|
||||||
|
- A statement that reporters will not be penalized or publicly named without consent
|
||||||
|
- A link to the full disclosure policy if one exists
|
||||||
|
|
||||||
|
Example:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
|
To report a security issue, please use
|
||||||
|
[GitHub Private Security Advisories](https://github.com/owner/repo/security/advisories/new)
|
||||||
|
or email `security@example.com`.
|
||||||
|
|
||||||
|
We will acknowledge your report within **48 hours** and provide a remediation
|
||||||
|
timeline within **7 days**. Reporters are credited with their consent.
|
||||||
|
We do not pursue legal action against good-faith security researchers.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Maintenance Rules
|
||||||
|
|
||||||
|
- **Review cadence**: Update the `Last reviewed` date in the Known Vulnerabilities section at least once per release cycle, even if no entries changed.
|
||||||
|
- **No silent patches**: Every security fix — no matter how minor — must produce an entry in `## Patched Vulnerabilities` before or alongside the release.
|
||||||
|
- **No redaction**: Do not redact or soften historical entries. Accuracy builds trust; minimizing past issues destroys it.
|
||||||
|
- **Dependency vulnerabilities**: Transitive dependency CVEs that affect Charon's exposed attack surface must be tracked here the same as first-party vulnerabilities. Pure dev-dependency CVEs with no runtime impact may be omitted at maintainer discretion, but must still be noted in the relevant dependency update PR.
|
||||||
|
- **Partial mitigations**: If a workaround is deployed but the root cause is not fixed, the entry stays in `## Known Vulnerabilities` with `Status: Mitigated (partial)` and the workaround documented in `**Planned Remediation**`.
|
||||||
@@ -9,7 +9,7 @@ description: 'Repository structure guidelines to maintain organized file placeme
|
|||||||
|
|
||||||
The repository root should contain ONLY:
|
The repository root should contain ONLY:
|
||||||
|
|
||||||
- Essential config files (`.gitignore`, `.pre-commit-config.yaml`, `Makefile`, etc.)
|
- Essential config files (`.gitignore`, `Makefile`, etc.)
|
||||||
- Standard project files (`README.md`, `CONTRIBUTING.md`, `LICENSE`, `CHANGELOG.md`)
|
- Standard project files (`README.md`, `CONTRIBUTING.md`, `LICENSE`, `CHANGELOG.md`)
|
||||||
- Go workspace files (`go.work`, `go.work.sum`)
|
- Go workspace files (`go.work`, `go.work.sum`)
|
||||||
- VS Code workspace (`Chiron.code-workspace`)
|
- VS Code workspace (`Chiron.code-workspace`)
|
||||||
|
|||||||
20
.github/instructions/subagent.instructions.md
vendored
20
.github/instructions/subagent.instructions.md
vendored
@@ -23,21 +23,21 @@ runSubagent({
|
|||||||
|
|
||||||
- Validate: `plan_file` exists and contains a `Handoff Contract` JSON.
|
- Validate: `plan_file` exists and contains a `Handoff Contract` JSON.
|
||||||
- Kickoff: call `Planning` to create the plan if not present.
|
- Kickoff: call `Planning` to create the plan if not present.
|
||||||
- Decide: check if work should be split into multiple PRs (size, risk, cross-domain impact).
|
- Decide: check how to organize work into logical commits within a single PR (size, risk, cross-domain impact).
|
||||||
- Run: execute `Backend Dev` then `Frontend Dev` sequentially.
|
- Run: execute `Backend Dev` then `Frontend Dev` sequentially.
|
||||||
- Parallel: run `QA and Security`, `DevOps` and `Doc Writer` in parallel for CI / QA checks and documentation.
|
- Parallel: run `QA and Security`, `DevOps` and `Doc Writer` in parallel for CI / QA checks and documentation.
|
||||||
- Return: a JSON summary with `subagent_results`, `overall_status`, and aggregated artifacts.
|
- Return: a JSON summary with `subagent_results`, `overall_status`, and aggregated artifacts.
|
||||||
|
|
||||||
2.1) Multi-PR Slicing Protocol
|
2.1) Multi-Commit Slicing Protocol
|
||||||
|
|
||||||
- If a task is large or high-risk, split into PR slices and execute in order.
|
- All work for a single feature ships as one PR with ordered logical commits.
|
||||||
- Each slice must have:
|
- Each commit must have:
|
||||||
- Scope boundary (what is included/excluded)
|
- Scope boundary (what is included/excluded)
|
||||||
- Dependency on previous slices
|
- Dependency on previous commits
|
||||||
- Validation gates (tests/scans required for that slice)
|
- Validation gates (tests/scans required for that commit)
|
||||||
- Explicit rollback notes
|
- Explicit rollback notes for the PR as a whole
|
||||||
- Do not start the next slice until the current slice is complete and verified.
|
- Do not start the next commit until the current commit is complete and verified.
|
||||||
- Keep each slice independently reviewable and deployable.
|
- Keep each commit independently reviewable within the PR.
|
||||||
|
|
||||||
3) Return Contract that all subagents must return
|
3) Return Contract that all subagents must return
|
||||||
|
|
||||||
@@ -55,7 +55,7 @@ runSubagent({
|
|||||||
|
|
||||||
- On a subagent failure, the Management agent must capture `tests.output` and decide to retry (1 retry maximum), or request a revert/rollback.
|
- On a subagent failure, the Management agent must capture `tests.output` and decide to retry (1 retry maximum), or request a revert/rollback.
|
||||||
- Clearly mark the `status` as `failed`, and include `errors` and `failing_tests` in the `summary`.
|
- Clearly mark the `status` as `failed`, and include `errors` and `failing_tests` in the `summary`.
|
||||||
- For multi-PR execution, mark failed slice as blocked and stop downstream slices until resolved.
|
- For multi-commit execution, mark failed commit as blocked and stop downstream commits until resolved.
|
||||||
|
|
||||||
5) Example: Run a full Feature Implementation
|
5) Example: Run a full Feature Implementation
|
||||||
|
|
||||||
|
|||||||
21
.github/instructions/testing.instructions.md
vendored
21
.github/instructions/testing.instructions.md
vendored
@@ -12,9 +12,19 @@ instruction files take precedence over agent files and operator documentation.
|
|||||||
|
|
||||||
**MANDATORY**: Before running unit tests, verify the application UI/UX functions correctly end-to-end.
|
**MANDATORY**: Before running unit tests, verify the application UI/UX functions correctly end-to-end.
|
||||||
|
|
||||||
## 0.5 Local Patch Coverage Preflight (Before Unit Tests)
|
## 0.5 Local Patch Coverage Report (After Coverage Tests)
|
||||||
|
|
||||||
**MANDATORY**: After E2E and before backend/frontend unit coverage runs, generate a local patch report so uncovered changed lines are visible early.
|
**MANDATORY**: After running backend and frontend coverage tests (which generate
|
||||||
|
`backend/coverage.txt` and `frontend/coverage/lcov.info`), run the local patch
|
||||||
|
report to identify uncovered lines in changed files.
|
||||||
|
|
||||||
|
**Purpose**: Overall coverage can be healthy while the specific lines you changed
|
||||||
|
are untested. This step catches that gap. If uncovered lines are found in
|
||||||
|
feature code, add targeted tests before completing the task.
|
||||||
|
|
||||||
|
**Prerequisites**: Coverage artifacts must exist before running the report:
|
||||||
|
- `backend/coverage.txt` — generated by `scripts/go-test-coverage.sh`
|
||||||
|
- `frontend/coverage/lcov.info` — generated by `scripts/frontend-test-coverage.sh`
|
||||||
|
|
||||||
Run one of the following from `/projects/Charon`:
|
Run one of the following from `/projects/Charon`:
|
||||||
|
|
||||||
@@ -26,11 +36,14 @@ Test: Local Patch Report
|
|||||||
bash scripts/local-patch-report.sh
|
bash scripts/local-patch-report.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Required artifacts:
|
Required output artifacts:
|
||||||
- `test-results/local-patch-report.md`
|
- `test-results/local-patch-report.md`
|
||||||
- `test-results/local-patch-report.json`
|
- `test-results/local-patch-report.json`
|
||||||
|
|
||||||
This preflight is advisory for thresholds during rollout, but artifact generation is required in DoD.
|
**Action on results**: If patch coverage for any changed file is below 90%, add
|
||||||
|
tests targeting the uncovered changed lines. Re-run coverage and this report to
|
||||||
|
verify improvement. Artifact generation is required for DoD regardless of
|
||||||
|
threshold results.
|
||||||
|
|
||||||
### PREREQUISITE: Start E2E Environment
|
### PREREQUISITE: Start E2E Environment
|
||||||
|
|
||||||
|
|||||||
170
.github/renovate.json
vendored
170
.github/renovate.json
vendored
@@ -6,11 +6,11 @@
|
|||||||
":separateMultipleMajorReleases",
|
":separateMultipleMajorReleases",
|
||||||
"helpers:pinGitHubActionDigests"
|
"helpers:pinGitHubActionDigests"
|
||||||
],
|
],
|
||||||
"baseBranches": [
|
"baseBranchPatterns": [
|
||||||
"feature/beta-release",
|
"feature/beta-release",
|
||||||
"development"
|
"development"
|
||||||
|
|
||||||
],
|
],
|
||||||
|
"postUpdateOptions": ["npmDedupe"],
|
||||||
"timezone": "America/New_York",
|
"timezone": "America/New_York",
|
||||||
"dependencyDashboard": true,
|
"dependencyDashboard": true,
|
||||||
"dependencyDashboardApproval": true,
|
"dependencyDashboardApproval": true,
|
||||||
@@ -27,7 +27,10 @@
|
|||||||
"rebaseWhen": "auto",
|
"rebaseWhen": "auto",
|
||||||
|
|
||||||
"vulnerabilityAlerts": {
|
"vulnerabilityAlerts": {
|
||||||
"enabled": true
|
"enabled": true,
|
||||||
|
"dependencyDashboardApproval": false,
|
||||||
|
"automerge": false,
|
||||||
|
"labels": ["security", "vulnerability"]
|
||||||
},
|
},
|
||||||
|
|
||||||
"rangeStrategy": "bump",
|
"rangeStrategy": "bump",
|
||||||
@@ -36,6 +39,19 @@
|
|||||||
"platformAutomerge": true,
|
"platformAutomerge": true,
|
||||||
|
|
||||||
"customManagers": [
|
"customManagers": [
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track caddy-security plugin version in Dockerfile",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^Dockerfile$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"ARG CADDY_SECURITY_VERSION=(?<currentValue>[^\\s]+)"
|
||||||
|
],
|
||||||
|
"depNameTemplate": "github.com/greenpau/caddy-security",
|
||||||
|
"datasourceTemplate": "go",
|
||||||
|
"versioningTemplate": "semver"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"customType": "regex",
|
"customType": "regex",
|
||||||
"description": "Track Go dependencies patched in Dockerfile for Caddy CVE fixes",
|
"description": "Track Go dependencies patched in Dockerfile for Caddy CVE fixes",
|
||||||
@@ -53,12 +69,45 @@
|
|||||||
"description": "Track Alpine base image digest in Dockerfile for security updates",
|
"description": "Track Alpine base image digest in Dockerfile for security updates",
|
||||||
"managerFilePatterns": ["/^Dockerfile$/"],
|
"managerFilePatterns": ["/^Dockerfile$/"],
|
||||||
"matchStrings": [
|
"matchStrings": [
|
||||||
"#\\s*renovate:\\s*datasource=docker\\s+depName=alpine.*\\nARG CADDY_IMAGE=alpine:(?<currentValue>[^\\s@]+@sha256:[a-f0-9]+)"
|
"#\\s*renovate:\\s*datasource=docker\\s+depName=alpine.*\\nARG ALPINE_IMAGE=alpine:(?<currentValue>[^@\\s]+)@(?<currentDigest>sha256:[a-f0-9]+)"
|
||||||
],
|
],
|
||||||
"depNameTemplate": "alpine",
|
"depNameTemplate": "alpine",
|
||||||
"datasourceTemplate": "docker",
|
"datasourceTemplate": "docker",
|
||||||
"versioningTemplate": "docker"
|
"versioningTemplate": "docker"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track Go toolchain version ARG in Dockerfile",
|
||||||
|
"managerFilePatterns": ["/^Dockerfile$/"],
|
||||||
|
"matchStrings": [
|
||||||
|
"#\\s*renovate:\\s*datasource=docker\\s+depName=golang.*\\nARG GO_VERSION=(?<currentValue>[^\\s]+)"
|
||||||
|
],
|
||||||
|
"depNameTemplate": "golang",
|
||||||
|
"datasourceTemplate": "docker",
|
||||||
|
"versioningTemplate": "docker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track expr-lang version ARG in Dockerfile",
|
||||||
|
"managerFilePatterns": ["/^Dockerfile$/"],
|
||||||
|
"matchStrings": [
|
||||||
|
"#\\s*renovate:\\s*datasource=go\\s+depName=github\\.com/expr-lang/expr.*\\nARG EXPR_LANG_VERSION=(?<currentValue>[^\\s]+)"
|
||||||
|
],
|
||||||
|
"depNameTemplate": "github.com/expr-lang/expr",
|
||||||
|
"datasourceTemplate": "go",
|
||||||
|
"versioningTemplate": "semver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track golang.org/x/net version ARG in Dockerfile",
|
||||||
|
"managerFilePatterns": ["/^Dockerfile$/"],
|
||||||
|
"matchStrings": [
|
||||||
|
"#\\s*renovate:\\s*datasource=go\\s+depName=golang\\.org/x/net.*\\nARG XNET_VERSION=(?<currentValue>[^\\s]+)"
|
||||||
|
],
|
||||||
|
"depNameTemplate": "golang.org/x/net",
|
||||||
|
"datasourceTemplate": "go",
|
||||||
|
"versioningTemplate": "semver"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"customType": "regex",
|
"customType": "regex",
|
||||||
"description": "Track Delve version in Dockerfile",
|
"description": "Track Delve version in Dockerfile",
|
||||||
@@ -81,6 +130,32 @@
|
|||||||
"datasourceTemplate": "go",
|
"datasourceTemplate": "go",
|
||||||
"versioningTemplate": "semver"
|
"versioningTemplate": "semver"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track gotestsum version in codecov workflow",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.github/workflows/codecov-upload\\.yml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"gotestsum@v(?<currentValue>[^\\s]+)"
|
||||||
|
],
|
||||||
|
"depNameTemplate": "gotest.tools/gotestsum",
|
||||||
|
"datasourceTemplate": "go",
|
||||||
|
"versioningTemplate": "semver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track gotestsum version in quality checks workflow",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.github/workflows/quality-checks\\.yml$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"gotestsum@v(?<currentValue>[^\\s]+)"
|
||||||
|
],
|
||||||
|
"depNameTemplate": "gotest.tools/gotestsum",
|
||||||
|
"datasourceTemplate": "go",
|
||||||
|
"versioningTemplate": "semver"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"customType": "regex",
|
"customType": "regex",
|
||||||
"description": "Track govulncheck version in scripts",
|
"description": "Track govulncheck version in scripts",
|
||||||
@@ -117,27 +192,78 @@
|
|||||||
{
|
{
|
||||||
"customType": "regex",
|
"customType": "regex",
|
||||||
"description": "Track GO_VERSION in Actions workflows",
|
"description": "Track GO_VERSION in Actions workflows",
|
||||||
"fileMatch": ["^\\.github/workflows/.*\\.yml$"],
|
"managerFilePatterns": ["/^\\.github/workflows/.*\\.yml$/"],
|
||||||
"matchStrings": [
|
"matchStrings": [
|
||||||
"GO_VERSION: ['\"]?(?<currentValue>[\\d\\.]+)['\"]?"
|
"GO_VERSION: ['\"]?(?<currentValue>[\\d\\.]+)['\"]?"
|
||||||
],
|
],
|
||||||
"depNameTemplate": "golang/go",
|
"depNameTemplate": "golang/go",
|
||||||
"datasourceTemplate": "golang-version",
|
"datasourceTemplate": "golang-version",
|
||||||
"versioningTemplate": "semver"
|
"versioningTemplate": "semver"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track Syft version in workflows and scripts",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.github/workflows/nightly-build\\.yml$/",
|
||||||
|
"/^\\.github/skills/security-scan-docker-image-scripts/run\\.sh$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"SYFT_VERSION=\\\"v(?<currentValue>[^\\\"\\s]+)\\\"",
|
||||||
|
"set_default_env \\\"SYFT_VERSION\\\" \\\"v(?<currentValue>[^\\\"]+)\\\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "anchore/syft",
|
||||||
|
"datasourceTemplate": "github-releases",
|
||||||
|
"versioningTemplate": "semver",
|
||||||
|
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track Grype version in workflows and scripts",
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.github/workflows/supply-chain-pr\\.yml$/",
|
||||||
|
"/^\\.github/skills/security-scan-docker-image-scripts/run\\.sh$/"
|
||||||
|
],
|
||||||
|
"matchStrings": [
|
||||||
|
"anchore/grype/main/install\\.sh \\| sh -s -- -b /usr/local/bin v(?<currentValue>[0-9]+\\.[0-9]+\\.[0-9]+)",
|
||||||
|
"set_default_env \\\"GRYPE_VERSION\\\" \\\"v(?<currentValue>[^\\\"]+)\\\""
|
||||||
|
],
|
||||||
|
"depNameTemplate": "anchore/grype",
|
||||||
|
"datasourceTemplate": "github-releases",
|
||||||
|
"versioningTemplate": "semver",
|
||||||
|
"extractVersionTemplate": "^v(?<version>.*)$"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"customType": "regex",
|
||||||
|
"description": "Track go-version in skill example workflows",
|
||||||
|
"managerFilePatterns": ["/^\\.github/skills/examples/.*\\.yml$/"],
|
||||||
|
"matchStrings": [
|
||||||
|
"go-version: [\"']?(?<currentValue>[\\d\\.]+)[\"']?"
|
||||||
|
],
|
||||||
|
"depNameTemplate": "golang/go",
|
||||||
|
"datasourceTemplate": "golang-version",
|
||||||
|
"versioningTemplate": "semver"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|
||||||
|
"github-actions": {
|
||||||
|
"managerFilePatterns": [
|
||||||
|
"/^\\.github/skills/examples/.*\\.ya?ml$/"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
"packageRules": [
|
"packageRules": [
|
||||||
{
|
{
|
||||||
"description": "THE MEGAZORD: Group ALL non-major updates (NPM, Docker, Go, Actions) into one PR",
|
"description": "THE MEGAZORD: Group ALL non-major updates (NPM, Docker, Go, Actions) into one PR",
|
||||||
"matchPackagePatterns": ["*"],
|
|
||||||
"matchUpdateTypes": [
|
"matchUpdateTypes": [
|
||||||
"minor",
|
"minor",
|
||||||
"patch",
|
"patch",
|
||||||
"pin",
|
"pin",
|
||||||
"digest"
|
"digest"
|
||||||
],
|
],
|
||||||
"groupName": "non-major-updates"
|
"groupName": "non-major-updates",
|
||||||
|
"matchPackageNames": [
|
||||||
|
"*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"description": "Feature branches: Auto-merge non-major updates after proven stable",
|
"description": "Feature branches: Auto-merge non-major updates after proven stable",
|
||||||
@@ -169,11 +295,41 @@
|
|||||||
"matchPackageNames": ["caddy"],
|
"matchPackageNames": ["caddy"],
|
||||||
"allowedVersions": "<3.0.0"
|
"allowedVersions": "<3.0.0"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Go: keep pgx within v4 (CrowdSec requires pgx/v4 module path)",
|
||||||
|
"matchDatasources": ["go"],
|
||||||
|
"matchPackageNames": ["github.com/jackc/pgx/v4"],
|
||||||
|
"allowedVersions": "<5.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Go: keep go-jose/v3 within v3 (v4 is a different Go module path)",
|
||||||
|
"matchDatasources": ["go"],
|
||||||
|
"matchPackageNames": ["github.com/go-jose/go-jose/v3"],
|
||||||
|
"allowedVersions": "<4.0.0"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Go: keep go-jose/v4 within v4 (v5 would be a different Go module path)",
|
||||||
|
"matchDatasources": ["go"],
|
||||||
|
"matchPackageNames": ["github.com/go-jose/go-jose/v4"],
|
||||||
|
"allowedVersions": "<5.0.0"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Safety: Keep MAJOR updates separate and require manual review",
|
"description": "Safety: Keep MAJOR updates separate and require manual review",
|
||||||
"matchUpdateTypes": ["major"],
|
"matchUpdateTypes": ["major"],
|
||||||
"automerge": false,
|
"automerge": false,
|
||||||
"labels": ["manual-review"]
|
"labels": ["manual-review"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fix Renovate lookup for geoip2-golang v2 module path",
|
||||||
|
"matchDatasources": ["go"],
|
||||||
|
"matchPackageNames": ["github.com/oschwald/geoip2-golang/v2"],
|
||||||
|
"sourceUrl": "https://github.com/oschwald/geoip2-golang"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Fix Renovate lookup for google/uuid",
|
||||||
|
"matchDatasources": ["go"],
|
||||||
|
"matchPackageNames": ["github.com/google/uuid"],
|
||||||
|
"sourceUrl": "https://github.com/google/uuid"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
2
.github/skills/README.md
vendored
2
.github/skills/README.md
vendored
@@ -63,7 +63,7 @@ Agent Skills are self-documenting, AI-discoverable task definitions that combine
|
|||||||
|
|
||||||
| Skill Name | Category | Description | Status |
|
| Skill Name | Category | Description | Status |
|
||||||
|------------|----------|-------------|--------|
|
|------------|----------|-------------|--------|
|
||||||
| [qa-precommit-all](./qa-precommit-all.SKILL.md) | qa | Run all pre-commit hooks on entire codebase | ✅ Active |
|
| [qa-lefthook-all](./qa-lefthook-all.SKILL.md) | qa | Run all lefthook pre-commit‑phase hooks on entire codebase | ✅ Active |
|
||||||
|
|
||||||
### Utility Skills
|
### Utility Skills
|
||||||
|
|
||||||
|
|||||||
@@ -20,12 +20,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: '1.23'
|
go-version: "1.26.2"
|
||||||
|
|
||||||
- name: Run GORM Security Scanner
|
- name: Run GORM Security Scanner
|
||||||
id: gorm-scan
|
id: gorm-scan
|
||||||
@@ -56,7 +56,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Comment on PR
|
- name: Comment on PR
|
||||||
if: always() && github.event_name == 'pull_request'
|
if: always() && github.event_name == 'pull_request'
|
||||||
uses: actions/github-script@v7
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const critical = ${{ steps.parse-report.outputs.critical }};
|
const critical = ${{ steps.parse-report.outputs.critical }};
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload GORM Scan Report
|
- name: Upload GORM Scan Report
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: gorm-security-report-${{ github.run_id }}
|
name: gorm-security-report-${{ github.run_id }}
|
||||||
path: docs/reports/gorm-scan-ci-*.txt
|
path: docs/reports/gorm-scan-ci-*.txt
|
||||||
|
|||||||
349
.github/skills/qa-lefthook-all.SKILL.md
vendored
Normal file
349
.github/skills/qa-lefthook-all.SKILL.md
vendored
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
---
|
||||||
|
# agentskills.io specification v1.0
|
||||||
|
name: "qa-lefthook-all"
|
||||||
|
version: "1.0.0"
|
||||||
|
description: "Run all lefthook pre-commit-phase hooks for comprehensive code quality validation"
|
||||||
|
author: "Charon Project"
|
||||||
|
license: "MIT"
|
||||||
|
tags:
|
||||||
|
- "qa"
|
||||||
|
- "quality"
|
||||||
|
- "pre-commit"
|
||||||
|
- "linting"
|
||||||
|
- "validation"
|
||||||
|
compatibility:
|
||||||
|
os:
|
||||||
|
- "linux"
|
||||||
|
- "darwin"
|
||||||
|
shells:
|
||||||
|
- "bash"
|
||||||
|
requirements:
|
||||||
|
- name: "python3"
|
||||||
|
version: ">=3.8"
|
||||||
|
optional: false
|
||||||
|
- name: "lefthook"
|
||||||
|
version: ">=0.14"
|
||||||
|
optional: false
|
||||||
|
environment_variables:
|
||||||
|
- name: "SKIP"
|
||||||
|
description: "Comma-separated list of hook IDs to skip"
|
||||||
|
default: ""
|
||||||
|
required: false
|
||||||
|
parameters:
|
||||||
|
- name: "files"
|
||||||
|
type: "string"
|
||||||
|
description: "Specific files to check (default: all staged files)"
|
||||||
|
default: "--all-files"
|
||||||
|
required: false
|
||||||
|
outputs:
|
||||||
|
- name: "validation_report"
|
||||||
|
type: "stdout"
|
||||||
|
description: "Results of all pre-commit hook executions"
|
||||||
|
- name: "exit_code"
|
||||||
|
type: "number"
|
||||||
|
description: "0 if all hooks pass, non-zero if any fail"
|
||||||
|
metadata:
|
||||||
|
category: "qa"
|
||||||
|
subcategory: "quality"
|
||||||
|
execution_time: "medium"
|
||||||
|
risk_level: "low"
|
||||||
|
ci_cd_safe: true
|
||||||
|
requires_network: false
|
||||||
|
idempotent: true
|
||||||
|
---
|
||||||
|
|
||||||
|
# QA Pre-commit All
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Executes all configured lefthook pre-commit-phase hooks to validate code quality, formatting, security, and best practices across the entire codebase. This skill runs checks for Python, Go, JavaScript/TypeScript, Markdown, YAML, and more.
|
||||||
|
|
||||||
|
This skill is designed for CI/CD pipelines and local quality validation before committing code.
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Python 3.8 or higher installed and in PATH
|
||||||
|
- Python virtual environment activated (`.venv`)
|
||||||
|
- Pre-commit installed in virtual environment: `pip install pre-commit`
|
||||||
|
- Pre-commit hooks installed: `pre-commit install`
|
||||||
|
- All language-specific tools installed (Go, Node.js, etc.)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Basic Usage
|
||||||
|
|
||||||
|
Run all pre-commit-phase hooks on all files:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/charon
|
||||||
|
lefthook run pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
### Staged Files Only
|
||||||
|
|
||||||
|
Run lefthook on staged files only (faster):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lefthook run pre-commit --staged
|
||||||
|
```
|
||||||
|
|
||||||
|
### Specific Hook
|
||||||
|
|
||||||
|
Run only a specific hook by ID:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lefthook run pre-commit --hooks=trailing-whitespace
|
||||||
|
```
|
||||||
|
|
||||||
|
### Skip Specific Hooks
|
||||||
|
|
||||||
|
Skip certain hooks during execution:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SKIP=prettier,eslint .github/skills/scripts/skill-runner.sh qa-precommit-all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Required | Default | Description |
|
||||||
|
|-----------|------|----------|---------|-------------|
|
||||||
|
| files | string | No | --all-files | File selection mode (--all-files or staged) |
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
| Variable | Required | Default | Description |
|
||||||
|
|----------|----------|---------|-------------|
|
||||||
|
| SKIP | No | "" | Comma-separated hook IDs to skip |
|
||||||
|
| PRE_COMMIT_HOME | No | ~/.cache/pre-commit | Pre-commit cache directory |
|
||||||
|
|
||||||
|
## Outputs
|
||||||
|
|
||||||
|
- **Success Exit Code**: 0 (all hooks passed)
|
||||||
|
- **Error Exit Codes**: Non-zero (one or more hooks failed)
|
||||||
|
- **Output**: Detailed results from each hook
|
||||||
|
|
||||||
|
## Pre-commit Hooks Included
|
||||||
|
|
||||||
|
The following hooks are configured in `.pre-commit-config.yaml`:
|
||||||
|
|
||||||
|
### General Hooks
|
||||||
|
- **trailing-whitespace**: Remove trailing whitespace
|
||||||
|
- **end-of-file-fixer**: Ensure files end with newline
|
||||||
|
- **check-yaml**: Validate YAML syntax
|
||||||
|
- **check-json**: Validate JSON syntax
|
||||||
|
- **check-merge-conflict**: Detect merge conflict markers
|
||||||
|
- **check-added-large-files**: Prevent committing large files
|
||||||
|
|
||||||
|
### Python Hooks
|
||||||
|
- **black**: Code formatting
|
||||||
|
- **isort**: Import sorting
|
||||||
|
- **flake8**: Linting
|
||||||
|
- **mypy**: Type checking
|
||||||
|
|
||||||
|
### Go Hooks
|
||||||
|
- **gofmt**: Code formatting
|
||||||
|
- **go-vet**: Static analysis
|
||||||
|
- **golangci-lint**: Comprehensive linting
|
||||||
|
|
||||||
|
### JavaScript/TypeScript Hooks
|
||||||
|
- **prettier**: Code formatting
|
||||||
|
- **eslint**: Linting and code quality
|
||||||
|
|
||||||
|
### Markdown Hooks
|
||||||
|
- **markdownlint**: Markdown linting and formatting
|
||||||
|
|
||||||
|
### Security Hooks
|
||||||
|
- **detect-private-key**: Prevent committing private keys
|
||||||
|
- **detect-aws-credentials**: Prevent committing AWS credentials
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Example 1: Full Quality Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run all hooks on all files
|
||||||
|
source .venv/bin/activate
|
||||||
|
.github/skills/scripts/skill-runner.sh qa-precommit-all
|
||||||
|
```
|
||||||
|
|
||||||
|
Output:
|
||||||
|
```
|
||||||
|
Trim Trailing Whitespace.....................................Passed
|
||||||
|
Fix End of Files.............................................Passed
|
||||||
|
Check Yaml...................................................Passed
|
||||||
|
Check JSON...................................................Passed
|
||||||
|
Check for merge conflicts....................................Passed
|
||||||
|
Check for added large files..................................Passed
|
||||||
|
black........................................................Passed
|
||||||
|
isort........................................................Passed
|
||||||
|
prettier.....................................................Passed
|
||||||
|
eslint.......................................................Passed
|
||||||
|
markdownlint.................................................Passed
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Quick Staged Files Check
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Run only on staged files (faster for pre-commit)
|
||||||
|
.github/skills/scripts/skill-runner.sh qa-precommit-all staged
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Skip Slow Hooks
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Skip time-consuming hooks for quick validation
|
||||||
|
SKIP=golangci-lint,mypy .github/skills/scripts/skill-runner.sh qa-precommit-all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: CI/CD Pipeline Integration
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# GitHub Actions example
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: '3.11'
|
||||||
|
|
||||||
|
- name: Install pre-commit
|
||||||
|
run: pip install pre-commit
|
||||||
|
|
||||||
|
- name: Run QA Pre-commit Checks
|
||||||
|
run: .github/skills/scripts/skill-runner.sh qa-precommit-all
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 5: Auto-fix Mode
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Some hooks can auto-fix issues
|
||||||
|
# Run twice: first to fix, second to validate
|
||||||
|
.github/skills/scripts/skill-runner.sh qa-precommit-all || \
|
||||||
|
.github/skills/scripts/skill-runner.sh qa-precommit-all
|
||||||
|
```
|
||||||
|
|
||||||
|
## Error Handling
|
||||||
|
|
||||||
|
### Common Issues
|
||||||
|
|
||||||
|
**Virtual environment not activated**:
|
||||||
|
```bash
|
||||||
|
Error: pre-commit not found
|
||||||
|
Solution: source .venv/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
**Pre-commit not installed**:
|
||||||
|
```bash
|
||||||
|
Error: pre-commit command not available
|
||||||
|
Solution: pip install pre-commit
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hooks not installed**:
|
||||||
|
```bash
|
||||||
|
Error: Run 'pre-commit install'
|
||||||
|
Solution: pre-commit install
|
||||||
|
```
|
||||||
|
|
||||||
|
**Hook execution failed**:
|
||||||
|
```bash
|
||||||
|
Hook X failed
|
||||||
|
Solution: Review error output and fix reported issues
|
||||||
|
```
|
||||||
|
|
||||||
|
**Language tool missing**:
|
||||||
|
```bash
|
||||||
|
Error: golangci-lint not found
|
||||||
|
Solution: Install required language tools
|
||||||
|
```
|
||||||
|
|
||||||
|
## Exit Codes
|
||||||
|
|
||||||
|
- **0**: All hooks passed
|
||||||
|
- **1**: One or more hooks failed
|
||||||
|
- **Other**: Hook execution error
|
||||||
|
|
||||||
|
## Hook Fixing Strategies
|
||||||
|
|
||||||
|
### Auto-fixable Issues
|
||||||
|
These hooks automatically fix issues:
|
||||||
|
- `trailing-whitespace`
|
||||||
|
- `end-of-file-fixer`
|
||||||
|
- `black`
|
||||||
|
- `isort`
|
||||||
|
- `prettier`
|
||||||
|
- `gofmt`
|
||||||
|
|
||||||
|
**Workflow**: Run pre-commit, review changes, commit fixed files
|
||||||
|
|
||||||
|
### Manual Fixes Required
|
||||||
|
These hooks only report issues:
|
||||||
|
- `check-yaml`
|
||||||
|
- `check-json`
|
||||||
|
- `flake8`
|
||||||
|
- `eslint`
|
||||||
|
- `markdownlint`
|
||||||
|
- `go-vet`
|
||||||
|
- `golangci-lint`
|
||||||
|
|
||||||
|
**Workflow**: Review errors, manually fix code, re-run pre-commit
|
||||||
|
|
||||||
|
## Related Skills
|
||||||
|
|
||||||
|
- [test-backend-coverage](./test-backend-coverage.SKILL.md) - Backend test coverage
|
||||||
|
- [test-frontend-coverage](./test-frontend-coverage.SKILL.md) - Frontend test coverage
|
||||||
|
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Security scanning
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Pre-commit hooks cache their environments for faster execution
|
||||||
|
- First run may be slow while environments are set up
|
||||||
|
- Subsequent runs are much faster (seconds vs minutes)
|
||||||
|
- Hooks run in parallel where possible
|
||||||
|
- Failed hooks stop execution (fail-fast behavior)
|
||||||
|
- Use `SKIP` to bypass specific hooks temporarily
|
||||||
|
- Recommended to run before every commit
|
||||||
|
- Can be integrated into Git pre-commit hook for automatic checks
|
||||||
|
- Cache location: `~/.cache/pre-commit` (configurable)
|
||||||
|
|
||||||
|
## Performance Tips
|
||||||
|
|
||||||
|
- **Initial Setup**: First run takes longer (installing hook environments)
|
||||||
|
- **Incremental**: Run on staged files only for faster feedback
|
||||||
|
- **Parallel**: Pre-commit runs compatible hooks in parallel
|
||||||
|
- **Cache**: Hook environments are cached and reused
|
||||||
|
- **Skip**: Use `SKIP` to bypass slow hooks during development
|
||||||
|
|
||||||
|
## Integration with Git
|
||||||
|
|
||||||
|
To automatically run on every commit:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Install Git pre-commit hook
|
||||||
|
pre-commit install
|
||||||
|
|
||||||
|
# Now pre-commit runs automatically on git commit
|
||||||
|
git commit -m "Your commit message"
|
||||||
|
```
|
||||||
|
|
||||||
|
To bypass pre-commit hook temporarily:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git commit --no-verify -m "Emergency commit"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
Pre-commit configuration is in `.pre-commit-config.yaml`. To update hooks:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Update to latest versions
|
||||||
|
pre-commit autoupdate
|
||||||
|
|
||||||
|
# Clean cache and re-install
|
||||||
|
pre-commit clean
|
||||||
|
pre-commit install --install-hooks
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2025-12-20
|
||||||
|
**Maintained by**: Charon Project
|
||||||
|
**Source**: `pre-commit run --all-files`
|
||||||
26
.github/skills/qa-precommit-all.SKILL.md
vendored
26
.github/skills/qa-precommit-all.SKILL.md
vendored
@@ -1,8 +1,8 @@
|
|||||||
---
|
---
|
||||||
# agentskills.io specification v1.0
|
# agentskills.io specification v1.0
|
||||||
name: "qa-precommit-all"
|
name: "qa-lefthook-all"
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
description: "Run all pre-commit hooks for comprehensive code quality validation"
|
description: "Run all lefthook pre-commit-phase hooks for comprehensive code quality validation"
|
||||||
author: "Charon Project"
|
author: "Charon Project"
|
||||||
license: "MIT"
|
license: "MIT"
|
||||||
tags:
|
tags:
|
||||||
@@ -21,15 +21,11 @@ requirements:
|
|||||||
- name: "python3"
|
- name: "python3"
|
||||||
version: ">=3.8"
|
version: ">=3.8"
|
||||||
optional: false
|
optional: false
|
||||||
- name: "pre-commit"
|
- name: "lefthook"
|
||||||
version: ">=2.0"
|
version: ">=0.14"
|
||||||
optional: false
|
optional: false
|
||||||
environment_variables:
|
environment_variables:
|
||||||
- name: "PRE_COMMIT_HOME"
|
- name: "SKIP"
|
||||||
description: "Pre-commit cache directory"
|
|
||||||
default: "~/.cache/pre-commit"
|
|
||||||
required: false
|
|
||||||
- name: "SKIP"
|
|
||||||
description: "Comma-separated list of hook IDs to skip"
|
description: "Comma-separated list of hook IDs to skip"
|
||||||
default: ""
|
default: ""
|
||||||
required: false
|
required: false
|
||||||
@@ -60,7 +56,7 @@ metadata:
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
Executes all configured pre-commit hooks to validate code quality, formatting, security, and best practices across the entire codebase. This skill runs checks for Python, Go, JavaScript/TypeScript, Markdown, YAML, and more.
|
Executes all configured lefthook pre-commit-phase hooks to validate code quality, formatting, security, and best practices across the entire codebase. This skill runs checks for Python, Go, JavaScript/TypeScript, Markdown, YAML, and more.
|
||||||
|
|
||||||
This skill is designed for CI/CD pipelines and local quality validation before committing code.
|
This skill is designed for CI/CD pipelines and local quality validation before committing code.
|
||||||
|
|
||||||
@@ -76,19 +72,19 @@ This skill is designed for CI/CD pipelines and local quality validation before c
|
|||||||
|
|
||||||
### Basic Usage
|
### Basic Usage
|
||||||
|
|
||||||
Run all hooks on all files:
|
Run all pre-commit-phase hooks on all files:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd /path/to/charon
|
cd /path/to/charon
|
||||||
.github/skills/scripts/skill-runner.sh qa-precommit-all
|
lefthook run pre-commit
|
||||||
```
|
```
|
||||||
|
|
||||||
### Staged Files Only
|
### Staged Files Only
|
||||||
|
|
||||||
Run hooks on staged files only (faster):
|
Run lefthook on staged files only (faster):
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
.github/skills/scripts/skill-runner.sh qa-precommit-all staged
|
lefthook run pre-commit --staged
|
||||||
```
|
```
|
||||||
|
|
||||||
### Specific Hook
|
### Specific Hook
|
||||||
@@ -96,7 +92,7 @@ Run hooks on staged files only (faster):
|
|||||||
Run only a specific hook by ID:
|
Run only a specific hook by ID:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SKIP="" .github/skills/scripts/skill-runner.sh qa-precommit-all trailing-whitespace
|
lefthook run pre-commit --hooks=trailing-whitespace
|
||||||
```
|
```
|
||||||
|
|
||||||
### Skip Specific Hooks
|
### Skip Specific Hooks
|
||||||
|
|||||||
2
.github/skills/security-scan-codeql.SKILL.md
vendored
2
.github/skills/security-scan-codeql.SKILL.md
vendored
@@ -251,7 +251,7 @@ Solution: Verify source-root points to correct directory
|
|||||||
|
|
||||||
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Container/dependency vulnerabilities
|
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Container/dependency vulnerabilities
|
||||||
- [security-scan-go-vuln](./security-scan-go-vuln.SKILL.md) - Go-specific CVE checking
|
- [security-scan-go-vuln](./security-scan-go-vuln.SKILL.md) - Go-specific CVE checking
|
||||||
- [qa-precommit-all](./qa-precommit-all.SKILL.md) - Pre-commit quality checks
|
- [qa-lefthook-all](./qa-lefthook-all.SKILL.md) - Lefthook pre-commit-phase quality checks
|
||||||
|
|
||||||
## CI Alignment
|
## CI Alignment
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ fi
|
|||||||
# Check Grype
|
# Check Grype
|
||||||
if ! command -v grype >/dev/null 2>&1; then
|
if ! command -v grype >/dev/null 2>&1; then
|
||||||
log_error "Grype not found - install from: https://github.com/anchore/grype"
|
log_error "Grype not found - install from: https://github.com/anchore/grype"
|
||||||
log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.107.0"
|
log_error "Installation: curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.111.0"
|
||||||
error_exit "Grype is required for vulnerability scanning" 2
|
error_exit "Grype is required for vulnerability scanning" 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -50,8 +50,8 @@ SYFT_INSTALLED_VERSION=$(syft version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\
|
|||||||
GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
GRYPE_INSTALLED_VERSION=$(grype version | grep -oP 'Version:\s*\Kv?[0-9]+\.[0-9]+\.[0-9]+' | head -1 || echo "unknown")
|
||||||
|
|
||||||
# Set defaults matching CI workflow
|
# Set defaults matching CI workflow
|
||||||
set_default_env "SYFT_VERSION" "v1.17.0"
|
set_default_env "SYFT_VERSION" "v1.42.4"
|
||||||
set_default_env "GRYPE_VERSION" "v0.107.0"
|
set_default_env "GRYPE_VERSION" "v0.111.0"
|
||||||
set_default_env "IMAGE_TAG" "charon:local"
|
set_default_env "IMAGE_TAG" "charon:local"
|
||||||
set_default_env "FAIL_ON_SEVERITY" "Critical,High"
|
set_default_env "FAIL_ON_SEVERITY" "Critical,High"
|
||||||
|
|
||||||
@@ -139,7 +139,10 @@ log_info "This may take 30-60 seconds on first run (database download)"
|
|||||||
|
|
||||||
# Run Grype against the SBOM (generated from image, not filesystem)
|
# Run Grype against the SBOM (generated from image, not filesystem)
|
||||||
# This matches exactly what CI does in supply-chain-pr.yml
|
# This matches exactly what CI does in supply-chain-pr.yml
|
||||||
|
# --config ensures .grype.yaml ignore rules are applied, separating
|
||||||
|
# ignored matches from actionable ones in the JSON output
|
||||||
if grype sbom:sbom.cyclonedx.json \
|
if grype sbom:sbom.cyclonedx.json \
|
||||||
|
--config .grype.yaml \
|
||||||
--output json \
|
--output json \
|
||||||
--file grype-results.json; then
|
--file grype-results.json; then
|
||||||
log_success "Vulnerability scan complete"
|
log_success "Vulnerability scan complete"
|
||||||
@@ -149,6 +152,7 @@ fi
|
|||||||
|
|
||||||
# Generate SARIF output for GitHub Security (matches CI)
|
# Generate SARIF output for GitHub Security (matches CI)
|
||||||
grype sbom:sbom.cyclonedx.json \
|
grype sbom:sbom.cyclonedx.json \
|
||||||
|
--config .grype.yaml \
|
||||||
--output sarif \
|
--output sarif \
|
||||||
--file grype-results.sarif 2>/dev/null || true
|
--file grype-results.sarif 2>/dev/null || true
|
||||||
|
|
||||||
|
|||||||
2
.github/skills/security-scan-gorm.SKILL.md
vendored
2
.github/skills/security-scan-gorm.SKILL.md
vendored
@@ -545,7 +545,7 @@ Solution: Add suppression comment: // gorm-scanner:ignore [reason]
|
|||||||
|
|
||||||
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Container vulnerability scanning
|
- [security-scan-trivy](./security-scan-trivy.SKILL.md) - Container vulnerability scanning
|
||||||
- [security-scan-codeql](./security-scan-codeql.SKILL.md) - Static analysis for Go/JS
|
- [security-scan-codeql](./security-scan-codeql.SKILL.md) - Static analysis for Go/JS
|
||||||
- [qa-precommit-all](./qa-precommit-all.SKILL.md) - Pre-commit quality checks
|
- [qa-lefthook-all](./qa-lefthook-all.SKILL.md) - Lefthook pre-commit-phase quality checks
|
||||||
|
|
||||||
## Best Practices
|
## Best Practices
|
||||||
|
|
||||||
|
|||||||
2
.github/skills/security-scan-trivy.SKILL.md
vendored
2
.github/skills/security-scan-trivy.SKILL.md
vendored
@@ -227,7 +227,7 @@ Solution: Review and remediate reported vulnerabilities
|
|||||||
## Related Skills
|
## Related Skills
|
||||||
|
|
||||||
- [security-scan-go-vuln](./security-scan-go-vuln.SKILL.md) - Go-specific vulnerability checking
|
- [security-scan-go-vuln](./security-scan-go-vuln.SKILL.md) - Go-specific vulnerability checking
|
||||||
- [qa-precommit-all](./qa-precommit-all.SKILL.md) - Pre-commit quality checks
|
- [qa-lefthook-all](./qa-lefthook-all.SKILL.md) - Lefthook pre-commit-phase quality checks
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
|
|||||||
3
.github/workflows/auto-add-to-project.yml
vendored
3
.github/workflows/auto-add-to-project.yml
vendored
@@ -8,6 +8,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}
|
group: ${{ github.workflow }}-${{ github.event.issue.number || github.event.pull_request.number }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
add-to-project:
|
add-to-project:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
5
.github/workflows/auto-changelog.yml
vendored
5
.github/workflows/auto-changelog.yml
vendored
@@ -12,6 +12,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-draft:
|
update-draft:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -21,6 +24,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
- name: Draft Release
|
- name: Draft Release
|
||||||
uses: release-drafter/release-drafter@6db134d15f3909ccc9eefd369f02bd1e9cffdf97 # v6
|
uses: release-drafter/release-drafter@5de93583980a40bd78603b6dfdcda5b4df377b32 # v7
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
5
.github/workflows/auto-label-issues.yml
vendored
5
.github/workflows/auto-label-issues.yml
vendored
@@ -8,6 +8,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
group: ${{ github.workflow }}-${{ github.event.issue.number }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
auto-label:
|
auto-label:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -15,7 +18,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Auto-label based on title and body
|
- name: Auto-label based on title and body
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const issue = context.payload.issue;
|
const issue = context.payload.issue;
|
||||||
|
|||||||
4
.github/workflows/auto-versioning.yml
vendored
4
.github/workflows/auto-versioning.yml
vendored
@@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Calculate Semantic Version
|
- name: Calculate Semantic Version
|
||||||
id: semver
|
id: semver
|
||||||
uses: paulhatch/semantic-version@f29500c9d60a99ed5168e39ee367e0976884c46e # v6.0.1
|
uses: paulhatch/semantic-version@9f72830310d5ed81233b641ee59253644cd8a8fc # v6.0.2
|
||||||
with:
|
with:
|
||||||
# The prefix to use to create tags
|
# The prefix to use to create tags
|
||||||
tag_prefix: "v"
|
tag_prefix: "v"
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create GitHub Release (creates tag via API)
|
- name: Create GitHub Release (creates tag via API)
|
||||||
if: ${{ steps.semver.outputs.changed == 'true' && steps.check_release.outputs.exists == 'false' }}
|
if: ${{ steps.semver.outputs.changed == 'true' && steps.check_release.outputs.exists == 'false' }}
|
||||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ steps.determine_tag.outputs.tag }}
|
tag_name: ${{ steps.determine_tag.outputs.tag }}
|
||||||
name: Release ${{ steps.determine_tag.outputs.tag }}
|
name: Release ${{ steps.determine_tag.outputs.tag }}
|
||||||
|
|||||||
7
.github/workflows/benchmark.yml
vendored
7
.github/workflows/benchmark.yml
vendored
@@ -12,7 +12,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: '1.26.0'
|
GO_VERSION: '1.26.2'
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
|
|
||||||
# Minimal permissions at workflow level; write permissions granted at job level for push only
|
# Minimal permissions at workflow level; write permissions granted at job level for push only
|
||||||
@@ -35,9 +35,10 @@ jobs:
|
|||||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Run Benchmark
|
- name: Run Benchmark
|
||||||
@@ -51,7 +52,7 @@ jobs:
|
|||||||
# This avoids gh-pages branch errors and permission issues on fork PRs
|
# This avoids gh-pages branch errors and permission issues on fork PRs
|
||||||
if: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main'
|
if: github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main'
|
||||||
# Security: Pinned to full SHA for supply chain security
|
# Security: Pinned to full SHA for supply chain security
|
||||||
uses: benchmark-action/github-action-benchmark@4e0b38bc48375986542b13c0d8976b7b80c60c00 # v1
|
uses: benchmark-action/github-action-benchmark@a60cea5bc7b49e15c1f58f411161f99e0df48372 # v1.22.0
|
||||||
with:
|
with:
|
||||||
name: Go Benchmark
|
name: Go Benchmark
|
||||||
tool: 'go'
|
tool: 'go'
|
||||||
|
|||||||
2
.github/workflows/caddy-major-monitor.yml
vendored
2
.github/workflows/caddy-major-monitor.yml
vendored
@@ -18,7 +18,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Check for Caddy v3 and open issue
|
- name: Check for Caddy v3 and open issue
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const upstream = { owner: 'caddyserver', repo: 'caddy' };
|
const upstream = { owner: 'caddyserver', repo: 'caddy' };
|
||||||
|
|||||||
5
.github/workflows/cerberus-integration.yml
vendored
5
.github/workflows/cerberus-integration.yml
vendored
@@ -20,6 +20,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cerberus-integration:
|
cerberus-integration:
|
||||||
name: Cerberus Security Stack Integration
|
name: Cerberus Security Stack Integration
|
||||||
@@ -31,7 +34,7 @@ jobs:
|
|||||||
- name: Build Docker image (Local)
|
- name: Build Docker image (Local)
|
||||||
run: |
|
run: |
|
||||||
echo "Building image locally for integration tests..."
|
echo "Building image locally for integration tests..."
|
||||||
docker build -t charon:local .
|
docker build -t charon:local --build-arg CI="${CI:-false}" .
|
||||||
echo "✅ Successfully built charon:local"
|
echo "✅ Successfully built charon:local"
|
||||||
|
|
||||||
- name: Run Cerberus integration tests
|
- name: Run Cerberus integration tests
|
||||||
|
|||||||
22
.github/workflows/codecov-upload.yml
vendored
22
.github/workflows/codecov-upload.yml
vendored
@@ -23,7 +23,7 @@ concurrency:
|
|||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: '1.26.0'
|
GO_VERSION: '1.26.2'
|
||||||
NODE_VERSION: '24.12.0'
|
NODE_VERSION: '24.12.0'
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
|
|
||||||
@@ -45,9 +45,10 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
# SECURITY: Keep pull_request (not pull_request_target) for secret-bearing backend tests.
|
# SECURITY: Keep pull_request (not pull_request_target) for secret-bearing backend tests.
|
||||||
@@ -125,6 +126,9 @@ jobs:
|
|||||||
echo "__CHARON_EOF__"
|
echo "__CHARON_EOF__"
|
||||||
} >> "$GITHUB_ENV"
|
} >> "$GITHUB_ENV"
|
||||||
|
|
||||||
|
- name: Install gotestsum
|
||||||
|
run: go install gotest.tools/gotestsum@v1.13.0
|
||||||
|
|
||||||
- name: Run Go tests with coverage
|
- name: Run Go tests with coverage
|
||||||
working-directory: ${{ github.workspace }}
|
working-directory: ${{ github.workspace }}
|
||||||
env:
|
env:
|
||||||
@@ -133,8 +137,16 @@ jobs:
|
|||||||
bash scripts/go-test-coverage.sh 2>&1 | tee backend/test-output.txt
|
bash scripts/go-test-coverage.sh 2>&1 | tee backend/test-output.txt
|
||||||
exit "${PIPESTATUS[0]}"
|
exit "${PIPESTATUS[0]}"
|
||||||
|
|
||||||
|
- name: Upload test output artifact
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: backend-test-output
|
||||||
|
path: backend/test-output.txt
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
- name: Upload backend coverage to Codecov
|
- name: Upload backend coverage to Codecov
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
files: ./backend/coverage.txt
|
files: ./backend/coverage.txt
|
||||||
@@ -154,7 +166,7 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -171,7 +183,7 @@ jobs:
|
|||||||
exit "${PIPESTATUS[0]}"
|
exit "${PIPESTATUS[0]}"
|
||||||
|
|
||||||
- name: Upload frontend coverage to Codecov
|
- name: Upload frontend coverage to Codecov
|
||||||
uses: codecov/codecov-action@671740ac38dd9b0130fbe1cec585b89eea48d3de # v5
|
uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.CODECOV_TOKEN }}
|
token: ${{ secrets.CODECOV_TOKEN }}
|
||||||
directory: ./frontend/coverage
|
directory: ./frontend/coverage
|
||||||
|
|||||||
18
.github/workflows/codeql.yml
vendored
18
.github/workflows/codeql.yml
vendored
@@ -15,6 +15,7 @@ concurrency:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
|
GO_VERSION: '1.26.2'
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -39,14 +40,19 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.sha }}
|
# Use github.ref (full ref path) instead of github.ref_name:
|
||||||
|
# - push/schedule: resolves to refs/heads/<branch>, checking out latest HEAD
|
||||||
|
# - pull_request: resolves to refs/pull/<n>/merge, the correct PR merge ref
|
||||||
|
# github.ref_name fails for PRs because it yields "<n>/merge" which checkout
|
||||||
|
# interprets as a branch name (refs/heads/<n>/merge) that does not exist.
|
||||||
|
ref: ${{ github.ref }}
|
||||||
|
|
||||||
- name: Verify CodeQL parity guard
|
- name: Verify CodeQL parity guard
|
||||||
if: matrix.language == 'go'
|
if: matrix.language == 'go'
|
||||||
run: bash scripts/ci/check-codeql-parity.sh
|
run: bash scripts/ci/check-codeql-parity.sh
|
||||||
|
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
|
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
queries: security-and-quality
|
queries: security-and-quality
|
||||||
@@ -57,9 +63,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
if: matrix.language == 'go'
|
if: matrix.language == 'go'
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: 1.26.0
|
go-version: ${{ env.GO_VERSION }}
|
||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Verify Go toolchain and build
|
- name: Verify Go toolchain and build
|
||||||
@@ -86,10 +92,10 @@ jobs:
|
|||||||
run: mkdir -p sarif-results
|
run: mkdir -p sarif-results
|
||||||
|
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
|
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
|
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||||
with:
|
with:
|
||||||
category: "/language:${{ matrix.language }}"
|
category: "/language:${{ matrix.language }}"
|
||||||
output: sarif-results/${{ matrix.language }}
|
output: sarif-results/${{ matrix.language }}
|
||||||
|
|||||||
6
.github/workflows/container-prune.yml
vendored
6
.github/workflows/container-prune.yml
vendored
@@ -88,7 +88,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload GHCR prune artifacts
|
- name: Upload GHCR prune artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: prune-ghcr-log-${{ github.run_id }}
|
name: prune-ghcr-log-${{ github.run_id }}
|
||||||
path: |
|
path: |
|
||||||
@@ -159,7 +159,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker Hub prune artifacts
|
- name: Upload Docker Hub prune artifacts
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: prune-dockerhub-log-${{ github.run_id }}
|
name: prune-dockerhub-log-${{ github.run_id }}
|
||||||
path: |
|
path: |
|
||||||
@@ -172,7 +172,7 @@ jobs:
|
|||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- name: Download all artifacts
|
- name: Download all artifacts
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
pattern: prune-*-log-${{ github.run_id }}
|
pattern: prune-*-log-${{ github.run_id }}
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
|
|||||||
5
.github/workflows/create-labels.yml
vendored
5
.github/workflows/create-labels.yml
vendored
@@ -8,6 +8,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}
|
group: ${{ github.workflow }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-labels:
|
create-labels:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -15,7 +18,7 @@ jobs:
|
|||||||
issues: write
|
issues: write
|
||||||
steps:
|
steps:
|
||||||
- name: Create all project labels
|
- name: Create all project labels
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const labels = [
|
const labels = [
|
||||||
|
|||||||
5
.github/workflows/crowdsec-integration.yml
vendored
5
.github/workflows/crowdsec-integration.yml
vendored
@@ -20,6 +20,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
crowdsec-integration:
|
crowdsec-integration:
|
||||||
name: CrowdSec Bouncer Integration
|
name: CrowdSec Bouncer Integration
|
||||||
@@ -31,7 +34,7 @@ jobs:
|
|||||||
- name: Build Docker image (Local)
|
- name: Build Docker image (Local)
|
||||||
run: |
|
run: |
|
||||||
echo "Building image locally for integration tests..."
|
echo "Building image locally for integration tests..."
|
||||||
docker build -t charon:local .
|
docker build -t charon:local --build-arg CI="${CI:-false}" .
|
||||||
echo "✅ Successfully built charon:local"
|
echo "✅ Successfully built charon:local"
|
||||||
|
|
||||||
- name: Run CrowdSec integration tests
|
- name: Run CrowdSec integration tests
|
||||||
|
|||||||
60
.github/workflows/docker-build.yml
vendored
60
.github/workflows/docker-build.yml
vendored
@@ -23,7 +23,7 @@ name: Docker Build, Publish & Test
|
|||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main, development]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_run:
|
workflow_run:
|
||||||
workflows: ["Docker Lint"]
|
workflows: ["Docker Lint"]
|
||||||
@@ -33,6 +33,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GHCR_REGISTRY: ghcr.io
|
GHCR_REGISTRY: ghcr.io
|
||||||
DOCKERHUB_REGISTRY: docker.io
|
DOCKERHUB_REGISTRY: docker.io
|
||||||
@@ -42,7 +45,7 @@ env:
|
|||||||
TRIGGER_HEAD_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
TRIGGER_HEAD_SHA: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_sha || github.sha }}
|
||||||
TRIGGER_REF: ${{ github.event_name == 'workflow_run' && format('refs/heads/{0}', github.event.workflow_run.head_branch) || github.ref }}
|
TRIGGER_REF: ${{ github.event_name == 'workflow_run' && format('refs/heads/{0}', github.event.workflow_run.head_branch) || github.ref }}
|
||||||
TRIGGER_HEAD_REF: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.head_ref }}
|
TRIGGER_HEAD_REF: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.head_ref }}
|
||||||
TRIGGER_PR_NUMBER: ${{ github.event_name == 'workflow_run' && join(github.event.workflow_run.pull_requests.*.number, '') || github.event.pull_request.number }}
|
TRIGGER_PR_NUMBER: ${{ github.event_name == 'workflow_run' && join(github.event.workflow_run.pull_requests.*.number, '') || format('{0}', github.event.pull_request.number) }}
|
||||||
TRIGGER_ACTOR: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.actor.login || github.actor }}
|
TRIGGER_ACTOR: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.actor.login || github.actor }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
@@ -115,21 +118,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
if: steps.skip.outputs.skip_build != 'true'
|
if: steps.skip.outputs.skip_build != 'true'
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
if: steps.skip.outputs.skip_build != 'true'
|
if: steps.skip.outputs.skip_build != 'true'
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
- name: Resolve Alpine base image digest
|
- name: Resolve Alpine base image digest
|
||||||
if: steps.skip.outputs.skip_build != 'true'
|
if: steps.skip.outputs.skip_build != 'true'
|
||||||
id: caddy
|
id: alpine
|
||||||
run: |
|
run: |
|
||||||
docker pull alpine:3.23.3
|
ALPINE_TAG=$(grep -m1 'ARG ALPINE_IMAGE=' Dockerfile | sed 's/ARG ALPINE_IMAGE=alpine://' | cut -d'@' -f1)
|
||||||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' alpine:3.23.3)
|
docker pull "alpine:${ALPINE_TAG}"
|
||||||
|
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "alpine:${ALPINE_TAG}")
|
||||||
echo "image=$DIGEST" >> "$GITHUB_OUTPUT"
|
echo "image=$DIGEST" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
if: steps.skip.outputs.skip_build != 'true'
|
if: steps.skip.outputs.skip_build != 'true'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GHCR_REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -137,7 +141,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: steps.skip.outputs.skip_build != 'true' && env.HAS_DOCKERHUB_TOKEN == 'true'
|
if: steps.skip.outputs.skip_build != 'true' && env.HAS_DOCKERHUB_TOKEN == 'true'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -199,7 +203,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate Docker metadata
|
- name: Generate Docker metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
@@ -233,7 +237,7 @@ jobs:
|
|||||||
- name: Build and push Docker image (with retry)
|
- name: Build and push Docker image (with retry)
|
||||||
if: steps.skip.outputs.skip_build != 'true'
|
if: steps.skip.outputs.skip_build != 'true'
|
||||||
id: build-and-push
|
id: build-and-push
|
||||||
uses: nick-fields/retry@ce71cc2ab81d554ebbe88c79ab5975992d79ba08 # v3.0.2
|
uses: nick-fields/retry@ad984534de44a9489a53aefd81eb77f87c70dc60 # v4.0.0
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 25
|
timeout_minutes: 25
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
@@ -271,7 +275,7 @@ jobs:
|
|||||||
--build-arg "VERSION=${{ steps.meta.outputs.version }}"
|
--build-arg "VERSION=${{ steps.meta.outputs.version }}"
|
||||||
--build-arg "BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}"
|
--build-arg "BUILD_DATE=${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.created'] }}"
|
||||||
--build-arg "VCS_REF=${{ env.TRIGGER_HEAD_SHA }}"
|
--build-arg "VCS_REF=${{ env.TRIGGER_HEAD_SHA }}"
|
||||||
--build-arg "CADDY_IMAGE=${{ steps.caddy.outputs.image }}"
|
--build-arg "ALPINE_IMAGE=${{ steps.alpine.outputs.image }}"
|
||||||
--iidfile /tmp/image-digest.txt
|
--iidfile /tmp/image-digest.txt
|
||||||
.
|
.
|
||||||
)
|
)
|
||||||
@@ -343,7 +347,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Image Artifact
|
- name: Upload Image Artifact
|
||||||
if: success() && steps.skip.outputs.skip_build != 'true' && env.TRIGGER_EVENT == 'pull_request'
|
if: success() && steps.skip.outputs.skip_build != 'true' && env.TRIGGER_EVENT == 'pull_request'
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ${{ env.TRIGGER_EVENT == 'pull_request' && format('pr-image-{0}', env.TRIGGER_PR_NUMBER) || 'push-image' }}
|
name: ${{ env.TRIGGER_EVENT == 'pull_request' && format('pr-image-{0}', env.TRIGGER_PR_NUMBER) || 'push-image' }}
|
||||||
path: /tmp/charon-pr-image.tar
|
path: /tmp/charon-pr-image.tar
|
||||||
@@ -531,23 +535,25 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Trivy scan (table output)
|
- name: Run Trivy scan (table output)
|
||||||
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||||
format: 'table'
|
format: 'table'
|
||||||
severity: 'CRITICAL,HIGH'
|
severity: 'CRITICAL,HIGH'
|
||||||
exit-code: '0'
|
exit-code: '0'
|
||||||
|
version: 'v0.70.0'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner (SARIF)
|
- name: Run Trivy vulnerability scanner (SARIF)
|
||||||
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
||||||
id: trivy
|
id: trivy
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||||
format: 'sarif'
|
format: 'sarif'
|
||||||
output: 'trivy-results.sarif'
|
output: 'trivy-results.sarif'
|
||||||
severity: 'CRITICAL,HIGH'
|
severity: 'CRITICAL,HIGH'
|
||||||
|
version: 'v0.70.0'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Check Trivy SARIF exists
|
- name: Check Trivy SARIF exists
|
||||||
@@ -562,7 +568,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Trivy results
|
- name: Upload Trivy results
|
||||||
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true'
|
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.trivy-check.outputs.exists == 'true'
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-results.sarif'
|
sarif_file: 'trivy-results.sarif'
|
||||||
category: '.github/workflows/docker-build.yml:build-and-push'
|
category: '.github/workflows/docker-build.yml:build-and-push'
|
||||||
@@ -571,7 +577,7 @@ jobs:
|
|||||||
# Generate SBOM (Software Bill of Materials) for supply chain security
|
# Generate SBOM (Software Bill of Materials) for supply chain security
|
||||||
# Only for production builds (main/development) - feature branches use downstream supply-chain-pr.yml
|
# Only for production builds (main/development) - feature branches use downstream supply-chain-pr.yml
|
||||||
- name: Generate SBOM
|
- name: Generate SBOM
|
||||||
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
|
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||||
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
||||||
with:
|
with:
|
||||||
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build-and-push.outputs.digest }}
|
||||||
@@ -580,7 +586,7 @@ jobs:
|
|||||||
|
|
||||||
# Create verifiable attestation for the SBOM
|
# Create verifiable attestation for the SBOM
|
||||||
- name: Attest SBOM
|
- name: Attest SBOM
|
||||||
uses: actions/attest-sbom@07e74fc4e78d1aad915e867f9a094073a9f71527 # v4.0.0
|
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
|
||||||
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
||||||
with:
|
with:
|
||||||
subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
subject-name: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
@@ -591,7 +597,7 @@ jobs:
|
|||||||
# Install Cosign for keyless signing
|
# Install Cosign for keyless signing
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
if: env.TRIGGER_EVENT != 'pull_request' && steps.skip.outputs.skip_build != 'true' && steps.skip.outputs.is_feature_push != 'true'
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
|
|
||||||
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
|
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
|
||||||
- name: Sign GHCR Image
|
- name: Sign GHCR Image
|
||||||
@@ -657,7 +663,7 @@ jobs:
|
|||||||
echo "image_ref=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${PR_TAG}" >> "$GITHUB_OUTPUT"
|
echo "image_ref=${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:${PR_TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GHCR_REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -689,22 +695,24 @@ jobs:
|
|||||||
echo "✅ Image freshness validated"
|
echo "✅ Image freshness validated"
|
||||||
|
|
||||||
- name: Run Trivy scan on PR image (table output)
|
- name: Run Trivy scan on PR image (table output)
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ steps.pr-image.outputs.image_ref }}
|
image-ref: ${{ steps.pr-image.outputs.image_ref }}
|
||||||
format: 'table'
|
format: 'table'
|
||||||
severity: 'CRITICAL,HIGH'
|
severity: 'CRITICAL,HIGH'
|
||||||
exit-code: '0'
|
exit-code: '0'
|
||||||
|
version: 'v0.70.0'
|
||||||
|
|
||||||
- name: Run Trivy scan on PR image (SARIF - blocking)
|
- name: Run Trivy scan on PR image (SARIF - blocking)
|
||||||
id: trivy-scan
|
id: trivy-scan
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ steps.pr-image.outputs.image_ref }}
|
image-ref: ${{ steps.pr-image.outputs.image_ref }}
|
||||||
format: 'sarif'
|
format: 'sarif'
|
||||||
output: 'trivy-pr-results.sarif'
|
output: 'trivy-pr-results.sarif'
|
||||||
severity: 'CRITICAL,HIGH'
|
severity: 'CRITICAL,HIGH'
|
||||||
exit-code: '1' # Intended to block, but continued on error for now
|
exit-code: '1' # Intended to block, but continued on error for now
|
||||||
|
version: 'v0.70.0'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Check Trivy PR SARIF exists
|
- name: Check Trivy PR SARIF exists
|
||||||
@@ -719,14 +727,14 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Trivy scan results
|
- name: Upload Trivy scan results
|
||||||
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-pr-results.sarif'
|
sarif_file: 'trivy-pr-results.sarif'
|
||||||
category: 'docker-pr-image'
|
category: 'docker-pr-image'
|
||||||
|
|
||||||
- name: Upload Trivy compatibility results (docker-build category)
|
- name: Upload Trivy compatibility results (docker-build category)
|
||||||
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-pr-results.sarif'
|
sarif_file: 'trivy-pr-results.sarif'
|
||||||
category: '.github/workflows/docker-build.yml:build-and-push'
|
category: '.github/workflows/docker-build.yml:build-and-push'
|
||||||
@@ -734,7 +742,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Trivy compatibility results (docker-publish alias)
|
- name: Upload Trivy compatibility results (docker-publish alias)
|
||||||
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-pr-results.sarif'
|
sarif_file: 'trivy-pr-results.sarif'
|
||||||
category: '.github/workflows/docker-publish.yml:build-and-push'
|
category: '.github/workflows/docker-publish.yml:build-and-push'
|
||||||
@@ -742,7 +750,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Trivy compatibility results (nightly alias)
|
- name: Upload Trivy compatibility results (nightly alias)
|
||||||
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
if: always() && steps.trivy-pr-check.outputs.exists == 'true'
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-pr-results.sarif'
|
sarif_file: 'trivy-pr-results.sarif'
|
||||||
category: 'trivy-nightly'
|
category: 'trivy-nightly'
|
||||||
|
|||||||
6
.github/workflows/docs-to-issues.yml
vendored
6
.github/workflows/docs-to-issues.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
|||||||
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
ref: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Detect changed files
|
- name: Detect changed files
|
||||||
id: changes
|
id: changes
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
COMMIT_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
COMMIT_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
with:
|
with:
|
||||||
@@ -95,7 +95,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Process issue files
|
- name: Process issue files
|
||||||
id: process
|
id: process
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
DRY_RUN: ${{ github.event.inputs.dry_run || 'false' }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
6
.github/workflows/docs.yml
vendored
6
.github/workflows/docs.yml
vendored
@@ -38,7 +38,7 @@ jobs:
|
|||||||
|
|
||||||
# Step 2: Set up Node.js (for building any JS-based doc tools)
|
# Step 2: Set up Node.js (for building any JS-based doc tools)
|
||||||
- name: 🔧 Set up Node.js
|
- name: 🔧 Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
@@ -352,7 +352,7 @@ jobs:
|
|||||||
|
|
||||||
# Step 4: Upload the built site
|
# Step 4: Upload the built site
|
||||||
- name: 📤 Upload artifact
|
- name: 📤 Upload artifact
|
||||||
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4
|
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5
|
||||||
with:
|
with:
|
||||||
path: '_site'
|
path: '_site'
|
||||||
|
|
||||||
@@ -372,7 +372,7 @@ jobs:
|
|||||||
# Deploy to GitHub Pages
|
# Deploy to GitHub Pages
|
||||||
- name: 🚀 Deploy to GitHub Pages
|
- name: 🚀 Deploy to GitHub Pages
|
||||||
id: deployment
|
id: deployment
|
||||||
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4
|
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5
|
||||||
|
|
||||||
# Create a summary
|
# Create a summary
|
||||||
- name: 📋 Create deployment summary
|
- name: 📋 Create deployment summary
|
||||||
|
|||||||
122
.github/workflows/e2e-tests-split.yml
vendored
122
.github/workflows/e2e-tests-split.yml
vendored
@@ -83,7 +83,7 @@ on:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: '20'
|
NODE_VERSION: '20'
|
||||||
GO_VERSION: '1.26.0'
|
GO_VERSION: '1.26.2'
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
DOCKERHUB_REGISTRY: docker.io
|
DOCKERHUB_REGISTRY: docker.io
|
||||||
IMAGE_NAME: ${{ github.repository_owner }}/charon
|
IMAGE_NAME: ${{ github.repository_owner }}/charon
|
||||||
@@ -142,22 +142,23 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
if: steps.resolve-image.outputs.image_source == 'build'
|
if: steps.resolve-image.outputs.image_source == 'build'
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
cache: true
|
cache: true
|
||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
if: steps.resolve-image.outputs.image_source == 'build'
|
if: steps.resolve-image.outputs.image_source == 'build'
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
|
|
||||||
- name: Cache npm dependencies
|
- name: Cache npm dependencies
|
||||||
if: steps.resolve-image.outputs.image_source == 'build'
|
if: steps.resolve-image.outputs.image_source == 'build'
|
||||||
uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5
|
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5
|
||||||
with:
|
with:
|
||||||
path: ~/.npm
|
path: ~/.npm
|
||||||
key: npm-${{ hashFiles('package-lock.json') }}
|
key: npm-${{ hashFiles('package-lock.json') }}
|
||||||
@@ -169,12 +170,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
if: steps.resolve-image.outputs.image_source == 'build'
|
if: steps.resolve-image.outputs.image_source == 'build'
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4
|
||||||
|
|
||||||
- name: Build Docker image
|
- name: Build Docker image
|
||||||
id: build-image
|
id: build-image
|
||||||
if: steps.resolve-image.outputs.image_source == 'build'
|
if: steps.resolve-image.outputs.image_source == 'build'
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
file: ./Dockerfile
|
file: ./Dockerfile
|
||||||
@@ -190,7 +191,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker image artifact
|
- name: Upload Docker image artifact
|
||||||
if: steps.resolve-image.outputs.image_source == 'build'
|
if: steps.resolve-image.outputs.image_source == 'build'
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
path: charon-e2e-image.tar
|
path: charon-e2e-image.tar
|
||||||
@@ -224,7 +225,7 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -232,7 +233,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: needs.build.outputs.image_source == 'registry'
|
if: needs.build.outputs.image_source == 'registry'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -247,7 +248,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Docker image artifact
|
- name: Download Docker image artifact
|
||||||
if: needs.build.outputs.image_source == 'build'
|
if: needs.build.outputs.image_source == 'build'
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
|
|
||||||
@@ -347,7 +348,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload HTML report (Chromium Security)
|
- name: Upload HTML report (Chromium Security)
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-report-chromium-security
|
name: playwright-report-chromium-security
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
@@ -355,7 +356,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Chromium Security coverage (if enabled)
|
- name: Upload Chromium Security coverage (if enabled)
|
||||||
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-coverage-chromium-security
|
name: e2e-coverage-chromium-security
|
||||||
path: coverage/e2e/
|
path: coverage/e2e/
|
||||||
@@ -363,7 +364,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test traces on failure
|
- name: Upload test traces on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: traces-chromium-security
|
name: traces-chromium-security
|
||||||
path: test-results/**/*.zip
|
path: test-results/**/*.zip
|
||||||
@@ -382,7 +383,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload diagnostics
|
- name: Upload diagnostics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-diagnostics-chromium-security
|
name: e2e-diagnostics-chromium-security
|
||||||
path: diagnostics/
|
path: diagnostics/
|
||||||
@@ -395,7 +396,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker logs on failure
|
- name: Upload Docker logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docker-logs-chromium-security
|
name: docker-logs-chromium-security
|
||||||
path: docker-logs-chromium-security.txt
|
path: docker-logs-chromium-security.txt
|
||||||
@@ -426,7 +427,7 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -434,7 +435,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: needs.build.outputs.image_source == 'registry'
|
if: needs.build.outputs.image_source == 'registry'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -449,7 +450,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Docker image artifact
|
- name: Download Docker image artifact
|
||||||
if: needs.build.outputs.image_source == 'build'
|
if: needs.build.outputs.image_source == 'build'
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
|
|
||||||
@@ -557,7 +558,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload HTML report (Firefox Security)
|
- name: Upload HTML report (Firefox Security)
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-report-firefox-security
|
name: playwright-report-firefox-security
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
@@ -565,7 +566,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Firefox Security coverage (if enabled)
|
- name: Upload Firefox Security coverage (if enabled)
|
||||||
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-coverage-firefox-security
|
name: e2e-coverage-firefox-security
|
||||||
path: coverage/e2e/
|
path: coverage/e2e/
|
||||||
@@ -573,7 +574,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test traces on failure
|
- name: Upload test traces on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: traces-firefox-security
|
name: traces-firefox-security
|
||||||
path: test-results/**/*.zip
|
path: test-results/**/*.zip
|
||||||
@@ -592,7 +593,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload diagnostics
|
- name: Upload diagnostics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-diagnostics-firefox-security
|
name: e2e-diagnostics-firefox-security
|
||||||
path: diagnostics/
|
path: diagnostics/
|
||||||
@@ -605,7 +606,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker logs on failure
|
- name: Upload Docker logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docker-logs-firefox-security
|
name: docker-logs-firefox-security
|
||||||
path: docker-logs-firefox-security.txt
|
path: docker-logs-firefox-security.txt
|
||||||
@@ -636,7 +637,7 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -644,7 +645,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: needs.build.outputs.image_source == 'registry'
|
if: needs.build.outputs.image_source == 'registry'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -659,7 +660,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Docker image artifact
|
- name: Download Docker image artifact
|
||||||
if: needs.build.outputs.image_source == 'build'
|
if: needs.build.outputs.image_source == 'build'
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
|
|
||||||
@@ -767,7 +768,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload HTML report (WebKit Security)
|
- name: Upload HTML report (WebKit Security)
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-report-webkit-security
|
name: playwright-report-webkit-security
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
@@ -775,7 +776,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload WebKit Security coverage (if enabled)
|
- name: Upload WebKit Security coverage (if enabled)
|
||||||
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-coverage-webkit-security
|
name: e2e-coverage-webkit-security
|
||||||
path: coverage/e2e/
|
path: coverage/e2e/
|
||||||
@@ -783,7 +784,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test traces on failure
|
- name: Upload test traces on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: traces-webkit-security
|
name: traces-webkit-security
|
||||||
path: test-results/**/*.zip
|
path: test-results/**/*.zip
|
||||||
@@ -802,7 +803,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload diagnostics
|
- name: Upload diagnostics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-diagnostics-webkit-security
|
name: e2e-diagnostics-webkit-security
|
||||||
path: diagnostics/
|
path: diagnostics/
|
||||||
@@ -815,7 +816,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker logs on failure
|
- name: Upload Docker logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docker-logs-webkit-security
|
name: docker-logs-webkit-security
|
||||||
path: docker-logs-webkit-security.txt
|
path: docker-logs-webkit-security.txt
|
||||||
@@ -858,7 +859,7 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -898,7 +899,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: needs.build.outputs.image_source == 'registry'
|
if: needs.build.outputs.image_source == 'registry'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -913,7 +914,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Docker image artifact
|
- name: Download Docker image artifact
|
||||||
if: needs.build.outputs.image_source == 'build'
|
if: needs.build.outputs.image_source == 'build'
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
|
|
||||||
@@ -979,6 +980,7 @@ jobs:
|
|||||||
--project=chromium \
|
--project=chromium \
|
||||||
--shard=${{ matrix.shard }}/${{ matrix.total-shards }} \
|
--shard=${{ matrix.shard }}/${{ matrix.total-shards }} \
|
||||||
--output=playwright-output/chromium-shard-${{ matrix.shard }} \
|
--output=playwright-output/chromium-shard-${{ matrix.shard }} \
|
||||||
|
tests/a11y \
|
||||||
tests/core \
|
tests/core \
|
||||||
tests/dns-provider-crud.spec.ts \
|
tests/dns-provider-crud.spec.ts \
|
||||||
tests/dns-provider-types.spec.ts \
|
tests/dns-provider-types.spec.ts \
|
||||||
@@ -1003,7 +1005,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload HTML report (Chromium shard ${{ matrix.shard }})
|
- name: Upload HTML report (Chromium shard ${{ matrix.shard }})
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-report-chromium-shard-${{ matrix.shard }}
|
name: playwright-report-chromium-shard-${{ matrix.shard }}
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
@@ -1011,7 +1013,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright output (Chromium shard ${{ matrix.shard }})
|
- name: Upload Playwright output (Chromium shard ${{ matrix.shard }})
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-output-chromium-shard-${{ matrix.shard }}
|
name: playwright-output-chromium-shard-${{ matrix.shard }}
|
||||||
path: playwright-output/chromium-shard-${{ matrix.shard }}/
|
path: playwright-output/chromium-shard-${{ matrix.shard }}/
|
||||||
@@ -1019,7 +1021,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Chromium coverage (if enabled)
|
- name: Upload Chromium coverage (if enabled)
|
||||||
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-coverage-chromium-shard-${{ matrix.shard }}
|
name: e2e-coverage-chromium-shard-${{ matrix.shard }}
|
||||||
path: coverage/e2e/
|
path: coverage/e2e/
|
||||||
@@ -1027,7 +1029,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test traces on failure
|
- name: Upload test traces on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: traces-chromium-shard-${{ matrix.shard }}
|
name: traces-chromium-shard-${{ matrix.shard }}
|
||||||
path: test-results/**/*.zip
|
path: test-results/**/*.zip
|
||||||
@@ -1046,7 +1048,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload diagnostics
|
- name: Upload diagnostics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-diagnostics-chromium-shard-${{ matrix.shard }}
|
name: e2e-diagnostics-chromium-shard-${{ matrix.shard }}
|
||||||
path: diagnostics/
|
path: diagnostics/
|
||||||
@@ -1059,7 +1061,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker logs on failure
|
- name: Upload Docker logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docker-logs-chromium-shard-${{ matrix.shard }}
|
name: docker-logs-chromium-shard-${{ matrix.shard }}
|
||||||
path: docker-logs-chromium-shard-${{ matrix.shard }}.txt
|
path: docker-logs-chromium-shard-${{ matrix.shard }}.txt
|
||||||
@@ -1095,7 +1097,7 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -1135,7 +1137,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: needs.build.outputs.image_source == 'registry'
|
if: needs.build.outputs.image_source == 'registry'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -1150,7 +1152,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Docker image artifact
|
- name: Download Docker image artifact
|
||||||
if: needs.build.outputs.image_source == 'build'
|
if: needs.build.outputs.image_source == 'build'
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
|
|
||||||
@@ -1224,6 +1226,7 @@ jobs:
|
|||||||
--project=firefox \
|
--project=firefox \
|
||||||
--shard=${{ matrix.shard }}/${{ matrix.total-shards }} \
|
--shard=${{ matrix.shard }}/${{ matrix.total-shards }} \
|
||||||
--output=playwright-output/firefox-shard-${{ matrix.shard }} \
|
--output=playwright-output/firefox-shard-${{ matrix.shard }} \
|
||||||
|
tests/a11y \
|
||||||
tests/core \
|
tests/core \
|
||||||
tests/dns-provider-crud.spec.ts \
|
tests/dns-provider-crud.spec.ts \
|
||||||
tests/dns-provider-types.spec.ts \
|
tests/dns-provider-types.spec.ts \
|
||||||
@@ -1248,7 +1251,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload HTML report (Firefox shard ${{ matrix.shard }})
|
- name: Upload HTML report (Firefox shard ${{ matrix.shard }})
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-report-firefox-shard-${{ matrix.shard }}
|
name: playwright-report-firefox-shard-${{ matrix.shard }}
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
@@ -1256,7 +1259,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright output (Firefox shard ${{ matrix.shard }})
|
- name: Upload Playwright output (Firefox shard ${{ matrix.shard }})
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: playwright-output-firefox-shard-${{ matrix.shard }}
|
name: playwright-output-firefox-shard-${{ matrix.shard }}
|
||||||
path: playwright-output/firefox-shard-${{ matrix.shard }}/
|
path: playwright-output/firefox-shard-${{ matrix.shard }}/
|
||||||
@@ -1264,7 +1267,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Firefox coverage (if enabled)
|
- name: Upload Firefox coverage (if enabled)
|
||||||
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-coverage-firefox-shard-${{ matrix.shard }}
|
name: e2e-coverage-firefox-shard-${{ matrix.shard }}
|
||||||
path: coverage/e2e/
|
path: coverage/e2e/
|
||||||
@@ -1272,7 +1275,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test traces on failure
|
- name: Upload test traces on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: traces-firefox-shard-${{ matrix.shard }}
|
name: traces-firefox-shard-${{ matrix.shard }}
|
||||||
path: test-results/**/*.zip
|
path: test-results/**/*.zip
|
||||||
@@ -1291,7 +1294,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload diagnostics
|
- name: Upload diagnostics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: e2e-diagnostics-firefox-shard-${{ matrix.shard }}
|
name: e2e-diagnostics-firefox-shard-${{ matrix.shard }}
|
||||||
path: diagnostics/
|
path: diagnostics/
|
||||||
@@ -1304,7 +1307,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker logs on failure
|
- name: Upload Docker logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docker-logs-firefox-shard-${{ matrix.shard }}
|
name: docker-logs-firefox-shard-${{ matrix.shard }}
|
||||||
path: docker-logs-firefox-shard-${{ matrix.shard }}.txt
|
path: docker-logs-firefox-shard-${{ matrix.shard }}.txt
|
||||||
@@ -1340,7 +1343,7 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
@@ -1380,7 +1383,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: needs.build.outputs.image_source == 'registry'
|
if: needs.build.outputs.image_source == 'registry'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
registry: ${{ env.DOCKERHUB_REGISTRY }}
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -1395,7 +1398,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Download Docker image artifact
|
- name: Download Docker image artifact
|
||||||
if: needs.build.outputs.image_source == 'build'
|
if: needs.build.outputs.image_source == 'build'
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
|
||||||
with:
|
with:
|
||||||
name: docker-image
|
name: docker-image
|
||||||
|
|
||||||
@@ -1469,6 +1472,7 @@ jobs:
|
|||||||
--project=webkit \
|
--project=webkit \
|
||||||
--shard=${{ matrix.shard }}/${{ matrix.total-shards }} \
|
--shard=${{ matrix.shard }}/${{ matrix.total-shards }} \
|
||||||
--output=playwright-output/webkit-shard-${{ matrix.shard }} \
|
--output=playwright-output/webkit-shard-${{ matrix.shard }} \
|
||||||
|
tests/a11y \
|
||||||
tests/core \
|
tests/core \
|
||||||
tests/dns-provider-crud.spec.ts \
|
tests/dns-provider-crud.spec.ts \
|
||||||
tests/dns-provider-types.spec.ts \
|
tests/dns-provider-types.spec.ts \
|
||||||
@@ -1493,7 +1497,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload HTML report (WebKit shard ${{ matrix.shard }})
|
- name: Upload HTML report (WebKit shard ${{ matrix.shard }})
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: playwright-report-webkit-shard-${{ matrix.shard }}
|
name: playwright-report-webkit-shard-${{ matrix.shard }}
|
||||||
path: playwright-report/
|
path: playwright-report/
|
||||||
@@ -1501,7 +1505,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Playwright output (WebKit shard ${{ matrix.shard }})
|
- name: Upload Playwright output (WebKit shard ${{ matrix.shard }})
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: playwright-output-webkit-shard-${{ matrix.shard }}
|
name: playwright-output-webkit-shard-${{ matrix.shard }}
|
||||||
path: playwright-output/webkit-shard-${{ matrix.shard }}/
|
path: playwright-output/webkit-shard-${{ matrix.shard }}/
|
||||||
@@ -1509,7 +1513,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload WebKit coverage (if enabled)
|
- name: Upload WebKit coverage (if enabled)
|
||||||
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
if: always() && (inputs.playwright_coverage == 'true' || vars.PLAYWRIGHT_COVERAGE == '1')
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: e2e-coverage-webkit-shard-${{ matrix.shard }}
|
name: e2e-coverage-webkit-shard-${{ matrix.shard }}
|
||||||
path: coverage/e2e/
|
path: coverage/e2e/
|
||||||
@@ -1517,7 +1521,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload test traces on failure
|
- name: Upload test traces on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: traces-webkit-shard-${{ matrix.shard }}
|
name: traces-webkit-shard-${{ matrix.shard }}
|
||||||
path: test-results/**/*.zip
|
path: test-results/**/*.zip
|
||||||
@@ -1536,7 +1540,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload diagnostics
|
- name: Upload diagnostics
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: e2e-diagnostics-webkit-shard-${{ matrix.shard }}
|
name: e2e-diagnostics-webkit-shard-${{ matrix.shard }}
|
||||||
path: diagnostics/
|
path: diagnostics/
|
||||||
@@ -1549,7 +1553,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Docker logs on failure
|
- name: Upload Docker logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: docker-logs-webkit-shard-${{ matrix.shard }}
|
name: docker-logs-webkit-shard-${{ matrix.shard }}
|
||||||
path: docker-logs-webkit-shard-${{ matrix.shard }}.txt
|
path: docker-logs-webkit-shard-${{ matrix.shard }}.txt
|
||||||
@@ -1605,7 +1609,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Check test results
|
- name: Check test results
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
EFFECTIVE_BROWSER: ${{ inputs.browser || 'all' }}
|
EFFECTIVE_BROWSER: ${{ inputs.browser || 'all' }}
|
||||||
EFFECTIVE_CATEGORY: ${{ inputs.test_category || 'all' }}
|
EFFECTIVE_CATEGORY: ${{ inputs.test_category || 'all' }}
|
||||||
|
|||||||
3
.github/workflows/gh_cache_cleanup.yml
vendored
3
.github/workflows/gh_cache_cleanup.yml
vendored
@@ -7,6 +7,9 @@ on:
|
|||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
cleanup:
|
cleanup:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
3
.github/workflows/history-rewrite-tests.yml
vendored
3
.github/workflows/history-rewrite-tests.yml
vendored
@@ -9,6 +9,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.workflow_run.head_branch || github.head_ref || github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|||||||
202
.github/workflows/nightly-build.yml
vendored
202
.github/workflows/nightly-build.yml
vendored
@@ -15,13 +15,16 @@ on:
|
|||||||
default: "false"
|
default: "false"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: '1.26.0'
|
GO_VERSION: '1.26.2'
|
||||||
NODE_VERSION: '24.12.0'
|
NODE_VERSION: '24.12.0'
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
GHCR_REGISTRY: ghcr.io
|
GHCR_REGISTRY: ghcr.io
|
||||||
DOCKERHUB_REGISTRY: docker.io
|
DOCKERHUB_REGISTRY: docker.io
|
||||||
IMAGE_NAME: wikid82/charon
|
IMAGE_NAME: wikid82/charon
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-development-to-nightly:
|
sync-development-to-nightly:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -86,7 +89,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Dispatch Missing Nightly Validation Workflows
|
- name: Dispatch Missing Nightly Validation Workflows
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const owner = context.repo.owner;
|
const owner = context.repo.owner;
|
||||||
@@ -148,27 +151,37 @@ jobs:
|
|||||||
id-token: write
|
id-token: write
|
||||||
outputs:
|
outputs:
|
||||||
version: ${{ steps.meta.outputs.version }}
|
version: ${{ steps.meta.outputs.version }}
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
digest: ${{ steps.resolve_digest.outputs.digest }}
|
||||||
digest: ${{ steps.build.outputs.digest }}
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout nightly branch
|
- name: Checkout nightly branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set lowercase image name
|
- name: Set lowercase image name
|
||||||
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
|
- name: Resolve Alpine base image digest
|
||||||
|
id: alpine
|
||||||
|
run: |
|
||||||
|
ALPINE_IMAGE_REF=$(grep -m1 'ARG ALPINE_IMAGE=' Dockerfile | cut -d'=' -f2-)
|
||||||
|
if [[ -z "$ALPINE_IMAGE_REF" ]]; then
|
||||||
|
echo "::error::Failed to parse ALPINE_IMAGE from Dockerfile"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Resolved Alpine image: ${ALPINE_IMAGE_REF}"
|
||||||
|
echo "image=${ALPINE_IMAGE_REF}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GHCR_REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -176,7 +189,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Log in to Docker Hub
|
- name: Log in to Docker Hub
|
||||||
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: docker.io
|
registry: docker.io
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
@@ -184,7 +197,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
with:
|
with:
|
||||||
images: |
|
images: |
|
||||||
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
@@ -199,7 +212,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build and push Docker image
|
- name: Build and push Docker image
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
@@ -210,22 +223,52 @@ jobs:
|
|||||||
VERSION=nightly-${{ github.sha }}
|
VERSION=nightly-${{ github.sha }}
|
||||||
VCS_REF=${{ github.sha }}
|
VCS_REF=${{ github.sha }}
|
||||||
BUILD_DATE=${{ github.event.repository.pushed_at }}
|
BUILD_DATE=${{ github.event.repository.pushed_at }}
|
||||||
|
ALPINE_IMAGE=${{ steps.alpine.outputs.image }}
|
||||||
cache-from: type=gha
|
cache-from: type=gha
|
||||||
cache-to: type=gha,mode=max
|
cache-to: type=gha,mode=max
|
||||||
provenance: true
|
provenance: true
|
||||||
sbom: true
|
sbom: true
|
||||||
|
|
||||||
|
- name: Resolve and export image digest
|
||||||
|
id: resolve_digest
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
DIGEST="${{ steps.build.outputs.digest }}"
|
||||||
|
|
||||||
|
if [[ -z "$DIGEST" ]]; then
|
||||||
|
echo "Build action digest empty; querying GHCR registry API..."
|
||||||
|
GHCR_TOKEN=$(curl -sf \
|
||||||
|
-u "${{ github.actor }}:${{ secrets.GITHUB_TOKEN }}" \
|
||||||
|
"https://ghcr.io/token?scope=repository:${{ env.IMAGE_NAME }}:pull&service=ghcr.io" \
|
||||||
|
| jq -r '.token')
|
||||||
|
DIGEST=$(curl -sfI \
|
||||||
|
-H "Authorization: Bearer ${GHCR_TOKEN}" \
|
||||||
|
-H "Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json,application/vnd.oci.image.manifest.v1+json" \
|
||||||
|
"https://ghcr.io/v2/${{ env.IMAGE_NAME }}/manifests/nightly" \
|
||||||
|
| grep -i '^docker-content-digest:' | awk '{print $2}' | tr -d '\r' || true)
|
||||||
|
[[ -n "$DIGEST" ]] && echo "Resolved from GHCR API: ${DIGEST}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$DIGEST" ]]; then
|
||||||
|
echo "::error::Could not determine image digest from step output or GHCR registry API"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "RESOLVED_DIGEST=${DIGEST}" >> "$GITHUB_ENV"
|
||||||
|
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Exported digest: ${DIGEST}"
|
||||||
|
|
||||||
- name: Record nightly image digest
|
- name: Record nightly image digest
|
||||||
run: |
|
run: |
|
||||||
echo "## 🧾 Nightly Image Digest" >> "$GITHUB_STEP_SUMMARY"
|
echo "## 🧾 Nightly Image Digest" >> "$GITHUB_STEP_SUMMARY"
|
||||||
echo "- ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
|
echo "- ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.resolve_digest.outputs.digest }}" >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
- name: Generate SBOM
|
- name: Generate SBOM
|
||||||
id: sbom_primary
|
id: sbom_primary
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
|
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||||
with:
|
with:
|
||||||
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}
|
image: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}
|
||||||
format: cyclonedx-json
|
format: cyclonedx-json
|
||||||
output-file: sbom-nightly.json
|
output-file: sbom-nightly.json
|
||||||
syft-version: v1.42.1
|
syft-version: v1.42.1
|
||||||
@@ -242,7 +285,7 @@ jobs:
|
|||||||
|
|
||||||
echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback"
|
echo "Primary SBOM generation failed or produced missing/invalid output; using deterministic Syft fallback"
|
||||||
|
|
||||||
SYFT_VERSION="v1.42.1"
|
SYFT_VERSION="v1.42.4"
|
||||||
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
||||||
ARCH="$(uname -m)"
|
ARCH="$(uname -m)"
|
||||||
case "$ARCH" in
|
case "$ARCH" in
|
||||||
@@ -263,7 +306,12 @@ jobs:
|
|||||||
tar -xzf "$TARBALL" syft
|
tar -xzf "$TARBALL" syft
|
||||||
chmod +x syft
|
chmod +x syft
|
||||||
|
|
||||||
./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ steps.build.outputs.digest }}" -o cyclonedx-json=sbom-nightly.json
|
DIGEST="${{ steps.resolve_digest.outputs.digest }}"
|
||||||
|
if [[ -z "$DIGEST" ]]; then
|
||||||
|
echo "::error::Digest from resolve_digest step is empty; the digest-resolution step did not complete successfully"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
./syft "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${DIGEST}" -o cyclonedx-json=sbom-nightly.json
|
||||||
|
|
||||||
- name: Verify SBOM artifact
|
- name: Verify SBOM artifact
|
||||||
if: always()
|
if: always()
|
||||||
@@ -280,7 +328,7 @@ jobs:
|
|||||||
' sbom-nightly.json >/dev/null
|
' sbom-nightly.json >/dev/null
|
||||||
|
|
||||||
- name: Upload SBOM artifact
|
- name: Upload SBOM artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: sbom-nightly
|
name: sbom-nightly
|
||||||
path: sbom-nightly.json
|
path: sbom-nightly.json
|
||||||
@@ -288,13 +336,13 @@ jobs:
|
|||||||
|
|
||||||
# Install Cosign for keyless signing
|
# Install Cosign for keyless signing
|
||||||
- name: Install Cosign
|
- name: Install Cosign
|
||||||
uses: sigstore/cosign-installer@faadad0cce49287aee09b3a48701e75088a2c6ad # v4.0.0
|
uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
|
||||||
|
|
||||||
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
|
# Sign GHCR image with keyless signing (Sigstore/Fulcio)
|
||||||
- name: Sign GHCR Image
|
- name: Sign GHCR Image
|
||||||
run: |
|
run: |
|
||||||
echo "Signing GHCR nightly image with keyless signing..."
|
echo "Signing GHCR nightly image with keyless signing..."
|
||||||
cosign sign --yes "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
|
cosign sign --yes "${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
|
||||||
echo "✅ GHCR nightly image signed successfully"
|
echo "✅ GHCR nightly image signed successfully"
|
||||||
|
|
||||||
# Sign Docker Hub image with keyless signing (Sigstore/Fulcio)
|
# Sign Docker Hub image with keyless signing (Sigstore/Fulcio)
|
||||||
@@ -302,7 +350,7 @@ jobs:
|
|||||||
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
||||||
run: |
|
run: |
|
||||||
echo "Signing Docker Hub nightly image with keyless signing..."
|
echo "Signing Docker Hub nightly image with keyless signing..."
|
||||||
cosign sign --yes "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
|
cosign sign --yes "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
|
||||||
echo "✅ Docker Hub nightly image signed successfully"
|
echo "✅ Docker Hub nightly image signed successfully"
|
||||||
|
|
||||||
# Attach SBOM to Docker Hub image
|
# Attach SBOM to Docker Hub image
|
||||||
@@ -310,7 +358,7 @@ jobs:
|
|||||||
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
if: env.HAS_DOCKERHUB_TOKEN == 'true'
|
||||||
run: |
|
run: |
|
||||||
echo "Attaching SBOM to Docker Hub nightly image..."
|
echo "Attaching SBOM to Docker Hub nightly image..."
|
||||||
cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}"
|
cosign attach sbom --sbom sbom-nightly.json "${{ env.DOCKERHUB_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.resolve_digest.outputs.digest }}"
|
||||||
echo "✅ SBOM attached to Docker Hub nightly image"
|
echo "✅ SBOM attached to Docker Hub nightly image"
|
||||||
|
|
||||||
test-nightly-image:
|
test-nightly-image:
|
||||||
@@ -324,13 +372,13 @@ jobs:
|
|||||||
- name: Checkout nightly branch
|
- name: Checkout nightly branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
|
||||||
|
|
||||||
- name: Set lowercase image name
|
- name: Set lowercase image name
|
||||||
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
- name: Log in to GitHub Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.GHCR_REGISTRY }}
|
registry: ${{ env.GHCR_REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -341,18 +389,33 @@ jobs:
|
|||||||
|
|
||||||
- name: Run container smoke test
|
- name: Run container smoke test
|
||||||
run: |
|
run: |
|
||||||
|
IMAGE_REF="${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
|
||||||
docker run --name charon-nightly -d \
|
docker run --name charon-nightly -d \
|
||||||
-p 8080:8080 \
|
-p 8080:8080 \
|
||||||
"${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}"
|
"${IMAGE_REF}"
|
||||||
|
|
||||||
# Wait for container to start
|
# Wait for container to become healthy
|
||||||
sleep 10
|
echo "⏳ Waiting for Charon to be healthy..."
|
||||||
|
MAX_ATTEMPTS=30
|
||||||
|
ATTEMPT=0
|
||||||
|
while [[ ${ATTEMPT} -lt ${MAX_ATTEMPTS} ]]; do
|
||||||
|
ATTEMPT=$((ATTEMPT + 1))
|
||||||
|
echo "Attempt ${ATTEMPT}/${MAX_ATTEMPTS}..."
|
||||||
|
if docker exec charon-nightly wget -qO- http://127.0.0.1:8080/health > /dev/null 2>&1; then
|
||||||
|
echo "✅ Charon is healthy!"
|
||||||
|
docker exec charon-nightly wget -qO- http://127.0.0.1:8080/health
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
|
|
||||||
# Check container is running
|
if [[ ${ATTEMPT} -ge ${MAX_ATTEMPTS} ]]; then
|
||||||
docker ps | grep charon-nightly
|
echo "❌ Health check failed after ${MAX_ATTEMPTS} attempts"
|
||||||
|
docker logs charon-nightly
|
||||||
# Basic health check
|
docker stop charon-nightly
|
||||||
curl -f http://localhost:8080/health || exit 1
|
docker rm charon-nightly
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Cleanup
|
# Cleanup
|
||||||
docker stop charon-nightly
|
docker stop charon-nightly
|
||||||
@@ -378,32 +441,34 @@ jobs:
|
|||||||
- name: Checkout nightly branch
|
- name: Checkout nightly branch
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
ref: nightly
|
ref: ${{ github.event_name == 'workflow_dispatch' && github.ref || 'nightly' }}
|
||||||
|
|
||||||
- name: Set lowercase image name
|
- name: Set lowercase image name
|
||||||
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
run: echo "IMAGE_NAME_LC=${IMAGE_NAME,,}" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Download SBOM
|
- name: Download SBOM
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3 # v8.0.0
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||||
with:
|
with:
|
||||||
name: sbom-nightly
|
name: sbom-nightly
|
||||||
|
|
||||||
- name: Scan with Grype
|
- name: Scan with Grype
|
||||||
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
|
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
|
||||||
with:
|
with:
|
||||||
sbom: sbom-nightly.json
|
sbom: sbom-nightly.json
|
||||||
fail-build: false
|
fail-build: false
|
||||||
severity-cutoff: high
|
severity-cutoff: high
|
||||||
|
|
||||||
- name: Scan with Trivy
|
- name: Scan with Trivy
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}@${{ needs.build-and-push-nightly.outputs.digest }}
|
image-ref: ${{ env.GHCR_REGISTRY }}/${{ env.IMAGE_NAME }}:nightly@${{ needs.build-and-push-nightly.outputs.digest }}
|
||||||
format: 'sarif'
|
format: 'sarif'
|
||||||
output: 'trivy-nightly.sarif'
|
output: 'trivy-nightly.sarif'
|
||||||
|
version: 'v0.70.0'
|
||||||
|
trivyignores: '.trivyignore'
|
||||||
|
|
||||||
- name: Upload Trivy results
|
- name: Upload Trivy results
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-nightly.sarif'
|
sarif_file: 'trivy-nightly.sarif'
|
||||||
category: 'trivy-nightly'
|
category: 'trivy-nightly'
|
||||||
@@ -506,18 +571,81 @@ jobs:
|
|||||||
echo "- Structured SARIF counts: CRITICAL=${CRITICAL_COUNT}, HIGH=${HIGH_COUNT}, MEDIUM=${MEDIUM_COUNT}"
|
echo "- Structured SARIF counts: CRITICAL=${CRITICAL_COUNT}, HIGH=${HIGH_COUNT}, MEDIUM=${MEDIUM_COUNT}"
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
# List all Critical/High/Medium findings with details for triage
|
||||||
|
# shellcheck disable=SC2016
|
||||||
|
LIST_FINDINGS='
|
||||||
|
.runs[] as $run
|
||||||
|
| ($run.tool.driver.rules // []) as $rules
|
||||||
|
| $run.results[]?
|
||||||
|
| . as $result
|
||||||
|
| (
|
||||||
|
(
|
||||||
|
if (($result.ruleIndex | type) == "number") then
|
||||||
|
($rules[$result.ruleIndex] // {})
|
||||||
|
else
|
||||||
|
{}
|
||||||
|
end
|
||||||
|
) as $ruleByIndex
|
||||||
|
| (
|
||||||
|
[$rules[]? | select((.id // "") == ($result.ruleId // ""))][0] // {}
|
||||||
|
) as $ruleById
|
||||||
|
| ($ruleByIndex // $ruleById) as $rule
|
||||||
|
| ($rule.properties["security-severity"] // null) as $sev
|
||||||
|
| (try ($sev | tonumber) catch null) as $score
|
||||||
|
| select($score != null and $score >= 4.0)
|
||||||
|
| {
|
||||||
|
id: ($result.ruleId // "unknown"),
|
||||||
|
score: $score,
|
||||||
|
severity: (
|
||||||
|
if $score >= 9.0 then "CRITICAL"
|
||||||
|
elif $score >= 7.0 then "HIGH"
|
||||||
|
else "MEDIUM"
|
||||||
|
end
|
||||||
|
),
|
||||||
|
message: ($result.message.text // $rule.shortDescription.text // "no description")[0:120]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
'
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo "=== Vulnerability Details ==="
|
||||||
|
jq -r "[ ${LIST_FINDINGS} ] | sort_by(-.score) | .[] | \"\\(.severity) (\\(.score)): \\(.id) — \\(.message)\"" trivy-nightly.sarif || true
|
||||||
|
echo "============================="
|
||||||
|
echo ""
|
||||||
|
|
||||||
if [ "$CRITICAL_COUNT" -gt 0 ]; then
|
if [ "$CRITICAL_COUNT" -gt 0 ]; then
|
||||||
echo "❌ Critical vulnerabilities found in nightly build (${CRITICAL_COUNT})"
|
echo "❌ Critical vulnerabilities found in nightly build (${CRITICAL_COUNT})"
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "### ❌ Critical CVEs blocking nightly"
|
||||||
|
echo '```'
|
||||||
|
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"CRITICAL\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
|
||||||
|
echo '```'
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$HIGH_COUNT" -gt 0 ]; then
|
if [ "$HIGH_COUNT" -gt 0 ]; then
|
||||||
echo "❌ High vulnerabilities found in nightly build (${HIGH_COUNT})"
|
echo "❌ High vulnerabilities found in nightly build (${HIGH_COUNT})"
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "### ❌ High CVEs blocking nightly"
|
||||||
|
echo '```'
|
||||||
|
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"HIGH\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
|
||||||
|
echo '```'
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$MEDIUM_COUNT" -gt 0 ]; then
|
if [ "$MEDIUM_COUNT" -gt 0 ]; then
|
||||||
echo "::warning::Medium vulnerabilities found in nightly build (${MEDIUM_COUNT}). Non-blocking by policy; triage with SLA per .github/security-severity-policy.yml"
|
echo "::warning::Medium vulnerabilities found in nightly build (${MEDIUM_COUNT}). Non-blocking by policy; triage with SLA per .github/security-severity-policy.yml"
|
||||||
|
{
|
||||||
|
echo ""
|
||||||
|
echo "### ⚠️ Medium CVEs (non-blocking)"
|
||||||
|
echo '```'
|
||||||
|
jq -r "[ ${LIST_FINDINGS} | select(.severity == \"MEDIUM\") ] | sort_by(-.score) | .[] | \"\\(.id) (score: \\(.score)): \\(.message)\"" trivy-nightly.sarif || true
|
||||||
|
echo '```'
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ No Critical/High vulnerabilities found"
|
echo "✅ No Critical/High vulnerabilities found"
|
||||||
|
|||||||
6
.github/workflows/pr-checklist.yml
vendored
6
.github/workflows/pr-checklist.yml
vendored
@@ -12,6 +12,10 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ inputs.pr_number || github.event.pull_request.number }}
|
group: ${{ github.workflow }}-${{ inputs.pr_number || github.event.pull_request.number }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pull-requests: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
validate:
|
validate:
|
||||||
name: Validate history-rewrite checklist (conditional)
|
name: Validate history-rewrite checklist (conditional)
|
||||||
@@ -21,7 +25,7 @@ jobs:
|
|||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
|
||||||
- name: Validate PR checklist (only for history-rewrite changes)
|
- name: Validate PR checklist (only for history-rewrite changes)
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
PR_NUMBER: ${{ inputs.pr_number }}
|
PR_NUMBER: ${{ inputs.pr_number }}
|
||||||
with:
|
with:
|
||||||
|
|||||||
13
.github/workflows/propagate-changes.yml
vendored
13
.github/workflows/propagate-changes.yml
vendored
@@ -28,15 +28,17 @@ jobs:
|
|||||||
(github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'development')
|
(github.event.workflow_run.head_branch == 'main' || github.event.workflow_run.head_branch == 'development')
|
||||||
steps:
|
steps:
|
||||||
- name: Set up Node (for github-script)
|
- name: Set up Node (for github-script)
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
- name: Propagate Changes
|
- name: Propagate Changes
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
env:
|
env:
|
||||||
CURRENT_BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }}
|
CURRENT_BRANCH: ${{ github.event.workflow_run.head_branch || github.ref_name }}
|
||||||
CURRENT_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
CURRENT_SHA: ${{ github.event.workflow_run.head_sha || github.sha }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const currentBranch = process.env.CURRENT_BRANCH || context.ref.replace('refs/heads/', '');
|
const currentBranch = process.env.CURRENT_BRANCH || context.ref.replace('refs/heads/', '');
|
||||||
@@ -133,7 +135,9 @@ jobs:
|
|||||||
|
|
||||||
const sensitive = files.some(fn => configPaths.some(sp => fn.startsWith(sp) || fn.includes(sp)));
|
const sensitive = files.some(fn => configPaths.some(sp => fn.startsWith(sp) || fn.includes(sp)));
|
||||||
if (sensitive) {
|
if (sensitive) {
|
||||||
core.info(`${src} -> ${base} contains sensitive changes (${files.join(', ')}). Skipping automatic propagation.`);
|
const preview = files.slice(0, 25).join(', ');
|
||||||
|
const suffix = files.length > 25 ? ` …(+${files.length - 25} more)` : '';
|
||||||
|
core.info(`${src} -> ${base} contains sensitive changes (${preview}${suffix}). Skipping automatic propagation.`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -203,6 +207,3 @@ jobs:
|
|||||||
await createPR('development', targetBranch);
|
await createPR('development', targetBranch);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
CHARON_TOKEN: ${{ secrets.CHARON_TOKEN }}
|
|
||||||
|
|||||||
39
.github/workflows/quality-checks.yml
vendored
39
.github/workflows/quality-checks.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
|
- nightly
|
||||||
- main
|
- main
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
@@ -15,7 +16,7 @@ permissions:
|
|||||||
checks: write
|
checks: write
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: '1.26.0'
|
GO_VERSION: '1.26.2'
|
||||||
NODE_VERSION: '24.12.0'
|
NODE_VERSION: '24.12.0'
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
|
|
||||||
@@ -30,9 +31,10 @@ jobs:
|
|||||||
ref: ${{ github.sha }}
|
ref: ${{ github.sha }}
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Run auth protection contract tests
|
- name: Run auth protection contract tests
|
||||||
@@ -136,23 +138,34 @@ jobs:
|
|||||||
} >> "$GITHUB_ENV"
|
} >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6.3.0
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Repo health check
|
- name: Repo health check
|
||||||
run: |
|
run: |
|
||||||
bash "scripts/repo_health_check.sh"
|
bash "scripts/repo_health_check.sh"
|
||||||
|
|
||||||
|
- name: Install gotestsum
|
||||||
|
run: go install gotest.tools/gotestsum@v1.13.0
|
||||||
|
|
||||||
- name: Run Go tests
|
- name: Run Go tests
|
||||||
id: go-tests
|
id: go-tests
|
||||||
working-directory: ${{ github.workspace }}
|
working-directory: ${{ github.workspace }}
|
||||||
env:
|
env:
|
||||||
CGO_ENABLED: 1
|
CGO_ENABLED: 1
|
||||||
run: |
|
run: |
|
||||||
bash "scripts/go-test-coverage.sh" 2>&1 | tee backend/test-output.txt
|
bash "scripts/go-test-coverage.sh" 2>&1 | tee backend/test-output.txt; exit "${PIPESTATUS[0]}"
|
||||||
exit "${PIPESTATUS[0]}"
|
|
||||||
|
- name: Upload test output artifact
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
|
with:
|
||||||
|
name: backend-test-output
|
||||||
|
path: backend/test-output.txt
|
||||||
|
retention-days: 7
|
||||||
|
|
||||||
- name: Go Test Summary
|
- name: Go Test Summary
|
||||||
if: always()
|
if: always()
|
||||||
@@ -229,11 +242,12 @@ jobs:
|
|||||||
PERF_MAX_MS_GETSTATUS_P95_PARALLEL: 1500ms
|
PERF_MAX_MS_GETSTATUS_P95_PARALLEL: 1500ms
|
||||||
PERF_MAX_MS_LISTDECISIONS_P95: 2000ms
|
PERF_MAX_MS_LISTDECISIONS_P95: 2000ms
|
||||||
run: |
|
run: |
|
||||||
|
go test -run TestPerf -v ./internal/api/handlers -count=1 2>&1 | tee perf-output.txt; PERF_STATUS="${PIPESTATUS[0]}"
|
||||||
{
|
{
|
||||||
echo "## 🔍 Running performance assertions (TestPerf)"
|
echo "## 🔍 Running performance assertions (TestPerf)"
|
||||||
go test -run TestPerf -v ./internal/api/handlers -count=1 | tee perf-output.txt
|
cat perf-output.txt
|
||||||
} >> "$GITHUB_STEP_SUMMARY"
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
exit "${PIPESTATUS[0]}"
|
exit "$PERF_STATUS"
|
||||||
|
|
||||||
frontend-quality:
|
frontend-quality:
|
||||||
name: Frontend (React)
|
name: Frontend (React)
|
||||||
@@ -248,12 +262,18 @@ jobs:
|
|||||||
bash "scripts/repo_health_check.sh"
|
bash "scripts/repo_health_check.sh"
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
cache: 'npm'
|
cache: 'npm'
|
||||||
cache-dependency-path: frontend/package-lock.json
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Verify lockfile integrity and audit dependencies
|
||||||
|
working-directory: frontend
|
||||||
|
run: |
|
||||||
|
npm ci --ignore-scripts
|
||||||
|
npm audit --audit-level=critical
|
||||||
|
|
||||||
- name: Check if frontend was modified in PR
|
- name: Check if frontend was modified in PR
|
||||||
id: check-frontend
|
id: check-frontend
|
||||||
run: |
|
run: |
|
||||||
@@ -295,8 +315,7 @@ jobs:
|
|||||||
id: frontend-tests
|
id: frontend-tests
|
||||||
working-directory: ${{ github.workspace }}
|
working-directory: ${{ github.workspace }}
|
||||||
run: |
|
run: |
|
||||||
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt
|
bash scripts/frontend-test-coverage.sh 2>&1 | tee frontend/test-output.txt; exit "${PIPESTATUS[0]}"
|
||||||
exit "${PIPESTATUS[0]}"
|
|
||||||
|
|
||||||
- name: Frontend Test Summary
|
- name: Frontend Test Summary
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
7
.github/workflows/rate-limit-integration.yml
vendored
7
.github/workflows/rate-limit-integration.yml
vendored
@@ -20,6 +20,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
rate-limit-integration:
|
rate-limit-integration:
|
||||||
name: Rate Limiting Integration
|
name: Rate Limiting Integration
|
||||||
@@ -31,7 +34,7 @@ jobs:
|
|||||||
- name: Build Docker image (Local)
|
- name: Build Docker image (Local)
|
||||||
run: |
|
run: |
|
||||||
echo "Building image locally for integration tests..."
|
echo "Building image locally for integration tests..."
|
||||||
docker build -t charon:local .
|
docker build -t charon:local --build-arg CI="${CI:-false}" .
|
||||||
echo "✅ Successfully built charon:local"
|
echo "✅ Successfully built charon:local"
|
||||||
|
|
||||||
- name: Run rate limit integration tests
|
- name: Run rate limit integration tests
|
||||||
@@ -68,7 +71,7 @@ jobs:
|
|||||||
|
|
||||||
echo "### Caddy Admin Config (rate_limit handlers)"
|
echo "### Caddy Admin Config (rate_limit handlers)"
|
||||||
echo '```json'
|
echo '```json'
|
||||||
curl -s http://localhost:2119/config 2>/dev/null | grep -A 20 '"handler":"rate_limit"' | head -30 || echo "Could not retrieve Caddy config"
|
curl -s http://localhost:2119/config/ 2>/dev/null | grep -A 20 '"handler":"rate_limit"' | head -30 || echo "Could not retrieve Caddy config"
|
||||||
echo '```'
|
echo '```'
|
||||||
echo ""
|
echo ""
|
||||||
|
|
||||||
|
|||||||
11
.github/workflows/release-goreleaser.yml
vendored
11
.github/workflows/release-goreleaser.yml
vendored
@@ -10,7 +10,7 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: '1.26.0'
|
GO_VERSION: '1.26.2'
|
||||||
NODE_VERSION: '24.12.0'
|
NODE_VERSION: '24.12.0'
|
||||||
GOTOOLCHAIN: auto
|
GOTOOLCHAIN: auto
|
||||||
|
|
||||||
@@ -45,13 +45,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@4b73464bb391d4059bd26b0524d20df3927bd417 # v6
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
with:
|
with:
|
||||||
go-version: ${{ env.GO_VERSION }}
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
cache-dependency-path: backend/go.sum
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
- name: Set up Node.js
|
- name: Set up Node.js
|
||||||
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
|
||||||
with:
|
with:
|
||||||
node-version: ${{ env.NODE_VERSION }}
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Install Cross-Compilation Tools (Zig)
|
- name: Install Cross-Compilation Tools (Zig)
|
||||||
# Security: Pinned to full SHA for supply chain security
|
# Security: Pinned to full SHA for supply chain security
|
||||||
uses: goto-bus-stop/setup-zig@abea47f85e598557f500fa1fd2ab7464fcb39406 # v2
|
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
|
||||||
with:
|
with:
|
||||||
version: 0.13.0
|
version: 0.13.0
|
||||||
|
|
||||||
@@ -74,7 +75,7 @@ jobs:
|
|||||||
|
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@ec59f474b9834571250b370d4735c50f8e2d1e29 # v7
|
uses: goreleaser/goreleaser-action@e24998b8b67b290c2fa8b7c14fcfa7de2c5c9b8c # v7
|
||||||
with:
|
with:
|
||||||
distribution: goreleaser
|
distribution: goreleaser
|
||||||
version: '~> v2.5'
|
version: '~> v2.5'
|
||||||
|
|||||||
10
.github/workflows/renovate.yml
vendored
10
.github/workflows/renovate.yml
vendored
@@ -14,6 +14,9 @@ permissions:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
issues: write
|
issues: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
GO_VERSION: '1.26.2'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
renovate:
|
renovate:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -24,8 +27,13 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||||
|
with:
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
- name: Run Renovate
|
- name: Run Renovate
|
||||||
uses: renovatebot/github-action@7b4b65bf31e07d4e3e51708d07700fb41bc03166 # v46.1.3
|
uses: renovatebot/github-action@83ec54fee49ab67d9cd201084c1ff325b4b462e4 # v46.1.10
|
||||||
with:
|
with:
|
||||||
configurationFile: .github/renovate.json
|
configurationFile: .github/renovate.json
|
||||||
token: ${{ secrets.RENOVATE_TOKEN || secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.RENOVATE_TOKEN || secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
2
.github/workflows/renovate_prune.yml
vendored
2
.github/workflows/renovate_prune.yml
vendored
@@ -30,7 +30,7 @@ jobs:
|
|||||||
echo "GITHUB_TOKEN=${{ secrets.CHARON_TOKEN }}" >> "$GITHUB_ENV"
|
echo "GITHUB_TOKEN=${{ secrets.CHARON_TOKEN }}" >> "$GITHUB_ENV"
|
||||||
fi
|
fi
|
||||||
- name: Prune renovate branches
|
- name: Prune renovate branches
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
github-token: ${{ env.GITHUB_TOKEN }}
|
github-token: ${{ env.GITHUB_TOKEN }}
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
5
.github/workflows/repo-health.yml
vendored
5
.github/workflows/repo-health.yml
vendored
@@ -9,6 +9,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
|
group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref_name }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
repo_health:
|
repo_health:
|
||||||
name: Repo health
|
name: Repo health
|
||||||
@@ -34,7 +37,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload health output
|
- name: Upload health output
|
||||||
if: always()
|
if: always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: repo-health-output
|
name: repo-health-output
|
||||||
path: |
|
path: |
|
||||||
|
|||||||
15
.github/workflows/security-pr.yml
vendored
15
.github/workflows/security-pr.yml
vendored
@@ -22,6 +22,9 @@ concurrency:
|
|||||||
group: security-pr-${{ github.event_name == 'workflow_run' && github.event.workflow_run.event || github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref }}
|
group: security-pr-${{ github.event_name == 'workflow_run' && github.event.workflow_run.event || github.event_name }}-${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
security-scan:
|
security-scan:
|
||||||
name: Trivy Binary Scan
|
name: Trivy Binary Scan
|
||||||
@@ -240,7 +243,7 @@ jobs:
|
|||||||
- name: Download PR image artifact
|
- name: Download PR image artifact
|
||||||
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_run' || github.event_name == 'workflow_dispatch'
|
||||||
# actions/download-artifact v4.1.8
|
# actions/download-artifact v4.1.8
|
||||||
uses: actions/download-artifact@70fc10c6e5e1ce46ad2ea6f2b72d43f7d47b13c3
|
uses: actions/download-artifact@484a0b528fb4d7bd804637ccb632e47a0e638317
|
||||||
with:
|
with:
|
||||||
name: ${{ steps.check-artifact.outputs.artifact_name }}
|
name: ${{ steps.check-artifact.outputs.artifact_name }}
|
||||||
run-id: ${{ steps.check-artifact.outputs.run_id }}
|
run-id: ${{ steps.check-artifact.outputs.run_id }}
|
||||||
@@ -361,8 +364,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Trivy filesystem scan (SARIF output)
|
- name: Run Trivy filesystem scan (SARIF output)
|
||||||
if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
|
if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
|
||||||
# aquasecurity/trivy-action v0.33.1
|
# aquasecurity/trivy-action 0.35.0
|
||||||
uses: aquasecurity/trivy-action@4c61e6329bab9be735ca35291551614bc663dff3
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
|
||||||
with:
|
with:
|
||||||
scan-type: 'fs'
|
scan-type: 'fs'
|
||||||
scan-ref: ${{ steps.extract.outputs.binary_path }}
|
scan-ref: ${{ steps.extract.outputs.binary_path }}
|
||||||
@@ -385,7 +388,7 @@ jobs:
|
|||||||
- name: Upload Trivy SARIF to GitHub Security
|
- name: Upload Trivy SARIF to GitHub Security
|
||||||
if: always() && steps.trivy-sarif-check.outputs.exists == 'true'
|
if: always() && steps.trivy-sarif-check.outputs.exists == 'true'
|
||||||
# github/codeql-action v4
|
# github/codeql-action v4
|
||||||
uses: github/codeql-action/upload-sarif@0ec47d036c68ae0cf94c629009b1029407111281
|
uses: github/codeql-action/upload-sarif@34950e1b113b30df4edee1a6d3a605242df0c40b
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-binary-results.sarif'
|
sarif_file: 'trivy-binary-results.sarif'
|
||||||
category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}
|
category: ${{ steps.pr-info.outputs.is_push == 'true' && format('security-scan-{0}', github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name) || format('security-scan-pr-{0}', steps.pr-info.outputs.pr_number) }}
|
||||||
@@ -393,8 +396,8 @@ jobs:
|
|||||||
|
|
||||||
- name: Run Trivy filesystem scan (fail on CRITICAL/HIGH)
|
- name: Run Trivy filesystem scan (fail on CRITICAL/HIGH)
|
||||||
if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
|
if: steps.check-artifact.outputs.artifact_exists == 'true' || github.event_name == 'push' || github.event_name == 'pull_request'
|
||||||
# aquasecurity/trivy-action v0.33.1
|
# aquasecurity/trivy-action 0.35.0
|
||||||
uses: aquasecurity/trivy-action@4c61e6329bab9be735ca35291551614bc663dff3
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1
|
||||||
with:
|
with:
|
||||||
scan-type: 'fs'
|
scan-type: 'fs'
|
||||||
scan-ref: ${{ steps.extract.outputs.binary_path }}
|
scan-ref: ${{ steps.extract.outputs.binary_path }}
|
||||||
|
|||||||
31
.github/workflows/security-weekly-rebuild.yml
vendored
31
.github/workflows/security-weekly-rebuild.yml
vendored
@@ -19,6 +19,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.ref }}
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REGISTRY: ghcr.io
|
REGISTRY: ghcr.io
|
||||||
IMAGE_NAME: ${{ github.repository_owner }}/charon
|
IMAGE_NAME: ${{ github.repository_owner }}/charon
|
||||||
@@ -36,16 +39,21 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
||||||
|
with:
|
||||||
|
# Explicitly fetch the current HEAD of the ref at run time, not the
|
||||||
|
# SHA that was frozen when this scheduled job was queued. Without this,
|
||||||
|
# a queued job can run days later with stale code.
|
||||||
|
ref: ${{ github.ref_name }}
|
||||||
|
|
||||||
- name: Normalize image name
|
- name: Normalize image name
|
||||||
run: |
|
run: |
|
||||||
echo "IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
echo "IMAGE_NAME=$(echo "${{ env.IMAGE_NAME }}" | tr '[:upper:]' '[:lower:]')" >> "$GITHUB_ENV"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3.7.0
|
uses: docker/setup-qemu-action@ce360397dd3f832beb865e1373c09c0e9f86d70a # v4.0.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3.12.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Resolve Debian base image digest
|
- name: Resolve Debian base image digest
|
||||||
id: base-image
|
id: base-image
|
||||||
@@ -56,7 +64,7 @@ jobs:
|
|||||||
echo "Base image digest: $DIGEST"
|
echo "Base image digest: $DIGEST"
|
||||||
|
|
||||||
- name: Log in to Container Registry
|
- name: Log in to Container Registry
|
||||||
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3.7.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ${{ env.REGISTRY }}
|
registry: ${{ env.REGISTRY }}
|
||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
@@ -64,7 +72,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Extract metadata
|
- name: Extract metadata
|
||||||
id: meta
|
id: meta
|
||||||
uses: docker/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5.10.0
|
uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6.0.0
|
||||||
with:
|
with:
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
tags: |
|
tags: |
|
||||||
@@ -72,7 +80,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Build Docker image (NO CACHE)
|
- name: Build Docker image (NO CACHE)
|
||||||
id: build
|
id: build
|
||||||
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -88,38 +96,41 @@ jobs:
|
|||||||
BASE_IMAGE=${{ steps.base-image.outputs.digest }}
|
BASE_IMAGE=${{ steps.base-image.outputs.digest }}
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner (CRITICAL+HIGH)
|
- name: Run Trivy vulnerability scanner (CRITICAL+HIGH)
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
|
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
|
||||||
format: 'table'
|
format: 'table'
|
||||||
severity: 'CRITICAL,HIGH'
|
severity: 'CRITICAL,HIGH'
|
||||||
exit-code: '1' # Fail workflow if vulnerabilities found
|
exit-code: '1' # Fail workflow if vulnerabilities found
|
||||||
|
version: 'v0.70.0'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner (SARIF)
|
- name: Run Trivy vulnerability scanner (SARIF)
|
||||||
id: trivy-sarif
|
id: trivy-sarif
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
|
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
|
||||||
format: 'sarif'
|
format: 'sarif'
|
||||||
output: 'trivy-weekly-results.sarif'
|
output: 'trivy-weekly-results.sarif'
|
||||||
severity: 'CRITICAL,HIGH,MEDIUM'
|
severity: 'CRITICAL,HIGH,MEDIUM'
|
||||||
|
version: 'v0.70.0'
|
||||||
|
|
||||||
- name: Upload Trivy results to GitHub Security
|
- name: Upload Trivy results to GitHub Security
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
sarif_file: 'trivy-weekly-results.sarif'
|
sarif_file: 'trivy-weekly-results.sarif'
|
||||||
|
|
||||||
- name: Run Trivy vulnerability scanner (JSON for artifact)
|
- name: Run Trivy vulnerability scanner (JSON for artifact)
|
||||||
uses: aquasecurity/trivy-action@e368e328979b113139d6f9068e03accaed98a518 # 0.34.1
|
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
|
||||||
with:
|
with:
|
||||||
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
|
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}
|
||||||
format: 'json'
|
format: 'json'
|
||||||
output: 'trivy-weekly-results.json'
|
output: 'trivy-weekly-results.json'
|
||||||
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
|
severity: 'CRITICAL,HIGH,MEDIUM,LOW'
|
||||||
|
version: 'v0.70.0'
|
||||||
|
|
||||||
- name: Upload Trivy JSON results
|
- name: Upload Trivy JSON results
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
|
||||||
with:
|
with:
|
||||||
name: trivy-weekly-scan-${{ github.run_number }}
|
name: trivy-weekly-scan-${{ github.run_number }}
|
||||||
path: trivy-weekly-results.json
|
path: trivy-weekly-results.json
|
||||||
|
|||||||
46
.github/workflows/supply-chain-pr.yml
vendored
46
.github/workflows/supply-chain-pr.yml
vendored
@@ -266,7 +266,7 @@ jobs:
|
|||||||
# Generate SBOM using official Anchore action (auto-updated by Renovate)
|
# Generate SBOM using official Anchore action (auto-updated by Renovate)
|
||||||
- name: Generate SBOM
|
- name: Generate SBOM
|
||||||
if: steps.set-target.outputs.image_name != ''
|
if: steps.set-target.outputs.image_name != ''
|
||||||
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
|
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||||
id: sbom
|
id: sbom
|
||||||
with:
|
with:
|
||||||
image: ${{ steps.set-target.outputs.image_name }}
|
image: ${{ steps.set-target.outputs.image_name }}
|
||||||
@@ -281,19 +281,19 @@ jobs:
|
|||||||
echo "component_count=${COMPONENT_COUNT}" >> "$GITHUB_OUTPUT"
|
echo "component_count=${COMPONENT_COUNT}" >> "$GITHUB_OUTPUT"
|
||||||
echo "✅ SBOM generated with ${COMPONENT_COUNT} components"
|
echo "✅ SBOM generated with ${COMPONENT_COUNT} components"
|
||||||
|
|
||||||
# Scan for vulnerabilities using manual Grype installation (pinned to v0.107.1)
|
# Scan for vulnerabilities using manual Grype installation (pinned to v0.110.0)
|
||||||
- name: Install Grype
|
- name: Install Grype
|
||||||
if: steps.set-target.outputs.image_name != ''
|
if: steps.set-target.outputs.image_name != ''
|
||||||
run: |
|
run: |
|
||||||
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.107.1
|
curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin v0.111.0
|
||||||
|
|
||||||
- name: Scan for vulnerabilities
|
- name: Scan for vulnerabilities
|
||||||
if: steps.set-target.outputs.image_name != ''
|
if: steps.set-target.outputs.image_name != ''
|
||||||
id: grype-scan
|
id: grype-scan
|
||||||
run: |
|
run: |
|
||||||
echo "🔍 Scanning SBOM for vulnerabilities..."
|
echo "🔍 Scanning SBOM for vulnerabilities..."
|
||||||
grype sbom:sbom.cyclonedx.json -o json > grype-results.json
|
grype sbom:sbom.cyclonedx.json --config .grype.yaml -o json > grype-results.json
|
||||||
grype sbom:sbom.cyclonedx.json -o sarif > grype-results.sarif
|
grype sbom:sbom.cyclonedx.json --config .grype.yaml -o sarif > grype-results.sarif
|
||||||
|
|
||||||
- name: Debug Output Files
|
- name: Debug Output Files
|
||||||
if: steps.set-target.outputs.image_name != ''
|
if: steps.set-target.outputs.image_name != ''
|
||||||
@@ -362,7 +362,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload SARIF to GitHub Security
|
- name: Upload SARIF to GitHub Security
|
||||||
if: steps.check-artifact.outputs.artifact_found == 'true'
|
if: steps.check-artifact.outputs.artifact_found == 'true'
|
||||||
uses: github/codeql-action/upload-sarif@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4
|
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
with:
|
with:
|
||||||
sarif_file: grype-results.sarif
|
sarif_file: grype-results.sarif
|
||||||
@@ -381,9 +381,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Comment on PR
|
- name: Comment on PR
|
||||||
if: steps.set-target.outputs.image_name != '' && steps.pr-number.outputs.is_push != 'true' && steps.pr-number.outputs.pr_number != ''
|
if: steps.set-target.outputs.image_name != '' && steps.pr-number.outputs.is_push != 'true' && steps.pr-number.outputs.pr_number != ''
|
||||||
|
continue-on-error: true
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
PR_NUMBER="${{ steps.pr-number.outputs.pr_number }}"
|
PR_NUMBER="${{ steps.pr-number.outputs.pr_number }}"
|
||||||
COMPONENT_COUNT="${{ steps.sbom-count.outputs.component_count }}"
|
COMPONENT_COUNT="${{ steps.sbom-count.outputs.component_count }}"
|
||||||
CRITICAL_COUNT="${{ steps.vuln-summary.outputs.critical_count }}"
|
CRITICAL_COUNT="${{ steps.vuln-summary.outputs.critical_count }}"
|
||||||
@@ -429,29 +432,38 @@ jobs:
|
|||||||
EOF
|
EOF
|
||||||
)
|
)
|
||||||
|
|
||||||
# Find and update existing comment or create new one
|
# Fetch existing comments — skip gracefully on 403 / permission errors
|
||||||
COMMENT_ID=$(gh api \
|
COMMENTS_JSON=""
|
||||||
|
if ! COMMENTS_JSON=$(gh api \
|
||||||
-H "Accept: application/vnd.github+json" \
|
-H "Accept: application/vnd.github+json" \
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
"/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
"/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" 2>/dev/null); then
|
||||||
--jq '.[] | select(.body | contains("Supply Chain Verification Results")) | .id' | head -1)
|
echo "⚠️ Cannot access PR comments (likely token permissions / fork / event context). Skipping PR comment."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ -n "${COMMENT_ID}" ]]; then
|
COMMENT_ID=$(echo "${COMMENTS_JSON}" | jq -r '.[] | select(.body | contains("Supply Chain Verification Results")) | .id' | head -1)
|
||||||
|
|
||||||
|
if [[ -n "${COMMENT_ID:-}" && "${COMMENT_ID}" != "null" ]]; then
|
||||||
echo "📝 Updating existing comment..."
|
echo "📝 Updating existing comment..."
|
||||||
gh api \
|
if ! gh api --method PATCH \
|
||||||
--method PATCH \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
-H "Accept: application/vnd.github+json" \
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
"/repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
|
"/repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \
|
||||||
-f body="${COMMENT_BODY}"
|
-f body="${COMMENT_BODY}"; then
|
||||||
|
echo "⚠️ Failed to update comment (permissions?). Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
echo "📝 Creating new comment..."
|
echo "📝 Creating new comment..."
|
||||||
gh api \
|
if ! gh api --method POST \
|
||||||
--method POST \
|
|
||||||
-H "Accept: application/vnd.github+json" \
|
-H "Accept: application/vnd.github+json" \
|
||||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||||
"/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
"/repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \
|
||||||
-f body="${COMMENT_BODY}"
|
-f body="${COMMENT_BODY}"; then
|
||||||
|
echo "⚠️ Failed to create comment (permissions?). Skipping."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "✅ PR comment posted"
|
echo "✅ PR comment posted"
|
||||||
|
|||||||
10
.github/workflows/supply-chain-verify.yml
vendored
10
.github/workflows/supply-chain-verify.yml
vendored
@@ -119,7 +119,7 @@ jobs:
|
|||||||
# Generate SBOM using official Anchore action (auto-updated by Renovate)
|
# Generate SBOM using official Anchore action (auto-updated by Renovate)
|
||||||
- name: Generate and Verify SBOM
|
- name: Generate and Verify SBOM
|
||||||
if: steps.image-check.outputs.exists == 'true'
|
if: steps.image-check.outputs.exists == 'true'
|
||||||
uses: anchore/sbom-action@17ae1740179002c89186b61233e0f892c3118b11 # v0.23.0
|
uses: anchore/sbom-action@e22c389904149dbc22b58101806040fa8d37a610 # v0.24.0
|
||||||
with:
|
with:
|
||||||
image: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
|
image: ghcr.io/${{ github.repository_owner }}/charon:${{ steps.tag.outputs.tag }}
|
||||||
format: cyclonedx-json
|
format: cyclonedx-json
|
||||||
@@ -144,7 +144,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload SBOM Artifact
|
- name: Upload SBOM Artifact
|
||||||
if: steps.image-check.outputs.exists == 'true' && always()
|
if: steps.image-check.outputs.exists == 'true' && always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: sbom-${{ steps.tag.outputs.tag }}
|
name: sbom-${{ steps.tag.outputs.tag }}
|
||||||
path: sbom-verify.cyclonedx.json
|
path: sbom-verify.cyclonedx.json
|
||||||
@@ -233,7 +233,7 @@ jobs:
|
|||||||
# Scan for vulnerabilities using official Anchore action (auto-updated by Renovate)
|
# Scan for vulnerabilities using official Anchore action (auto-updated by Renovate)
|
||||||
- name: Scan for Vulnerabilities
|
- name: Scan for Vulnerabilities
|
||||||
if: steps.validate-sbom.outputs.valid == 'true'
|
if: steps.validate-sbom.outputs.valid == 'true'
|
||||||
uses: anchore/scan-action@7037fa011853d5a11690026fb85feee79f4c946c # v7.3.2
|
uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0
|
||||||
id: scan
|
id: scan
|
||||||
with:
|
with:
|
||||||
sbom: sbom-verify.cyclonedx.json
|
sbom: sbom-verify.cyclonedx.json
|
||||||
@@ -324,7 +324,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Upload Vulnerability Scan Artifact
|
- name: Upload Vulnerability Scan Artifact
|
||||||
if: steps.validate-sbom.outputs.valid == 'true' && always()
|
if: steps.validate-sbom.outputs.valid == 'true' && always()
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: vulnerability-scan-${{ steps.tag.outputs.tag }}
|
name: vulnerability-scan-${{ steps.tag.outputs.tag }}
|
||||||
path: |
|
path: |
|
||||||
@@ -362,7 +362,7 @@ jobs:
|
|||||||
if: |
|
if: |
|
||||||
github.event_name == 'pull_request' ||
|
github.event_name == 'pull_request' ||
|
||||||
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
|
(github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request')
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
result-encoding: string
|
result-encoding: string
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
11
.github/workflows/update-geolite2.yml
vendored
11
.github/workflows/update-geolite2.yml
vendored
@@ -2,7 +2,7 @@ name: Update GeoLite2 Checksum
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: '0 2 * * 1' # Weekly on Mondays at 2 AM UTC
|
- cron: '0 2 * * 0' # Weekly on Sundays at 2 AM UTC
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -105,7 +105,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Create Pull Request
|
- name: Create Pull Request
|
||||||
if: steps.checksum.outputs.needs_update == 'true'
|
if: steps.checksum.outputs.needs_update == 'true'
|
||||||
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
|
uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
|
||||||
with:
|
with:
|
||||||
title: "chore(docker): update GeoLite2-Country.mmdb checksum"
|
title: "chore(docker): update GeoLite2-Country.mmdb checksum"
|
||||||
body: |
|
body: |
|
||||||
@@ -141,7 +141,8 @@ jobs:
|
|||||||
---
|
---
|
||||||
|
|
||||||
**Auto-generated by:** `.github/workflows/update-geolite2.yml`
|
**Auto-generated by:** `.github/workflows/update-geolite2.yml`
|
||||||
**Trigger:** Scheduled weekly check (Mondays 2 AM UTC)
|
- **Trigger:** Scheduled weekly check (Sundays 2 AM UTC)
|
||||||
|
base: development
|
||||||
branch: bot/update-geolite2-checksum
|
branch: bot/update-geolite2-checksum
|
||||||
delete-branch: true
|
delete-branch: true
|
||||||
commit-message: |
|
commit-message: |
|
||||||
@@ -160,7 +161,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Report failure via GitHub Issue
|
- name: Report failure via GitHub Issue
|
||||||
if: failure()
|
if: failure()
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const errorType = '${{ steps.checksum.outputs.error }}' || 'unknown';
|
const errorType = '${{ steps.checksum.outputs.error }}' || 'unknown';
|
||||||
@@ -182,7 +183,7 @@ jobs:
|
|||||||
|
|
||||||
### Workflow Details
|
### Workflow Details
|
||||||
- **Run URL:** ${runUrl}
|
- **Run URL:** ${runUrl}
|
||||||
- **Triggered:** ${context.eventName === 'schedule' ? 'Scheduled (weekly)' : 'Manual dispatch'}
|
- **Triggered:** ${context.eventName === 'schedule' ? 'Scheduled (weekly, Sundays)' : 'Manual dispatch'}
|
||||||
- **Timestamp:** ${new Date().toISOString()}
|
- **Timestamp:** ${new Date().toISOString()}
|
||||||
|
|
||||||
### Required Actions
|
### Required Actions
|
||||||
|
|||||||
5
.github/workflows/waf-integration.yml
vendored
5
.github/workflows/waf-integration.yml
vendored
@@ -20,6 +20,9 @@ concurrency:
|
|||||||
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.workflow_run.event || github.event_name }}-${{ github.event.workflow_run.head_branch || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
waf-integration:
|
waf-integration:
|
||||||
name: Coraza WAF Integration
|
name: Coraza WAF Integration
|
||||||
@@ -31,7 +34,7 @@ jobs:
|
|||||||
- name: Build Docker image (Local)
|
- name: Build Docker image (Local)
|
||||||
run: |
|
run: |
|
||||||
echo "Building image locally for integration tests..."
|
echo "Building image locally for integration tests..."
|
||||||
docker build -t charon:local .
|
docker build -t charon:local --build-arg CI="${CI:-false}" .
|
||||||
echo "✅ Successfully built charon:local"
|
echo "✅ Successfully built charon:local"
|
||||||
|
|
||||||
- name: Run WAF integration tests
|
- name: Run WAF integration tests
|
||||||
|
|||||||
16
.github/workflows/weekly-nightly-promotion.yml
vendored
16
.github/workflows/weekly-nightly-promotion.yml
vendored
@@ -47,7 +47,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Check Nightly Workflow Status
|
- name: Check Nightly Workflow Status
|
||||||
id: check
|
id: check
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const skipCheck = '${{ inputs.skip_workflow_check }}' === 'true';
|
const skipCheck = '${{ inputs.skip_workflow_check }}' === 'true';
|
||||||
@@ -200,8 +200,8 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: needs.check-nightly-health.outputs.is_healthy == 'true'
|
if: needs.check-nightly-health.outputs.is_healthy == 'true'
|
||||||
outputs:
|
outputs:
|
||||||
pr_number: ${{ steps.create-pr.outputs.pr_number }}
|
pr_number: ${{ steps.create-pr.outputs.pr_number || steps.existing-pr.outputs.pr_number }}
|
||||||
pr_url: ${{ steps.create-pr.outputs.pr_url }}
|
pr_url: ${{ steps.create-pr.outputs.pr_url || steps.existing-pr.outputs.pr_url }}
|
||||||
skipped: ${{ steps.check-diff.outputs.skipped }}
|
skipped: ${{ steps.check-diff.outputs.skipped }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
@@ -274,7 +274,7 @@ jobs:
|
|||||||
- name: Check for Existing PR
|
- name: Check for Existing PR
|
||||||
id: existing-pr
|
id: existing-pr
|
||||||
if: steps.check-diff.outputs.skipped != 'true'
|
if: steps.check-diff.outputs.skipped != 'true'
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const { data: pulls } = await github.rest.pulls.list({
|
const { data: pulls } = await github.rest.pulls.list({
|
||||||
@@ -297,7 +297,7 @@ jobs:
|
|||||||
- name: Create Promotion PR
|
- name: Create Promotion PR
|
||||||
id: create-pr
|
id: create-pr
|
||||||
if: steps.check-diff.outputs.skipped != 'true' && steps.existing-pr.outputs.exists != 'true'
|
if: steps.check-diff.outputs.skipped != 'true' && steps.existing-pr.outputs.exists != 'true'
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
@@ -399,7 +399,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Update Existing PR
|
- name: Update Existing PR
|
||||||
if: steps.check-diff.outputs.skipped != 'true' && steps.existing-pr.outputs.exists == 'true'
|
if: steps.check-diff.outputs.skipped != 'true' && steps.existing-pr.outputs.exists == 'true'
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const prNumber = ${{ steps.existing-pr.outputs.pr_number }};
|
const prNumber = ${{ steps.existing-pr.outputs.pr_number }};
|
||||||
@@ -425,7 +425,7 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Dispatch missing required workflows on nightly head
|
- name: Dispatch missing required workflows on nightly head
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const owner = context.repo.owner;
|
const owner = context.repo.owner;
|
||||||
@@ -483,7 +483,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Create Failure Issue
|
- name: Create Failure Issue
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const isHealthy = '${{ needs.check-nightly-health.outputs.is_healthy }}';
|
const isHealthy = '${{ needs.check-nightly-health.outputs.is_healthy }}';
|
||||||
|
|||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -78,6 +78,11 @@ backend/node_modules/
|
|||||||
backend/package.json
|
backend/package.json
|
||||||
backend/package-lock.json
|
backend/package-lock.json
|
||||||
|
|
||||||
|
# Root-level artifact files (non-documentation)
|
||||||
|
FIREFOX_E2E_FIXES_SUMMARY.md
|
||||||
|
verify-security-state-for-ui-tests
|
||||||
|
categories.txt
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Databases
|
# Databases
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -297,6 +302,7 @@ docs/plans/current_spec_notes.md
|
|||||||
tests/etc/passwd
|
tests/etc/passwd
|
||||||
trivy-image-report.json
|
trivy-image-report.json
|
||||||
trivy-fs-report.json
|
trivy-fs-report.json
|
||||||
|
trivy-report.json
|
||||||
backend/# Tools Configuration.md
|
backend/# Tools Configuration.md
|
||||||
docs/plans/requirements.md
|
docs/plans/requirements.md
|
||||||
docs/plans/design.md
|
docs/plans/design.md
|
||||||
@@ -308,3 +314,12 @@ validation-evidence/**
|
|||||||
.github/agents/# Tools Configuration.md
|
.github/agents/# Tools Configuration.md
|
||||||
docs/reports/codecove_patch_report.md
|
docs/reports/codecove_patch_report.md
|
||||||
vuln-results.json
|
vuln-results.json
|
||||||
|
test_output.txt
|
||||||
|
coverage_results.txt
|
||||||
|
final-results.json
|
||||||
|
new-results.json
|
||||||
|
scan_output.json
|
||||||
|
coverage_output.txt
|
||||||
|
frontend/lint_output.txt
|
||||||
|
lefthook_out.txt
|
||||||
|
backend/test_out.txt
|
||||||
|
|||||||
698
.grype.yaml
698
.grype.yaml
@@ -4,136 +4,618 @@
|
|||||||
# Documentation: https://github.com/anchore/grype#specifying-matches-to-ignore
|
# Documentation: https://github.com/anchore/grype#specifying-matches-to-ignore
|
||||||
|
|
||||||
ignore:
|
ignore:
|
||||||
# CVE-2026-22184: zlib Global Buffer Overflow in untgz utility
|
# CVE-2026-2673: OpenSSL TLS 1.3 server key exchange group downgrade
|
||||||
# Severity: CRITICAL
|
# Severity: HIGH (CVSS 7.5)
|
||||||
# Package: zlib 1.3.1-r2 (Alpine Linux base image)
|
# Packages: libcrypto3 3.5.5-r0 and libssl3 3.5.5-r0 (Alpine apk)
|
||||||
# Status: No upstream fix available as of 2026-01-16
|
# Status: No upstream fix available — Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18
|
||||||
#
|
#
|
||||||
# Vulnerability Details:
|
# Vulnerability Details:
|
||||||
# - Global buffer overflow in TGZfname() function
|
# - When DEFAULT is in the TLS 1.3 group configuration, the OpenSSL server may select
|
||||||
# - Unbounded strcpy() allows attacker-controlled archive names
|
# a weaker key exchange group than preferred, enabling a limited key exchange downgrade.
|
||||||
# - Can lead to memory corruption, DoS, potential RCE
|
# - Only affects systems acting as a raw TLS 1.3 server using OpenSSL's server-side group negotiation.
|
||||||
#
|
#
|
||||||
# Risk Assessment: ACCEPTED (Low exploitability in Charon context)
|
# Root Cause (No Fix Available):
|
||||||
# - Charon does not use untgz utility directly
|
# - Alpine upstream has not published a patched libcrypto3/libssl3 for Alpine 3.23.
|
||||||
# - No untrusted tar archive processing in application code
|
# - Checked: Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18.
|
||||||
# - Attack surface limited to OS-level utilities
|
# - Fix path: once Alpine publishes a patched libcrypto3/libssl3, rebuild the Docker image
|
||||||
# - Multiple layers of containerization and isolation
|
# and remove this suppression.
|
||||||
#
|
#
|
||||||
# Mitigation:
|
# Risk Assessment: ACCEPTED (No upstream fix; limited exposure in Charon context)
|
||||||
# - Monitor Alpine Linux security feed daily for zlib patches
|
# - Charon terminates TLS at the Caddy layer — the Go backend does not act as a raw TLS 1.3 server.
|
||||||
# - Container runs with minimal privileges (no-new-privileges)
|
# - The vulnerability requires the affected application to directly configure TLS 1.3 server
|
||||||
# - Read-only filesystem where possible
|
# group negotiation via OpenSSL, which Charon does not do.
|
||||||
# - Network isolation via Docker networks
|
|
||||||
#
|
|
||||||
# Review:
|
|
||||||
# - Daily checks for Alpine security updates
|
|
||||||
# - Automatic re-scan via CI/CD on every commit
|
|
||||||
# - Manual review scheduled for 2026-01-23 (7 days)
|
|
||||||
#
|
|
||||||
# Removal Criteria:
|
|
||||||
# - Alpine releases zlib 1.3.1-r3 or higher with CVE fix
|
|
||||||
# - OR upstream zlib project releases patched version
|
|
||||||
# - Remove this suppression immediately after fix available
|
|
||||||
#
|
|
||||||
# References:
|
|
||||||
# - CVE: https://nvd.nist.gov/vuln/detail/CVE-2026-22184
|
|
||||||
# - Alpine Security: https://security.alpinelinux.org/
|
|
||||||
# - GitHub Issue: https://github.com/Wikid82/Charon/issues/TBD
|
|
||||||
- vulnerability: CVE-2026-22184
|
|
||||||
package:
|
|
||||||
name: zlib
|
|
||||||
version: "1.3.1-r2"
|
|
||||||
type: apk # Alpine package
|
|
||||||
reason: |
|
|
||||||
CRITICAL buffer overflow in untgz utility. No fix available from Alpine
|
|
||||||
as of 2026-01-16. Risk accepted: Charon does not directly use untgz or
|
|
||||||
process untrusted tar archives. Attack surface limited to base OS utilities.
|
|
||||||
Monitoring Alpine security feed for upstream patch.
|
|
||||||
expiry: "2026-01-23" # Re-evaluate in 7 days
|
|
||||||
|
|
||||||
# Action items when this suppression expires:
|
|
||||||
# 1. Check Alpine security feed: https://security.alpinelinux.org/
|
|
||||||
# 2. Check zlib releases: https://github.com/madler/zlib/releases
|
|
||||||
# 3. If fix available: Update Dockerfile, rebuild, remove suppression
|
|
||||||
# 4. If no fix: Extend expiry by 7 days, document justification
|
|
||||||
# 5. If extended 3+ times: Escalate to security team for review
|
|
||||||
|
|
||||||
# GHSA-69x3-g4r3-p962 / CVE-2026-25793: Nebula ECDSA Signature Malleability
|
|
||||||
# Severity: HIGH (CVSS 8.1)
|
|
||||||
# Package: github.com/slackhq/nebula v1.9.7 (embedded in /usr/bin/caddy)
|
|
||||||
# Status: Cannot upgrade — smallstep/certificates v0.30.0-rc2 still pins nebula v1.9.x
|
|
||||||
#
|
|
||||||
# Vulnerability Details:
|
|
||||||
# - ECDSA signature malleability allows bypassing certificate blocklists
|
|
||||||
# - Attacker can forge alternate valid P256 ECDSA signatures for revoked
|
|
||||||
# certificates (CVSSv3: AV:N/AC:H/PR:L/UI:N/S:U/C:H/I:H/A:N)
|
|
||||||
# - Only affects configurations using Nebula-based certificate authorities
|
|
||||||
# (non-default and uncommon in Charon deployments)
|
|
||||||
#
|
|
||||||
# Root Cause (Compile-Time Dependency Lock):
|
|
||||||
# - Caddy is built with caddy-security plugin, which transitively requires
|
|
||||||
# github.com/smallstep/certificates. That package pins nebula v1.9.x.
|
|
||||||
# - Checked: smallstep/certificates v0.27.5 → v0.30.0-rc2 all require nebula v1.9.4–v1.9.7.
|
|
||||||
# The nebula v1.10 API removal breaks compilation in the
|
|
||||||
# authority/provisioner package; xcaddy build fails with upgrade attempted.
|
|
||||||
# - Dockerfile caddy-builder stage pins nebula@v1.9.7 (Renovate tracked) with
|
|
||||||
# an inline comment explaining the constraint (Dockerfile line 247).
|
|
||||||
# - Fix path: once smallstep/certificates releases a version requiring
|
|
||||||
# nebula v1.10+, remove the pin and this suppression simultaneously.
|
|
||||||
#
|
|
||||||
# Risk Assessment: ACCEPTED (Low exploitability in Charon context)
|
|
||||||
# - Charon uses standard ACME/Let's Encrypt TLS; Nebula VPN PKI is not
|
|
||||||
# enabled by default and rarely configured in Charon deployments.
|
|
||||||
# - Exploiting this requires a valid certificate sharing the same issuer as
|
|
||||||
# a revoked one — an uncommon and targeted attack scenario.
|
|
||||||
# - Container-level isolation reduces the attack surface further.
|
# - Container-level isolation reduces the attack surface further.
|
||||||
#
|
#
|
||||||
# Mitigation (active while suppression is in effect):
|
# Mitigation (active while suppression is in effect):
|
||||||
# - Monitor smallstep/certificates releases at https://github.com/smallstep/certificates/releases
|
# - Monitor Alpine security advisories: https://security.alpinelinux.org/vuln/CVE-2026-2673
|
||||||
# - Weekly CI security rebuild flags any new CVEs in the full image.
|
# - Weekly CI security rebuild (security-weekly-rebuild.yml) flags any new CVEs in the full image.
|
||||||
# - Renovate annotation in Dockerfile (datasource=go depName=github.com/slackhq/nebula)
|
|
||||||
# will surface the pin for review when xcaddy build becomes compatible.
|
|
||||||
#
|
#
|
||||||
# Review:
|
# Review:
|
||||||
# - Reviewed 2026-02-19: smallstep/certificates latest stable remains v0.27.5;
|
# - Reviewed 2026-03-18 (initial suppression): no upstream fix available. Set 30-day review.
|
||||||
# no release requiring nebula v1.10+ has shipped. Suppression extended 14 days.
|
# - Extended 2026-04-04: Alpine 3.23 still ships 3.5.5-r0. No upstream fix available.
|
||||||
# - Next review: 2026-03-05. Remove suppression immediately once upstream fixes.
|
# - Next review: 2026-05-18. Remove suppression immediately once upstream fixes.
|
||||||
#
|
#
|
||||||
# Removal Criteria:
|
# Removal Criteria:
|
||||||
# - smallstep/certificates releases a stable version requiring nebula v1.10+
|
# - Alpine publishes a patched version of libcrypto3 and libssl3
|
||||||
# - Update Dockerfile caddy-builder patch to use the new versions
|
# - Rebuild Docker image and verify CVE-2026-2673 no longer appears in grype-results.json
|
||||||
# - Rebuild image, run security scan, confirm suppression no longer needed
|
# - Remove both these entries and the corresponding .trivyignore entry simultaneously
|
||||||
# - Remove both this entry and the corresponding .trivyignore entry
|
|
||||||
#
|
#
|
||||||
# References:
|
# References:
|
||||||
# - GHSA: https://github.com/advisories/GHSA-69x3-g4r3-p962
|
# - CVE-2026-2673: https://nvd.nist.gov/vuln/detail/CVE-2026-2673
|
||||||
|
# - Alpine security tracker: https://security.alpinelinux.org/vuln/CVE-2026-2673
|
||||||
|
- vulnerability: CVE-2026-2673
|
||||||
|
package:
|
||||||
|
name: libcrypto3
|
||||||
|
version: "3.5.5-r0"
|
||||||
|
type: apk
|
||||||
|
reason: |
|
||||||
|
HIGH — OpenSSL TLS 1.3 server key exchange group downgrade in libcrypto3 3.5.5-r0 (Alpine base image).
|
||||||
|
No upstream fix: Alpine 3.23 still ships libcrypto3 3.5.5-r0 as of 2026-03-18. Charon
|
||||||
|
terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS 1.3 server.
|
||||||
|
Risk accepted pending Alpine upstream patch.
|
||||||
|
expiry: "2026-05-18" # Extended 2026-04-04: Alpine 3.23 still ships 3.5.5-r0. Next review 2026-05-18.
|
||||||
|
|
||||||
|
# Action items when this suppression expires:
|
||||||
|
# 1. Check Alpine security tracker: https://security.alpinelinux.org/vuln/CVE-2026-2673
|
||||||
|
# 2. If a patched Alpine package is now available:
|
||||||
|
# a. Rebuild Docker image without suppression
|
||||||
|
# b. Run local security-scan-docker-image and confirm CVE is resolved
|
||||||
|
# c. Remove this suppression entry, the libssl3 entry below, and the .trivyignore entry
|
||||||
|
# 3. If no fix yet: Extend expiry by 14–30 days and update the review comment above
|
||||||
|
# 4. If extended 3+ times: Open an issue to track the upstream status formally
|
||||||
|
|
||||||
|
# CVE-2026-2673 (libssl3) — see full justification in the libcrypto3 entry above
|
||||||
|
- vulnerability: CVE-2026-2673
|
||||||
|
package:
|
||||||
|
name: libssl3
|
||||||
|
version: "3.5.5-r0"
|
||||||
|
type: apk
|
||||||
|
reason: |
|
||||||
|
HIGH — OpenSSL TLS 1.3 server key exchange group downgrade in libssl3 3.5.5-r0 (Alpine base image).
|
||||||
|
No upstream fix: Alpine 3.23 still ships libssl3 3.5.5-r0 as of 2026-03-18. Charon
|
||||||
|
terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS 1.3 server.
|
||||||
|
Risk accepted pending Alpine upstream patch.
|
||||||
|
expiry: "2026-05-18" # Extended 2026-04-04: see libcrypto3 entry above for action items.
|
||||||
|
|
||||||
|
# CVE-2026-31790: OpenSSL vulnerability in Alpine base image packages
|
||||||
|
# Severity: HIGH
|
||||||
|
# Packages: libcrypto3 3.5.5-r0 and libssl3 3.5.5-r0 (Alpine apk)
|
||||||
|
# Status: No upstream fix available — Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-04-09
|
||||||
|
#
|
||||||
|
# Root Cause (No Fix Available):
|
||||||
|
# - Alpine upstream has not published a patched libcrypto3/libssl3 for Alpine 3.23.
|
||||||
|
# - Checked: Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-04-09.
|
||||||
|
# - Fix path: once Alpine publishes a patched libcrypto3/libssl3, rebuild the Docker image
|
||||||
|
# and remove this suppression.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (No upstream fix; documented in SECURITY.md)
|
||||||
|
# - Charon terminates TLS at the Caddy layer — the Go backend does not act as a raw TLS server.
|
||||||
|
# - Container-level isolation reduces the attack surface further.
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor Alpine security advisories: https://security.alpinelinux.org/vuln/CVE-2026-31790
|
||||||
|
# - Weekly CI security rebuild (security-weekly-rebuild.yml) flags any new CVEs in the full image.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-04-09 (initial suppression): no upstream fix available. Set 30-day review.
|
||||||
|
# - Next review: 2026-05-09. Remove suppression immediately once upstream fixes.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - Alpine publishes a patched version of libcrypto3 and libssl3
|
||||||
|
# - Rebuild Docker image and verify CVE-2026-31790 no longer appears in grype-results.json
|
||||||
|
# - Remove both these entries and the corresponding .trivyignore entry simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - CVE-2026-31790: https://nvd.nist.gov/vuln/detail/CVE-2026-31790
|
||||||
|
# - Alpine security tracker: https://security.alpinelinux.org/vuln/CVE-2026-31790
|
||||||
|
- vulnerability: CVE-2026-31790
|
||||||
|
package:
|
||||||
|
name: libcrypto3
|
||||||
|
version: "3.5.5-r0"
|
||||||
|
type: apk
|
||||||
|
reason: |
|
||||||
|
HIGH — OpenSSL vulnerability in libcrypto3 3.5.5-r0 (Alpine base image).
|
||||||
|
No upstream fix: Alpine 3.23 still ships libcrypto3 3.5.5-r0 as of 2026-04-09. Charon
|
||||||
|
terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS server.
|
||||||
|
Risk accepted pending Alpine upstream patch. Documented in SECURITY.md.
|
||||||
|
expiry: "2026-05-09" # Reviewed 2026-04-09: no upstream fix available. Next review 2026-05-09.
|
||||||
|
|
||||||
|
# Action items when this suppression expires:
|
||||||
|
# 1. Check Alpine security tracker: https://security.alpinelinux.org/vuln/CVE-2026-31790
|
||||||
|
# 2. If a patched Alpine package is now available:
|
||||||
|
# a. Rebuild Docker image without suppression
|
||||||
|
# b. Run local security-scan-docker-image and confirm CVE is resolved
|
||||||
|
# c. Remove this suppression entry, the libssl3 entry below, and the .trivyignore entry
|
||||||
|
# 3. If no fix yet: Extend expiry by 14–30 days and update the review comment above
|
||||||
|
# 4. If extended 3+ times: Open an issue to track the upstream status formally
|
||||||
|
|
||||||
|
# CVE-2026-31790 (libssl3) — see full justification in the libcrypto3 entry above
|
||||||
|
- vulnerability: CVE-2026-31790
|
||||||
|
package:
|
||||||
|
name: libssl3
|
||||||
|
version: "3.5.5-r0"
|
||||||
|
type: apk
|
||||||
|
reason: |
|
||||||
|
HIGH — OpenSSL vulnerability in libssl3 3.5.5-r0 (Alpine base image).
|
||||||
|
No upstream fix: Alpine 3.23 still ships libssl3 3.5.5-r0 as of 2026-04-09. Charon
|
||||||
|
terminates TLS at the Caddy layer; the Go backend does not act as a raw TLS server.
|
||||||
|
Risk accepted pending Alpine upstream patch. Documented in SECURITY.md.
|
||||||
|
expiry: "2026-05-09" # Reviewed 2026-04-09: see libcrypto3 entry above for action items.
|
||||||
|
|
||||||
|
# GHSA-69x3-g4r3-p962 / CVE-2026-25793: Nebula ECDSA Signature Malleability
|
||||||
|
# Severity: HIGH (CVSS 8.1)
|
||||||
|
# Package: github.com/slackhq/nebula v1.9.7 (embedded in /usr/bin/caddy via smallstep/certificates)
|
||||||
|
# Status: Fix exists in nebula v1.10.3 — smallstep/certificates cannot compile against v1.10+ APIs
|
||||||
|
#
|
||||||
|
# Vulnerability Details:
|
||||||
|
# - ECDSA signature malleability in nebula allows potential authentication bypass via
|
||||||
|
# crafted certificate signatures (CWE-347).
|
||||||
|
# - CVSSv3: AV:N/AC:H/PR:N/UI:N/S:U/C:H/I:H/A:N (CVSS 8.1)
|
||||||
|
#
|
||||||
|
# Root Cause (Third-Party Binary + Upstream API Incompatibility):
|
||||||
|
# - Charon does not use nebula directly. The library is compiled into the Caddy binary
|
||||||
|
# via the caddy-security plugin → smallstep/certificates dependency chain.
|
||||||
|
# - Nebula v1.10.3 patches the vulnerability but removes legacy APIs that
|
||||||
|
# smallstep/certificates (through v0.30.2) depends on, causing compile failures.
|
||||||
|
# - Fix path: once smallstep/certificates releases a version compatible with nebula >= v1.10.3,
|
||||||
|
# update the Dockerfile and remove this suppression.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (No direct use + upstream API incompatibility blocks fix)
|
||||||
|
# - Charon does not use Nebula VPN PKI by default. The vulnerable code path is only
|
||||||
|
# reachable if Nebula-based certificate provisioning is explicitly configured.
|
||||||
|
# - The attack requires network access and a crafted certificate, which is not part of
|
||||||
|
# standard Charon deployment.
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor smallstep/certificates releases: https://github.com/smallstep/certificates/releases
|
||||||
|
# - Monitor nebula releases: https://github.com/slackhq/nebula/releases
|
||||||
|
# - Weekly CI security rebuild flags the moment a compatible upstream ships.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-02-19 (initial suppression in .trivyignore): certificates v0.27.5 pins nebula v1.9.x.
|
||||||
|
# - Re-evaluated 2026-04-10: nebula v1.10.3 has the fix but certificates (through v0.30.2)
|
||||||
|
# uses legacy APIs removed in v1.10+. Still blocked. Set 30-day review.
|
||||||
|
# - Next review: 2026-05-10. Remove suppression once certificates ships with nebula >= v1.10.3.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - smallstep/certificates releases a version compatible with nebula >= v1.10.3
|
||||||
|
# - Update Dockerfile nebula pin, rebuild, run security-scan-docker-image, confirm resolved
|
||||||
|
# - Remove this entry and the corresponding .trivyignore entry simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - GHSA-69x3-g4r3-p962: https://github.com/advisories/GHSA-69x3-g4r3-p962
|
||||||
# - CVE-2026-25793: https://nvd.nist.gov/vuln/detail/CVE-2026-25793
|
# - CVE-2026-25793: https://nvd.nist.gov/vuln/detail/CVE-2026-25793
|
||||||
# - smallstep/certificates: https://github.com/smallstep/certificates/releases
|
# - Nebula releases: https://github.com/slackhq/nebula/releases
|
||||||
# - Dockerfile pin: caddy-builder stage, line ~247 (go get nebula@v1.9.7)
|
# - smallstep/certificates releases: https://github.com/smallstep/certificates/releases
|
||||||
- vulnerability: GHSA-69x3-g4r3-p962
|
- vulnerability: CVE-2026-25793
|
||||||
package:
|
package:
|
||||||
name: github.com/slackhq/nebula
|
name: github.com/slackhq/nebula
|
||||||
version: "v1.9.7"
|
version: "v1.9.7"
|
||||||
type: go-module
|
type: go-module
|
||||||
reason: |
|
reason: |
|
||||||
HIGH — ECDSA signature malleability in nebula v1.9.7 embedded in /usr/bin/caddy.
|
HIGH — ECDSA signature malleability in nebula v1.9.7 embedded in /usr/bin/caddy.
|
||||||
Cannot upgrade: smallstep/certificates v0.27.5 (latest stable as of 2026-02-19)
|
Fix exists in nebula v1.10.3 but smallstep/certificates (through v0.30.2) uses legacy APIs
|
||||||
still requires nebula v1.9.x (verified across v0.27.5–v0.30.0-rc2). Charon does
|
removed in v1.10+, causing compile failures. Charon does not use Nebula VPN PKI by default.
|
||||||
not use Nebula VPN PKI by default. Risk accepted pending upstream smallstep fix.
|
Risk accepted; no remediation until smallstep/certificates ships with nebula >= v1.10.3.
|
||||||
Reviewed 2026-02-19: no new smallstep release changes this assessment.
|
Re-evaluated 2026-04-10: still blocked by upstream API incompatibility.
|
||||||
expiry: "2026-03-05" # Re-evaluate in 14 days (2026-02-19 + 14 days)
|
expiry: "2026-05-10" # Re-evaluated 2026-04-10: certificates through v0.30.2 incompatible with nebula v1.10+.
|
||||||
|
|
||||||
|
# GHSA-6g7g-w4f8-9c9x: buger/jsonparser Delete panic on malformed JSON (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5)
|
||||||
|
# Package: github.com/buger/jsonparser v1.1.1 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
|
||||||
|
# Status: UPSTREAM FIX EXISTS (v1.1.2 released 2026-03-20) — awaiting CrowdSec to update dependency
|
||||||
|
# NOTE: As of 2026-04-20, grype v0.111.0 with fresh DB no longer flags this finding in the image.
|
||||||
|
# This suppression is retained as a safety net in case future DB updates re-surface it.
|
||||||
|
#
|
||||||
|
# Vulnerability Details:
|
||||||
|
# - The Delete function fails to validate offsets on malformed JSON input, producing a
|
||||||
|
# negative slice index and a runtime panic — denial of service (CWE-125).
|
||||||
|
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
|
||||||
|
#
|
||||||
|
# Root Cause (Third-Party Binary — Fix Exists Upstream, Not Yet in CrowdSec):
|
||||||
|
# - Charon does not use buger/jsonparser directly. It is compiled into CrowdSec binaries.
|
||||||
|
# - buger/jsonparser released v1.1.2 on 2026-03-20 fixing issue #275.
|
||||||
|
# - CrowdSec has not yet released a version built with buger/jsonparser v1.1.2.
|
||||||
|
# - Fix path: once CrowdSec updates their dependency and rebuilds, rebuild the Docker image
|
||||||
|
# and remove this suppression.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (Limited exploitability; fix exists upstream but not yet in CrowdSec)
|
||||||
|
# - The DoS vector requires passing malformed JSON to the vulnerable Delete function within
|
||||||
|
# CrowdSec's internal processing pipeline; this is not a direct attack surface in Charon.
|
||||||
|
# - CrowdSec's exposed surface is its HTTP API (not raw JSON stream parsing via this path).
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor CrowdSec releases for a build using buger/jsonparser >= v1.1.2.
|
||||||
|
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
# - Weekly CI security rebuild flags the moment a fixed image ships.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-03-19 (initial suppression): no upstream fix. Set 30-day review.
|
||||||
|
# - Extended 2026-04-04: no upstream fix. buger/jsonparser issue #275 still open.
|
||||||
|
# - Updated 2026-04-20: buger/jsonparser v1.1.2 released 2026-03-20. CrowdSec not yet updated.
|
||||||
|
# Grype v0.111.0 with fresh DB (2026-04-20) no longer flags this finding. Suppression retained
|
||||||
|
# as a safety net. Next review: 2026-05-19 — remove if CrowdSec ships with v1.1.2+.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - CrowdSec releases a version built with buger/jsonparser >= v1.1.2
|
||||||
|
# - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved
|
||||||
|
# - Remove this entry and the corresponding .trivyignore entry simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - GHSA-6g7g-w4f8-9c9x: https://github.com/advisories/GHSA-6g7g-w4f8-9c9x
|
||||||
|
# - Upstream fix: https://github.com/buger/jsonparser/releases/tag/v1.1.2
|
||||||
|
# - golang/vulndb: https://github.com/golang/vulndb/issues/4514
|
||||||
|
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
- vulnerability: GHSA-6g7g-w4f8-9c9x
|
||||||
|
package:
|
||||||
|
name: github.com/buger/jsonparser
|
||||||
|
version: "v1.1.1"
|
||||||
|
type: go-module
|
||||||
|
reason: |
|
||||||
|
HIGH — DoS panic via malformed JSON in buger/jsonparser v1.1.1 embedded in CrowdSec binaries.
|
||||||
|
Upstream fix: buger/jsonparser v1.1.2 released 2026-03-20; CrowdSec has not yet updated their
|
||||||
|
dependency. Grype no longer flags this as of 2026-04-20 (fresh DB). Suppression retained as
|
||||||
|
safety net pending CrowdSec update. Charon does not use this package directly.
|
||||||
|
Updated 2026-04-20: fix v1.1.2 exists upstream; awaiting CrowdSec dependency update.
|
||||||
|
expiry: "2026-05-19" # Review 2026-05-19: remove if CrowdSec ships with buger/jsonparser >= v1.1.2.
|
||||||
|
|
||||||
# Action items when this suppression expires:
|
# Action items when this suppression expires:
|
||||||
# 1. Check smallstep/certificates releases: https://github.com/smallstep/certificates/releases
|
# 1. Check if CrowdSec has released a version with buger/jsonparser >= v1.1.2:
|
||||||
# 2. If a stable version requires nebula v1.10+:
|
# https://github.com/crowdsecurity/crowdsec/releases
|
||||||
# a. Update Dockerfile caddy-builder: remove the `go get nebula@v1.9.7` pin
|
# 2. If CrowdSec has updated: rebuild Docker image, run security-scan-docker-image,
|
||||||
# b. Optionally bump smallstep/certificates to the new version
|
# and remove this suppression entry and the corresponding .trivyignore entry
|
||||||
# c. Rebuild Docker image and verify no compile failures
|
# 3. If grype still does not flag it with fresh DB: consider removing the suppression as
|
||||||
# d. Re-run local security-scan-docker-image and confirm clean result
|
# it may no longer be necessary
|
||||||
# e. Remove this suppression entry
|
# 4. If no CrowdSec update yet: Extend expiry by 30 days
|
||||||
# 3. If no fix yet: Extend expiry by 14 days and document justification
|
|
||||||
# 4. If extended 3+ times: Open upstream issue on smallstep/certificates
|
# GHSA-jqcq-xjh3-6g23: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5)
|
||||||
|
# Package: github.com/jackc/pgproto3/v2 v2.3.3 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
|
||||||
|
# Status: NO fix in pgproto3/v2 (archived/EOL) — fix path requires CrowdSec to migrate to pgx/v5
|
||||||
|
#
|
||||||
|
# Vulnerability Details:
|
||||||
|
# - DataRow.Decode does not validate field lengths; a malicious or compromised PostgreSQL server
|
||||||
|
# can send a negative field length causing a slice-bounds panic — denial of service (CWE-129).
|
||||||
|
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H
|
||||||
|
#
|
||||||
|
# Root Cause (EOL Module + Third-Party Binary):
|
||||||
|
# - Charon does not use pgproto3/v2 directly nor communicate with PostgreSQL. The package
|
||||||
|
# is compiled into CrowdSec binaries for their internal database communication.
|
||||||
|
# - The pgproto3/v2 module is archived and EOL; no fix will be released. The fix path
|
||||||
|
# is migration to pgx/v5, which embeds an updated pgproto3/v3.
|
||||||
|
# - Fix path: once CrowdSec migrates to pgx/v5 and releases an updated binary, rebuild
|
||||||
|
# the Docker image and remove this suppression.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (Non-exploitable in Charon context + no upstream fix path)
|
||||||
|
# - The vulnerability requires a malicious PostgreSQL server response. Charon uses SQLite
|
||||||
|
# internally and does not run PostgreSQL. CrowdSec's database path is not exposed to
|
||||||
|
# external traffic in a standard Charon deployment.
|
||||||
|
# - The attack requires a compromised database server, which would imply full host compromise.
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor CrowdSec releases for pgx/v5 migration:
|
||||||
|
# https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
# - Weekly CI security rebuild flags the moment a fixed image ships.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-03-19 (initial suppression): pgproto3/v2 is EOL; no fix exists or will exist.
|
||||||
|
# Waiting on CrowdSec to migrate to pgx/v5. Set 30-day review.
|
||||||
|
# - Extended 2026-04-04: CrowdSec has not migrated to pgx/v5 yet.
|
||||||
|
# - Next review: 2026-05-19. Remove suppression once CrowdSec ships with pgx/v5.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - CrowdSec releases a version with pgx/v5 (pgproto3/v3) replacing pgproto3/v2
|
||||||
|
# - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved
|
||||||
|
# - Remove this entry and the corresponding .trivyignore entry simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - GHSA-jqcq-xjh3-6g23: https://github.com/advisories/GHSA-jqcq-xjh3-6g23
|
||||||
|
# - pgproto3/v2 archive notice: https://github.com/jackc/pgproto3
|
||||||
|
# - pgx/v5 (replacement): https://github.com/jackc/pgx
|
||||||
|
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
- vulnerability: GHSA-jqcq-xjh3-6g23
|
||||||
|
package:
|
||||||
|
name: github.com/jackc/pgproto3/v2
|
||||||
|
version: "v2.3.3"
|
||||||
|
type: go-module
|
||||||
|
reason: |
|
||||||
|
HIGH — DoS panic via negative field length in pgproto3/v2 v2.3.3 embedded in CrowdSec binaries.
|
||||||
|
pgproto3/v2 is archived/EOL with no fix planned; fix path requires CrowdSec to migrate to pgx/v5.
|
||||||
|
Charon uses SQLite, not PostgreSQL; this code path is not reachable in a standard deployment.
|
||||||
|
Risk accepted; no remediation until CrowdSec ships with pgx/v5.
|
||||||
|
Reviewed 2026-03-19: pgproto3/v2 EOL confirmed; CrowdSec has not migrated to pgx/v5 yet.
|
||||||
|
expiry: "2026-05-19" # Extended 2026-04-04: no fix path until CrowdSec migrates to pgx/v5.
|
||||||
|
|
||||||
|
# Action items when this suppression expires:
|
||||||
|
# 1. Check CrowdSec releases for pgx/v5 migration:
|
||||||
|
# https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
# 2. Verify with: `go version -m /path/to/crowdsec | grep pgproto3`
|
||||||
|
# Expected: pgproto3/v3 (or no pgproto3 reference if fully replaced)
|
||||||
|
# 3. If CrowdSec has migrated:
|
||||||
|
# a. Rebuild Docker image and run local security-scan-docker-image
|
||||||
|
# b. Remove this suppression entry and the corresponding .trivyignore entry
|
||||||
|
# 4. If not yet migrated: Extend expiry by 30 days and update the review comment above
|
||||||
|
# 5. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec requesting pgx/v5 migration
|
||||||
|
|
||||||
|
# GHSA-x6gf-mpr2-68h6 / CVE-2026-4427: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5)
|
||||||
|
# Package: github.com/jackc/pgproto3/v2 v2.3.3 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
|
||||||
|
# Status: NO fix in pgproto3/v2 (archived/EOL) — fix path requires CrowdSec to migrate to pgx/v5
|
||||||
|
# Note: This is the NVD/Red Hat advisory alias for the same underlying vulnerability as GHSA-jqcq-xjh3-6g23
|
||||||
|
#
|
||||||
|
# Vulnerability Details:
|
||||||
|
# - DataRow.Decode does not validate field lengths; a malicious or compromised PostgreSQL server
|
||||||
|
# can send a negative field length causing a slice-bounds panic — denial of service (CWE-129).
|
||||||
|
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (CVSS 7.5)
|
||||||
|
#
|
||||||
|
# Root Cause (EOL Module + Third-Party Binary):
|
||||||
|
# - Same underlying vulnerability as GHSA-jqcq-xjh3-6g23; tracked separately by NVD/Red Hat as CVE-2026-4427.
|
||||||
|
# - Charon does not use pgproto3/v2 directly nor communicate with PostgreSQL. The package
|
||||||
|
# is compiled into CrowdSec binaries for their internal database communication.
|
||||||
|
# - The pgproto3/v2 module is archived and EOL; no fix will be released. The fix path
|
||||||
|
# is migration to pgx/v5, which embeds an updated pgproto3/v3.
|
||||||
|
# - Fix path: once CrowdSec migrates to pgx/v5 and releases an updated binary, rebuild
|
||||||
|
# the Docker image and remove this suppression.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (Non-exploitable in Charon context + no upstream fix path)
|
||||||
|
# - The vulnerability requires a malicious PostgreSQL server response. Charon uses SQLite
|
||||||
|
# internally and does not run PostgreSQL. CrowdSec's database path is not exposed to
|
||||||
|
# external traffic in a standard Charon deployment.
|
||||||
|
# - The attack requires a compromised database server, which would imply full host compromise.
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor CrowdSec releases for pgx/v5 migration:
|
||||||
|
# https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
# - Weekly CI security rebuild flags the moment a fixed image ships.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-03-21 (initial suppression): pgproto3/v2 is EOL; no fix exists or will exist.
|
||||||
|
# Waiting on CrowdSec to migrate to pgx/v5. Set 30-day review. Sibling GHSA-jqcq-xjh3-6g23
|
||||||
|
# was already suppressed; this alias surfaced as a separate Grype match via NVD/Red Hat tracking.
|
||||||
|
# - Extended 2026-04-04: CrowdSec has not migrated to pgx/v5 yet.
|
||||||
|
# - Next review: 2026-05-21. Remove suppression once CrowdSec ships with pgx/v5.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - Same as GHSA-jqcq-xjh3-6g23: CrowdSec releases a version with pgx/v5 replacing pgproto3/v2
|
||||||
|
# - Rebuild Docker image, run security-scan-docker-image, confirm both advisories are resolved
|
||||||
|
# - Remove this entry, GHSA-jqcq-xjh3-6g23 entry, and both .trivyignore entries simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - GHSA-x6gf-mpr2-68h6: https://github.com/advisories/GHSA-x6gf-mpr2-68h6
|
||||||
|
# - CVE-2026-4427: https://nvd.nist.gov/vuln/detail/CVE-2026-4427
|
||||||
|
# - Red Hat: https://access.redhat.com/security/cve/CVE-2026-4427
|
||||||
|
# - pgproto3/v2 archive notice: https://github.com/jackc/pgproto3
|
||||||
|
# - pgx/v5 (replacement): https://github.com/jackc/pgx
|
||||||
|
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
- vulnerability: GHSA-x6gf-mpr2-68h6
|
||||||
|
package:
|
||||||
|
name: github.com/jackc/pgproto3/v2
|
||||||
|
version: "v2.3.3"
|
||||||
|
type: go-module
|
||||||
|
reason: |
|
||||||
|
HIGH — DoS panic via negative field length in pgproto3/v2 v2.3.3 embedded in CrowdSec binaries.
|
||||||
|
NVD/Red Hat alias (CVE-2026-4427) for the same underlying bug as GHSA-jqcq-xjh3-6g23.
|
||||||
|
pgproto3/v2 is archived/EOL with no fix planned; fix path requires CrowdSec to migrate to pgx/v5.
|
||||||
|
Charon uses SQLite, not PostgreSQL; this code path is not reachable in a standard deployment.
|
||||||
|
Risk accepted; no remediation until CrowdSec ships with pgx/v5.
|
||||||
|
Reviewed 2026-03-21: pgproto3/v2 EOL confirmed; CrowdSec has not migrated to pgx/v5 yet.
|
||||||
|
expiry: "2026-05-21" # Extended 2026-04-04: no fix path until CrowdSec migrates to pgx/v5.
|
||||||
|
|
||||||
|
# Action items when this suppression expires:
|
||||||
|
# 1. Check CrowdSec releases for pgx/v5 migration:
|
||||||
|
# https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
# 2. Verify with: `go version -m /path/to/crowdsec | grep pgproto3`
|
||||||
|
# Expected: pgproto3/v3 (or no pgproto3 reference if fully replaced)
|
||||||
|
# 3. If CrowdSec has migrated:
|
||||||
|
# a. Rebuild Docker image and run local security-scan-docker-image
|
||||||
|
# b. Remove this entry, GHSA-jqcq-xjh3-6g23 entry, and both .trivyignore entries
|
||||||
|
# 4. If not yet migrated: Extend expiry by 30 days and update the review comment above
|
||||||
|
# 5. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec requesting pgx/v5 migration
|
||||||
|
|
||||||
|
# CVE-2026-32286: pgproto3/v2 buffer overflow in DataRow handling (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5)
|
||||||
|
# Package: github.com/jackc/pgproto3/v2 v2.3.3 (embedded in /usr/local/bin/crowdsec and /usr/local/bin/cscli)
|
||||||
|
# Status: NO fix in pgproto3/v2 (archived/EOL) — fix path requires CrowdSec to migrate to pgx/v5
|
||||||
|
#
|
||||||
|
# Vulnerability Details:
|
||||||
|
# - Buffer overflow in pgproto3/v2 DataRow handling allows a malicious or compromised PostgreSQL
|
||||||
|
# server to trigger a denial of service via crafted protocol messages (CWE-120).
|
||||||
|
# - CVSSv3: AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H (CVSS 7.5)
|
||||||
|
#
|
||||||
|
# Root Cause (EOL Module + Third-Party Binary):
|
||||||
|
# - Same affected module as GHSA-jqcq-xjh3-6g23 and GHSA-x6gf-mpr2-68h6 — pgproto3/v2 v2.3.3
|
||||||
|
# is the final release (repository archived Jul 12, 2025). No fix will be released.
|
||||||
|
# - Charon does not use pgproto3/v2 directly nor communicate with PostgreSQL. The package
|
||||||
|
# is compiled into CrowdSec binaries for their internal database communication.
|
||||||
|
# - Fix exists only in pgproto3/v3 (used by pgx/v5). CrowdSec v1.7.7 (latest) still depends
|
||||||
|
# on pgx/v4 → pgproto3/v2. Dockerfile already applies best-effort mitigation (pgx/v4@v4.18.3).
|
||||||
|
# - Fix path: once CrowdSec migrates to pgx/v5, rebuild the Docker image and remove this suppression.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (Non-exploitable in Charon context + no upstream fix path)
|
||||||
|
# - The vulnerability requires a malicious PostgreSQL server response. Charon uses SQLite
|
||||||
|
# internally and does not run PostgreSQL. CrowdSec's database path is not exposed to
|
||||||
|
# external traffic in a standard Charon deployment.
|
||||||
|
# - CrowdSec's PostgreSQL code path is not directly exposed to untrusted network input in
|
||||||
|
# Charon's deployment.
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor CrowdSec releases for pgx/v5 migration:
|
||||||
|
# https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
# - Weekly CI security rebuild flags the moment a fixed image ships.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-04-10 (initial suppression): pgproto3/v2 is EOL; no fix exists or will exist.
|
||||||
|
# Waiting on CrowdSec to migrate to pgx/v5. Set 90-day review.
|
||||||
|
# - Next review: 2026-07-09. Remove suppression once CrowdSec ships with pgx/v5.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - Same as GHSA-jqcq-xjh3-6g23: CrowdSec releases a version with pgx/v5 replacing pgproto3/v2
|
||||||
|
# - Rebuild Docker image, run security-scan-docker-image, confirm all pgproto3/v2 advisories are resolved
|
||||||
|
# - Remove this entry, GHSA-jqcq-xjh3-6g23 entry, GHSA-x6gf-mpr2-68h6 entry, and all .trivyignore entries simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - CVE-2026-32286: https://nvd.nist.gov/vuln/detail/CVE-2026-32286
|
||||||
|
# - pgproto3/v2 archive notice: https://github.com/jackc/pgproto3
|
||||||
|
# - pgx/v5 (replacement): https://github.com/jackc/pgx
|
||||||
|
# - CrowdSec releases: https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
- vulnerability: CVE-2026-32286
|
||||||
|
package:
|
||||||
|
name: github.com/jackc/pgproto3/v2
|
||||||
|
version: "v2.3.3"
|
||||||
|
type: go-module
|
||||||
|
reason: |
|
||||||
|
HIGH — Buffer overflow in pgproto3/v2 v2.3.3 DataRow handling, embedded in CrowdSec binaries.
|
||||||
|
pgproto3/v2 v2.3.3 is the final release (archived Jul 2025); no fix will be released.
|
||||||
|
Fix exists only in pgproto3/v3 (pgx/v5). CrowdSec v1.7.7 still depends on pgx/v4 → pgproto3/v2.
|
||||||
|
Charon uses SQLite, not PostgreSQL; this code path is not reachable in a standard deployment.
|
||||||
|
Risk accepted; no remediation until CrowdSec ships with pgx/v5.
|
||||||
|
Reviewed 2026-04-10: pgproto3/v2 EOL confirmed; CrowdSec has not migrated to pgx/v5 yet.
|
||||||
|
expiry: "2026-07-09" # Reviewed 2026-04-10: no fix path until CrowdSec migrates to pgx/v5. 90-day expiry.
|
||||||
|
|
||||||
|
# Action items when this suppression expires:
|
||||||
|
# 1. Check CrowdSec releases for pgx/v5 migration:
|
||||||
|
# https://github.com/crowdsecurity/crowdsec/releases
|
||||||
|
# 2. Verify with: `go version -m /path/to/crowdsec | grep pgproto3`
|
||||||
|
# Expected: pgproto3/v3 (or no pgproto3 reference if fully replaced)
|
||||||
|
# 3. If CrowdSec has migrated:
|
||||||
|
# a. Rebuild Docker image and run local security-scan-docker-image
|
||||||
|
# b. Remove this entry, GHSA-jqcq-xjh3-6g23 entry, GHSA-x6gf-mpr2-68h6 entry, and all .trivyignore entries
|
||||||
|
# 4. If not yet migrated: Extend expiry by 30 days and update the review comment above
|
||||||
|
# 5. If extended 3+ times: Open an upstream issue on crowdsecurity/crowdsec requesting pgx/v5 migration
|
||||||
|
|
||||||
|
# GHSA-pxq6-2prw-chj9 / CVE-2026-33997: Moby off-by-one error in plugin privilege validation
|
||||||
|
# Severity: MEDIUM (CVSS 6.8)
|
||||||
|
# Package: github.com/docker/docker v28.5.2+incompatible (go-module)
|
||||||
|
# Status: Fixed in moby/moby v29.3.1 — NO fix available for docker/docker import path
|
||||||
|
#
|
||||||
|
# Vulnerability Details:
|
||||||
|
# - Off-by-one error in Moby's plugin privilege validation allows potential privilege escalation
|
||||||
|
# via crafted plugin configurations.
|
||||||
|
#
|
||||||
|
# Root Cause (No Fix Available for Import Path):
|
||||||
|
# - Same import path issue as CVE-2026-34040. The fix exists in moby/moby v29.3.1 but not
|
||||||
|
# for the docker/docker import path that Charon uses.
|
||||||
|
# - Fix path: same dependency migration pattern as CVE-2026-34040 (if needed) or upstream fix.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (Not exploitable in Charon context)
|
||||||
|
# - Charon uses the Docker client SDK only (list containers). The vulnerability is in Docker's
|
||||||
|
# plugin privilege validation, which is server-side functionality.
|
||||||
|
# - Charon does not run a Docker daemon, install Docker plugins, or interact with plugin privileges.
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor docker/docker releases: https://github.com/moby/moby/releases
|
||||||
|
# - Weekly CI security rebuild flags the moment a fixed version ships.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-03-30 (initial suppression): no fix for docker/docker import path. Set 30-day review.
|
||||||
|
# - Next review: 2026-04-30. Remove suppression once a fix is available for the docker/docker import path.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - docker/docker publishes a patched version OR moby/moby/v2 stabilizes
|
||||||
|
# - Update dependency, rebuild, run security-scan-docker-image, confirm finding is resolved
|
||||||
|
# - Remove this entry and all corresponding .trivyignore entries simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - GHSA-pxq6-2prw-chj9: https://github.com/advisories/GHSA-pxq6-2prw-chj9
|
||||||
|
# - CVE-2026-33997: https://nvd.nist.gov/vuln/detail/CVE-2026-33997
|
||||||
|
# - moby/moby releases: https://github.com/moby/moby/releases
|
||||||
|
- vulnerability: GHSA-pxq6-2prw-chj9
|
||||||
|
package:
|
||||||
|
name: github.com/docker/docker
|
||||||
|
version: "v28.5.2+incompatible"
|
||||||
|
type: go-module
|
||||||
|
reason: |
|
||||||
|
MEDIUM — Off-by-one error in Moby plugin privilege validation in docker/docker v28.5.2+incompatible.
|
||||||
|
Fixed in moby/moby v29.3.1 but no fix for docker/docker import path.
|
||||||
|
Charon uses Docker client SDK only (list containers); the vulnerability is in Docker's server-side
|
||||||
|
plugin privilege validation. Charon does not run a Docker daemon or install Docker plugins.
|
||||||
|
Risk accepted; no remediation path until docker/docker publishes a fix or moby/moby/v2 stabilizes.
|
||||||
|
Reviewed 2026-03-30: no patched release available for docker/docker import path.
|
||||||
|
expiry: "2026-04-30" # 30-day review: no fix for docker/docker import path. Extend in 30-day increments with documented justification.
|
||||||
|
|
||||||
|
# Action items when this suppression expires:
|
||||||
|
# 1. Check docker/docker and moby/moby releases: https://github.com/moby/moby/releases
|
||||||
|
# 2. Check if moby/moby/v2 has stabilized: https://github.com/moby/moby
|
||||||
|
# 3. If a fix has shipped for docker/docker import path OR moby/moby/v2 is stable:
|
||||||
|
# a. Update the dependency and rebuild Docker image
|
||||||
|
# b. Run local security-scan-docker-image and confirm finding is resolved
|
||||||
|
# c. Remove this entry and all corresponding .trivyignore entries
|
||||||
|
# 4. If no fix yet: Extend expiry by 30 days and update the review comment above
|
||||||
|
# 5. If extended 3+ times: Open an issue to track moby/moby/v2 migration feasibility
|
||||||
|
|
||||||
|
# GHSA-78h2-9frx-2jm8: go-jose JWE decryption panic (DoS)
|
||||||
|
# Severity: HIGH
|
||||||
|
# Packages: github.com/go-jose/go-jose/v3 v3.0.4 and github.com/go-jose/go-jose/v4 v4.1.3
|
||||||
|
# (embedded in /usr/bin/caddy)
|
||||||
|
# Status: Fix available in go-jose/v3 v3.0.5 and go-jose/v4 v4.1.4 — requires upstream Caddy rebuild
|
||||||
|
#
|
||||||
|
# Vulnerability Details:
|
||||||
|
# - JWE decryption can trigger a panic due to improper input validation, causing
|
||||||
|
# a denial-of-service condition (runtime crash).
|
||||||
|
#
|
||||||
|
# Root Cause (Third-Party Binary):
|
||||||
|
# - Charon does not use go-jose directly. The library is compiled into the Caddy binary
|
||||||
|
# shipped in the Docker image.
|
||||||
|
# - Fixes are available upstream (v3.0.5 and v4.1.4) but require a Caddy rebuild to pick up.
|
||||||
|
# - Fix path: once the upstream Caddy release includes the patched go-jose versions,
|
||||||
|
# rebuild the Docker image and remove these suppressions.
|
||||||
|
#
|
||||||
|
# Risk Assessment: ACCEPTED (No direct use + fix requires upstream rebuild)
|
||||||
|
# - Charon does not import or call go-jose functions; the library is only present as a
|
||||||
|
# transitive dependency inside the Caddy binary.
|
||||||
|
# - The attack vector requires crafted JWE input reaching Caddy's internal JWT handling,
|
||||||
|
# which is limited to authenticated admin-API paths not exposed in Charon deployments.
|
||||||
|
#
|
||||||
|
# Mitigation (active while suppression is in effect):
|
||||||
|
# - Monitor Caddy releases: https://github.com/caddyserver/caddy/releases
|
||||||
|
# - Weekly CI security rebuild flags the moment a fixed image ships.
|
||||||
|
#
|
||||||
|
# Review:
|
||||||
|
# - Reviewed 2026-04-05 (initial suppression): fix available upstream but not yet in Caddy release.
|
||||||
|
# Set 30-day review.
|
||||||
|
# - Next review: 2026-05-05. Remove suppression once Caddy ships with patched go-jose.
|
||||||
|
#
|
||||||
|
# Removal Criteria:
|
||||||
|
# - Caddy releases a version built with go-jose/v3 >= v3.0.5 and go-jose/v4 >= v4.1.4
|
||||||
|
# - Rebuild Docker image, run security-scan-docker-image, confirm finding is resolved
|
||||||
|
# - Remove both entries (v3 and v4) and any corresponding .trivyignore entries simultaneously
|
||||||
|
#
|
||||||
|
# References:
|
||||||
|
# - GHSA-78h2-9frx-2jm8: https://github.com/advisories/GHSA-78h2-9frx-2jm8
|
||||||
|
# - go-jose releases: https://github.com/go-jose/go-jose/releases
|
||||||
|
# - Caddy releases: https://github.com/caddyserver/caddy/releases
|
||||||
|
- vulnerability: GHSA-78h2-9frx-2jm8
|
||||||
|
package:
|
||||||
|
name: github.com/go-jose/go-jose/v3
|
||||||
|
version: "v3.0.4"
|
||||||
|
type: go-module
|
||||||
|
reason: |
|
||||||
|
HIGH — JWE decryption panic in go-jose v3.0.4 embedded in /usr/bin/caddy.
|
||||||
|
Fix available in v3.0.5 but requires upstream Caddy rebuild. Charon does not use go-jose
|
||||||
|
directly. Deferring to next Caddy release.
|
||||||
|
expiry: "2026-05-05" # 30-day review: remove once Caddy ships with go-jose/v3 >= v3.0.5.
|
||||||
|
|
||||||
|
# Action items when this suppression expires:
|
||||||
|
# 1. Check Caddy releases: https://github.com/caddyserver/caddy/releases
|
||||||
|
# 2. Verify with: `go version -m /usr/bin/caddy | grep go-jose`
|
||||||
|
# Expected: go-jose/v3 >= v3.0.5
|
||||||
|
# 3. If Caddy has updated:
|
||||||
|
# a. Rebuild Docker image and run local security-scan-docker-image
|
||||||
|
# b. Remove this entry, the v4 entry below, and any corresponding .trivyignore entries
|
||||||
|
# 4. If not yet updated: Extend expiry by 30 days and update the review comment above
|
||||||
|
# 5. If extended 3+ times: Open an upstream issue on caddyserver/caddy requesting go-jose update
|
||||||
|
|
||||||
|
# GHSA-78h2-9frx-2jm8 (go-jose/v4) — see full justification in the go-jose/v3 entry above
|
||||||
|
- vulnerability: GHSA-78h2-9frx-2jm8
|
||||||
|
package:
|
||||||
|
name: github.com/go-jose/go-jose/v4
|
||||||
|
version: "v4.1.3"
|
||||||
|
type: go-module
|
||||||
|
reason: |
|
||||||
|
HIGH — JWE decryption panic in go-jose v4.1.3 embedded in /usr/bin/caddy.
|
||||||
|
Fix available in v4.1.4 but requires upstream Caddy rebuild. Charon does not use go-jose
|
||||||
|
directly. Deferring to next Caddy release.
|
||||||
|
expiry: "2026-05-05" # 30-day review: see go-jose/v3 entry above for action items.
|
||||||
|
|
||||||
# Match exclusions (patterns to ignore during scanning)
|
# Match exclusions (patterns to ignore during scanning)
|
||||||
# Use sparingly - prefer specific CVE suppressions above
|
# Use sparingly - prefer specific CVE suppressions above
|
||||||
|
|||||||
@@ -1,211 +0,0 @@
|
|||||||
# NOTE: golangci-lint-fast now includes test files (_test.go) to catch security
|
|
||||||
# issues earlier. The fast config uses gosec with critical-only checks (G101,
|
|
||||||
# G110, G305, G401, G501, G502, G503) for acceptable performance.
|
|
||||||
# Last updated: 2026-02-02
|
|
||||||
|
|
||||||
repos:
|
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
||||||
rev: v6.0.0
|
|
||||||
hooks:
|
|
||||||
- id: end-of-file-fixer
|
|
||||||
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)'
|
|
||||||
- id: trailing-whitespace
|
|
||||||
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|.*\.tsbuildinfo$)'
|
|
||||||
- id: check-yaml
|
|
||||||
- id: check-added-large-files
|
|
||||||
args: ['--maxkb=2500']
|
|
||||||
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
||||||
rev: v0.10.0.1
|
|
||||||
hooks:
|
|
||||||
- id: shellcheck
|
|
||||||
name: shellcheck
|
|
||||||
exclude: '^(frontend/(coverage|dist|node_modules|\.vite)/|test-results|codeql-agent-results)/'
|
|
||||||
args: ['--severity=error']
|
|
||||||
- repo: https://github.com/rhysd/actionlint
|
|
||||||
rev: v1.7.10
|
|
||||||
hooks:
|
|
||||||
- id: actionlint
|
|
||||||
name: actionlint (GitHub Actions)
|
|
||||||
files: '^\.github/workflows/.*\.ya?ml$'
|
|
||||||
- repo: local
|
|
||||||
hooks:
|
|
||||||
- id: dockerfile-check
|
|
||||||
name: dockerfile validation
|
|
||||||
entry: tools/dockerfile_check.sh
|
|
||||||
language: script
|
|
||||||
files: "Dockerfile.*"
|
|
||||||
pass_filenames: true
|
|
||||||
- id: go-test-coverage
|
|
||||||
name: Go Test Coverage (Manual)
|
|
||||||
entry: scripts/go-test-coverage.sh
|
|
||||||
language: script
|
|
||||||
files: '\.go$'
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual] # Only runs when explicitly called
|
|
||||||
- id: go-vet
|
|
||||||
name: Go Vet
|
|
||||||
entry: bash -c 'cd backend && go vet ./...'
|
|
||||||
language: system
|
|
||||||
files: '\.go$'
|
|
||||||
pass_filenames: false
|
|
||||||
- id: golangci-lint-fast
|
|
||||||
name: golangci-lint (Fast Linters - BLOCKING)
|
|
||||||
entry: scripts/pre-commit-hooks/golangci-lint-fast.sh
|
|
||||||
language: script
|
|
||||||
files: '\.go$'
|
|
||||||
# Test files are now included to catch security issues (gosec critical checks)
|
|
||||||
pass_filenames: false
|
|
||||||
description: "Runs fast, essential linters (staticcheck, govet, errcheck, ineffassign, unused, gosec critical) - BLOCKS commits on failure"
|
|
||||||
- id: check-version-match
|
|
||||||
name: Check .version matches latest Git tag
|
|
||||||
entry: bash -c 'scripts/check-version-match-tag.sh'
|
|
||||||
language: system
|
|
||||||
files: '\.version$'
|
|
||||||
pass_filenames: false
|
|
||||||
- id: check-lfs-large-files
|
|
||||||
name: Prevent large files that are not tracked by LFS
|
|
||||||
entry: bash scripts/pre-commit-hooks/check-lfs-for-large-files.sh
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
always_run: true
|
|
||||||
- id: block-codeql-db-commits
|
|
||||||
name: Prevent committing CodeQL DB artifacts
|
|
||||||
entry: bash scripts/pre-commit-hooks/block-codeql-db-commits.sh
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
always_run: true
|
|
||||||
- id: block-data-backups-commit
|
|
||||||
name: Prevent committing data/backups files
|
|
||||||
entry: bash scripts/pre-commit-hooks/block-data-backups-commit.sh
|
|
||||||
language: system
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
always_run: true
|
|
||||||
|
|
||||||
# === MANUAL/CI-ONLY HOOKS ===
|
|
||||||
# These are slow and should only run on-demand or in CI
|
|
||||||
# Run manually with: pre-commit run golangci-lint-full --all-files
|
|
||||||
- id: go-test-race
|
|
||||||
name: Go Test Race (Manual)
|
|
||||||
entry: bash -c 'cd backend && go test -race ./...'
|
|
||||||
language: system
|
|
||||||
files: '\.go$'
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [manual] # Only runs when explicitly called
|
|
||||||
|
|
||||||
- id: golangci-lint-full
|
|
||||||
name: golangci-lint (Full - Manual)
|
|
||||||
entry: scripts/pre-commit-hooks/golangci-lint-full.sh
|
|
||||||
language: script
|
|
||||||
files: '\.go$'
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [manual] # Only runs when explicitly called
|
|
||||||
|
|
||||||
- id: hadolint
|
|
||||||
name: Hadolint Dockerfile Check (Manual)
|
|
||||||
entry: bash -c 'docker run --rm -i hadolint/hadolint < Dockerfile'
|
|
||||||
language: system
|
|
||||||
files: 'Dockerfile'
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [manual] # Only runs when explicitly called
|
|
||||||
- id: frontend-type-check
|
|
||||||
name: Frontend TypeScript Check
|
|
||||||
entry: bash -c 'cd frontend && npx tsc --noEmit'
|
|
||||||
language: system
|
|
||||||
files: '^frontend/.*\.(ts|tsx)$'
|
|
||||||
pass_filenames: false
|
|
||||||
- id: frontend-lint
|
|
||||||
name: Frontend Lint (Fix)
|
|
||||||
entry: bash -c 'cd frontend && npm run lint -- --fix'
|
|
||||||
language: system
|
|
||||||
files: '^frontend/.*\.(ts|tsx|js|jsx)$'
|
|
||||||
pass_filenames: false
|
|
||||||
|
|
||||||
- id: frontend-test-coverage
|
|
||||||
name: Frontend Test Coverage (Manual)
|
|
||||||
entry: scripts/frontend-test-coverage.sh
|
|
||||||
language: script
|
|
||||||
files: '^frontend/.*\\.(ts|tsx|js|jsx)$'
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual]
|
|
||||||
|
|
||||||
- id: security-scan
|
|
||||||
name: Security Vulnerability Scan (Manual)
|
|
||||||
entry: scripts/security-scan.sh
|
|
||||||
language: script
|
|
||||||
files: '(\.go$|go\.mod$|go\.sum$)'
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual] # Only runs when explicitly called
|
|
||||||
|
|
||||||
- id: codeql-go-scan
|
|
||||||
name: CodeQL Go Security Scan (Manual - Slow)
|
|
||||||
entry: scripts/pre-commit-hooks/codeql-go-scan.sh
|
|
||||||
language: script
|
|
||||||
files: '\.go$'
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual] # Performance: 30-60s, only run on-demand
|
|
||||||
|
|
||||||
- id: codeql-js-scan
|
|
||||||
name: CodeQL JavaScript/TypeScript Security Scan (Manual - Slow)
|
|
||||||
entry: scripts/pre-commit-hooks/codeql-js-scan.sh
|
|
||||||
language: script
|
|
||||||
files: '^frontend/.*\.(ts|tsx|js|jsx)$'
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual] # Performance: 30-60s, only run on-demand
|
|
||||||
|
|
||||||
- id: codeql-check-findings
|
|
||||||
name: Block HIGH/CRITICAL CodeQL Findings
|
|
||||||
entry: scripts/pre-commit-hooks/codeql-check-findings.sh
|
|
||||||
language: script
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual] # Only runs after CodeQL scans
|
|
||||||
|
|
||||||
- id: codeql-parity-check
|
|
||||||
name: CodeQL Suite/Trigger Parity Guard (Manual)
|
|
||||||
entry: scripts/ci/check-codeql-parity.sh
|
|
||||||
language: script
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual]
|
|
||||||
|
|
||||||
- id: gorm-security-scan
|
|
||||||
name: GORM Security Scanner (Manual)
|
|
||||||
entry: scripts/pre-commit-hooks/gorm-security-check.sh
|
|
||||||
language: script
|
|
||||||
files: '\.go$'
|
|
||||||
pass_filenames: false
|
|
||||||
stages: [manual] # Manual stage initially (soft launch)
|
|
||||||
verbose: true
|
|
||||||
description: "Detects GORM ID leaks and common GORM security mistakes"
|
|
||||||
|
|
||||||
- id: semgrep-scan
|
|
||||||
name: Semgrep Security Scan (Manual)
|
|
||||||
entry: scripts/pre-commit-hooks/semgrep-scan.sh
|
|
||||||
language: script
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual] # Manual stage initially (reversible rollout)
|
|
||||||
|
|
||||||
- id: gitleaks-tuned-scan
|
|
||||||
name: Gitleaks Security Scan (Tuned, Manual)
|
|
||||||
entry: scripts/pre-commit-hooks/gitleaks-tuned-scan.sh
|
|
||||||
language: script
|
|
||||||
pass_filenames: false
|
|
||||||
verbose: true
|
|
||||||
stages: [manual] # Manual stage initially (reversible rollout)
|
|
||||||
|
|
||||||
- repo: https://github.com/igorshubovych/markdownlint-cli
|
|
||||||
rev: v0.47.0
|
|
||||||
hooks:
|
|
||||||
- id: markdownlint
|
|
||||||
args: ["--fix"]
|
|
||||||
exclude: '^(node_modules|\.venv|test-results|codeql-db|codeql-agent-results)/'
|
|
||||||
stages: [manual]
|
|
||||||
100
.trivyignore
100
.trivyignore
@@ -3,7 +3,103 @@ playwright/.auth/
|
|||||||
|
|
||||||
# GHSA-69x3-g4r3-p962 / CVE-2026-25793: Nebula ECDSA Signature Malleability
|
# GHSA-69x3-g4r3-p962 / CVE-2026-25793: Nebula ECDSA Signature Malleability
|
||||||
# Severity: HIGH (CVSS 8.1) — Package: github.com/slackhq/nebula v1.9.7 in /usr/bin/caddy
|
# Severity: HIGH (CVSS 8.1) — Package: github.com/slackhq/nebula v1.9.7 in /usr/bin/caddy
|
||||||
# Cannot upgrade: smallstep/certificates v0.27.5 (latest stable as of 2026-02-19) still pins nebula v1.9.x.
|
# Fix exists in nebula v1.10.3, but smallstep/certificates (through v0.30.2) uses legacy nebula
|
||||||
# Charon does not use Nebula VPN PKI by default. Review by: 2026-03-05
|
# APIs removed in v1.10+, causing compile failures. Waiting on certificates upstream update.
|
||||||
|
# Charon does not use Nebula VPN PKI by default. Review by: 2026-05-10
|
||||||
# See also: .grype.yaml for full justification
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-05-10
|
||||||
CVE-2026-25793
|
CVE-2026-25793
|
||||||
|
|
||||||
|
# CVE-2026-27171: zlib CPU spin via crc32_combine64 infinite loop (DoS)
|
||||||
|
# Severity: MEDIUM (CVSS 5.5 NVD / 2.9 MITRE) — Package: zlib 1.3.1-r2 in Alpine base image
|
||||||
|
# Fix requires zlib >= 1.3.2. No upstream fix available: Alpine 3.23 still ships zlib 1.3.1-r2.
|
||||||
|
# Attack requires local access (AV:L); the vulnerable code path is not reachable via Charon's
|
||||||
|
# network-facing surface. Non-blocking by CI policy (MEDIUM). Review by: 2026-05-21
|
||||||
|
# exp: 2026-05-21
|
||||||
|
CVE-2026-27171
|
||||||
|
|
||||||
|
# CVE-2026-2673: OpenSSL TLS 1.3 server key exchange group downgrade (libcrypto3/libssl3)
|
||||||
|
# Severity: HIGH (CVSS 7.5) — Packages: libcrypto3 3.5.5-r0 and libssl3 3.5.5-r0 in Alpine base image
|
||||||
|
# No upstream fix available: Alpine 3.23 still ships libcrypto3/libssl3 3.5.5-r0 as of 2026-03-18.
|
||||||
|
# When DEFAULT is in TLS 1.3 group config, server may select a weaker key exchange group.
|
||||||
|
# Charon terminates TLS at the Caddy layer — the Go backend does not act as a raw TLS 1.3 server.
|
||||||
|
# Review by: 2026-05-18
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-05-18
|
||||||
|
CVE-2026-2673
|
||||||
|
|
||||||
|
# CVE-2026-33186 / GHSA-p77j-4mvh-x3m3: gRPC-Go authorization bypass via missing leading slash
|
||||||
|
# Severity: CRITICAL (CVSS 9.1) — Package: google.golang.org/grpc, embedded in CrowdSec (v1.74.2) and Caddy (v1.79.1)
|
||||||
|
# Fix exists at v1.79.3 — Charon's own dep is patched. Waiting on CrowdSec and Caddy upstream releases.
|
||||||
|
# CrowdSec's and Caddy's grpc servers are not exposed externally in a standard Charon deployment.
|
||||||
|
# Suppressed for CrowdSec/Caddy embedded binaries only — Charon's direct deps are fixed (v1.79.3).
|
||||||
|
# Review by: 2026-05-04
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-05-04
|
||||||
|
CVE-2026-33186
|
||||||
|
|
||||||
|
# GHSA-479m-364c-43vc: goxmldsig XML signature validation bypass (loop variable capture)
|
||||||
|
# Severity: HIGH (CVSS 7.5) — Package: github.com/russellhaering/goxmldsig v1.5.0, embedded in /usr/bin/caddy
|
||||||
|
# Fix exists at v1.6.0 — waiting on Caddy upstream (or caddy-security plugin) to release with patched goxmldsig.
|
||||||
|
# Charon does not configure SAML-based SSO by default; the vulnerable path is not reachable in a standard deployment.
|
||||||
|
# Awaiting Caddy upstream update to include goxmldsig v1.6.0.
|
||||||
|
# Review by: 2026-05-04
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-05-04
|
||||||
|
GHSA-479m-364c-43vc
|
||||||
|
|
||||||
|
# GHSA-6g7g-w4f8-9c9x: buger/jsonparser Delete panic on malformed JSON (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5) — Package: github.com/buger/jsonparser v1.1.1, embedded in CrowdSec binaries
|
||||||
|
# No upstream fix available as of 2026-03-19 (issue #275 open, golang/vulndb #4514 open).
|
||||||
|
# Charon does not use this package; the vector requires reaching CrowdSec's internal processing pipeline.
|
||||||
|
# Review by: 2026-05-19
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-05-19
|
||||||
|
GHSA-6g7g-w4f8-9c9x
|
||||||
|
|
||||||
|
# GHSA-jqcq-xjh3-6g23: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5) — Package: github.com/jackc/pgproto3/v2 v2.3.3, embedded in CrowdSec binaries
|
||||||
|
# pgproto3/v2 is archived/EOL — no fix will be released. Fix path requires CrowdSec to migrate to pgx/v5.
|
||||||
|
# Charon uses SQLite; the PostgreSQL code path is not reachable in a standard deployment.
|
||||||
|
# Review by: 2026-05-19
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-05-19
|
||||||
|
GHSA-jqcq-xjh3-6g23
|
||||||
|
|
||||||
|
# GHSA-x6gf-mpr2-68h6 / CVE-2026-4427: pgproto3/v2 DataRow.Decode panic on negative field length (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5) — Package: github.com/jackc/pgproto3/v2 v2.3.3, embedded in CrowdSec binaries
|
||||||
|
# NVD/Red Hat alias (CVE-2026-4427) for the same underlying bug as GHSA-jqcq-xjh3-6g23.
|
||||||
|
# pgproto3/v2 is archived/EOL — no fix will be released. Fix path requires CrowdSec to migrate to pgx/v5.
|
||||||
|
# Charon uses SQLite; the PostgreSQL code path is not reachable in a standard deployment.
|
||||||
|
# Review by: 2026-05-21
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-05-21
|
||||||
|
GHSA-x6gf-mpr2-68h6
|
||||||
|
|
||||||
|
# CVE-2026-32286: pgproto3/v2 buffer overflow in DataRow handling (DoS)
|
||||||
|
# Severity: HIGH (CVSS 7.5) — Package: github.com/jackc/pgproto3/v2 v2.3.3, embedded in CrowdSec binaries
|
||||||
|
# pgproto3/v2 v2.3.3 is the final release — repository archived Jul 12, 2025. No fix will be released.
|
||||||
|
# Fix exists only in pgproto3/v3 (used by pgx/v5). CrowdSec v1.7.7 (latest) still depends on pgx/v4 → pgproto3/v2.
|
||||||
|
# Dockerfile already applies best-effort mitigation (pgx/v4@v4.18.3).
|
||||||
|
# Charon uses SQLite; the PostgreSQL code path is not reachable in a standard deployment.
|
||||||
|
# Review by: 2026-07-09
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-07-09
|
||||||
|
CVE-2026-32286
|
||||||
|
|
||||||
|
# CVE-2026-33997 / GHSA-pxq6-2prw-chj9: Moby off-by-one error in plugin privilege validation
|
||||||
|
# Severity: MEDIUM (CVSS 6.8) — Package: github.com/docker/docker v28.5.2+incompatible
|
||||||
|
# Fixed in moby/moby v29.3.1 but no fix for docker/docker import path.
|
||||||
|
# Charon uses Docker client SDK only (list containers); plugin privilege validation is server-side.
|
||||||
|
# Review by: 2026-04-30
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-04-30
|
||||||
|
CVE-2026-33997
|
||||||
|
|
||||||
|
# GHSA-pxq6-2prw-chj9: Moby off-by-one error in plugin privilege validation (GHSA alias)
|
||||||
|
# Severity: MEDIUM (CVSS 6.8) — Package: github.com/docker/docker v28.5.2+incompatible
|
||||||
|
# GHSA alias for CVE-2026-33997. See CVE-2026-33997 entry above for full details.
|
||||||
|
# Review by: 2026-04-30
|
||||||
|
# See also: .grype.yaml for full justification
|
||||||
|
# exp: 2026-04-30
|
||||||
|
GHSA-pxq6-2prw-chj9
|
||||||
|
|||||||
14
.vscode/tasks.json
vendored
14
.vscode/tasks.json
vendored
@@ -371,9 +371,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Lint: Pre-commit (All Files)",
|
"label": "Lint: Lefthook Pre-commit (All Files)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": ".github/skills/scripts/skill-runner.sh qa-precommit-all",
|
"command": "lefthook run pre-commit",
|
||||||
"group": "test",
|
"group": "test",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
@@ -466,9 +466,9 @@
|
|||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Security: Semgrep Scan (Manual Hook)",
|
"label": "Security: Semgrep Scan (Lefthook Pre-push)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pre-commit run --hook-stage manual semgrep-scan --all-files",
|
"command": "lefthook run pre-push",
|
||||||
"group": "test",
|
"group": "test",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
@@ -480,9 +480,9 @@
|
|||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "Security: Gitleaks Scan (Tuned Manual Hook)",
|
"label": "Security: Gitleaks Scan (Lefthook Pre-push)",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "pre-commit run --hook-stage manual gitleaks-tuned-scan --all-files",
|
"command": "lefthook run pre-push",
|
||||||
"group": "test",
|
"group": "test",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
@@ -727,7 +727,7 @@
|
|||||||
{
|
{
|
||||||
"label": "Security: Caddy PR-1 Compatibility Matrix",
|
"label": "Security: Caddy PR-1 Compatibility Matrix",
|
||||||
"type": "shell",
|
"type": "shell",
|
||||||
"command": "cd /projects/Charon && bash scripts/caddy-compat-matrix.sh --candidate-version 2.11.1 --patch-scenarios A,B,C --platforms linux/amd64,linux/arm64 --smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health --output-dir test-results/caddy-compat --docs-report docs/reports/caddy-compatibility-matrix.md",
|
"command": "cd /projects/Charon && bash scripts/caddy-compat-matrix.sh --candidate-version 2.11.2 --patch-scenarios A,B,C --platforms linux/amd64,linux/arm64 --smoke-set boot_caddy,plugin_modules,config_validate,admin_api_health --output-dir test-results/caddy-compat --docs-report docs/reports/caddy-compatibility-matrix.md",
|
||||||
"group": "test",
|
"group": "test",
|
||||||
"problemMatcher": []
|
"problemMatcher": []
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ graph TB
|
|||||||
| **HTTP Framework** | Gin | Latest | Routing, middleware, HTTP handling |
|
| **HTTP Framework** | Gin | Latest | Routing, middleware, HTTP handling |
|
||||||
| **Database** | SQLite | 3.x | Embedded database |
|
| **Database** | SQLite | 3.x | Embedded database |
|
||||||
| **ORM** | GORM | Latest | Database abstraction layer |
|
| **ORM** | GORM | Latest | Database abstraction layer |
|
||||||
| **Reverse Proxy** | Caddy Server | 2.11.1 | Embedded HTTP/HTTPS proxy |
|
| **Reverse Proxy** | Caddy Server | 2.11.2 | Embedded HTTP/HTTPS proxy |
|
||||||
| **WebSocket** | gorilla/websocket | Latest | Real-time log streaming |
|
| **WebSocket** | gorilla/websocket | Latest | Real-time log streaming |
|
||||||
| **Crypto** | golang.org/x/crypto | Latest | Password hashing, encryption |
|
| **Crypto** | golang.org/x/crypto | Latest | Password hashing, encryption |
|
||||||
| **Metrics** | Prometheus Client | Latest | Application metrics |
|
| **Metrics** | Prometheus Client | Latest | Application metrics |
|
||||||
@@ -139,15 +139,15 @@ graph TB
|
|||||||
| Component | Technology | Version | Purpose |
|
| Component | Technology | Version | Purpose |
|
||||||
|-----------|-----------|---------|---------|
|
|-----------|-----------|---------|---------|
|
||||||
| **Framework** | React | 19.2.3 | UI framework |
|
| **Framework** | React | 19.2.3 | UI framework |
|
||||||
| **Language** | TypeScript | 5.x | Type-safe JavaScript |
|
| **Language** | TypeScript | 6.x | Type-safe JavaScript |
|
||||||
| **Build Tool** | Vite | 6.1.9 | Fast bundler and dev server |
|
| **Build Tool** | Vite | 8.0.0-beta.18 | Fast bundler and dev server |
|
||||||
| **CSS Framework** | Tailwind CSS | 3.x | Utility-first CSS |
|
| **CSS Framework** | Tailwind CSS | 4.2.1 | Utility-first CSS |
|
||||||
| **Routing** | React Router | 7.x | Client-side routing |
|
| **Routing** | React Router | 7.x | Client-side routing |
|
||||||
| **HTTP Client** | Fetch API | Native | API communication |
|
| **HTTP Client** | Fetch API | Native | API communication |
|
||||||
| **State Management** | React Hooks + Context | Native | Global state |
|
| **State Management** | React Hooks + Context | Native | Global state |
|
||||||
| **Internationalization** | i18next | Latest | 5 language support |
|
| **Internationalization** | i18next | Latest | 5 language support |
|
||||||
| **Unit Testing** | Vitest | 2.x | Fast unit test runner |
|
| **Unit Testing** | Vitest | 4.1.0-beta.6 | Fast unit test runner |
|
||||||
| **E2E Testing** | Playwright | 1.50.x | Browser automation |
|
| **E2E Testing** | Playwright | 1.58.2 | Browser automation |
|
||||||
|
|
||||||
### Infrastructure
|
### Infrastructure
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ graph TB
|
|||||||
│ │ └── main.tsx # Application entry point
|
│ │ └── main.tsx # Application entry point
|
||||||
│ ├── public/ # Static assets
|
│ ├── public/ # Static assets
|
||||||
│ ├── package.json # NPM dependencies
|
│ ├── package.json # NPM dependencies
|
||||||
│ └── vite.config.js # Vite configuration
|
│ └── vite.config.ts # Vite configuration
|
||||||
│
|
│
|
||||||
├── .docker/ # Docker configuration
|
├── .docker/ # Docker configuration
|
||||||
│ ├── compose/ # Docker Compose files
|
│ ├── compose/ # Docker Compose files
|
||||||
@@ -306,11 +306,13 @@ graph TB
|
|||||||
**Key Modules:**
|
**Key Modules:**
|
||||||
|
|
||||||
#### API Layer (`internal/api/`)
|
#### API Layer (`internal/api/`)
|
||||||
|
|
||||||
- **Handlers:** Process HTTP requests, validate input, return responses
|
- **Handlers:** Process HTTP requests, validate input, return responses
|
||||||
- **Middleware:** CORS, GZIP, authentication, logging, metrics, panic recovery
|
- **Middleware:** CORS, GZIP, authentication, logging, metrics, panic recovery
|
||||||
- **Routes:** Route registration and grouping (public vs authenticated)
|
- **Routes:** Route registration and grouping (public vs authenticated)
|
||||||
|
|
||||||
**Example Endpoints:**
|
**Example Endpoints:**
|
||||||
|
|
||||||
- `GET /api/v1/proxy-hosts` - List all proxy hosts
|
- `GET /api/v1/proxy-hosts` - List all proxy hosts
|
||||||
- `POST /api/v1/proxy-hosts` - Create new proxy host
|
- `POST /api/v1/proxy-hosts` - Create new proxy host
|
||||||
- `PUT /api/v1/proxy-hosts/:id` - Update proxy host
|
- `PUT /api/v1/proxy-hosts/:id` - Update proxy host
|
||||||
@@ -318,6 +320,7 @@ graph TB
|
|||||||
- `WS /api/v1/logs` - WebSocket for real-time logs
|
- `WS /api/v1/logs` - WebSocket for real-time logs
|
||||||
|
|
||||||
#### Service Layer (`internal/services/`)
|
#### Service Layer (`internal/services/`)
|
||||||
|
|
||||||
- **ProxyService:** CRUD operations for proxy hosts, validation logic
|
- **ProxyService:** CRUD operations for proxy hosts, validation logic
|
||||||
- **CertificateService:** ACME certificate provisioning and renewal
|
- **CertificateService:** ACME certificate provisioning and renewal
|
||||||
- **DockerService:** Container discovery and monitoring
|
- **DockerService:** Container discovery and monitoring
|
||||||
@@ -327,12 +330,14 @@ graph TB
|
|||||||
**Design Pattern:** Services contain business logic and call multiple repositories/managers
|
**Design Pattern:** Services contain business logic and call multiple repositories/managers
|
||||||
|
|
||||||
#### Caddy Manager (`internal/caddy/`)
|
#### Caddy Manager (`internal/caddy/`)
|
||||||
|
|
||||||
- **Manager:** Orchestrates Caddy configuration updates
|
- **Manager:** Orchestrates Caddy configuration updates
|
||||||
- **Config Builder:** Generates Caddy JSON from database models
|
- **Config Builder:** Generates Caddy JSON from database models
|
||||||
- **Reload Logic:** Atomic config application with rollback on failure
|
- **Reload Logic:** Atomic config application with rollback on failure
|
||||||
- **Security Integration:** Injects Cerberus middleware into Caddy pipelines
|
- **Security Integration:** Injects Cerberus middleware into Caddy pipelines
|
||||||
|
|
||||||
**Responsibilities:**
|
**Responsibilities:**
|
||||||
|
|
||||||
1. Generate Caddy JSON configuration from database state
|
1. Generate Caddy JSON configuration from database state
|
||||||
2. Validate configuration before applying
|
2. Validate configuration before applying
|
||||||
3. Trigger Caddy reload via JSON API
|
3. Trigger Caddy reload via JSON API
|
||||||
@@ -340,22 +345,26 @@ graph TB
|
|||||||
5. Integrate security layers (WAF, ACL, Rate Limiting)
|
5. Integrate security layers (WAF, ACL, Rate Limiting)
|
||||||
|
|
||||||
#### Security Suite (`internal/cerberus/`)
|
#### Security Suite (`internal/cerberus/`)
|
||||||
|
|
||||||
- **ACL (Access Control Lists):** IP-based allow/deny rules, GeoIP blocking
|
- **ACL (Access Control Lists):** IP-based allow/deny rules, GeoIP blocking
|
||||||
- **WAF (Web Application Firewall):** Coraza engine with OWASP CRS
|
- **WAF (Web Application Firewall):** Coraza engine with OWASP CRS
|
||||||
- **CrowdSec:** Behavior-based threat detection with global intelligence
|
- **CrowdSec:** Behavior-based threat detection with global intelligence
|
||||||
- **Rate Limiter:** Per-IP request throttling
|
- **Rate Limiter:** Per-IP request throttling
|
||||||
|
|
||||||
**Integration Points:**
|
**Integration Points:**
|
||||||
|
|
||||||
- Middleware injection into Caddy request pipeline
|
- Middleware injection into Caddy request pipeline
|
||||||
- Database-driven rule configuration
|
- Database-driven rule configuration
|
||||||
- Metrics collection for security events
|
- Metrics collection for security events
|
||||||
|
|
||||||
#### Database Layer (`internal/database/`)
|
#### Database Layer (`internal/database/`)
|
||||||
|
|
||||||
- **Migrations:** Automatic schema versioning with GORM AutoMigrate
|
- **Migrations:** Automatic schema versioning with GORM AutoMigrate
|
||||||
- **Seeding:** Default settings and admin user creation
|
- **Seeding:** Default settings and admin user creation
|
||||||
- **Connection Management:** SQLite with WAL mode and connection pooling
|
- **Connection Management:** SQLite with WAL mode and connection pooling
|
||||||
|
|
||||||
**Schema Overview:**
|
**Schema Overview:**
|
||||||
|
|
||||||
- **ProxyHost:** Domain, upstream target, SSL config
|
- **ProxyHost:** Domain, upstream target, SSL config
|
||||||
- **RemoteServer:** Upstream server definitions
|
- **RemoteServer:** Upstream server definitions
|
||||||
- **CaddyConfig:** Generated Caddy configuration (audit trail)
|
- **CaddyConfig:** Generated Caddy configuration (audit trail)
|
||||||
@@ -372,6 +381,7 @@ graph TB
|
|||||||
**Component Architecture:**
|
**Component Architecture:**
|
||||||
|
|
||||||
#### Pages (`src/pages/`)
|
#### Pages (`src/pages/`)
|
||||||
|
|
||||||
- **Dashboard:** System overview, recent activity, quick actions
|
- **Dashboard:** System overview, recent activity, quick actions
|
||||||
- **ProxyHosts:** List, create, edit, delete proxy configurations
|
- **ProxyHosts:** List, create, edit, delete proxy configurations
|
||||||
- **Certificates:** Manage SSL/TLS certificates, view expiry
|
- **Certificates:** Manage SSL/TLS certificates, view expiry
|
||||||
@@ -380,17 +390,20 @@ graph TB
|
|||||||
- **Users:** User management (admin only)
|
- **Users:** User management (admin only)
|
||||||
|
|
||||||
#### Components (`src/components/`)
|
#### Components (`src/components/`)
|
||||||
|
|
||||||
- **Forms:** Reusable form inputs with validation
|
- **Forms:** Reusable form inputs with validation
|
||||||
- **Modals:** Dialog components for CRUD operations
|
- **Modals:** Dialog components for CRUD operations
|
||||||
- **Tables:** Data tables with sorting, filtering, pagination
|
- **Tables:** Data tables with sorting, filtering, pagination
|
||||||
- **Layout:** Header, sidebar, navigation
|
- **Layout:** Header, sidebar, navigation
|
||||||
|
|
||||||
#### API Client (`src/api/`)
|
#### API Client (`src/api/`)
|
||||||
|
|
||||||
- Centralized API calls with error handling
|
- Centralized API calls with error handling
|
||||||
- Request/response type definitions
|
- Request/response type definitions
|
||||||
- Authentication token management
|
- Authentication token management
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const getProxyHosts = async (): Promise<ProxyHost[]> => {
|
export const getProxyHosts = async (): Promise<ProxyHost[]> => {
|
||||||
const response = await fetch('/api/v1/proxy-hosts', {
|
const response = await fetch('/api/v1/proxy-hosts', {
|
||||||
@@ -402,11 +415,13 @@ export const getProxyHosts = async (): Promise<ProxyHost[]> => {
|
|||||||
```
|
```
|
||||||
|
|
||||||
#### State Management
|
#### State Management
|
||||||
|
|
||||||
- **React Context:** Global state for auth, theme, language
|
- **React Context:** Global state for auth, theme, language
|
||||||
- **Local State:** Component-specific state with `useState`
|
- **Local State:** Component-specific state with `useState`
|
||||||
- **Custom Hooks:** Encapsulate API calls and side effects
|
- **Custom Hooks:** Encapsulate API calls and side effects
|
||||||
|
|
||||||
**Example Hook:**
|
**Example Hook:**
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
export const useProxyHosts = () => {
|
export const useProxyHosts = () => {
|
||||||
const [hosts, setHosts] = useState<ProxyHost[]>([]);
|
const [hosts, setHosts] = useState<ProxyHost[]>([]);
|
||||||
@@ -425,11 +440,13 @@ export const useProxyHosts = () => {
|
|||||||
**Purpose:** High-performance reverse proxy with automatic HTTPS
|
**Purpose:** High-performance reverse proxy with automatic HTTPS
|
||||||
|
|
||||||
**Integration:**
|
**Integration:**
|
||||||
|
|
||||||
- Embedded as a library in the Go backend
|
- Embedded as a library in the Go backend
|
||||||
- Configured via JSON API (not Caddyfile)
|
- Configured via JSON API (not Caddyfile)
|
||||||
- Listens on ports 80 (HTTP) and 443 (HTTPS)
|
- Listens on ports 80 (HTTP) and 443 (HTTPS)
|
||||||
|
|
||||||
**Features Used:**
|
**Features Used:**
|
||||||
|
|
||||||
- Dynamic configuration updates without restarts
|
- Dynamic configuration updates without restarts
|
||||||
- Automatic HTTPS with Let's Encrypt and ZeroSSL
|
- Automatic HTTPS with Let's Encrypt and ZeroSSL
|
||||||
- DNS challenge support for wildcard certificates
|
- DNS challenge support for wildcard certificates
|
||||||
@@ -437,6 +454,7 @@ export const useProxyHosts = () => {
|
|||||||
- Request logging and metrics
|
- Request logging and metrics
|
||||||
|
|
||||||
**Configuration Flow:**
|
**Configuration Flow:**
|
||||||
|
|
||||||
1. User creates proxy host via frontend
|
1. User creates proxy host via frontend
|
||||||
2. Backend validates and saves to database
|
2. Backend validates and saves to database
|
||||||
3. Caddy Manager generates JSON configuration
|
3. Caddy Manager generates JSON configuration
|
||||||
@@ -461,12 +479,14 @@ For each proxy host, Charon generates **two routes** with the same domain:
|
|||||||
- Handlers: Full Cerberus security suite
|
- Handlers: Full Cerberus security suite
|
||||||
|
|
||||||
This pattern is **intentional and valid**:
|
This pattern is **intentional and valid**:
|
||||||
|
|
||||||
- Emergency route provides break-glass access to security controls
|
- Emergency route provides break-glass access to security controls
|
||||||
- Main route protects application with enterprise security features
|
- Main route protects application with enterprise security features
|
||||||
- Caddy processes routes in order (emergency matches first)
|
- Caddy processes routes in order (emergency matches first)
|
||||||
- Validator allows duplicate hosts when one has paths and one doesn't
|
- Validator allows duplicate hosts when one has paths and one doesn't
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
// Emergency Route (evaluated first)
|
// Emergency Route (evaluated first)
|
||||||
{
|
{
|
||||||
@@ -488,6 +508,7 @@ This pattern is **intentional and valid**:
|
|||||||
**Purpose:** Persistent data storage
|
**Purpose:** Persistent data storage
|
||||||
|
|
||||||
**Why SQLite:**
|
**Why SQLite:**
|
||||||
|
|
||||||
- Embedded (no external database server)
|
- Embedded (no external database server)
|
||||||
- Serverless (perfect for single-user/small team)
|
- Serverless (perfect for single-user/small team)
|
||||||
- ACID compliant with WAL mode
|
- ACID compliant with WAL mode
|
||||||
@@ -495,16 +516,19 @@ This pattern is **intentional and valid**:
|
|||||||
- Backup-friendly (single file)
|
- Backup-friendly (single file)
|
||||||
|
|
||||||
**Configuration:**
|
**Configuration:**
|
||||||
|
|
||||||
- **WAL Mode:** Allows concurrent reads during writes
|
- **WAL Mode:** Allows concurrent reads during writes
|
||||||
- **Foreign Keys:** Enforced referential integrity
|
- **Foreign Keys:** Enforced referential integrity
|
||||||
- **Pragma Settings:** Performance optimizations
|
- **Pragma Settings:** Performance optimizations
|
||||||
|
|
||||||
**Backup Strategy:**
|
**Backup Strategy:**
|
||||||
|
|
||||||
- Automated daily backups to `data/backups/`
|
- Automated daily backups to `data/backups/`
|
||||||
- Retention: 7 daily, 4 weekly, 12 monthly backups
|
- Retention: 7 daily, 4 weekly, 12 monthly backups
|
||||||
- Backup during low-traffic periods
|
- Backup during low-traffic periods
|
||||||
|
|
||||||
**Migrations:**
|
**Migrations:**
|
||||||
|
|
||||||
- GORM AutoMigrate for schema changes
|
- GORM AutoMigrate for schema changes
|
||||||
- Manual migrations for complex data transformations
|
- Manual migrations for complex data transformations
|
||||||
- Rollback support via backup restoration
|
- Rollback support via backup restoration
|
||||||
@@ -537,6 +561,7 @@ graph LR
|
|||||||
**Purpose:** Prevent brute-force attacks and API abuse
|
**Purpose:** Prevent brute-force attacks and API abuse
|
||||||
|
|
||||||
**Implementation:**
|
**Implementation:**
|
||||||
|
|
||||||
- Per-IP request counters with sliding window
|
- Per-IP request counters with sliding window
|
||||||
- Configurable thresholds (e.g., 100 req/min, 1000 req/hour)
|
- Configurable thresholds (e.g., 100 req/min, 1000 req/hour)
|
||||||
- HTTP 429 response when limit exceeded
|
- HTTP 429 response when limit exceeded
|
||||||
@@ -547,12 +572,15 @@ graph LR
|
|||||||
**Purpose:** Behavior-based threat detection
|
**Purpose:** Behavior-based threat detection
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
- Local log analysis (brute-force, port scans, exploits)
|
- Local log analysis (brute-force, port scans, exploits)
|
||||||
- Global threat intelligence (crowd-sourced IP reputation)
|
- Global threat intelligence (crowd-sourced IP reputation)
|
||||||
- Automatic IP banning with configurable duration
|
- Automatic IP banning with configurable duration
|
||||||
- Decision management API (view, create, delete bans)
|
- Decision management API (view, create, delete bans)
|
||||||
|
- IP whitelist management: operators add/remove IPs and CIDRs via the management UI; entries are persisted in SQLite and regenerated into a `crowdsecurity/whitelists` parser YAML on every mutating operation and at startup
|
||||||
|
|
||||||
**Modes:**
|
**Modes:**
|
||||||
|
|
||||||
- **Local Only:** No external API calls
|
- **Local Only:** No external API calls
|
||||||
- **API Mode:** Sync with CrowdSec cloud for global intelligence
|
- **API Mode:** Sync with CrowdSec cloud for global intelligence
|
||||||
|
|
||||||
@@ -561,12 +589,14 @@ graph LR
|
|||||||
**Purpose:** IP-based access control
|
**Purpose:** IP-based access control
|
||||||
|
|
||||||
**Features:**
|
**Features:**
|
||||||
|
|
||||||
- Per-proxy-host allow/deny rules
|
- Per-proxy-host allow/deny rules
|
||||||
- CIDR range support (e.g., `192.168.1.0/24`)
|
- CIDR range support (e.g., `192.168.1.0/24`)
|
||||||
- Geographic blocking via GeoIP2 (MaxMind)
|
- Geographic blocking via GeoIP2 (MaxMind)
|
||||||
- Admin whitelist (emergency access)
|
- Admin whitelist (emergency access)
|
||||||
|
|
||||||
**Evaluation Order:**
|
**Evaluation Order:**
|
||||||
|
|
||||||
1. Check admin whitelist (always allow)
|
1. Check admin whitelist (always allow)
|
||||||
2. Check deny list (explicit block)
|
2. Check deny list (explicit block)
|
||||||
3. Check allow list (explicit allow)
|
3. Check allow list (explicit allow)
|
||||||
@@ -579,6 +609,7 @@ graph LR
|
|||||||
**Engine:** Coraza with OWASP Core Rule Set (CRS)
|
**Engine:** Coraza with OWASP Core Rule Set (CRS)
|
||||||
|
|
||||||
**Detection Categories:**
|
**Detection Categories:**
|
||||||
|
|
||||||
- SQL Injection (SQLi)
|
- SQL Injection (SQLi)
|
||||||
- Cross-Site Scripting (XSS)
|
- Cross-Site Scripting (XSS)
|
||||||
- Remote Code Execution (RCE)
|
- Remote Code Execution (RCE)
|
||||||
@@ -587,12 +618,14 @@ graph LR
|
|||||||
- Command Injection
|
- Command Injection
|
||||||
|
|
||||||
**Modes:**
|
**Modes:**
|
||||||
|
|
||||||
- **Monitor:** Log but don't block (testing)
|
- **Monitor:** Log but don't block (testing)
|
||||||
- **Block:** Return HTTP 403 for violations
|
- **Block:** Return HTTP 403 for violations
|
||||||
|
|
||||||
### Layer 5: Application Security
|
### Layer 5: Application Security
|
||||||
|
|
||||||
**Additional Protections:**
|
**Additional Protections:**
|
||||||
|
|
||||||
- **SSRF Prevention:** Block requests to private IP ranges in webhooks/URL validation
|
- **SSRF Prevention:** Block requests to private IP ranges in webhooks/URL validation
|
||||||
- **HTTP Security Headers:** CSP, HSTS, X-Frame-Options, X-Content-Type-Options
|
- **HTTP Security Headers:** CSP, HSTS, X-Frame-Options, X-Content-Type-Options
|
||||||
- **Input Validation:** Server-side validation for all user inputs
|
- **Input Validation:** Server-side validation for all user inputs
|
||||||
@@ -610,6 +643,7 @@ graph LR
|
|||||||
3. **Direct Database Access:** Manual SQLite update as last resort
|
3. **Direct Database Access:** Manual SQLite update as last resort
|
||||||
|
|
||||||
**Emergency Token:**
|
**Emergency Token:**
|
||||||
|
|
||||||
- 64-character hex token set via `CHARON_EMERGENCY_TOKEN`
|
- 64-character hex token set via `CHARON_EMERGENCY_TOKEN`
|
||||||
- Grants temporary admin access
|
- Grants temporary admin access
|
||||||
- Rotated after each use
|
- Rotated after each use
|
||||||
@@ -635,6 +669,7 @@ Charon operates with **two distinct traffic flows** on separate ports, each with
|
|||||||
- **Testing:** Playwright E2E tests verify UI/UX functionality on this port
|
- **Testing:** Playwright E2E tests verify UI/UX functionality on this port
|
||||||
|
|
||||||
**Why No Middleware?**
|
**Why No Middleware?**
|
||||||
|
|
||||||
- Management interface must remain accessible even when security modules are misconfigured
|
- Management interface must remain accessible even when security modules are misconfigured
|
||||||
- Emergency endpoints (`/api/v1/emergency/*`) require unrestricted access for system recovery
|
- Emergency endpoints (`/api/v1/emergency/*`) require unrestricted access for system recovery
|
||||||
- Separation of concerns: admin access control is handled by JWT, not proxy-level security
|
- Separation of concerns: admin access control is handled by JWT, not proxy-level security
|
||||||
@@ -797,6 +832,7 @@ sequenceDiagram
|
|||||||
**Rationale:** Simplicity over scalability - target audience is home users and small teams
|
**Rationale:** Simplicity over scalability - target audience is home users and small teams
|
||||||
|
|
||||||
**Container Contents:**
|
**Container Contents:**
|
||||||
|
|
||||||
- Frontend static files (Vite build output)
|
- Frontend static files (Vite build output)
|
||||||
- Go backend binary
|
- Go backend binary
|
||||||
- Embedded Caddy server
|
- Embedded Caddy server
|
||||||
@@ -911,11 +947,13 @@ services:
|
|||||||
### High Availability Considerations
|
### High Availability Considerations
|
||||||
|
|
||||||
**Current Limitations:**
|
**Current Limitations:**
|
||||||
|
|
||||||
- SQLite does not support clustering
|
- SQLite does not support clustering
|
||||||
- Single point of failure (one container)
|
- Single point of failure (one container)
|
||||||
- Not designed for horizontal scaling
|
- Not designed for horizontal scaling
|
||||||
|
|
||||||
**Future Options:**
|
**Future Options:**
|
||||||
|
|
||||||
- PostgreSQL backend for HA deployments
|
- PostgreSQL backend for HA deployments
|
||||||
- Read replicas for load balancing
|
- Read replicas for load balancing
|
||||||
- Container orchestration (Kubernetes, Docker Swarm)
|
- Container orchestration (Kubernetes, Docker Swarm)
|
||||||
@@ -927,6 +965,7 @@ services:
|
|||||||
### Local Development Setup
|
### Local Development Setup
|
||||||
|
|
||||||
1. **Prerequisites:**
|
1. **Prerequisites:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
- Go 1.26+ (backend development)
|
- Go 1.26+ (backend development)
|
||||||
- Node.js 23+ and npm (frontend development)
|
- Node.js 23+ and npm (frontend development)
|
||||||
@@ -935,12 +974,14 @@ services:
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Clone Repository:**
|
2. **Clone Repository:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git clone https://github.com/Wikid82/Charon.git
|
git clone https://github.com/Wikid82/Charon.git
|
||||||
cd Charon
|
cd Charon
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Backend Development:**
|
3. **Backend Development:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
go mod download
|
go mod download
|
||||||
@@ -949,6 +990,7 @@ services:
|
|||||||
```
|
```
|
||||||
|
|
||||||
4. **Frontend Development:**
|
4. **Frontend Development:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
@@ -957,6 +999,7 @@ services:
|
|||||||
```
|
```
|
||||||
|
|
||||||
5. **Full-Stack Development (Docker):**
|
5. **Full-Stack Development (Docker):**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose -f .docker/compose/docker-compose.dev.yml up
|
docker-compose -f .docker/compose/docker-compose.dev.yml up
|
||||||
# Frontend + Backend + Caddy in one container
|
# Frontend + Backend + Caddy in one container
|
||||||
@@ -965,12 +1008,14 @@ services:
|
|||||||
### Git Workflow
|
### Git Workflow
|
||||||
|
|
||||||
**Branch Strategy:**
|
**Branch Strategy:**
|
||||||
|
|
||||||
- `main`: Stable production branch
|
- `main`: Stable production branch
|
||||||
- `feature/*`: New feature development
|
- `feature/*`: New feature development
|
||||||
- `fix/*`: Bug fixes
|
- `fix/*`: Bug fixes
|
||||||
- `chore/*`: Maintenance tasks
|
- `chore/*`: Maintenance tasks
|
||||||
|
|
||||||
**Commit Convention:**
|
**Commit Convention:**
|
||||||
|
|
||||||
- `feat:` New user-facing feature
|
- `feat:` New user-facing feature
|
||||||
- `fix:` Bug fix in application code
|
- `fix:` Bug fix in application code
|
||||||
- `chore:` Infrastructure, CI/CD, dependencies
|
- `chore:` Infrastructure, CI/CD, dependencies
|
||||||
@@ -979,6 +1024,7 @@ services:
|
|||||||
- `test:` Adding or updating tests
|
- `test:` Adding or updating tests
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```
|
```
|
||||||
feat: add DNS-01 challenge support for Cloudflare
|
feat: add DNS-01 challenge support for Cloudflare
|
||||||
|
|
||||||
@@ -1031,6 +1077,7 @@ Closes #123
|
|||||||
**Purpose:** Validate critical user flows in a real browser
|
**Purpose:** Validate critical user flows in a real browser
|
||||||
|
|
||||||
**Scope:**
|
**Scope:**
|
||||||
|
|
||||||
- User authentication
|
- User authentication
|
||||||
- Proxy host CRUD operations
|
- Proxy host CRUD operations
|
||||||
- Certificate provisioning
|
- Certificate provisioning
|
||||||
@@ -1038,6 +1085,7 @@ Closes #123
|
|||||||
- Real-time log streaming
|
- Real-time log streaming
|
||||||
|
|
||||||
**Execution:**
|
**Execution:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run against Docker container
|
# Run against Docker container
|
||||||
npx playwright test --project=chromium
|
npx playwright test --project=chromium
|
||||||
@@ -1050,10 +1098,12 @@ npx playwright test --debug
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Coverage Modes:**
|
**Coverage Modes:**
|
||||||
|
|
||||||
- **Docker Mode:** Integration testing, no coverage (0% reported)
|
- **Docker Mode:** Integration testing, no coverage (0% reported)
|
||||||
- **Vite Dev Mode:** Coverage collection with V8 inspector
|
- **Vite Dev Mode:** Coverage collection with V8 inspector
|
||||||
|
|
||||||
**Why Two Modes?**
|
**Why Two Modes?**
|
||||||
|
|
||||||
- Playwright coverage requires source maps and raw source files
|
- Playwright coverage requires source maps and raw source files
|
||||||
- Docker serves pre-built production files (no source maps)
|
- Docker serves pre-built production files (no source maps)
|
||||||
- Vite dev server exposes source files for coverage instrumentation
|
- Vite dev server exposes source files for coverage instrumentation
|
||||||
@@ -1067,6 +1117,7 @@ npx playwright test --debug
|
|||||||
**Coverage Target:** 85% minimum
|
**Coverage Target:** 85% minimum
|
||||||
|
|
||||||
**Execution:**
|
**Execution:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
go test ./...
|
go test ./...
|
||||||
@@ -1079,11 +1130,13 @@ go test -cover ./...
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Test Organization:**
|
**Test Organization:**
|
||||||
|
|
||||||
- `*_test.go` files alongside source code
|
- `*_test.go` files alongside source code
|
||||||
- Table-driven tests for comprehensive coverage
|
- Table-driven tests for comprehensive coverage
|
||||||
- Mocks for external dependencies (database, HTTP clients)
|
- Mocks for external dependencies (database, HTTP clients)
|
||||||
|
|
||||||
**Example:**
|
**Example:**
|
||||||
|
|
||||||
```go
|
```go
|
||||||
func TestCreateProxyHost(t *testing.T) {
|
func TestCreateProxyHost(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -1123,6 +1176,7 @@ func TestCreateProxyHost(t *testing.T) {
|
|||||||
**Coverage Target:** 85% minimum
|
**Coverage Target:** 85% minimum
|
||||||
|
|
||||||
**Execution:**
|
**Execution:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run all tests
|
# Run all tests
|
||||||
npm test
|
npm test
|
||||||
@@ -1135,6 +1189,7 @@ npm run test:coverage
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Test Organization:**
|
**Test Organization:**
|
||||||
|
|
||||||
- `*.test.tsx` files alongside components
|
- `*.test.tsx` files alongside components
|
||||||
- Mock API calls with MSW (Mock Service Worker)
|
- Mock API calls with MSW (Mock Service Worker)
|
||||||
- Snapshot tests for UI consistency
|
- Snapshot tests for UI consistency
|
||||||
@@ -1146,12 +1201,14 @@ npm run test:coverage
|
|||||||
**Location:** `backend/integration/`
|
**Location:** `backend/integration/`
|
||||||
|
|
||||||
**Scope:**
|
**Scope:**
|
||||||
|
|
||||||
- API endpoint end-to-end flows
|
- API endpoint end-to-end flows
|
||||||
- Database migrations
|
- Database migrations
|
||||||
- Caddy manager integration
|
- Caddy manager integration
|
||||||
- CrowdSec API calls
|
- CrowdSec API calls
|
||||||
|
|
||||||
**Execution:**
|
**Execution:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
go test ./integration/...
|
go test ./integration/...
|
||||||
```
|
```
|
||||||
@@ -1161,6 +1218,7 @@ go test ./integration/...
|
|||||||
**Automated Hooks (via `.pre-commit-config.yaml`):**
|
**Automated Hooks (via `.pre-commit-config.yaml`):**
|
||||||
|
|
||||||
**Fast Stage (< 5 seconds):**
|
**Fast Stage (< 5 seconds):**
|
||||||
|
|
||||||
- Trailing whitespace removal
|
- Trailing whitespace removal
|
||||||
- EOF fixer
|
- EOF fixer
|
||||||
- YAML syntax check
|
- YAML syntax check
|
||||||
@@ -1168,11 +1226,13 @@ go test ./integration/...
|
|||||||
- Markdown link validation
|
- Markdown link validation
|
||||||
|
|
||||||
**Manual Stage (run explicitly):**
|
**Manual Stage (run explicitly):**
|
||||||
|
|
||||||
- Backend coverage tests (60-90s)
|
- Backend coverage tests (60-90s)
|
||||||
- Frontend coverage tests (30-60s)
|
- Frontend coverage tests (30-60s)
|
||||||
- TypeScript type checking (10-20s)
|
- TypeScript type checking (10-20s)
|
||||||
|
|
||||||
**Why Manual?**
|
**Why Manual?**
|
||||||
|
|
||||||
- Coverage tests are slow and would block commits
|
- Coverage tests are slow and would block commits
|
||||||
- Developers run them on-demand before pushing
|
- Developers run them on-demand before pushing
|
||||||
- CI enforces coverage on pull requests
|
- CI enforces coverage on pull requests
|
||||||
@@ -1180,10 +1240,12 @@ go test ./integration/...
|
|||||||
### Continuous Integration (GitHub Actions)
|
### Continuous Integration (GitHub Actions)
|
||||||
|
|
||||||
**Workflow Triggers:**
|
**Workflow Triggers:**
|
||||||
|
|
||||||
- `push` to `main`, `feature/*`, `fix/*`
|
- `push` to `main`, `feature/*`, `fix/*`
|
||||||
- `pull_request` to `main`
|
- `pull_request` to `main`
|
||||||
|
|
||||||
**CI Jobs:**
|
**CI Jobs:**
|
||||||
|
|
||||||
1. **Lint:** golangci-lint, ESLint, markdownlint, hadolint
|
1. **Lint:** golangci-lint, ESLint, markdownlint, hadolint
|
||||||
2. **Test:** Go tests, Vitest, Playwright
|
2. **Test:** Go tests, Vitest, Playwright
|
||||||
3. **Security:** Trivy, CodeQL, Grype, Govulncheck
|
3. **Security:** Trivy, CodeQL, Grype, Govulncheck
|
||||||
@@ -1205,6 +1267,7 @@ go test ./integration/...
|
|||||||
- **PRERELEASE:** `-beta.1`, `-rc.1`, etc.
|
- **PRERELEASE:** `-beta.1`, `-rc.1`, etc.
|
||||||
|
|
||||||
**Examples:**
|
**Examples:**
|
||||||
|
|
||||||
- `1.0.0` - Stable release
|
- `1.0.0` - Stable release
|
||||||
- `1.1.0` - New feature (DNS provider support)
|
- `1.1.0` - New feature (DNS provider support)
|
||||||
- `1.1.1` - Bug fix (GORM query fix)
|
- `1.1.1` - Bug fix (GORM query fix)
|
||||||
@@ -1215,12 +1278,14 @@ go test ./integration/...
|
|||||||
### Build Pipeline (Multi-Platform)
|
### Build Pipeline (Multi-Platform)
|
||||||
|
|
||||||
**Platforms Supported:**
|
**Platforms Supported:**
|
||||||
|
|
||||||
- `linux/amd64`
|
- `linux/amd64`
|
||||||
- `linux/arm64`
|
- `linux/arm64`
|
||||||
|
|
||||||
**Build Process:**
|
**Build Process:**
|
||||||
|
|
||||||
1. **Frontend Build:**
|
1. **Frontend Build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm ci --only=production
|
npm ci --only=production
|
||||||
@@ -1229,6 +1294,7 @@ go test ./integration/...
|
|||||||
```
|
```
|
||||||
|
|
||||||
2. **Backend Build:**
|
2. **Backend Build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cd backend
|
cd backend
|
||||||
go build -o charon cmd/api/main.go
|
go build -o charon cmd/api/main.go
|
||||||
@@ -1236,6 +1302,7 @@ go test ./integration/...
|
|||||||
```
|
```
|
||||||
|
|
||||||
3. **Docker Image Build:**
|
3. **Docker Image Build:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker buildx build \
|
docker buildx build \
|
||||||
--platform linux/amd64,linux/arm64 \
|
--platform linux/amd64,linux/arm64 \
|
||||||
@@ -1292,6 +1359,7 @@ go test ./integration/...
|
|||||||
- Level: SLSA Build L3 (hermetic builds)
|
- Level: SLSA Build L3 (hermetic builds)
|
||||||
|
|
||||||
**Verification Example:**
|
**Verification Example:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Verify image signature
|
# Verify image signature
|
||||||
cosign verify \
|
cosign verify \
|
||||||
@@ -1309,6 +1377,7 @@ grype ghcr.io/wikid82/charon@sha256:<index-digest>
|
|||||||
### Rollback Strategy
|
### Rollback Strategy
|
||||||
|
|
||||||
**Container Rollback:**
|
**Container Rollback:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# List available versions
|
# List available versions
|
||||||
docker images wikid82/charon
|
docker images wikid82/charon
|
||||||
@@ -1319,6 +1388,7 @@ docker-compose up -d --pull always wikid82/charon:1.1.1
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Database Rollback:**
|
**Database Rollback:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Restore from backup
|
# Restore from backup
|
||||||
docker exec charon /app/scripts/restore-backup.sh \
|
docker exec charon /app/scripts/restore-backup.sh \
|
||||||
@@ -1355,11 +1425,13 @@ docker exec charon /app/scripts/restore-backup.sh \
|
|||||||
### API Extensibility
|
### API Extensibility
|
||||||
|
|
||||||
**REST API Design:**
|
**REST API Design:**
|
||||||
|
|
||||||
- Version prefix: `/api/v1/`
|
- Version prefix: `/api/v1/`
|
||||||
- Future versions: `/api/v2/` (backward-compatible)
|
- Future versions: `/api/v2/` (backward-compatible)
|
||||||
- Deprecation policy: 2 major versions supported
|
- Deprecation policy: 2 major versions supported
|
||||||
|
|
||||||
**WebHooks (Future):**
|
**WebHooks (Future):**
|
||||||
|
|
||||||
- Event notifications for external systems
|
- Event notifications for external systems
|
||||||
- Triggers: Proxy host created, certificate renewed, security event
|
- Triggers: Proxy host created, certificate renewed, security event
|
||||||
- Payload: JSON with event type and data
|
- Payload: JSON with event type and data
|
||||||
@@ -1369,6 +1441,7 @@ docker exec charon /app/scripts/restore-backup.sh \
|
|||||||
**Current:** Cerberus security middleware injected into Caddy pipeline
|
**Current:** Cerberus security middleware injected into Caddy pipeline
|
||||||
|
|
||||||
**Future:**
|
**Future:**
|
||||||
|
|
||||||
- User-defined middleware (rate limiting rules, custom headers)
|
- User-defined middleware (rate limiting rules, custom headers)
|
||||||
- JavaScript/Lua scripting for request transformation
|
- JavaScript/Lua scripting for request transformation
|
||||||
- Plugin marketplace for community contributions
|
- Plugin marketplace for community contributions
|
||||||
@@ -1452,6 +1525,7 @@ docker exec charon /app/scripts/restore-backup.sh \
|
|||||||
**GitHub Copilot Instructions:**
|
**GitHub Copilot Instructions:**
|
||||||
|
|
||||||
All agents (`Planning`, `Backend_Dev`, `Frontend_Dev`, `DevOps`) must reference `ARCHITECTURE.md` when:
|
All agents (`Planning`, `Backend_Dev`, `Frontend_Dev`, `DevOps`) must reference `ARCHITECTURE.md` when:
|
||||||
|
|
||||||
- Creating new components
|
- Creating new components
|
||||||
- Modifying core systems
|
- Modifying core systems
|
||||||
- Changing integration points
|
- Changing integration points
|
||||||
|
|||||||
50
CHANGELOG.md
50
CHANGELOG.md
@@ -7,17 +7,55 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **CrowdSec Dashboard**: Visual analytics for CrowdSec security data within the Security section
|
||||||
|
- Summary cards showing total bans, active bans, unique IPs, and top scenario
|
||||||
|
- Interactive charts: ban timeline (area), top attacking IPs (bar), scenario breakdown (pie)
|
||||||
|
- Configurable time range selector (1h, 6h, 24h, 7d, 30d)
|
||||||
|
- Active decisions table with IP, scenario, duration, type, and time remaining
|
||||||
|
- Alerts feed with pagination sourced from CrowdSec LAPI
|
||||||
|
- CSV and JSON export for decisions data
|
||||||
|
- Server-side caching (30–60s TTL) for fast dashboard loads
|
||||||
|
- Full i18n support across all 5 locales (en, de, fr, es, zh)
|
||||||
|
- Keyboard navigable, screen-reader compatible (WCAG 2.2 AA)
|
||||||
|
|
||||||
|
- **Notifications:** Added Ntfy notification provider with support for self-hosted and cloud instances, optional Bearer token authentication, and JSON template customization
|
||||||
|
|
||||||
|
- **Certificate Deletion**: Clean up expired and unused certificates directly from the Certificates page
|
||||||
|
- Expired Let's Encrypt certificates not attached to any proxy host can now be deleted
|
||||||
|
- Custom and staging certificates remain deletable when not in use
|
||||||
|
- In-use certificates show a disabled delete button with a tooltip explaining why
|
||||||
|
- Native browser confirmation replaced with an accessible, themed confirmation dialog
|
||||||
|
|
||||||
|
- **Pushover Notification Provider**: Send push notifications to your devices via the Pushover app
|
||||||
|
- Supports JSON templates (minimal, detailed, custom)
|
||||||
|
- Application API Token stored securely — never exposed in API responses
|
||||||
|
- User Key stored in the URL field, following the same pattern as Telegram
|
||||||
|
- Feature flag: `feature.notifications.service.pushover.enabled` (on by default)
|
||||||
|
- Emergency priority (2) is intentionally unsupported — deferred to a future release
|
||||||
|
|
||||||
|
- **Slack Notification Provider**: Send alerts to Slack channels via Incoming Webhooks
|
||||||
|
- Supports JSON templates (minimal, detailed, custom) with Slack's native `text` format
|
||||||
|
- Webhook URL stored securely — never exposed in API responses
|
||||||
|
- Optional channel display name for easy identification in provider list
|
||||||
|
- Feature flag: `feature.notifications.service.slack.enabled` (on by default)
|
||||||
|
- See [Notification Guide](docs/features/notifications.md) for setup instructions
|
||||||
|
|
||||||
### CI/CD
|
### CI/CD
|
||||||
|
|
||||||
- **Supply Chain**: Optimized verification workflow to prevent redundant builds
|
- **Supply Chain**: Optimized verification workflow to prevent redundant builds
|
||||||
- Change: Removed direct Push/PR triggers; now waits for 'Docker Build' via `workflow_run`
|
- Change: Removed direct Push/PR triggers; now waits for 'Docker Build' via `workflow_run`
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
- **Supply Chain**: Enhanced PR verification workflow stability and accuracy
|
- **Supply Chain**: Enhanced PR verification workflow stability and accuracy
|
||||||
- **Vulnerability Reporting**: Eliminated false negatives ("0 vulnerabilities") by enforcing strict failure conditions
|
- **Vulnerability Reporting**: Eliminated false negatives ("0 vulnerabilities") by enforcing strict failure conditions
|
||||||
- **Tooling**: Switched to manual Grype installation ensuring usage of latest stable binary
|
- **Tooling**: Switched to manual Grype installation ensuring usage of latest stable binary
|
||||||
- **Observability**: Improved debugging visibility for vulnerability scans and SARIF generation
|
- **Observability**: Improved debugging visibility for vulnerability scans and SARIF generation
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
- **E2E Tests**: Reduced feature flag API calls by 90% through conditional polling optimization (Phase 2)
|
- **E2E Tests**: Reduced feature flag API calls by 90% through conditional polling optimization (Phase 2)
|
||||||
- Conditional skip: Exits immediately if flags already in expected state (~50% of cases)
|
- Conditional skip: Exits immediately if flags already in expected state (~50% of cases)
|
||||||
- Request coalescing: Shares in-flight API requests between parallel test workers
|
- Request coalescing: Shares in-flight API requests between parallel test workers
|
||||||
@@ -29,6 +67,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- Prevents timeout errors in Firefox/WebKit caused by strict label matching
|
- Prevents timeout errors in Firefox/WebKit caused by strict label matching
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **Notifications:** Fixed Pushover token-clearing bug where tokens were silently stripped on provider create/update
|
||||||
|
- **TCP Monitor Creation**: Fixed misleading form UX that caused silent HTTP 500 errors when creating TCP monitors
|
||||||
|
- Corrected URL placeholder to show `host:port` format instead of the incorrect `tcp://host:port` prefix
|
||||||
|
- Added dynamic per-type placeholder and helper text (HTTP monitors show a full URL example; TCP monitors show `host:port`)
|
||||||
|
- Added client-side validation that blocks form submission when a scheme prefix (e.g. `tcp://`) is detected, with an inline error message
|
||||||
|
- Reordered form fields so the monitor type selector appears above the URL input, making the dynamic helper text immediately relevant
|
||||||
|
- i18n: Added 5 new translation keys across en, de, fr, es, and zh locales
|
||||||
|
- **CI: Rate Limit Integration Tests**: Hardened test script reliability — login now validates HTTP status, Caddy admin API readiness gated on `/config/` poll, security config failures are fatal with full diagnostics, and poll interval increased to 5s
|
||||||
|
- **CI: Rate Limit Integration Tests**: Removed stale GeoIP database SHA256 checksum from Dockerfile non-CI path (hash was perpetually stale due to weekly upstream updates)
|
||||||
|
- **CI: Rate Limit Integration Tests**: Fixed Caddy admin API debug dump URL to use canonical trailing slash in workflow
|
||||||
- Fixed: Added robust validation and debug logging for Docker image tags to prevent invalid reference errors.
|
- Fixed: Added robust validation and debug logging for Docker image tags to prevent invalid reference errors.
|
||||||
- Fixed: Removed log masking for image references and added manifest validation to debug CI failures.
|
- Fixed: Removed log masking for image references and added manifest validation to debug CI failures.
|
||||||
- **Proxy Hosts**: Fixed ACL and Security Headers dropdown selections so create/edit saves now keep the selected values (including clearing to none) after submit and reload.
|
- **Proxy Hosts**: Fixed ACL and Security Headers dropdown selections so create/edit saves now keep the selected values (including clearing to none) after submit and reload.
|
||||||
@@ -41,6 +90,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||||||
- **Test Performance**: Reduced system settings test execution time by 31% (from 23 minutes to 16 minutes)
|
- **Test Performance**: Reduced system settings test execution time by 31% (from 23 minutes to 16 minutes)
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- **Testing Infrastructure**: Enhanced E2E test helpers with better synchronization and error handling
|
- **Testing Infrastructure**: Enhanced E2E test helpers with better synchronization and error handling
|
||||||
- **CI**: Optimized E2E workflow shards [Reduced from 4 to 3]
|
- **CI**: Optimized E2E workflow shards [Reduced from 4 to 3]
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,17 @@ This project follows a Code of Conduct that all contributors are expected to adh
|
|||||||
|
|
||||||
### Development Tools
|
### Development Tools
|
||||||
|
|
||||||
Install golangci-lint for pre-commit hooks (required for Go development):
|
Install golangci-lint for lefthook pre-commit-phase hooks (required for Go development):
|
||||||
|
|
||||||
|
Also install lefthook itself so the git hooks work:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Option 1: Homebrew (macOS/Linux)
|
||||||
|
brew install lefthook
|
||||||
|
|
||||||
|
# Option 2: Go install
|
||||||
|
go install github.com/evilmartians/lefthook@latest
|
||||||
|
```
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Option 1: Homebrew (macOS/Linux)
|
# Option 1: Homebrew (macOS/Linux)
|
||||||
@@ -59,7 +69,7 @@ golangci-lint --version
|
|||||||
# Should output: golangci-lint has version 1.xx.x ...
|
# Should output: golangci-lint has version 1.xx.x ...
|
||||||
```
|
```
|
||||||
|
|
||||||
**Note:** Pre-commit hooks will **BLOCK commits** if golangci-lint finds issues. This is intentional - fix the issues before committing.
|
**Note:** Lefthook pre-commit-phase hooks will **BLOCK commits** if golangci-lint finds issues. This is intentional - fix the issues before committing.
|
||||||
|
|
||||||
### CI/CD Go Version Management
|
### CI/CD Go Version Management
|
||||||
|
|
||||||
@@ -72,19 +82,22 @@ For local development, install go 1.26.0+ from [go.dev/dl](https://go.dev/dl/).
|
|||||||
When the project's Go version is updated (usually by Renovate):
|
When the project's Go version is updated (usually by Renovate):
|
||||||
|
|
||||||
1. **Pull the latest changes**
|
1. **Pull the latest changes**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
git pull
|
git pull
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Update your local Go installation**
|
2. **Update your local Go installation**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run the Go update skill (downloads and installs the new version)
|
# Run the Go update skill (downloads and installs the new version)
|
||||||
.github/skills/scripts/skill-runner.sh utility-update-go-version
|
.github/skills/scripts/skill-runner.sh utility-update-go-version
|
||||||
```
|
```
|
||||||
|
|
||||||
3. **Rebuild your development tools**
|
3. **Rebuild your development tools**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# This fixes pre-commit hook errors and IDE issues
|
# This fixes lefthook hook errors and IDE issues
|
||||||
./scripts/rebuild-go-tools.sh
|
./scripts/rebuild-go-tools.sh
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -104,7 +117,7 @@ Rebuilding tools with `./scripts/rebuild-go-tools.sh` fixes this by compiling th
|
|||||||
|
|
||||||
**What if I forget?**
|
**What if I forget?**
|
||||||
|
|
||||||
Don't worry! The pre-commit hook will detect the version mismatch and automatically rebuild tools for you. You'll see:
|
Don't worry! The lefthook pre-commit hook will detect the version mismatch and automatically rebuild tools for you. You'll see:
|
||||||
|
|
||||||
```
|
```
|
||||||
⚠️ golangci-lint Go version mismatch:
|
⚠️ golangci-lint Go version mismatch:
|
||||||
|
|||||||
188
Dockerfile
188
Dockerfile
@@ -8,24 +8,49 @@ ARG VCS_REF
|
|||||||
# Set BUILD_DEBUG=1 to build with debug symbols (required for Delve debugging)
|
# Set BUILD_DEBUG=1 to build with debug symbols (required for Delve debugging)
|
||||||
ARG BUILD_DEBUG=0
|
ARG BUILD_DEBUG=0
|
||||||
|
|
||||||
|
# ---- Pinned Toolchain Versions ----
|
||||||
|
# renovate: datasource=docker depName=golang versioning=docker
|
||||||
|
ARG GO_VERSION=1.26.2
|
||||||
|
|
||||||
|
# renovate: datasource=docker depName=alpine versioning=docker
|
||||||
|
ARG ALPINE_IMAGE=alpine:3.23.4@sha256:5b10f432ef3da1b8d4c7eb6c487f2f5a8f096bc91145e68878dd4a5019afde11
|
||||||
|
|
||||||
|
# ---- Shared CrowdSec Version ----
|
||||||
|
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
|
||||||
|
ARG CROWDSEC_VERSION=1.7.7
|
||||||
|
# CrowdSec fallback tarball checksum (v${CROWDSEC_VERSION})
|
||||||
|
ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd
|
||||||
|
|
||||||
|
# ---- Shared Go Security Patches ----
|
||||||
|
# renovate: datasource=go depName=github.com/expr-lang/expr
|
||||||
|
ARG EXPR_LANG_VERSION=1.17.8
|
||||||
|
# renovate: datasource=go depName=golang.org/x/net
|
||||||
|
ARG XNET_VERSION=0.53.0
|
||||||
|
# renovate: datasource=go depName=github.com/smallstep/certificates
|
||||||
|
ARG SMALLSTEP_CERTIFICATES_VERSION=0.30.0
|
||||||
|
# renovate: datasource=npm depName=npm
|
||||||
|
ARG NPM_VERSION=11.11.1
|
||||||
|
|
||||||
# Allow pinning Caddy version - Renovate will update this
|
# Allow pinning Caddy version - Renovate will update this
|
||||||
# Build the most recent Caddy 2.x release (keeps major pinned under v3).
|
# Build the most recent Caddy 2.x release (keeps major pinned under v3).
|
||||||
# Setting this to '2' tells xcaddy to resolve the latest v2.x tag so we
|
# Setting this to '2' tells xcaddy to resolve the latest v2.x tag so we
|
||||||
# avoid accidentally pulling a v3 major release. Renovate can still update
|
# avoid accidentally pulling a v3 major release. Renovate can still update
|
||||||
# this ARG to a specific v2.x tag when desired.
|
# this ARG to a specific v2.x tag when desired.
|
||||||
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
|
## Try to build the requested Caddy v2.x tag (Renovate can update this ARG).
|
||||||
## If the requested tag isn't available, fall back to a known-good v2.11.1 build.
|
## If the requested tag isn't available, fall back to a known-good v2.11.2 build.
|
||||||
ARG CADDY_VERSION=2.11.1
|
ARG CADDY_VERSION=2.11.2
|
||||||
ARG CADDY_CANDIDATE_VERSION=2.11.1
|
ARG CADDY_CANDIDATE_VERSION=2.11.2
|
||||||
ARG CADDY_USE_CANDIDATE=0
|
ARG CADDY_USE_CANDIDATE=0
|
||||||
ARG CADDY_PATCH_SCENARIO=B
|
ARG CADDY_PATCH_SCENARIO=B
|
||||||
|
# renovate: datasource=go depName=github.com/greenpau/caddy-security
|
||||||
|
ARG CADDY_SECURITY_VERSION=1.1.62
|
||||||
|
# renovate: datasource=go depName=github.com/corazawaf/coraza-caddy
|
||||||
|
ARG CORAZA_CADDY_VERSION=2.5.0
|
||||||
## When an official caddy image tag isn't available on the host, use a
|
## When an official caddy image tag isn't available on the host, use a
|
||||||
## plain Alpine base image and overwrite its caddy binary with our
|
## plain Alpine base image and overwrite its caddy binary with our
|
||||||
## xcaddy-built binary in the later COPY step. This avoids relying on
|
## xcaddy-built binary in the later COPY step. This avoids relying on
|
||||||
## upstream caddy image tags while still shipping a pinned caddy binary.
|
## upstream caddy image tags while still shipping a pinned caddy binary.
|
||||||
## Alpine 3.23 base to reduce glibc CVE exposure and image size.
|
## Alpine 3.23 base to reduce glibc CVE exposure and image size.
|
||||||
# renovate: datasource=docker depName=alpine versioning=docker
|
|
||||||
ARG CADDY_IMAGE=alpine:3.23.3
|
|
||||||
|
|
||||||
# ---- Cross-Compilation Helpers ----
|
# ---- Cross-Compilation Helpers ----
|
||||||
# renovate: datasource=docker depName=tonistiigi/xx
|
# renovate: datasource=docker depName=tonistiigi/xx
|
||||||
@@ -36,8 +61,7 @@ FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f9
|
|||||||
# This fixes 22 HIGH/CRITICAL CVEs in stdlib embedded in Debian's gosu package
|
# This fixes 22 HIGH/CRITICAL CVEs in stdlib embedded in Debian's gosu package
|
||||||
# CVEs fixed: CVE-2023-24531, CVE-2023-24540, CVE-2023-29402, CVE-2023-29404,
|
# CVEs fixed: CVE-2023-24531, CVE-2023-24540, CVE-2023-29402, CVE-2023-29404,
|
||||||
# CVE-2023-29405, CVE-2024-24790, CVE-2025-22871, and 15 more
|
# CVE-2023-29405, CVE-2024-24790, CVE-2025-22871, and 15 more
|
||||||
# renovate: datasource=docker depName=golang
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS gosu-builder
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS gosu-builder
|
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
|
|
||||||
WORKDIR /tmp/gosu
|
WORKDIR /tmp/gosu
|
||||||
@@ -68,7 +92,7 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
# ---- Frontend Builder ----
|
# ---- Frontend Builder ----
|
||||||
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
|
# Build the frontend using the BUILDPLATFORM to avoid arm64 musl Rollup native issues
|
||||||
# renovate: datasource=docker depName=node
|
# renovate: datasource=docker depName=node
|
||||||
FROM --platform=$BUILDPLATFORM node:24.14.0-alpine AS frontend-builder
|
FROM --platform=$BUILDPLATFORM node:24.15.0-alpine@sha256:d1b3b4da11eefd5941e7f0b9cf17783fc99d9c6fc34884a665f40a06dbdfc94f AS frontend-builder
|
||||||
WORKDIR /app/frontend
|
WORKDIR /app/frontend
|
||||||
|
|
||||||
# Copy frontend package files
|
# Copy frontend package files
|
||||||
@@ -79,9 +103,12 @@ ARG VERSION=dev
|
|||||||
# Make version available to Vite as VITE_APP_VERSION during the frontend build
|
# Make version available to Vite as VITE_APP_VERSION during the frontend build
|
||||||
ENV VITE_APP_VERSION=${VERSION}
|
ENV VITE_APP_VERSION=${VERSION}
|
||||||
|
|
||||||
# Set environment to bypass native binary requirement for cross-arch builds
|
# Vite 8: Rolldown native bindings auto-resolved per platform via optionalDependencies
|
||||||
ENV npm_config_rollup_skip_nodejs_native=1 \
|
ARG NPM_VERSION
|
||||||
ROLLUP_SKIP_NODEJS_NATIVE=1
|
# hadolint ignore=DL3017
|
||||||
|
RUN apk upgrade --no-cache && \
|
||||||
|
npm install -g npm@${NPM_VERSION} --no-fund --no-audit && \
|
||||||
|
npm cache clean --force
|
||||||
|
|
||||||
RUN npm ci
|
RUN npm ci
|
||||||
|
|
||||||
@@ -91,8 +118,7 @@ RUN --mount=type=cache,target=/app/frontend/node_modules/.cache \
|
|||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
# ---- Backend Builder ----
|
# ---- Backend Builder ----
|
||||||
# renovate: datasource=docker depName=golang
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS backend-builder
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS backend-builder
|
|
||||||
# Copy xx helpers for cross-compilation
|
# Copy xx helpers for cross-compilation
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
|
|
||||||
@@ -105,7 +131,7 @@ SHELL ["/bin/ash", "-o", "pipefail", "-c"]
|
|||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
# hadolint ignore=DL3018
|
# hadolint ignore=DL3018
|
||||||
RUN apk add --no-cache clang lld
|
RUN apk add --no-cache git clang lld
|
||||||
# hadolint ignore=DL3059
|
# hadolint ignore=DL3059
|
||||||
# hadolint ignore=DL3018
|
# hadolint ignore=DL3018
|
||||||
# Install musl (headers + runtime) and gcc for cross-compilation linker
|
# Install musl (headers + runtime) and gcc for cross-compilation linker
|
||||||
@@ -134,7 +160,7 @@ RUN set -eux; \
|
|||||||
# Note: xx-go install puts binaries in /go/bin/TARGETOS_TARGETARCH/dlv if cross-compiling.
|
# Note: xx-go install puts binaries in /go/bin/TARGETOS_TARGETARCH/dlv if cross-compiling.
|
||||||
# We find it and move it to /go/bin/dlv so it's in a consistent location for the next stage.
|
# We find it and move it to /go/bin/dlv so it's in a consistent location for the next stage.
|
||||||
# renovate: datasource=go depName=github.com/go-delve/delve
|
# renovate: datasource=go depName=github.com/go-delve/delve
|
||||||
ARG DLV_VERSION=1.26.0
|
ARG DLV_VERSION=1.26.2
|
||||||
# hadolint ignore=DL3059,DL4006
|
# hadolint ignore=DL3059,DL4006
|
||||||
RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@v${DLV_VERSION} && \
|
RUN CGO_ENABLED=0 xx-go install github.com/go-delve/delve/cmd/dlv@v${DLV_VERSION} && \
|
||||||
DLV_PATH=$(find /go/bin -name dlv -type f | head -n 1) && \
|
DLV_PATH=$(find /go/bin -name dlv -type f | head -n 1) && \
|
||||||
@@ -194,19 +220,23 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
# ---- Caddy Builder ----
|
# ---- Caddy Builder ----
|
||||||
# Build Caddy from source to ensure we use the latest Go version and dependencies
|
# Build Caddy from source to ensure we use the latest Go version and dependencies
|
||||||
# This fixes vulnerabilities found in the pre-built Caddy images (e.g. CVE-2025-59530, stdlib issues)
|
# This fixes vulnerabilities found in the pre-built Caddy images (e.g. CVE-2025-59530, stdlib issues)
|
||||||
# renovate: datasource=docker depName=golang
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS caddy-builder
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.26-alpine AS caddy-builder
|
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ARG CADDY_VERSION
|
ARG CADDY_VERSION
|
||||||
ARG CADDY_CANDIDATE_VERSION
|
ARG CADDY_CANDIDATE_VERSION
|
||||||
ARG CADDY_USE_CANDIDATE
|
ARG CADDY_USE_CANDIDATE
|
||||||
ARG CADDY_PATCH_SCENARIO
|
ARG CADDY_PATCH_SCENARIO
|
||||||
|
ARG CADDY_SECURITY_VERSION
|
||||||
|
ARG CORAZA_CADDY_VERSION
|
||||||
# renovate: datasource=go depName=github.com/caddyserver/xcaddy
|
# renovate: datasource=go depName=github.com/caddyserver/xcaddy
|
||||||
ARG XCADDY_VERSION=0.4.5
|
ARG XCADDY_VERSION=0.4.5
|
||||||
|
ARG EXPR_LANG_VERSION
|
||||||
|
ARG XNET_VERSION
|
||||||
|
ARG SMALLSTEP_CERTIFICATES_VERSION
|
||||||
|
|
||||||
# hadolint ignore=DL3018
|
# hadolint ignore=DL3018
|
||||||
RUN apk add --no-cache git
|
RUN apk add --no-cache bash git
|
||||||
# hadolint ignore=DL3062
|
# hadolint ignore=DL3062
|
||||||
RUN --mount=type=cache,target=/go/pkg/mod \
|
RUN --mount=type=cache,target=/go/pkg/mod \
|
||||||
go install github.com/caddyserver/xcaddy/cmd/xcaddy@v${XCADDY_VERSION}
|
go install github.com/caddyserver/xcaddy/cmd/xcaddy@v${XCADDY_VERSION}
|
||||||
@@ -218,7 +248,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \
|
|||||||
# hadolint ignore=SC2016
|
# hadolint ignore=SC2016
|
||||||
RUN --mount=type=cache,target=/root/.cache/go-build \
|
RUN --mount=type=cache,target=/root/.cache/go-build \
|
||||||
--mount=type=cache,target=/go/pkg/mod \
|
--mount=type=cache,target=/go/pkg/mod \
|
||||||
sh -c 'set -e; \
|
bash -c 'set -e; \
|
||||||
CADDY_TARGET_VERSION="${CADDY_VERSION}"; \
|
CADDY_TARGET_VERSION="${CADDY_VERSION}"; \
|
||||||
if [ "${CADDY_USE_CANDIDATE}" = "1" ]; then \
|
if [ "${CADDY_USE_CANDIDATE}" = "1" ]; then \
|
||||||
CADDY_TARGET_VERSION="${CADDY_CANDIDATE_VERSION}"; \
|
CADDY_TARGET_VERSION="${CADDY_CANDIDATE_VERSION}"; \
|
||||||
@@ -229,8 +259,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
echo "Stage 1: Generate go.mod with xcaddy..."; \
|
echo "Stage 1: Generate go.mod with xcaddy..."; \
|
||||||
# Run xcaddy to generate the build directory and go.mod
|
# Run xcaddy to generate the build directory and go.mod
|
||||||
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_TARGET_VERSION} \
|
GOOS=$TARGETOS GOARCH=$TARGETARCH xcaddy build v${CADDY_TARGET_VERSION} \
|
||||||
--with github.com/greenpau/caddy-security \
|
--with github.com/caddyserver/caddy/v2@v${CADDY_TARGET_VERSION} \
|
||||||
--with github.com/corazawaf/coraza-caddy/v2 \
|
--with github.com/greenpau/caddy-security@v${CADDY_SECURITY_VERSION} \
|
||||||
|
--with github.com/corazawaf/coraza-caddy/v2@v${CORAZA_CADDY_VERSION} \
|
||||||
--with github.com/hslatman/caddy-crowdsec-bouncer@v0.10.0 \
|
--with github.com/hslatman/caddy-crowdsec-bouncer@v0.10.0 \
|
||||||
--with github.com/zhangjiayin/caddy-geoip2 \
|
--with github.com/zhangjiayin/caddy-geoip2 \
|
||||||
--with github.com/mholt/caddy-ratelimit \
|
--with github.com/mholt/caddy-ratelimit \
|
||||||
@@ -247,10 +278,40 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
# Patch ALL dependencies BEFORE building the final binary
|
# Patch ALL dependencies BEFORE building the final binary
|
||||||
# These patches fix CVEs in transitive dependencies
|
# These patches fix CVEs in transitive dependencies
|
||||||
# Renovate tracks these via regex manager in renovate.json
|
# Renovate tracks these via regex manager in renovate.json
|
||||||
# renovate: datasource=go depName=github.com/expr-lang/expr
|
go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION}; \
|
||||||
go get github.com/expr-lang/expr@v1.17.7; \
|
|
||||||
# renovate: datasource=go depName=github.com/hslatman/ipstore
|
# renovate: datasource=go depName=github.com/hslatman/ipstore
|
||||||
go get github.com/hslatman/ipstore@v0.4.0; \
|
go get github.com/hslatman/ipstore@v0.4.0; \
|
||||||
|
go get golang.org/x/net@v${XNET_VERSION}; \
|
||||||
|
# CVE-2026-33186: gRPC-Go auth bypass (fixed in v1.79.3)
|
||||||
|
# CVE-2026-34986: go-jose/v4 transitive fix (requires grpc >= v1.80.0)
|
||||||
|
# Pin here so the Caddy binary is patched immediately;
|
||||||
|
# remove once Caddy ships a release built with grpc >= v1.80.0.
|
||||||
|
# renovate: datasource=go depName=google.golang.org/grpc
|
||||||
|
go get google.golang.org/grpc@v1.80.0; \
|
||||||
|
# CVE-2026-34986: go-jose JOSE/JWT validation bypass
|
||||||
|
# renovate: datasource=go depName=github.com/go-jose/go-jose/v3
|
||||||
|
go get github.com/go-jose/go-jose/v3@v3.0.5; \
|
||||||
|
# renovate: datasource=go depName=github.com/go-jose/go-jose/v4
|
||||||
|
go get github.com/go-jose/go-jose/v4@v4.1.4; \
|
||||||
|
# CVE-2026-39883: OTel SDK resource leak
|
||||||
|
# renovate: datasource=go depName=go.opentelemetry.io/otel/sdk
|
||||||
|
go get go.opentelemetry.io/otel/sdk@v1.43.0; \
|
||||||
|
# CVE-2026-39882: OTel HTTP exporter request smuggling
|
||||||
|
# renovate: datasource=go depName=go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp
|
||||||
|
go get go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp@v0.19.0; \
|
||||||
|
# renovate: datasource=go depName=go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp
|
||||||
|
go get go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp@v1.43.0; \
|
||||||
|
# renovate: datasource=go depName=go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
|
||||||
|
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp@v1.43.0; \
|
||||||
|
# GHSA-479m-364c-43vc: goxmldsig XML signature validation bypass (loop variable capture)
|
||||||
|
# Fix available at v1.6.0. Pin here so the Caddy binary is patched immediately;
|
||||||
|
# remove once caddy-security ships a release built with goxmldsig >= v1.6.0.
|
||||||
|
# renovate: datasource=go depName=github.com/russellhaering/goxmldsig
|
||||||
|
go get github.com/russellhaering/goxmldsig@v1.6.0; \
|
||||||
|
# CVE-2026-30836: smallstep/certificates 0.30.0-rc3 vulnerability
|
||||||
|
# Fix available at v0.30.0. Pin here so the Caddy binary is patched immediately;
|
||||||
|
# remove once caddy-security ships a release built with smallstep/certificates >= v0.30.0.
|
||||||
|
go get github.com/smallstep/certificates@v${SMALLSTEP_CERTIFICATES_VERSION}; \
|
||||||
if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \
|
if [ "${CADDY_PATCH_SCENARIO}" = "A" ]; then \
|
||||||
# Rollback scenario: keep explicit nebula pin if upstream compatibility regresses.
|
# Rollback scenario: keep explicit nebula pin if upstream compatibility regresses.
|
||||||
# NOTE: smallstep/certificates (pulled by caddy-security stack) currently
|
# NOTE: smallstep/certificates (pulled by caddy-security stack) currently
|
||||||
@@ -284,10 +345,9 @@ RUN --mount=type=cache,target=/root/.cache/go-build \
|
|||||||
rm -rf /tmp/buildenv_* /tmp/caddy-initial'
|
rm -rf /tmp/buildenv_* /tmp/caddy-initial'
|
||||||
|
|
||||||
# ---- CrowdSec Builder ----
|
# ---- CrowdSec Builder ----
|
||||||
# Build CrowdSec from source to ensure we use Go 1.26.0+ and avoid stdlib vulnerabilities
|
# Build CrowdSec from source to ensure we use Go 1.26.2+ and avoid stdlib vulnerabilities
|
||||||
# (CVE-2025-58183, CVE-2025-58186, CVE-2025-58187, CVE-2025-61729)
|
# (CVE-2025-58183, CVE-2025-58186, CVE-2025-58187, CVE-2025-61729)
|
||||||
# renovate: datasource=docker depName=golang versioning=docker
|
FROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS crowdsec-builder
|
||||||
FROM --platform=$BUILDPLATFORM golang:1.26.0-alpine AS crowdsec-builder
|
|
||||||
COPY --from=xx / /
|
COPY --from=xx / /
|
||||||
|
|
||||||
WORKDIR /tmp/crowdsec
|
WORKDIR /tmp/crowdsec
|
||||||
@@ -295,11 +355,10 @@ WORKDIR /tmp/crowdsec
|
|||||||
ARG TARGETPLATFORM
|
ARG TARGETPLATFORM
|
||||||
ARG TARGETOS
|
ARG TARGETOS
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
# CrowdSec version - Renovate can update this
|
ARG CROWDSEC_VERSION
|
||||||
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
|
ARG CROWDSEC_RELEASE_SHA256
|
||||||
ARG CROWDSEC_VERSION=1.7.6
|
ARG EXPR_LANG_VERSION
|
||||||
# CrowdSec fallback tarball checksum (v${CROWDSEC_VERSION})
|
ARG XNET_VERSION
|
||||||
ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd
|
|
||||||
|
|
||||||
# hadolint ignore=DL3018
|
# hadolint ignore=DL3018
|
||||||
RUN apk add --no-cache git clang lld
|
RUN apk add --no-cache git clang lld
|
||||||
@@ -313,10 +372,27 @@ RUN git clone --depth 1 --branch "v${CROWDSEC_VERSION}" https://github.com/crowd
|
|||||||
|
|
||||||
# Patch dependencies to fix CVEs in transitive dependencies
|
# Patch dependencies to fix CVEs in transitive dependencies
|
||||||
# This follows the same pattern as Caddy's dependency patches
|
# This follows the same pattern as Caddy's dependency patches
|
||||||
# renovate: datasource=go depName=github.com/expr-lang/expr
|
|
||||||
# renovate: datasource=go depName=golang.org/x/crypto
|
# renovate: datasource=go depName=golang.org/x/crypto
|
||||||
RUN go get github.com/expr-lang/expr@v1.17.7 && \
|
RUN go get github.com/expr-lang/expr@v${EXPR_LANG_VERSION} && \
|
||||||
go get golang.org/x/crypto@v0.46.0 && \
|
go get golang.org/x/crypto@v0.46.0 && \
|
||||||
|
go get golang.org/x/net@v${XNET_VERSION} && \
|
||||||
|
# CVE-2026-33186 (GHSA-p77j-4mvh-x3m3): gRPC-Go auth bypass via missing leading slash
|
||||||
|
# Fix available at v1.79.3. Pin here so the CrowdSec binary is patched immediately;
|
||||||
|
# remove once CrowdSec ships a release built with grpc >= v1.79.3.
|
||||||
|
# renovate: datasource=go depName=google.golang.org/grpc
|
||||||
|
go get google.golang.org/grpc@v1.80.0 && \
|
||||||
|
# CVE-2026-32286: pgproto3/v2 buffer overflow (no v2 fix exists; bump pgx/v4 to latest patch)
|
||||||
|
# renovate: datasource=go depName=github.com/jackc/pgx/v4
|
||||||
|
go get github.com/jackc/pgx/v4@v4.18.3 && \
|
||||||
|
# GHSA-xmrv-pmrh-hhx2: AWS SDK v2 event stream injection
|
||||||
|
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream
|
||||||
|
go get github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream@v1.7.9 && \
|
||||||
|
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs
|
||||||
|
go get github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs@v1.69.1 && \
|
||||||
|
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/kinesis
|
||||||
|
go get github.com/aws/aws-sdk-go-v2/service/kinesis@v1.43.6 && \
|
||||||
|
# renovate: datasource=go depName=github.com/aws/aws-sdk-go-v2/service/s3
|
||||||
|
go get github.com/aws/aws-sdk-go-v2/service/s3@v1.99.1 && \
|
||||||
go mod tidy
|
go mod tidy
|
||||||
|
|
||||||
# Fix compatibility issues with expr-lang v1.17.7
|
# Fix compatibility issues with expr-lang v1.17.7
|
||||||
@@ -346,18 +422,15 @@ RUN mkdir -p /crowdsec-out/config && \
|
|||||||
cp -r config/* /crowdsec-out/config/ || true
|
cp -r config/* /crowdsec-out/config/ || true
|
||||||
|
|
||||||
# ---- CrowdSec Fallback (for architectures where build fails) ----
|
# ---- CrowdSec Fallback (for architectures where build fails) ----
|
||||||
# renovate: datasource=docker depName=alpine versioning=docker
|
FROM ${ALPINE_IMAGE} AS crowdsec-fallback
|
||||||
FROM alpine:3.23.3 AS crowdsec-fallback
|
|
||||||
|
|
||||||
SHELL ["/bin/ash", "-o", "pipefail", "-c"]
|
SHELL ["/bin/ash", "-o", "pipefail", "-c"]
|
||||||
|
|
||||||
WORKDIR /tmp/crowdsec
|
WORKDIR /tmp/crowdsec
|
||||||
|
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
# CrowdSec version - Renovate can update this
|
ARG CROWDSEC_VERSION
|
||||||
# renovate: datasource=github-releases depName=crowdsecurity/crowdsec
|
ARG CROWDSEC_RELEASE_SHA256
|
||||||
ARG CROWDSEC_VERSION=1.7.6
|
|
||||||
ARG CROWDSEC_RELEASE_SHA256=704e37121e7ac215991441cef0d8732e33fa3b1a2b2b88b53a0bfe5e38f863bd
|
|
||||||
|
|
||||||
# hadolint ignore=DL3018
|
# hadolint ignore=DL3018
|
||||||
RUN apk add --no-cache curl ca-certificates
|
RUN apk add --no-cache curl ca-certificates
|
||||||
@@ -386,17 +459,17 @@ RUN set -eux; \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# ---- Final Runtime with Caddy ----
|
# ---- Final Runtime with Caddy ----
|
||||||
FROM ${CADDY_IMAGE}
|
FROM ${ALPINE_IMAGE}
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Install runtime dependencies for Charon, including bash for maintenance scripts
|
# Install runtime dependencies for Charon, including bash for maintenance scripts
|
||||||
# Note: gosu is now built from source (see gosu-builder stage) to avoid CVEs from Debian's pre-compiled version
|
# Note: gosu is now built from source (see gosu-builder stage) to avoid CVEs from Debian's pre-compiled version
|
||||||
# Explicitly upgrade packages to fix security vulnerabilities
|
# Explicitly upgrade packages to fix security vulnerabilities
|
||||||
# binutils provides objdump for debug symbol detection in docker-entrypoint.sh
|
|
||||||
# hadolint ignore=DL3018
|
# hadolint ignore=DL3018
|
||||||
RUN apk add --no-cache \
|
RUN apk add --no-cache \
|
||||||
bash ca-certificates sqlite-libs sqlite tzdata curl gettext libcap libcap-utils \
|
bash ca-certificates sqlite-libs sqlite tzdata gettext libcap libcap-utils \
|
||||||
c-ares binutils libc-utils busybox-extras
|
c-ares busybox-extras \
|
||||||
|
&& apk upgrade --no-cache zlib libcrypto3 libssl3 musl musl-utils
|
||||||
|
|
||||||
# Copy gosu binary from gosu-builder (built with Go 1.26+ to avoid stdlib CVEs)
|
# Copy gosu binary from gosu-builder (built with Go 1.26+ to avoid stdlib CVEs)
|
||||||
COPY --from=gosu-builder /gosu-out/gosu /usr/sbin/gosu
|
COPY --from=gosu-builder /gosu-out/gosu /usr/sbin/gosu
|
||||||
@@ -413,12 +486,13 @@ SHELL ["/bin/ash", "-o", "pipefail", "-c"]
|
|||||||
# Note: In production, users should provide their own MaxMind license key
|
# Note: In production, users should provide their own MaxMind license key
|
||||||
# This uses the publicly available GeoLite2 database
|
# This uses the publicly available GeoLite2 database
|
||||||
# In CI, timeout quickly rather than retrying to save build time
|
# In CI, timeout quickly rather than retrying to save build time
|
||||||
ARG GEOLITE2_COUNTRY_SHA256=d3031e02196523cbb5f74291122033f2be277b2130abedd4b5bee52ba79832be
|
ARG GEOLITE2_COUNTRY_SHA256=62049119bd084e19fff4689bebe258f18a5f27a386e6d26ba5180941b613fc2b
|
||||||
RUN mkdir -p /app/data/geoip && \
|
RUN mkdir -p /app/data/geoip && \
|
||||||
if [ -n "$CI" ]; then \
|
if [ "$CI" = "true" ] || [ "$CI" = "1" ]; then \
|
||||||
echo "⏱️ CI detected - quick download (10s timeout, no retries)"; \
|
echo "⏱️ CI detected - quick download (10s timeout, no retries)"; \
|
||||||
if curl -fSL -m 10 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
|
||||||
-o /app/data/geoip/GeoLite2-Country.mmdb 2>/dev/null; then \
|
-T 10 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" 2>/dev/null \
|
||||||
|
&& [ -s /app/data/geoip/GeoLite2-Country.mmdb ]; then \
|
||||||
echo "✅ GeoIP downloaded"; \
|
echo "✅ GeoIP downloaded"; \
|
||||||
else \
|
else \
|
||||||
echo "⚠️ GeoIP skipped"; \
|
echo "⚠️ GeoIP skipped"; \
|
||||||
@@ -426,16 +500,12 @@ RUN mkdir -p /app/data/geoip && \
|
|||||||
fi; \
|
fi; \
|
||||||
else \
|
else \
|
||||||
echo "Local - full download (30s timeout, 3 retries)"; \
|
echo "Local - full download (30s timeout, 3 retries)"; \
|
||||||
if curl -fSL -m 30 --retry 3 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
if wget -qO /app/data/geoip/GeoLite2-Country.mmdb \
|
||||||
-o /app/data/geoip/GeoLite2-Country.mmdb; then \
|
-T 30 -t 4 "https://github.com/P3TERX/GeoLite.mmdb/raw/download/GeoLite2-Country.mmdb" \
|
||||||
if echo "${GEOLITE2_COUNTRY_SHA256} /app/data/geoip/GeoLite2-Country.mmdb" | sha256sum -c -; then \
|
&& [ -s /app/data/geoip/GeoLite2-Country.mmdb ]; then \
|
||||||
echo "✅ GeoIP checksum verified"; \
|
echo "✅ GeoIP downloaded"; \
|
||||||
else \
|
|
||||||
echo "⚠️ Checksum failed"; \
|
|
||||||
touch /app/data/geoip/GeoLite2-Country.mmdb.placeholder; \
|
|
||||||
fi; \
|
|
||||||
else \
|
else \
|
||||||
echo "⚠️ Download failed"; \
|
echo "⚠️ GeoIP download failed or empty — skipping"; \
|
||||||
touch /app/data/geoip/GeoLite2-Country.mmdb.placeholder; \
|
touch /app/data/geoip/GeoLite2-Country.mmdb.placeholder; \
|
||||||
fi; \
|
fi; \
|
||||||
fi
|
fi
|
||||||
@@ -446,7 +516,7 @@ COPY --from=caddy-builder /usr/bin/caddy /usr/bin/caddy
|
|||||||
# Allow non-root to bind privileged ports (80/443) securely
|
# Allow non-root to bind privileged ports (80/443) securely
|
||||||
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/caddy
|
RUN setcap 'cap_net_bind_service=+ep' /usr/bin/caddy
|
||||||
|
|
||||||
# Copy CrowdSec binaries from the crowdsec-builder stage (built with Go 1.26.0+)
|
# Copy CrowdSec binaries from the crowdsec-builder stage (built with Go 1.26.2+)
|
||||||
# This ensures we don't have stdlib vulnerabilities from older Go versions
|
# This ensures we don't have stdlib vulnerabilities from older Go versions
|
||||||
COPY --from=crowdsec-builder /crowdsec-out/crowdsec /usr/local/bin/crowdsec
|
COPY --from=crowdsec-builder /crowdsec-out/crowdsec /usr/local/bin/crowdsec
|
||||||
COPY --from=crowdsec-builder /crowdsec-out/cscli /usr/local/bin/cscli
|
COPY --from=crowdsec-builder /crowdsec-out/cscli /usr/local/bin/cscli
|
||||||
@@ -561,8 +631,8 @@ EXPOSE 80 443 443/udp 2019 8080
|
|||||||
|
|
||||||
# Security: Add healthcheck to monitor container health
|
# Security: Add healthcheck to monitor container health
|
||||||
# Verifies the Charon API is responding correctly
|
# Verifies the Charon API is responding correctly
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=15s --retries=3 \
|
||||||
CMD curl -f http://localhost:8080/api/v1/health || exit 1
|
CMD wget -q -O /dev/null http://localhost:8080/api/v1/health || exit 1
|
||||||
|
|
||||||
# Create CrowdSec symlink as root before switching to non-root user
|
# Create CrowdSec symlink as root before switching to non-root user
|
||||||
# This symlink allows CrowdSec to use persistent storage at /app/data/crowdsec/config
|
# This symlink allows CrowdSec to use persistent storage at /app/data/crowdsec/config
|
||||||
|
|||||||
9
Makefile
9
Makefile
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: help install test build run clean docker-build docker-run release go-check gopls-logs lint-fast lint-staticcheck-only
|
.PHONY: help install test build run clean docker-build docker-run release go-check gopls-logs lint-fast lint-staticcheck-only security-local
|
||||||
|
|
||||||
# Default target
|
# Default target
|
||||||
help:
|
help:
|
||||||
@@ -22,6 +22,7 @@ help:
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "Security targets:"
|
@echo "Security targets:"
|
||||||
@echo " security-scan - Quick security scan (govulncheck on Go deps)"
|
@echo " security-scan - Quick security scan (govulncheck on Go deps)"
|
||||||
|
@echo " security-local - Run govulncheck + semgrep (p/golang) locally before push"
|
||||||
@echo " security-scan-full - Full container scan with Trivy"
|
@echo " security-scan-full - Full container scan with Trivy"
|
||||||
@echo " security-scan-deps - Check for outdated Go dependencies"
|
@echo " security-scan-deps - Check for outdated Go dependencies"
|
||||||
|
|
||||||
@@ -145,6 +146,12 @@ security-scan:
|
|||||||
@echo "Running security scan (govulncheck)..."
|
@echo "Running security scan (govulncheck)..."
|
||||||
@./scripts/security-scan.sh
|
@./scripts/security-scan.sh
|
||||||
|
|
||||||
|
security-local: ## Run govulncheck + semgrep (p/golang) before push — fast local gate
|
||||||
|
@echo "[1/2] Running govulncheck..."
|
||||||
|
@./scripts/security-scan.sh
|
||||||
|
@echo "[2/2] Running Semgrep (p/golang, ERROR+WARNING)..."
|
||||||
|
@SEMGREP_CONFIG=p/golang ./scripts/pre-commit-hooks/semgrep-scan.sh
|
||||||
|
|
||||||
security-scan-full:
|
security-scan-full:
|
||||||
@echo "Building local Docker image for security scan..."
|
@echo "Building local Docker image for security scan..."
|
||||||
docker build --build-arg VCS_REF=$(shell git rev-parse HEAD) -t charon:local .
|
docker build --build-arg VCS_REF=$(shell git rev-parse HEAD) -t charon:local .
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -54,7 +54,7 @@ If you can use a website, you can run Charon.
|
|||||||
Charon includes security features that normally require multiple tools:
|
Charon includes security features that normally require multiple tools:
|
||||||
|
|
||||||
- Web Application Firewall (WAF)
|
- Web Application Firewall (WAF)
|
||||||
- CrowdSec intrusion detection
|
- CrowdSec intrusion detection with analytics dashboard
|
||||||
- Access Control Lists (ACLs)
|
- Access Control Lists (ACLs)
|
||||||
- Rate limiting
|
- Rate limiting
|
||||||
- Emergency recovery tools
|
- Emergency recovery tools
|
||||||
@@ -94,6 +94,7 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
```
|
```
|
||||||
|
|
||||||
> **Docker Socket Access:** Charon runs as a non-root user. If you mount the Docker socket for container discovery, the container needs permission to read it. Find your socket's group ID and add it to the compose file:
|
> **Docker Socket Access:** Charon runs as a non-root user. If you mount the Docker socket for container discovery, the container needs permission to read it. Find your socket's group ID and add it to the compose file:
|
||||||
>
|
>
|
||||||
> ```bash
|
> ```bash
|
||||||
@@ -107,26 +108,34 @@ services:
|
|||||||
> - "998"
|
> - "998"
|
||||||
> ```
|
> ```
|
||||||
|
|
||||||
### 2️⃣ Generate encryption key:
|
### 2️⃣ Generate encryption key
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
openssl rand -base64 32
|
openssl rand -base64 32
|
||||||
```
|
```
|
||||||
### 3️⃣ Start Charon:
|
|
||||||
|
### 3️⃣ Start Charon
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker-compose up -d
|
docker-compose up -d
|
||||||
```
|
```
|
||||||
### 4️⃣ Access the dashboard:
|
|
||||||
|
### 4️⃣ Access the dashboard
|
||||||
|
|
||||||
Open your browser and navigate to `http://localhost:8080` to access the dashboard and create your admin account.
|
Open your browser and navigate to `http://localhost:8080` to access the dashboard and create your admin account.
|
||||||
|
|
||||||
```code
|
```code
|
||||||
http://localhost:8080
|
http://localhost:8080
|
||||||
```
|
```
|
||||||
### Getting Started:
|
|
||||||
Full setup instructions and documentation are available at [https://wikid82.github.io/Charon/docs/getting-started.html](https://wikid82.github.io/Charon/docs/getting-started.html).
|
|
||||||
|
|
||||||
|
### Getting Started
|
||||||
|
|
||||||
|
Full setup instructions and documentation are available at [https://wikid82.github.io/Charon/docs/getting-started.html](https://wikid82.github.io/Charon/docs/getting-started.html).
|
||||||
|
|
||||||
--- ## ✨ Top 10 Features
|
--- ## ✨ Top 10 Features
|
||||||
|
|
||||||
### 🎯 **Point & Click Management**
|
### 🎯 **Point & Click Management**
|
||||||
|
|
||||||
No config files. No terminal commands. Just click, type your domain name, and you're live. If you can use a website, you can run Charon.
|
No config files. No terminal commands. Just click, type your domain name, and you're live. If you can use a website, you can run Charon.
|
||||||
|
|
||||||
### 🔐 **Automatic HTTPS Certificates**
|
### 🔐 **Automatic HTTPS Certificates**
|
||||||
@@ -139,7 +148,7 @@ Secure all your subdomains with a single *.example.com certificate. Supports 15+
|
|||||||
|
|
||||||
### 🛡️ **Enterprise-Grade Security Built In**
|
### 🛡️ **Enterprise-Grade Security Built In**
|
||||||
|
|
||||||
Web Application Firewall, rate limiting, geographic blocking, access control lists, and intrusion detection via CrowdSec. Protection that "just works."
|
Web Application Firewall, rate limiting, geographic blocking, access control lists, and intrusion detection via CrowdSec—with a built-in analytics dashboard showing attack trends, top offenders, and ban history. Protection that "just works."
|
||||||
|
|
||||||
### 🔐 **Supply Chain Security**
|
### 🔐 **Supply Chain Security**
|
||||||
|
|
||||||
@@ -160,6 +169,7 @@ See exactly what's happening with live request logs, uptime monitoring, and inst
|
|||||||
### 📥 **Migration Made Easy**
|
### 📥 **Migration Made Easy**
|
||||||
|
|
||||||
Already invested in another reverse proxy? Bring your work with you by importing your existing configurations with one click:
|
Already invested in another reverse proxy? Bring your work with you by importing your existing configurations with one click:
|
||||||
|
|
||||||
- **Caddyfile** — Migrate from other Caddy setups
|
- **Caddyfile** — Migrate from other Caddy setups
|
||||||
- **Nginx** — Import from Nginx based configurations (Coming Soon)
|
- **Nginx** — Import from Nginx based configurations (Coming Soon)
|
||||||
- **Traefik** - Import from Traefik based configurations (Coming Soon)
|
- **Traefik** - Import from Traefik based configurations (Coming Soon)
|
||||||
|
|||||||
984
SECURITY.md
984
SECURITY.md
File diff suppressed because it is too large
Load Diff
@@ -24,8 +24,10 @@ Example: `0.1.0-alpha`, `1.0.0-beta.1`, `2.0.0-rc.2`
|
|||||||
1. **Create and push a release tag**:
|
1. **Create and push a release tag**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
git tag -a v1.0.0 -m "Release v1.0.0"
|
git tag -a v1.0.0 -m "Release v1.0.0"
|
||||||
git push origin v1.0.0
|
git push origin v1.0.0
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **GitHub Actions automatically**:
|
2. **GitHub Actions automatically**:
|
||||||
@@ -51,10 +53,12 @@ Use it only when you need local/version-file parity checks:
|
|||||||
echo "1.0.0" > .version
|
echo "1.0.0" > .version
|
||||||
```
|
```
|
||||||
|
|
||||||
2. **Validate `.version` matches the latest tag**:
|
1. **Validate `.version` matches the latest tag**:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
bash scripts/check-version-match-tag.sh
|
bash scripts/check-version-match-tag.sh
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Deterministic Rollout Verification Gates (Mandatory)
|
### Deterministic Rollout Verification Gates (Mandatory)
|
||||||
|
|||||||
@@ -255,7 +255,11 @@ func main() {
|
|||||||
cerb := cerberus.New(cfg.Security, db)
|
cerb := cerberus.New(cfg.Security, db)
|
||||||
|
|
||||||
// Pass config to routes for auth service and certificate service
|
// Pass config to routes for auth service and certificate service
|
||||||
if err := routes.RegisterWithDeps(router, db, cfg, caddyManager, cerb); err != nil {
|
// Lifecycle context cancelled on shutdown to stop background goroutines
|
||||||
|
appCtx, appCancel := context.WithCancel(context.Background())
|
||||||
|
defer appCancel()
|
||||||
|
|
||||||
|
if err := routes.RegisterWithDeps(appCtx, router, db, cfg, caddyManager, cerb); err != nil {
|
||||||
log.Fatalf("register routes: %v", err)
|
log.Fatalf("register routes: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +295,9 @@ func main() {
|
|||||||
sig := <-quit
|
sig := <-quit
|
||||||
logger.Log().Infof("Received signal %v, initiating graceful shutdown...", sig)
|
logger.Log().Infof("Received signal %v, initiating graceful shutdown...", sig)
|
||||||
|
|
||||||
|
// Cancel the app-wide context to stop background goroutines (e.g. cert expiry checker)
|
||||||
|
appCancel()
|
||||||
|
|
||||||
// Graceful shutdown with timeout
|
// Graceful shutdown with timeout
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ func TestResetPasswordCommand_Succeeds(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
email := "user@example.com"
|
email := "user@example.com"
|
||||||
user := models.User{UUID: "u-1", Email: email, Name: "User", Role: "admin", Enabled: true}
|
user := models.User{UUID: "u-1", Email: email, Name: "User", Role: models.RoleAdmin, Enabled: true}
|
||||||
user.PasswordHash = "$2a$10$example_hashed_password"
|
user.PasswordHash = "$2a$10$example_hashed_password"
|
||||||
if err = db.Create(&user).Error; err != nil {
|
if err = db.Create(&user).Error; err != nil {
|
||||||
t.Fatalf("seed user: %v", err)
|
t.Fatalf("seed user: %v", err)
|
||||||
@@ -257,7 +257,7 @@ func TestMain_ResetPasswordCommand_InProcess(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
email := "user@example.com"
|
email := "user@example.com"
|
||||||
user := models.User{UUID: "u-1", Email: email, Name: "User", Role: "admin", Enabled: true}
|
user := models.User{UUID: "u-1", Email: email, Name: "User", Role: models.RoleAdmin, Enabled: true}
|
||||||
user.PasswordHash = "$2a$10$example_hashed_password"
|
user.PasswordHash = "$2a$10$example_hashed_password"
|
||||||
user.FailedLoginAttempts = 3
|
user.FailedLoginAttempts = 3
|
||||||
if err = db.Create(&user).Error; err != nil {
|
if err = db.Create(&user).Error; err != nil {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ func TestSeedMain_ForceAdminUpdatesExistingUserPassword(t *testing.T) {
|
|||||||
UUID: "existing-user",
|
UUID: "existing-user",
|
||||||
Email: "admin@localhost",
|
Email: "admin@localhost",
|
||||||
Name: "Old Name",
|
Name: "Old Name",
|
||||||
Role: "viewer",
|
Role: models.RolePassthrough,
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
PasswordHash: "$2a$10$example_hashed_password",
|
PasswordHash: "$2a$10$example_hashed_password",
|
||||||
}
|
}
|
||||||
@@ -134,7 +134,7 @@ func TestSeedMain_ForceAdminWithoutPasswordUpdatesMetadata(t *testing.T) {
|
|||||||
UUID: "existing-user-no-pass",
|
UUID: "existing-user-no-pass",
|
||||||
Email: "admin@localhost",
|
Email: "admin@localhost",
|
||||||
Name: "Old Name",
|
Name: "Old Name",
|
||||||
Role: "viewer",
|
Role: models.RolePassthrough,
|
||||||
Enabled: false,
|
Enabled: false,
|
||||||
PasswordHash: "$2a$10$example_hashed_password",
|
PasswordHash: "$2a$10$example_hashed_password",
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
module github.com/Wikid82/charon/backend
|
module github.com/Wikid82/charon/backend
|
||||||
|
|
||||||
go 1.26
|
go 1.26.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/docker/docker v28.5.2+incompatible
|
github.com/gin-contrib/gzip v1.2.6
|
||||||
github.com/gin-contrib/gzip v1.2.5
|
|
||||||
github.com/gin-gonic/gin v1.12.0
|
github.com/gin-gonic/gin v1.12.0
|
||||||
github.com/glebarez/sqlite v1.11.0
|
github.com/glebarez/sqlite v1.11.0
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1
|
github.com/golang-jwt/jwt/v5 v5.3.1
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/mattn/go-sqlite3 v1.14.34
|
github.com/mattn/go-sqlite3 v1.14.42
|
||||||
|
github.com/moby/moby/client v0.4.1
|
||||||
github.com/oschwald/geoip2-golang/v2 v2.1.0
|
github.com/oschwald/geoip2-golang/v2 v2.1.0
|
||||||
github.com/prometheus/client_golang v1.23.2
|
github.com/prometheus/client_golang v1.23.2
|
||||||
github.com/robfig/cron/v3 v3.0.1
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
github.com/sirupsen/logrus v1.9.4
|
github.com/sirupsen/logrus v1.9.4
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
golang.org/x/crypto v0.48.0
|
golang.org/x/crypto v0.50.0
|
||||||
golang.org/x/net v0.51.0
|
golang.org/x/net v0.53.0
|
||||||
golang.org/x/text v0.34.0
|
golang.org/x/text v0.36.0
|
||||||
golang.org/x/time v0.14.0
|
golang.org/x/time v0.15.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.7.1
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||||
github.com/bytedance/sonic v1.15.0 // indirect
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
github.com/containerd/errdefs v1.0.0 // indirect
|
github.com/containerd/errdefs v1.0.0 // indirect
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||||
github.com/containerd/log v0.1.0 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/distribution/reference v0.6.0 // indirect
|
github.com/distribution/reference v0.6.0 // indirect
|
||||||
github.com/docker/go-connections v0.6.0 // indirect
|
github.com/docker/go-connections v0.7.0 // indirect
|
||||||
github.com/docker/go-units v0.5.0 // indirect
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||||
github.com/go-logr/logr v1.4.3 // indirect
|
github.com/go-logr/logr v1.4.3 // indirect
|
||||||
github.com/go-logr/stdr v1.2.2 // indirect
|
github.com/go-logr/stdr v1.2.2 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.30.1 // indirect
|
github.com/go-playground/validator/v10 v10.30.2 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.6 // indirect
|
||||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
github.com/jinzhu/now v1.1.5 // indirect
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
@@ -58,20 +58,17 @@ require (
|
|||||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
github.com/moby/moby/api v1.54.2 // indirect
|
||||||
github.com/moby/term v0.5.2 // indirect
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/morikuni/aec v1.0.0 // indirect
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
github.com/opencontainers/image-spec v1.1.1 // indirect
|
github.com/opencontainers/image-spec v1.1.1 // indirect
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||||
github.com/pkg/errors v0.9.1 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_model v0.6.2 // indirect
|
github.com/prometheus/client_model v0.6.2 // indirect
|
||||||
github.com/prometheus/common v0.67.5 // indirect
|
github.com/prometheus/common v0.67.5 // indirect
|
||||||
@@ -79,24 +76,22 @@ require (
|
|||||||
github.com/quic-go/qpack v0.6.0 // indirect
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||||
github.com/stretchr/objx v0.5.2 // indirect
|
github.com/stretchr/objx v0.5.3 // indirect
|
||||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 // indirect
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 // indirect
|
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
go.yaml.in/yaml/v2 v2.4.4 // indirect
|
||||||
go.yaml.in/yaml/v2 v2.4.3 // indirect
|
golang.org/x/arch v0.26.0 // indirect
|
||||||
golang.org/x/arch v0.24.0 // indirect
|
golang.org/x/sys v0.43.0 // indirect
|
||||||
golang.org/x/sys v0.41.0 // indirect
|
|
||||||
google.golang.org/protobuf v1.36.11 // indirect
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
gotest.tools/v3 v3.5.2 // indirect
|
modernc.org/libc v1.72.0 // indirect
|
||||||
modernc.org/libc v1.69.0 // indirect
|
|
||||||
modernc.org/mathutil v1.7.1 // indirect
|
modernc.org/mathutil v1.7.1 // indirect
|
||||||
modernc.org/memory v1.11.0 // indirect
|
modernc.org/memory v1.11.0 // indirect
|
||||||
modernc.org/sqlite v1.46.1 // indirect
|
modernc.org/sqlite v1.49.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
168
backend/go.sum
168
backend/go.sum
@@ -1,17 +1,13 @@
|
|||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg=
|
|
||||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
|
|
||||||
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
|
||||||
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
|
||||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
|
||||||
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
|
||||||
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
|
|
||||||
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
|
|
||||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
@@ -20,17 +16,13 @@ github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG
|
|||||||
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
|
||||||
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk=
|
||||||
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
|
|
||||||
github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo=
|
|
||||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
|
||||||
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
|
||||||
github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
|
github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c=
|
||||||
github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q=
|
||||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
|
||||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
|
||||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
@@ -39,10 +31,10 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
|||||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
|
||||||
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s=
|
||||||
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||||
@@ -60,10 +52,10 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK2xqPNk8vgvu5JQ=
|
||||||
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
|
||||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=
|
||||||
@@ -77,8 +69,6 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU=
|
|
||||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs=
|
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
@@ -99,25 +89,21 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
|||||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
|
||||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||||
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
|
||||||
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
|
||||||
github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
|
github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg=
|
||||||
github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
|
github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
|
||||||
github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
|
github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY=
|
||||||
github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
|
github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ=
|
||||||
github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
|
|
||||||
github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
|
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
|
|
||||||
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
|
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||||
@@ -130,10 +116,8 @@ github.com/oschwald/geoip2-golang/v2 v2.1.0 h1:DjnLhNJu9WHwTrmoiQFvgmyJoczhdnm7L
|
|||||||
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
|
github.com/oschwald/geoip2-golang/v2 v2.1.0/go.mod h1:qdVmcPgrTJ4q2eP9tHq/yldMTdp2VMr33uVdFbHBiBc=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1 h1:lA8FH0oOrM4u7mLvowq8IT6a3Q/qEnqRzLQn9eH5ojc=
|
||||||
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
|
github.com/oschwald/maxminddb-golang/v2 v2.1.1/go.mod h1:PLdx6PR+siSIoXqqy7C7r3SB3KZnhxWr1Dp6g0Hacl8=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||||
@@ -159,8 +143,9 @@ github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC4
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
|
||||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
|
||||||
|
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
@@ -172,59 +157,46 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS
|
|||||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
go.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
|
||||||
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 h1:CqXxU8VOmDefoh0+ztfGaymYbhdB/tT3zs79QaZTNGY=
|
||||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0=
|
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0/go.mod h1:BuhAPThV8PBHBvg8ZzZ/Ok3idOdhWIodywz2xEcRbJo=
|
||||||
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms=
|
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
|
||||||
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g=
|
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
|
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
|
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0 h1:aTL7F04bJHUlztTsNGJ2l+6he8c+y/b//eR0jjjemT4=
|
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.38.0/go.mod h1:kldtb7jDTeol0l3ewcmd8SDvx3EmIE7lyvqbasU3QC4=
|
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
|
||||||
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc=
|
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
|
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
|
||||||
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
|
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
|
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
|
|
||||||
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw=
|
|
||||||
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1 h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
|
|
||||||
go.opentelemetry.io/proto/otlp v1.7.1/go.mod h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
|
|
||||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
|
go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
|
||||||
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
|
go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
|
||||||
golang.org/x/arch v0.24.0 h1:qlJ3M9upxvFfwRM51tTg3Yl+8CP9vCC1E7vlFpgv99Y=
|
golang.org/x/arch v0.26.0 h1:jZ6dpec5haP/fUv1kLCbuJy6dnRrfX6iVK08lZBFpk4=
|
||||||
golang.org/x/arch v0.24.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
golang.org/x/arch v0.26.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8=
|
||||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||||
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
|
||||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
|
||||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=
|
||||||
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=
|
||||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5 h1:BIRfGDEjiHRrk0QKZe3Xv2ieMhtgRGeLcZQ0mIVn4EY=
|
|
||||||
google.golang.org/genproto/googleapis/api v0.0.0-20250825161204-c5933d9347a5/go.mod h1:j3QtIyytwqGr1JUDtYXwtMXWPKsEa5LtzIFN1Wn5WvE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5 h1:eaY8u2EuxbRv7c3NiGK0/NedzVsCcV6hDuU5qPX5EGE=
|
|
||||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250825161204-c5933d9347a5/go.mod h1:M4/wBTSeyLxupu3W3tJtOgB14jILAS/XWPSSa3TAlJc=
|
|
||||||
google.golang.org/grpc v1.75.0 h1:+TW+dqTd2Biwe6KKfhE5JpiYIBWq865PhKGSXiivqt4=
|
|
||||||
google.golang.org/grpc v1.75.0/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
|
||||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
@@ -241,10 +213,10 @@ gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
|||||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
|
||||||
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
|
||||||
modernc.org/cc/v4 v4.27.1 h1:9W30zRlYrefrDV2JE2O8VDtJ1yPGownxciz5rrbQZis=
|
modernc.org/cc/v4 v4.27.3 h1:uNCgn37E5U09mTv1XgskEVUJ8ADKpmFMPxzGJ0TSo+U=
|
||||||
modernc.org/cc/v4 v4.27.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
modernc.org/cc/v4 v4.27.3/go.mod h1:3YjcbCqhoTTHPycJDRl2WZKKFj0nwcOIPBfEZK0Hdk8=
|
||||||
modernc.org/ccgo/v4 v4.31.0 h1:/bsaxqdgX3gy/0DboxcvWrc3NpzH+6wpFfI/ZaA/hrg=
|
modernc.org/ccgo/v4 v4.32.4 h1:L5OB8rpEX4ZsXEQwGozRfJyJSFHbbNVOoQ59DU9/KuU=
|
||||||
modernc.org/ccgo/v4 v4.31.0/go.mod h1:jKe8kPBjIN/VdGTVqARTQ8N1gAziBmiISY8j5HoKwjg=
|
modernc.org/ccgo/v4 v4.32.4/go.mod h1:lY7f+fiTDHfcv6YlRgSkxYfhs+UvOEEzj49jAn2TOx0=
|
||||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||||
@@ -253,8 +225,8 @@ modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
|||||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||||
modernc.org/libc v1.69.0 h1:YQJ5QMSReTgQ3QFmI0dudfjXIjCcYTUxcH8/9P9f0D8=
|
modernc.org/libc v1.72.0 h1:IEu559v9a0XWjw0DPoVKtXpO2qt5NVLAnFaBbjq+n8c=
|
||||||
modernc.org/libc v1.69.0/go.mod h1:YfLLduUEbodNV2xLU5JOnRHBTAHVHsVW3bVYGw0ZCV4=
|
modernc.org/libc v1.72.0/go.mod h1:tTU8DL8A+XLVkEY3x5E/tO7s2Q/q42EtnNWda/L5QhQ=
|
||||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||||
@@ -263,9 +235,13 @@ modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
|||||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||||
modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
|
modernc.org/sqlite v1.49.1 h1:dYGHTKcX1sJ+EQDnUzvz4TJ5GbuvhNJa8Fg6ElGx73U=
|
||||||
modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
|
modernc.org/sqlite v1.49.1/go.mod h1:m0w8xhwYUVY3H6pSDwc3gkJ/irZT/0YEXwBlhaxQEew=
|
||||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||||
|
pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
|
||||||
|
pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.7.1 h1:bxkUPRsvTPNRBZa4M/aSX4PyMOEbq3V8I6hbkG4F4Q8=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.7.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||||
|
|||||||
@@ -121,7 +121,6 @@ func TestAccessListHandler_List_DBError(t *testing.T) {
|
|||||||
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
// Don't migrate the table to cause error
|
// Don't migrate the table to cause error
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
|
||||||
handler := NewAccessListHandler(db)
|
handler := NewAccessListHandler(db)
|
||||||
@@ -138,7 +137,6 @@ func TestAccessListHandler_Get_DBError(t *testing.T) {
|
|||||||
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
// Don't migrate the table to cause error
|
// Don't migrate the table to cause error
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
|
||||||
handler := NewAccessListHandler(db)
|
handler := NewAccessListHandler(db)
|
||||||
@@ -157,7 +155,6 @@ func TestAccessListHandler_Delete_InternalError(t *testing.T) {
|
|||||||
// Migrate AccessList but not ProxyHost to cause internal error on delete
|
// Migrate AccessList but not ProxyHost to cause internal error on delete
|
||||||
_ = db.AutoMigrate(&models.AccessList{})
|
_ = db.AutoMigrate(&models.AccessList{})
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
|
||||||
handler := NewAccessListHandler(db)
|
handler := NewAccessListHandler(db)
|
||||||
@@ -285,7 +282,6 @@ func TestAccessListHandler_TestIP_InternalError(t *testing.T) {
|
|||||||
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
db, _ := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
// Don't migrate - this causes a "no such table" error which is an internal error
|
// Don't migrate - this causes a "no such table" error which is an internal error
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
|
||||||
handler := NewAccessListHandler(db)
|
handler := NewAccessListHandler(db)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ func setupAccessListTestRouter(t *testing.T) (*gin.Engine, *gorm.DB) {
|
|||||||
err = db.AutoMigrate(&models.AccessList{}, &models.ProxyHost{})
|
err = db.AutoMigrate(&models.AccessList{}, &models.ProxyHost{})
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
|
|
||||||
handler := NewAccessListHandler(db)
|
handler := NewAccessListHandler(db)
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ func setupImportCoverageDB(t *testing.T) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_Commit_InvalidJSON(t *testing.T) {
|
func TestImportHandler_Commit_InvalidJSON(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -44,7 +43,6 @@ func TestImportHandler_Commit_InvalidJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) {
|
func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -67,7 +65,6 @@ func TestImportHandler_Commit_InvalidSessionUUID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_Commit_SessionNotFound(t *testing.T) {
|
func TestImportHandler_Commit_SessionNotFound(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -98,7 +95,6 @@ func setupRemoteServerCoverageDB2(t *testing.T) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoteServerHandler_TestConnection_Unreachable(t *testing.T) {
|
func TestRemoteServerHandler_TestConnection_Unreachable(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupRemoteServerCoverageDB2(t)
|
db := setupRemoteServerCoverageDB2(t)
|
||||||
svc := services.NewRemoteServerService(db)
|
svc := services.NewRemoteServerService(db)
|
||||||
h := NewRemoteServerHandler(svc, nil)
|
h := NewRemoteServerHandler(svc, nil)
|
||||||
@@ -137,7 +133,6 @@ func setupSecurityCoverageDB3(t *testing.T) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_GetConfig_InternalError(t *testing.T) {
|
func TestSecurityHandler_GetConfig_InternalError(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||||
@@ -157,7 +152,6 @@ func TestSecurityHandler_GetConfig_InternalError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_UpdateConfig_ApplyCaddyError(t *testing.T) {
|
func TestSecurityHandler_UpdateConfig_ApplyCaddyError(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
// Create handler with nil caddy manager (ApplyConfig will be called but is nil)
|
// Create handler with nil caddy manager (ApplyConfig will be called but is nil)
|
||||||
@@ -181,7 +175,6 @@ func TestSecurityHandler_UpdateConfig_ApplyCaddyError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_GenerateBreakGlass_Error(t *testing.T) {
|
func TestSecurityHandler_GenerateBreakGlass_Error(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||||
@@ -201,7 +194,6 @@ func TestSecurityHandler_GenerateBreakGlass_Error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_ListDecisions_Error(t *testing.T) {
|
func TestSecurityHandler_ListDecisions_Error(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||||
@@ -220,7 +212,6 @@ func TestSecurityHandler_ListDecisions_Error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_ListRuleSets_Error(t *testing.T) {
|
func TestSecurityHandler_ListRuleSets_Error(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||||
@@ -239,7 +230,6 @@ func TestSecurityHandler_ListRuleSets_Error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_UpsertRuleSet_Error(t *testing.T) {
|
func TestSecurityHandler_UpsertRuleSet_Error(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||||
@@ -265,7 +255,6 @@ func TestSecurityHandler_UpsertRuleSet_Error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_CreateDecision_LogError(t *testing.T) {
|
func TestSecurityHandler_CreateDecision_LogError(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||||
@@ -291,7 +280,6 @@ func TestSecurityHandler_CreateDecision_LogError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSecurityHandler_DeleteRuleSet_Error(t *testing.T) {
|
func TestSecurityHandler_DeleteRuleSet_Error(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSecurityCoverageDB3(t)
|
db := setupSecurityCoverageDB3(t)
|
||||||
|
|
||||||
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
h := NewSecurityHandler(config.SecurityConfig{}, db, nil)
|
||||||
@@ -313,7 +301,6 @@ func TestSecurityHandler_DeleteRuleSet_Error(t *testing.T) {
|
|||||||
// CrowdSec ImportConfig additional coverage tests
|
// CrowdSec ImportConfig additional coverage tests
|
||||||
|
|
||||||
func TestCrowdsec_ImportConfig_EmptyUpload(t *testing.T) {
|
func TestCrowdsec_ImportConfig_EmptyUpload(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupCrowdDB(t)
|
db := setupCrowdDB(t)
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
|
|
||||||
@@ -344,7 +331,6 @@ func TestCrowdsec_ImportConfig_EmptyUpload(t *testing.T) {
|
|||||||
// Backup Handler additional coverage tests
|
// Backup Handler additional coverage tests
|
||||||
|
|
||||||
func TestBackupHandler_List_DBError(t *testing.T) {
|
func TestBackupHandler_List_DBError(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
// Use a non-writable temp dir to simulate errors
|
// Use a non-writable temp dir to simulate errors
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
@@ -370,7 +356,6 @@ func TestBackupHandler_List_DBError(t *testing.T) {
|
|||||||
// ImportHandler UploadMulti coverage tests
|
// ImportHandler UploadMulti coverage tests
|
||||||
|
|
||||||
func TestImportHandler_UploadMulti_InvalidJSON(t *testing.T) {
|
func TestImportHandler_UploadMulti_InvalidJSON(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -387,7 +372,6 @@ func TestImportHandler_UploadMulti_InvalidJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_UploadMulti_MissingCaddyfile(t *testing.T) {
|
func TestImportHandler_UploadMulti_MissingCaddyfile(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -411,7 +395,6 @@ func TestImportHandler_UploadMulti_MissingCaddyfile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_UploadMulti_EmptyContent(t *testing.T) {
|
func TestImportHandler_UploadMulti_EmptyContent(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -435,7 +418,6 @@ func TestImportHandler_UploadMulti_EmptyContent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_UploadMulti_PathTraversal(t *testing.T) {
|
func TestImportHandler_UploadMulti_PathTraversal(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -481,7 +463,6 @@ func setupLogsDownloadTest(t *testing.T) (h *LogsHandler, logsDir string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogsHandler_Download_PathTraversal(t *testing.T) {
|
func TestLogsHandler_Download_PathTraversal(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
h, _ := setupLogsDownloadTest(t)
|
h, _ := setupLogsDownloadTest(t)
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -496,7 +477,6 @@ func TestLogsHandler_Download_PathTraversal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogsHandler_Download_NotFound(t *testing.T) {
|
func TestLogsHandler_Download_NotFound(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
h, _ := setupLogsDownloadTest(t)
|
h, _ := setupLogsDownloadTest(t)
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -511,7 +491,6 @@ func TestLogsHandler_Download_NotFound(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestLogsHandler_Download_Success(t *testing.T) {
|
func TestLogsHandler_Download_Success(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
h, logsDir := setupLogsDownloadTest(t)
|
h, logsDir := setupLogsDownloadTest(t)
|
||||||
|
|
||||||
// Create a log file to download
|
// Create a log file to download
|
||||||
@@ -531,7 +510,6 @@ func TestLogsHandler_Download_Success(t *testing.T) {
|
|||||||
// Import Handler Upload error tests
|
// Import Handler Upload error tests
|
||||||
|
|
||||||
func TestImportHandler_Upload_InvalidJSON(t *testing.T) {
|
func TestImportHandler_Upload_InvalidJSON(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -548,7 +526,6 @@ func TestImportHandler_Upload_InvalidJSON(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_Upload_EmptyContent(t *testing.T) {
|
func TestImportHandler_Upload_EmptyContent(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -571,7 +548,6 @@ func TestImportHandler_Upload_EmptyContent(t *testing.T) {
|
|||||||
// Additional Backup Handler tests
|
// Additional Backup Handler tests
|
||||||
|
|
||||||
func TestBackupHandler_List_ServiceError(t *testing.T) {
|
func TestBackupHandler_List_ServiceError(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
// Create a temp dir with invalid permission for backup dir
|
// Create a temp dir with invalid permission for backup dir
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
@@ -608,7 +584,6 @@ func TestBackupHandler_List_ServiceError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackupHandler_Delete_PathTraversal(t *testing.T) {
|
func TestBackupHandler_Delete_PathTraversal(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
dataDir := filepath.Join(tmpDir, "data")
|
dataDir := filepath.Join(tmpDir, "data")
|
||||||
@@ -639,7 +614,6 @@ func TestBackupHandler_Delete_PathTraversal(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestBackupHandler_Delete_InternalError2(t *testing.T) {
|
func TestBackupHandler_Delete_InternalError2(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
dataDir := filepath.Join(tmpDir, "data")
|
dataDir := filepath.Join(tmpDir, "data")
|
||||||
@@ -689,7 +663,6 @@ func TestBackupHandler_Delete_InternalError2(t *testing.T) {
|
|||||||
// Remote Server TestConnection error paths
|
// Remote Server TestConnection error paths
|
||||||
|
|
||||||
func TestRemoteServerHandler_TestConnection_NotFound2(t *testing.T) {
|
func TestRemoteServerHandler_TestConnection_NotFound2(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupRemoteServerCoverageDB2(t)
|
db := setupRemoteServerCoverageDB2(t)
|
||||||
svc := services.NewRemoteServerService(db)
|
svc := services.NewRemoteServerService(db)
|
||||||
h := NewRemoteServerHandler(svc, nil)
|
h := NewRemoteServerHandler(svc, nil)
|
||||||
@@ -704,7 +677,6 @@ func TestRemoteServerHandler_TestConnection_NotFound2(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoteServerHandler_TestConnectionCustom_Unreachable2(t *testing.T) {
|
func TestRemoteServerHandler_TestConnectionCustom_Unreachable2(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupRemoteServerCoverageDB2(t)
|
db := setupRemoteServerCoverageDB2(t)
|
||||||
svc := services.NewRemoteServerService(db)
|
svc := services.NewRemoteServerService(db)
|
||||||
h := NewRemoteServerHandler(svc, nil)
|
h := NewRemoteServerHandler(svc, nil)
|
||||||
@@ -735,7 +707,6 @@ func setupAuthCoverageDB(t *testing.T) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthHandler_Register_InvalidJSON(t *testing.T) {
|
func TestAuthHandler_Register_InvalidJSON(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuthCoverageDB(t)
|
db := setupAuthCoverageDB(t)
|
||||||
|
|
||||||
cfg := config.Config{JWTSecret: "test-secret"}
|
cfg := config.Config{JWTSecret: "test-secret"}
|
||||||
@@ -755,7 +726,6 @@ func TestAuthHandler_Register_InvalidJSON(t *testing.T) {
|
|||||||
// Health handler coverage
|
// Health handler coverage
|
||||||
|
|
||||||
func TestHealthHandler_Basic(t *testing.T) {
|
func TestHealthHandler_Basic(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
c, _ := gin.CreateTestContext(w)
|
c, _ := gin.CreateTestContext(w)
|
||||||
@@ -771,7 +741,6 @@ func TestHealthHandler_Basic(t *testing.T) {
|
|||||||
// Backup Create error coverage
|
// Backup Create error coverage
|
||||||
|
|
||||||
func TestBackupHandler_Create_Error(t *testing.T) {
|
func TestBackupHandler_Create_Error(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
// Use a path where database file doesn't exist
|
// Use a path where database file doesn't exist
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
@@ -811,7 +780,6 @@ func setupSettingsCoverageDB(t *testing.T) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSettingsHandler_GetSettings_Error(t *testing.T) {
|
func TestSettingsHandler_GetSettings_Error(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSettingsCoverageDB(t)
|
db := setupSettingsCoverageDB(t)
|
||||||
|
|
||||||
h := NewSettingsHandler(db)
|
h := NewSettingsHandler(db)
|
||||||
@@ -830,7 +798,6 @@ func TestSettingsHandler_GetSettings_Error(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSettingsHandler_UpdateSetting_InvalidJSON(t *testing.T) {
|
func TestSettingsHandler_UpdateSetting_InvalidJSON(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupSettingsCoverageDB(t)
|
db := setupSettingsCoverageDB(t)
|
||||||
|
|
||||||
h := NewSettingsHandler(db)
|
h := NewSettingsHandler(db)
|
||||||
@@ -849,7 +816,6 @@ func TestSettingsHandler_UpdateSetting_InvalidJSON(t *testing.T) {
|
|||||||
// Additional remote server TestConnection tests
|
// Additional remote server TestConnection tests
|
||||||
|
|
||||||
func TestRemoteServerHandler_TestConnection_Reachable(t *testing.T) {
|
func TestRemoteServerHandler_TestConnection_Reachable(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupRemoteServerCoverageDB2(t)
|
db := setupRemoteServerCoverageDB2(t)
|
||||||
svc := services.NewRemoteServerService(db)
|
svc := services.NewRemoteServerService(db)
|
||||||
h := NewRemoteServerHandler(svc, nil)
|
h := NewRemoteServerHandler(svc, nil)
|
||||||
@@ -873,7 +839,6 @@ func TestRemoteServerHandler_TestConnection_Reachable(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRemoteServerHandler_TestConnection_EmptyHost(t *testing.T) {
|
func TestRemoteServerHandler_TestConnection_EmptyHost(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupRemoteServerCoverageDB2(t)
|
db := setupRemoteServerCoverageDB2(t)
|
||||||
svc := services.NewRemoteServerService(db)
|
svc := services.NewRemoteServerService(db)
|
||||||
h := NewRemoteServerHandler(svc, nil)
|
h := NewRemoteServerHandler(svc, nil)
|
||||||
@@ -900,7 +865,6 @@ func TestRemoteServerHandler_TestConnection_EmptyHost(t *testing.T) {
|
|||||||
// Additional UploadMulti test with valid Caddyfile content
|
// Additional UploadMulti test with valid Caddyfile content
|
||||||
|
|
||||||
func TestImportHandler_UploadMulti_ValidCaddyfile(t *testing.T) {
|
func TestImportHandler_UploadMulti_ValidCaddyfile(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
@@ -925,7 +889,6 @@ func TestImportHandler_UploadMulti_ValidCaddyfile(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestImportHandler_UploadMulti_SubdirFile(t *testing.T) {
|
func TestImportHandler_UploadMulti_SubdirFile(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupImportCoverageDB(t)
|
db := setupImportCoverageDB(t)
|
||||||
|
|
||||||
h := NewImportHandler(db, "", t.TempDir(), "")
|
h := NewImportHandler(db, "", t.TempDir(), "")
|
||||||
|
|||||||
@@ -63,7 +63,10 @@ func (h *AuditLogHandler) List(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate pagination metadata
|
// Calculate pagination metadata
|
||||||
totalPages := (int(total) + limit - 1) / limit
|
var totalPages int
|
||||||
|
if limit > 0 {
|
||||||
|
totalPages = (int(total) + limit - 1) / limit
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"audit_logs": audits,
|
"audit_logs": audits,
|
||||||
@@ -127,7 +130,10 @@ func (h *AuditLogHandler) ListByProvider(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Calculate pagination metadata
|
// Calculate pagination metadata
|
||||||
totalPages := (int(total) + limit - 1) / limit
|
var totalPages int
|
||||||
|
if limit > 0 {
|
||||||
|
totalPages = (int(total) + limit - 1) / limit
|
||||||
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{
|
c.JSON(http.StatusOK, gin.H{
|
||||||
"audit_logs": audits,
|
"audit_logs": audits,
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ func setupAuditLogTestDB(t *testing.T) *gorm.DB {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuditLogHandler_List(t *testing.T) {
|
func TestAuditLogHandler_List(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -130,7 +129,6 @@ func TestAuditLogHandler_List(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuditLogHandler_Get(t *testing.T) {
|
func TestAuditLogHandler_Get(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -198,7 +196,6 @@ func TestAuditLogHandler_Get(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuditLogHandler_ListByProvider(t *testing.T) {
|
func TestAuditLogHandler_ListByProvider(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -286,7 +283,6 @@ func TestAuditLogHandler_ListByProvider(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuditLogHandler_ListWithDateFilters(t *testing.T) {
|
func TestAuditLogHandler_ListWithDateFilters(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -371,7 +367,6 @@ func TestAuditLogHandler_ListWithDateFilters(t *testing.T) {
|
|||||||
|
|
||||||
// TestAuditLogHandler_ServiceErrors tests error handling when service layer fails
|
// TestAuditLogHandler_ServiceErrors tests error handling when service layer fails
|
||||||
func TestAuditLogHandler_ServiceErrors(t *testing.T) {
|
func TestAuditLogHandler_ServiceErrors(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -422,7 +417,6 @@ func TestAuditLogHandler_ServiceErrors(t *testing.T) {
|
|||||||
|
|
||||||
// TestAuditLogHandler_List_PaginationBoundaryEdgeCases tests pagination boundary edge cases
|
// TestAuditLogHandler_List_PaginationBoundaryEdgeCases tests pagination boundary edge cases
|
||||||
func TestAuditLogHandler_List_PaginationBoundaryEdgeCases(t *testing.T) {
|
func TestAuditLogHandler_List_PaginationBoundaryEdgeCases(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -513,7 +507,6 @@ func TestAuditLogHandler_List_PaginationBoundaryEdgeCases(t *testing.T) {
|
|||||||
|
|
||||||
// TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases tests pagination boundary edge cases for provider list
|
// TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases tests pagination boundary edge cases for provider list
|
||||||
func TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases(t *testing.T) {
|
func TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -583,7 +576,6 @@ func TestAuditLogHandler_ListByProvider_PaginationBoundaryEdgeCases(t *testing.T
|
|||||||
|
|
||||||
// TestAuditLogHandler_List_InvalidDateFormats tests handling of invalid date formats
|
// TestAuditLogHandler_List_InvalidDateFormats tests handling of invalid date formats
|
||||||
func TestAuditLogHandler_List_InvalidDateFormats(t *testing.T) {
|
func TestAuditLogHandler_List_InvalidDateFormats(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
db := setupAuditLogTestDB(t)
|
db := setupAuditLogTestDB(t)
|
||||||
securityService := services.NewSecurityService(db)
|
securityService := services.NewSecurityService(db)
|
||||||
defer securityService.Close()
|
defer securityService.Close()
|
||||||
@@ -624,7 +616,6 @@ func TestAuditLogHandler_List_InvalidDateFormats(t *testing.T) {
|
|||||||
|
|
||||||
// TestAuditLogHandler_Get_InternalError tests Get when service returns internal error
|
// TestAuditLogHandler_Get_InternalError tests Get when service returns internal error
|
||||||
func TestAuditLogHandler_Get_InternalError(t *testing.T) {
|
func TestAuditLogHandler_Get_InternalError(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
// Create a fresh DB and immediately close it to simulate internal error
|
// Create a fresh DB and immediately close it to simulate internal error
|
||||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
|
||||||
|
|||||||
@@ -77,12 +77,12 @@ func originHost(rawURL string) string {
|
|||||||
return normalizeHost(parsedURL.Host)
|
return normalizeHost(parsedURL.Host)
|
||||||
}
|
}
|
||||||
|
|
||||||
func isLocalHost(host string) bool {
|
func isLocalOrPrivateHost(host string) bool {
|
||||||
if strings.EqualFold(host, "localhost") {
|
if strings.EqualFold(host, "localhost") {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if ip := net.ParseIP(host); ip != nil && ip.IsLoopback() {
|
if ip := net.ParseIP(host); ip != nil && (ip.IsLoopback() || ip.IsPrivate()) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ func isLocalRequest(c *gin.Context) bool {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if isLocalHost(host) {
|
if isLocalOrPrivateHost(host) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -126,18 +126,16 @@ func isLocalRequest(c *gin.Context) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setSecureCookie sets an auth cookie with security best practices
|
// setSecureCookie sets an auth cookie with security best practices
|
||||||
// - HttpOnly: prevents JavaScript access (XSS protection)
|
// - HttpOnly: prevents JavaScript access (XSS protection)
|
||||||
// - Secure: true for HTTPS; false only for local non-HTTPS loopback flows
|
// - Secure: always true (all major browsers honour Secure on localhost HTTP;
|
||||||
// - SameSite: Strict for HTTPS, Lax for HTTP/IP to allow forward-auth redirects
|
// HTTP-on-private-IP without TLS is an unsupported deployment)
|
||||||
|
// - SameSite: Lax for any local/private-network request (regardless of scheme),
|
||||||
|
// Strict otherwise (public HTTPS only)
|
||||||
func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
|
func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
|
||||||
scheme := requestScheme(c)
|
scheme := requestScheme(c)
|
||||||
secure := true
|
|
||||||
sameSite := http.SameSiteStrictMode
|
sameSite := http.SameSiteStrictMode
|
||||||
if scheme != "https" {
|
if scheme != "https" {
|
||||||
sameSite = http.SameSiteLaxMode
|
sameSite = http.SameSiteLaxMode
|
||||||
if isLocalRequest(c) {
|
|
||||||
secure = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if isLocalRequest(c) {
|
if isLocalRequest(c) {
|
||||||
@@ -154,7 +152,7 @@ func setSecureCookie(c *gin.Context, name, value string, maxAge int) {
|
|||||||
maxAge, // maxAge in seconds
|
maxAge, // maxAge in seconds
|
||||||
"/", // path
|
"/", // path
|
||||||
domain, // domain (empty = current host)
|
domain, // domain (empty = current host)
|
||||||
secure, // secure (always true)
|
true, // secure
|
||||||
true, // httpOnly (no JS access)
|
true, // httpOnly (no JS access)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -381,7 +379,7 @@ func (h *AuthHandler) Verify(c *gin.Context) {
|
|||||||
|
|
||||||
// Set headers for downstream services
|
// Set headers for downstream services
|
||||||
c.Header("X-Forwarded-User", user.Email)
|
c.Header("X-Forwarded-User", user.Email)
|
||||||
c.Header("X-Forwarded-Groups", user.Role)
|
c.Header("X-Forwarded-Groups", string(user.Role))
|
||||||
c.Header("X-Forwarded-Name", user.Name)
|
c.Header("X-Forwarded-Name", user.Name)
|
||||||
|
|
||||||
// Return 200 OK - access granted
|
// Return 200 OK - access granted
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/Wikid82/charon/backend/internal/api/middleware"
|
"github.com/Wikid82/charon/backend/internal/api/middleware"
|
||||||
@@ -45,7 +44,6 @@ func TestAuthHandler_Login(t *testing.T) {
|
|||||||
_ = user.SetPassword("password123")
|
_ = user.SetPassword("password123")
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/login", handler.Login)
|
r.POST("/login", handler.Login)
|
||||||
|
|
||||||
@@ -65,9 +63,6 @@ func TestAuthHandler_Login(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestSetSecureCookie_HTTPS_Strict(t *testing.T) {
|
func TestSetSecureCookie_HTTPS_Strict(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
_ = os.Setenv("CHARON_ENV", "production")
|
|
||||||
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
req := httptest.NewRequest("POST", "https://example.com/login", http.NoBody)
|
req := httptest.NewRequest("POST", "https://example.com/login", http.NoBody)
|
||||||
@@ -83,7 +78,6 @@ func TestSetSecureCookie_HTTPS_Strict(t *testing.T) {
|
|||||||
|
|
||||||
func TestSetSecureCookie_HTTP_Lax(t *testing.T) {
|
func TestSetSecureCookie_HTTP_Lax(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
req := httptest.NewRequest("POST", "http://192.0.2.10/login", http.NoBody)
|
req := httptest.NewRequest("POST", "http://192.0.2.10/login", http.NoBody)
|
||||||
@@ -100,7 +94,6 @@ func TestSetSecureCookie_HTTP_Lax(t *testing.T) {
|
|||||||
|
|
||||||
func TestSetSecureCookie_HTTP_Loopback_Insecure(t *testing.T) {
|
func TestSetSecureCookie_HTTP_Loopback_Insecure(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
req := httptest.NewRequest("POST", "http://127.0.0.1:8080/login", http.NoBody)
|
req := httptest.NewRequest("POST", "http://127.0.0.1:8080/login", http.NoBody)
|
||||||
@@ -112,15 +105,12 @@ func TestSetSecureCookie_HTTP_Loopback_Insecure(t *testing.T) {
|
|||||||
cookies := recorder.Result().Cookies()
|
cookies := recorder.Result().Cookies()
|
||||||
require.Len(t, cookies, 1)
|
require.Len(t, cookies, 1)
|
||||||
cookie := cookies[0]
|
cookie := cookies[0]
|
||||||
assert.False(t, cookie.Secure)
|
assert.True(t, cookie.Secure)
|
||||||
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetSecureCookie_ForwardedHTTPS_LocalhostForcesInsecure(t *testing.T) {
|
func TestSetSecureCookie_ForwardedHTTPS_LocalhostForcesInsecure(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
_ = os.Setenv("CHARON_ENV", "production")
|
|
||||||
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
@@ -139,9 +129,6 @@ func TestSetSecureCookie_ForwardedHTTPS_LocalhostForcesInsecure(t *testing.T) {
|
|||||||
|
|
||||||
func TestSetSecureCookie_ForwardedHTTPS_LoopbackForcesInsecure(t *testing.T) {
|
func TestSetSecureCookie_ForwardedHTTPS_LoopbackForcesInsecure(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
_ = os.Setenv("CHARON_ENV", "production")
|
|
||||||
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
@@ -160,9 +147,6 @@ func TestSetSecureCookie_ForwardedHTTPS_LoopbackForcesInsecure(t *testing.T) {
|
|||||||
|
|
||||||
func TestSetSecureCookie_ForwardedHostLocalhostForcesInsecure(t *testing.T) {
|
func TestSetSecureCookie_ForwardedHostLocalhostForcesInsecure(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
_ = os.Setenv("CHARON_ENV", "production")
|
|
||||||
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
@@ -182,9 +166,6 @@ func TestSetSecureCookie_ForwardedHostLocalhostForcesInsecure(t *testing.T) {
|
|||||||
|
|
||||||
func TestSetSecureCookie_OriginLoopbackForcesInsecure(t *testing.T) {
|
func TestSetSecureCookie_OriginLoopbackForcesInsecure(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
_ = os.Setenv("CHARON_ENV", "production")
|
|
||||||
defer func() { _ = os.Unsetenv("CHARON_ENV") }()
|
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
@@ -202,6 +183,108 @@ func TestSetSecureCookie_OriginLoopbackForcesInsecure(t *testing.T) {
|
|||||||
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSetSecureCookie_HTTP_PrivateIP_Insecure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
req := httptest.NewRequest("POST", "http://192.168.1.50:8080/login", http.NoBody)
|
||||||
|
req.Host = "192.168.1.50:8080"
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
ctx.Request = req
|
||||||
|
|
||||||
|
setSecureCookie(ctx, "auth_token", "abc", 60)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
cookie := cookies[0]
|
||||||
|
assert.True(t, cookie.Secure)
|
||||||
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetSecureCookie_HTTP_10Network_Insecure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
req := httptest.NewRequest("POST", "http://10.0.0.5:8080/login", http.NoBody)
|
||||||
|
req.Host = "10.0.0.5:8080"
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
ctx.Request = req
|
||||||
|
|
||||||
|
setSecureCookie(ctx, "auth_token", "abc", 60)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
cookie := cookies[0]
|
||||||
|
assert.True(t, cookie.Secure)
|
||||||
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetSecureCookie_HTTP_172Network_Insecure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
req := httptest.NewRequest("POST", "http://172.16.0.1:8080/login", http.NoBody)
|
||||||
|
req.Host = "172.16.0.1:8080"
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
ctx.Request = req
|
||||||
|
|
||||||
|
setSecureCookie(ctx, "auth_token", "abc", 60)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
cookie := cookies[0]
|
||||||
|
assert.True(t, cookie.Secure)
|
||||||
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetSecureCookie_HTTPS_PrivateIP_Secure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
req := httptest.NewRequest("POST", "https://192.168.1.50:8080/login", http.NoBody)
|
||||||
|
req.Host = "192.168.1.50:8080"
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "https")
|
||||||
|
ctx.Request = req
|
||||||
|
|
||||||
|
setSecureCookie(ctx, "auth_token", "abc", 60)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
cookie := cookies[0]
|
||||||
|
assert.True(t, cookie.Secure)
|
||||||
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetSecureCookie_HTTP_IPv6ULA_Insecure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
req := httptest.NewRequest("POST", "http://[fd12::1]:8080/login", http.NoBody)
|
||||||
|
req.Host = "[fd12::1]:8080"
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
ctx.Request = req
|
||||||
|
|
||||||
|
setSecureCookie(ctx, "auth_token", "abc", 60)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
cookie := cookies[0]
|
||||||
|
assert.True(t, cookie.Secure)
|
||||||
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSetSecureCookie_HTTP_PublicIP_Secure(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
recorder := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
|
req := httptest.NewRequest("POST", "http://203.0.113.5:8080/login", http.NoBody)
|
||||||
|
req.Host = "203.0.113.5:8080"
|
||||||
|
req.Header.Set("X-Forwarded-Proto", "http")
|
||||||
|
ctx.Request = req
|
||||||
|
|
||||||
|
setSecureCookie(ctx, "auth_token", "abc", 60)
|
||||||
|
cookies := recorder.Result().Cookies()
|
||||||
|
require.Len(t, cookies, 1)
|
||||||
|
cookie := cookies[0]
|
||||||
|
assert.True(t, cookie.Secure)
|
||||||
|
assert.Equal(t, http.SameSiteLaxMode, cookie.SameSite)
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsProduction(t *testing.T) {
|
func TestIsProduction(t *testing.T) {
|
||||||
t.Setenv("CHARON_ENV", "production")
|
t.Setenv("CHARON_ENV", "production")
|
||||||
assert.True(t, isProduction())
|
assert.True(t, isProduction())
|
||||||
@@ -214,7 +297,6 @@ func TestIsProduction(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestRequestScheme(t *testing.T) {
|
func TestRequestScheme(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
t.Run("forwarded proto first value wins", func(t *testing.T) {
|
t.Run("forwarded proto first value wins", func(t *testing.T) {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
@@ -271,16 +353,20 @@ func TestHostHelpers(t *testing.T) {
|
|||||||
assert.Equal(t, "localhost", originHost("http://localhost:8080/path"))
|
assert.Equal(t, "localhost", originHost("http://localhost:8080/path"))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("isLocalHost", func(t *testing.T) {
|
t.Run("isLocalOrPrivateHost", func(t *testing.T) {
|
||||||
assert.True(t, isLocalHost("localhost"))
|
assert.True(t, isLocalOrPrivateHost("localhost"))
|
||||||
assert.True(t, isLocalHost("127.0.0.1"))
|
assert.True(t, isLocalOrPrivateHost("127.0.0.1"))
|
||||||
assert.True(t, isLocalHost("::1"))
|
assert.True(t, isLocalOrPrivateHost("::1"))
|
||||||
assert.False(t, isLocalHost("example.com"))
|
assert.True(t, isLocalOrPrivateHost("192.168.1.50"))
|
||||||
|
assert.True(t, isLocalOrPrivateHost("10.0.0.1"))
|
||||||
|
assert.True(t, isLocalOrPrivateHost("172.16.0.1"))
|
||||||
|
assert.True(t, isLocalOrPrivateHost("fd12::1"))
|
||||||
|
assert.False(t, isLocalOrPrivateHost("203.0.113.5"))
|
||||||
|
assert.False(t, isLocalOrPrivateHost("example.com"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsLocalRequest(t *testing.T) {
|
func TestIsLocalRequest(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
|
|
||||||
t.Run("forwarded host list includes localhost", func(t *testing.T) {
|
t.Run("forwarded host list includes localhost", func(t *testing.T) {
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
@@ -315,7 +401,6 @@ func TestIsLocalRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClearSecureCookie(t *testing.T) {
|
func TestClearSecureCookie(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
ctx.Request = httptest.NewRequest("POST", "http://example.com/logout", http.NoBody)
|
ctx.Request = httptest.NewRequest("POST", "http://example.com/logout", http.NoBody)
|
||||||
@@ -326,12 +411,12 @@ func TestClearSecureCookie(t *testing.T) {
|
|||||||
require.Len(t, cookies, 1)
|
require.Len(t, cookies, 1)
|
||||||
assert.Equal(t, "auth_token", cookies[0].Name)
|
assert.Equal(t, "auth_token", cookies[0].Name)
|
||||||
assert.Equal(t, -1, cookies[0].MaxAge)
|
assert.Equal(t, -1, cookies[0].MaxAge)
|
||||||
|
assert.True(t, cookies[0].Secure)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthHandler_Login_Errors(t *testing.T) {
|
func TestAuthHandler_Login_Errors(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/login", handler.Login)
|
r.POST("/login", handler.Login)
|
||||||
|
|
||||||
@@ -359,7 +444,6 @@ func TestAuthHandler_Register(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/register", handler.Register)
|
r.POST("/register", handler.Register)
|
||||||
|
|
||||||
@@ -383,7 +467,6 @@ func TestAuthHandler_Register_Duplicate(t *testing.T) {
|
|||||||
handler, db := setupAuthHandler(t)
|
handler, db := setupAuthHandler(t)
|
||||||
db.Create(&models.User{UUID: uuid.NewString(), Email: "dup@example.com", Name: "Dup"})
|
db.Create(&models.User{UUID: uuid.NewString(), Email: "dup@example.com", Name: "Dup"})
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/register", handler.Register)
|
r.POST("/register", handler.Register)
|
||||||
|
|
||||||
@@ -405,7 +488,6 @@ func TestAuthHandler_Logout(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/logout", handler.Logout)
|
r.POST("/logout", handler.Logout)
|
||||||
|
|
||||||
@@ -430,11 +512,10 @@ func TestAuthHandler_Me(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "me@example.com",
|
Email: "me@example.com",
|
||||||
Name: "Me User",
|
Name: "Me User",
|
||||||
Role: "admin",
|
Role: models.RoleAdmin,
|
||||||
}
|
}
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
// Simulate middleware
|
// Simulate middleware
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
@@ -460,7 +541,6 @@ func TestAuthHandler_Me(t *testing.T) {
|
|||||||
func TestAuthHandler_Me_NotFound(t *testing.T) {
|
func TestAuthHandler_Me_NotFound(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", uint(999)) // Non-existent ID
|
c.Set("userID", uint(999)) // Non-existent ID
|
||||||
@@ -488,7 +568,6 @@ func TestAuthHandler_ChangePassword(t *testing.T) {
|
|||||||
_ = user.SetPassword("oldpassword")
|
_ = user.SetPassword("oldpassword")
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
// Simulate middleware
|
// Simulate middleware
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
@@ -523,7 +602,6 @@ func TestAuthHandler_ChangePassword_WrongOld(t *testing.T) {
|
|||||||
_ = user.SetPassword("correct")
|
_ = user.SetPassword("correct")
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -547,7 +625,6 @@ func TestAuthHandler_ChangePassword_WrongOld(t *testing.T) {
|
|||||||
func TestAuthHandler_ChangePassword_Errors(t *testing.T) {
|
func TestAuthHandler_ChangePassword_Errors(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/change-password", handler.ChangePassword)
|
r.POST("/change-password", handler.ChangePassword)
|
||||||
|
|
||||||
@@ -594,7 +671,6 @@ func TestNewAuthHandlerWithDB(t *testing.T) {
|
|||||||
func TestAuthHandler_Verify_NoCookie(t *testing.T) {
|
func TestAuthHandler_Verify_NoCookie(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/verify", handler.Verify)
|
r.GET("/verify", handler.Verify)
|
||||||
|
|
||||||
@@ -609,7 +685,6 @@ func TestAuthHandler_Verify_NoCookie(t *testing.T) {
|
|||||||
func TestAuthHandler_Verify_InvalidToken(t *testing.T) {
|
func TestAuthHandler_Verify_InvalidToken(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/verify", handler.Verify)
|
r.GET("/verify", handler.Verify)
|
||||||
|
|
||||||
@@ -630,7 +705,7 @@ func TestAuthHandler_Verify_ValidToken(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "test@example.com",
|
Email: "test@example.com",
|
||||||
Name: "Test User",
|
Name: "Test User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
_ = user.SetPassword("password123")
|
_ = user.SetPassword("password123")
|
||||||
@@ -639,7 +714,6 @@ func TestAuthHandler_Verify_ValidToken(t *testing.T) {
|
|||||||
// Generate token
|
// Generate token
|
||||||
token, _ := handler.authService.GenerateToken(user)
|
token, _ := handler.authService.GenerateToken(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/verify", handler.Verify)
|
r.GET("/verify", handler.Verify)
|
||||||
|
|
||||||
@@ -661,7 +735,7 @@ func TestAuthHandler_Verify_BearerToken(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "bearer@example.com",
|
Email: "bearer@example.com",
|
||||||
Name: "Bearer User",
|
Name: "Bearer User",
|
||||||
Role: "admin",
|
Role: models.RoleAdmin,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
_ = user.SetPassword("password123")
|
_ = user.SetPassword("password123")
|
||||||
@@ -669,7 +743,6 @@ func TestAuthHandler_Verify_BearerToken(t *testing.T) {
|
|||||||
|
|
||||||
token, _ := handler.authService.GenerateToken(user)
|
token, _ := handler.authService.GenerateToken(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/verify", handler.Verify)
|
r.GET("/verify", handler.Verify)
|
||||||
|
|
||||||
@@ -690,7 +763,7 @@ func TestAuthHandler_Verify_DisabledUser(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "disabled@example.com",
|
Email: "disabled@example.com",
|
||||||
Name: "Disabled User",
|
Name: "Disabled User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
}
|
}
|
||||||
_ = user.SetPassword("password123")
|
_ = user.SetPassword("password123")
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
@@ -699,7 +772,6 @@ func TestAuthHandler_Verify_DisabledUser(t *testing.T) {
|
|||||||
|
|
||||||
token, _ := handler.authService.GenerateToken(user)
|
token, _ := handler.authService.GenerateToken(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/verify", handler.Verify)
|
r.GET("/verify", handler.Verify)
|
||||||
|
|
||||||
@@ -730,7 +802,7 @@ func TestAuthHandler_Verify_ForwardAuthDenied(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "denied@example.com",
|
Email: "denied@example.com",
|
||||||
Name: "Denied User",
|
Name: "Denied User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
PermissionMode: models.PermissionModeDenyAll,
|
PermissionMode: models.PermissionModeDenyAll,
|
||||||
}
|
}
|
||||||
@@ -739,7 +811,6 @@ func TestAuthHandler_Verify_ForwardAuthDenied(t *testing.T) {
|
|||||||
|
|
||||||
token, _ := handler.authService.GenerateToken(user)
|
token, _ := handler.authService.GenerateToken(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/verify", handler.Verify)
|
r.GET("/verify", handler.Verify)
|
||||||
|
|
||||||
@@ -755,7 +826,6 @@ func TestAuthHandler_Verify_ForwardAuthDenied(t *testing.T) {
|
|||||||
func TestAuthHandler_VerifyStatus_NotAuthenticated(t *testing.T) {
|
func TestAuthHandler_VerifyStatus_NotAuthenticated(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/status", handler.VerifyStatus)
|
r.GET("/status", handler.VerifyStatus)
|
||||||
|
|
||||||
@@ -772,7 +842,6 @@ func TestAuthHandler_VerifyStatus_NotAuthenticated(t *testing.T) {
|
|||||||
func TestAuthHandler_VerifyStatus_InvalidToken(t *testing.T) {
|
func TestAuthHandler_VerifyStatus_InvalidToken(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/status", handler.VerifyStatus)
|
r.GET("/status", handler.VerifyStatus)
|
||||||
|
|
||||||
@@ -795,7 +864,7 @@ func TestAuthHandler_VerifyStatus_Authenticated(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "status@example.com",
|
Email: "status@example.com",
|
||||||
Name: "Status User",
|
Name: "Status User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
_ = user.SetPassword("password123")
|
_ = user.SetPassword("password123")
|
||||||
@@ -803,7 +872,6 @@ func TestAuthHandler_VerifyStatus_Authenticated(t *testing.T) {
|
|||||||
|
|
||||||
token, _ := handler.authService.GenerateToken(user)
|
token, _ := handler.authService.GenerateToken(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/status", handler.VerifyStatus)
|
r.GET("/status", handler.VerifyStatus)
|
||||||
|
|
||||||
@@ -828,7 +896,7 @@ func TestAuthHandler_VerifyStatus_DisabledUser(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "disabled2@example.com",
|
Email: "disabled2@example.com",
|
||||||
Name: "Disabled User 2",
|
Name: "Disabled User 2",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
}
|
}
|
||||||
_ = user.SetPassword("password123")
|
_ = user.SetPassword("password123")
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
@@ -837,7 +905,6 @@ func TestAuthHandler_VerifyStatus_DisabledUser(t *testing.T) {
|
|||||||
|
|
||||||
token, _ := handler.authService.GenerateToken(user)
|
token, _ := handler.authService.GenerateToken(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/status", handler.VerifyStatus)
|
r.GET("/status", handler.VerifyStatus)
|
||||||
|
|
||||||
@@ -855,7 +922,6 @@ func TestAuthHandler_VerifyStatus_DisabledUser(t *testing.T) {
|
|||||||
func TestAuthHandler_GetAccessibleHosts_Unauthorized(t *testing.T) {
|
func TestAuthHandler_GetAccessibleHosts_Unauthorized(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/hosts", handler.GetAccessibleHosts)
|
r.GET("/hosts", handler.GetAccessibleHosts)
|
||||||
|
|
||||||
@@ -880,13 +946,12 @@ func TestAuthHandler_GetAccessibleHosts_AllowAll(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "allowall@example.com",
|
Email: "allowall@example.com",
|
||||||
Name: "Allow All User",
|
Name: "Allow All User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
PermissionMode: models.PermissionModeAllowAll,
|
PermissionMode: models.PermissionModeAllowAll,
|
||||||
}
|
}
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -917,13 +982,12 @@ func TestAuthHandler_GetAccessibleHosts_DenyAll(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "denyall@example.com",
|
Email: "denyall@example.com",
|
||||||
Name: "Deny All User",
|
Name: "Deny All User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
PermissionMode: models.PermissionModeDenyAll,
|
PermissionMode: models.PermissionModeDenyAll,
|
||||||
}
|
}
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -956,14 +1020,13 @@ func TestAuthHandler_GetAccessibleHosts_PermittedHosts(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "permitted@example.com",
|
Email: "permitted@example.com",
|
||||||
Name: "Permitted User",
|
Name: "Permitted User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
PermissionMode: models.PermissionModeDenyAll,
|
PermissionMode: models.PermissionModeDenyAll,
|
||||||
PermittedHosts: []models.ProxyHost{*host1}, // Only host1
|
PermittedHosts: []models.ProxyHost{*host1}, // Only host1
|
||||||
}
|
}
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -986,7 +1049,6 @@ func TestAuthHandler_GetAccessibleHosts_UserNotFound(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", uint(99999))
|
c.Set("userID", uint(99999))
|
||||||
@@ -1004,7 +1066,6 @@ func TestAuthHandler_GetAccessibleHosts_UserNotFound(t *testing.T) {
|
|||||||
func TestAuthHandler_CheckHostAccess_Unauthorized(t *testing.T) {
|
func TestAuthHandler_CheckHostAccess_Unauthorized(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/hosts/:hostId/access", handler.CheckHostAccess)
|
r.GET("/hosts/:hostId/access", handler.CheckHostAccess)
|
||||||
|
|
||||||
@@ -1022,7 +1083,6 @@ func TestAuthHandler_CheckHostAccess_InvalidHostID(t *testing.T) {
|
|||||||
user := &models.User{UUID: uuid.NewString(), Email: "check@example.com", Enabled: true}
|
user := &models.User{UUID: uuid.NewString(), Email: "check@example.com", Enabled: true}
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -1052,7 +1112,6 @@ func TestAuthHandler_CheckHostAccess_Allowed(t *testing.T) {
|
|||||||
}
|
}
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -1085,7 +1144,6 @@ func TestAuthHandler_CheckHostAccess_Denied(t *testing.T) {
|
|||||||
}
|
}
|
||||||
db.Create(user)
|
db.Create(user)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -1111,7 +1169,7 @@ func TestAuthHandler_Logout_InvalidatesBearerSession(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "logout-session@example.com",
|
Email: "logout-session@example.com",
|
||||||
Name: "Logout Session",
|
Name: "Logout Session",
|
||||||
Role: "admin",
|
Role: models.RoleAdmin,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
}
|
}
|
||||||
_ = user.SetPassword("password123")
|
_ = user.SetPassword("password123")
|
||||||
@@ -1162,7 +1220,6 @@ func TestAuthHandler_Me_RequiresUserContext(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/me", handler.Me)
|
r.GET("/me", handler.Me)
|
||||||
|
|
||||||
@@ -1222,10 +1279,10 @@ func TestAuthHandler_HelperFunctions(t *testing.T) {
|
|||||||
assert.Equal(t, "example.com", originHost("https://example.com/path"))
|
assert.Equal(t, "example.com", originHost("https://example.com/path"))
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("isLocalHost and isLocalRequest", func(t *testing.T) {
|
t.Run("isLocalOrPrivateHost and isLocalRequest", func(t *testing.T) {
|
||||||
assert.True(t, isLocalHost("localhost"))
|
assert.True(t, isLocalOrPrivateHost("localhost"))
|
||||||
assert.True(t, isLocalHost("127.0.0.1"))
|
assert.True(t, isLocalOrPrivateHost("127.0.0.1"))
|
||||||
assert.False(t, isLocalHost("example.com"))
|
assert.False(t, isLocalOrPrivateHost("example.com"))
|
||||||
|
|
||||||
recorder := httptest.NewRecorder()
|
recorder := httptest.NewRecorder()
|
||||||
ctx, _ := gin.CreateTestContext(recorder)
|
ctx, _ := gin.CreateTestContext(recorder)
|
||||||
@@ -1242,11 +1299,10 @@ func TestAuthHandler_Refresh(t *testing.T) {
|
|||||||
|
|
||||||
handler, db := setupAuthHandler(t)
|
handler, db := setupAuthHandler(t)
|
||||||
|
|
||||||
user := &models.User{UUID: uuid.NewString(), Email: "refresh@example.com", Name: "Refresh User", Role: "user", Enabled: true}
|
user := &models.User{UUID: uuid.NewString(), Email: "refresh@example.com", Name: "Refresh User", Role: models.RoleUser, Enabled: true}
|
||||||
require.NoError(t, user.SetPassword("password123"))
|
require.NoError(t, user.SetPassword("password123"))
|
||||||
require.NoError(t, db.Create(user).Error)
|
require.NoError(t, db.Create(user).Error)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/refresh", func(c *gin.Context) {
|
r.POST("/refresh", func(c *gin.Context) {
|
||||||
c.Set("userID", user.ID)
|
c.Set("userID", user.ID)
|
||||||
@@ -1267,7 +1323,6 @@ func TestAuthHandler_Refresh_Unauthorized(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/refresh", handler.Refresh)
|
r.POST("/refresh", handler.Refresh)
|
||||||
|
|
||||||
@@ -1282,7 +1337,6 @@ func TestAuthHandler_Register_BadRequest(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.POST("/register", handler.Register)
|
r.POST("/register", handler.Register)
|
||||||
|
|
||||||
@@ -1298,7 +1352,6 @@ func TestAuthHandler_Logout_InvalidateSessionsFailure(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", uint(999999))
|
c.Set("userID", uint(999999))
|
||||||
@@ -1332,7 +1385,7 @@ func TestAuthHandler_Verify_UsesOriginalHostFallback(t *testing.T) {
|
|||||||
UUID: uuid.NewString(),
|
UUID: uuid.NewString(),
|
||||||
Email: "originalhost@example.com",
|
Email: "originalhost@example.com",
|
||||||
Name: "Original Host User",
|
Name: "Original Host User",
|
||||||
Role: "user",
|
Role: models.RoleUser,
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
PermissionMode: models.PermissionModeAllowAll,
|
PermissionMode: models.PermissionModeAllowAll,
|
||||||
}
|
}
|
||||||
@@ -1342,7 +1395,6 @@ func TestAuthHandler_Verify_UsesOriginalHostFallback(t *testing.T) {
|
|||||||
token, err := handler.authService.GenerateToken(user)
|
token, err := handler.authService.GenerateToken(user)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.GET("/verify", handler.Verify)
|
r.GET("/verify", handler.Verify)
|
||||||
|
|
||||||
@@ -1360,7 +1412,6 @@ func TestAuthHandler_GetAccessibleHosts_DatabaseUnavailable(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", uint(1))
|
c.Set("userID", uint(1))
|
||||||
@@ -1380,7 +1431,6 @@ func TestAuthHandler_CheckHostAccess_DatabaseUnavailable(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler, _ := setupAuthHandler(t)
|
handler, _ := setupAuthHandler(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", uint(1))
|
c.Set("userID", uint(1))
|
||||||
@@ -1400,7 +1450,6 @@ func TestAuthHandler_CheckHostAccess_UserNotFound(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
handler, _ := setupAuthHandlerWithDB(t)
|
handler, _ := setupAuthHandlerWithDB(t)
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.Set("userID", uint(999999))
|
c.Set("userID", uint(999999))
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestBackupHandlerSanitizesFilename(t *testing.T) {
|
func TestBackupHandlerSanitizesFilename(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
// prepare a fake "database"
|
// prepare a fake "database"
|
||||||
dbPath := filepath.Join(tmpDir, "db.sqlite")
|
dbPath := filepath.Join(tmpDir, "db.sqlite")
|
||||||
|
|||||||
@@ -41,7 +41,8 @@ func (h *CerberusLogsHandler) LiveLogs(c *gin.Context) {
|
|||||||
logger.Log().Info("Cerberus logs WebSocket connection attempt")
|
logger.Log().Info("Cerberus logs WebSocket connection attempt")
|
||||||
|
|
||||||
// Upgrade HTTP connection to WebSocket
|
// Upgrade HTTP connection to WebSocket
|
||||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
// CheckOrigin is enforced on the shared upgrader in logs_ws.go (same package).
|
||||||
|
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) // nosemgrep: go.gorilla.security.audit.websocket-missing-origin-check.websocket-missing-origin-check
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().WithError(err).Error("Failed to upgrade Cerberus logs WebSocket")
|
logger.Log().WithError(err).Error("Failed to upgrade Cerberus logs WebSocket")
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCerberusLogsHandler_NewHandler verifies handler creation.
|
// TestCerberusLogsHandler_NewHandler verifies handler creation.
|
||||||
|
|||||||
@@ -2,14 +2,18 @@ package handlers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
"github.com/Wikid82/charon/backend/internal/logger"
|
"github.com/Wikid82/charon/backend/internal/logger"
|
||||||
|
"github.com/Wikid82/charon/backend/internal/models"
|
||||||
"github.com/Wikid82/charon/backend/internal/services"
|
"github.com/Wikid82/charon/backend/internal/services"
|
||||||
"github.com/Wikid82/charon/backend/internal/util"
|
"github.com/Wikid82/charon/backend/internal/util"
|
||||||
)
|
)
|
||||||
@@ -28,9 +32,10 @@ type CertificateHandler struct {
|
|||||||
service *services.CertificateService
|
service *services.CertificateService
|
||||||
backupService BackupServiceInterface
|
backupService BackupServiceInterface
|
||||||
notificationService *services.NotificationService
|
notificationService *services.NotificationService
|
||||||
|
db *gorm.DB
|
||||||
// Rate limiting for notifications
|
// Rate limiting for notifications
|
||||||
notificationMu sync.Mutex
|
notificationMu sync.Mutex
|
||||||
lastNotificationTime map[uint]time.Time
|
lastNotificationTime map[string]time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewCertificateHandler(service *services.CertificateService, backupService BackupServiceInterface, ns *services.NotificationService) *CertificateHandler {
|
func NewCertificateHandler(service *services.CertificateService, backupService BackupServiceInterface, ns *services.NotificationService) *CertificateHandler {
|
||||||
@@ -38,10 +43,18 @@ func NewCertificateHandler(service *services.CertificateService, backupService B
|
|||||||
service: service,
|
service: service,
|
||||||
backupService: backupService,
|
backupService: backupService,
|
||||||
notificationService: ns,
|
notificationService: ns,
|
||||||
lastNotificationTime: make(map[uint]time.Time),
|
lastNotificationTime: make(map[string]time.Time),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDB sets the database connection for user lookups (export re-auth).
|
||||||
|
func (h *CertificateHandler) SetDB(db *gorm.DB) {
|
||||||
|
h.db = db
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxFileSize is 1MB for certificate file uploads.
|
||||||
|
const maxFileSize = 1 << 20
|
||||||
|
|
||||||
func (h *CertificateHandler) List(c *gin.Context) {
|
func (h *CertificateHandler) List(c *gin.Context) {
|
||||||
certs, err := h.service.ListCertificates()
|
certs, err := h.service.ListCertificates()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -53,34 +66,41 @@ func (h *CertificateHandler) List(c *gin.Context) {
|
|||||||
c.JSON(http.StatusOK, certs)
|
c.JSON(http.StatusOK, certs)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UploadCertificateRequest struct {
|
func (h *CertificateHandler) Get(c *gin.Context) {
|
||||||
Name string `form:"name" binding:"required"`
|
certUUID := c.Param("uuid")
|
||||||
Certificate string `form:"certificate"` // PEM content
|
if certUUID == "" {
|
||||||
PrivateKey string `form:"private_key"` // PEM content
|
c.JSON(http.StatusBadRequest, gin.H{"error": "uuid is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
detail, err := h.service.GetCertificate(certUUID)
|
||||||
|
if err != nil {
|
||||||
|
if err == services.ErrCertNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log().WithError(err).Error("failed to get certificate")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to get certificate"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *CertificateHandler) Upload(c *gin.Context) {
|
func (h *CertificateHandler) Upload(c *gin.Context) {
|
||||||
// Handle multipart form
|
|
||||||
name := c.PostForm("name")
|
name := c.PostForm("name")
|
||||||
if name == "" {
|
if name == "" {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read files
|
// Read certificate file
|
||||||
certFile, err := c.FormFile("certificate_file")
|
certFile, err := c.FormFile("certificate_file")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "certificate_file is required"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "certificate_file is required"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
keyFile, err := c.FormFile("key_file")
|
|
||||||
if err != nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "key_file is required"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open and read content
|
|
||||||
certSrc, err := certFile.Open()
|
certSrc, err := certFile.Open()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open cert file"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open cert file"})
|
||||||
@@ -92,40 +112,80 @@ func (h *CertificateHandler) Upload(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
keySrc, err := keyFile.Open()
|
certBytes, err := io.ReadAll(io.LimitReader(certSrc, maxFileSize))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open key file"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read certificate file"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer func() {
|
certPEM := string(certBytes)
|
||||||
if errClose := keySrc.Close(); errClose != nil {
|
|
||||||
logger.Log().WithError(errClose).Warn("failed to close key file")
|
// Read private key file (optional — format detection is content-based in the service)
|
||||||
|
var keyPEM string
|
||||||
|
keyFile, err := c.FormFile("key_file")
|
||||||
|
if err == nil {
|
||||||
|
keySrc, errOpen := keyFile.Open()
|
||||||
|
if errOpen != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open key file"})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}()
|
defer func() {
|
||||||
|
if errClose := keySrc.Close(); errClose != nil {
|
||||||
|
logger.Log().WithError(errClose).Warn("failed to close key file")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Read to string
|
keyBytes, errRead := io.ReadAll(io.LimitReader(keySrc, maxFileSize))
|
||||||
// Limit size to avoid DoS (e.g. 1MB)
|
if errRead != nil {
|
||||||
certBytes := make([]byte, 1024*1024)
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read key file"})
|
||||||
n, _ := certSrc.Read(certBytes)
|
return
|
||||||
certPEM := string(certBytes[:n])
|
}
|
||||||
|
keyPEM = string(keyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
keyBytes := make([]byte, 1024*1024)
|
// Read chain file (optional)
|
||||||
n, _ = keySrc.Read(keyBytes)
|
var chainPEM string
|
||||||
keyPEM := string(keyBytes[:n])
|
chainFile, err := c.FormFile("chain_file")
|
||||||
|
if err == nil {
|
||||||
|
chainSrc, errOpen := chainFile.Open()
|
||||||
|
if errOpen != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open chain file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if errClose := chainSrc.Close(); errClose != nil {
|
||||||
|
logger.Log().WithError(errClose).Warn("failed to close chain file")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
cert, err := h.service.UploadCertificate(name, certPEM, keyPEM)
|
chainBytes, errRead := io.ReadAll(io.LimitReader(chainSrc, maxFileSize))
|
||||||
|
if errRead != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read chain file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chainPEM = string(chainBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Require key_file for non-PFX formats (PFX embeds the private key)
|
||||||
|
if keyPEM == "" {
|
||||||
|
format := services.DetectFormat(certBytes)
|
||||||
|
if format != services.FormatPFX {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "key_file is required for PEM/DER certificate uploads"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := h.service.UploadCertificate(name, certPEM, keyPEM, chainPEM)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().WithError(err).Error("failed to upload certificate")
|
logger.Log().WithError(err).Error("failed to upload certificate")
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to upload certificate"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Notification
|
|
||||||
if h.notificationService != nil {
|
if h.notificationService != nil {
|
||||||
h.notificationService.SendExternal(c.Request.Context(),
|
h.notificationService.SendExternal(c.Request.Context(),
|
||||||
"cert",
|
"cert",
|
||||||
"Certificate Uploaded",
|
"Certificate Uploaded",
|
||||||
fmt.Sprintf("Certificate %s uploaded", util.SanitizeForLog(cert.Name)),
|
"A new custom certificate was successfully uploaded.",
|
||||||
map[string]any{
|
map[string]any{
|
||||||
"Name": util.SanitizeForLog(cert.Name),
|
"Name": util.SanitizeForLog(cert.Name),
|
||||||
"Domains": util.SanitizeForLog(cert.Domains),
|
"Domains": util.SanitizeForLog(cert.Domains),
|
||||||
@@ -137,24 +197,255 @@ func (h *CertificateHandler) Upload(c *gin.Context) {
|
|||||||
c.JSON(http.StatusCreated, cert)
|
c.JSON(http.StatusCreated, cert)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type updateCertificateRequest struct {
|
||||||
|
Name string `json:"name" binding:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CertificateHandler) Update(c *gin.Context) {
|
||||||
|
certUUID := c.Param("uuid")
|
||||||
|
if certUUID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "uuid is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req updateCertificateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "name is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := h.service.UpdateCertificate(certUUID, req.Name)
|
||||||
|
if err != nil {
|
||||||
|
if err == services.ErrCertNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log().WithError(err).Error("failed to update certificate")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update certificate"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CertificateHandler) Validate(c *gin.Context) {
|
||||||
|
// Read certificate file
|
||||||
|
certFile, err := c.FormFile("certificate_file")
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "certificate_file is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
certSrc, err := certFile.Open()
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open cert file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if errClose := certSrc.Close(); errClose != nil {
|
||||||
|
logger.Log().WithError(errClose).Warn("failed to close certificate file")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
certBytes, err := io.ReadAll(io.LimitReader(certSrc, maxFileSize))
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read certificate file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read optional key file
|
||||||
|
var keyPEM string
|
||||||
|
keyFile, err := c.FormFile("key_file")
|
||||||
|
if err == nil {
|
||||||
|
keySrc, errOpen := keyFile.Open()
|
||||||
|
if errOpen != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open key file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if errClose := keySrc.Close(); errClose != nil {
|
||||||
|
logger.Log().WithError(errClose).Warn("failed to close key file")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
keyBytes, errRead := io.ReadAll(io.LimitReader(keySrc, maxFileSize))
|
||||||
|
if errRead != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read key file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyPEM = string(keyBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read optional chain file
|
||||||
|
var chainPEM string
|
||||||
|
chainFile, err := c.FormFile("chain_file")
|
||||||
|
if err == nil {
|
||||||
|
chainSrc, errOpen := chainFile.Open()
|
||||||
|
if errOpen != nil {
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to open chain file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if errClose := chainSrc.Close(); errClose != nil {
|
||||||
|
logger.Log().WithError(errClose).Warn("failed to close chain file")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
chainBytes, errRead := io.ReadAll(io.LimitReader(chainSrc, maxFileSize))
|
||||||
|
if errRead != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "failed to read chain file"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
chainPEM = string(chainBytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := h.service.ValidateCertificate(string(certBytes), keyPEM, chainPEM)
|
||||||
|
if err != nil {
|
||||||
|
logger.Log().WithError(err).Error("failed to validate certificate")
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{
|
||||||
|
"error": "validation failed",
|
||||||
|
"errors": []string{err.Error()},
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
type exportCertificateRequest struct {
|
||||||
|
Format string `json:"format" binding:"required"`
|
||||||
|
IncludeKey bool `json:"include_key"`
|
||||||
|
PFXPassword string `json:"pfx_password"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *CertificateHandler) Export(c *gin.Context) {
|
||||||
|
certUUID := c.Param("uuid")
|
||||||
|
if certUUID == "" {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "uuid is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req exportCertificateRequest
|
||||||
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
|
c.JSON(http.StatusBadRequest, gin.H{"error": "format is required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-authenticate when requesting private key
|
||||||
|
if req.IncludeKey {
|
||||||
|
if req.Password == "" {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "password required to export private key"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userVal, exists := c.Get("user")
|
||||||
|
if !exists || h.db == nil {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "authentication required"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap, ok := userVal.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "invalid session"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := userMap["id"]
|
||||||
|
if !ok {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "invalid session"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var user models.User
|
||||||
|
if err := h.db.First(&user, userID).Error; err != nil {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "user not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !user.CheckPassword(req.Password) {
|
||||||
|
c.JSON(http.StatusForbidden, gin.H{"error": "incorrect password"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data, filename, err := h.service.ExportCertificate(certUUID, req.Format, req.IncludeKey, req.PFXPassword)
|
||||||
|
if err != nil {
|
||||||
|
if err == services.ErrCertNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log().WithError(fmt.Errorf("%s", util.SanitizeForLog(err.Error()))).Error("failed to export certificate")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to export certificate"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
|
||||||
|
c.Data(http.StatusOK, "application/octet-stream", data)
|
||||||
|
}
|
||||||
|
|
||||||
func (h *CertificateHandler) Delete(c *gin.Context) {
|
func (h *CertificateHandler) Delete(c *gin.Context) {
|
||||||
idStr := c.Param("id")
|
idStr := c.Param("uuid")
|
||||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
|
||||||
if err != nil {
|
// Support both numeric ID (legacy) and UUID
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
if numID, err := strconv.ParseUint(idStr, 10, 32); err == nil && numID > 0 {
|
||||||
|
inUse, err := h.service.IsCertificateInUse(uint(numID))
|
||||||
|
if err != nil {
|
||||||
|
logger.Log().WithError(err).WithField("certificate_id", numID).Error("failed to check certificate usage")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check certificate usage"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if inUse {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "certificate is in use by one or more proxy hosts"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if h.backupService != nil {
|
||||||
|
if availableSpace, err := h.backupService.GetAvailableSpace(); err != nil {
|
||||||
|
logger.Log().WithError(err).Warn("unable to check disk space, proceeding with backup")
|
||||||
|
} else if availableSpace < 100*1024*1024 {
|
||||||
|
logger.Log().WithField("available_bytes", availableSpace).Warn("low disk space, skipping backup")
|
||||||
|
c.JSON(http.StatusInsufficientStorage, gin.H{"error": "insufficient disk space for backup"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := h.backupService.CreateBackup(); err != nil {
|
||||||
|
logger.Log().WithError(err).Error("failed to create backup before deletion")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create backup before deletion"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := h.service.DeleteCertificateByID(uint(numID)); err != nil {
|
||||||
|
if err == services.ErrCertInUse {
|
||||||
|
c.JSON(http.StatusConflict, gin.H{"error": "certificate is in use by one or more proxy hosts"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log().WithError(err).WithField("certificate_id", numID).Error("failed to delete certificate")
|
||||||
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete certificate"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
h.sendDeleteNotification(c, fmt.Sprintf("%d", numID))
|
||||||
|
c.JSON(http.StatusOK, gin.H{"message": "certificate deleted"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate ID range
|
// UUID path - parse to validate format and produce a canonical, safe string
|
||||||
if id == 0 {
|
parsedUUID, parseErr := uuid.Parse(idStr)
|
||||||
|
if parseErr != nil {
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
certUUID := parsedUUID.String()
|
||||||
|
|
||||||
// Check if certificate is in use before proceeding
|
inUse, err := h.service.IsCertificateInUseByUUID(certUUID)
|
||||||
inUse, err := h.service.IsCertificateInUse(uint(id))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().WithError(err).WithField("certificate_id", id).Error("failed to check certificate usage")
|
if err == services.ErrCertNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log().WithError(err).WithField("certificate_uuid", util.SanitizeForLog(certUUID)).Error("failed to check certificate usage")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check certificate usage"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to check certificate usage"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -163,13 +454,10 @@ func (h *CertificateHandler) Delete(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create backup before deletion
|
|
||||||
if h.backupService != nil {
|
if h.backupService != nil {
|
||||||
// Check disk space before backup (require at least 100MB free)
|
|
||||||
if availableSpace, err := h.backupService.GetAvailableSpace(); err != nil {
|
if availableSpace, err := h.backupService.GetAvailableSpace(); err != nil {
|
||||||
logger.Log().WithError(err).Warn("unable to check disk space, proceeding with backup")
|
logger.Log().WithError(err).Warn("unable to check disk space, proceeding with backup")
|
||||||
} else if availableSpace < 100*1024*1024 {
|
} else if availableSpace < 100*1024*1024 {
|
||||||
logger.Log().WithField("available_bytes", availableSpace).Warn("low disk space, skipping backup")
|
|
||||||
c.JSON(http.StatusInsufficientStorage, gin.H{"error": "insufficient disk space for backup"})
|
c.JSON(http.StatusInsufficientStorage, gin.H{"error": "insufficient disk space for backup"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -181,38 +469,62 @@ func (h *CertificateHandler) Delete(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Proceed with deletion
|
if err := h.service.DeleteCertificate(certUUID); err != nil {
|
||||||
if err := h.service.DeleteCertificate(uint(id)); err != nil {
|
|
||||||
if err == services.ErrCertInUse {
|
if err == services.ErrCertInUse {
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "certificate is in use by one or more proxy hosts"})
|
c.JSON(http.StatusConflict, gin.H{"error": "certificate is in use by one or more proxy hosts"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Log().WithError(err).WithField("certificate_id", id).Error("failed to delete certificate")
|
if err == services.ErrCertNotFound {
|
||||||
|
c.JSON(http.StatusNotFound, gin.H{"error": "certificate not found"})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Log().WithError(err).WithField("certificate_uuid", util.SanitizeForLog(certUUID)).Error("failed to delete certificate")
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete certificate"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete certificate"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send Notification with rate limiting (1 per cert per 10 seconds)
|
h.sendDeleteNotification(c, certUUID)
|
||||||
if h.notificationService != nil {
|
|
||||||
h.notificationMu.Lock()
|
|
||||||
lastTime, exists := h.lastNotificationTime[uint(id)]
|
|
||||||
if !exists || time.Since(lastTime) > 10*time.Second {
|
|
||||||
h.lastNotificationTime[uint(id)] = time.Now()
|
|
||||||
h.notificationMu.Unlock()
|
|
||||||
h.notificationService.SendExternal(c.Request.Context(),
|
|
||||||
"cert",
|
|
||||||
"Certificate Deleted",
|
|
||||||
fmt.Sprintf("Certificate ID %d deleted", id),
|
|
||||||
map[string]any{
|
|
||||||
"ID": id,
|
|
||||||
"Action": "deleted",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
h.notificationMu.Unlock()
|
|
||||||
logger.Log().WithField("certificate_id", id).Debug("notification rate limited")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "certificate deleted"})
|
c.JSON(http.StatusOK, gin.H{"message": "certificate deleted"})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *CertificateHandler) sendDeleteNotification(c *gin.Context, certRef string) {
|
||||||
|
if h.notificationService == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-validate to produce a CodeQL-safe value (breaks taint from user input).
|
||||||
|
// Callers already pass validated data; this is defense-in-depth.
|
||||||
|
safeRef := sanitizeCertRef(certRef)
|
||||||
|
|
||||||
|
h.notificationMu.Lock()
|
||||||
|
lastTime, exists := h.lastNotificationTime[certRef]
|
||||||
|
if exists && time.Since(lastTime) < 10*time.Second {
|
||||||
|
h.notificationMu.Unlock()
|
||||||
|
logger.Log().WithField("certificate_ref", safeRef).Debug("notification rate limited")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
h.lastNotificationTime[certRef] = time.Now()
|
||||||
|
h.notificationMu.Unlock()
|
||||||
|
|
||||||
|
h.notificationService.SendExternal(c.Request.Context(),
|
||||||
|
"cert",
|
||||||
|
"Certificate Deleted",
|
||||||
|
fmt.Sprintf("Certificate %s deleted", safeRef),
|
||||||
|
map[string]any{
|
||||||
|
"Ref": safeRef,
|
||||||
|
"Action": "deleted",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sanitizeCertRef re-validates a certificate reference (UUID or numeric ID)
|
||||||
|
// and returns a safe string representation. Returns a placeholder if invalid.
|
||||||
|
func sanitizeCertRef(ref string) string {
|
||||||
|
if parsed, err := uuid.Parse(ref); err == nil {
|
||||||
|
return parsed.String()
|
||||||
|
}
|
||||||
|
if n, err := strconv.ParseUint(ref, 10, 64); err == nil {
|
||||||
|
return strconv.FormatUint(n, 10)
|
||||||
|
}
|
||||||
|
return "[invalid-ref]"
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,18 @@
|
|||||||
package handlers
|
package handlers
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"mime/multipart"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/Wikid82/charon/backend/internal/models"
|
"github.com/Wikid82/charon/backend/internal/models"
|
||||||
"github.com/Wikid82/charon/backend/internal/services"
|
"github.com/Wikid82/charon/backend/internal/services"
|
||||||
@@ -16,10 +22,9 @@ func TestCertificateHandler_List_DBError(t *testing.T) {
|
|||||||
db := OpenTestDB(t)
|
db := OpenTestDB(t)
|
||||||
// Don't migrate to cause error
|
// Don't migrate to cause error
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.GET("/api/certificates", h.List)
|
r.GET("/api/certificates", h.List)
|
||||||
|
|
||||||
@@ -33,12 +38,11 @@ func TestCertificateHandler_List_DBError(t *testing.T) {
|
|||||||
func TestCertificateHandler_Delete_InvalidID(t *testing.T) {
|
func TestCertificateHandler_Delete_InvalidID(t *testing.T) {
|
||||||
db := OpenTestDBWithMigrations(t)
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/invalid", http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/invalid", http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -50,12 +54,11 @@ func TestCertificateHandler_Delete_InvalidID(t *testing.T) {
|
|||||||
func TestCertificateHandler_Delete_NotFound(t *testing.T) {
|
func TestCertificateHandler_Delete_NotFound(t *testing.T) {
|
||||||
db := OpenTestDBWithMigrations(t)
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/9999", http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/9999", http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -71,14 +74,13 @@ func TestCertificateHandler_Delete_NoBackupService(t *testing.T) {
|
|||||||
cert := models.SSLCertificate{UUID: "test-cert-no-backup", Name: "no-backup-cert", Provider: "custom", Domains: "nobackup.example.com"}
|
cert := models.SSLCertificate{UUID: "test-cert-no-backup", Name: "no-backup-cert", Provider: "custom", Domains: "nobackup.example.com"}
|
||||||
db.Create(&cert)
|
db.Create(&cert)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
// No backup service
|
// No backup service
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -97,12 +99,11 @@ func TestCertificateHandler_Delete_CheckUsageDBError(t *testing.T) {
|
|||||||
cert := models.SSLCertificate{UUID: "test-cert-db-err", Name: "db-error-cert", Provider: "custom", Domains: "dberr.example.com"}
|
cert := models.SSLCertificate{UUID: "test-cert-db-err", Name: "db-error-cert", Provider: "custom", Domains: "dberr.example.com"}
|
||||||
db.Create(&cert)
|
db.Create(&cert)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -118,10 +119,9 @@ func TestCertificateHandler_List_WithCertificates(t *testing.T) {
|
|||||||
db.Create(&models.SSLCertificate{UUID: "cert-1", Name: "Cert 1", Provider: "custom", Domains: "one.example.com"})
|
db.Create(&models.SSLCertificate{UUID: "cert-1", Name: "Cert 1", Provider: "custom", Domains: "one.example.com"})
|
||||||
db.Create(&models.SSLCertificate{UUID: "cert-2", Name: "Cert 2", Provider: "custom", Domains: "two.example.com"})
|
db.Create(&models.SSLCertificate{UUID: "cert-2", Name: "Cert 2", Provider: "custom", Domains: "two.example.com"})
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.GET("/api/certificates", h.List)
|
r.GET("/api/certificates", h.List)
|
||||||
|
|
||||||
@@ -139,12 +139,11 @@ func TestCertificateHandler_Delete_ZeroID(t *testing.T) {
|
|||||||
// DELETE /api/certificates/0 should return 400 Bad Request
|
// DELETE /api/certificates/0 should return 400 Bad Request
|
||||||
db := OpenTestDBWithMigrations(t)
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/0", http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/0", http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -173,11 +172,10 @@ func TestCertificateHandler_DBSetupOrdering(t *testing.T) {
|
|||||||
t.Fatalf("expected proxy_hosts table to exist before service initialization")
|
t.Fatalf("expected proxy_hosts table to exist before service initialization")
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
|
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.GET("/api/certificates", h.List)
|
r.GET("/api/certificates", h.List)
|
||||||
|
|
||||||
@@ -187,3 +185,395 @@ func TestCertificateHandler_DBSetupOrdering(t *testing.T) {
|
|||||||
|
|
||||||
assert.Equal(t, http.StatusOK, w.Code)
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Get handler tests ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Get_Success(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
expiry := time.Now().Add(30 * 24 * time.Hour)
|
||||||
|
db.Create(&models.SSLCertificate{UUID: "get-uuid-1", Name: "Get Test", Provider: "custom", Domains: "get.example.com", ExpiresAt: &expiry})
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.GET("/api/certificates/:uuid", h.Get)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/certificates/get-uuid-1", http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "get-uuid-1")
|
||||||
|
assert.Contains(t, w.Body.String(), "Get Test")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Get_NotFound(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.GET("/api/certificates/:uuid", h.Get)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/certificates/nonexistent-uuid", http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Get_EmptyUUID(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
// Route with empty uuid param won't match, test the handler directly with blank uuid
|
||||||
|
r.GET("/api/certificates/", h.Get)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/certificates/", http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Empty uuid should return 400 or 404 depending on router handling
|
||||||
|
assert.True(t, w.Code == http.StatusBadRequest || w.Code == http.StatusNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- SetDB test ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_SetDB(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
assert.Nil(t, h.db)
|
||||||
|
|
||||||
|
h.SetDB(db)
|
||||||
|
assert.NotNil(t, h.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update handler tests ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Update_Success(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
expiry := time.Now().Add(30 * 24 * time.Hour)
|
||||||
|
db.Create(&models.SSLCertificate{UUID: "upd-uuid-1", Name: "Old Name", Provider: "custom", Domains: "update.example.com", ExpiresAt: &expiry})
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.PUT("/api/certificates/:uuid", h.Update)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]string{"name": "New Name"})
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/certificates/upd-uuid-1", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "New Name")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Update_NotFound(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.PUT("/api/certificates/:uuid", h.Update)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]string{"name": "New Name"})
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/certificates/nonexistent-uuid", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Update_BadJSON(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.PUT("/api/certificates/:uuid", h.Update)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/certificates/some-uuid", strings.NewReader("{invalid"))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Update_MissingName(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.PUT("/api/certificates/:uuid", h.Update)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]string{})
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/certificates/some-uuid", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validate handler tests ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Validate_Success(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
_, _ = part.Write([]byte(certPEM))
|
||||||
|
part2, _ := writer.CreateFormFile("key_file", "key.pem")
|
||||||
|
_, _ = part2.Write([]byte(keyPEM))
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "valid")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Validate_NoCertFile(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", strings.NewReader(""))
|
||||||
|
req.Header.Set("Content-Type", "multipart/form-data")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Validate_CertOnly(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
certPEM, _, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
_, _ = part.Write([]byte(certPEM))
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Export handler tests ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_EmptyUUID(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{"format": "pem"})
|
||||||
|
// Use a route that provides :uuid param as empty would not match normal routing
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates//export", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
// Router won't match empty uuid, so 404 or redirect
|
||||||
|
assert.True(t, w.Code == http.StatusNotFound || w.Code == http.StatusMovedPermanently || w.Code == http.StatusBadRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_BadJSON(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/some-uuid/export", strings.NewReader("{bad"))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_NotFound(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{"format": "pem"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/nonexistent-uuid/export", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_PEMSuccess(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
certPEM, _, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cert := models.SSLCertificate{UUID: "export-uuid-1", Name: "Export Test", Provider: "custom", Domains: "export.example.com", Certificate: certPEM}
|
||||||
|
db.Create(&cert)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{"format": "pem"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/export-uuid-1/export", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Header().Get("Content-Disposition"), "Export Test.pem")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_IncludeKeyNoPassword(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
cert := models.SSLCertificate{UUID: "export-uuid-2", Name: "Key Test", Provider: "custom", Domains: "key.example.com"}
|
||||||
|
db.Create(&cert)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{"format": "pem", "include_key": true})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/export-uuid-2/export", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "password required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_IncludeKeyNoDBSet(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
cert := models.SSLCertificate{UUID: "export-uuid-3", Name: "No DB Test", Provider: "custom", Domains: "nodb.example.com"}
|
||||||
|
db.Create(&cert)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
// h.db is nil - not set via SetDB
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]any{"format": "pem", "include_key": true, "password": "test123"})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/export-uuid-3/export", bytes.NewBuffer(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "authentication required")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete via UUID path tests ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Delete_UUIDPath_NotFound(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
// Valid UUID format but does not exist
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/00000000-0000-0000-0000-000000000001", http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Delete_UUIDPath_InUse(t *testing.T) {
|
||||||
|
db := OpenTestDBWithMigrations(t)
|
||||||
|
cert := models.SSLCertificate{UUID: "11111111-1111-1111-1111-111111111111", Name: "InUse UUID", Provider: "custom", Domains: "uuid-inuse.example.com"}
|
||||||
|
db.Create(&cert)
|
||||||
|
|
||||||
|
ph := models.ProxyHost{UUID: "ph-uuid-del", Name: "Proxy", DomainNames: "uuid-inuse.example.com", ForwardHost: "localhost", ForwardPort: 8080, CertificateID: &cert.ID}
|
||||||
|
db.Create(&ph)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/11111111-1111-1111-1111-111111111111", http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusConflict, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- sanitizeCertRef tests ---
|
||||||
|
|
||||||
|
func TestSanitizeCertRef(t *testing.T) {
|
||||||
|
assert.Equal(t, "00000000-0000-0000-0000-000000000001", sanitizeCertRef("00000000-0000-0000-0000-000000000001"))
|
||||||
|
assert.Equal(t, "123", sanitizeCertRef("123"))
|
||||||
|
assert.Equal(t, "[invalid-ref]", sanitizeCertRef("not-valid"))
|
||||||
|
assert.Equal(t, "0", sanitizeCertRef("0"))
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,707 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Wikid82/charon/backend/internal/models"
|
||||||
|
"github.com/Wikid82/charon/backend/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Delete UUID path with backup service ---
|
||||||
|
|
||||||
|
func TestDelete_UUID_WithBackup_Success(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
db.Create(&models.SSLCertificate{UUID: certUUID, Name: "backup-uuid", Provider: "custom", Domains: "backup.test"})
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
mock := &mockBackupService{
|
||||||
|
createFunc: func() (string, error) { return "/tmp/backup.tar.gz", nil },
|
||||||
|
availableSpaceFunc: func() (int64, error) { return 1024 * 1024 * 1024, nil },
|
||||||
|
}
|
||||||
|
h := NewCertificateHandler(svc, mock, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_UUID_NotFound(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
nonExistentUUID := uuid.New().String()
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+nonExistentUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_UUID_InUse(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
cert := models.SSLCertificate{UUID: certUUID, Name: "inuse-uuid", Provider: "custom", Domains: "inuse.test"}
|
||||||
|
db.Create(&cert)
|
||||||
|
db.Create(&models.ProxyHost{UUID: "ph-uuid-inuse", Name: "ph", DomainNames: "inuse.test", ForwardHost: "localhost", ForwardPort: 8080, CertificateID: &cert.ID})
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusConflict, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_UUID_BackupLowSpace(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
db.Create(&models.SSLCertificate{UUID: certUUID, Name: "low-space", Provider: "custom", Domains: "lowspace.test"})
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
mock := &mockBackupService{
|
||||||
|
availableSpaceFunc: func() (int64, error) { return 1024, nil }, // 1KB - too low
|
||||||
|
}
|
||||||
|
h := NewCertificateHandler(svc, mock, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInsufficientStorage, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_UUID_BackupSpaceCheckError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
db.Create(&models.SSLCertificate{UUID: certUUID, Name: "space-err", Provider: "custom", Domains: "spaceerr.test"})
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
mock := &mockBackupService{
|
||||||
|
availableSpaceFunc: func() (int64, error) { return 0, fmt.Errorf("disk error") },
|
||||||
|
createFunc: func() (string, error) { return "/tmp/backup.tar.gz", nil },
|
||||||
|
}
|
||||||
|
h := NewCertificateHandler(svc, mock, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
// Space check error → proceeds with backup → succeeds
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_UUID_BackupCreateError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
db.Create(&models.SSLCertificate{UUID: certUUID, Name: "backup-fail", Provider: "custom", Domains: "backupfail.test"})
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
mock := &mockBackupService{
|
||||||
|
availableSpaceFunc: func() (int64, error) { return 1024 * 1024 * 1024, nil },
|
||||||
|
createFunc: func() (string, error) { return "", fmt.Errorf("backup creation failed") },
|
||||||
|
}
|
||||||
|
h := NewCertificateHandler(svc, mock, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete UUID with notification service ---
|
||||||
|
|
||||||
|
func TestDelete_UUID_WithNotification(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.Setting{}, &models.Notification{}, &models.NotificationProvider{}))
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
db.Create(&models.SSLCertificate{UUID: certUUID, Name: "notify-cert", Provider: "custom", Domains: "notify.test"})
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
notifSvc := services.NewNotificationService(db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, notifSvc)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validate handler ---
|
||||||
|
|
||||||
|
func TestValidate_Success(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
certPEM, _, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, err := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = part.Write([]byte(certPEM))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, writer.Close())
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_InvalidCert(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
part, err := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = part.Write([]byte("not a certificate"))
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, writer.Close())
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "unrecognized certificate format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_NoCertFile(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", http.NoBody)
|
||||||
|
req.Header.Set("Content-Type", "multipart/form-data; boundary=boundary")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidate_WithKeyAndChain(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
body := &bytes.Buffer{}
|
||||||
|
writer := multipart.NewWriter(body)
|
||||||
|
|
||||||
|
certPart, err := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = certPart.Write([]byte(certPEM))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
keyPart, err := writer.CreateFormFile("key_file", "key.pem")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = keyPart.Write([]byte(keyPEM))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
chainPart, err := writer.CreateFormFile("chain_file", "chain.pem")
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = chainPart.Write([]byte(certPEM)) // self-signed chain
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.NoError(t, writer.Close())
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Get handler DB error (non-NotFound) ---
|
||||||
|
|
||||||
|
func TestGet_DBError(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Deliberately don't migrate - any query will fail with "no such table"
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(t.TempDir(), db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.GET("/api/certificates/:uuid", h.Get)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/api/certificates/"+uuid.New().String(), http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
// Should be 500 since the table doesn't exist
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Export handler: re-auth and service error paths ---
|
||||||
|
|
||||||
|
func TestExport_IncludeKey_MissingPassword(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"pem","include_key":true}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExport_IncludeKey_NoUserContext(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
r := gin.New() // no middleware — "user" key absent
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExport_IncludeKey_InvalidClaimsType(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(func(c *gin.Context) { c.Set("user", "not-a-map"); c.Next() })
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExport_IncludeKey_UserIDNotInClaims(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(func(c *gin.Context) { c.Set("user", map[string]any{}); c.Next() }) // no "id" key
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExport_IncludeKey_UserNotFoundInDB(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(func(c *gin.Context) { c.Set("user", map[string]any{"id": float64(9999)}); c.Next() })
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"somepass"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExport_IncludeKey_WrongPassword(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
|
||||||
|
u := &models.User{UUID: uuid.New().String(), Email: "export@example.com", Name: "Export User"}
|
||||||
|
require.NoError(t, u.SetPassword("correctpass"))
|
||||||
|
require.NoError(t, db.Create(u).Error)
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(func(c *gin.Context) { c.Set("user", map[string]any{"id": float64(u.ID)}); c.Next() })
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"pem","include_key":true,"password":"wrongpass"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExport_CertNotFound(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"pem"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+uuid.New().String()+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusNotFound, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExport_ServiceError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
cert := models.SSLCertificate{UUID: certUUID, Name: "test", Domains: "test.example.com", Provider: "custom"}
|
||||||
|
require.NoError(t, db.Create(&cert).Error)
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
body := bytes.NewBufferString(`{"format":"unsupported_xyz"}`)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+certUUID+"/export", body)
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete numeric ID paths ---
|
||||||
|
|
||||||
|
func TestDelete_NumericID_UsageCheckError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{})) // no ProxyHost → IsCertificateInUse fails
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/1", http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_NumericID_LowDiskSpace(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
cert := models.SSLCertificate{UUID: uuid.New().String(), Name: "low-space", Domains: "lowspace.example.com", Provider: "custom"}
|
||||||
|
require.NoError(t, db.Create(&cert).Error)
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
backup := &mockBackupService{
|
||||||
|
availableSpaceFunc: func() (int64, error) { return 1024, nil }, // < 100 MB
|
||||||
|
createFunc: func() (string, error) { return "", nil },
|
||||||
|
}
|
||||||
|
h := NewCertificateHandler(svc, backup, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert.ID), http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInsufficientStorage, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_NumericID_BackupError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
cert := models.SSLCertificate{UUID: uuid.New().String(), Name: "backup-err", Domains: "backuperr.example.com", Provider: "custom"}
|
||||||
|
require.NoError(t, db.Create(&cert).Error)
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
backup := &mockBackupService{
|
||||||
|
availableSpaceFunc: func() (int64, error) { return 1 << 30, nil }, // 1 GB — plenty
|
||||||
|
createFunc: func() (string, error) { return "", fmt.Errorf("backup create failed") },
|
||||||
|
}
|
||||||
|
h := NewCertificateHandler(svc, backup, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert.ID), http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDelete_NumericID_DeleteError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.ProxyHost{})) // no SSLCertificate → DeleteCertificateByID fails
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/42", http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Delete UUID: internal usage-check error ---
|
||||||
|
|
||||||
|
func TestDelete_UUID_UsageCheckInternalError(t *testing.T) {
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{})) // no ProxyHost → IsCertificateInUse fails
|
||||||
|
|
||||||
|
certUUID := uuid.New().String()
|
||||||
|
cert := models.SSLCertificate{UUID: certUUID, Name: "uuid-err", Domains: "uuiderr.example.com", Provider: "custom"}
|
||||||
|
require.NoError(t, db.Create(&cert).Error)
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+certUUID, http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- sendDeleteNotification: rate limit ---
|
||||||
|
|
||||||
|
func TestSendDeleteNotification_RateLimit(t *testing.T) {
|
||||||
|
dsn := fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())
|
||||||
|
db, err := gorm.Open(sqlite.Open(dsn), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
ns := services.NewNotificationService(db, nil)
|
||||||
|
svc := services.NewCertificateService(t.TempDir(), db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, ns)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(w)
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodDelete, "/", http.NoBody)
|
||||||
|
|
||||||
|
certRef := uuid.New().String()
|
||||||
|
h.sendDeleteNotification(ctx, certRef) // first call — sets timestamp
|
||||||
|
h.sendDeleteNotification(ctx, certRef) // second call — hits rate limit branch
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update: empty UUID param (lines 207-209) ---
|
||||||
|
|
||||||
|
func TestUpdate_EmptyUUID(t *testing.T) {
|
||||||
|
svc := services.NewCertificateService(t.TempDir(), nil, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
ctx, _ := gin.CreateTestContext(w)
|
||||||
|
ctx.Request = httptest.NewRequest(http.MethodPut, "/api/certificates/", bytes.NewBufferString(`{"name":"test"}`))
|
||||||
|
ctx.Request.Header.Set("Content-Type", "application/json")
|
||||||
|
// No Params set — c.Param("uuid") returns ""
|
||||||
|
h.Update(ctx)
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Update: DB error (non-ErrCertNotFound) → lines 223-225 ---
|
||||||
|
|
||||||
|
func TestUpdate_DBError(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Deliberately no AutoMigrate → ssl_certificates table absent → "no such table" error
|
||||||
|
|
||||||
|
svc := services.NewCertificateService(t.TempDir(), db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.PUT("/api/certificates/:uuid", h.Update)
|
||||||
|
|
||||||
|
body, _ := json.Marshal(map[string]string{"name": "new-name"})
|
||||||
|
req := httptest.NewRequest(http.MethodPut, "/api/certificates/"+uuid.New().String(), bytes.NewReader(body))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
assert.Equal(t, http.StatusInternalServerError, w.Code)
|
||||||
|
}
|
||||||
@@ -25,15 +25,14 @@ func TestCertificateHandler_Delete_RequiresAuth(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
// Add a middleware that rejects all unauthenticated requests
|
// Add a middleware that rejects all unauthenticated requests
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||||
})
|
})
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/1", http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/1", http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -55,13 +54,12 @@ func TestCertificateHandler_List_RequiresAuth(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
// Add a middleware that rejects all unauthenticated requests
|
// Add a middleware that rejects all unauthenticated requests
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||||
})
|
})
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.GET("/api/certificates", h.List)
|
r.GET("/api/certificates", h.List)
|
||||||
|
|
||||||
@@ -85,13 +83,12 @@ func TestCertificateHandler_Upload_RequiresAuth(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
// Add a middleware that rejects all unauthenticated requests
|
// Add a middleware that rejects all unauthenticated requests
|
||||||
r.Use(func(c *gin.Context) {
|
r.Use(func(c *gin.Context) {
|
||||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "Unauthorized"})
|
||||||
})
|
})
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.POST("/api/certificates", h.Upload)
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
@@ -126,10 +123,9 @@ func TestCertificateHandler_Delete_DiskSpaceCheck(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert: %v", err)
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
// Mock backup service that reports low disk space
|
// Mock backup service that reports low disk space
|
||||||
mockBackup := &mockBackupService{
|
mockBackup := &mockBackupService{
|
||||||
@@ -139,7 +135,7 @@ func TestCertificateHandler_Delete_DiskSpaceCheck(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackup, nil)
|
h := NewCertificateHandler(svc, mockBackup, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -179,10 +175,9 @@ func TestCertificateHandler_Delete_NotificationRateLimiting(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert2: %v", err)
|
t.Fatalf("failed to create cert2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
mockBackup := &mockBackupService{
|
mockBackup := &mockBackupService{
|
||||||
createFunc: func() (string, error) {
|
createFunc: func() (string, error) {
|
||||||
@@ -191,7 +186,7 @@ func TestCertificateHandler_Delete_NotificationRateLimiting(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackup, nil)
|
h := NewCertificateHandler(svc, mockBackup, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
// Delete first cert
|
// Delete first cert
|
||||||
req1 := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert1.ID), http.NoBody)
|
req1 := httptest.NewRequest(http.MethodDelete, fmt.Sprintf("/api/certificates/%d", cert1.ID), http.NoBody)
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"gorm.io/driver/sqlite"
|
"gorm.io/driver/sqlite"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
|
||||||
@@ -34,13 +36,12 @@ func mockAuthMiddleware() gin.HandlerFunc {
|
|||||||
|
|
||||||
func setupCertTestRouter(t *testing.T, db *gorm.DB) *gin.Engine {
|
func setupCertTestRouter(t *testing.T, db *gorm.DB) *gin.Engine {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
|
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -108,10 +109,9 @@ func TestDeleteCertificate_CreatesBackup(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert: %v", err)
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
// Mock BackupService
|
// Mock BackupService
|
||||||
backupCalled := false
|
backupCalled := false
|
||||||
@@ -123,7 +123,7 @@ func TestDeleteCertificate_CreatesBackup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackupService, nil)
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -162,10 +162,9 @@ func TestDeleteCertificate_BackupFailure(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert: %v", err)
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
// Mock BackupService that fails
|
// Mock BackupService that fails
|
||||||
mockBackupService := &mockBackupService{
|
mockBackupService := &mockBackupService{
|
||||||
@@ -175,7 +174,7 @@ func TestDeleteCertificate_BackupFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackupService, nil)
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -216,10 +215,9 @@ func TestDeleteCertificate_InUse_NoBackup(t *testing.T) {
|
|||||||
t.Fatalf("failed to create proxy host: %v", err)
|
t.Fatalf("failed to create proxy host: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
// Mock BackupService
|
// Mock BackupService
|
||||||
backupCalled := false
|
backupCalled := false
|
||||||
@@ -231,7 +229,7 @@ func TestDeleteCertificate_InUse_NoBackup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackupService, nil)
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -294,11 +292,10 @@ func TestCertificateHandler_List(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.GET("/api/certificates", h.List)
|
r.GET("/api/certificates", h.List)
|
||||||
|
|
||||||
@@ -322,10 +319,9 @@ func TestCertificateHandler_Upload_MissingName(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.POST("/api/certificates", h.Upload)
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
@@ -350,10 +346,9 @@ func TestCertificateHandler_Upload_MissingCertFile(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.POST("/api/certificates", h.Upload)
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
@@ -381,10 +376,9 @@ func TestCertificateHandler_Upload_MissingKeyFile(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.POST("/api/certificates", h.Upload)
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
@@ -408,13 +402,17 @@ func TestCertificateHandler_Upload_MissingKeyFile_MultipartWithCert(t *testing.T
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.POST("/api/certificates", h.Upload)
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
|
certPEM, _, genErr := generateSelfSignedCertPEM()
|
||||||
|
if genErr != nil {
|
||||||
|
t.Fatalf("failed to generate self-signed cert: %v", genErr)
|
||||||
|
}
|
||||||
|
|
||||||
var body bytes.Buffer
|
var body bytes.Buffer
|
||||||
writer := multipart.NewWriter(&body)
|
writer := multipart.NewWriter(&body)
|
||||||
_ = writer.WriteField("name", "testcert")
|
_ = writer.WriteField("name", "testcert")
|
||||||
@@ -422,7 +420,7 @@ func TestCertificateHandler_Upload_MissingKeyFile_MultipartWithCert(t *testing.T
|
|||||||
if createErr != nil {
|
if createErr != nil {
|
||||||
t.Fatalf("failed to create form file: %v", createErr)
|
t.Fatalf("failed to create form file: %v", createErr)
|
||||||
}
|
}
|
||||||
_, _ = part.Write([]byte("-----BEGIN CERTIFICATE-----\nMIIB\n-----END CERTIFICATE-----"))
|
_, _ = part.Write([]byte(certPEM))
|
||||||
_ = writer.Close()
|
_ = writer.Close()
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
||||||
@@ -433,7 +431,7 @@ func TestCertificateHandler_Upload_MissingKeyFile_MultipartWithCert(t *testing.T
|
|||||||
if w.Code != http.StatusBadRequest {
|
if w.Code != http.StatusBadRequest {
|
||||||
t.Fatalf("expected 400 Bad Request, got %d, body=%s", w.Code, w.Body.String())
|
t.Fatalf("expected 400 Bad Request, got %d, body=%s", w.Code, w.Body.String())
|
||||||
}
|
}
|
||||||
if !strings.Contains(w.Body.String(), "key_file") {
|
if !strings.Contains(w.Body.String(), "key_file is required") {
|
||||||
t.Fatalf("expected error message about key_file, got: %s", w.Body.String())
|
t.Fatalf("expected error message about key_file, got: %s", w.Body.String())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -448,14 +446,13 @@ func TestCertificateHandler_Upload_Success(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
|
|
||||||
// Create a mock CertificateService that returns a created certificate
|
// Create a mock CertificateService that returns a created certificate
|
||||||
// Create a temporary services.CertificateService with a temp dir and DB
|
// Create a temporary services.CertificateService with a temp dir and DB
|
||||||
tmpDir := t.TempDir()
|
tmpDir := t.TempDir()
|
||||||
svc := services.NewCertificateService(tmpDir, db)
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.POST("/api/certificates", h.Upload)
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
@@ -516,6 +513,41 @@ func generateSelfSignedCertPEM() (certPEM, keyPEM string, err error) {
|
|||||||
|
|
||||||
// Note: mockCertificateService removed — helper tests now use real service instances or testify mocks inlined where required.
|
// Note: mockCertificateService removed — helper tests now use real service instances or testify mocks inlined where required.
|
||||||
|
|
||||||
|
// TestCertificateHandler_Upload_WithNotificationService verifies that the notification
|
||||||
|
// path is exercised when a non-nil NotificationService is provided.
|
||||||
|
func TestCertificateHandler_Upload_WithNotificationService(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.Setting{}, &models.NotificationProvider{}))
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
ns := services.NewNotificationService(db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, ns)
|
||||||
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
_ = writer.WriteField("name", "cert-with-ns")
|
||||||
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
_, _ = part.Write([]byte(certPEM))
|
||||||
|
part2, _ := writer.CreateFormFile("key_file", "key.pem")
|
||||||
|
_, _ = part2.Write([]byte(keyPEM))
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
// Test Delete with invalid ID format
|
// Test Delete with invalid ID format
|
||||||
func TestDeleteCertificate_InvalidID(t *testing.T) {
|
func TestDeleteCertificate_InvalidID(t *testing.T) {
|
||||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
@@ -526,12 +558,11 @@ func TestDeleteCertificate_InvalidID(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/invalid", http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/invalid", http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -552,12 +583,11 @@ func TestDeleteCertificate_ZeroID(t *testing.T) {
|
|||||||
t.Fatalf("failed to migrate: %v", err)
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/0", http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/0", http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -584,10 +614,9 @@ func TestDeleteCertificate_LowDiskSpace(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert: %v", err)
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
// Mock BackupService with low disk space
|
// Mock BackupService with low disk space
|
||||||
mockBackupService := &mockBackupService{
|
mockBackupService := &mockBackupService{
|
||||||
@@ -597,7 +626,7 @@ func TestDeleteCertificate_LowDiskSpace(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackupService, nil)
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -633,10 +662,9 @@ func TestDeleteCertificate_DiskSpaceCheckError(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert: %v", err)
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
// Mock BackupService with space check error but backup succeeds
|
// Mock BackupService with space check error but backup succeeds
|
||||||
mockBackupService := &mockBackupService{
|
mockBackupService := &mockBackupService{
|
||||||
@@ -649,7 +677,7 @@ func TestDeleteCertificate_DiskSpaceCheckError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackupService, nil)
|
h := NewCertificateHandler(svc, mockBackupService, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -661,6 +689,122 @@ func TestDeleteCertificate_DiskSpaceCheckError(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test that an expired Let's Encrypt certificate not in use can be deleted.
|
||||||
|
// The backend has no provider-based restrictions; deletion policy is frontend-only.
|
||||||
|
func TestDeleteCertificate_ExpiredLetsEncrypt_NotInUse(t *testing.T) {
|
||||||
|
dbPath := t.TempDir() + "/cert_expired_le.db"
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=1", dbPath)), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open db: %v", err)
|
||||||
|
}
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to access sql db: %v", err)
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxOpenConns(1)
|
||||||
|
sqlDB.SetMaxIdleConns(1)
|
||||||
|
|
||||||
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
|
||||||
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expired := time.Now().Add(-24 * time.Hour)
|
||||||
|
cert := models.SSLCertificate{
|
||||||
|
UUID: "expired-le-cert",
|
||||||
|
Name: "expired-le",
|
||||||
|
Provider: "letsencrypt",
|
||||||
|
Domains: "expired.example.com",
|
||||||
|
ExpiresAt: &expired,
|
||||||
|
}
|
||||||
|
if err = db.Create(&cert).Error; err != nil {
|
||||||
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
|
mockBS := &mockBackupService{
|
||||||
|
createFunc: func() (string, error) {
|
||||||
|
return "backup-expired-le.tar.gz", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewCertificateHandler(svc, mockBS, nil)
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200 OK, got %d, body=%s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var found models.SSLCertificate
|
||||||
|
if err = db.First(&found, cert.ID).Error; err == nil {
|
||||||
|
t.Fatal("expected expired LE certificate to be deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that a valid (non-expired) Let's Encrypt certificate not in use can be deleted.
|
||||||
|
// Confirms the backend imposes no provider-based restrictions on deletion.
|
||||||
|
func TestDeleteCertificate_ValidLetsEncrypt_NotInUse(t *testing.T) {
|
||||||
|
dbPath := t.TempDir() + "/cert_valid_le.db"
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?_journal_mode=WAL&_busy_timeout=5000&_foreign_keys=1", dbPath)), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to open db: %v", err)
|
||||||
|
}
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to access sql db: %v", err)
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxOpenConns(1)
|
||||||
|
sqlDB.SetMaxIdleConns(1)
|
||||||
|
|
||||||
|
if err = db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}); err != nil {
|
||||||
|
t.Fatalf("failed to migrate: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
future := time.Now().Add(30 * 24 * time.Hour)
|
||||||
|
cert := models.SSLCertificate{
|
||||||
|
UUID: "valid-le-cert",
|
||||||
|
Name: "valid-le",
|
||||||
|
Provider: "letsencrypt",
|
||||||
|
Domains: "valid.example.com",
|
||||||
|
ExpiresAt: &future,
|
||||||
|
}
|
||||||
|
if err = db.Create(&cert).Error; err != nil {
|
||||||
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
|
|
||||||
|
mockBS := &mockBackupService{
|
||||||
|
createFunc: func() (string, error) {
|
||||||
|
return "backup-valid-le.tar.gz", nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
h := NewCertificateHandler(svc, mockBS, nil)
|
||||||
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200 OK, got %d, body=%s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
var found models.SSLCertificate
|
||||||
|
if err = db.First(&found, cert.ID).Error; err == nil {
|
||||||
|
t.Fatal("expected valid LE certificate to be deleted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Test Delete when IsCertificateInUse fails
|
// Test Delete when IsCertificateInUse fails
|
||||||
func TestDeleteCertificate_UsageCheckError(t *testing.T) {
|
func TestDeleteCertificate_UsageCheckError(t *testing.T) {
|
||||||
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
@@ -679,12 +823,11 @@ func TestDeleteCertificate_UsageCheckError(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert: %v", err)
|
t.Fatalf("failed to create cert: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
h := NewCertificateHandler(svc, nil, nil)
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert.ID), http.NoBody)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -717,11 +860,10 @@ func TestDeleteCertificate_NotificationRateLimit(t *testing.T) {
|
|||||||
t.Fatalf("failed to create cert2: %v", err)
|
t.Fatalf("failed to create cert2: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
gin.SetMode(gin.TestMode)
|
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
r.Use(mockAuthMiddleware())
|
r.Use(mockAuthMiddleware())
|
||||||
svc := services.NewCertificateService("/tmp", db)
|
svc := services.NewCertificateService("/tmp", db, nil)
|
||||||
ns := services.NewNotificationService(db)
|
ns := services.NewNotificationService(db, nil)
|
||||||
|
|
||||||
mockBackupService := &mockBackupService{
|
mockBackupService := &mockBackupService{
|
||||||
createFunc: func() (string, error) {
|
createFunc: func() (string, error) {
|
||||||
@@ -730,7 +872,7 @@ func TestDeleteCertificate_NotificationRateLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := NewCertificateHandler(svc, mockBackupService, ns)
|
h := NewCertificateHandler(svc, mockBackupService, ns)
|
||||||
r.DELETE("/api/certificates/:id", h.Delete)
|
r.DELETE("/api/certificates/:uuid", h.Delete)
|
||||||
|
|
||||||
// Delete first certificate
|
// Delete first certificate
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert1.ID), http.NoBody)
|
req := httptest.NewRequest(http.MethodDelete, "/api/certificates/"+toStr(cert1.ID), http.NoBody)
|
||||||
|
|||||||
@@ -0,0 +1,382 @@
|
|||||||
|
package handlers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"mime/multipart"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"github.com/Wikid82/charon/backend/internal/crypto"
|
||||||
|
"github.com/Wikid82/charon/backend/internal/models"
|
||||||
|
"github.com/Wikid82/charon/backend/internal/services"
|
||||||
|
)
|
||||||
|
|
||||||
|
// --- Upload: with chain file (covers chain_file multipart branch) ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Upload_WithChainFile(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
_ = writer.WriteField("name", "chain-cert")
|
||||||
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
_, _ = part.Write([]byte(certPEM))
|
||||||
|
part2, _ := writer.CreateFormFile("key_file", "key.pem")
|
||||||
|
_, _ = part2.Write([]byte(keyPEM))
|
||||||
|
part3, _ := writer.CreateFormFile("chain_file", "chain.pem")
|
||||||
|
_, _ = part3.Write([]byte(certPEM))
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusCreated, w.Code, "body: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Upload: invalid cert data ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Upload_InvalidCertData(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates", h.Upload)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
_ = writer.WriteField("name", "bad-cert")
|
||||||
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
_, _ = part.Write([]byte("not-a-cert"))
|
||||||
|
part2, _ := writer.CreateFormFile("key_file", "key.pem")
|
||||||
|
_, _ = part2.Write([]byte("not-a-key"))
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Export re-authentication flow ---
|
||||||
|
|
||||||
|
func setupExportRouter(t *testing.T, db *gorm.DB) (*gin.Engine, *CertificateHandler) {
|
||||||
|
t.Helper()
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
return r, h
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTestEncSvc(t *testing.T) *crypto.EncryptionService {
|
||||||
|
t.Helper()
|
||||||
|
key := make([]byte, 32)
|
||||||
|
for i := range key {
|
||||||
|
key[i] = byte(i)
|
||||||
|
}
|
||||||
|
svc, err := crypto.NewEncryptionService(base64.StdEncoding.EncodeToString(key))
|
||||||
|
require.NoError(t, err)
|
||||||
|
return svc
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_IncludeKeySuccess(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
|
||||||
|
user := models.User{UUID: "export-user-1", Email: "export@test.com", Name: "Exporter"}
|
||||||
|
require.NoError(t, user.SetPassword("correctpassword"))
|
||||||
|
require.NoError(t, db.Create(&user).Error)
|
||||||
|
|
||||||
|
encSvc := newTestEncSvc(t)
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, encSvc)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
h.SetDB(db)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
info, err := svc.UploadCertificate("export-cert", certPEM, keyPEM, "")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Set("user", map[string]any{"id": user.ID})
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"format": "pem",
|
||||||
|
"include_key": true,
|
||||||
|
"password": "correctpassword",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/"+info.UUID+"/export", bytes.NewReader(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code, "body: %s", w.Body.String())
|
||||||
|
assert.Contains(t, w.Header().Get("Content-Disposition"), "export-cert.pem")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_IncludeKeyWrongPassword(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
|
||||||
|
r, h := setupExportRouter(t, db)
|
||||||
|
|
||||||
|
user := models.User{UUID: "wrong-pw-user", Email: "wrong@test.com", Name: "Wrong"}
|
||||||
|
require.NoError(t, user.SetPassword("rightpass"))
|
||||||
|
require.NoError(t, db.Create(&user).Error)
|
||||||
|
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Set("user", map[string]any{"id": user.ID})
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"format": "pem",
|
||||||
|
"include_key": true,
|
||||||
|
"password": "wrongpass",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/fake-uuid/export", bytes.NewReader(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "incorrect password")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_NoUserInContext(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
|
||||||
|
r, h := setupExportRouter(t, db)
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"format": "pem",
|
||||||
|
"include_key": true,
|
||||||
|
"password": "anything",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/fake-uuid/export", bytes.NewReader(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "authentication required")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_InvalidSession(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
|
||||||
|
r, h := setupExportRouter(t, db)
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Set("user", "not-a-map")
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"format": "pem",
|
||||||
|
"include_key": true,
|
||||||
|
"password": "anything",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/fake-uuid/export", bytes.NewReader(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "invalid session")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_MissingUserID(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
|
||||||
|
r, h := setupExportRouter(t, db)
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Set("user", map[string]any{"name": "test"})
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"format": "pem",
|
||||||
|
"include_key": true,
|
||||||
|
"password": "anything",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/fake-uuid/export", bytes.NewReader(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "invalid session")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Export_UserNotFound(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}, &models.User{}))
|
||||||
|
|
||||||
|
r, h := setupExportRouter(t, db)
|
||||||
|
r.Use(func(c *gin.Context) {
|
||||||
|
c.Set("user", map[string]any{"id": uint(9999)})
|
||||||
|
c.Next()
|
||||||
|
})
|
||||||
|
r.POST("/api/certificates/:uuid/export", h.Export)
|
||||||
|
|
||||||
|
payload, _ := json.Marshal(map[string]any{
|
||||||
|
"format": "pem",
|
||||||
|
"include_key": true,
|
||||||
|
"password": "anything",
|
||||||
|
})
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/fake-uuid/export", bytes.NewReader(payload))
|
||||||
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusForbidden, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "user not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Validate handler with key and chain ---
|
||||||
|
|
||||||
|
func TestCertificateHandler_Validate_WithKeyAndChain(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
certPEM, keyPEM, err := generateSelfSignedCertPEM()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
_, _ = part.Write([]byte(certPEM))
|
||||||
|
part2, _ := writer.CreateFormFile("key_file", "key.pem")
|
||||||
|
_, _ = part2.Write([]byte(keyPEM))
|
||||||
|
part3, _ := writer.CreateFormFile("chain_file", "chain.pem")
|
||||||
|
_, _ = part3.Write([]byte(certPEM))
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code, "body: %s", w.Body.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Validate_InvalidCert(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
part, _ := writer.CreateFormFile("certificate_file", "cert.pem")
|
||||||
|
_, _ = part.Write([]byte("not-a-cert"))
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusOK, w.Code)
|
||||||
|
var resp map[string]any
|
||||||
|
require.NoError(t, json.Unmarshal(w.Body.Bytes(), &resp))
|
||||||
|
errList, ok := resp["errors"].([]any)
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Greater(t, len(errList), 0, "expected validation errors in response")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCertificateHandler_Validate_MissingCertFile(t *testing.T) {
|
||||||
|
db, err := gorm.Open(sqlite.Open(fmt.Sprintf("file:%s?mode=memory&cache=shared", t.Name())), &gorm.Config{})
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NoError(t, db.AutoMigrate(&models.SSLCertificate{}, &models.ProxyHost{}))
|
||||||
|
|
||||||
|
tmpDir := t.TempDir()
|
||||||
|
svc := services.NewCertificateService(tmpDir, db, nil)
|
||||||
|
h := NewCertificateHandler(svc, nil, nil)
|
||||||
|
|
||||||
|
r := gin.New()
|
||||||
|
r.Use(mockAuthMiddleware())
|
||||||
|
r.POST("/api/certificates/validate", h.Validate)
|
||||||
|
|
||||||
|
var body bytes.Buffer
|
||||||
|
writer := multipart.NewWriter(&body)
|
||||||
|
_ = writer.WriteField("name", "test")
|
||||||
|
_ = writer.Close()
|
||||||
|
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/api/certificates/validate", &body)
|
||||||
|
req.Header.Set("Content-Type", writer.FormDataContentType())
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
r.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusBadRequest, w.Code)
|
||||||
|
assert.Contains(t, w.Body.String(), "certificate_file is required")
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user