Compare commits
1636 commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
05b54687b6 | ||
![]() |
c4473839c4 | ||
![]() |
d2c4bc55fc | ||
![]() |
2abc078e53 | ||
![]() |
ceb4479ec4 | ||
![]() |
845d4542bb | ||
![]() |
f4ed7b3520 | ||
![]() |
60431804d8 | ||
![]() |
0f942a95f1 | ||
![]() |
97e6588a45 | ||
![]() |
725cae1fa8 | ||
![]() |
c64332d30a | ||
![]() |
718d1c54b2 | ||
![]() |
b48b728317 | ||
![]() |
fb393f1c57 | ||
![]() |
630cbf0c70 | ||
![]() |
95f27677e4 | ||
![]() |
c6e40191dd | ||
![]() |
0746e0c091 | ||
![]() |
2291a232cb | ||
![]() |
0e8a1c681b | ||
![]() |
990dd5e08e | ||
![]() |
2682f801df | ||
![]() |
912c4bca70 | ||
![]() |
26bcd0912a | ||
![]() |
63bd31b471 | ||
![]() |
be97466809 | ||
![]() |
df13f43156 | ||
![]() |
368d22ec30 | ||
![]() |
f6bb8412c5 | ||
![]() |
2e1ddec107 | ||
![]() |
52f86c2d10 | ||
![]() |
7779c7ff0c | ||
![]() |
75a50c0c9d | ||
![]() |
d9f2a22ee5 | ||
![]() |
c76325b91b | ||
![]() |
dd71f0a866 | ||
![]() |
b63e64ee9f | ||
![]() |
6de62a1468 | ||
![]() |
b411782648 | ||
![]() |
2f49088163 | ||
![]() |
fd2bb8927c | ||
![]() |
e9b0f3c54e | ||
![]() |
a6b0e58380 | ||
![]() |
caca4032d1 | ||
![]() |
7dd86e2b95 | ||
![]() |
06bebdeac7 | ||
![]() |
742f5e8cda | ||
![]() |
6c042f18f0 | ||
![]() |
2a7e8383c8 | ||
![]() |
b1c09f7512 | ||
![]() |
bd785ede15 | ||
![]() |
1a56a0e0b9 | ||
![]() |
49e0735b53 | ||
![]() |
6daaab1789 | ||
![]() |
e8ff13bc17 | ||
![]() |
a928b4d001 | ||
![]() |
44ec3b9e01 | ||
![]() |
6c5e8afde9 | ||
![]() |
1eab943ec2 | ||
![]() |
8108e4156d | ||
![]() |
5731491b4e | ||
![]() |
98560d0cf5 | ||
![]() |
e611d01c90 | ||
![]() |
5356ccc6cd | ||
![]() |
d8877a71fc | ||
![]() |
e7ecea764e | ||
![]() |
41b43733b0 | ||
![]() |
8e9e091656 | ||
![]() |
a23fe06d68 | ||
![]() |
4bf640c6e8 | ||
![]() |
e34af358d7 | ||
![]() |
0df8f54fbb | ||
![]() |
8da490f593 | ||
![]() |
3e3df5e4c6 | ||
![]() |
f3ea88f64c | ||
![]() |
e976614645 | ||
![]() |
717fc97ca0 | ||
![]() |
97c441dab6 | ||
![]() |
8de8bf0e06 | ||
![]() |
2bbf0b4762 | ||
![]() |
45571cea08 | ||
![]() |
d34fb7e8a8 | ||
![]() |
4561eb787b | ||
![]() |
a6a4d460d7 | ||
![]() |
eada3739e6 | ||
![]() |
bdecf38616 | ||
![]() |
5c83695177 | ||
![]() |
2853410576 | ||
![]() |
58a1d7164f | ||
![]() |
332af5dd8d | ||
![]() |
fa56d35a48 | ||
![]() |
df159b0167 | ||
![]() |
af1df0696b | ||
![]() |
3208a40ef3 | ||
![]() |
4e724f6c0a | ||
![]() |
fdc525164a | ||
![]() |
81acad0d66 | ||
![]() |
5ff8a03195 | ||
![]() |
4160bb8102 | ||
![]() |
f5fbe4a200 | ||
![]() |
45c669fb65 | ||
![]() |
825c08aa9d | ||
![]() |
af14f1085f | ||
![]() |
e6f5d157b8 | ||
![]() |
785fce4dc7 | ||
![]() |
17db4cb970 | ||
![]() |
4192af30d5 | ||
![]() |
3921c3f480 | ||
![]() |
6507e8f4cd | ||
![]() |
66544baa7f | ||
![]() |
311dfdee1f | ||
![]() |
91b0fce955 | ||
![]() |
532e97e00f | ||
![]() |
d7116a4a6f | ||
![]() |
2fb6f209aa | ||
![]() |
3f9e8e81e6 | ||
![]() |
8c75efdb2a | ||
![]() |
f75cdeb239 | ||
![]() |
4b8e6cd780 | ||
![]() |
84606eb207 | ||
![]() |
dc698ecea8 | ||
![]() |
455acf7c90 | ||
![]() |
19d36c0fb2 | ||
![]() |
ce32fc019e | ||
![]() |
6ffb68322f | ||
![]() |
fa8d5b6992 | ||
![]() |
5d0d5ac9c9 | ||
![]() |
d760b401e6 | ||
![]() |
4df4e5b3bf | ||
![]() |
70e8377c0d | ||
![]() |
685cda545b | ||
![]() |
f156f178cd | ||
![]() |
a52f1b75ff | ||
![]() |
421ef3bf9c | ||
![]() |
08794c5b6d | ||
![]() |
a65223aa5b | ||
![]() |
24b5e8f100 | ||
![]() |
c6e4762f28 | ||
![]() |
6acbcb0a33 | ||
![]() |
e452dc80bd | ||
![]() |
fd309134a2 | ||
![]() |
48f011dc1c | ||
![]() |
75d8ad9798 | ||
![]() |
03bb194d2c | ||
![]() |
6ca053ca67 | ||
![]() |
1e0bcedef5 | ||
![]() |
733f5e165b | ||
![]() |
0ef5f20aa7 | ||
![]() |
fca8883cd9 | ||
![]() |
896dfefcdf | ||
![]() |
6960419a2e | ||
![]() |
adba4e2a2f | ||
![]() |
aa4f02c798 | ||
![]() |
260f5a7992 | ||
![]() |
0f722916b8 | ||
![]() |
a59ae61441 | ||
![]() |
437a97510a | ||
![]() |
f306d59016 | ||
![]() |
58f91dc951 | ||
![]() |
bd47dac6a3 | ||
![]() |
5d5a1117e1 | ||
![]() |
ecd1a8bfed | ||
![]() |
f99f003a50 | ||
![]() |
5622ac8338 | ||
![]() |
da746f77d5 | ||
![]() |
1c03fbe99e | ||
![]() |
a504113186 | ||
![]() |
2a2b09b52a | ||
![]() |
ca784b147b | ||
![]() |
b6f272d09a | ||
![]() |
a62e28fdfb | ||
![]() |
bc9bfa81b2 | ||
![]() |
162768bdec | ||
![]() |
2212c2f847 | ||
![]() |
33e3fdabe4 | ||
![]() |
e1932ff01e | ||
![]() |
052accd6bb | ||
![]() |
240f057f95 | ||
![]() |
6e34d609b7 | ||
![]() |
fd22bb5ec2 | ||
![]() |
1530d93fc1 | ||
![]() |
e0e9e3ef16 | ||
![]() |
44eb4d4a94 | ||
![]() |
822fcdacbb | ||
![]() |
08694adf1b | ||
![]() |
c2c173ac7e | ||
![]() |
a79fcaf378 | ||
![]() |
bc3a179af9 | ||
![]() |
9b07e1f7ce | ||
![]() |
a851e14c88 | ||
![]() |
a941576acc | ||
![]() |
89f704ef18 | ||
![]() |
67cdf91f94 | ||
![]() |
51f70e47e3 | ||
![]() |
12d9fba4b3 | ||
![]() |
6eabb461ce | ||
![]() |
b1c9717e21 | ||
![]() |
4a4b309790 | ||
![]() |
acd2a14dc9 | ||
![]() |
c10aad79d9 | ||
![]() |
1b4ec66148 | ||
![]() |
04f3dc09f9 | ||
![]() |
c707b72b03 | ||
![]() |
84cbff16d4 | ||
![]() |
b1f85693c2 | ||
![]() |
518c7f178a | ||
![]() |
4acb4f8df3 | ||
![]() |
3e86f52250 | ||
![]() |
8cca4346a5 | ||
![]() |
90d3a21853 | ||
![]() |
1ab4487b65 | ||
![]() |
486f96e7ac | ||
![]() |
8bb7da3994 | ||
![]() |
0f3ae64062 | ||
![]() |
0c4093dcca | ||
![]() |
23968e472d | ||
![]() |
a5ab73d458 | ||
![]() |
f8755be9cd | ||
![]() |
d1bfaddb69 | ||
![]() |
bfc92ca1c5 | ||
![]() |
ed3d501081 | ||
![]() |
7e5ab344a2 | ||
![]() |
7c5cbef51a | ||
![]() |
6b0bdc5eeb | ||
![]() |
1aa4fc5949 | ||
![]() |
380cbf70a9 | ||
![]() |
05c1825622 | ||
![]() |
6a61b919e7 | ||
![]() |
15542b78fb | ||
![]() |
b164373997 | ||
![]() |
ffcab0b2bc | ||
![]() |
32e9eb4be4 | ||
![]() |
76d4bc7788 | ||
![]() |
ec199162dc | ||
![]() |
1dcf9d1ae1 | ||
![]() |
7ffa0cc787 | ||
![]() |
ec53c672dc | ||
![]() |
92f923cfa8 | ||
![]() |
947b247a40 | ||
![]() |
d7ef51e6ba | ||
![]() |
a51bce8f8d | ||
![]() |
47eb2e240d | ||
![]() |
ddd6ee8e42 | ||
![]() |
5cd4406f5e | ||
![]() |
4934fce769 | ||
![]() |
272cf543b3 | ||
![]() |
d2d788c5dc | ||
![]() |
a4dc5053d2 | ||
![]() |
19de3a8a77 | ||
![]() |
e7ad3d88ae | ||
![]() |
3cd4847093 | ||
![]() |
b2a6eb92bf | ||
![]() |
f0cda0406b | ||
![]() |
ff7acd3347 | ||
![]() |
a6b55f2b5e | ||
![]() |
a254b436c7 | ||
![]() |
3b1563a538 | ||
![]() |
0ecb6eefee | ||
![]() |
6e228f3f3f | ||
![]() |
28238cb01f | ||
![]() |
0dd22e8b93 | ||
![]() |
9ae8bd79c5 | ||
![]() |
6b5da29e3d | ||
![]() |
6c20d38c41 | ||
![]() |
338141f067 | ||
![]() |
9235f55c47 | ||
![]() |
61d4ccbfdd | ||
![]() |
89028f17cf | ||
![]() |
3253b16f0f | ||
![]() |
5618ba9f46 | ||
![]() |
d39131d154 | ||
![]() |
8b5ad6990d | ||
![]() |
6dadfcb2ef | ||
![]() |
7a4796d655 | ||
![]() |
cba6de024f | ||
![]() |
bfda483c0a | ||
![]() |
3cb9dbdb21 | ||
![]() |
b8e6bd8c9a | ||
![]() |
95ed308207 | ||
![]() |
0d1c4c6070 | ||
![]() |
8f6659a2ec | ||
![]() |
9dba6db676 | ||
![]() |
37c0c067a8 | ||
![]() |
e4dcdd2572 | ||
![]() |
ac01faf483 | ||
![]() |
4c08e1e68c | ||
![]() |
d5b6f2974b | ||
![]() |
64deeab1ec | ||
![]() |
b2212f4225 | ||
![]() |
ce276d3838 | ||
![]() |
43ef32aa8d | ||
![]() |
0040569fa9 | ||
![]() |
6b9e065764 | ||
![]() |
d45bec4047 | ||
![]() |
702da0f59a | ||
![]() |
f02f34d64c | ||
![]() |
fd94e2c056 | ||
![]() |
aff80a2863 | ||
![]() |
22146eb3e4 | ||
![]() |
b562103024 | ||
![]() |
25868f27de | ||
![]() |
0c9943b740 | ||
![]() |
32f196a774 | ||
![]() |
c588be0842 | ||
![]() |
f2154e362b | ||
![]() |
2aa55e9444 | ||
![]() |
e36df40ba7 | ||
![]() |
86d9384954 | ||
![]() |
b4d9223625 | ||
![]() |
1ec52431b6 | ||
![]() |
e8e2ade8f0 | ||
![]() |
6a6501691a | ||
![]() |
caaed7c515 | ||
![]() |
afeb541eac | ||
![]() |
93c22f29cf | ||
![]() |
0f319b31fd | ||
![]() |
d6361d0a40 | ||
![]() |
cd9d8f309d | ||
![]() |
0334a9afe8 | ||
![]() |
31c5727a90 | ||
![]() |
644c767019 | ||
![]() |
6ba682a32f | ||
![]() |
1d5baa657f | ||
![]() |
2cb7b0bee6 | ||
![]() |
a18df9c3bb | ||
![]() |
ffadd42779 | ||
![]() |
55247cd46a | ||
![]() |
643445b7cf | ||
![]() |
9dfc66ef04 | ||
![]() |
ae53c0f1cc | ||
![]() |
718721b341 | ||
![]() |
5cb7013575 | ||
![]() |
a01ce18b98 | ||
![]() |
1a6f12c88e | ||
![]() |
5e7c0e0f49 | ||
![]() |
867245aefb | ||
![]() |
389ea4293f | ||
![]() |
77d58652a3 | ||
![]() |
4bc225f26b | ||
![]() |
fc78845a97 | ||
![]() |
395cace69f | ||
![]() |
7106d396dc | ||
![]() |
f12ff3dfed | ||
![]() |
b52b4252c1 | ||
![]() |
202112bcae | ||
![]() |
46fff0b544 | ||
![]() |
b6b6fd026b | ||
![]() |
9ac5aeda79 | ||
![]() |
3c16139c44 | ||
![]() |
bb16552aca | ||
![]() |
e73ceafdba | ||
![]() |
9af546bd0a | ||
![]() |
11b7b1bc88 | ||
![]() |
6c8ddbccac | ||
![]() |
f9ca14f010 | ||
![]() |
1295de928a | ||
![]() |
01d7c1a5c2 | ||
![]() |
c10bca93df | ||
![]() |
2fa826318e | ||
![]() |
59afb285f3 | ||
![]() |
9967d60987 | ||
![]() |
486b56d1ed | ||
![]() |
8bcb4c2436 | ||
![]() |
73f71a0aa3 | ||
![]() |
17cd792826 | ||
![]() |
bd41f855cf | ||
![]() |
e61d5a3034 | ||
![]() |
ebe25d7653 | ||
![]() |
893394ef5f | ||
![]() |
e404e0b608 | ||
![]() |
956703c31a | ||
![]() |
85839b0199 | ||
![]() |
6e18c652cb | ||
![]() |
a910b7beca | ||
![]() |
d26e17f505 | ||
![]() |
aeca8f40c2 | ||
![]() |
4137482f65 | ||
![]() |
98c6038fde | ||
![]() |
507da49b5a | ||
![]() |
9beb5388cb | ||
![]() |
d4c0643122 | ||
![]() |
e42841cd00 | ||
![]() |
62caffb102 | ||
![]() |
fddf597040 | ||
![]() |
8bfeb7d90d | ||
![]() |
40e6b205bc | ||
![]() |
da6106bd23 | ||
![]() |
4c4b545e9b | ||
![]() |
f7409d47be | ||
![]() |
062f71fb92 | ||
![]() |
89c3c18c19 | ||
![]() |
c3c2608947 | ||
![]() |
2c8769adf6 | ||
![]() |
381220daf4 | ||
![]() |
b9a3acb03f | ||
![]() |
76429f033a | ||
![]() |
cf747d65e0 | ||
![]() |
25bb23d8b7 | ||
![]() |
6096cb3c9b | ||
![]() |
4e2c9c185b | ||
![]() |
8da9d5eefd | ||
![]() |
5b3200173e | ||
![]() |
edd062522d | ||
![]() |
3cc6b2c0d0 | ||
![]() |
9ccdddaab1 | ||
![]() |
0191faf3a8 | ||
![]() |
2a8e97d558 | ||
![]() |
7673a20467 | ||
![]() |
18764eff0e | ||
![]() |
e3cb4ab2c4 | ||
![]() |
26e2bbd709 | ||
![]() |
3b1f412e44 | ||
![]() |
a4eee41fd7 | ||
![]() |
228e4f9acc | ||
![]() |
d757cf8e84 | ||
![]() |
396dcf8e6e | ||
![]() |
12c32d507c | ||
![]() |
f6544962ea | ||
![]() |
084186c67a | ||
![]() |
92a9d6c321 | ||
![]() |
3c1b957050 | ||
![]() |
4fbc3402fb | ||
![]() |
6720d89845 | ||
![]() |
f6924f8c57 | ||
![]() |
9167bd107d | ||
![]() |
b2d3520519 | ||
![]() |
364b833d67 | ||
![]() |
0416a41d58 | ||
![]() |
1f9f81da70 | ||
![]() |
025f14f879 | ||
![]() |
e5fe74ce77 | ||
![]() |
e1400d28f1 | ||
![]() |
534328ca30 | ||
![]() |
eddb994c0b | ||
![]() |
6e3ca35941 | ||
![]() |
0ea1508ff9 | ||
![]() |
fcf44f7b31 | ||
![]() |
412b4c4b0b | ||
![]() |
0ddd42c01f | ||
![]() |
77f2968267 | ||
![]() |
8aca0ea860 | ||
![]() |
abbc130844 | ||
![]() |
424215f228 | ||
![]() |
e1f5ed41df | ||
![]() |
5ac33aab03 | ||
![]() |
4ae41a363d | ||
![]() |
71b7a594bd | ||
![]() |
2701454f23 | ||
![]() |
e1f4a71357 | ||
![]() |
a753ea6981 | ||
![]() |
8f71edaadd | ||
![]() |
4ff8f498ce | ||
![]() |
6bb20fa951 | ||
![]() |
88587822c1 | ||
![]() |
c0e6c1ac78 | ||
![]() |
d286b044e7 | ||
![]() |
0d1adfc7db | ||
![]() |
3041023ed8 | ||
![]() |
66dfded0cf | ||
![]() |
6b744884b0 | ||
![]() |
774a8cfc00 | ||
![]() |
0c5d233563 | ||
![]() |
9a5a937695 | ||
![]() |
7fa469d0b0 | ||
![]() |
1e018bdaf8 | ||
![]() |
0279e549bd | ||
![]() |
3132aa54b7 | ||
![]() |
38ab6be7c2 | ||
![]() |
3fa555fb25 | ||
![]() |
ea6401ce09 | ||
![]() |
61bea26486 | ||
![]() |
772d5b5c32 | ||
![]() |
1095f6c875 | ||
![]() |
169b844212 | ||
![]() |
f39fbf07fa | ||
![]() |
d769fff1e8 | ||
![]() |
68d4bdc1bd | ||
![]() |
bbfb7d1cfa | ||
![]() |
3884c5f47d | ||
![]() |
80de87ac34 | ||
![]() |
a3e5f0a3a0 | ||
![]() |
91eb39cff6 | ||
![]() |
dc38e5ac00 | ||
![]() |
a74e424d53 | ||
![]() |
d87f088b8f | ||
![]() |
86971da274 | ||
![]() |
618be9ff68 | ||
![]() |
c77fe16943 | ||
![]() |
94c7efdb5b | ||
![]() |
b1f2063a9a | ||
![]() |
855f9e6f8d | ||
![]() |
e61a464951 | ||
![]() |
b451d190b7 | ||
![]() |
9c90144867 | ||
![]() |
6aaf3cd50b | ||
![]() |
0a114ca7d1 | ||
![]() |
e161507d08 | ||
![]() |
9faa49c7e8 | ||
![]() |
d95b7afe61 | ||
![]() |
9d5aaf5ea2 | ||
![]() |
5b0fe4b7f1 | ||
![]() |
e71d146a2d | ||
![]() |
137e7408fd | ||
![]() |
e63a3ab92b | ||
![]() |
c7014dba8f | ||
![]() |
0a748d324e | ||
![]() |
16a3be49e2 | ||
![]() |
e27a0a0e14 | ||
![]() |
b2c2c5ac59 | ||
![]() |
a19748ae35 | ||
![]() |
85ab9c68a2 | ||
![]() |
f6d6c5bb2b | ||
![]() |
ef4e61e05e | ||
![]() |
54fc5e48dc | ||
![]() |
7f04d12333 | ||
![]() |
76c84c69c4 | ||
![]() |
6888fa2133 | ||
![]() |
dd76c07293 | ||
![]() |
d7119b73ab | ||
![]() |
a6bb2cf5e1 | ||
![]() |
83db873f5b | ||
![]() |
cdd2401ff6 | ||
![]() |
83b6c2cfef | ||
![]() |
3962daa3bd | ||
![]() |
9ae99964e6 | ||
![]() |
37c8d4419a | ||
![]() |
301782ae18 | ||
![]() |
ff17a961fc | ||
![]() |
60b3f63851 | ||
![]() |
39a4a256fd | ||
![]() |
01ea78c10e | ||
![]() |
40b5bcb918 | ||
![]() |
75d8b821ff | ||
![]() |
8acce4637a | ||
![]() |
addf60b3ee | ||
![]() |
20a1bc7d44 | ||
![]() |
8e74575c03 | ||
![]() |
be18fea136 | ||
![]() |
76ea3a063f | ||
![]() |
90c38db9f2 | ||
![]() |
b7d1e2c483 | ||
![]() |
8fce946850 | ||
![]() |
19a01d20dd | ||
![]() |
65fa2bf8c3 | ||
![]() |
12a4a5fb14 | ||
![]() |
83c3818504 | ||
![]() |
5de500d6da | ||
![]() |
5ff6bfba9c | ||
![]() |
06ddd01c70 | ||
![]() |
5aca11af70 | ||
![]() |
ecb32d74c6 | ||
![]() |
f280505eaa | ||
![]() |
812b87ab48 | ||
![]() |
0f5560b62a | ||
![]() |
6c15da4ece | ||
![]() |
ebd709a4fe | ||
![]() |
2820f9b986 | ||
![]() |
9f911318f3 | ||
![]() |
1d7d377f8b | ||
![]() |
a0b264047c | ||
![]() |
987f119c4b | ||
![]() |
b6be18ca65 | ||
![]() |
7e871d2278 | ||
![]() |
8f130196f8 | ||
![]() |
628af6e2d0 | ||
![]() |
8024693f4f | ||
![]() |
e927717fa0 | ||
![]() |
3bf95e1a83 | ||
![]() |
e37d09e5b4 | ||
![]() |
3fb3decf49 | ||
![]() |
b0f370bae2 | ||
![]() |
6193047c35 | ||
![]() |
02be5f3618 | ||
![]() |
47cc60bda9 | ||
![]() |
ce60c7b056 | ||
![]() |
d369656b26 | ||
![]() |
e5833699c0 | ||
![]() |
83ed8a61aa | ||
![]() |
4bffc0df21 | ||
![]() |
1e4441b6ae | ||
![]() |
7bb74b9664 | ||
![]() |
4f29ce2ee7 | ||
![]() |
0c35d9d43c | ||
![]() |
4f25738d6b | ||
![]() |
47dbfa770d | ||
![]() |
91b0f8fee1 | ||
![]() |
2e91a82aa7 | ||
![]() |
f25fdecc3f | ||
![]() |
b603bdfccc | ||
![]() |
3d8c891699 | ||
![]() |
ecb5562b57 | ||
![]() |
2142f7bb5c | ||
![]() |
51800132cd | ||
![]() |
73663ff9e7 | ||
![]() |
942aed1219 | ||
![]() |
157589d31e | ||
![]() |
ba4396e52c | ||
![]() |
6fb962a941 | ||
![]() |
cd4dabde0e | ||
![]() |
c4deaf0994 | ||
![]() |
ca12432a2a | ||
![]() |
d986ae0ee5 | ||
![]() |
943bb58086 | ||
![]() |
c49c1cbf2a | ||
![]() |
7284c0a47a | ||
![]() |
a84e4b6b15 | ||
![]() |
822e441d3a | ||
![]() |
185f9ad541 | ||
![]() |
dfc4126384 | ||
![]() |
033082a31e | ||
![]() |
2d81e751a1 | ||
![]() |
899f3e7eb8 | ||
![]() |
fd1c38811e | ||
![]() |
59f6610721 | ||
![]() |
72c1753fb7 | ||
![]() |
401739b036 | ||
![]() |
e4463c412b | ||
![]() |
38b37db55b | ||
![]() |
6efc2688b1 | ||
![]() |
c022eb1b86 | ||
![]() |
ef3a130d54 | ||
![]() |
7d6523db29 | ||
![]() |
5d2c99bb17 | ||
![]() |
6b71da6b78 | ||
![]() |
ff88faf402 | ||
![]() |
a32aa96752 | ||
![]() |
f68bc113a7 | ||
![]() |
579cecde04 | ||
![]() |
fe23da6e0c | ||
![]() |
e4ff26d613 | ||
![]() |
409721414b | ||
![]() |
6c19beb937 | ||
![]() |
11965f08db | ||
![]() |
10ee07cea0 | ||
![]() |
cc228f1868 | ||
![]() |
fdda940ac0 | ||
![]() |
9131d9d568 | ||
![]() |
311cda31fe | ||
![]() |
3d72ca731a | ||
![]() |
fd3e668fe1 | ||
![]() |
fa0e590778 | ||
![]() |
7c3dbffcc6 | ||
![]() |
4a6a9c4355 | ||
![]() |
f2528f3e29 | ||
![]() |
d15014f82e | ||
![]() |
60f1228030 | ||
![]() |
104f5d1fe6 | ||
![]() |
66543493b5 | ||
![]() |
aa974d26c6 | ||
![]() |
fde9640364 | ||
![]() |
c5079ac15e | ||
![]() |
2067ab0427 | ||
![]() |
5bdd3bbfcb | ||
![]() |
3288fad341 | ||
![]() |
8b82939d33 | ||
![]() |
4ac01ed880 | ||
![]() |
99513f64fd | ||
![]() |
3beb84bcfe | ||
![]() |
b0889d7751 | ||
![]() |
502a3cf841 | ||
![]() |
523343b174 | ||
![]() |
3b4da7e637 | ||
![]() |
895691dad1 | ||
![]() |
59fc403e32 | ||
![]() |
f860a037b5 | ||
![]() |
8aca00326d | ||
![]() |
668627f890 | ||
![]() |
344b1dc559 | ||
![]() |
df88f4e1e9 | ||
![]() |
67e464281f | ||
![]() |
23ffa1e04f | ||
![]() |
02d2eab18c | ||
![]() |
22479a289d | ||
![]() |
2088bb1f91 | ||
![]() |
b7c4bfd4e3 | ||
![]() |
e545933923 | ||
![]() |
7b4f300eb2 | ||
![]() |
ac6e0c1b89 | ||
![]() |
c1334b9a8b | ||
![]() |
03c9216026 | ||
![]() |
bb2f0e938f | ||
![]() |
ae6bf39495 | ||
![]() |
24b540ecde | ||
![]() |
487bf4e74a | ||
![]() |
7866729d3b | ||
![]() |
6b0097a24b | ||
![]() |
04a8fb7f81 | ||
![]() |
33383faf9e | ||
![]() |
2b7e3ff1e7 | ||
![]() |
0ecb6dcd4d | ||
![]() |
ec0d2a5ed2 | ||
![]() |
a96b3e077d | ||
![]() |
2b7f6b2b84 | ||
![]() |
8fecc2c00b | ||
![]() |
708fa8280a | ||
![]() |
7144dca68a | ||
![]() |
7359586f1c | ||
![]() |
4b3c9c2806 | ||
![]() |
7674f907c4 | ||
![]() |
f78270188f | ||
![]() |
1d9f861f28 | ||
![]() |
daae241ff9 | ||
![]() |
a50a3362bd | ||
![]() |
ec8a1ecec1 | ||
![]() |
74659a82ab | ||
![]() |
ddd75eae9a | ||
![]() |
b95a67751e | ||
![]() |
83841d801c | ||
![]() |
65c0b9ebcf | ||
![]() |
3ba67bad3d | ||
![]() |
9b3be5c2e8 | ||
![]() |
b203b3f444 | ||
![]() |
af30ba0e3b | ||
![]() |
c920a301e0 | ||
![]() |
44525e235a | ||
![]() |
6120571421 | ||
![]() |
edced6818a | ||
![]() |
6798dd7ba5 | ||
![]() |
bfbe180101 | ||
![]() |
52447f6999 | ||
![]() |
568eb1d4e0 | ||
![]() |
d4a7288826 | ||
![]() |
21e5b0d6d0 | ||
![]() |
0708073a0c | ||
![]() |
1ba6c67ff2 | ||
![]() |
1f06f242cc | ||
![]() |
9b3ff82542 | ||
![]() |
87239c62c1 | ||
![]() |
03b5184837 | ||
![]() |
52fbda1a5e | ||
![]() |
110272484d | ||
![]() |
7d97729eea | ||
![]() |
b06167a3fa | ||
![]() |
f3317f78d5 | ||
![]() |
b2130b1593 | ||
![]() |
9d199fd4a9 | ||
![]() |
414282a2c9 | ||
![]() |
faf3670e7f | ||
![]() |
e674537d0b | ||
![]() |
c4652d60a7 | ||
![]() |
ea40ffd655 | ||
![]() |
7d0f89df29 | ||
![]() |
21255b6391 | ||
![]() |
64e0832b85 | ||
![]() |
bacea50485 | ||
![]() |
1f5224b74b | ||
![]() |
bd2757c63d | ||
![]() |
e46ca38cbb | ||
![]() |
27194a9f9c | ||
![]() |
1aac5d78d9 | ||
![]() |
6b18674960 | ||
![]() |
eea07b7a1a | ||
![]() |
15a9e16530 | ||
![]() |
bd3722f075 | ||
![]() |
fe5f9bfc28 | ||
![]() |
dacf6ebc64 | ||
![]() |
c742242094 | ||
![]() |
1002affc16 | ||
![]() |
16b1ab06a9 | ||
![]() |
866c200c31 | ||
![]() |
1fc29d094f | ||
![]() |
39f57f1487 | ||
![]() |
804b6f4c5d | ||
![]() |
e13867f7c9 | ||
![]() |
e1954adc32 | ||
![]() |
0eea20fa7c | ||
![]() |
3adb90e7b7 | ||
![]() |
496dacb7ff | ||
![]() |
865ff5c88d | ||
![]() |
85e5a7e8ed | ||
![]() |
24b1a99c42 | ||
![]() |
8decbe7670 | ||
![]() |
5c8e2a8510 | ||
![]() |
bd91ddaf52 | ||
![]() |
7b8cd63b04 | ||
![]() |
fa35e8c0ba | ||
![]() |
9fac14488a | ||
![]() |
39da36361c | ||
![]() |
73f336363a | ||
![]() |
37ada58f3a | ||
![]() |
607b7d4b6f | ||
![]() |
920f3d2a7d | ||
![]() |
5c1c941851 | ||
![]() |
2c0c7225f5 | ||
![]() |
0a372d83b9 | ||
![]() |
5e80bf3043 | ||
![]() |
9739de61b0 | ||
![]() |
134ac2e68c | ||
![]() |
e05515f79d | ||
![]() |
e868adee2f | ||
![]() |
5f62d738fc | ||
![]() |
492e0dfeb1 | ||
![]() |
b1cbf2e2e5 | ||
![]() |
14dd6b9026 | ||
![]() |
69dd8d2892 | ||
![]() |
ca4cd6d559 | ||
![]() |
02e0f3c095 | ||
![]() |
863facaa33 | ||
![]() |
15902dcba6 | ||
![]() |
8e7e799304 | ||
![]() |
dbdc7529b3 | ||
![]() |
4be884824f | ||
![]() |
520e40db37 | ||
![]() |
46f20193f9 | ||
![]() |
33ff938504 | ||
![]() |
7fafb483ad | ||
![]() |
342e7f5272 | ||
![]() |
a4f4eabf0a | ||
![]() |
628d7be1d8 | ||
![]() |
3c6834fc18 | ||
![]() |
3d6f015211 | ||
![]() |
a6ed08b239 | ||
![]() |
0a39066f9d | ||
![]() |
a1d5a02646 | ||
![]() |
b91fcb8e9b | ||
![]() |
d71279f023 | ||
![]() |
c78c833400 | ||
![]() |
a2d91119d4 | ||
![]() |
10585bfecc | ||
![]() |
b572f64dc6 | ||
![]() |
ff72a3c1c7 | ||
![]() |
67841d54ee | ||
![]() |
9c1b78395a | ||
![]() |
581ddf78fc | ||
![]() |
567e0ab7d1 | ||
![]() |
1c0fe09576 | ||
![]() |
bdda8691ff | ||
![]() |
4b311684ab | ||
![]() |
b7f1c5455f | ||
![]() |
799cc82bb5 | ||
![]() |
df7c51f34e | ||
![]() |
88a4801d6a | ||
![]() |
e88e9946f9 | ||
![]() |
1fc9587919 | ||
![]() |
29573682ec | ||
![]() |
d199223be0 | ||
![]() |
8ea9e83798 | ||
![]() |
1a293a2a27 | ||
![]() |
f1cfcfe7cc | ||
![]() |
357899b83e | ||
![]() |
a7c7ea5712 | ||
![]() |
0483b9c641 | ||
![]() |
5009e9e483 | ||
![]() |
1e1741aa45 | ||
![]() |
fe09737d80 | ||
![]() |
4b843d145a | ||
![]() |
cdab206d05 | ||
![]() |
9c1c4093a3 | ||
![]() |
131ed1b0a7 | ||
![]() |
bf3ea71630 | ||
![]() |
e6a2a7386c | ||
![]() |
7c7fe70cb2 | ||
![]() |
18030e6c58 | ||
![]() |
220bbe5862 | ||
![]() |
6d6d82b3af | ||
![]() |
98f2ac5e7c | ||
![]() |
635e633520 | ||
![]() |
39f7e38444 | ||
![]() |
c2b298c93a | ||
![]() |
b8547da4c3 | ||
![]() |
fae1f96856 | ||
![]() |
afbdcd520b | ||
![]() |
fbcb2ed7fd | ||
![]() |
0449ec1868 | ||
![]() |
a49b023a28 | ||
![]() |
f1dbe8c9dd | ||
![]() |
5fcf47c79f | ||
![]() |
8f111680bf | ||
![]() |
64369b5c2b | ||
![]() |
392708a804 | ||
![]() |
ddfe95e45d | ||
![]() |
2dcce2ae72 | ||
![]() |
f22e4eb24e | ||
![]() |
2e37d5ce97 | ||
![]() |
be977d1cc4 | ||
![]() |
589a30cd5f | ||
![]() |
a645c928d4 | ||
![]() |
0f4ab71f01 | ||
![]() |
44b11c2e5b | ||
![]() |
1bd8cc79c8 | ||
![]() |
c17c651458 | ||
![]() |
8074a233e8 | ||
![]() |
8909fbdb22 | ||
![]() |
242706a475 | ||
![]() |
2169dc674f | ||
![]() |
4273a0f243 | ||
![]() |
6de175ad8a | ||
![]() |
ec4e193cbb | ||
![]() |
615895da9d | ||
![]() |
ef20183ecb | ||
![]() |
d7a269a6e4 | ||
![]() |
d9f111c4ca | ||
![]() |
e6a35e8714 | ||
![]() |
0f7f21a7db | ||
![]() |
4771d04a9d | ||
![]() |
2044a0677e | ||
![]() |
526a4dbd08 | ||
![]() |
8060f54f27 | ||
![]() |
d6d9bdd2ec | ||
![]() |
4b9a0c4ef7 | ||
![]() |
37756e8082 | ||
![]() |
4a3ec85686 | ||
![]() |
b3786961de | ||
![]() |
71165bcd30 | ||
![]() |
5f2797c83c | ||
![]() |
17ed27052c | ||
![]() |
0190614342 | ||
![]() |
099469c5d2 | ||
![]() |
a8089c8ddb | ||
![]() |
a0a5776e51 | ||
![]() |
bf4bc0c9fc | ||
![]() |
9cf2d5ab5c | ||
![]() |
1e63727064 | ||
![]() |
7e36432bf5 | ||
![]() |
33ef6eaea6 | ||
![]() |
ba7f4fcec0 | ||
![]() |
c54c9f2951 | ||
![]() |
058237c9dd | ||
![]() |
dad22a6aba | ||
![]() |
b202e387cf | ||
![]() |
cea793fc3a | ||
![]() |
ec5587a734 | ||
![]() |
b076f1d582 | ||
![]() |
16b9fd82f0 | ||
![]() |
d8dc01cd94 | ||
![]() |
3f24bcdbcf | ||
![]() |
2f18808d68 | ||
![]() |
7329a9d4b2 | ||
![]() |
aef62f74f8 | ||
![]() |
2c4edac28b | ||
![]() |
caf1dc71fb | ||
![]() |
7ff67c2311 | ||
![]() |
7baa5d623b | ||
![]() |
6491100ef1 | ||
![]() |
8df58df757 | ||
![]() |
42ff269bc8 | ||
![]() |
9d337bfb0e | ||
![]() |
0667552132 | ||
![]() |
411baa4dcf | ||
![]() |
b792c45921 | ||
![]() |
c2708ab6c0 | ||
![]() |
af8c55330d | ||
![]() |
c5566e92f3 | ||
![]() |
f7f4ca9541 | ||
![]() |
9a6a254a90 | ||
![]() |
e4cc5fc997 | ||
![]() |
023ac9e138 | ||
![]() |
b57eb92bbc | ||
![]() |
24797c1534 | ||
![]() |
42a1bc0260 | ||
![]() |
bb30a3f966 | ||
![]() |
a74a41dac5 | ||
![]() |
81793fe8bf | ||
![]() |
76e97303a5 | ||
![]() |
548b0b5518 | ||
![]() |
b09339ae91 | ||
![]() |
6d42a8c8be | ||
![]() |
b050d27e20 | ||
![]() |
0dd37240a5 | ||
![]() |
2cd3248431 | ||
![]() |
6f68f4056e | ||
![]() |
b6aca81ebd | ||
![]() |
8120fe7d5d | ||
![]() |
bec231c1e6 | ||
![]() |
e7de05d6c9 | ||
![]() |
aca37a38e7 | ||
![]() |
4e6b9597f8 | ||
![]() |
52132ce398 | ||
![]() |
0c35263c29 | ||
![]() |
c077668511 | ||
![]() |
7c0593c659 | ||
![]() |
59ad91a8ca | ||
![]() |
10ce45c054 | ||
![]() |
5a15f9b39b | ||
![]() |
19817083d1 | ||
![]() |
a77b3e2690 | ||
![]() |
e35efc5b2d | ||
![]() |
b66366c28c | ||
![]() |
c5dda0ffba | ||
![]() |
c3dbe0080c | ||
![]() |
a300d5818f | ||
![]() |
0f5e922851 | ||
![]() |
45c1075ae0 | ||
![]() |
1928d385b0 | ||
![]() |
9a42190e13 | ||
![]() |
2763366309 | ||
![]() |
75ba0e2bfc | ||
![]() |
fb74b2fda7 | ||
![]() |
e4f6cdfc14 | ||
![]() |
d18620858e | ||
![]() |
9bc7e6ffcf | ||
![]() |
afe1704aa6 | ||
![]() |
1d156ab19d | ||
![]() |
556892cf86 | ||
![]() |
43d5690432 | ||
![]() |
5817fa4147 | ||
![]() |
8b90f4b2b2 | ||
![]() |
5a0843852a | ||
![]() |
40ab8fa738 | ||
![]() |
ef4bf6a8ab | ||
![]() |
76e3612088 | ||
![]() |
5c7c12c62d | ||
![]() |
dd53d19777 | ||
![]() |
02765a74fa | ||
![]() |
8c878b0669 | ||
![]() |
ead0a06f0c | ||
![]() |
052ec6e632 | ||
![]() |
4fc7335112 | ||
![]() |
ad28a979e9 | ||
![]() |
d5f17ee377 | ||
![]() |
6d1adb1784 | ||
![]() |
d2bd01d009 | ||
![]() |
35eea39db7 | ||
![]() |
9c9639d19b | ||
![]() |
383eebf2b6 | ||
![]() |
5f2d7f6a65 | ||
![]() |
840939d489 | ||
![]() |
01ee09c547 | ||
![]() |
715c32ef9c | ||
![]() |
dbde90d24c | ||
![]() |
0814a7b19d | ||
![]() |
8310c10ce3 | ||
![]() |
240e5ad3ab | ||
![]() |
894388ec6c | ||
![]() |
19323ba4aa | ||
![]() |
363abafe4b | ||
![]() |
64a672216d | ||
![]() |
9b3db6b6bb | ||
![]() |
b93b8d9a2e | ||
![]() |
3b04bd3b5b | ||
![]() |
59a537514f | ||
![]() |
cc1ab8c50d | ||
![]() |
b1a7ffb92f | ||
![]() |
c81fc87d4e | ||
![]() |
a88848009a | ||
![]() |
a17f150e5d | ||
![]() |
9d133ea2b5 | ||
![]() |
40ed810c0b | ||
![]() |
6e92da76ad | ||
![]() |
50fb1e3df1 | ||
![]() |
7b798c1f6d | ||
![]() |
cc72800f50 | ||
![]() |
3bca25fd6d | ||
![]() |
4a11060930 | ||
![]() |
c109e0e7dd | ||
![]() |
a6e405422c | ||
![]() |
6c676c4869 | ||
![]() |
ba71c55492 | ||
![]() |
8e3004ebb3 | ||
![]() |
6c4ec64ca9 | ||
![]() |
cf175ab07e | ||
![]() |
f46f0ae553 | ||
![]() |
f86ec1c389 | ||
![]() |
ed38ca3a73 | ||
![]() |
9c8ca5c73a | ||
![]() |
ced64cd2f5 | ||
![]() |
efa06870c0 | ||
![]() |
970f374b31 | ||
![]() |
3105897f37 | ||
![]() |
7126f8f0ff | ||
![]() |
25a8ab6163 | ||
![]() |
6b13d73fca | ||
![]() |
64d8c0357b | ||
![]() |
f86e0c0a5a | ||
![]() |
6b3e22e99a | ||
![]() |
755f2103be | ||
![]() |
6a3adcff0e | ||
![]() |
002268b32f | ||
![]() |
106254f020 | ||
![]() |
d913ac160e | ||
![]() |
ec53fbfdab | ||
![]() |
51824d3bb8 | ||
![]() |
e6dca618a6 | ||
![]() |
88d06260d7 | ||
![]() |
6e272d3f88 | ||
![]() |
ab93cec736 | ||
![]() |
01130a5e17 | ||
![]() |
4c306187a6 | ||
![]() |
458dcd1979 | ||
![]() |
121dcd79ba | ||
![]() |
44d701a9dd | ||
![]() |
3c2462fbc5 | ||
![]() |
e5204bc1b1 | ||
![]() |
fa1ee2a856 | ||
![]() |
c8ec37ea4e | ||
![]() |
9c133ee3d0 | ||
![]() |
2e146ea3fc | ||
![]() |
c7fb6a1428 | ||
![]() |
ca67be8fca | ||
![]() |
4e6f6fe3a2 | ||
![]() |
7dee103b6e | ||
![]() |
2a1a96c1a7 | ||
![]() |
4917aa23c9 | ||
![]() |
f7ad11e18e | ||
![]() |
e6c3100d69 | ||
![]() |
96df5d6410 | ||
![]() |
dd03d07355 | ||
![]() |
0652e9ed08 | ||
![]() |
3c768490ba | ||
![]() |
37c2a10e21 | ||
![]() |
0ffbbbec61 | ||
![]() |
7362828a3b | ||
![]() |
a4998913d8 | ||
![]() |
8b0527bf9d | ||
![]() |
7ca3625706 | ||
![]() |
fb54388e93 | ||
![]() |
d1ce543440 | ||
![]() |
cbada3d435 | ||
![]() |
4bf996a716 | ||
![]() |
f10187bd6d | ||
![]() |
4708b509e6 | ||
![]() |
9034bb7919 | ||
![]() |
2b2a11fec7 | ||
![]() |
02886140a7 | ||
![]() |
cf57c89177 | ||
![]() |
0d46890d6b | ||
![]() |
25a2d528b0 | ||
![]() |
76a80380e7 | ||
![]() |
3f99330b3d | ||
![]() |
66919924d3 | ||
![]() |
a7b1c02bd5 | ||
![]() |
0d075f32cd | ||
![]() |
b6bce96350 | ||
![]() |
f8e52f1a91 | ||
![]() |
c41234e6e1 | ||
![]() |
0f4b2881b6 | ||
![]() |
b576fd7c66 | ||
![]() |
4d4d6d802c | ||
![]() |
3bb2128bf4 | ||
![]() |
1bd6b8f7b9 | ||
![]() |
2961a0ed02 | ||
![]() |
203be9bfbf | ||
![]() |
2bc9f33e12 | ||
![]() |
6bd80dbb86 | ||
![]() |
cc653ce0d7 | ||
![]() |
3b4c773890 | ||
![]() |
0ccc69696b | ||
![]() |
079172568a | ||
![]() |
5c7f873e18 | ||
![]() |
1e200d99d8 | ||
![]() |
285351f4f9 | ||
![]() |
37eedce72e | ||
![]() |
de87d0d951 | ||
![]() |
af4bb350c0 | ||
![]() |
c2fd173d1e | ||
![]() |
fb308d5596 | ||
![]() |
db5ffb0040 | ||
![]() |
9d2cd58f31 | ||
![]() |
f0db3742de | ||
![]() |
bd5c119f85 | ||
![]() |
e92862213e | ||
![]() |
990599a0b5 | ||
![]() |
d10b5e2aa2 | ||
![]() |
0ecfe75687 | ||
![]() |
f5e54083c7 | ||
![]() |
6b5f272c0a | ||
![]() |
ff400c9bca | ||
![]() |
e929ba16de | ||
![]() |
88846ac115 | ||
![]() |
cca76da2d6 | ||
![]() |
dd41c30fba | ||
![]() |
e651379964 | ||
![]() |
e67fad06d3 | ||
![]() |
652b75ee68 | ||
![]() |
e6afafa4d8 | ||
![]() |
21dfc784b1 | ||
![]() |
055a0f08cb | ||
![]() |
5ae69aa293 | ||
![]() |
9b5cb6abc9 | ||
![]() |
9b680ae455 | ||
![]() |
90e354650b | ||
![]() |
7a1b955ad1 | ||
![]() |
e89543f725 | ||
![]() |
078c994159 | ||
![]() |
21dd4851f1 | ||
![]() |
b8e24a1e0b | ||
![]() |
1d955f4258 | ||
![]() |
c0f14f34be | ||
![]() |
a42d99529b | ||
![]() |
2f937cb53a | ||
![]() |
899b2abae7 | ||
![]() |
0ad6165ed2 | ||
![]() |
d7d591ff84 | ||
![]() |
bed90a832e | ||
![]() |
ea640dfb6d | ||
![]() |
589cb72d41 | ||
![]() |
4aca9941cb | ||
![]() |
e54b5beb8d | ||
![]() |
b40fd36607 | ||
![]() |
68c11dd827 | ||
![]() |
b5d0d56a11 | ||
![]() |
ff0fb50032 | ||
![]() |
2018f9c800 | ||
![]() |
c5d7509782 | ||
![]() |
5af620469a | ||
![]() |
f1f77762b3 | ||
![]() |
448a227079 | ||
![]() |
488f28e3a3 | ||
![]() |
c188d401a3 | ||
![]() |
950759f6d6 | ||
![]() |
4dbbd4b3c4 | ||
![]() |
f64f20fd53 | ||
![]() |
1a605f33da | ||
![]() |
b0746fbc4d | ||
![]() |
05ac3ca402 | ||
![]() |
25ed1c265d | ||
![]() |
53f9837e6a | ||
![]() |
2a6369658a | ||
![]() |
fc7369c4ea | ||
![]() |
01028d0a09 | ||
![]() |
cedfca07c2 | ||
![]() |
34a4371dde | ||
![]() |
535f0353df | ||
![]() |
657b765021 | ||
![]() |
c56581903c | ||
![]() |
d657a68299 | ||
![]() |
6a08e4ed7f | ||
![]() |
73e0bbaf93 | ||
![]() |
c11a297b19 | ||
![]() |
204fa88e4d | ||
![]() |
e55e177baf | ||
![]() |
3ecbb5a662 | ||
![]() |
d8b8795619 | ||
![]() |
b6ee006078 | ||
![]() |
ca3e9ea487 | ||
![]() |
033c8e17e8 | ||
![]() |
1b39893fcf | ||
![]() |
1d21679b3f | ||
![]() |
556f93827a | ||
![]() |
bb37e2e70d | ||
![]() |
42bc31cf23 | ||
![]() |
3994aec7fe | ||
![]() |
b29730520f | ||
![]() |
7f0cac8ee6 | ||
![]() |
c026fc7f16 | ||
![]() |
ce6a61df1c | ||
![]() |
71c1d9431f | ||
![]() |
7e81bdddef | ||
![]() |
3705c0be50 | ||
![]() |
f25d02a7c8 | ||
![]() |
68080e5d24 | ||
![]() |
4bb503ceae | ||
![]() |
2aee7be628 | ||
![]() |
bf6b791420 | ||
![]() |
c1c76645a7 | ||
![]() |
b5db93ee74 | ||
![]() |
b42c42379a | ||
![]() |
3cb770c3bb | ||
![]() |
8a42601d3d | ||
![]() |
6693bff2f5 | ||
![]() |
eb0bd70046 | ||
![]() |
f881510f79 | ||
![]() |
98277f5bb7 | ||
![]() |
4433e3bf87 | ||
![]() |
8db7fc1f58 | ||
![]() |
1c7faabd8b | ||
![]() |
1d13fe754b | ||
![]() |
fd830b4293 | ||
![]() |
ab4878d4e6 | ||
![]() |
75f0adebb7 | ||
![]() |
a19f13ab45 | ||
![]() |
e82ad5704d | ||
![]() |
9102d37f27 | ||
![]() |
60740f6279 | ||
![]() |
b0d4744b15 | ||
![]() |
5e2412cc5d | ||
![]() |
430b4bafbc | ||
![]() |
7e9ce901a4 | ||
![]() |
88e1095478 | ||
![]() |
446fd499c8 | ||
![]() |
a47dab73e3 | ||
![]() |
2679867061 | ||
![]() |
c10f798545 | ||
![]() |
d86ba98cff | ||
![]() |
4bb34d8e77 | ||
![]() |
01c557e209 | ||
![]() |
c3a2022941 | ||
![]() |
cd06929e75 | ||
![]() |
10c98b6547 | ||
![]() |
e45cb167bd | ||
![]() |
8df8ed9e3e | ||
![]() |
59a9a6b6bf | ||
![]() |
20ef67a699 | ||
![]() |
20ccb32124 | ||
![]() |
5ad7e9e729 | ||
![]() |
b007b8e932 | ||
![]() |
f2a0155d90 | ||
![]() |
73a10ef0e5 | ||
![]() |
1e899c2211 | ||
![]() |
bf192593b6 | ||
![]() |
29323494c9 | ||
![]() |
6d28599efa | ||
![]() |
1f2f6c87d5 | ||
![]() |
4166d9ff48 | ||
![]() |
e59c33a8e2 | ||
![]() |
bdca1d79c1 | ||
![]() |
1938e1a62d | ||
![]() |
190fa112dd | ||
![]() |
f1b0414c89 | ||
![]() |
7f8faa7565 | ||
![]() |
f1d23b51f6 | ||
![]() |
28446b6d29 | ||
![]() |
9d1e2f3795 | ||
![]() |
ecad34091e | ||
![]() |
111e17e884 | ||
![]() |
c1abf69979 | ||
![]() |
17ad9de738 | ||
![]() |
d09cc0eeb3 | ||
![]() |
ccec56c1a6 | ||
![]() |
f2d14c8ca2 | ||
![]() |
0981aa98d8 | ||
![]() |
534c535490 | ||
![]() |
7863bad596 | ||
![]() |
09a63ab868 | ||
![]() |
c2d4fb037a | ||
![]() |
92f290ebe0 | ||
![]() |
a8b16a66b1 | ||
![]() |
a627887841 | ||
![]() |
70055b3fd6 | ||
![]() |
6f8b6cdb42 | ||
![]() |
5b7ac4a473 | ||
![]() |
8b504e9f67 | ||
![]() |
a3d00fe130 | ||
![]() |
da84805f5f | ||
![]() |
4565d82f79 | ||
![]() |
22ada59393 | ||
![]() |
7d93302e05 | ||
![]() |
7f40160f6e | ||
![]() |
2888bda959 | ||
![]() |
18ff3a3a30 | ||
![]() |
dae4458a6f | ||
![]() |
260332c726 | ||
![]() |
9f515cb7ef | ||
![]() |
22c4962768 | ||
![]() |
e8709074f0 | ||
![]() |
50ee846e87 | ||
![]() |
359a9cb8ce | ||
![]() |
564c4155a8 | ||
![]() |
cb003ea347 | ||
![]() |
e74f221044 | ||
![]() |
bb25a261ad | ||
![]() |
c5bd603cce | ||
![]() |
36844e50b3 | ||
![]() |
862105669f | ||
![]() |
0530aa6886 | ||
![]() |
6724c2aca4 | ||
![]() |
13172fc490 | ||
![]() |
25562e9575 | ||
![]() |
81e7db71ed | ||
![]() |
8bd53a89c0 | ||
![]() |
82ec6b3d80 | ||
![]() |
986a0be812 | ||
![]() |
0c5c2a0ac2 | ||
![]() |
5544000d38 | ||
![]() |
9ec0ea08bb | ||
![]() |
fbfd5de096 | ||
![]() |
1778ee840e | ||
![]() |
b15fc96ef8 | ||
![]() |
eaeaa5ccd1 | ||
![]() |
c2517e8eb4 | ||
![]() |
eda9c03c82 | ||
![]() |
f25acab313 | ||
![]() |
66f943446a | ||
![]() |
a63b6729bf | ||
![]() |
9a1babc365 | ||
![]() |
8a87e76fe5 | ||
![]() |
bd7bcff2fd | ||
![]() |
f00f7778bc | ||
![]() |
2d990c4e7f | ||
![]() |
23ba741f2e | ||
![]() |
825f8f83b2 | ||
![]() |
b79c897d99 | ||
![]() |
ea86a4f4ec | ||
![]() |
ad4521f2cc | ||
![]() |
1c005d6923 | ||
![]() |
13881edbaa | ||
![]() |
f2b30db684 | ||
![]() |
bb679310c7 | ||
![]() |
79080d4e36 | ||
![]() |
fd8c23eb09 | ||
![]() |
b6d73f48cd | ||
![]() |
23a8707fad | ||
![]() |
0ed92b20a0 | ||
![]() |
674630f7b1 | ||
![]() |
da5ca5f0ba | ||
![]() |
6f4c72776a | ||
![]() |
f8054e1f32 | ||
![]() |
bd69c8042a | ||
![]() |
89f1c92d7e | ||
![]() |
6dcc9e7810 | ||
![]() |
cbbe210736 | ||
![]() |
a9353e3016 | ||
![]() |
339cb6cce7 | ||
![]() |
759bd2134c | ||
![]() |
28c1ea1e23 | ||
![]() |
7831dabaa8 | ||
![]() |
f411ab4fcd | ||
![]() |
d057d811b2 | ||
![]() |
fd744408c3 | ||
![]() |
9d016f262f | ||
![]() |
2e76097d35 | ||
![]() |
8707140fb2 | ||
![]() |
b7190c9ecc | ||
![]() |
e5487aacdb | ||
![]() |
71325d9134 | ||
![]() |
a16fb1475d | ||
![]() |
0be78d4cbf | ||
![]() |
c6eb2afa20 | ||
![]() |
fa11a94e21 | ||
![]() |
b2b60072ea | ||
![]() |
230876b281 | ||
![]() |
4f49fd1d7a | ||
![]() |
71ac0d2fce | ||
![]() |
000fec27df | ||
![]() |
a09b86876f | ||
![]() |
dbb420f79e | ||
![]() |
5f339ab312 | ||
![]() |
fba50dee1e | ||
![]() |
dccd6ebb40 | ||
![]() |
c9cf4d3b3b | ||
![]() |
a6f3d49e46 | ||
![]() |
6bff2d0325 | ||
![]() |
a042ef3097 | ||
![]() |
52b3431595 | ||
![]() |
a9f2b5c150 | ||
![]() |
c38b086349 | ||
![]() |
62cbf99dbb | ||
![]() |
2fdf7624da | ||
![]() |
f8917cc014 | ||
![]() |
96ea924064 | ||
![]() |
748d30e889 | ||
![]() |
1be552de6c | ||
![]() |
6f713ba9b9 | ||
![]() |
1533e7e83c | ||
![]() |
dcaa6afadf | ||
![]() |
92c5b8c263 | ||
![]() |
07d386cd9c | ||
![]() |
fb1884a4ca | ||
![]() |
bae76faf25 | ||
![]() |
f0ea8312db | ||
![]() |
9b97633043 | ||
![]() |
a8e6f9d1e5 | ||
![]() |
c6aab9893a | ||
![]() |
62bbeaa2b5 | ||
![]() |
dc31dbf226 | ||
![]() |
687077010c | ||
![]() |
7a2742198c | ||
![]() |
5d3454e22a | ||
![]() |
160b5f8486 | ||
![]() |
b270bfbcfe | ||
![]() |
d431fe9f21 | ||
![]() |
57aa1c6b24 | ||
![]() |
b7286d6a85 | ||
![]() |
b81c735d81 | ||
![]() |
7f45c43eb1 | ||
![]() |
2db6882202 | ||
![]() |
b2ef6a555c | ||
![]() |
6624fce66a | ||
![]() |
a6fabcf481 | ||
![]() |
3801dcc277 | ||
![]() |
25dfcebf4c | ||
![]() |
5595070e67 | ||
![]() |
1956f52be5 | ||
![]() |
742435f178 | ||
![]() |
1398a74c6d | ||
![]() |
9df29191f7 | ||
![]() |
ceb69f0cef | ||
![]() |
fe8040683e | ||
![]() |
a3b638890d | ||
![]() |
747065229e | ||
![]() |
7525f11975 | ||
![]() |
fbebee01d3 | ||
![]() |
911d2d6d5c | ||
![]() |
8128dcf61b | ||
![]() |
b10c7e9bef | ||
![]() |
d8f0f5a3a9 | ||
![]() |
085dcc5eb6 | ||
![]() |
eef59cad9e | ||
![]() |
aedef28c18 | ||
![]() |
b7096be6e6 | ||
![]() |
895f336900 | ||
![]() |
8dc8d2c81d | ||
![]() |
0356f8404b | ||
![]() |
daf2a350ea | ||
![]() |
d23512e9c6 | ||
![]() |
f8abb01bbc | ||
![]() |
b19046939c | ||
![]() |
0f2733418a | ||
![]() |
50d7c4ce96 | ||
![]() |
2da55f411b | ||
![]() |
d2ffb190f9 | ||
![]() |
6f623f9a96 | ||
![]() |
5e561e30bd | ||
![]() |
89c8d1a527 | ||
![]() |
acc0960c17 | ||
![]() |
773d0f4d84 | ||
![]() |
b9ba2ffb55 | ||
![]() |
7ba1ab4a66 | ||
![]() |
a2b6b31a26 | ||
![]() |
8718cc9aac | ||
![]() |
794d3221d0 | ||
![]() |
e6cb7f3a79 | ||
![]() |
55d5b6842c | ||
![]() |
00ee2d0fdc | ||
![]() |
056c7801c6 | ||
![]() |
151af2d0d8 | ||
![]() |
851ad300cb | ||
![]() |
6b4674104c | ||
![]() |
a104e6d053 | ||
![]() |
40b7bfaf69 | ||
![]() |
66161bc8ae | ||
![]() |
0864f13cb8 | ||
![]() |
177480cff7 | ||
![]() |
7fe6741df3 | ||
![]() |
87a90583fe | ||
![]() |
bc2566f3e5 | ||
![]() |
4ef080e7bd | ||
![]() |
18f5a1dfdd | ||
![]() |
44304a30e7 | ||
![]() |
a099a164e1 | ||
![]() |
a0c1ca49d0 | ||
![]() |
c1c1a33dd3 | ||
![]() |
680c5c14ac | ||
![]() |
98297f741f | ||
![]() |
d0ac43b00f | ||
![]() |
a62bac0ca0 | ||
![]() |
3dd42bc9fd | ||
![]() |
7691e5b663 | ||
![]() |
672785ba17 | ||
![]() |
eef1847873 | ||
![]() |
f6826c7e47 | ||
![]() |
454e2850b5 | ||
![]() |
b9ae94b874 | ||
![]() |
81ef26f406 | ||
![]() |
cb9eda429e | ||
![]() |
e4993996a5 | ||
![]() |
02b2193d64 | ||
![]() |
44076dd3d5 | ||
![]() |
652b54ee81 | ||
![]() |
a4923a362f | ||
![]() |
29df9704a2 | ||
![]() |
2e30793188 | ||
![]() |
68c749bc9c | ||
![]() |
bdda08223e | ||
![]() |
02351e925b | ||
![]() |
c63d1153a3 | ||
![]() |
9545ba87e6 | ||
![]() |
e93a8b0c39 | ||
![]() |
be0e1cd79f | ||
![]() |
5446857377 | ||
![]() |
bb60d29ac8 | ||
![]() |
d6987ae8f1 | ||
![]() |
8651a1aefc | ||
![]() |
26f77bed88 | ||
![]() |
e3525f970b | ||
![]() |
64c5fa7360 | ||
![]() |
c37f020da3 | ||
![]() |
051a1b427a | ||
![]() |
293d098fa9 | ||
![]() |
6bee95b368 | ||
![]() |
fc05a49cc3 | ||
![]() |
f9a4ae2b3f | ||
![]() |
a58df696cf | ||
![]() |
08c0167f15 | ||
![]() |
8185aa248a | ||
![]() |
ea3b9d9de0 | ||
![]() |
72d1fe4c3b | ||
![]() |
430b7cd90d | ||
![]() |
f2e38b0d28 | ||
![]() |
82d6e6938a | ||
![]() |
4e8aa19c09 | ||
![]() |
ea4e4153af | ||
![]() |
9911803e3a | ||
![]() |
937bd20c18 | ||
![]() |
083b9897d8 | ||
![]() |
04ff874eb5 | ||
![]() |
19d6a979c8 | ||
![]() |
cda7beddbf | ||
![]() |
b51d666dcb | ||
![]() |
7563975eef | ||
![]() |
80387fe66e | ||
![]() |
b9f09f2ad8 | ||
![]() |
edcc822d79 | ||
![]() |
87ff0883cf | ||
![]() |
372d8680c3 | ||
![]() |
a06f8373ae | ||
![]() |
3fa5122db7 | ||
![]() |
8d43abbf4c | ||
![]() |
ed919a55be | ||
![]() |
fd6ecd25df | ||
![]() |
7657a0cc37 | ||
![]() |
a199cd8b36 | ||
![]() |
7557ce8156 | ||
![]() |
6cb5ec0460 | ||
![]() |
160478b419 | ||
![]() |
1abdfc9b10 | ||
![]() |
c03235ed64 | ||
![]() |
bdc1649560 | ||
![]() |
ec81bc1f9a | ||
![]() |
8e2c9bc705 | ||
![]() |
12c877b41b | ||
![]() |
fa12c29598 | ||
![]() |
2e0b0597c5 | ||
![]() |
9f946ca4a4 | ||
![]() |
72508fa2ef | ||
![]() |
6d7a1b4e74 | ||
![]() |
c020ff8c64 | ||
![]() |
d9a37683e7 | ||
![]() |
060a595244 | ||
![]() |
ed24638200 | ||
![]() |
aad2e1421e | ||
![]() |
34a6156097 | ||
![]() |
1b54bbb909 | ||
![]() |
e87f3f1f0b | ||
![]() |
3dfc9de409 | ||
![]() |
1f91bd8af0 | ||
![]() |
087549e2ba | ||
![]() |
38a750d3df | ||
![]() |
6757fa3cee | ||
![]() |
1970a90813 | ||
![]() |
88c72340e3 | ||
![]() |
aa9badc70c | ||
![]() |
d601e21afb | ||
![]() |
176d5197f6 | ||
![]() |
ae191f3426 | ||
![]() |
ccb4b65db9 | ||
![]() |
74dd723ebf | ||
![]() |
68ccbd1f69 | ||
![]() |
f8821b8982 | ||
![]() |
32f32b41c7 | ||
![]() |
4bb1e69b40 | ||
![]() |
ec7aaac9d0 | ||
![]() |
019c8d3e18 | ||
![]() |
62daa98bf3 | ||
![]() |
2611f7fa23 |
1310 changed files with 154802 additions and 21456 deletions
6
.dockerignore
Normal file
6
.dockerignore
Normal file
|
@ -0,0 +1,6 @@
|
|||
# We include .git in the build context because excluding it would break the
|
||||
# "make release" target, which uses git to retrieve the build version and tag.
|
||||
#.git
|
||||
|
||||
/tests
|
||||
/crowdsec-v*
|
136
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
136
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal file
|
@ -0,0 +1,136 @@
|
|||
name: Bug report
|
||||
description: Report a bug encountered while operating crowdsec
|
||||
labels: kind/bug
|
||||
body:
|
||||
- type: textarea
|
||||
id: problem
|
||||
attributes:
|
||||
label: What happened?
|
||||
description: |
|
||||
Please provide as much info as possible. Not doing so may result in your bug not being addressed in a timely manner.
|
||||
If this matter is security related, please disclose it privately to security@crowdsec.net
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: What did you expect to happen?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: repro
|
||||
attributes:
|
||||
label: How can we reproduce it (as minimally and precisely as possible)?
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
label: Anything else we need to know?
|
||||
|
||||
- type: textarea
|
||||
id: Version
|
||||
attributes:
|
||||
label: Crowdsec version
|
||||
value: |
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ cscli version
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: osVersion
|
||||
attributes:
|
||||
label: OS version
|
||||
value: |
|
||||
<details>
|
||||
|
||||
```console
|
||||
# On Linux:
|
||||
$ cat /etc/os-release
|
||||
# paste output here
|
||||
$ uname -a
|
||||
# paste output here
|
||||
|
||||
# On Windows:
|
||||
C:\> wmic os get Caption, Version, BuildNumber, OSArchitecture
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- type: textarea
|
||||
id: collections
|
||||
attributes:
|
||||
label: Enabled collections and parsers
|
||||
value: |
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ cscli hub list -o raw
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- type: textarea
|
||||
id: acquis
|
||||
attributes:
|
||||
label: Acquisition config
|
||||
value: |
|
||||
<details>
|
||||
```console
|
||||
# On Linux:
|
||||
$ cat /etc/crowdsec/acquis.yaml /etc/crowdsec/acquis.d/*
|
||||
# paste output here
|
||||
|
||||
# On Windows:
|
||||
C:\> Get-Content C:\ProgramData\CrowdSec\config\acquis.yaml
|
||||
# paste output here
|
||||
</details>
|
||||
|
||||
- type: textarea
|
||||
id: config
|
||||
attributes:
|
||||
label: Config show
|
||||
value: |
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ cscli config show
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- type: textarea
|
||||
id: metrics
|
||||
attributes:
|
||||
label: Prometheus metrics
|
||||
value: |
|
||||
<details>
|
||||
|
||||
```console
|
||||
$ cscli metrics
|
||||
# paste output here
|
||||
```
|
||||
|
||||
</details>
|
||||
|
||||
- type: textarea
|
||||
id: customizations
|
||||
attributes:
|
||||
label: "Related custom configs versions (if applicable) : notification plugins, custom scenarios, parsers etc."
|
||||
value: |
|
||||
<details>
|
||||
|
||||
</details>
|
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
4
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
contact_links:
|
||||
- name: Support Request
|
||||
url: https://discourse.crowdsec.net
|
||||
about: Support request or question relating to Crowdsec
|
27
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
27
.github/ISSUE_TEMPLATE/feature_request.yaml
vendored
Normal file
|
@ -0,0 +1,27 @@
|
|||
name: Feature request
|
||||
description: Suggest an improvement or a new feature
|
||||
body:
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
label: What would you like to be added?
|
||||
description: |
|
||||
Significant feature requests are unlikely to make progress as issues. Please consider engaging on discord (discord.gg/crowdsec) and forums (https://discourse.crowdsec.net), instead.
|
||||
value: |
|
||||
For feature request please pick a kind label by removing `<!-- -->` that wrap the example lines below
|
||||
|
||||
|
||||
<!-- /kind feature -->
|
||||
<!-- Completely new feature not currently available -->
|
||||
|
||||
<!-- /kind enhancement -->
|
||||
<!-- Feature is available but this extends or adds extra functionality -->
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: rationale
|
||||
attributes:
|
||||
label: Why is this needed?
|
||||
validations:
|
||||
required: true
|
2
.github/buildkit.toml
vendored
Normal file
2
.github/buildkit.toml
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
[worker.oci]
|
||||
# max-parallelism = 2
|
10
.github/codecov.yml
vendored
Normal file
10
.github/codecov.yml
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
# we measure coverage but don't enforce it
|
||||
# https://docs.codecov.com/docs/codecov-yaml
|
||||
coverage:
|
||||
status:
|
||||
patch:
|
||||
default:
|
||||
target: 0%
|
||||
project:
|
||||
default:
|
||||
target: 0%
|
104
.github/governance.yml
vendored
Normal file
104
.github/governance.yml
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
version: v1
|
||||
|
||||
issue:
|
||||
captures:
|
||||
- regex: 'version: v*(.+)-[rc*]?'
|
||||
github_release: true
|
||||
ignore_case: true
|
||||
label: 'version/$CAPTURED'
|
||||
|
||||
- regex: 'Platform: *(windows?|ms|wins?|microsoft).*'
|
||||
label: 'os/win'
|
||||
ignore_case: true
|
||||
|
||||
- regex: 'Platform: *(freebsd|bsd).*'
|
||||
label: 'os/freebsd'
|
||||
ignore_case: true
|
||||
|
||||
- regex: 'Platform: *(linux|linus|lin).*'
|
||||
label: 'os/linux'
|
||||
ignore_case: true
|
||||
|
||||
- regex: 'Platform: *(macos|mac|apple|macintosh|macbook).*'
|
||||
label: 'os/mac'
|
||||
ignore_case: true
|
||||
|
||||
labels:
|
||||
- prefix: triage
|
||||
list: ['accepted']
|
||||
multiple: false
|
||||
author_association:
|
||||
collaborator: true
|
||||
member: true
|
||||
owner: true
|
||||
needs:
|
||||
comment: |
|
||||
@$AUTHOR: Thanks for opening an issue, it is currently awaiting triage.
|
||||
|
||||
In the meantime, you can:
|
||||
|
||||
1. Check [Crowdsec Documentation](https://docs.crowdsec.net/) to see if your issue can be self resolved.
|
||||
2. You can also join our [Discord](https://discord.gg/crowdsec).
|
||||
3. Check [Releases](https://github.com/crowdsecurity/crowdsec/releases/latest) to make sure your agent is on the latest version.
|
||||
|
||||
- prefix: kind
|
||||
list: ['feature', 'bug', 'packaging', 'enhancement', 'refactoring']
|
||||
multiple: false
|
||||
author_association:
|
||||
author: true
|
||||
collaborator: true
|
||||
member: true
|
||||
owner: true
|
||||
needs:
|
||||
comment: |
|
||||
@$AUTHOR: There are no 'kind' label on this issue. You need a 'kind' label to start the triage process.
|
||||
* `/kind feature`
|
||||
* `/kind enhancement`
|
||||
* `/kind refactoring`
|
||||
* `/kind bug`
|
||||
* `/kind packaging`
|
||||
|
||||
- prefix: os
|
||||
list: ['mac', 'win', 'linux', 'freebsd']
|
||||
multiple: true
|
||||
|
||||
pull_request:
|
||||
labels:
|
||||
- prefix: kind
|
||||
multiple: false
|
||||
list: [ 'feature', 'enhancement', 'fix', 'chore', 'dependencies', 'refactoring']
|
||||
needs:
|
||||
comment: |
|
||||
@$AUTHOR: There are no 'kind' label on this PR. You need a 'kind' label to generate the release automatically.
|
||||
* `/kind feature`
|
||||
* `/kind enhancement`
|
||||
* `/kind refactoring`
|
||||
* `/kind fix`
|
||||
* `/kind chore`
|
||||
* `/kind dependencies`
|
||||
status:
|
||||
context: 'Kind Label'
|
||||
description:
|
||||
success: Ready for review & merge.
|
||||
failure: Missing kind label to generate release automatically.
|
||||
|
||||
- prefix: area
|
||||
list: [ "agent", "local-api", "cscli", "security", "configuration", "appsec"]
|
||||
multiple: true
|
||||
needs:
|
||||
comment: |
|
||||
@$AUTHOR: There are no area labels on this PR. You can add as many areas as you see fit.
|
||||
* `/area agent`
|
||||
* `/area local-api`
|
||||
* `/area cscli`
|
||||
* `/area appsec`
|
||||
* `/area security`
|
||||
* `/area configuration`
|
||||
|
||||
- prefix: priority
|
||||
multiple: false
|
||||
list: [ 'urgent', 'important' ]
|
||||
author_association:
|
||||
collaborator: true
|
||||
member: true
|
||||
owner: true
|
35
.github/release-drafter.yml
vendored
35
.github/release-drafter.yml
vendored
|
@ -1,11 +1,38 @@
|
|||
categories:
|
||||
- title: 'New Features'
|
||||
labels:
|
||||
- 'new feature'
|
||||
- 'kind/feature'
|
||||
- title: 'Improvements'
|
||||
labels:
|
||||
- 'kind/enhancement'
|
||||
- 'enhancement'
|
||||
- 'improvement'
|
||||
- title: 'Bug Fixes'
|
||||
labels:
|
||||
- 'kind/fix'
|
||||
- 'fix'
|
||||
- 'bugfix'
|
||||
- 'bug'
|
||||
- title: 'Documentation'
|
||||
labels:
|
||||
- 'documentation'
|
||||
- 'doc'
|
||||
- title: 'Chore / Deps'
|
||||
labels:
|
||||
- 'kind/dependencies'
|
||||
- 'kind/chore'
|
||||
tag-template: "- $TITLE @$AUTHOR (#$NUMBER)"
|
||||
template: |
|
||||
Crowdsec $NEXT_PATCH_VERSION
|
||||
|
||||
## What’s Changed
|
||||
## Changes
|
||||
|
||||
$CHANGES
|
||||
|
||||
## Geolite2 notice
|
||||
|
||||
This product includes GeoLite2 data created by MaxMind, available from <a href="https://www.maxmind.com">https://www.maxmind.com</a>.
|
||||
|
||||
|
||||
## Installation
|
||||
|
||||
Take a look at the [installation instructions](https://doc.crowdsec.net/docs/getting_started/install_crowdsec).
|
||||
|
||||
|
|
1
.github/workflows/.yamllint
vendored
Symbolic link
1
.github/workflows/.yamllint
vendored
Symbolic link
|
@ -0,0 +1 @@
|
|||
../../.yamllint
|
75
.github/workflows/bats-hub.yml
vendored
Normal file
75
.github/workflows/bats-hub.yml
vendored
Normal file
|
@ -0,0 +1,75 @@
|
|||
name: (sub) Bats / Hub
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
GIST_BADGES_SECRET:
|
||||
required: true
|
||||
GIST_BADGES_ID:
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
test-file: ["hub-1.bats", "hub-2.bats", "hub-3.bats"]
|
||||
|
||||
name: "Functional tests"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
|
||||
- name: "Force machineid"
|
||||
run: |
|
||||
sudo chmod +w /etc/machine-id
|
||||
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
|
||||
|
||||
- name: "Check out CrowdSec repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
GOBIN: /usr/local/bin
|
||||
run: |
|
||||
sudo apt -qq -y -o=Dpkg::Use-Pty=0 install build-essential daemonize jq libre2-dev
|
||||
|
||||
- name: "Build crowdsec and fixture"
|
||||
run: make bats-clean bats-build bats-fixture BUILD_STATIC=1
|
||||
|
||||
- name: "Run hub tests"
|
||||
run: |
|
||||
./test/bin/generate-hub-tests
|
||||
./test/run-tests ./test/dyn-bats/${{ matrix.test-file }} --formatter $(pwd)/test/lib/color-formatter
|
||||
|
||||
- name: "Collect hub coverage"
|
||||
run: ./test/bin/collect-hub-coverage >> $GITHUB_ENV
|
||||
|
||||
- name: "Create Parsers badge"
|
||||
uses: schneegans/dynamic-badges-action@v1.7.0
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.repository_owner == 'crowdsecurity' }}
|
||||
with:
|
||||
auth: ${{ secrets.GIST_BADGES_SECRET }}
|
||||
gistID: ${{ secrets.GIST_BADGES_ID }}
|
||||
filename: crowdsec_parsers_badge.json
|
||||
label: Hub Parsers
|
||||
message: ${{ env.PARSERS_COV }}
|
||||
color: ${{ env.SCENARIO_BADGE_COLOR }}
|
||||
|
||||
- name: "Create Scenarios badge"
|
||||
uses: schneegans/dynamic-badges-action@v1.7.0
|
||||
if: ${{ github.ref == 'refs/heads/master' && github.repository_owner == 'crowdsecurity' }}
|
||||
with:
|
||||
auth: ${{ secrets.GIST_BADGES_SECRET }}
|
||||
gistID: ${{ secrets.GIST_BADGES_ID }}
|
||||
filename: crowdsec_scenarios_badge.json
|
||||
label: Hub Scenarios
|
||||
message: ${{ env.SCENARIOS_COV }}
|
||||
color: ${{ env.SCENARIO_BADGE_COLOR }}
|
92
.github/workflows/bats-mysql.yml
vendored
Normal file
92
.github/workflows/bats-mysql.yml
vendored
Normal file
|
@ -0,0 +1,92 @@
|
|||
name: (sub) Bats / MySQL
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
database_image:
|
||||
required: true
|
||||
type: string
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Functional tests"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
database:
|
||||
image: ${{ inputs.database_image }}
|
||||
env:
|
||||
MYSQL_ROOT_PASSWORD: "secret"
|
||||
ports:
|
||||
- 3306:3306
|
||||
|
||||
steps:
|
||||
|
||||
- name: "Force machineid"
|
||||
run: |
|
||||
sudo chmod +w /etc/machine-id
|
||||
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
|
||||
|
||||
- name: "Check out CrowdSec repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
GOBIN: /usr/local/bin
|
||||
run: |
|
||||
sudo apt -qq -y -o=Dpkg::Use-Pty=0 install build-essential daemonize jq libre2-dev
|
||||
|
||||
- name: "Build crowdsec and fixture"
|
||||
run: |
|
||||
make clean bats-build bats-fixture BUILD_STATIC=1
|
||||
env:
|
||||
DB_BACKEND: mysql
|
||||
MYSQL_HOST: 127.0.0.1
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_PASSWORD: "secret"
|
||||
MYSQL_USER: root
|
||||
|
||||
- name: "Run tests"
|
||||
run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
|
||||
env:
|
||||
DB_BACKEND: mysql
|
||||
MYSQL_HOST: 127.0.0.1
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_PASSWORD: "secret"
|
||||
MYSQL_USER: root
|
||||
|
||||
#
|
||||
# In case you need to inspect the database status after the failure of a given test
|
||||
#
|
||||
# - name: "Run specified tests"
|
||||
# run: ./test/run-tests test/bats/<filename>.bats -f "<test name>"
|
||||
|
||||
- name: Show database dump
|
||||
run: ./test/instance-db dump /dev/fd/1
|
||||
env:
|
||||
DB_BACKEND: mysql
|
||||
MYSQL_HOST: 127.0.0.1
|
||||
MYSQL_PORT: 3306
|
||||
MYSQL_PASSWORD: "secret"
|
||||
MYSQL_USER: root
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: "Show stack traces"
|
||||
run: for file in $(find /tmp/crowdsec-crash.*.txt); do echo ">>>>> $file"; cat $file; echo; done
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: "Show crowdsec logs"
|
||||
run: for file in $(find ./test/local/var/log -type f); do echo ">>>>> $file"; cat $file; echo; done
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: "Show database logs"
|
||||
run: docker logs "${{ job.services.database.id }}"
|
||||
if: ${{ always() }}
|
85
.github/workflows/bats-postgres.yml
vendored
Normal file
85
.github/workflows/bats-postgres.yml
vendored
Normal file
|
@ -0,0 +1,85 @@
|
|||
name: (sub) Bats / Postgres
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Functional tests"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
services:
|
||||
database:
|
||||
image: postgres:16
|
||||
env:
|
||||
POSTGRES_PASSWORD: "secret"
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready -u postgres
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
|
||||
- name: "Install pg_dump v16"
|
||||
# we can remove this when it's released on ubuntu-latest
|
||||
run: |
|
||||
sudo sh -c 'echo "deb http://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list'
|
||||
wget -qO- https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo tee /etc/apt/trusted.gpg.d/pgdg.asc &>/dev/null
|
||||
sudo apt update
|
||||
sudo apt -qq -y -o=Dpkg::Use-Pty=0 install postgresql-client-16
|
||||
|
||||
- name: "Force machineid"
|
||||
run: |
|
||||
sudo chmod +w /etc/machine-id
|
||||
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
|
||||
|
||||
- name: "Check out CrowdSec repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
GOBIN: /usr/local/bin
|
||||
run: |
|
||||
sudo apt -qq -y -o=Dpkg::Use-Pty=0 install build-essential daemonize jq libre2-dev
|
||||
|
||||
- name: "Build crowdsec and fixture (DB_BACKEND: pgx)"
|
||||
run: |
|
||||
make clean bats-build bats-fixture BUILD_STATIC=1
|
||||
env:
|
||||
DB_BACKEND: pgx
|
||||
PGHOST: 127.0.0.1
|
||||
PGPORT: 5432
|
||||
PGPASSWORD: "secret"
|
||||
PGUSER: postgres
|
||||
|
||||
- name: "Run tests (DB_BACKEND: pgx)"
|
||||
run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
|
||||
env:
|
||||
DB_BACKEND: pgx
|
||||
PGHOST: 127.0.0.1
|
||||
PGPORT: 5432
|
||||
PGPASSWORD: "secret"
|
||||
PGUSER: postgres
|
||||
|
||||
- name: "Show stack traces"
|
||||
run: for file in $(find /tmp/crowdsec-crash.*.txt); do echo ">>>>> $file"; cat $file; echo; done
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: "Show crowdsec logs"
|
||||
run: for file in $(find ./test/local/var/log -type f); do echo ">>>>> $file"; cat $file; echo; done
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: "Show database logs"
|
||||
run: docker logs "${{ job.services.database.id }}"
|
||||
if: ${{ always() }}
|
84
.github/workflows/bats-sqlite-coverage.yml
vendored
Normal file
84
.github/workflows/bats-sqlite-coverage.yml
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
name: (sub) Bats / sqlite + coverage
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
|
||||
env:
|
||||
TEST_COVERAGE: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Functional tests"
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 20
|
||||
|
||||
steps:
|
||||
|
||||
- name: "Force machineid"
|
||||
run: |
|
||||
sudo chmod +w /etc/machine-id
|
||||
echo githubciXXXXXXXXXXXXXXXXXXXXXXXX | sudo tee /etc/machine-id
|
||||
|
||||
- name: "Check out CrowdSec repository"
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: true
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: "Install bats dependencies"
|
||||
env:
|
||||
GOBIN: /usr/local/bin
|
||||
run: |
|
||||
sudo apt -qq -y -o=Dpkg::Use-Pty=0 install build-essential daemonize jq libre2-dev
|
||||
|
||||
- name: "Build crowdsec and fixture"
|
||||
run: |
|
||||
make clean bats-build bats-fixture BUILD_STATIC=1
|
||||
|
||||
- name: "Run tests"
|
||||
run: ./test/run-tests ./test/bats --formatter $(pwd)/test/lib/color-formatter
|
||||
|
||||
- name: "Collect coverage data"
|
||||
run: |
|
||||
go tool covdata textfmt -i test/coverage -o coverage-bats-raw.out
|
||||
# filter out unwanted packages, should match the argument to "go-acc --ignore"
|
||||
grep -v \
|
||||
-e '/pkg/database' \
|
||||
-e '/plugins/notifications' \
|
||||
-e '/pkg/protobufs' \
|
||||
-e '/pkg/cwversions' \
|
||||
-e '/pkg/models' \
|
||||
< coverage-bats-raw.out \
|
||||
> coverage-bats.out
|
||||
|
||||
#
|
||||
# In case you need to inspect the database status after the failure of a given test
|
||||
#
|
||||
# - name: "Run specified tests"
|
||||
# run: ./test/run-tests test/bats/<filename>.bats -f "<test name>"
|
||||
|
||||
- name: "Show database dump"
|
||||
run: |
|
||||
./test/instance-crowdsec stop
|
||||
sqlite3 ./test/local/var/lib/crowdsec/data/crowdsec.db '.dump'
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: "Show stack traces"
|
||||
run: for file in $(find /tmp/crowdsec-crash.*.txt); do echo ">>>>> $file"; cat $file; echo; done
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: "Show crowdsec logs"
|
||||
run: for file in $(find ./test/local/var/log -type f); do echo ">>>>> $file"; cat $file; echo; done
|
||||
if: ${{ always() }}
|
||||
|
||||
- name: Upload crowdsec coverage to codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: ./coverage-bats.out
|
||||
flags: bats
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
53
.github/workflows/bats.yml
vendored
Normal file
53
.github/workflows/bats.yml
vendored
Normal file
|
@ -0,0 +1,53 @@
|
|||
---
|
||||
# This workflow is actually running
|
||||
# only functional tests, but the
|
||||
# name is used for the badge in README.md
|
||||
|
||||
name: Tests
|
||||
|
||||
# Main workflow for functional tests, it calls all the others through parallel jobs.
|
||||
#
|
||||
# https://docs.github.com/en/actions/using-workflows/reusing-workflows
|
||||
#
|
||||
# There is no need to merge coverage output because codecov.io should take care of that.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- "README.md"
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- "README.md"
|
||||
|
||||
jobs:
|
||||
sqlite:
|
||||
uses: ./.github/workflows/bats-sqlite-coverage.yml
|
||||
|
||||
# Jobs for Postgres (and sometimes MySQL) can have failing tests on GitHub
|
||||
# CI, but they pass when run on devs' machines or in the release checks. We
|
||||
# disable them here by default. Remove if...false to enable them.
|
||||
|
||||
mariadb:
|
||||
uses: ./.github/workflows/bats-mysql.yml
|
||||
with:
|
||||
database_image: mariadb:latest
|
||||
|
||||
mysql:
|
||||
uses: ./.github/workflows/bats-mysql.yml
|
||||
with:
|
||||
database_image: mysql:latest
|
||||
|
||||
postgres:
|
||||
uses: ./.github/workflows/bats-postgres.yml
|
||||
|
||||
hub:
|
||||
uses: ./.github/workflows/bats-hub.yml
|
||||
secrets:
|
||||
GIST_BADGES_ID: ${{ secrets.GIST_BADGES_ID }}
|
||||
GIST_BADGES_SECRET: ${{ secrets.GIST_BADGES_SECRET }}
|
32
.github/workflows/build-binary-package.yml
vendored
32
.github/workflows/build-binary-package.yml
vendored
|
@ -1,32 +0,0 @@
|
|||
# .github/workflows/build-docker-image.yml
|
||||
name: build-binary-package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: prereleased
|
||||
|
||||
jobs:
|
||||
build-binary-package:
|
||||
name: Build and upload binary package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- name: Check tag == json version
|
||||
run: |
|
||||
jsonver=$(cat RELEASE.json | jq -r .Version)
|
||||
lasttag=$(git for-each-ref --sort=-v:refname --count=1 --format '%(refname)' | cut -d '/' -f3)
|
||||
if [ ${jsonver} != ${lasttag} ] ; then echo "version mismatch : ${jsonver} in json, ${lasttag} in git" ; exit 2 ; else echo "${jsonver} == ${lasttag}" ; fi
|
||||
- name: Build the binaries
|
||||
run: make release
|
||||
- name: Upload to release
|
||||
uses: JasonEtco/upload-to-release@master
|
||||
with:
|
||||
args: crowdsec-release.tgz application/x-gzip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
35
.github/workflows/cache-cleanup.yaml
vendored
Normal file
35
.github/workflows/cache-cleanup.yaml
vendored
Normal file
|
@ -0,0 +1,35 @@
|
|||
# https://docs.github.com/en/actions/using-workflows/caching-dependencies-to-speed-up-workflows#managing-caches
|
||||
|
||||
name: cleanup caches by a branch
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
jobs:
|
||||
cleanup:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Cleanup
|
||||
run: |
|
||||
gh extension install actions/gh-actions-cache
|
||||
|
||||
REPO=${{ github.repository }}
|
||||
BRANCH="refs/pull/${{ github.event.pull_request.number }}/merge"
|
||||
|
||||
echo "Fetching list of cache key"
|
||||
cacheKeysForPR=$(gh actions-cache list -R $REPO -B $BRANCH | cut -f 1 )
|
||||
|
||||
## Setting this to not fail the workflow while deleting cache keys.
|
||||
set +e
|
||||
echo "Deleting caches..."
|
||||
for cacheKey in $cacheKeysForPR
|
||||
do
|
||||
gh actions-cache delete $cacheKey -R $REPO -B $BRANCH --confirm
|
||||
done
|
||||
echo "Done"
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
46
.github/workflows/ci-windows-build-msi.yml
vendored
Normal file
46
.github/workflows/ci-windows-build-msi.yml
vendored
Normal file
|
@ -0,0 +1,46 @@
|
|||
name: build-msi (windows)
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- prereleased
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths:
|
||||
- windows/installer/*.wxs
|
||||
- .github/workflows/ci-windows-build-msi.yml
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths:
|
||||
- windows/installer/*.wxs
|
||||
- .github/workflows/ci-windows-build-msi.yml
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build
|
||||
runs-on: windows-2019
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: false
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: Build
|
||||
run: make windows_installer BUILD_RE2_WASM=1
|
||||
- name: Upload MSI
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
path: crowdsec*msi
|
||||
name: crowdsec.msi
|
21
.github/workflows/ci_release-drafter.yml
vendored
Normal file
21
.github/workflows/ci_release-drafter.yml
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches to consider in the event; optional, defaults to all
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||
- uses: release-drafter/release-drafter@v6
|
||||
with:
|
||||
config-name: release-drafter.yml
|
||||
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||
# config-name: my-config.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
84
.github/workflows/codeql-analysis.yml
vendored
Normal file
84
.github/workflows/codeql-analysis.yml
vendored
Normal file
|
@ -0,0 +1,84 @@
|
|||
# yamllint disable rule:comments
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
schedule:
|
||||
- cron: '15 16 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'go' ]
|
||||
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
|
||||
# Learn more:
|
||||
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
# required to pick up tags for BUILD_VERSION
|
||||
fetch-depth: 0
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
cache-dependency-path: "**/go.sum"
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||
# By default, queries listed here will override any specified in a config file.
|
||||
# Prefix the list here with "+" to use these queries and those in the config file.
|
||||
# queries: ./path/to/local/query, your-org/your-repo/queries@main
|
||||
|
||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||
# If this step fails, then you should remove it and run the build manually (see below)
|
||||
# - name: Autobuild
|
||||
# uses: github/codeql-action/autobuild@v3
|
||||
|
||||
# ℹ️ Command-line programs to run using the OS shell.
|
||||
# 📚 https://git.io/JvXDl
|
||||
|
||||
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
|
||||
# and modify them (or add more) to build your code if your project
|
||||
# uses a compiled language
|
||||
|
||||
- run: |
|
||||
make clean build BUILD_RE2_WASM=1
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
87
.github/workflows/docker-tests.yml
vendored
Normal file
87
.github/workflows/docker-tests.yml
vendored
Normal file
|
@ -0,0 +1,87 @@
|
|||
name: Test Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
|
||||
jobs:
|
||||
test_flavor:
|
||||
strategy:
|
||||
# we could test all the flavors in a single pytest job,
|
||||
# but let's split them (and the image build) in multiple runners for performance
|
||||
matrix:
|
||||
# can be slim, full or debian (no debian slim).
|
||||
flavor: ["slim", "debian"]
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
config: .github/buildkit.toml
|
||||
|
||||
- name: "Build image"
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile${{ matrix.flavor == 'debian' && '.debian' || '' }}
|
||||
tags: crowdsecurity/crowdsec:test${{ matrix.flavor == 'full' && '' || '-' }}${{ matrix.flavor == 'full' && '' || matrix.flavor }}
|
||||
target: ${{ matrix.flavor == 'debian' && 'full' || matrix.flavor }}
|
||||
platforms: linux/amd64
|
||||
load: true
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=min
|
||||
|
||||
- name: "Setup Python"
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.x"
|
||||
|
||||
- name: "Install pipenv"
|
||||
run: |
|
||||
cd docker/test
|
||||
python -m pip install --upgrade pipenv wheel
|
||||
|
||||
#- name: "Cache virtualenvs"
|
||||
# id: cache-pipenv
|
||||
# uses: actions/cache@v4
|
||||
# with:
|
||||
# path: ~/.local/share/virtualenvs
|
||||
# key: ${{ runner.os }}-pipenv-${{ hashFiles('**/Pipfile.lock') }}
|
||||
|
||||
- name: "Install dependencies"
|
||||
#if: steps.cache-pipenv.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd docker/test
|
||||
pipenv install --deploy
|
||||
|
||||
- name: "Create Docker network"
|
||||
run: docker network create net-test
|
||||
|
||||
- name: "Run tests"
|
||||
env:
|
||||
CROWDSEC_TEST_VERSION: test
|
||||
CROWDSEC_TEST_FLAVORS: ${{ matrix.flavor }}
|
||||
CROWDSEC_TEST_NETWORK: net-test
|
||||
CROWDSEC_TEST_TIMEOUT: 90
|
||||
# running serially to reduce test flakiness
|
||||
run: |
|
||||
cd docker/test
|
||||
pipenv run pytest -n 1 --durations=0 --color=yes
|
66
.github/workflows/go-tests-windows.yml
vendored
Normal file
66
.github/workflows/go-tests-windows.yml
vendored
Normal file
|
@ -0,0 +1,66 @@
|
|||
name: Go tests (windows)
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
|
||||
env:
|
||||
RICHGO_FORCE_COLOR: 1
|
||||
CROWDSEC_FEATURE_DISABLE_HTTP_RETRY_BACKOFF: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build + tests"
|
||||
runs-on: windows-2022
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out CrowdSec repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: false
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
make build BUILD_RE2_WASM=1
|
||||
|
||||
- name: Run tests
|
||||
run: |
|
||||
go install github.com/kyoh86/richgo@v0.3.10
|
||||
go test -coverprofile coverage.out -covermode=atomic ./... > out.txt
|
||||
if(!$?) { cat out.txt | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter; Exit 1 }
|
||||
cat out.txt | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter
|
||||
|
||||
- name: Upload unit coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: coverage.out
|
||||
flags: unit-windows
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
version: v1.57
|
||||
args: --issues-exit-code=1 --timeout 10m
|
||||
only-new-issues: false
|
||||
# the cache is already managed above, enabling it here
|
||||
# gives errors when extracting
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
167
.github/workflows/go-tests.yml
vendored
Normal file
167
.github/workflows/go-tests.yml
vendored
Normal file
|
@ -0,0 +1,167 @@
|
|||
---
|
||||
# This workflow is actually running
|
||||
# tests (with localstack) but the
|
||||
# name is used for the badge in README.md
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
- releases/**
|
||||
paths-ignore:
|
||||
- 'README.md'
|
||||
|
||||
# these env variables are for localstack, so we can emulate aws services
|
||||
env:
|
||||
RICHGO_FORCE_COLOR: 1
|
||||
AWS_HOST: localstack
|
||||
# these are to mimic aws config
|
||||
AWS_ACCESS_KEY_ID: test
|
||||
AWS_SECRET_ACCESS_KEY: test
|
||||
AWS_REGION: us-east-1
|
||||
CROWDSEC_FEATURE_DISABLE_HTTP_RETRY_BACKOFF: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: "Build + tests"
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
localstack:
|
||||
image: localstack/localstack:3.0
|
||||
ports:
|
||||
- 4566:4566 # Localstack exposes all services on the same port
|
||||
env:
|
||||
DEBUG: ""
|
||||
LAMBDA_EXECUTOR: ""
|
||||
KINESIS_ERROR_PROBABILITY: ""
|
||||
DOCKER_HOST: unix:///var/run/docker.sock
|
||||
KINESIS_INITIALIZE_STREAMS: ${{ env.KINESIS_INITIALIZE_STREAMS }}
|
||||
LOCALSTACK_HOST: ${{ env.AWS_HOST }} # Required so that resource urls are provided properly
|
||||
# e.g sqs url will get localhost if we don't set this env to map our service
|
||||
options: >-
|
||||
--name=localstack
|
||||
--health-cmd="curl -sS 127.0.0.1:4566 || exit 1"
|
||||
--health-interval=10s
|
||||
--health-timeout=5s
|
||||
--health-retries=3
|
||||
zoo1:
|
||||
image: confluentinc/cp-zookeeper:7.4.3
|
||||
ports:
|
||||
- "2181:2181"
|
||||
env:
|
||||
ZOOKEEPER_CLIENT_PORT: 2181
|
||||
ZOOKEEPER_SERVER_ID: 1
|
||||
ZOOKEEPER_SERVERS: zoo1:2888:3888
|
||||
options: >-
|
||||
--name=zoo1
|
||||
--health-cmd "jps -l | grep zookeeper"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
kafka1:
|
||||
image: crowdsecurity/kafka-ssl
|
||||
ports:
|
||||
- "9093:9093"
|
||||
- "9092:9092"
|
||||
- "9999:9999"
|
||||
env:
|
||||
KAFKA_ADVERTISED_LISTENERS: LISTENER_DOCKER_INTERNAL://127.0.0.1:19092,LISTENER_DOCKER_EXTERNAL://127.0.0.1:9092,LISTENER_DOCKER_EXTERNAL_SSL://127.0.0.1:9093
|
||||
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: LISTENER_DOCKER_INTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL:PLAINTEXT,LISTENER_DOCKER_EXTERNAL_SSL:SSL
|
||||
KAFKA_INTER_BROKER_LISTENER_NAME: LISTENER_DOCKER_INTERNAL
|
||||
KAFKA_ZOOKEEPER_CONNECT: "zoo1:2181"
|
||||
KAFKA_BROKER_ID: 1
|
||||
KAFKA_LOG4J_LOGGERS: "kafka.controller=INFO,kafka.producer.async.DefaultEventHandler=INFO,state.change.logger=INFO"
|
||||
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
|
||||
KAFKA_TRANSACTION_STATE_LOG_REPLICATION_FACTOR: 1
|
||||
KAFKA_TRANSACTION_STATE_LOG_MIN_ISR: 1
|
||||
KAFKA_JMX_PORT: 9999
|
||||
KAFKA_JMX_HOSTNAME: "127.0.0.1"
|
||||
KAFKA_AUTHORIZER_CLASS_NAME: kafka.security.authorizer.AclAuthorizer
|
||||
KAFKA_ALLOW_EVERYONE_IF_NO_ACL_FOUND: "true"
|
||||
KAFKA_SSL_KEYSTORE_FILENAME: kafka.kafka1.keystore.jks
|
||||
KAFKA_SSL_KEYSTORE_CREDENTIALS: kafka1_keystore_creds
|
||||
KAFKA_SSL_KEY_CREDENTIALS: kafka1_sslkey_creds
|
||||
KAFKA_SSL_TRUSTSTORE_FILENAME: kafka.kafka1.truststore.jks
|
||||
KAFKA_SSL_TRUSTSTORE_CREDENTIALS: kafka1_truststore_creds
|
||||
KAFKA_SSL_ENABLED_PROTOCOLS: TLSv1.2
|
||||
KAFKA_SSL_PROTOCOL: TLSv1.2
|
||||
KAFKA_SSL_CLIENT_AUTH: none
|
||||
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
|
||||
options: >-
|
||||
--name=kafka1
|
||||
--health-cmd "kafka-broker-api-versions --version"
|
||||
--health-interval 10s
|
||||
--health-timeout 10s
|
||||
--health-retries 5
|
||||
|
||||
loki:
|
||||
image: grafana/loki:2.9.1
|
||||
ports:
|
||||
- "3100:3100"
|
||||
options: >-
|
||||
--name=loki1
|
||||
--health-cmd "wget -q -O - http://localhost:3100/ready | grep 'ready'"
|
||||
--health-interval 30s
|
||||
--health-timeout 10s
|
||||
--health-retries 5
|
||||
--health-start-period 30s
|
||||
|
||||
steps:
|
||||
|
||||
- name: Check out CrowdSec repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: false
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: Create localstack streams
|
||||
run: |
|
||||
aws --endpoint-url=http://127.0.0.1:4566 --region us-east-1 kinesis create-stream --stream-name stream-1-shard --shard-count 1
|
||||
aws --endpoint-url=http://127.0.0.1:4566 --region us-east-1 kinesis create-stream --stream-name stream-2-shards --shard-count 2
|
||||
|
||||
- name: Build and run tests, static
|
||||
run: |
|
||||
sudo apt -qq -y -o=Dpkg::Use-Pty=0 install build-essential libre2-dev
|
||||
go install github.com/ory/go-acc@v0.2.8
|
||||
go install github.com/kyoh86/richgo@v0.3.10
|
||||
set -o pipefail
|
||||
make build BUILD_STATIC=1
|
||||
make go-acc | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter
|
||||
|
||||
- name: Run tests again, dynamic
|
||||
run: |
|
||||
make clean build
|
||||
set -o pipefail
|
||||
make go-acc | sed 's/ *coverage:.*of statements in.*//' | richgo testfilter
|
||||
|
||||
- name: Upload unit coverage to Codecov
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
files: coverage.out
|
||||
flags: unit-linux
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v4
|
||||
with:
|
||||
version: v1.57
|
||||
args: --issues-exit-code=1 --timeout 10m
|
||||
only-new-issues: false
|
||||
# the cache is already managed above, enabling it here
|
||||
# gives errors when extracting
|
||||
skip-pkg-cache: true
|
||||
skip-build-cache: true
|
35
.github/workflows/go.yml
vendored
35
.github/workflows/go.yml
vendored
|
@ -1,35 +0,0 @@
|
|||
name: Go
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
name: Build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Go 1.13
|
||||
uses: actions/setup-go@v1
|
||||
with:
|
||||
go-version: 1.13
|
||||
id: go
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: make build
|
||||
- name: Test-Parser
|
||||
run: go test -coverprofile=tests.out github.com/crowdsecurity/crowdsec/pkg/parser && go tool cover -html=tests.out -o coverage_parser.html
|
||||
- name: Test-Buckets
|
||||
run: go test -coverprofile=tests.out github.com/crowdsecurity/crowdsec/pkg/leakybucket && go tool cover -html=tests.out -o coverage_buckets.html
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: coverage_parser.html
|
||||
path: ./coverage_parser.html
|
||||
- uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: coverage_buckets.html
|
||||
path: ./coverage_buckets.html
|
24
.github/workflows/golangci-lint.yml
vendored
24
.github/workflows/golangci-lint.yml
vendored
|
@ -1,24 +0,0 @@
|
|||
name: golangci-lint
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
branches:
|
||||
- master
|
||||
pull_request:
|
||||
jobs:
|
||||
golangci:
|
||||
name: lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: golangci-lint
|
||||
uses: golangci/golangci-lint-action@v1
|
||||
with:
|
||||
# Required: the version of golangci-lint is required and must be specified without patch version: we always use the latest patch version.
|
||||
version: v1.26
|
||||
# Optional: golangci-lint command line arguments.
|
||||
args: --issues-exit-code=0
|
||||
only-new-issues: true
|
||||
|
||||
|
30
.github/workflows/governance-bot.yaml
vendored
Normal file
30
.github/workflows/governance-bot.yaml
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
# .github/workflow/governance.yml
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ synchronize, opened, labeled, unlabeled ]
|
||||
issues:
|
||||
types: [ opened, labeled, unlabeled ]
|
||||
issue_comment:
|
||||
types: [ created ]
|
||||
|
||||
# You can use permissions to modify the default permissions granted to the GITHUB_TOKEN,
|
||||
# adding or removing access as required, so that you only allow the minimum required access.
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
statuses: write
|
||||
checks: write
|
||||
|
||||
jobs:
|
||||
governance:
|
||||
name: Governance
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Semantic versioning, lock to different version: v2, v2.0 or a commit hash.
|
||||
- uses: BirthdayResearch/oss-governance-bot@v4
|
||||
with:
|
||||
# You can use a PAT to post a comment/label/status so that it shows up as a user instead of github-actions
|
||||
github-token: ${{secrets.GITHUB_TOKEN}} # optional, default to '${{ github.token }}'
|
||||
config-path: .github/governance.yml # optional, default to '.github/governance.yml'
|
47
.github/workflows/publish-docker-master.yml
vendored
Normal file
47
.github/workflows/publish-docker-master.yml
vendored
Normal file
|
@ -0,0 +1,47 @@
|
|||
name: (push-master) Publish latest Docker images
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ master ]
|
||||
paths:
|
||||
- 'pkg/**'
|
||||
- 'cmd/**'
|
||||
- 'mk/**'
|
||||
- 'docker/docker_start.sh'
|
||||
- 'docker/config.yaml'
|
||||
- '.github/workflows/publish-docker-master.yml'
|
||||
- '.github/workflows/publish-docker.yml'
|
||||
- 'Dockerfile'
|
||||
- 'Dockerfile.debian'
|
||||
- 'go.mod'
|
||||
- 'go.sum'
|
||||
- 'Makefile'
|
||||
|
||||
jobs:
|
||||
dev-alpine:
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
with:
|
||||
platform: linux/amd64
|
||||
crowdsec_version: ""
|
||||
image_version: dev
|
||||
latest: false
|
||||
push: true
|
||||
slim: false
|
||||
debian: false
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
dev-debian:
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
with:
|
||||
platform: linux/amd64
|
||||
crowdsec_version: ""
|
||||
image_version: dev
|
||||
latest: false
|
||||
push: true
|
||||
slim: false
|
||||
debian: true
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
48
.github/workflows/publish-docker-release.yml
vendored
Normal file
48
.github/workflows/publish-docker-release.yml
vendored
Normal file
|
@ -0,0 +1,48 @@
|
|||
name: (manual) Publish Docker images
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
image_version:
|
||||
description: Docker Image version (base tag, i.e. v1.6.0-2)
|
||||
required: true
|
||||
crowdsec_version:
|
||||
description: Crowdsec version (BUILD_VERSION)
|
||||
required: true
|
||||
latest:
|
||||
description: Overwrite latest (and slim) tags?
|
||||
default: false
|
||||
required: true
|
||||
push:
|
||||
description: Really push?
|
||||
default: false
|
||||
required: true
|
||||
|
||||
jobs:
|
||||
alpine:
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
with:
|
||||
image_version: ${{ github.event.inputs.image_version }}
|
||||
crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
|
||||
latest: ${{ github.event.inputs.latest == 'true' }}
|
||||
push: ${{ github.event.inputs.push == 'true' }}
|
||||
slim: true
|
||||
debian: false
|
||||
platform: "linux/amd64,linux/386,linux/arm64,linux/arm/v7,linux/arm/v6"
|
||||
|
||||
debian:
|
||||
uses: ./.github/workflows/publish-docker.yml
|
||||
secrets:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
|
||||
with:
|
||||
image_version: ${{ github.event.inputs.image_version }}
|
||||
crowdsec_version: ${{ github.event.inputs.crowdsec_version }}
|
||||
latest: ${{ github.event.inputs.latest == 'true' }}
|
||||
push: ${{ github.event.inputs.push == 'true' }}
|
||||
slim: false
|
||||
debian: true
|
||||
platform: "linux/amd64,linux/386,linux/arm64"
|
125
.github/workflows/publish-docker.yml
vendored
Normal file
125
.github/workflows/publish-docker.yml
vendored
Normal file
|
@ -0,0 +1,125 @@
|
|||
name: (sub) Publish Docker images
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
DOCKER_USERNAME:
|
||||
required: true
|
||||
DOCKER_PASSWORD:
|
||||
required: true
|
||||
inputs:
|
||||
platform:
|
||||
required: true
|
||||
type: string
|
||||
image_version:
|
||||
required: true
|
||||
type: string
|
||||
crowdsec_version:
|
||||
required: true
|
||||
type: string
|
||||
latest:
|
||||
required: true
|
||||
type: boolean
|
||||
push:
|
||||
required: true
|
||||
type: boolean
|
||||
slim:
|
||||
required: true
|
||||
type: boolean
|
||||
debian:
|
||||
required: true
|
||||
type: boolean
|
||||
|
||||
jobs:
|
||||
push_to_registry:
|
||||
name: Push Docker image to registries
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
with:
|
||||
config: .github/buildkit.toml
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Prepare (slim)
|
||||
if: ${{ inputs.slim }}
|
||||
id: slim
|
||||
run: |
|
||||
DOCKERHUB_IMAGE=${{ secrets.DOCKER_USERNAME }}/crowdsec
|
||||
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/crowdsec
|
||||
VERSION=${{ inputs.image_version }}
|
||||
DEBIAN=${{ inputs.debian && '-debian' || '' }}
|
||||
TAGS="${DOCKERHUB_IMAGE}:${VERSION}-slim${DEBIAN},${GHCR_IMAGE}:${VERSION}-slim${DEBIAN}"
|
||||
if [[ ${{ inputs.latest }} == true ]]; then
|
||||
TAGS=$TAGS,${DOCKERHUB_IMAGE}:slim${DEBIAN},${GHCR_IMAGE}:slim${DEBIAN}
|
||||
fi
|
||||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Prepare (full)
|
||||
id: full
|
||||
run: |
|
||||
DOCKERHUB_IMAGE=${{ secrets.DOCKER_USERNAME }}/crowdsec
|
||||
GHCR_IMAGE=ghcr.io/${{ github.repository_owner }}/crowdsec
|
||||
VERSION=${{ inputs.image_version }}
|
||||
DEBIAN=${{ inputs.debian && '-debian' || '' }}
|
||||
TAGS="${DOCKERHUB_IMAGE}:${VERSION}${DEBIAN},${GHCR_IMAGE}:${VERSION}${DEBIAN}"
|
||||
if [[ ${{ inputs.latest }} == true ]]; then
|
||||
TAGS=$TAGS,${DOCKERHUB_IMAGE}:latest${DEBIAN},${GHCR_IMAGE}:latest${DEBIAN}
|
||||
fi
|
||||
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
|
||||
echo "created=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Build and push image (slim)
|
||||
if: ${{ inputs.slim }}
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile${{ inputs.debian && '.debian' || '' }}
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ steps.slim.outputs.tags }}
|
||||
target: slim
|
||||
platforms: ${{ inputs.platform }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.event.repository.html_url }}
|
||||
org.opencontainers.image.created=${{ steps.slim.outputs.created }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
build-args: |
|
||||
BUILD_VERSION=${{ inputs.crowdsec_version }}
|
||||
|
||||
- name: Build and push image (full)
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile${{ inputs.debian && '.debian' || '' }}
|
||||
push: ${{ inputs.push }}
|
||||
tags: ${{ steps.full.outputs.tags }}
|
||||
target: full
|
||||
platforms: ${{ inputs.platform }}
|
||||
labels: |
|
||||
org.opencontainers.image.source=${{ github.event.repository.html_url }}
|
||||
org.opencontainers.image.created=${{ steps.full.outputs.created }}
|
||||
org.opencontainers.image.revision=${{ github.sha }}
|
||||
build-args: |
|
||||
BUILD_VERSION=${{ inputs.crowdsec_version }}
|
40
.github/workflows/publish-tarball-release.yml
vendored
Normal file
40
.github/workflows/publish-tarball-release.yml
vendored
Normal file
|
@ -0,0 +1,40 @@
|
|||
# .github/workflows/build-docker-image.yml
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- prereleased
|
||||
|
||||
permissions:
|
||||
# Use write for: hub release edit
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build and upload binary package
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
|
||||
- name: Check out code into the Go module directory
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
submodules: false
|
||||
|
||||
- name: "Set up Go"
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.22.2"
|
||||
|
||||
- name: Build the binaries
|
||||
run: |
|
||||
sudo apt -qq -y -o=Dpkg::Use-Pty=0 install build-essential libre2-dev
|
||||
make vendor release BUILD_STATIC=1
|
||||
|
||||
- name: Upload to release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
tag_name="${GITHUB_REF##*/}"
|
||||
gh release upload "$tag_name" crowdsec-release.tgz vendor.tgz *-vendor.tar.xz
|
21
.github/workflows/release-drafter.yml
vendored
21
.github/workflows/release-drafter.yml
vendored
|
@ -1,21 +0,0 @@
|
|||
name: Release Drafter
|
||||
|
||||
on:
|
||||
push:
|
||||
# branches to consider in the event; optional, defaults to all
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
update_release_draft:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Drafts your next Release notes as Pull Requests are merged into "master"
|
||||
- uses: release-drafter/release-drafter@v5
|
||||
with:
|
||||
config-name: release-drafter.yml
|
||||
# (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml
|
||||
# config-name: my-config.yml
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
26
.github/workflows/update_docker_hub_doc.yml
vendored
Normal file
26
.github/workflows/update_docker_hub_doc.yml
vendored
Normal file
|
@ -0,0 +1,26 @@
|
|||
name: (push-master) Update Docker Hub README
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'docker/README.md'
|
||||
|
||||
jobs:
|
||||
update-docker-hub-readme:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Check out the repo
|
||||
uses: actions/checkout@v4
|
||||
if: ${{ github.repository_owner == 'crowdsecurity' }}
|
||||
-
|
||||
name: Update docker hub README
|
||||
uses: ms-jpq/sync-dockerhub-readme@v1
|
||||
if: ${{ github.repository_owner == 'crowdsecurity' }}
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
repository: crowdsecurity/crowdsec
|
||||
readme: "./docker/README.md"
|
55
.gitignore
vendored
55
.gitignore
vendored
|
@ -4,12 +4,59 @@
|
|||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*~
|
||||
.pc
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
# IDEs
|
||||
.vscode
|
||||
.idea
|
||||
|
||||
# If vendor is included, allow prebuilt (wasm?) libraries.
|
||||
!vendor/**/*.so
|
||||
|
||||
# Test binaries, built with `go test -c`
|
||||
*.test
|
||||
*.cover
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
# Test dependencies
|
||||
test/tools/*
|
||||
|
||||
# VMs used for dev/test
|
||||
|
||||
.vagrant
|
||||
|
||||
# Test binaries, built from *_test.go
|
||||
pkg/csplugin/tests/cs_plugin_test*
|
||||
|
||||
# Output of go-acc, go -cover
|
||||
*.out
|
||||
test/coverage/*
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
# Development artifacts, backups, etc
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Dependencies are not vendored by default, but a tarball is created by "make vendor"
|
||||
# and provided in the release. Used by gentoo, etc.
|
||||
vendor/
|
||||
vendor.tgz
|
||||
|
||||
# crowdsec binaries
|
||||
cmd/crowdsec-cli/cscli
|
||||
cmd/crowdsec/crowdsec
|
||||
cmd/notification-*/notification-*
|
||||
|
||||
# Test cache (downloaded files)
|
||||
.cache
|
||||
|
||||
# Release stuff
|
||||
crowdsec-v*
|
||||
msi
|
||||
*.msi
|
||||
**/*.nupkg
|
||||
*.tgz
|
||||
|
||||
# Python
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
*.egg-info
|
||||
|
|
16
.gitmodules
vendored
Normal file
16
.gitmodules
vendored
Normal file
|
@ -0,0 +1,16 @@
|
|||
[submodule "tests/lib/bats-core"]
|
||||
path = test/lib/bats-core
|
||||
url = https://github.com/crowdsecurity/bats-core.git
|
||||
branch = v1.7.0
|
||||
[submodule "tests/lib/bats-file"]
|
||||
path = test/lib/bats-file
|
||||
url = https://github.com/crowdsecurity/bats-file.git
|
||||
[submodule "tests/lib/bats-assert"]
|
||||
path = test/lib/bats-assert
|
||||
url = https://github.com/crowdsecurity/bats-assert.git
|
||||
[submodule "tests/lib/bats-support"]
|
||||
path = test/lib/bats-support
|
||||
url = https://github.com/crowdsecurity/bats-support.git
|
||||
[submodule "tests/lib/bats-mock"]
|
||||
path = test/lib/bats-mock
|
||||
url = https://github.com/crowdsecurity/bats-mock.git
|
382
.golangci.yml
Normal file
382
.golangci.yml
Normal file
|
@ -0,0 +1,382 @@
|
|||
# https://github.com/golangci/golangci-lint/blob/master/.golangci.reference.yml
|
||||
|
||||
linters-settings:
|
||||
cyclop:
|
||||
# lower this after refactoring
|
||||
max-complexity: 48
|
||||
|
||||
gci:
|
||||
sections:
|
||||
- standard
|
||||
- default
|
||||
- prefix(github.com/crowdsecurity)
|
||||
- prefix(github.com/crowdsecurity/crowdsec)
|
||||
|
||||
gomoddirectives:
|
||||
replace-allow-list:
|
||||
- golang.org/x/time/rate
|
||||
|
||||
gocognit:
|
||||
# lower this after refactoring
|
||||
min-complexity: 145
|
||||
|
||||
gocyclo:
|
||||
# lower this after refactoring
|
||||
min-complexity: 48
|
||||
|
||||
funlen:
|
||||
# Checks the number of lines in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 60
|
||||
# lower this after refactoring
|
||||
lines: 437
|
||||
# Checks the number of statements in a function.
|
||||
# If lower than 0, disable the check.
|
||||
# Default: 40
|
||||
# lower this after refactoring
|
||||
statements: 122
|
||||
|
||||
govet:
|
||||
enable-all: true
|
||||
disable:
|
||||
- reflectvaluecompare
|
||||
- fieldalignment
|
||||
|
||||
lll:
|
||||
# lower this after refactoring
|
||||
line-length: 2607
|
||||
|
||||
maintidx:
|
||||
# raise this after refactoring
|
||||
under: 11
|
||||
|
||||
misspell:
|
||||
locale: US
|
||||
|
||||
nestif:
|
||||
# lower this after refactoring
|
||||
min-complexity: 28
|
||||
|
||||
nlreturn:
|
||||
block-size: 5
|
||||
|
||||
nolintlint:
|
||||
allow-unused: false # report any unused nolint directives
|
||||
require-explanation: false # don't require an explanation for nolint directives
|
||||
require-specific: false # don't require nolint directives to be specific about which linter is being skipped
|
||||
|
||||
interfacebloat:
|
||||
max: 12
|
||||
|
||||
depguard:
|
||||
rules:
|
||||
wrap:
|
||||
deny:
|
||||
- pkg: "github.com/pkg/errors"
|
||||
desc: "errors.Wrap() is deprecated in favor of fmt.Errorf()"
|
||||
files:
|
||||
- "!**/pkg/database/*.go"
|
||||
- "!**/pkg/exprhelpers/*.go"
|
||||
- "!**/pkg/acquisition/modules/appsec/appsec.go"
|
||||
- "!**/pkg/acquisition/modules/loki/internal/lokiclient/loki_client.go"
|
||||
- "!**/pkg/apiserver/controllers/v1/errors.go"
|
||||
yaml:
|
||||
files:
|
||||
- "!**/pkg/acquisition/acquisition.go"
|
||||
- "!**/pkg/acquisition/acquisition_test.go"
|
||||
- "!**/pkg/acquisition/modules/appsec/appsec.go"
|
||||
- "!**/pkg/acquisition/modules/cloudwatch/cloudwatch.go"
|
||||
- "!**/pkg/acquisition/modules/docker/docker.go"
|
||||
- "!**/pkg/acquisition/modules/file/file.go"
|
||||
- "!**/pkg/acquisition/modules/journalctl/journalctl.go"
|
||||
- "!**/pkg/acquisition/modules/kafka/kafka.go"
|
||||
- "!**/pkg/acquisition/modules/kinesis/kinesis.go"
|
||||
- "!**/pkg/acquisition/modules/kubernetesaudit/k8s_audit.go"
|
||||
- "!**/pkg/acquisition/modules/loki/loki.go"
|
||||
- "!**/pkg/acquisition/modules/loki/timestamp_test.go"
|
||||
- "!**/pkg/acquisition/modules/s3/s3.go"
|
||||
- "!**/pkg/acquisition/modules/syslog/syslog.go"
|
||||
- "!**/pkg/acquisition/modules/wineventlog/wineventlog_windows.go"
|
||||
- "!**/pkg/appsec/appsec.go"
|
||||
- "!**/pkg/appsec/loader.go"
|
||||
- "!**/pkg/csplugin/broker.go"
|
||||
- "!**/pkg/csplugin/broker_test.go"
|
||||
- "!**/pkg/dumps/bucket_dump.go"
|
||||
- "!**/pkg/dumps/parser_dump.go"
|
||||
- "!**/pkg/hubtest/coverage.go"
|
||||
- "!**/pkg/hubtest/hubtest_item.go"
|
||||
- "!**/pkg/hubtest/parser_assert.go"
|
||||
- "!**/pkg/hubtest/scenario_assert.go"
|
||||
- "!**/pkg/leakybucket/buckets_test.go"
|
||||
- "!**/pkg/leakybucket/manager_load.go"
|
||||
- "!**/pkg/metabase/metabase.go"
|
||||
- "!**/pkg/parser/node.go"
|
||||
- "!**/pkg/parser/node_test.go"
|
||||
- "!**/pkg/parser/parsing_test.go"
|
||||
- "!**/pkg/parser/stage.go"
|
||||
deny:
|
||||
- pkg: "gopkg.in/yaml.v2"
|
||||
desc: "yaml.v2 is deprecated for new code in favor of yaml.v3"
|
||||
|
||||
wsl:
|
||||
# Allow blocks to end with comments
|
||||
allow-trailing-comment: true
|
||||
|
||||
linters:
|
||||
enable-all: true
|
||||
disable:
|
||||
#
|
||||
# DEPRECATED by golangi-lint
|
||||
#
|
||||
- deadcode
|
||||
- exhaustivestruct
|
||||
- golint
|
||||
- ifshort
|
||||
- interfacer
|
||||
- maligned
|
||||
- nosnakecase
|
||||
- scopelint
|
||||
- structcheck
|
||||
- varcheck
|
||||
|
||||
#
|
||||
# Disabled until fixed for go 1.22
|
||||
#
|
||||
|
||||
- copyloopvar # copyloopvar is a linter detects places where loop variables are copied
|
||||
- intrange # intrange is a linter to find places where for loops could make use of an integer range.
|
||||
|
||||
#
|
||||
# Enabled
|
||||
#
|
||||
|
||||
# - asasalint # check for pass []any as any in variadic func(...any)
|
||||
# - asciicheck # checks that all code identifiers does not have non-ASCII symbols in the name
|
||||
# - bidichk # Checks for dangerous unicode character sequences
|
||||
# - bodyclose # checks whether HTTP response body is closed successfully
|
||||
# - cyclop # checks function and package cyclomatic complexity
|
||||
# - decorder # check declaration order and count of types, constants, variables and functions
|
||||
# - depguard # Go linter that checks if package imports are in a list of acceptable packages
|
||||
# - dupword # checks for duplicate words in the source code
|
||||
# - durationcheck # check for two durations multiplied together
|
||||
# - errcheck # errcheck is a program for checking for unchecked errors in Go code. These unchecked errors can be critical bugs in some cases
|
||||
# - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
|
||||
# - execinquery # execinquery is a linter about query string checker in Query function which reads your Go src files and warning it finds
|
||||
# - exportloopref # checks for pointers to enclosing loop variables
|
||||
# - funlen # Tool for detection of long functions
|
||||
# - ginkgolinter # enforces standards of using ginkgo and gomega
|
||||
# - gocheckcompilerdirectives # Checks that go compiler directive comments (//go:) are valid.
|
||||
# - gochecknoinits # Checks that no init functions are present in Go code
|
||||
# - gochecksumtype # Run exhaustiveness checks on Go "sum types"
|
||||
# - gocognit # Computes and checks the cognitive complexity of functions
|
||||
# - gocritic # Provides diagnostics that check for bugs, performance and style issues.
|
||||
# - gocyclo # Computes and checks the cyclomatic complexity of functions
|
||||
# - goheader # Checks is file header matches to pattern
|
||||
# - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
|
||||
# - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
|
||||
# - goprintffuncname # Checks that printf-like functions are named with `f` at the end
|
||||
# - gosimple # (megacheck): Linter for Go source code that specializes in simplifying code
|
||||
# - gosmopolitan # Report certain i18n/l10n anti-patterns in your Go codebase
|
||||
# - govet # (vet, vetshadow): Vet examines Go source code and reports suspicious constructs. It is roughly the same as 'go vet' and uses its passes.
|
||||
# - grouper # Analyze expression groups.
|
||||
# - importas # Enforces consistent import aliases
|
||||
# - ineffassign # Detects when assignments to existing variables are not used
|
||||
# - interfacebloat # A linter that checks the number of methods inside an interface.
|
||||
# - lll # Reports long lines
|
||||
# - loggercheck # (logrlint): Checks key value pairs for common logger libraries (kitlog,klog,logr,zap).
|
||||
# - logrlint # Check logr arguments.
|
||||
# - maintidx # maintidx measures the maintainability index of each function.
|
||||
# - makezero # Finds slice declarations with non-zero initial length
|
||||
# - mirror # reports wrong mirror patterns of bytes/strings usage
|
||||
# - misspell # Finds commonly misspelled English words
|
||||
# - nakedret # Checks that functions with naked returns are not longer than a maximum size (can be zero).
|
||||
# - nestif # Reports deeply nested if statements
|
||||
# - nilerr # Finds the code that returns nil even if it checks that the error is not nil.
|
||||
# - nolintlint # Reports ill-formed or insufficient nolint directives
|
||||
# - nonamedreturns # Reports all named returns
|
||||
# - nosprintfhostport # Checks for misuse of Sprintf to construct a host with port in a URL.
|
||||
# - perfsprint # Checks that fmt.Sprintf can be replaced with a faster alternative.
|
||||
# - predeclared # find code that shadows one of Go's predeclared identifiers
|
||||
# - reassign # Checks that package variables are not reassigned
|
||||
# - rowserrcheck # checks whether Rows.Err of rows is checked successfully
|
||||
# - sloglint # ensure consistent code style when using log/slog
|
||||
# - spancheck # Checks for mistakes with OpenTelemetry/Census spans.
|
||||
# - sqlclosecheck # Checks that sql.Rows, sql.Stmt, sqlx.NamedStmt, pgx.Query are closed.
|
||||
# - staticcheck # (megacheck): It's a set of rules from staticcheck. It's not the same thing as the staticcheck binary. The author of staticcheck doesn't support or approve the use of staticcheck as a library inside golangci-lint.
|
||||
# - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17
|
||||
# - testableexamples # linter checks if examples are testable (have an expected output)
|
||||
# - testifylint # Checks usage of github.com/stretchr/testify.
|
||||
# - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes
|
||||
# - unconvert # Remove unnecessary type conversions
|
||||
# - unused # (megacheck): Checks Go code for unused constants, variables, functions and types
|
||||
# - usestdlibvars # A linter that detect the possibility to use variables/constants from the Go standard library.
|
||||
# - wastedassign # Finds wasted assignment statements
|
||||
# - zerologlint # Detects the wrong usage of `zerolog` that a user forgets to dispatch with `Send` or `Msg`
|
||||
|
||||
#
|
||||
# Recommended? (easy)
|
||||
#
|
||||
|
||||
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
|
||||
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and reports occations, where the check for the returned error can be omitted.
|
||||
- exhaustive # check exhaustiveness of enum switch statements
|
||||
- gci # Gci control golang package import order and make it always deterministic.
|
||||
- godot # Check if comments end in a period
|
||||
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
|
||||
- goimports # Check import statements are formatted according to the 'goimport' command. Reformat imports in autofix mode.
|
||||
- gosec # (gas): Inspects source code for security problems
|
||||
- inamedparam # reports interfaces with unnamed method parameters
|
||||
- musttag # enforce field tags in (un)marshaled structs
|
||||
- promlinter # Check Prometheus metrics naming via promlint
|
||||
- protogetter # Reports direct reads from proto message fields when getters should be used
|
||||
- revive # Fast, configurable, extensible, flexible, and beautiful linter for Go. Drop-in replacement of golint.
|
||||
- tagalign # check that struct tags are well aligned
|
||||
- thelper # thelper detects tests helpers which is not start with t.Helper() method.
|
||||
- wrapcheck # Checks that errors returned from external packages are wrapped
|
||||
|
||||
#
|
||||
# Recommended? (requires some work)
|
||||
#
|
||||
|
||||
- containedctx # containedctx is a linter that detects struct contained context.Context field
|
||||
- contextcheck # check whether the function uses a non-inherited context
|
||||
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.
|
||||
- gomnd # An analyzer to detect magic numbers.
|
||||
- ireturn # Accept Interfaces, Return Concrete Types
|
||||
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value.
|
||||
- noctx # Finds sending http request without context.Context
|
||||
- unparam # Reports unused function parameters
|
||||
|
||||
#
|
||||
# Formatting only, useful in IDE but should not be forced on CI?
|
||||
#
|
||||
|
||||
- gofumpt # Gofumpt checks whether code was gofumpt-ed.
|
||||
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
|
||||
- whitespace # Whitespace is a linter that checks for unnecessary newlines at the start and end of functions, if, for, etc.
|
||||
- wsl # add or remove empty lines
|
||||
|
||||
#
|
||||
# Well intended, but not ready for this
|
||||
#
|
||||
- dupl # Tool for code clone detection
|
||||
- forcetypeassert # finds forced type assertions
|
||||
- godox # Tool for detection of FIXME, TODO and other comment keywords
|
||||
- goerr113 # Go linter to check the errors handling expressions
|
||||
- paralleltest # Detects missing usage of t.Parallel() method in your Go test
|
||||
- testpackage # linter that makes you use a separate _test package
|
||||
|
||||
#
|
||||
# Too strict / too many false positives (for now?)
|
||||
#
|
||||
- exhaustruct # Checks if all structure fields are initialized
|
||||
- forbidigo # Forbids identifiers
|
||||
- gochecknoglobals # Check that no global variables exist.
|
||||
- goconst # Finds repeated strings that could be replaced by a constant
|
||||
- stylecheck # Stylecheck is a replacement for golint
|
||||
- tagliatelle # Checks the struct tags.
|
||||
- varnamelen # checks that the length of a variable's name matches its scope
|
||||
|
||||
#
|
||||
# Under evaluation
|
||||
#
|
||||
|
||||
- prealloc # Finds slice declarations that could potentially be preallocated
|
||||
|
||||
|
||||
issues:
|
||||
# “Look, that’s why there’s rules, understand? So that you think before you
|
||||
# break ‘em.” ― Terry Pratchett
|
||||
|
||||
exclude-dirs:
|
||||
- pkg/time/rate
|
||||
|
||||
exclude-files:
|
||||
- pkg/yamlpatch/merge.go
|
||||
- pkg/yamlpatch/merge_test.go
|
||||
|
||||
exclude-generated-strict: true
|
||||
|
||||
max-issues-per-linter: 0
|
||||
max-same-issues: 0
|
||||
exclude-rules:
|
||||
|
||||
# Won't fix:
|
||||
|
||||
# `err` is often shadowed, we may continue to do it
|
||||
- linters:
|
||||
- govet
|
||||
text: "shadow: declaration of \"err\" shadows declaration"
|
||||
|
||||
- linters:
|
||||
- errcheck
|
||||
text: "Error return value of `.*` is not checked"
|
||||
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "ifElseChain: rewrite if-else to switch statement"
|
||||
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "captLocal: `.*' should not be capitalized"
|
||||
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "appendAssign: append result not assigned to the same slice"
|
||||
|
||||
- linters:
|
||||
- gocritic
|
||||
text: "commentFormatting: put a space between `//` and comment text"
|
||||
|
||||
# Will fix, trivial - just beware of merge conflicts
|
||||
|
||||
- linters:
|
||||
- perfsprint
|
||||
text: "fmt.Sprintf can be replaced .*"
|
||||
|
||||
- linters:
|
||||
- perfsprint
|
||||
text: "fmt.Errorf can be replaced with errors.New"
|
||||
|
||||
#
|
||||
# Will fix, easy but some neurons required
|
||||
#
|
||||
|
||||
- linters:
|
||||
- errorlint
|
||||
text: "non-wrapping format verb for fmt.Errorf. Use `%w` to format errors"
|
||||
|
||||
- linters:
|
||||
- errorlint
|
||||
text: "type assertion on error will fail on wrapped errors. Use errors.As to check for specific errors"
|
||||
|
||||
- linters:
|
||||
- errorlint
|
||||
text: "type switch on error will fail on wrapped errors. Use errors.As to check for specific errors"
|
||||
|
||||
- linters:
|
||||
- errorlint
|
||||
text: "type assertion on error will fail on wrapped errors. Use errors.Is to check for specific errors"
|
||||
|
||||
- linters:
|
||||
- errorlint
|
||||
text: "comparing with .* will fail on wrapped errors. Use errors.Is to check for a specific error"
|
||||
|
||||
- linters:
|
||||
- errorlint
|
||||
text: "switch on an error will fail on wrapped errors. Use errors.Is to check for specific errors"
|
||||
|
||||
- linters:
|
||||
- nosprintfhostport
|
||||
text: "host:port in url should be constructed with net.JoinHostPort and not directly with fmt.Sprintf"
|
||||
|
||||
# https://github.com/timakin/bodyclose
|
||||
- linters:
|
||||
- bodyclose
|
||||
text: "response body must be closed"
|
||||
|
||||
# named/naked returns are evil, with a single exception
|
||||
# https://go.dev/wiki/CodeReviewComments#named-result-parameters
|
||||
- linters:
|
||||
- nonamedreturns
|
||||
text: "named return .* with type .* found"
|
43
.yamllint
Normal file
43
.yamllint
Normal file
|
@ -0,0 +1,43 @@
|
|||
---
|
||||
rules:
|
||||
braces:
|
||||
min-spaces-inside: 0
|
||||
max-spaces-inside: 1
|
||||
brackets:
|
||||
min-spaces-inside: 0
|
||||
max-spaces-inside: 1
|
||||
colons:
|
||||
max-spaces-before: 0
|
||||
max-spaces-after: 1
|
||||
commas:
|
||||
max-spaces-before: 0
|
||||
min-spaces-after: 1
|
||||
max-spaces-after: 1
|
||||
comments:
|
||||
level: warning
|
||||
require-starting-space: true
|
||||
min-spaces-from-content: 2
|
||||
comments-indentation:
|
||||
level: warning
|
||||
document-end: disable
|
||||
document-start: disable
|
||||
empty-lines:
|
||||
max: 2
|
||||
max-start: 0
|
||||
max-end: 0
|
||||
hyphens:
|
||||
max-spaces-after: 1
|
||||
indentation:
|
||||
spaces: consistent
|
||||
indent-sequences: whatever
|
||||
check-multi-line-strings: false
|
||||
key-duplicates: enable
|
||||
line-length:
|
||||
max: 180
|
||||
allow-non-breakable-words: true
|
||||
allow-non-breakable-inline-mappings: false
|
||||
new-line-at-end-of-file: enable
|
||||
new-lines:
|
||||
type: unix
|
||||
trailing-spaces: enable
|
||||
truthy: disable
|
3
CONTRIBUTING.md
Normal file
3
CONTRIBUTING.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
|
||||
Please refer to [Contributing to CrowdSec](https://doc.crowdsec.net/docs/next/contributing/getting_started).
|
||||
|
63
Dockerfile
Normal file
63
Dockerfile
Normal file
|
@ -0,0 +1,63 @@
|
|||
# vim: set ft=dockerfile:
|
||||
FROM golang:1.22.2-alpine3.18 AS build
|
||||
|
||||
ARG BUILD_VERSION
|
||||
|
||||
WORKDIR /go/src/crowdsec
|
||||
|
||||
# We like to choose the release of re2 to use, and Alpine does not ship a static version anyway.
|
||||
ENV RE2_VERSION=2023-03-01
|
||||
ENV BUILD_VERSION=${BUILD_VERSION}
|
||||
|
||||
# wizard.sh requires GNU coreutils
|
||||
RUN apk add --no-cache git g++ gcc libc-dev make bash gettext binutils-gold coreutils pkgconfig && \
|
||||
wget https://github.com/google/re2/archive/refs/tags/${RE2_VERSION}.tar.gz && \
|
||||
tar -xzf ${RE2_VERSION}.tar.gz && \
|
||||
cd re2-${RE2_VERSION} && \
|
||||
make install && \
|
||||
echo "githubciXXXXXXXXXXXXXXXXXXXXXXXX" > /etc/machine-id && \
|
||||
go install github.com/mikefarah/yq/v4@v4.43.1
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN make clean release DOCKER_BUILD=1 BUILD_STATIC=1 && \
|
||||
cd crowdsec-v* && \
|
||||
./wizard.sh --docker-mode && \
|
||||
cd - >/dev/null && \
|
||||
cscli hub update && \
|
||||
cscli collections install crowdsecurity/linux && \
|
||||
cscli parsers install crowdsecurity/whitelists
|
||||
|
||||
# In case we need to remove agents here..
|
||||
# cscli machines list -o json | yq '.[].machineId' | xargs -r cscli machines delete
|
||||
|
||||
FROM alpine:latest as slim
|
||||
|
||||
RUN apk add --no-cache --repository=http://dl-cdn.alpinelinux.org/alpine/edge/community tzdata bash rsync && \
|
||||
mkdir -p /staging/etc/crowdsec && \
|
||||
mkdir -p /staging/etc/crowdsec/acquis.d && \
|
||||
mkdir -p /staging/var/lib/crowdsec && \
|
||||
mkdir -p /var/lib/crowdsec/data
|
||||
|
||||
COPY --from=build /go/bin/yq /usr/local/bin/crowdsec /usr/local/bin/cscli /usr/local/bin/
|
||||
COPY --from=build /etc/crowdsec /staging/etc/crowdsec
|
||||
COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
|
||||
COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
|
||||
COPY --from=build /var/lib/crowdsec /staging/var/lib/crowdsec
|
||||
RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml
|
||||
|
||||
ENTRYPOINT /bin/bash /docker_start.sh
|
||||
|
||||
FROM slim as full
|
||||
|
||||
# Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
|
||||
# The files are here for reference, as users will need to mount a new version to be actually able to use notifications
|
||||
COPY --from=build \
|
||||
/go/src/crowdsec/cmd/notification-email/email.yaml \
|
||||
/go/src/crowdsec/cmd/notification-http/http.yaml \
|
||||
/go/src/crowdsec/cmd/notification-slack/slack.yaml \
|
||||
/go/src/crowdsec/cmd/notification-splunk/splunk.yaml \
|
||||
/go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml \
|
||||
/staging/etc/crowdsec/notifications/
|
||||
|
||||
COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
|
87
Dockerfile.debian
Normal file
87
Dockerfile.debian
Normal file
|
@ -0,0 +1,87 @@
|
|||
# vim: set ft=dockerfile:
|
||||
FROM golang:1.22.2-bookworm AS build
|
||||
|
||||
ARG BUILD_VERSION
|
||||
|
||||
WORKDIR /go/src/crowdsec
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV DEBCONF_NOWARNINGS="yes"
|
||||
|
||||
# We like to choose the release of re2 to use, the debian version is usually older.
|
||||
ENV RE2_VERSION=2023-03-01
|
||||
ENV BUILD_VERSION=${BUILD_VERSION}
|
||||
|
||||
# wizard.sh requires GNU coreutils
|
||||
RUN apt-get update && \
|
||||
apt-get install -y -q git gcc libc-dev make bash gettext binutils-gold coreutils tzdata && \
|
||||
wget https://github.com/google/re2/archive/refs/tags/${RE2_VERSION}.tar.gz && \
|
||||
tar -xzf ${RE2_VERSION}.tar.gz && \
|
||||
cd re2-${RE2_VERSION} && \
|
||||
make && \
|
||||
make install && \
|
||||
echo "githubciXXXXXXXXXXXXXXXXXXXXXXXX" > /etc/machine-id && \
|
||||
go install github.com/mikefarah/yq/v4@v4.43.1
|
||||
|
||||
COPY . .
|
||||
|
||||
RUN make clean release DOCKER_BUILD=1 BUILD_STATIC=1 && \
|
||||
cd crowdsec-v* && \
|
||||
./wizard.sh --docker-mode && \
|
||||
cd - >/dev/null && \
|
||||
cscli hub update && \
|
||||
cscli collections install crowdsecurity/linux && \
|
||||
cscli parsers install crowdsecurity/whitelists
|
||||
|
||||
# In case we need to remove agents here..
|
||||
# cscli machines list -o json | yq '.[].machineId' | xargs -r cscli machines delete
|
||||
|
||||
FROM debian:bookworm-slim as slim
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
ENV DEBCONF_NOWARNINGS="yes"
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y -q --install-recommends --no-install-suggests \
|
||||
procps \
|
||||
systemd \
|
||||
iproute2 \
|
||||
ca-certificates \
|
||||
bash \
|
||||
tzdata \
|
||||
rsync && \
|
||||
mkdir -p /staging/etc/crowdsec && \
|
||||
mkdir -p /staging/etc/crowdsec/acquis.d && \
|
||||
mkdir -p /staging/var/lib/crowdsec && \
|
||||
mkdir -p /var/lib/crowdsec/data
|
||||
|
||||
COPY --from=build /go/bin/yq /usr/local/bin/crowdsec /usr/local/bin/cscli /usr/local/bin/
|
||||
COPY --from=build /etc/crowdsec /staging/etc/crowdsec
|
||||
COPY --from=build /go/src/crowdsec/docker/docker_start.sh /
|
||||
COPY --from=build /go/src/crowdsec/docker/config.yaml /staging/etc/crowdsec/config.yaml
|
||||
RUN yq -n '.url="http://0.0.0.0:8080"' | install -m 0600 /dev/stdin /staging/etc/crowdsec/local_api_credentials.yaml && \
|
||||
yq eval -i ".plugin_config.group = \"nogroup\"" /staging/etc/crowdsec/config.yaml
|
||||
|
||||
ENTRYPOINT /bin/bash docker_start.sh
|
||||
|
||||
FROM slim as plugins
|
||||
|
||||
# Due to the wizard using cp -n, we have to copy the config files directly from the source as -n does not exist in busybox cp
|
||||
# The files are here for reference, as users will need to mount a new version to be actually able to use notifications
|
||||
COPY --from=build \
|
||||
/go/src/crowdsec/cmd/notification-email/email.yaml \
|
||||
/go/src/crowdsec/cmd/notification-http/http.yaml \
|
||||
/go/src/crowdsec/cmd/notification-slack/slack.yaml \
|
||||
/go/src/crowdsec/cmd/notification-splunk/splunk.yaml \
|
||||
/go/src/crowdsec/cmd/notification-sentinel/sentinel.yaml \
|
||||
/staging/etc/crowdsec/notifications/
|
||||
|
||||
COPY --from=build /usr/local/lib/crowdsec/plugins /usr/local/lib/crowdsec/plugins
|
||||
|
||||
FROM slim as geoip
|
||||
|
||||
COPY --from=build /var/lib/crowdsec /staging/var/lib/crowdsec
|
||||
|
||||
FROM plugins as full
|
||||
|
||||
COPY --from=build /var/lib/crowdsec /staging/var/lib/crowdsec
|
2
LICENSE
2
LICENSE
|
@ -1,6 +1,6 @@
|
|||
MIT License
|
||||
|
||||
Copyright (c) 2020 crowdsecurity
|
||||
Copyright (c) 2020-2023 Crowdsec
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
|
|
362
Makefile
362
Makefile
|
@ -1,118 +1,304 @@
|
|||
PREFIX?="/tmp/crowdsec/"
|
||||
CFG_PREFIX = $(PREFIX)"/etc/crowdsec/"
|
||||
BIN_PREFIX = $(PREFIX)"/usr/local/bin/"
|
||||
DATA_PREFIX = $(PREFIX)"/var/run/crowdsec/"
|
||||
include mk/platform.mk
|
||||
include mk/gmsl
|
||||
|
||||
PLUGIN_FOLDER="./plugins"
|
||||
PID_DIR = $(PREFIX)"/var/run/"
|
||||
CROWDSEC_FOLDER = "./cmd/crowdsec"
|
||||
CSCLI_FOLDER = "./cmd/crowdsec-cli/"
|
||||
CROWDSEC_BIN = "crowdsec"
|
||||
CSCLI_BIN = "cscli"
|
||||
BUILD_CMD="build"
|
||||
# By default, this build requires the C++ re2 library to be installed.
|
||||
#
|
||||
# Debian/Ubuntu: apt install libre2-dev
|
||||
# Fedora/CentOS: dnf install re2-devel
|
||||
# FreeBSD: pkg install re2
|
||||
# Alpine: apk add re2-dev
|
||||
# Windows: choco install re2
|
||||
# MacOS: brew install re2
|
||||
|
||||
GOARCH=amd64
|
||||
GOOS=linux
|
||||
REQUIRE_GOVERSION="1.13"
|
||||
# To build without re2, run "make BUILD_RE2_WASM=1"
|
||||
# The WASM version is slower and introduces a short delay when starting a process
|
||||
# (including cscli) so it is not recommended for production use.
|
||||
BUILD_RE2_WASM ?= 0
|
||||
|
||||
# To build static binaries, run "make BUILD_STATIC=1".
|
||||
# On some platforms, this requires additional packages
|
||||
# (e.g. glibc-static and libstdc++-static on fedora, centos.. which are on the powertools/crb repository).
|
||||
# If the static build fails at the link stage, it might be because the static library is not provided
|
||||
# for your distribution (look for libre2.a). See the Dockerfile for an example of how to build it.
|
||||
BUILD_STATIC ?= 0
|
||||
|
||||
#Current versioning information from env
|
||||
export BUILD_VERSION=$(shell cat RELEASE.json | jq -r .Version)
|
||||
export BUILD_GOVERSION="$(shell go version | cut -d " " -f3 | sed -r 's/[go]+//g')"
|
||||
export BUILD_CODENAME=$(shell cat RELEASE.json | jq -r .CodeName)
|
||||
export BUILD_TIMESTAMP=$(shell date +%F"_"%T)
|
||||
export BUILD_TAG="$(shell git rev-parse HEAD)"
|
||||
export LD_OPTS=-ldflags "-X github.com/crowdsecurity/crowdsec/pkg/cwversion.Version=$(BUILD_VERSION) \
|
||||
-X github.com/crowdsecurity/crowdsec/pkg/cwversion.BuildDate=$(BUILD_TIMESTAMP) \
|
||||
-X github.com/crowdsecurity/crowdsec/pkg/cwversion.Codename=$(BUILD_CODENAME) \
|
||||
-X github.com/crowdsecurity/crowdsec/pkg/cwversion.Tag=$(BUILD_TAG) \
|
||||
-X github.com/crowdsecurity/crowdsec/pkg/cwversion.GoVersion=$(BUILD_GOVERSION)"
|
||||
# List of plugins to build
|
||||
PLUGINS ?= $(patsubst ./cmd/notification-%,%,$(wildcard ./cmd/notification-*))
|
||||
|
||||
# Can be overriden, if you can deal with the consequences
|
||||
BUILD_REQUIRE_GO_MAJOR ?= 1
|
||||
BUILD_REQUIRE_GO_MINOR ?= 21
|
||||
|
||||
#--------------------------------------
|
||||
|
||||
GO = go
|
||||
GOTEST = $(GO) test
|
||||
|
||||
BUILD_CODENAME ?= alphaga
|
||||
|
||||
CROWDSEC_FOLDER = ./cmd/crowdsec
|
||||
CSCLI_FOLDER = ./cmd/crowdsec-cli/
|
||||
PLUGINS_DIR_PREFIX = ./cmd/notification-
|
||||
|
||||
CROWDSEC_BIN = crowdsec$(EXT)
|
||||
CSCLI_BIN = cscli$(EXT)
|
||||
|
||||
# semver comparison to select the hub branch requires the version to start with "v"
|
||||
ifneq ($(call substr,$(BUILD_VERSION),1,1),v)
|
||||
$(error BUILD_VERSION "$(BUILD_VERSION)" should start with "v")
|
||||
endif
|
||||
|
||||
# Directory for the release files
|
||||
RELDIR = crowdsec-$(BUILD_VERSION)
|
||||
|
||||
all: clean test build
|
||||
GO_MODULE_NAME = github.com/crowdsecurity/crowdsec
|
||||
|
||||
build: clean goversion crowdsec cscli
|
||||
# Check if a given value is considered truthy and returns "0" or "1".
|
||||
# A truthy value is one of the following: "1", "yes", or "true", case-insensitive.
|
||||
#
|
||||
# Usage:
|
||||
# ifeq ($(call bool,$(FOO)),1)
|
||||
# $(info Let's foo)
|
||||
# endif
|
||||
bool = $(if $(filter $(call lc, $1),1 yes true),1,0)
|
||||
|
||||
static: goversion crowdsec_static cscli_static
|
||||
#--------------------------------------
|
||||
#
|
||||
# Define MAKE_FLAGS and LD_OPTS for the sub-makefiles in cmd/
|
||||
#
|
||||
|
||||
goversion:
|
||||
CURRENT_GOVERSION="$(shell go version | cut -d " " -f3 | sed -r 's/[go]+//g')"
|
||||
RESPECT_VERSION="$(shell echo "$(CURRENT_GOVERSION),$(REQUIRE_GOVERSION)" | tr ',' '\n' | sort -V)"
|
||||
MAKE_FLAGS = --no-print-directory GOARCH=$(GOARCH) GOOS=$(GOOS) RM="$(RM)" WIN_IGNORE_ERR="$(WIN_IGNORE_ERR)" CP="$(CP)" CPR="$(CPR)" MKDIR="$(MKDIR)"
|
||||
|
||||
clean:
|
||||
@make -C $(CROWDSEC_FOLDER) clean --no-print-directory
|
||||
@make -C $(CSCLI_FOLDER) clean --no-print-directory
|
||||
@rm -f $(CROWDSEC_BIN)
|
||||
@rm -f $(CSCLI_BIN)
|
||||
@rm -f *.log
|
||||
LD_OPTS_VARS= \
|
||||
-X 'github.com/crowdsecurity/go-cs-lib/version.Version=$(BUILD_VERSION)' \
|
||||
-X 'github.com/crowdsecurity/go-cs-lib/version.BuildDate=$(BUILD_TIMESTAMP)' \
|
||||
-X 'github.com/crowdsecurity/go-cs-lib/version.Tag=$(BUILD_TAG)' \
|
||||
-X '$(GO_MODULE_NAME)/pkg/cwversion.Codename=$(BUILD_CODENAME)' \
|
||||
-X '$(GO_MODULE_NAME)/pkg/csconfig.defaultConfigDir=$(DEFAULT_CONFIGDIR)' \
|
||||
-X '$(GO_MODULE_NAME)/pkg/csconfig.defaultDataDir=$(DEFAULT_DATADIR)'
|
||||
|
||||
cscli:
|
||||
ifeq ($(lastword $(RESPECT_VERSION)), $(CURRENT_GOVERSION))
|
||||
@make -C $(CSCLI_FOLDER) build --no-print-directory
|
||||
else
|
||||
@echo "Required golang version is $(REQUIRE_GOVERSION). The current one is $(CURRENT_GOVERSION). Exiting.."
|
||||
@exit 1;
|
||||
ifneq (,$(DOCKER_BUILD))
|
||||
LD_OPTS_VARS += -X '$(GO_MODULE_NAME)/pkg/cwversion.System=docker'
|
||||
endif
|
||||
|
||||
GO_TAGS := netgo,osusergo,sqlite_omit_load_extension
|
||||
|
||||
crowdsec:
|
||||
ifeq ($(lastword $(RESPECT_VERSION)), $(CURRENT_GOVERSION))
|
||||
@make -C $(CROWDSEC_FOLDER) build --no-print-directory
|
||||
else
|
||||
@echo "Required golang version is $(REQUIRE_GOVERSION). The current one is $(CURRENT_GOVERSION). Exiting.."
|
||||
@exit 1;
|
||||
# this will be used by Go in the make target, some distributions require it
|
||||
export PKG_CONFIG_PATH:=/usr/local/lib/pkgconfig:$(PKG_CONFIG_PATH)
|
||||
|
||||
ifeq ($(call bool,$(BUILD_RE2_WASM)),0)
|
||||
ifeq ($(PKG_CONFIG),)
|
||||
$(error "pkg-config is not available. Please install pkg-config.")
|
||||
endif
|
||||
|
||||
|
||||
cscli_static:
|
||||
ifeq ($(lastword $(RESPECT_VERSION)), $(CURRENT_GOVERSION))
|
||||
@make -C $(CSCLI_FOLDER) static --no-print-directory
|
||||
ifeq ($(RE2_CHECK),)
|
||||
RE2_FAIL := "libre2-dev is not installed, please install it or set BUILD_RE2_WASM=1 to use the WebAssembly version"
|
||||
else
|
||||
@echo "Required golang version is $(REQUIRE_GOVERSION). The current one is $(CURRENT_GOVERSION). Exiting.."
|
||||
@exit 1;
|
||||
# += adds a space that we don't want
|
||||
GO_TAGS := $(GO_TAGS),re2_cgo
|
||||
LD_OPTS_VARS += -X '$(GO_MODULE_NAME)/pkg/cwversion.Libre2=C++'
|
||||
endif
|
||||
endif
|
||||
|
||||
|
||||
crowdsec_static:
|
||||
ifeq ($(lastword $(RESPECT_VERSION)), $(CURRENT_GOVERSION))
|
||||
@make -C $(CROWDSEC_FOLDER) static --no-print-directory
|
||||
# Build static to avoid the runtime dependency on libre2.so
|
||||
ifeq ($(call bool,$(BUILD_STATIC)),1)
|
||||
BUILD_TYPE = static
|
||||
EXTLDFLAGS := -extldflags '-static'
|
||||
else
|
||||
@echo "Required golang version is $(REQUIRE_GOVERSION). The current one is $(CURRENT_GOVERSION). Exiting.."
|
||||
@exit 1;
|
||||
BUILD_TYPE = dynamic
|
||||
EXTLDFLAGS :=
|
||||
endif
|
||||
|
||||
|
||||
#.PHONY: test
|
||||
test:
|
||||
ifeq ($(lastword $(RESPECT_VERSION)), $(CURRENT_GOVERSION))
|
||||
@make -C $(CROWDSEC_FOLDER) test --no-print-directory
|
||||
# Build with debug symbols, and disable optimizations + inlining, to use Delve
|
||||
ifeq ($(call bool,$(DEBUG)),1)
|
||||
STRIP_SYMBOLS :=
|
||||
DISABLE_OPTIMIZATION := -gcflags "-N -l"
|
||||
else
|
||||
@echo "Required golang version is $(REQUIRE_GOVERSION). The current one is $(CURRENT_GOVERSION). Exiting.."
|
||||
@exit 1;
|
||||
STRIP_SYMBOLS := -s -w
|
||||
DISABLE_OPTIMIZATION :=
|
||||
endif
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall:
|
||||
@rm -rf "$(CFG_PREFIX)" || exit
|
||||
@rm -rf "$(DATA_PREFIX)" || exit
|
||||
@rm -rf "$(SYSTEMD_PATH_FILE)" || exit
|
||||
export LD_OPTS=-ldflags "$(STRIP_SYMBOLS) $(EXTLDFLAGS) $(LD_OPTS_VARS)" \
|
||||
-trimpath -tags $(GO_TAGS) $(DISABLE_OPTIMIZATION)
|
||||
|
||||
ifeq ($(call bool,$(TEST_COVERAGE)),1)
|
||||
LD_OPTS += -cover
|
||||
endif
|
||||
|
||||
#--------------------------------------
|
||||
|
||||
.PHONY: build
|
||||
build: pre-build goversion crowdsec cscli plugins ## Build crowdsec, cscli and plugins
|
||||
|
||||
.PHONY: pre-build
|
||||
pre-build: ## Sanity checks and build information
|
||||
$(info Building $(BUILD_VERSION) ($(BUILD_TAG)) $(BUILD_TYPE) for $(GOOS)/$(GOARCH))
|
||||
|
||||
ifneq (,$(RE2_FAIL))
|
||||
$(error $(RE2_FAIL))
|
||||
endif
|
||||
|
||||
ifneq (,$(RE2_CHECK))
|
||||
$(info Using C++ regexp library)
|
||||
else
|
||||
$(info Fallback to WebAssembly regexp library. To use the C++ version, make sure you have installed libre2-dev and pkg-config.)
|
||||
endif
|
||||
|
||||
ifeq ($(call bool,$(DEBUG)),1)
|
||||
$(info Building with debug symbols and disabled optimizations)
|
||||
endif
|
||||
|
||||
ifeq ($(call bool,$(TEST_COVERAGE)),1)
|
||||
$(info Test coverage collection enabled)
|
||||
endif
|
||||
|
||||
# intentional, empty line
|
||||
$(info )
|
||||
|
||||
.PHONY: all
|
||||
all: clean test build ## Clean, test and build (requires localstack)
|
||||
|
||||
.PHONY: plugins
|
||||
plugins: ## Build notification plugins
|
||||
@$(foreach plugin,$(PLUGINS), \
|
||||
$(MAKE) -C $(PLUGINS_DIR_PREFIX)$(plugin) build $(MAKE_FLAGS); \
|
||||
)
|
||||
|
||||
# same as "$(MAKE) -f debian/rules clean" but without the dependency on debhelper
|
||||
.PHONY: clean-debian
|
||||
clean-debian:
|
||||
@$(RM) -r debian/crowdsec
|
||||
@$(RM) -r debian/crowdsec
|
||||
@$(RM) -r debian/files
|
||||
@$(RM) -r debian/.debhelper
|
||||
@$(RM) -r debian/*.substvars
|
||||
@$(RM) -r debian/*-stamp
|
||||
|
||||
.PHONY: clean-rpm
|
||||
clean-rpm:
|
||||
@$(RM) -r rpm/BUILD
|
||||
@$(RM) -r rpm/BUILDROOT
|
||||
@$(RM) -r rpm/RPMS
|
||||
@$(RM) -r rpm/SOURCES/*.tar.gz
|
||||
@$(RM) -r rpm/SRPMS
|
||||
|
||||
.PHONY: clean
|
||||
clean: clean-debian clean-rpm testclean ## Remove build artifacts
|
||||
@$(MAKE) -C $(CROWDSEC_FOLDER) clean $(MAKE_FLAGS)
|
||||
@$(MAKE) -C $(CSCLI_FOLDER) clean $(MAKE_FLAGS)
|
||||
@$(RM) $(CROWDSEC_BIN) $(WIN_IGNORE_ERR)
|
||||
@$(RM) $(CSCLI_BIN) $(WIN_IGNORE_ERR)
|
||||
@$(RM) *.log $(WIN_IGNORE_ERR)
|
||||
@$(RM) crowdsec-release.tgz $(WIN_IGNORE_ERR)
|
||||
@$(foreach plugin,$(PLUGINS), \
|
||||
$(MAKE) -C $(PLUGINS_DIR_PREFIX)$(plugin) clean $(MAKE_FLAGS); \
|
||||
)
|
||||
|
||||
.PHONY: cscli
|
||||
cscli: goversion ## Build cscli
|
||||
@$(MAKE) -C $(CSCLI_FOLDER) build $(MAKE_FLAGS)
|
||||
|
||||
.PHONY: crowdsec
|
||||
crowdsec: goversion ## Build crowdsec
|
||||
@$(MAKE) -C $(CROWDSEC_FOLDER) build $(MAKE_FLAGS)
|
||||
|
||||
.PHONY: generate
|
||||
generate: ## Generate code for the database and APIs
|
||||
$(GO) generate ./pkg/database/ent
|
||||
$(GO) generate ./pkg/models
|
||||
|
||||
.PHONY: testclean
|
||||
testclean: bats-clean ## Remove test artifacts
|
||||
@$(RM) pkg/apiserver/ent $(WIN_IGNORE_ERR)
|
||||
@$(RM) pkg/cwhub/hubdir $(WIN_IGNORE_ERR)
|
||||
@$(RM) pkg/cwhub/install $(WIN_IGNORE_ERR)
|
||||
@$(RM) pkg/types/example.txt $(WIN_IGNORE_ERR)
|
||||
|
||||
# for the tests with localstack
|
||||
export AWS_ENDPOINT_FORCE=http://localhost:4566
|
||||
export AWS_ACCESS_KEY_ID=test
|
||||
export AWS_SECRET_ACCESS_KEY=test
|
||||
|
||||
testenv:
|
||||
@echo 'NOTE: You need Docker, docker-compose and run "make localstack" in a separate shell ("make localstack-stop" to terminate it)'
|
||||
|
||||
.PHONY: test
|
||||
test: testenv goversion ## Run unit tests with localstack
|
||||
$(GOTEST) $(LD_OPTS) ./...
|
||||
|
||||
.PHONY: go-acc
|
||||
go-acc: testenv goversion ## Run unit tests with localstack + coverage
|
||||
go-acc ./... -o coverage.out --ignore database,notifications,protobufs,cwversion,cstest,models -- $(LD_OPTS)
|
||||
|
||||
# mock AWS services
|
||||
.PHONY: localstack
|
||||
localstack: ## Run localstack containers (required for unit testing)
|
||||
docker-compose -f test/localstack/docker-compose.yml up
|
||||
|
||||
.PHONY: localstack-stop
|
||||
localstack-stop: ## Stop localstack containers
|
||||
docker-compose -f test/localstack/docker-compose.yml down
|
||||
|
||||
# build vendor.tgz to be distributed with the release
|
||||
.PHONY: vendor
|
||||
vendor: vendor-remove ## CI only - vendor dependencies and archive them for packaging
|
||||
$(GO) mod vendor
|
||||
tar czf vendor.tgz vendor
|
||||
tar --create --auto-compress --file=$(RELDIR)-vendor.tar.xz vendor
|
||||
|
||||
# remove vendor directories and vendor.tgz
|
||||
.PHONY: vendor-remove
|
||||
vendor-remove: ## Remove vendor dependencies and archives
|
||||
$(RM) vendor vendor.tgz *-vendor.tar.xz
|
||||
|
||||
.PHONY: package
|
||||
package:
|
||||
@echo "Building Release to dir $(RELDIR)"
|
||||
@$(MKDIR) $(RELDIR)/cmd/crowdsec
|
||||
@$(MKDIR) $(RELDIR)/cmd/crowdsec-cli
|
||||
@$(CP) $(CROWDSEC_FOLDER)/$(CROWDSEC_BIN) $(RELDIR)/cmd/crowdsec
|
||||
@$(CP) $(CSCLI_FOLDER)/$(CSCLI_BIN) $(RELDIR)/cmd/crowdsec-cli
|
||||
|
||||
@$(foreach plugin,$(PLUGINS), \
|
||||
$(MKDIR) $(RELDIR)/$(PLUGINS_DIR_PREFIX)$(plugin); \
|
||||
$(CP) $(PLUGINS_DIR_PREFIX)$(plugin)/notification-$(plugin)$(EXT) $(RELDIR)/$(PLUGINS_DIR_PREFIX)$(plugin); \
|
||||
$(CP) $(PLUGINS_DIR_PREFIX)$(plugin)/$(plugin).yaml $(RELDIR)/$(PLUGINS_DIR_PREFIX)$(plugin)/; \
|
||||
)
|
||||
|
||||
@$(CPR) ./config $(RELDIR)
|
||||
@$(CP) wizard.sh $(RELDIR)
|
||||
@$(CP) scripts/test_env.sh $(RELDIR)
|
||||
@$(CP) scripts/test_env.ps1 $(RELDIR)
|
||||
|
||||
@tar cvzf crowdsec-release.tgz $(RELDIR)
|
||||
|
||||
.PHONY: check_release
|
||||
check_release:
|
||||
ifneq ($(OS), Windows_NT)
|
||||
@if [ -d $(RELDIR) ]; then echo "$(RELDIR) already exists, abort" ; exit 1 ; fi
|
||||
else
|
||||
@if (Test-Path -Path $(RELDIR)) { echo "$(RELDIR) already exists, abort" ; exit 1 ; }
|
||||
endif
|
||||
|
||||
.PHONY:
|
||||
release: check_release build
|
||||
@echo Building Release to dir $(RELDIR)
|
||||
@mkdir -p $(RELDIR)/cmd/crowdsec
|
||||
@mkdir -p $(RELDIR)/cmd/crowdsec-cli
|
||||
@cp $(CROWDSEC_FOLDER)/$(CROWDSEC_BIN) $(RELDIR)/cmd/crowdsec
|
||||
@cp $(CSCLI_FOLDER)/$(CSCLI_BIN) $(RELDIR)/cmd/crowdsec-cli
|
||||
@cp -R ./config/ $(RELDIR)
|
||||
@cp -R ./data/ $(RELDIR)
|
||||
@cp wizard.sh $(RELDIR)
|
||||
@cp scripts/test_env.sh $(RELDIR)
|
||||
@bash ./scripts/build_plugins.sh
|
||||
@mkdir -p "$(RELDIR)/plugins/backend"
|
||||
@find ./plugins -type f -name "*.so" -exec install -Dm 644 {} "$(RELDIR)/{}" \; || exiting
|
||||
@tar cvzf crowdsec-release.tgz $(RELDIR)
|
||||
.PHONY: release
|
||||
release: check_release build package ## Build a release tarball
|
||||
|
||||
.PHONY: windows_installer
|
||||
windows_installer: build ## Windows - build the installer
|
||||
@.\make_installer.ps1 -version $(BUILD_VERSION)
|
||||
|
||||
.PHONY: chocolatey
|
||||
chocolatey: windows_installer ## Windows - build the chocolatey package
|
||||
@.\make_chocolatey.ps1 -version $(BUILD_VERSION)
|
||||
|
||||
# Include test/bats.mk only if it exists
|
||||
# to allow building without a test/ directory
|
||||
# (i.e. inside docker)
|
||||
ifeq (,$(wildcard test/bats.mk))
|
||||
bats-clean:
|
||||
else
|
||||
include test/bats.mk
|
||||
endif
|
||||
|
||||
include mk/goversion.mk
|
||||
include mk/help.mk
|
||||
|
|
159
README.md
159
README.md
|
@ -1,45 +1,162 @@
|
|||
|
||||
|
||||
<p align="center"> :warning: <b>Crowdsec BETA </b> :warning: </p>
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/assets/images/crowdsec_logo1.png" alt="CrowdSec" title="CrowdSec" />
|
||||
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/crowdsec_logo.png" alt="CrowdSec" title="CrowdSec" width="400" height="260"/>
|
||||
</p>
|
||||
|
||||
|
||||
</br>
|
||||
</br>
|
||||
</br>
|
||||
<p align="center">
|
||||
<img src="https://github.com/crowdsecurity/crowdsec/workflows/Go/badge.svg">
|
||||
<img src="https://github.com/crowdsecurity/crowdsec/workflows/build-binary-package/badge.svg">
|
||||
<img src="https://github.com/crowdsecurity/crowdsec/actions/workflows/go-tests.yml/badge.svg">
|
||||
<img src="https://github.com/crowdsecurity/crowdsec/actions/workflows/bats.yml/badge.svg">
|
||||
<a href="https://codecov.io/gh/crowdsecurity/crowdsec">
|
||||
<img src="https://codecov.io/gh/crowdsecurity/crowdsec/branch/master/graph/badge.svg?token=CQGSPNY3PT"/>
|
||||
</a>
|
||||
<img src="https://goreportcard.com/badge/github.com/crowdsecurity/crowdsec">
|
||||
<img src="https://img.shields.io/github/license/crowdsecurity/crowdsec">
|
||||
<img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/AlteredCoder/ed74e50c43e3b17bdfc4d93149f23d37/raw/crowdsec_parsers_badge.json">
|
||||
<img src="https://img.shields.io/endpoint?url=https://gist.githubusercontent.com/AlteredCoder/ed74e50c43e3b17bdfc4d93149f23d37/raw/crowdsec_scenarios_badge.json">
|
||||
<a href="https://hub.docker.com/r/crowdsecurity/crowdsec">
|
||||
<img src="https://img.shields.io/docker/pulls/crowdsecurity/crowdsec?logo=docker">
|
||||
</a>
|
||||
<a href="https://discord.com/invite/crowdsec">
|
||||
<img src="https://img.shields.io/discord/921520481163673640?label=Discord&logo=discord">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
:computer: <a href="https://app.crowdsec.net">Console (WebApp)</a>
|
||||
:books: <a href="https://doc.crowdsec.net">Documentation</a>
|
||||
:diamond_shape_with_a_dot_inside: <a href="https://hub.crowdsec.net">Hub</a>
|
||||
:speech_balloon: <a href="https://discourse.crowdsec.net">Discourse </a>
|
||||
:diamond_shape_with_a_dot_inside: <a href="https://hub.crowdsec.net">Configuration Hub</a>
|
||||
:speech_balloon: <a href="https://discourse.crowdsec.net">Discourse (Forum)</a>
|
||||
:speech_balloon: <a href="https://discord.gg/crowdsec">Discord (Live Chat)</a>
|
||||
</p>
|
||||
|
||||
## About the crowdsec project
|
||||
|
||||
Crowdsec is an open-source and lightweight software that allows you to detect peers with malevolent behaviors and block them from accessing your systems at various level (infrastructural, system, applicative).
|
||||
:dancer: This is a community-driven project, <a href="https://forms.gle/ZQBQcptG2wYGajRX8">we need your feedback</a>.
|
||||
|
||||
To achieve this, crowdsec reads logs from different sources (files, streams ...) to parse, normalize and enrich them before matching them to threats patterns called scenarios.
|
||||
## <TL;DR>
|
||||
|
||||
Crowdsec is a modular and plug-able framework, it ships a large variety of well known popular scenarios; users can choose what scenarios they want to be protected from as well as easily adding new custom ones to better fit their environment.
|
||||
CrowdSec is a free, modern & collaborative behavior detection engine, coupled with a global IP reputation network. It stacks on fail2ban's philosophy but is IPV6 compatible and 60x faster (Go vs Python), it uses Grok patterns to parse logs and YAML scenarios to identify behaviors. CrowdSec is engineered for modern Cloud / Containers / VM-based infrastructures (by decoupling detection and remediation). Once detected you can remedy threats with various bouncers (firewall block, nginx http 403, Captchas, etc.) while the aggressive IP can be sent to CrowdSec for curation before being shared among all users to further improve everyone's security. See [FAQ](https://doc.crowdsec.net/docs/faq) or read below for more.
|
||||
|
||||
Detected malevolent peers can then be prevented from accessing your resources by deploying [blockers](https://hub.crowdsec.net/browse/#blockers) at various levels (applicative, system, infrastructural) of your stack.
|
||||
## 2 mins install
|
||||
|
||||
One of the advantages of Crowdsec when compared to other solutions is its crowded aspect : Meta information about detected attacks (source IP, time and triggered scenario) are sent to a central API and then shared amongst all users.
|
||||
Installing it through the [Package system](https://doc.crowdsec.net/docs/getting_started/install_crowdsec) of your OS is the easiest way to proceed.
|
||||
Otherwise, you can install it from source.
|
||||
|
||||
Besides detecting and stopping attacks in real time based on your logs, it allows you to preemptively block known bad actors from accessing your information system.
|
||||
### From package (Debian)
|
||||
|
||||
## About this repository
|
||||
```sh
|
||||
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.deb.sh | sudo bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install crowdsec
|
||||
```
|
||||
|
||||
### From package (rhel/centos/amazon linux)
|
||||
|
||||
```sh
|
||||
curl -s https://packagecloud.io/install/repositories/crowdsec/crowdsec/script.rpm.sh | sudo bash
|
||||
sudo yum install crowdsec
|
||||
```
|
||||
|
||||
### From package (FreeBSD)
|
||||
|
||||
```
|
||||
sudo pkg update
|
||||
sudo pkg install crowdsec
|
||||
```
|
||||
|
||||
### From source
|
||||
|
||||
```sh
|
||||
wget https://github.com/crowdsecurity/crowdsec/releases/latest/download/crowdsec-release.tgz
|
||||
tar xzvf crowdsec-release.tgz
|
||||
cd crowdsec-v* && sudo ./wizard.sh -i
|
||||
```
|
||||
|
||||
## :information_source: About the CrowdSec project
|
||||
|
||||
Crowdsec is an open-source, lightweight software, detecting peers with aggressive behaviors to prevent them from accessing your systems. Its user-friendly design and assistance offer a low technical barrier of entry and nevertheless a high security gain.
|
||||
|
||||
The architecture is as follows :
|
||||
|
||||
<p align="center">
|
||||
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/crowdsec_architecture.png" alt="CrowdSec" title="CrowdSec"/>
|
||||
</p>
|
||||
|
||||
Once an unwanted behavior is detected, deal with it through a [bouncer](https://hub.crowdsec.net/browse/#bouncers). The aggressive IP, scenario triggered and timestamp are sent for curation, to avoid poisoning & false positives. (This can be disabled). If verified, this IP is then redistributed to all CrowdSec users running the same scenario.
|
||||
|
||||
## Outnumbering hackers all together
|
||||
|
||||
By sharing the threat they faced, all users are protecting each-others (hence the name Crowd-Security). Crowdsec is designed for modern infrastructures, with its "*Detect Here, Remedy There*" approach, letting you analyze logs coming from several sources in one place and block threats at various levels (applicative, system, infrastructural) of your stack.
|
||||
|
||||
CrowdSec ships by default with scenarios (brute force, port scan, web scan, etc.) adapted for most contexts, but you can easily extend it by picking more of them from the **[HUB](https://hub.crowdsec.net)**. It is also easy to adapt an existing one or create one yourself.
|
||||
|
||||
## :point_right: What it is not
|
||||
|
||||
CrowdSec is not a SIEM, storing your logs (neither locally nor remotely). Your data are analyzed locally and forgotten.
|
||||
|
||||
Signals sent to the curation platform are limited to the very strict minimum: IP, Scenario, Timestamp. They are only used to allow the system to spot new rogue IPs, and rule out false positives or poisoning attempts.
|
||||
|
||||
## :arrow_down: Install it !
|
||||
|
||||
Crowdsec is available for various platforms :
|
||||
|
||||
- [Use our debian repositories](https://doc.crowdsec.net/docs/getting_started/install_crowdsec) or the [official debian packages](https://packages.debian.org/search?keywords=crowdsec&searchon=names&suite=stable§ion=all)
|
||||
- An [image](https://hub.docker.com/r/crowdsecurity/crowdsec) is available for docker
|
||||
- [Prebuilt release packages](https://github.com/crowdsecurity/crowdsec/releases) are also available (suitable for `amd64`)
|
||||
- You can as well [build it from source](https://doc.crowdsec.net/docs/user_guides/building)
|
||||
|
||||
Or look directly at [installation documentation](https://doc.crowdsec.net/docs/getting_started/install_crowdsec) for other methods and platforms.
|
||||
|
||||
## :tada: Key benefits
|
||||
|
||||
### Fast assisted installation, no technical barrier
|
||||
|
||||
<details open>
|
||||
<summary>Initial configuration is automated, providing functional out-of-the-box setup</summary>
|
||||
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/crowdsec_install.gif?raw=true">
|
||||
</details>
|
||||
|
||||
### Out of the box detection
|
||||
|
||||
<details>
|
||||
<summary>Baseline detection is effective out-of-the-box, no fine-tuning required (click to expand)</summary>
|
||||
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/out-of-the-box-protection.gif?raw=true">
|
||||
</details>
|
||||
|
||||
### Easy bouncer deployment
|
||||
|
||||
<details>
|
||||
<summary>It's trivial to add bouncers to enforce decisions of crowdsec (click to expand)</summary>
|
||||
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/blocker-installation.gif?raw=true">
|
||||
</details>
|
||||
|
||||
### Easy dashboard access
|
||||
|
||||
<details>
|
||||
<summary>It's easy to deploy a metabase interface to view your data simply with cscli (click to expand)</summary>
|
||||
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/cscli-metabase.gif?raw=true">
|
||||
</details>
|
||||
|
||||
### Hot & Cold logs
|
||||
|
||||
<details>
|
||||
<summary>Process cold logs, for forensic, tests and chasing false positives & false negatives (click to expand)</summary>
|
||||
<img src="https://github.com/crowdsecurity/crowdsec-docs/blob/main/crowdsec-docs/static/img/forensic-mode.gif?raw=true">
|
||||
</details>
|
||||
|
||||
|
||||
## 📦 About this repository
|
||||
|
||||
This repository contains the code for the two main components of crowdsec :
|
||||
- `crowdsec` : the daemon a-la-fail2ban that can read, parse, enrich and apply heuristis to logs. This is the component in charge of "detecting" the attacks
|
||||
- `crowdsec` : the daemon a-la-fail2ban that can read, parse, enrich and apply heuristics to logs. This is the component in charge of "detecting" the attacks
|
||||
- `cscli` : the cli tool mainly used to interact with crowdsec : ban/unban/view current bans, enable/disable parsers and scenarios.
|
||||
|
||||
## :warning: Beta version
|
||||
|
||||
Please note that crowdsec is currently in beta version, use with caution !
|
||||
## Contributing
|
||||
|
||||
If you wish to contribute to the core of crowdsec, you are welcome to open a PR in this repository.
|
||||
|
||||
If you wish to add a new parser, scenario or collection, please open a PR in the [hub repository](https://github.com/crowdsecurity/hub).
|
||||
|
||||
If you wish to contribute to the documentation, please open a PR in the [documentation repository](http://github.com/crowdsecurity/crowdsec-docs).
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"Version": "v0.0.2",
|
||||
"CodeName": "road2beta"
|
||||
}
|
31
SECURITY.md
Normal file
31
SECURITY.md
Normal file
|
@ -0,0 +1,31 @@
|
|||
# Security Policy
|
||||
|
||||
## Scope
|
||||
|
||||
This security policy applies to :
|
||||
- Crowdsec agent
|
||||
- Crowdsec Local API
|
||||
- Crowdsec bouncers **developed and maintained** by the Crowdsec team [1]
|
||||
|
||||
Reports regarding developements of community members that are not part of the crowdsecurity organization will be thoroughly investigated nonetheless.
|
||||
|
||||
[1] Projects developed and maintained by the Crowdsec team are under the **crowdsecurity** github organization. Bouncers developed by community members that are not part of the Crowdsec organization are explictely excluded.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
We are extremely grateful to security researchers and users that report vulnerabilities regarding the Crowdsec project. All reports are thoroughly investigated by members of the Crowdsec organization.
|
||||
|
||||
You can email the private [security@crowdsec.net](mailto:security@crowdsec.net) list with the security details and the details expected for [all Crowdsec bug reports](https://github.com/crowdsecurity/crowdsec/blob/master/.github/ISSUE_TEMPLATE/bug_report.md).
|
||||
|
||||
You may encrypt your email to this list using the GPG key of the [Security team](https://doc.crowdsec.net/docs/next/contact_team). Encryption using GPG is NOT required to make a disclosure.
|
||||
|
||||
## When Should I Report a Vulnerability?
|
||||
|
||||
- You think you discovered a potential security vulnerability in Crowdsec
|
||||
- You are unsure how a vulnerability affects Crowdsec
|
||||
- You think you discovered a vulnerability in another project that Crowdsec depends on
|
||||
|
||||
For projects with their own vulnerability reporting and disclosure process, please report it directly there.
|
||||
|
||||
|
||||
<!-- Very heavily inspired from https://kubernetes.io/docs/reference/issues-security/security/ -->
|
186
azure-pipelines.yml
Normal file
186
azure-pipelines.yml
Normal file
|
@ -0,0 +1,186 @@
|
|||
trigger:
|
||||
tags:
|
||||
include:
|
||||
- "v*"
|
||||
exclude:
|
||||
- "v*freebsd"
|
||||
branches:
|
||||
exclude:
|
||||
- "*"
|
||||
pr: none
|
||||
|
||||
pool:
|
||||
vmImage: windows-latest
|
||||
|
||||
stages:
|
||||
- stage: Build
|
||||
jobs:
|
||||
- job: Build
|
||||
displayName: "Build"
|
||||
steps:
|
||||
- task: GoTool@0
|
||||
displayName: "Install Go"
|
||||
inputs:
|
||||
version: '1.22.2'
|
||||
|
||||
- pwsh: |
|
||||
choco install -y make
|
||||
displayName: "Install builds deps"
|
||||
- task: PowerShell@2
|
||||
inputs:
|
||||
targetType: 'inline'
|
||||
pwsh: true
|
||||
#we are not calling make windows_installer because we want to sign the binaries before they are added to the MSI
|
||||
script: |
|
||||
make build BUILD_RE2_WASM=1
|
||||
|
||||
- pwsh: |
|
||||
$build_version=$env:BUILD_SOURCEBRANCHNAME
|
||||
#Override the version if it's set in the pipeline
|
||||
if ( ${env:USERBUILDVERSION} -ne "")
|
||||
{
|
||||
$build_version = ${env:USERBUILDVERSION}
|
||||
}
|
||||
if ($build_version.StartsWith("v"))
|
||||
{
|
||||
$build_version = $build_version.Substring(1)
|
||||
}
|
||||
if ($build_version.Contains("-"))
|
||||
{
|
||||
$build_version = $build_version.Substring(0, $build_version.IndexOf("-"))
|
||||
}
|
||||
Write-Host "##vso[task.setvariable variable=BuildVersion;isOutput=true]$build_version"
|
||||
displayName: GetCrowdsecVersion
|
||||
name: GetCrowdsecVersion
|
||||
- pwsh: |
|
||||
Get-ChildItem -Path .\cmd -Directory | ForEach-Object {
|
||||
$dirName = $_.Name
|
||||
Get-ChildItem -Path .\cmd\$dirName -File -Filter '*.exe' | ForEach-Object {
|
||||
$fileName = $_.Name
|
||||
$destDir = Join-Path $(Build.ArtifactStagingDirectory) cmd\$dirName
|
||||
New-Item -ItemType Directory -Path $destDir -Force
|
||||
Copy-Item -Path .\cmd\$dirName\$fileName -Destination $destDir
|
||||
}
|
||||
}
|
||||
displayName: "Copy binaries to staging directory"
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: '$(Build.ArtifactStagingDirectory)'
|
||||
artifact: 'unsigned_binaries'
|
||||
displayName: "Upload binaries artifact"
|
||||
|
||||
- stage: Sign
|
||||
dependsOn: Build
|
||||
variables:
|
||||
- group: 'FOSS Build Variables'
|
||||
- name: BuildVersion
|
||||
value: $[ stageDependencies.Build.Build.outputs['GetCrowdsecVersion.BuildVersion'] ]
|
||||
condition: succeeded()
|
||||
jobs:
|
||||
- job: Sign
|
||||
displayName: "Sign"
|
||||
steps:
|
||||
- download: current
|
||||
artifact: unsigned_binaries
|
||||
displayName: "Download binaries artifact"
|
||||
- task: CopyFiles@2
|
||||
inputs:
|
||||
SourceFolder: '$(Pipeline.Workspace)/unsigned_binaries'
|
||||
TargetFolder: '$(Build.SourcesDirectory)'
|
||||
displayName: "Copy binaries to workspace"
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: "Install SignTool tool"
|
||||
inputs:
|
||||
command: 'custom'
|
||||
custom: 'tool'
|
||||
arguments: install --global sign --version 0.9.0-beta.23127.3
|
||||
- task: AzureKeyVault@2
|
||||
displayName: "Get signing parameters"
|
||||
inputs:
|
||||
azureSubscription: "Azure subscription"
|
||||
KeyVaultName: "$(KeyVaultName)"
|
||||
SecretsFilter: "TenantId,ClientId,ClientSecret,Certificate,KeyVaultUrl"
|
||||
- pwsh: |
|
||||
sign code azure-key-vault `
|
||||
"**/*.exe" `
|
||||
--base-directory "$(Build.SourcesDirectory)/cmd/" `
|
||||
--publisher-name "CrowdSec" `
|
||||
--description "CrowdSec" `
|
||||
--description-url "https://github.com/crowdsecurity/crowdsec" `
|
||||
--azure-key-vault-tenant-id "$(TenantId)" `
|
||||
--azure-key-vault-client-id "$(ClientId)" `
|
||||
--azure-key-vault-client-secret "$(ClientSecret)" `
|
||||
--azure-key-vault-certificate "$(Certificate)" `
|
||||
--azure-key-vault-url "$(KeyVaultUrl)"
|
||||
displayName: "Sign crowdsec binaries"
|
||||
- pwsh: |
|
||||
.\make_installer.ps1 -version '$(BuildVersion)'
|
||||
displayName: "Build Crowdsec MSI"
|
||||
name: BuildMSI
|
||||
- pwsh: |
|
||||
.\make_chocolatey.ps1 -version '$(BuildVersion)'
|
||||
displayName: "Build Chocolatey nupkg"
|
||||
- pwsh: |
|
||||
sign code azure-key-vault `
|
||||
"*.msi" `
|
||||
--base-directory "$(Build.SourcesDirectory)" `
|
||||
--publisher-name "CrowdSec" `
|
||||
--description "CrowdSec" `
|
||||
--description-url "https://github.com/crowdsecurity/crowdsec" `
|
||||
--azure-key-vault-tenant-id "$(TenantId)" `
|
||||
--azure-key-vault-client-id "$(ClientId)" `
|
||||
--azure-key-vault-client-secret "$(ClientSecret)" `
|
||||
--azure-key-vault-certificate "$(Certificate)" `
|
||||
--azure-key-vault-url "$(KeyVaultUrl)"
|
||||
displayName: "Sign MSI package"
|
||||
- pwsh: |
|
||||
sign code azure-key-vault `
|
||||
"*.nupkg" `
|
||||
--base-directory "$(Build.SourcesDirectory)" `
|
||||
--publisher-name "CrowdSec" `
|
||||
--description "CrowdSec" `
|
||||
--description-url "https://github.com/crowdsecurity/crowdsec" `
|
||||
--azure-key-vault-tenant-id "$(TenantId)" `
|
||||
--azure-key-vault-client-id "$(ClientId)" `
|
||||
--azure-key-vault-client-secret "$(ClientSecret)" `
|
||||
--azure-key-vault-certificate "$(Certificate)" `
|
||||
--azure-key-vault-url "$(KeyVaultUrl)"
|
||||
displayName: "Sign nuget package"
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: '$(Build.SourcesDirectory)/crowdsec_$(BuildVersion).msi'
|
||||
artifact: 'signed_msi_package'
|
||||
displayName: "Upload signed MSI artifact"
|
||||
- task: PublishPipelineArtifact@1
|
||||
inputs:
|
||||
targetPath: '$(Build.SourcesDirectory)/crowdsec.$(BuildVersion).nupkg'
|
||||
artifact: 'signed_nuget_package'
|
||||
displayName: "Upload signed nuget artifact"
|
||||
|
||||
- stage: Publish
|
||||
dependsOn: Sign
|
||||
jobs:
|
||||
- deployment: "Publish"
|
||||
displayName: "Publish to GitHub"
|
||||
environment: github
|
||||
strategy:
|
||||
runOnce:
|
||||
deploy:
|
||||
steps:
|
||||
- bash: |
|
||||
tag=$(curl -H "Accept: application/vnd.github.v3+json" https://api.github.com/repos/crowdsecurity/crowdsec/releases | jq -r '. | map(select(.prerelease==true)) | sort_by(.created_at) | reverse | .[0].tag_name')
|
||||
echo "##vso[task.setvariable variable=LatestPreRelease;isOutput=true]$tag"
|
||||
name: GetLatestPrelease
|
||||
- task: GitHubRelease@1
|
||||
inputs:
|
||||
gitHubConnection: "github.com_blotus"
|
||||
repositoryName: '$(Build.Repository.Name)'
|
||||
action: 'edit'
|
||||
tag: '$(GetLatestPrelease.LatestPreRelease)'
|
||||
assetUploadMode: 'replace'
|
||||
addChangeLog: false
|
||||
isPreRelease: true #we force prerelease because the pipeline is invoked on tag creation, which happens when we do a prerelease
|
||||
assets: |
|
||||
$(Pipeline.Workspace)/signed_msi_package/*.msi
|
||||
$(Pipeline.Workspace)/signed_nuget_package/*.nupkg
|
||||
condition: ne(variables['GetLatestPrelease.LatestPreRelease'], '')
|
|
@ -1,21 +1,23 @@
|
|||
# Go parameters
|
||||
GOCMD=go
|
||||
GOBUILD=$(GOCMD) build
|
||||
GOCLEAN=$(GOCMD) clean
|
||||
GOTEST=$(GOCMD) test
|
||||
GOGET=$(GOCMD) get
|
||||
BINARY_NAME=cscli
|
||||
PREFIX?="/"
|
||||
ifeq ($(OS), Windows_NT)
|
||||
SHELL := pwsh.exe
|
||||
.SHELLFLAGS := -NoProfile -Command
|
||||
EXT = .exe
|
||||
endif
|
||||
|
||||
GO = go
|
||||
GOBUILD = $(GO) build
|
||||
|
||||
BINARY_NAME = cscli$(EXT)
|
||||
PREFIX ?= "/"
|
||||
BIN_PREFIX = $(PREFIX)"/usr/local/bin/"
|
||||
|
||||
.PHONY: all
|
||||
all: clean build
|
||||
|
||||
build: clean
|
||||
@$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME) -v
|
||||
|
||||
static: clean
|
||||
@$(GOBUILD) -o $(BINARY_NAME) -v -a -tags netgo -ldflags '-w -extldflags "-static"'
|
||||
$(GOBUILD) $(LD_OPTS) -o $(BINARY_NAME)
|
||||
|
||||
.PHONY: install
|
||||
install: install-conf install-bin
|
||||
|
||||
install-conf:
|
||||
|
@ -24,8 +26,8 @@ install-bin:
|
|||
@install -v -m 755 -D "$(BINARY_NAME)" "$(BIN_PREFIX)/$(BINARY_NAME)" || exit
|
||||
|
||||
uninstall:
|
||||
@rm -rf $(CSCLI_CONFIG)
|
||||
@rm -rf $(BIN_PREFIX)$(BINARY_NAME)
|
||||
@$(RM) $(CSCLI_CONFIG) $(WIN_IGNORE_ERR)
|
||||
@$(RM) $(BIN_PREFIX)$(BINARY_NAME) $(WIN_IGNORE_ERR)
|
||||
|
||||
clean:
|
||||
@rm -f $(BINARY_NAME)
|
||||
@$(RM) $(BINARY_NAME) $(WIN_IGNORE_ERR)
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
see doc in `doc/`
|
560
cmd/crowdsec-cli/alerts.go
Normal file
560
cmd/crowdsec-cli/alerts.go
Normal file
|
@ -0,0 +1,560 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
func DecisionsFromAlert(alert *models.Alert) string {
|
||||
ret := ""
|
||||
decMap := make(map[string]int)
|
||||
|
||||
for _, decision := range alert.Decisions {
|
||||
k := *decision.Type
|
||||
if *decision.Simulated {
|
||||
k = fmt.Sprintf("(simul)%s", k)
|
||||
}
|
||||
|
||||
v := decMap[k]
|
||||
decMap[k] = v + 1
|
||||
}
|
||||
|
||||
for k, v := range decMap {
|
||||
if len(ret) > 0 {
|
||||
ret += " "
|
||||
}
|
||||
|
||||
ret += fmt.Sprintf("%s:%d", k, v)
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) alertsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(os.Stdout)
|
||||
header := []string{"id", "scope", "value", "reason", "country", "as", "decisions", "created_at"}
|
||||
|
||||
if printMachine {
|
||||
header = append(header, "machine")
|
||||
}
|
||||
|
||||
if err := csvwriter.Write(header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, alertItem := range *alerts {
|
||||
row := []string{
|
||||
strconv.FormatInt(alertItem.ID, 10),
|
||||
*alertItem.Source.Scope,
|
||||
*alertItem.Source.Value,
|
||||
*alertItem.Scenario,
|
||||
alertItem.Source.Cn,
|
||||
alertItem.Source.GetAsNumberName(),
|
||||
DecisionsFromAlert(alertItem),
|
||||
*alertItem.StartAt,
|
||||
}
|
||||
if printMachine {
|
||||
row = append(row, alertItem.MachineID)
|
||||
}
|
||||
|
||||
if err := csvwriter.Write(row); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
case "json":
|
||||
if *alerts == nil {
|
||||
// avoid returning "null" in json
|
||||
// could be cleaner if we used slice of alerts directly
|
||||
fmt.Println("[]")
|
||||
return nil
|
||||
}
|
||||
|
||||
x, _ := json.MarshalIndent(alerts, "", " ")
|
||||
fmt.Print(string(x))
|
||||
case "human":
|
||||
if len(*alerts) == 0 {
|
||||
fmt.Println("No active alerts")
|
||||
return nil
|
||||
}
|
||||
|
||||
alertsTable(color.Output, alerts, printMachine)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var alertTemplate = `
|
||||
################################################################################################
|
||||
|
||||
- ID : {{.ID}}
|
||||
- Date : {{.CreatedAt}}
|
||||
- Machine : {{.MachineID}}
|
||||
- Simulation : {{.Simulated}}
|
||||
- Reason : {{.Scenario}}
|
||||
- Events Count : {{.EventsCount}}
|
||||
- Scope:Value : {{.Source.Scope}}{{if .Source.Value}}:{{.Source.Value}}{{end}}
|
||||
- Country : {{.Source.Cn}}
|
||||
- AS : {{.Source.AsName}}
|
||||
- Begin : {{.StartAt}}
|
||||
- End : {{.StopAt}}
|
||||
- UUID : {{.UUID}}
|
||||
|
||||
`
|
||||
|
||||
func (cli *cliAlerts) displayOneAlert(alert *models.Alert, withDetail bool) error {
|
||||
tmpl, err := template.New("alert").Parse(alertTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = tmpl.Execute(os.Stdout, alert); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
alertDecisionsTable(color.Output, alert)
|
||||
|
||||
if len(alert.Meta) > 0 {
|
||||
fmt.Printf("\n - Context :\n")
|
||||
sort.Slice(alert.Meta, func(i, j int) bool {
|
||||
return alert.Meta[i].Key < alert.Meta[j].Key
|
||||
})
|
||||
|
||||
table := newTable(color.Output)
|
||||
table.SetRowLines(false)
|
||||
table.SetHeaders("Key", "Value")
|
||||
|
||||
for _, meta := range alert.Meta {
|
||||
var valSlice []string
|
||||
if err := json.Unmarshal([]byte(meta.Value), &valSlice); err != nil {
|
||||
return fmt.Errorf("unknown context value type '%s': %w", meta.Value, err)
|
||||
}
|
||||
|
||||
for _, value := range valSlice {
|
||||
table.AddRow(
|
||||
meta.Key,
|
||||
value,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
table.Render()
|
||||
}
|
||||
|
||||
if withDetail {
|
||||
fmt.Printf("\n - Events :\n")
|
||||
|
||||
for _, event := range alert.Events {
|
||||
alertEventTable(color.Output, event)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type cliAlerts struct {
|
||||
client *apiclient.ApiClient
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLIAlerts(getconfig configGetter) *cliAlerts {
|
||||
return &cliAlerts{
|
||||
cfg: getconfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "alerts [action]",
|
||||
Short: "Manage alerts",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
Aliases: []string{"alert"},
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := cfg.LoadAPIClient(); err != nil {
|
||||
return fmt.Errorf("loading api client: %w", err)
|
||||
}
|
||||
apiURL, err := url.Parse(cfg.API.Client.Credentials.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing api url %s: %w", apiURL, err)
|
||||
}
|
||||
|
||||
cli.client, err = apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: cfg.API.Client.Credentials.Login,
|
||||
Password: strfmt.Password(cfg.API.Client.Credentials.Password),
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiURL,
|
||||
VersionPrefix: "v1",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("new api client: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.NewListCmd())
|
||||
cmd.AddCommand(cli.NewInspectCmd())
|
||||
cmd.AddCommand(cli.NewFlushCmd())
|
||||
cmd.AddCommand(cli.NewDeleteCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewListCmd() *cobra.Command {
|
||||
alertListFilter := apiclient.AlertsListOpts{
|
||||
ScopeEquals: new(string),
|
||||
ValueEquals: new(string),
|
||||
ScenarioEquals: new(string),
|
||||
IPEquals: new(string),
|
||||
RangeEquals: new(string),
|
||||
Since: new(string),
|
||||
Until: new(string),
|
||||
TypeEquals: new(string),
|
||||
IncludeCAPI: new(bool),
|
||||
OriginEquals: new(string),
|
||||
}
|
||||
|
||||
limit := new(int)
|
||||
contained := new(bool)
|
||||
|
||||
var printMachine bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [filters]",
|
||||
Short: "List alerts",
|
||||
Example: `cscli alerts list
|
||||
cscli alerts list --ip 1.2.3.4
|
||||
cscli alerts list --range 1.2.3.0/24
|
||||
cscli alerts list --origin lists
|
||||
cscli alerts list -s crowdsecurity/ssh-bf
|
||||
cscli alerts list --type ban`,
|
||||
Long: `List alerts with optional filters`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if err := manageCliDecisionAlerts(alertListFilter.IPEquals, alertListFilter.RangeEquals,
|
||||
alertListFilter.ScopeEquals, alertListFilter.ValueEquals); err != nil {
|
||||
printHelp(cmd)
|
||||
return err
|
||||
}
|
||||
if limit != nil {
|
||||
alertListFilter.Limit = limit
|
||||
}
|
||||
|
||||
if *alertListFilter.Until == "" {
|
||||
alertListFilter.Until = nil
|
||||
} else if strings.HasSuffix(*alertListFilter.Until, "d") {
|
||||
/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
|
||||
realDuration := strings.TrimSuffix(*alertListFilter.Until, "d")
|
||||
days, err := strconv.Atoi(realDuration)
|
||||
if err != nil {
|
||||
printHelp(cmd)
|
||||
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Until)
|
||||
}
|
||||
*alertListFilter.Until = fmt.Sprintf("%d%s", days*24, "h")
|
||||
}
|
||||
if *alertListFilter.Since == "" {
|
||||
alertListFilter.Since = nil
|
||||
} else if strings.HasSuffix(*alertListFilter.Since, "d") {
|
||||
/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
|
||||
realDuration := strings.TrimSuffix(*alertListFilter.Since, "d")
|
||||
days, err := strconv.Atoi(realDuration)
|
||||
if err != nil {
|
||||
printHelp(cmd)
|
||||
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *alertListFilter.Since)
|
||||
}
|
||||
*alertListFilter.Since = fmt.Sprintf("%d%s", days*24, "h")
|
||||
}
|
||||
|
||||
if *alertListFilter.IncludeCAPI {
|
||||
*alertListFilter.Limit = 0
|
||||
}
|
||||
|
||||
if *alertListFilter.TypeEquals == "" {
|
||||
alertListFilter.TypeEquals = nil
|
||||
}
|
||||
if *alertListFilter.ScopeEquals == "" {
|
||||
alertListFilter.ScopeEquals = nil
|
||||
}
|
||||
if *alertListFilter.ValueEquals == "" {
|
||||
alertListFilter.ValueEquals = nil
|
||||
}
|
||||
if *alertListFilter.ScenarioEquals == "" {
|
||||
alertListFilter.ScenarioEquals = nil
|
||||
}
|
||||
if *alertListFilter.IPEquals == "" {
|
||||
alertListFilter.IPEquals = nil
|
||||
}
|
||||
if *alertListFilter.RangeEquals == "" {
|
||||
alertListFilter.RangeEquals = nil
|
||||
}
|
||||
|
||||
if *alertListFilter.OriginEquals == "" {
|
||||
alertListFilter.OriginEquals = nil
|
||||
}
|
||||
|
||||
if contained != nil && *contained {
|
||||
alertListFilter.Contains = new(bool)
|
||||
}
|
||||
|
||||
alerts, _, err := cli.client.Alerts.List(context.Background(), alertListFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list alerts: %w", err)
|
||||
}
|
||||
|
||||
if err = cli.alertsToTable(alerts, printMachine); err != nil {
|
||||
return fmt.Errorf("unable to list alerts: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SortFlags = false
|
||||
flags.BoolVarP(alertListFilter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
|
||||
flags.StringVar(alertListFilter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
|
||||
flags.StringVar(alertListFilter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
|
||||
flags.StringVarP(alertListFilter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
|
||||
flags.StringVarP(alertListFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
|
||||
flags.StringVarP(alertListFilter.RangeEquals, "range", "r", "", "restrict to alerts from this range (shorthand for --scope range --value <RANGE/X>)")
|
||||
flags.StringVar(alertListFilter.TypeEquals, "type", "", "restrict to alerts with given decision type (ie. ban, captcha)")
|
||||
flags.StringVar(alertListFilter.ScopeEquals, "scope", "", "restrict to alerts of this scope (ie. ip,range)")
|
||||
flags.StringVarP(alertListFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
|
||||
flags.StringVar(alertListFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
|
||||
flags.BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
flags.BoolVarP(&printMachine, "machine", "m", false, "print machines that sent alerts")
|
||||
flags.IntVarP(limit, "limit", "l", 50, "limit size of alerts list table (0 to view all alerts)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewDeleteCmd() *cobra.Command {
|
||||
var (
|
||||
ActiveDecision *bool
|
||||
AlertDeleteAll bool
|
||||
delAlertByID string
|
||||
)
|
||||
|
||||
alertDeleteFilter := apiclient.AlertsDeleteOpts{
|
||||
ScopeEquals: new(string),
|
||||
ValueEquals: new(string),
|
||||
ScenarioEquals: new(string),
|
||||
IPEquals: new(string),
|
||||
RangeEquals: new(string),
|
||||
}
|
||||
|
||||
contained := new(bool)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [filters] [--all]",
|
||||
Short: `Delete alerts
|
||||
/!\ This command can be use only on the same machine than the local API.`,
|
||||
Example: `cscli alerts delete --ip 1.2.3.4
|
||||
cscli alerts delete --range 1.2.3.0/24
|
||||
cscli alerts delete -s crowdsecurity/ssh-bf"`,
|
||||
DisableAutoGenTag: true,
|
||||
Aliases: []string{"remove"},
|
||||
Args: cobra.ExactArgs(0),
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if AlertDeleteAll {
|
||||
return nil
|
||||
}
|
||||
if *alertDeleteFilter.ScopeEquals == "" && *alertDeleteFilter.ValueEquals == "" &&
|
||||
*alertDeleteFilter.ScenarioEquals == "" && *alertDeleteFilter.IPEquals == "" &&
|
||||
*alertDeleteFilter.RangeEquals == "" && delAlertByID == "" {
|
||||
_ = cmd.Usage()
|
||||
return errors.New("at least one filter or --all must be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
if !AlertDeleteAll {
|
||||
if err = manageCliDecisionAlerts(alertDeleteFilter.IPEquals, alertDeleteFilter.RangeEquals,
|
||||
alertDeleteFilter.ScopeEquals, alertDeleteFilter.ValueEquals); err != nil {
|
||||
printHelp(cmd)
|
||||
return err
|
||||
}
|
||||
if ActiveDecision != nil {
|
||||
alertDeleteFilter.ActiveDecisionEquals = ActiveDecision
|
||||
}
|
||||
|
||||
if *alertDeleteFilter.ScopeEquals == "" {
|
||||
alertDeleteFilter.ScopeEquals = nil
|
||||
}
|
||||
if *alertDeleteFilter.ValueEquals == "" {
|
||||
alertDeleteFilter.ValueEquals = nil
|
||||
}
|
||||
if *alertDeleteFilter.ScenarioEquals == "" {
|
||||
alertDeleteFilter.ScenarioEquals = nil
|
||||
}
|
||||
if *alertDeleteFilter.IPEquals == "" {
|
||||
alertDeleteFilter.IPEquals = nil
|
||||
}
|
||||
if *alertDeleteFilter.RangeEquals == "" {
|
||||
alertDeleteFilter.RangeEquals = nil
|
||||
}
|
||||
if contained != nil && *contained {
|
||||
alertDeleteFilter.Contains = new(bool)
|
||||
}
|
||||
limit := 0
|
||||
alertDeleteFilter.Limit = &limit
|
||||
} else {
|
||||
limit := 0
|
||||
alertDeleteFilter = apiclient.AlertsDeleteOpts{Limit: &limit}
|
||||
}
|
||||
|
||||
var alerts *models.DeleteAlertsResponse
|
||||
if delAlertByID == "" {
|
||||
alerts, _, err = cli.client.Alerts.Delete(context.Background(), alertDeleteFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete alerts: %w", err)
|
||||
}
|
||||
} else {
|
||||
alerts, _, err = cli.client.Alerts.DeleteOne(context.Background(), delAlertByID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete alert: %w", err)
|
||||
}
|
||||
}
|
||||
log.Infof("%s alert(s) deleted", alerts.NbDeleted)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SortFlags = false
|
||||
flags.StringVar(alertDeleteFilter.ScopeEquals, "scope", "", "the scope (ie. ip,range)")
|
||||
flags.StringVarP(alertDeleteFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
|
||||
flags.StringVarP(alertDeleteFilter.ScenarioEquals, "scenario", "s", "", "the scenario (ie. crowdsecurity/ssh-bf)")
|
||||
flags.StringVarP(alertDeleteFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
|
||||
flags.StringVarP(alertDeleteFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
|
||||
flags.StringVar(&delAlertByID, "id", "", "alert ID")
|
||||
flags.BoolVarP(&AlertDeleteAll, "all", "a", false, "delete all alerts")
|
||||
flags.BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewInspectCmd() *cobra.Command {
|
||||
var details bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: `inspect "alert_id"`,
|
||||
Short: `Show info about an alert`,
|
||||
Example: `cscli alerts inspect 123`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
cfg := cli.cfg()
|
||||
if len(args) == 0 {
|
||||
printHelp(cmd)
|
||||
return errors.New("missing alert_id")
|
||||
}
|
||||
for _, alertID := range args {
|
||||
id, err := strconv.Atoi(alertID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad alert id %s", alertID)
|
||||
}
|
||||
alert, _, err := cli.client.Alerts.GetByID(context.Background(), id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't find alert with id %s: %w", alertID, err)
|
||||
}
|
||||
switch cfg.Cscli.Output {
|
||||
case "human":
|
||||
if err := cli.displayOneAlert(alert, details); err != nil {
|
||||
continue
|
||||
}
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(alert, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
|
||||
}
|
||||
fmt.Printf("%s\n", string(data))
|
||||
case "raw":
|
||||
data, err := yaml.Marshal(alert)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal alert with id %s: %w", alertID, err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().BoolVarP(&details, "details", "d", false, "show alerts with events")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliAlerts) NewFlushCmd() *cobra.Command {
|
||||
var (
|
||||
maxItems int
|
||||
maxAge string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: `flush`,
|
||||
Short: `Flush alerts
|
||||
/!\ This command can be used only on the same machine than the local API`,
|
||||
Example: `cscli alerts flush --max-items 1000 --max-age 7d`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
db, err := database.NewClient(cfg.DbConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create new database client: %w", err)
|
||||
}
|
||||
log.Info("Flushing alerts. !! This may take a long time !!")
|
||||
err = db.FlushAlerts(maxAge, maxItems)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to flush alerts: %w", err)
|
||||
}
|
||||
log.Info("Alerts flushed")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().IntVar(&maxItems, "max-items", 5000, "Maximum number of alert items to keep in the database")
|
||||
cmd.Flags().StringVar(&maxAge, "max-age", "7d", "Maximum age of alert items to keep in the database")
|
||||
|
||||
return cmd
|
||||
}
|
102
cmd/crowdsec-cli/alerts_table.go
Normal file
102
cmd/crowdsec-cli/alerts_table.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
)
|
||||
|
||||
func alertsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachine bool) {
|
||||
t := newTable(out)
|
||||
t.SetRowLines(false)
|
||||
header := []string{"ID", "value", "reason", "country", "as", "decisions", "created_at"}
|
||||
if printMachine {
|
||||
header = append(header, "machine")
|
||||
}
|
||||
t.SetHeaders(header...)
|
||||
|
||||
for _, alertItem := range *alerts {
|
||||
displayVal := *alertItem.Source.Scope
|
||||
if len(alertItem.Decisions) > 1 {
|
||||
displayVal = fmt.Sprintf("%s (%d %ss)", *alertItem.Source.Scope, len(alertItem.Decisions), *alertItem.Decisions[0].Scope)
|
||||
} else if *alertItem.Source.Value != "" {
|
||||
displayVal += ":" + *alertItem.Source.Value
|
||||
}
|
||||
|
||||
row := []string{
|
||||
strconv.Itoa(int(alertItem.ID)),
|
||||
displayVal,
|
||||
*alertItem.Scenario,
|
||||
alertItem.Source.Cn,
|
||||
alertItem.Source.GetAsNumberName(),
|
||||
DecisionsFromAlert(alertItem),
|
||||
*alertItem.StartAt,
|
||||
}
|
||||
|
||||
if printMachine {
|
||||
row = append(row, alertItem.MachineID)
|
||||
}
|
||||
|
||||
t.AddRow(row...)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
||||
|
||||
func alertDecisionsTable(out io.Writer, alert *models.Alert) {
|
||||
foundActive := false
|
||||
t := newTable(out)
|
||||
t.SetRowLines(false)
|
||||
t.SetHeaders("ID", "scope:value", "action", "expiration", "created_at")
|
||||
for _, decision := range alert.Decisions {
|
||||
parsedDuration, err := time.ParseDuration(*decision.Duration)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
}
|
||||
expire := time.Now().UTC().Add(parsedDuration)
|
||||
if time.Now().UTC().After(expire) {
|
||||
continue
|
||||
}
|
||||
foundActive = true
|
||||
scopeAndValue := *decision.Scope
|
||||
if *decision.Value != "" {
|
||||
scopeAndValue += ":" + *decision.Value
|
||||
}
|
||||
t.AddRow(
|
||||
strconv.Itoa(int(decision.ID)),
|
||||
scopeAndValue,
|
||||
*decision.Type,
|
||||
*decision.Duration,
|
||||
alert.CreatedAt,
|
||||
)
|
||||
}
|
||||
if foundActive {
|
||||
fmt.Printf(" - Active Decisions :\n")
|
||||
t.Render() // Send output
|
||||
}
|
||||
}
|
||||
|
||||
func alertEventTable(out io.Writer, event *models.Event) {
|
||||
fmt.Fprintf(out, "\n- Date: %s\n", *event.Timestamp)
|
||||
|
||||
t := newTable(out)
|
||||
t.SetHeaders("Key", "Value")
|
||||
sort.Slice(event.Meta, func(i, j int) bool {
|
||||
return event.Meta[i].Key < event.Meta[j].Key
|
||||
})
|
||||
|
||||
for _, meta := range event.Meta {
|
||||
t.AddRow(
|
||||
meta.Key,
|
||||
meta.Value,
|
||||
)
|
||||
}
|
||||
|
||||
t.Render() // Send output
|
||||
}
|
|
@ -1,267 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/outputs"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
|
||||
"github.com/denisbrodbeck/machineid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
passwordLength = 64
|
||||
upper = "ABCDEFGHIJKLMNOPQRSTUVWXY"
|
||||
lower = "abcdefghijklmnopqrstuvwxyz"
|
||||
digits = "0123456789"
|
||||
)
|
||||
|
||||
var (
|
||||
apiConfigFile = "api.yaml"
|
||||
userID string // for flag parsing
|
||||
outputCTX *outputs.Output
|
||||
)
|
||||
|
||||
func dumpCredentials() error {
|
||||
if config.output == "json" {
|
||||
credsYaml, err := json.Marshal(&outputCTX.API.Creds)
|
||||
if err != nil {
|
||||
log.Fatalf("Can't marshal credentials : %v", err)
|
||||
}
|
||||
fmt.Printf("%s\n", string(credsYaml))
|
||||
} else {
|
||||
credsYaml, err := yaml.Marshal(&outputCTX.API.Creds)
|
||||
if err != nil {
|
||||
log.Fatalf("Can't marshal credentials : %v", err)
|
||||
}
|
||||
fmt.Printf("%s\n", string(credsYaml))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func generatePassword() string {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
charset := upper + lower + digits
|
||||
|
||||
buf := make([]byte, passwordLength)
|
||||
buf[0] = digits[rand.Intn(len(digits))]
|
||||
buf[1] = upper[rand.Intn(len(upper))]
|
||||
buf[2] = lower[rand.Intn(len(lower))]
|
||||
|
||||
for i := 3; i < passwordLength; i++ {
|
||||
buf[i] = charset[rand.Intn(len(charset))]
|
||||
}
|
||||
rand.Shuffle(len(buf), func(i, j int) {
|
||||
buf[i], buf[j] = buf[j], buf[i]
|
||||
})
|
||||
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
func pullTOP() error {
|
||||
/*profile from cwhub*/
|
||||
var profiles []string
|
||||
if _, ok := cwhub.HubIdx[cwhub.SCENARIOS]; !ok || len(cwhub.HubIdx[cwhub.SCENARIOS]) == 0 {
|
||||
log.Errorf("no loaded scenarios, can't fill profiles")
|
||||
return fmt.Errorf("no profiles")
|
||||
}
|
||||
for _, item := range cwhub.HubIdx[cwhub.SCENARIOS] {
|
||||
if item.Tainted || !item.Installed {
|
||||
continue
|
||||
}
|
||||
profiles = append(profiles, item.Name)
|
||||
}
|
||||
outputCTX.API.Creds.Profile = strings.Join(profiles[:], ",")
|
||||
if err := outputCTX.API.Signin(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
|
||||
ret, err := outputCTX.API.PullTop()
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
log.Warningf("api pull returned %d entries", len(ret))
|
||||
for _, item := range ret {
|
||||
if _, ok := item["range_ip"]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := item["scenario"]; !ok {
|
||||
continue
|
||||
}
|
||||
item["scenario"] = fmt.Sprintf("api: %s", item["scenario"])
|
||||
|
||||
if _, ok := item["action"]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := item["expiration"]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := item["country"]; !ok {
|
||||
item["country"] = ""
|
||||
}
|
||||
if _, ok := item["as_org"]; !ok {
|
||||
item["as_org"] = ""
|
||||
}
|
||||
if _, ok := item["as_num"]; !ok {
|
||||
item["as_num"] = ""
|
||||
}
|
||||
var signalOcc types.SignalOccurence
|
||||
signalOcc, err = simpleBanToSignal(item["range_ip"], item["scenario"], item["expiration"], item["action"], item["as_name"], item["as_num"], item["country"], "api")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to convert ban to signal : %s", err)
|
||||
}
|
||||
if err := outputCTX.Insert(signalOcc); err != nil {
|
||||
log.Fatalf("Unable to write pull to sqliteDB : %+s", err.Error())
|
||||
}
|
||||
}
|
||||
outputCTX.Flush()
|
||||
log.Infof("Wrote %d bans from api to database.", len(ret))
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewAPICmd() *cobra.Command {
|
||||
|
||||
var cmdAPI = &cobra.Command{
|
||||
Use: "api [action]",
|
||||
Short: "Crowdsec API interaction",
|
||||
Long: `
|
||||
Allow to register your machine into crowdsec API to send and receive signal.
|
||||
`,
|
||||
Example: `
|
||||
cscli api register # Register to Crowdsec API
|
||||
cscli api pull # Pull malevolant IPs from Crowdsec API
|
||||
cscli api reset # Reset your machines credentials
|
||||
cscli api enroll # Enroll your machine to the user account you created on Crowdsec backend
|
||||
cscli api credentials # Display your API credentials
|
||||
`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
if !config.configured {
|
||||
return fmt.Errorf("you must configure cli before interacting with hub")
|
||||
}
|
||||
|
||||
outputConfig := outputs.OutputFactory{
|
||||
BackendFolder: config.BackendPluginFolder,
|
||||
}
|
||||
outputCTX, err = outputs.NewOutput(&outputConfig, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = outputCTX.LoadAPIConfig(path.Join(config.InstallFolder, apiConfigFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var cmdAPIRegister = &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register on Crowdsec API",
|
||||
Long: `This command will register your machine to crowdsec API to allow you to receive list of malveolent IPs.
|
||||
The printed machine_id and password should be added to your api.yaml file.`,
|
||||
Example: `cscli api register`,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
id, err := machineid.ID()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get machine id: %s", err)
|
||||
}
|
||||
password := generatePassword()
|
||||
|
||||
if err := outputCTX.API.RegisterMachine(id, password); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
fmt.Printf("machine_id: %s\n", outputCTX.API.Creds.User)
|
||||
fmt.Printf("password: %s\n", outputCTX.API.Creds.Password)
|
||||
},
|
||||
}
|
||||
|
||||
var cmdAPIEnroll = &cobra.Command{
|
||||
Use: "enroll",
|
||||
Short: "Associate your machine to an existing crowdsec user",
|
||||
Long: `Enrolling your machine into your user account will allow for more accurate lists and threat detection. See website to create user account.`,
|
||||
Example: `cscli api enroll -u 1234567890ffff`,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := outputCTX.API.Signin(); err != nil {
|
||||
log.Fatalf("unable to signin : %s", err)
|
||||
}
|
||||
if err := outputCTX.API.Enroll(userID); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var cmdAPIResetPassword = &cobra.Command{
|
||||
Use: "reset",
|
||||
Short: "Reset password on CrowdSec API",
|
||||
Long: `Attempts to reset your credentials to the API.`,
|
||||
Example: `cscli api reset`,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
id, err := machineid.ID()
|
||||
if err != nil {
|
||||
log.Fatalf("failed to get machine id: %s", err)
|
||||
}
|
||||
password := generatePassword()
|
||||
if err := outputCTX.API.ResetPassword(id, password); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
fmt.Printf("machine_id: %s\n", outputCTX.API.Creds.User)
|
||||
fmt.Printf("password: %s\n", outputCTX.API.Creds.Password)
|
||||
},
|
||||
}
|
||||
|
||||
var cmdAPIPull = &cobra.Command{
|
||||
Use: "pull",
|
||||
Short: "Pull crowdsec API TopX",
|
||||
Long: `Pulls a list of malveolent IPs relevant to your situation and add them into the local ban database.`,
|
||||
Example: `cscli api pull`,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := cwhub.GetHubIdx(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
err := pullTOP()
|
||||
if err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
var cmdAPICreds = &cobra.Command{
|
||||
Use: "credentials",
|
||||
Short: "Display api credentials",
|
||||
Long: ``,
|
||||
Example: `cscli api credentials`,
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := dumpCredentials(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
cmdAPI.AddCommand(cmdAPICreds)
|
||||
cmdAPIEnroll.Flags().StringVarP(&userID, "user", "u", "", "User ID (required)")
|
||||
if err := cmdAPIEnroll.MarkFlagRequired("user"); err != nil {
|
||||
log.Errorf("'user' flag : %s", err)
|
||||
}
|
||||
cmdAPI.AddCommand(cmdAPIEnroll)
|
||||
cmdAPI.AddCommand(cmdAPIResetPassword)
|
||||
cmdAPI.AddCommand(cmdAPIRegister)
|
||||
cmdAPI.AddCommand(cmdAPIPull)
|
||||
return cmdAPI
|
||||
}
|
|
@ -1,473 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwapi"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/outputs"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//it's a rip of the cli version, but in silent-mode
|
||||
func silenceInstallItem(name string, obtype string) (string, error) {
|
||||
for _, it := range cwhub.HubIdx[obtype] {
|
||||
if it.Name == name {
|
||||
if download_only && it.Downloaded && it.UpToDate {
|
||||
return fmt.Sprintf("%s is already downloaded and up-to-date", it.Name), nil
|
||||
}
|
||||
it, err := cwhub.DownloadLatest(it, cwhub.Hubdir, force_install)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error while downloading %s : %v", it.Name, err)
|
||||
}
|
||||
cwhub.HubIdx[obtype][it.Name] = it
|
||||
if download_only {
|
||||
return fmt.Sprintf("Downloaded %s to %s", it.Name, cwhub.Hubdir+"/"+it.RemotePath), nil
|
||||
}
|
||||
it, err = cwhub.EnableItem(it, cwhub.Installdir, cwhub.Hubdir)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("error while enabled %s : %v", it.Name, err)
|
||||
}
|
||||
cwhub.HubIdx[obtype][it.Name] = it
|
||||
|
||||
return fmt.Sprintf("Enabled %s", it.Name), nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("%s not found in hub index", name)
|
||||
}
|
||||
|
||||
/*help to copy the file, ioutil doesn't offer the feature*/
|
||||
|
||||
func copyFileContents(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer func() {
|
||||
cerr := out.Close()
|
||||
if err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
err = out.Sync()
|
||||
return
|
||||
}
|
||||
|
||||
/*copy the file, ioutile doesn't offer the feature*/
|
||||
func copyFile(sourceSymLink, destinationFile string) (err error) {
|
||||
|
||||
sourceFile, err := filepath.EvalSymlinks(sourceSymLink)
|
||||
if err != nil {
|
||||
log.Infof("Not a symlink : %s", err)
|
||||
sourceFile = sourceSymLink
|
||||
}
|
||||
|
||||
sourceFileStat, err := os.Stat(sourceFile)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
// cannot copy non-regular files (e.g., directories,
|
||||
// symlinks, devices, etc.)
|
||||
return fmt.Errorf("copyFile: non-regular source file %s (%q)", sourceFileStat.Name(), sourceFileStat.Mode().String())
|
||||
}
|
||||
destinationFileStat, err := os.Stat(destinationFile)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if !(destinationFileStat.Mode().IsRegular()) {
|
||||
return fmt.Errorf("copyFile: non-regular destination file %s (%q)", destinationFileStat.Name(), destinationFileStat.Mode().String())
|
||||
}
|
||||
if os.SameFile(sourceFileStat, destinationFileStat) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if err = os.Link(sourceFile, destinationFile); err == nil {
|
||||
return
|
||||
}
|
||||
err = copyFileContents(sourceFile, destinationFile)
|
||||
return
|
||||
}
|
||||
|
||||
/*given a backup directory, restore configs (parser,collections..) both tainted and untainted.
|
||||
as well attempts to restore api credentials after verifying the existing ones aren't good
|
||||
finally restores the acquis.yaml file*/
|
||||
func restoreFromDirectory(source string) error {
|
||||
var err error
|
||||
/*backup scenarios etc.*/
|
||||
for _, itype := range cwhub.ItemTypes {
|
||||
itemDirectory := fmt.Sprintf("%s/%s/", source, itype)
|
||||
if _, err = os.Stat(itemDirectory); err != nil {
|
||||
log.Infof("no %s in backup", itype)
|
||||
continue
|
||||
}
|
||||
/*restore the upstream items*/
|
||||
upstreamListFN := fmt.Sprintf("%s/upstream-%s.json", itemDirectory, itype)
|
||||
file, err := ioutil.ReadFile(upstreamListFN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while opening %s : %s", upstreamListFN, err)
|
||||
}
|
||||
var upstreamList []string
|
||||
err = json.Unmarshal([]byte(file), &upstreamList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshaling %s : %s", upstreamListFN, err)
|
||||
}
|
||||
for _, toinstall := range upstreamList {
|
||||
label, err := silenceInstallItem(toinstall, itype)
|
||||
if err != nil {
|
||||
log.Errorf("Error while installing %s : %s", toinstall, err)
|
||||
} else if label != "" {
|
||||
log.Infof("Installed %s : %s", toinstall, label)
|
||||
} else {
|
||||
log.Printf("Installed %s : ok", toinstall)
|
||||
}
|
||||
}
|
||||
/*restore the local and tainted items*/
|
||||
files, err := ioutil.ReadDir(itemDirectory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed enumerating files of %s : %s", itemDirectory, err)
|
||||
}
|
||||
for _, file := range files {
|
||||
//dir are stages, keep track
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
stage := file.Name()
|
||||
stagedir := fmt.Sprintf("%s/%s/%s/", config.InstallFolder, itype, stage)
|
||||
log.Debugf("Found stage %s in %s, target directory : %s", stage, itype, stagedir)
|
||||
if err = os.MkdirAll(stagedir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("error while creating stage directory %s : %s", stagedir, err)
|
||||
}
|
||||
/*find items*/
|
||||
ifiles, err := ioutil.ReadDir(itemDirectory + "/" + stage + "/")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed enumerating files of %s : %s", itemDirectory+"/"+stage, err)
|
||||
}
|
||||
//finaly copy item
|
||||
for _, tfile := range ifiles {
|
||||
log.Infof("Going to restore local/tainted [%s]", tfile.Name())
|
||||
sourceFile := fmt.Sprintf("%s/%s/%s", itemDirectory, stage, tfile.Name())
|
||||
destinationFile := fmt.Sprintf("%s%s", stagedir, tfile.Name())
|
||||
if err = copyFile(sourceFile, destinationFile); err != nil {
|
||||
return fmt.Errorf("failed copy %s %s to %s : %s", itype, sourceFile, destinationFile, err)
|
||||
} else {
|
||||
log.Infof("restored %s to %s", sourceFile, destinationFile)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
/*restore api credentials*/
|
||||
//check if credentials exists :
|
||||
// - if no, restore
|
||||
// - if yes, try them :
|
||||
// - if it works, left untouched
|
||||
// - if not, restore
|
||||
// -> try login
|
||||
if err := restoreAPICreds(source); err != nil {
|
||||
return fmt.Errorf("failed to restore api credentials : %s", err)
|
||||
}
|
||||
/*
|
||||
Restore acquis
|
||||
*/
|
||||
yamlAcquisFile := fmt.Sprintf("%s/acquis.yaml", config.InstallFolder)
|
||||
bac := fmt.Sprintf("%s/acquis.yaml", source)
|
||||
if err = copyFile(bac, yamlAcquisFile); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s : %s", bac, yamlAcquisFile, err)
|
||||
}
|
||||
log.Infof("Restore acquis to %s", yamlAcquisFile)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func restoreAPICreds(source string) error {
|
||||
var err error
|
||||
|
||||
/*check existing configuration*/
|
||||
apiyaml := path.Join(config.InstallFolder, apiConfigFile)
|
||||
|
||||
api := &cwapi.ApiCtx{}
|
||||
if err = api.LoadConfig(apiyaml); err != nil {
|
||||
return fmt.Errorf("unable to load api config %s : %s", apiyaml, err)
|
||||
}
|
||||
if api.Creds.User != "" {
|
||||
log.Infof("Credentials present in existing configuration, try before override")
|
||||
err := api.Signin()
|
||||
if err == nil {
|
||||
log.Infof("Credentials present allow authentication, don't override !")
|
||||
return nil
|
||||
} else {
|
||||
log.Infof("Credentials aren't valid : %s", err)
|
||||
}
|
||||
}
|
||||
/*existing config isn't good, override it !*/
|
||||
ret, err := ioutil.ReadFile(path.Join(source, "api_creds.json"))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read api creds from save : %s", err)
|
||||
}
|
||||
if err := json.Unmarshal(ret, &api.Creds); err != nil {
|
||||
return fmt.Errorf("failed unmarshaling saved credentials : %s", err)
|
||||
}
|
||||
api.CfgUser = api.Creds.User
|
||||
api.CfgPassword = api.Creds.Password
|
||||
/*override the existing yaml file*/
|
||||
if err := api.WriteConfig(apiyaml); err != nil {
|
||||
return fmt.Errorf("failed writing to %s : %s", apiyaml, err)
|
||||
} else {
|
||||
log.Infof("Overwritting %s with backup info", apiyaml)
|
||||
}
|
||||
|
||||
/*reload to check everything is safe*/
|
||||
if err = api.LoadConfig(apiyaml); err != nil {
|
||||
return fmt.Errorf("unable to load api config %s : %s", apiyaml, err)
|
||||
}
|
||||
|
||||
if err := api.Signin(); err != nil {
|
||||
log.Errorf("Failed to authenticate after credentials restaurtion : %v", err)
|
||||
} else {
|
||||
log.Infof("Successfully auth to API after credentials restauration")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func backupToDirectory(target string) error {
|
||||
var itemDirectory string
|
||||
var upstreamParsers []string
|
||||
var err error
|
||||
if target == "" {
|
||||
return fmt.Errorf("target directory can't be empty")
|
||||
}
|
||||
log.Warningf("Starting configuration backup")
|
||||
_, err = os.Stat(target)
|
||||
if err == nil {
|
||||
return fmt.Errorf("%s already exists", target)
|
||||
}
|
||||
if err = os.MkdirAll(target, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("error while creating %s : %s", target, err)
|
||||
}
|
||||
/*
|
||||
backup configurations :
|
||||
- parers, scenarios, collections, postoverflows
|
||||
*/
|
||||
|
||||
for _, itemType := range cwhub.ItemTypes {
|
||||
clog := log.WithFields(log.Fields{
|
||||
"type": itemType,
|
||||
})
|
||||
if _, ok := cwhub.HubIdx[itemType]; ok {
|
||||
itemDirectory = fmt.Sprintf("%s/%s/", target, itemType)
|
||||
if err := os.MkdirAll(itemDirectory, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("error while creating %s : %s", itemDirectory, err)
|
||||
}
|
||||
upstreamParsers = []string{}
|
||||
stage := ""
|
||||
for k, v := range cwhub.HubIdx[itemType] {
|
||||
clog = clog.WithFields(log.Fields{
|
||||
"file": v.Name,
|
||||
})
|
||||
if !v.Installed { //only backup installed ones
|
||||
clog.Debugf("[%s] : not installed", k)
|
||||
continue
|
||||
}
|
||||
|
||||
//for the local/tainted ones, we backup the full file
|
||||
if v.Tainted || v.Local || !v.UpToDate {
|
||||
//we need to backup stages for parsers
|
||||
if itemType == cwhub.PARSERS || itemType == cwhub.PARSERS_OVFLW {
|
||||
tmp := strings.Split(v.LocalPath, "/")
|
||||
stage = "/" + tmp[len(tmp)-2] + "/"
|
||||
fstagedir := fmt.Sprintf("%s%s", itemDirectory, stage)
|
||||
if err := os.MkdirAll(fstagedir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("error while creating stage dir %s : %s", fstagedir, err)
|
||||
}
|
||||
}
|
||||
clog.Debugf("[%s] : backuping file (tainted:%t local:%t up-to-date:%t)", k, v.Tainted, v.Local, v.UpToDate)
|
||||
tfile := fmt.Sprintf("%s%s%s", itemDirectory, stage, v.FileName)
|
||||
//clog.Infof("item : %s", spew.Sdump(v))
|
||||
if err = copyFile(v.LocalPath, tfile); err != nil {
|
||||
return fmt.Errorf("failed copy %s %s to %s : %s", itemType, v.LocalPath, tfile, err)
|
||||
}
|
||||
clog.Infof("local/tainted saved %s to %s", v.LocalPath, tfile)
|
||||
continue
|
||||
}
|
||||
clog.Debugf("[%s] : from hub, just backup name (up-to-date:%t)", k, v.UpToDate)
|
||||
clog.Infof("saving, version:%s, up-to-date:%t", v.Version, v.UpToDate)
|
||||
upstreamParsers = append(upstreamParsers, v.Name)
|
||||
}
|
||||
//write the upstream items
|
||||
upstreamParsersFname := fmt.Sprintf("%s/upstream-%s.json", itemDirectory, itemType)
|
||||
upstreamParsersContent, err := json.MarshalIndent(upstreamParsers, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshaling upstream parsers : %s", err)
|
||||
}
|
||||
err = ioutil.WriteFile(upstreamParsersFname, upstreamParsersContent, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to %s %s : %s", itemType, upstreamParsersFname, err)
|
||||
}
|
||||
clog.Infof("Wrote %d entries for %s to %s", len(upstreamParsers), itemType, upstreamParsersFname)
|
||||
|
||||
} else {
|
||||
clog.Infof("No %s to backup.", itemType)
|
||||
}
|
||||
}
|
||||
/*
|
||||
Backup acquis
|
||||
*/
|
||||
yamlAcquisFile := fmt.Sprintf("%s/acquis.yaml", config.InstallFolder)
|
||||
bac := fmt.Sprintf("%s/acquis.yaml", target)
|
||||
if err = copyFile(yamlAcquisFile, bac); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s : %s", yamlAcquisFile, bac, err)
|
||||
}
|
||||
log.Infof("Saved acquis to %s", bac)
|
||||
/*
|
||||
Backup default.yaml
|
||||
*/
|
||||
defyaml := fmt.Sprintf("%s/default.yaml", config.InstallFolder)
|
||||
bac = fmt.Sprintf("%s/default.yaml", target)
|
||||
if err = copyFile(defyaml, bac); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s : %s", yamlAcquisFile, bac, err)
|
||||
}
|
||||
log.Infof("Saved default yaml to %s", bac)
|
||||
/*
|
||||
Backup API info
|
||||
*/
|
||||
if outputCTX == nil {
|
||||
log.Fatalf("no API output context, won't save api credentials")
|
||||
}
|
||||
outputCTX.API = &cwapi.ApiCtx{}
|
||||
if err = outputCTX.API.LoadConfig(path.Join(config.InstallFolder, apiConfigFile)); err != nil {
|
||||
return fmt.Errorf("unable to load api config %s : %s", path.Join(config.InstallFolder, apiConfigFile), err)
|
||||
}
|
||||
credsYaml, err := json.Marshal(&outputCTX.API.Creds)
|
||||
if err != nil {
|
||||
log.Fatalf("can't marshal credentials : %v", err)
|
||||
}
|
||||
apiCredsDumped := fmt.Sprintf("%s/api_creds.json", target)
|
||||
err = ioutil.WriteFile(apiCredsDumped, credsYaml, 0600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write credentials to %s : %s", apiCredsDumped, err)
|
||||
}
|
||||
log.Infof("Saved configuration to %s", target)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBackupCmd() *cobra.Command {
|
||||
var cmdBackup = &cobra.Command{
|
||||
Use: "backup [save|restore] <directory>",
|
||||
Short: "Backup or restore configuration (api, parsers, scenarios etc.) to/from directory",
|
||||
Long: `This command is here to help you save and/or restore crowdsec configurations to simple replication`,
|
||||
Example: `cscli backup save ./my-backup
|
||||
cscli backup restore ./my-backup`,
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !config.configured {
|
||||
return fmt.Errorf("you must configure cli before interacting with hub")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
var cmdBackupSave = &cobra.Command{
|
||||
Use: "save <directory>",
|
||||
Short: "Backup configuration (api, parsers, scenarios etc.) to directory",
|
||||
Long: `backup command will try to save all relevant informations to crowdsec config, including :
|
||||
|
||||
- List of scenarios, parsers, postoverflows and collections that are up-to-date
|
||||
|
||||
- Actual backup of tainted/local/out-of-date scenarios, parsers, postoverflows and collections
|
||||
|
||||
- Backup of API credentials
|
||||
|
||||
- Backup of acqusition configuration
|
||||
|
||||
`,
|
||||
Example: `cscli backup save ./my-backup`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !config.configured {
|
||||
return fmt.Errorf("you must configure cli before interacting with hub")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
outputConfig := outputs.OutputFactory{
|
||||
BackendFolder: config.BackendPluginFolder,
|
||||
}
|
||||
outputCTX, err = outputs.NewOutput(&outputConfig, false)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load output plugins")
|
||||
}
|
||||
if err := cwhub.GetHubIdx(); err != nil {
|
||||
log.Fatalf("Failed to get Hub index : %v", err)
|
||||
}
|
||||
if err := backupToDirectory(args[0]); err != nil {
|
||||
log.Fatalf("Failed backuping to %s : %s", args[0], err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdBackup.AddCommand(cmdBackupSave)
|
||||
|
||||
var cmdBackupRestore = &cobra.Command{
|
||||
Use: "restore <directory>",
|
||||
Short: "Restore configuration (api, parsers, scenarios etc.) from directory",
|
||||
Long: `restore command will try to restore all saved information from <directory> to yor local setup, including :
|
||||
|
||||
- Installation of up-to-date scenarios/parsers/... via cscli
|
||||
|
||||
- Restauration of tainted/local/out-of-date scenarios/parsers/... file
|
||||
|
||||
- Restauration of API credentials (if the existing ones aren't working)
|
||||
|
||||
- Restauration of acqusition configuration
|
||||
`,
|
||||
Example: `cscli backup restore ./my-backup`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if !config.configured {
|
||||
return fmt.Errorf("you must configure cli before interacting with hub")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
var err error
|
||||
|
||||
outputConfig := outputs.OutputFactory{
|
||||
BackendFolder: config.BackendPluginFolder,
|
||||
}
|
||||
outputCTX, err = outputs.NewOutput(&outputConfig, false)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to load output plugins")
|
||||
}
|
||||
|
||||
if err := cwhub.GetHubIdx(); err != nil {
|
||||
log.Fatalf("failed to get Hub index : %v", err)
|
||||
}
|
||||
if err := restoreFromDirectory(args[0]); err != nil {
|
||||
log.Fatalf("failed restoring from %s : %s", args[0], err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdBackup.AddCommand(cmdBackupRestore)
|
||||
|
||||
return cmdBackup
|
||||
}
|
|
@ -1,317 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/outputs"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/parser"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
|
||||
"github.com/olekukonko/tablewriter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var remediationType string
|
||||
var atTime string
|
||||
var all bool
|
||||
|
||||
func simpleBanToSignal(targetIP string, reason string, expirationStr string, action string, asName string, asNum string, country string, banSource string) (types.SignalOccurence, error) {
|
||||
var signalOcc types.SignalOccurence
|
||||
|
||||
expiration, err := time.ParseDuration(expirationStr)
|
||||
if err != nil {
|
||||
return signalOcc, err
|
||||
}
|
||||
|
||||
asOrgInt := 0
|
||||
if asNum != "" {
|
||||
asOrgInt, err = strconv.Atoi(asNum)
|
||||
if err != nil {
|
||||
log.Infof("Invalid as value %s : %s", asNum, err)
|
||||
}
|
||||
}
|
||||
|
||||
banApp := types.BanApplication{
|
||||
MeasureSource: banSource,
|
||||
MeasureType: action,
|
||||
Until: time.Now().Add(expiration),
|
||||
IpText: targetIP,
|
||||
TargetCN: country,
|
||||
TargetAS: asOrgInt,
|
||||
TargetASName: asName,
|
||||
Reason: reason,
|
||||
}
|
||||
var parsedIP net.IP
|
||||
var parsedRange *net.IPNet
|
||||
if strings.Contains(targetIP, "/") {
|
||||
if _, parsedRange, err = net.ParseCIDR(targetIP); err != nil {
|
||||
return signalOcc, fmt.Errorf("'%s' is not a valid CIDR", targetIP)
|
||||
}
|
||||
if parsedRange == nil {
|
||||
return signalOcc, fmt.Errorf("unable to parse network : %s", err)
|
||||
}
|
||||
banApp.StartIp = types.IP2Int(parsedRange.IP)
|
||||
banApp.EndIp = types.IP2Int(types.LastAddress(parsedRange))
|
||||
} else {
|
||||
parsedIP = net.ParseIP(targetIP)
|
||||
if parsedIP == nil {
|
||||
return signalOcc, fmt.Errorf("'%s' is not a valid IP", targetIP)
|
||||
}
|
||||
}
|
||||
|
||||
var banApps = make([]types.BanApplication, 1)
|
||||
banApps = append(banApps, banApp)
|
||||
signalOcc = types.SignalOccurence{
|
||||
Scenario: reason,
|
||||
Events_count: 1,
|
||||
Start_at: time.Now(),
|
||||
Stop_at: time.Now(),
|
||||
BanApplications: banApps,
|
||||
Source_ip: targetIP,
|
||||
Source_AutonomousSystemNumber: asNum,
|
||||
Source_AutonomousSystemOrganization: asName,
|
||||
Source_Country: country,
|
||||
}
|
||||
return signalOcc, nil
|
||||
}
|
||||
|
||||
func BanList() error {
|
||||
at := time.Now()
|
||||
if atTime != "" {
|
||||
_, at = parser.GenDateParse(atTime)
|
||||
if at.IsZero() {
|
||||
return fmt.Errorf("unable to parse date '%s'", atTime)
|
||||
}
|
||||
}
|
||||
ret, err := outputCTX.ReadAT(at)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get records from sqlite : %v", err)
|
||||
}
|
||||
if config.output == "json" {
|
||||
x, _ := json.MarshalIndent(ret, "", " ")
|
||||
fmt.Printf("%s", string(x))
|
||||
} else if config.output == "human" {
|
||||
|
||||
uniqAS := map[string]bool{}
|
||||
uniqCN := map[string]bool{}
|
||||
|
||||
table := tablewriter.NewWriter(os.Stdout)
|
||||
table.SetHeader([]string{"Source", "Ip", "Reason", "Bans", "Action", "Country", "AS", "Events", "Expiration"})
|
||||
|
||||
dispcount := 0
|
||||
totcount := 0
|
||||
apicount := 0
|
||||
for _, rm := range ret {
|
||||
if !all && rm["source"] == "api" {
|
||||
apicount++
|
||||
if _, ok := uniqAS[rm["as"]]; !ok {
|
||||
uniqAS[rm["as"]] = true
|
||||
}
|
||||
if _, ok := uniqCN[rm["cn"]]; !ok {
|
||||
uniqCN[rm["cn"]] = true
|
||||
}
|
||||
continue
|
||||
}
|
||||
if dispcount < 20 {
|
||||
table.Append([]string{rm["source"], rm["iptext"], rm["reason"], rm["bancount"], rm["action"], rm["cn"], rm["as"], rm["events_count"], rm["until"]})
|
||||
}
|
||||
totcount++
|
||||
dispcount++
|
||||
|
||||
}
|
||||
if dispcount > 0 {
|
||||
if !all {
|
||||
fmt.Printf("%d local decisions:\n", totcount)
|
||||
}
|
||||
table.Render() // Send output
|
||||
if dispcount > 20 {
|
||||
fmt.Printf("Additional records stripped.\n")
|
||||
}
|
||||
} else {
|
||||
fmt.Printf("No local decisions.\n")
|
||||
}
|
||||
if !all {
|
||||
fmt.Printf("And %d records from API, %d distinct AS, %d distinct countries\n", apicount, len(uniqAS), len(uniqCN))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func BanAdd(target string, duration string, reason string, action string) error {
|
||||
var signalOcc types.SignalOccurence
|
||||
var err error
|
||||
|
||||
signalOcc, err = simpleBanToSignal(target, reason, duration, action, "", "", "", "cli")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to insert ban : %v", err)
|
||||
}
|
||||
err = outputCTX.Insert(signalOcc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = outputCTX.Flush()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("Wrote ban to database.")
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewBanCmds() *cobra.Command {
|
||||
/*TODO : add a remediation type*/
|
||||
var cmdBan = &cobra.Command{
|
||||
Use: "ban [command] <target> <duration> <reason>",
|
||||
Short: "Manage bans/mitigations",
|
||||
Long: `This is the main interaction point with local ban database for humans.
|
||||
|
||||
You can add/delete/list or flush current bans in your local ban DB.`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
var err error
|
||||
if !config.configured {
|
||||
return fmt.Errorf("you must configure cli before using bans")
|
||||
}
|
||||
|
||||
outputConfig := outputs.OutputFactory{
|
||||
BackendFolder: config.BackendPluginFolder,
|
||||
}
|
||||
|
||||
outputCTX, err = outputs.NewOutput(&outputConfig, false)
|
||||
if err != nil {
|
||||
return fmt.Errorf(err.Error())
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmdBan.PersistentFlags().StringVar(&config.dbPath, "db", "", "Set path to SQLite DB.")
|
||||
cmdBan.PersistentFlags().StringVar(&remediationType, "remediation", "ban", "Set specific remediation type : ban|slow|captcha")
|
||||
cmdBan.Flags().SortFlags = false
|
||||
cmdBan.PersistentFlags().SortFlags = false
|
||||
|
||||
var cmdBanAdd = &cobra.Command{
|
||||
Use: "add [ip|range] <target> <duration> <reason>",
|
||||
Short: "Adds a ban against a given ip/range for the provided duration",
|
||||
Long: `
|
||||
Allows to add a ban against a specific ip or range target for a specific duration.
|
||||
|
||||
The duration argument can be expressed in seconds(s), minutes(m) or hours (h).
|
||||
|
||||
See [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) for more informations.`,
|
||||
Example: `cscli ban add ip 1.2.3.4 24h "scan"
|
||||
cscli ban add range 1.2.3.0/24 24h "the whole range"`,
|
||||
Args: cobra.MinimumNArgs(4),
|
||||
}
|
||||
cmdBan.AddCommand(cmdBanAdd)
|
||||
var cmdBanAddIp = &cobra.Command{
|
||||
Use: "ip <target> <duration> <reason>",
|
||||
Short: "Adds the specific ip to the ban db",
|
||||
Long: `Duration must be [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration), expressed in s/m/h.`,
|
||||
Example: `cscli ban add ip 1.2.3.4 12h "the scan"`,
|
||||
Args: cobra.ExactArgs(3),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := BanAdd(args[0], args[1], args[2], remediationType); err != nil {
|
||||
log.Fatalf("failed to add ban to sqlite : %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdBanAdd.AddCommand(cmdBanAddIp)
|
||||
var cmdBanAddRange = &cobra.Command{
|
||||
Use: "range <target> <duration> <reason>",
|
||||
Short: "Adds the specific ip to the ban db",
|
||||
Long: `Duration must be [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) compatible, expressed in s/m/h.`,
|
||||
Example: `cscli ban add range 1.2.3.0/24 12h "the whole range"`,
|
||||
Args: cobra.ExactArgs(3),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := BanAdd(args[0], args[1], args[2], remediationType); err != nil {
|
||||
log.Fatalf("failed to add ban to sqlite : %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdBanAdd.AddCommand(cmdBanAddRange)
|
||||
var cmdBanDel = &cobra.Command{
|
||||
Use: "del [command] <target>",
|
||||
Short: "Delete bans from db",
|
||||
Long: "The removal of the bans can be applied on a single IP address or directly on a IP range.",
|
||||
Example: `cscli ban del ip 1.2.3.4
|
||||
cscli ban del range 1.2.3.0/24`,
|
||||
Args: cobra.MinimumNArgs(2),
|
||||
}
|
||||
cmdBan.AddCommand(cmdBanDel)
|
||||
|
||||
var cmdBanFlush = &cobra.Command{
|
||||
Use: "flush",
|
||||
Short: "Fush ban DB",
|
||||
Example: `cscli ban flush`,
|
||||
Args: cobra.NoArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := outputCTX.DeleteAll(); err != nil {
|
||||
log.Fatalf(err.Error())
|
||||
}
|
||||
log.Printf("Ban DB flushed")
|
||||
},
|
||||
}
|
||||
cmdBan.AddCommand(cmdBanFlush)
|
||||
var cmdBanDelIp = &cobra.Command{
|
||||
Use: "ip <target>",
|
||||
Short: "Delete bans for given ip from db",
|
||||
Example: `cscli ban del ip 1.2.3.4`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
count, err := outputCTX.Delete(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("failed to delete %s : %v", args[0], err)
|
||||
}
|
||||
log.Infof("Deleted %d entries", count)
|
||||
},
|
||||
}
|
||||
cmdBanDel.AddCommand(cmdBanDelIp)
|
||||
var cmdBanDelRange = &cobra.Command{
|
||||
Use: "range <target>",
|
||||
Short: "Delete bans for given ip from db",
|
||||
Example: `cscli ban del range 1.2.3.0/24`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
count, err := outputCTX.Delete(args[0])
|
||||
if err != nil {
|
||||
log.Fatalf("failed to delete %s : %v", args[0], err)
|
||||
}
|
||||
log.Infof("Deleted %d entries", count)
|
||||
},
|
||||
}
|
||||
cmdBanDel.AddCommand(cmdBanDelRange)
|
||||
|
||||
var cmdBanList = &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "List local or api bans/remediations",
|
||||
Long: `List the bans, by default only local decisions.
|
||||
|
||||
If --all/-a is specified, api-provided bans will be displayed too.
|
||||
|
||||
Time can be specified with --at and support a variety of date formats:
|
||||
- Jan 2 15:04:05
|
||||
- Mon Jan 02 15:04:05.000000 2006
|
||||
- 2006-01-02T15:04:05Z07:00
|
||||
- 2006/01/02
|
||||
- 2006/01/02 15:04
|
||||
- 2006-01-02
|
||||
- 2006-01-02 15:04
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := BanList(); err != nil {
|
||||
log.Fatalf("failed to list bans : %v", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdBanList.PersistentFlags().StringVar(&atTime, "at", "", "List bans at given time")
|
||||
cmdBanList.PersistentFlags().BoolVarP(&all, "all", "a", false, "List as well bans received from API")
|
||||
cmdBan.AddCommand(cmdBanList)
|
||||
return cmdBan
|
||||
}
|
320
cmd/crowdsec-cli/bouncers.go
Normal file
320
cmd/crowdsec-cli/bouncers.go
Normal file
|
@ -0,0 +1,320 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/fatih/color"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
middlewares "github.com/crowdsecurity/crowdsec/pkg/apiserver/middlewares/v1"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
func askYesNo(message string, defaultAnswer bool) (bool, error) {
|
||||
var answer bool
|
||||
|
||||
prompt := &survey.Confirm{
|
||||
Message: message,
|
||||
Default: defaultAnswer,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return defaultAnswer, err
|
||||
}
|
||||
|
||||
return answer, nil
|
||||
}
|
||||
|
||||
type cliBouncers struct {
|
||||
db *database.Client
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLIBouncers(cfg configGetter) *cliBouncers {
|
||||
return &cliBouncers{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "bouncers [action]",
|
||||
Short: "Manage bouncers [requires local API]",
|
||||
Long: `To list/add/delete/prune bouncers.
|
||||
Note: This command requires database direct access, so is intended to be run on Local API/master.
|
||||
`,
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"bouncer"},
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
|
||||
cfg := cli.cfg()
|
||||
|
||||
if err = require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cli.db, err = database.NewClient(cfg.DbConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("can't connect to the database: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newListCmd())
|
||||
cmd.AddCommand(cli.newAddCmd())
|
||||
cmd.AddCommand(cli.newDeleteCmd())
|
||||
cmd.AddCommand(cli.newPruneCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) list() error {
|
||||
out := color.Output
|
||||
|
||||
bouncers, err := cli.db.ListBouncers()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to list bouncers: %w", err)
|
||||
}
|
||||
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "human":
|
||||
getBouncersTable(out, bouncers)
|
||||
case "json":
|
||||
enc := json.NewEncoder(out)
|
||||
enc.SetIndent("", " ")
|
||||
|
||||
if err := enc.Encode(bouncers); err != nil {
|
||||
return fmt.Errorf("failed to marshal: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(out)
|
||||
|
||||
if err := csvwriter.Write([]string{"name", "ip", "revoked", "last_pull", "type", "version", "auth_type"}); err != nil {
|
||||
return fmt.Errorf("failed to write raw header: %w", err)
|
||||
}
|
||||
|
||||
for _, b := range bouncers {
|
||||
valid := "validated"
|
||||
if b.Revoked {
|
||||
valid = "pending"
|
||||
}
|
||||
|
||||
if err := csvwriter.Write([]string{b.Name, b.IPAddress, valid, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType}); err != nil {
|
||||
return fmt.Errorf("failed to write raw: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) newListCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "list",
|
||||
Short: "list all bouncers within the database",
|
||||
Example: `cscli bouncers list`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.list()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) add(bouncerName string, key string) error {
|
||||
var err error
|
||||
|
||||
keyLength := 32
|
||||
|
||||
if key == "" {
|
||||
key, err = middlewares.GenerateAPIKey(keyLength)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate api key: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
_, err = cli.db.CreateBouncer(bouncerName, "", middlewares.HashSHA512(key), types.ApiKeyAuthType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create bouncer: %w", err)
|
||||
}
|
||||
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "human":
|
||||
fmt.Printf("API key for '%s':\n\n", bouncerName)
|
||||
fmt.Printf(" %s\n\n", key)
|
||||
fmt.Print("Please keep this key since you will not be able to retrieve it!\n")
|
||||
case "raw":
|
||||
fmt.Print(key)
|
||||
case "json":
|
||||
j, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
return errors.New("unable to marshal api key")
|
||||
}
|
||||
|
||||
fmt.Print(string(j))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) newAddCmd() *cobra.Command {
|
||||
var key string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add MyBouncerName",
|
||||
Short: "add a single bouncer to the database",
|
||||
Example: `cscli bouncers add MyBouncerName
|
||||
cscli bouncers add MyBouncerName --key <random-key>`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return cli.add(args[0], key)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringP("length", "l", "", "length of the api key")
|
||||
_ = flags.MarkDeprecated("length", "use --key instead")
|
||||
flags.StringVarP(&key, "key", "k", "", "api key for the bouncer")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) deleteValid(_ *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
|
||||
bouncers, err := cli.db.ListBouncers()
|
||||
if err != nil {
|
||||
cobra.CompError("unable to list bouncers " + err.Error())
|
||||
}
|
||||
|
||||
ret := []string{}
|
||||
|
||||
for _, bouncer := range bouncers {
|
||||
if strings.Contains(bouncer.Name, toComplete) && !slices.Contains(args, bouncer.Name) {
|
||||
ret = append(ret, bouncer.Name)
|
||||
}
|
||||
}
|
||||
|
||||
return ret, cobra.ShellCompDirectiveNoFileComp
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) delete(bouncers []string) error {
|
||||
for _, bouncerID := range bouncers {
|
||||
err := cli.db.DeleteBouncer(bouncerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete bouncer '%s': %w", bouncerID, err)
|
||||
}
|
||||
|
||||
log.Infof("bouncer '%s' deleted successfully", bouncerID)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) newDeleteCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete MyBouncerName",
|
||||
Short: "delete bouncer(s) from the database",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
Aliases: []string{"remove"},
|
||||
DisableAutoGenTag: true,
|
||||
ValidArgsFunction: cli.deleteValid,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
return cli.delete(args)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) prune(duration time.Duration, force bool) error {
|
||||
if duration < 2*time.Minute {
|
||||
if yes, err := askYesNo(
|
||||
"The duration you provided is less than 2 minutes. " +
|
||||
"This may remove active bouncers. Continue?", false); err != nil {
|
||||
return err
|
||||
} else if !yes {
|
||||
fmt.Println("User aborted prune. No changes were made.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
bouncers, err := cli.db.QueryBouncersLastPulltimeLT(time.Now().UTC().Add(-duration))
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to query bouncers: %w", err)
|
||||
}
|
||||
|
||||
if len(bouncers) == 0 {
|
||||
fmt.Println("No bouncers to prune.")
|
||||
return nil
|
||||
}
|
||||
|
||||
getBouncersTable(color.Output, bouncers)
|
||||
|
||||
if !force {
|
||||
if yes, err := askYesNo(
|
||||
"You are about to PERMANENTLY remove the above bouncers from the database. " +
|
||||
"These will NOT be recoverable. Continue?", false); err != nil {
|
||||
return err
|
||||
} else if !yes {
|
||||
fmt.Println("User aborted prune. No changes were made.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
deleted, err := cli.db.BulkDeleteBouncers(bouncers)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to prune bouncers: %w", err)
|
||||
}
|
||||
|
||||
fmt.Fprintf(os.Stderr, "Successfully deleted %d bouncers\n", deleted)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliBouncers) newPruneCmd() *cobra.Command {
|
||||
var (
|
||||
duration time.Duration
|
||||
force bool
|
||||
)
|
||||
|
||||
const defaultDuration = 60 * time.Minute
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "prune",
|
||||
Short: "prune multiple bouncers from the database",
|
||||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
Example: `cscli bouncers prune -d 45m
|
||||
cscli bouncers prune -d 45m --force`,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.prune(duration, force)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.DurationVarP(&duration, "duration", "d", defaultDuration, "duration of time since last pull")
|
||||
flags.BoolVar(&force, "force", false, "force prune without asking for confirmation")
|
||||
|
||||
return cmd
|
||||
}
|
29
cmd/crowdsec-cli/bouncers_table.go
Normal file
29
cmd/crowdsec-cli/bouncers_table.go
Normal file
|
@ -0,0 +1,29 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/aquasecurity/table"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/database/ent"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/emoji"
|
||||
)
|
||||
|
||||
func getBouncersTable(out io.Writer, bouncers []*ent.Bouncer) {
|
||||
t := newLightTable(out)
|
||||
t.SetHeaders("Name", "IP Address", "Valid", "Last API pull", "Type", "Version", "Auth Type")
|
||||
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
||||
t.SetAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
||||
|
||||
for _, b := range bouncers {
|
||||
revoked := emoji.CheckMark
|
||||
if b.Revoked {
|
||||
revoked = emoji.Prohibited
|
||||
}
|
||||
|
||||
t.AddRow(b.Name, b.IPAddress, revoked, b.LastPull.Format(time.RFC3339), b.Type, b.Version, b.AuthType)
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
222
cmd/crowdsec-cli/capi.go
Normal file
222
cmd/crowdsec-cli/capi.go
Normal file
|
@ -0,0 +1,222 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
const (
|
||||
CAPIBaseURL = "https://api.crowdsec.net/"
|
||||
CAPIURLPrefix = "v3"
|
||||
)
|
||||
|
||||
type cliCapi struct {
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLICapi(cfg configGetter) *cliCapi {
|
||||
return &cliCapi{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliCapi) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "capi [action]",
|
||||
Short: "Manage interaction with Central API (CAPI)",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := require.CAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newRegisterCmd())
|
||||
cmd.AddCommand(cli.newStatusCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliCapi) register(capiUserPrefix string, outputFile string) error {
|
||||
cfg := cli.cfg()
|
||||
|
||||
capiUser, err := generateID(capiUserPrefix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to generate machine id: %w", err)
|
||||
}
|
||||
|
||||
password := strfmt.Password(generatePassword(passwordLength))
|
||||
|
||||
apiurl, err := url.Parse(types.CAPIBaseURL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to parse api url %s: %w", types.CAPIBaseURL, err)
|
||||
}
|
||||
|
||||
_, err = apiclient.RegisterClient(&apiclient.Config{
|
||||
MachineID: capiUser,
|
||||
Password: password,
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiurl,
|
||||
VersionPrefix: CAPIURLPrefix,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("api client register ('%s'): %w", types.CAPIBaseURL, err)
|
||||
}
|
||||
|
||||
log.Infof("Successfully registered to Central API (CAPI)")
|
||||
|
||||
var dumpFile string
|
||||
|
||||
switch {
|
||||
case outputFile != "":
|
||||
dumpFile = outputFile
|
||||
case cfg.API.Server.OnlineClient.CredentialsFilePath != "":
|
||||
dumpFile = cfg.API.Server.OnlineClient.CredentialsFilePath
|
||||
default:
|
||||
dumpFile = ""
|
||||
}
|
||||
|
||||
apiCfg := csconfig.ApiCredentialsCfg{
|
||||
Login: capiUser,
|
||||
Password: password.String(),
|
||||
URL: types.CAPIBaseURL,
|
||||
}
|
||||
|
||||
apiConfigDump, err := yaml.Marshal(apiCfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to marshal api credentials: %w", err)
|
||||
}
|
||||
|
||||
if dumpFile != "" {
|
||||
err = os.WriteFile(dumpFile, apiConfigDump, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("write api credentials in '%s' failed: %w", dumpFile, err)
|
||||
}
|
||||
|
||||
log.Infof("Central API credentials written to '%s'", dumpFile)
|
||||
} else {
|
||||
fmt.Println(string(apiConfigDump))
|
||||
}
|
||||
|
||||
log.Warning(ReloadMessage())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliCapi) newRegisterCmd() *cobra.Command {
|
||||
var (
|
||||
capiUserPrefix string
|
||||
outputFile string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "register",
|
||||
Short: "Register to Central API (CAPI)",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.register(capiUserPrefix, outputFile)
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().StringVarP(&outputFile, "file", "f", "", "output file destination")
|
||||
cmd.Flags().StringVar(&capiUserPrefix, "schmilblick", "", "set a schmilblick (use in tests only)")
|
||||
|
||||
if err := cmd.Flags().MarkHidden("schmilblick"); err != nil {
|
||||
log.Fatalf("failed to hide flag: %s", err)
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliCapi) status() error {
|
||||
cfg := cli.cfg()
|
||||
|
||||
if err := require.CAPIRegistered(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
password := strfmt.Password(cfg.API.Server.OnlineClient.Credentials.Password)
|
||||
|
||||
apiurl, err := url.Parse(cfg.API.Server.OnlineClient.Credentials.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing api url ('%s'): %w", cfg.API.Server.OnlineClient.Credentials.URL, err)
|
||||
}
|
||||
|
||||
hub, err := require.Hub(cfg, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get scenarios: %w", err)
|
||||
}
|
||||
|
||||
if len(scenarios) == 0 {
|
||||
return errors.New("no scenarios installed, abort")
|
||||
}
|
||||
|
||||
Client, err = apiclient.NewDefaultClient(apiurl, CAPIURLPrefix, fmt.Sprintf("crowdsec/%s", version.String()), nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("init default client: %w", err)
|
||||
}
|
||||
|
||||
t := models.WatcherAuthRequest{
|
||||
MachineID: &cfg.API.Server.OnlineClient.Credentials.Login,
|
||||
Password: &password,
|
||||
Scenarios: scenarios,
|
||||
}
|
||||
|
||||
log.Infof("Loaded credentials from %s", cfg.API.Server.OnlineClient.CredentialsFilePath)
|
||||
log.Infof("Trying to authenticate with username %s on %s", cfg.API.Server.OnlineClient.Credentials.Login, apiurl)
|
||||
|
||||
_, _, err = Client.Auth.AuthenticateWatcher(context.Background(), t)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to authenticate to Central API (CAPI): %w", err)
|
||||
}
|
||||
|
||||
log.Info("You can successfully interact with Central API (CAPI)")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliCapi) newStatusCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Check status with the Central API (CAPI)",
|
||||
Args: cobra.MinimumNArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.status()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
86
cmd/crowdsec-cli/completion.go
Normal file
86
cmd/crowdsec-cli/completion.go
Normal file
|
@ -0,0 +1,86 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func NewCompletionCmd() *cobra.Command {
|
||||
completionCmd := &cobra.Command{
|
||||
Use: "completion [bash|zsh|powershell|fish]",
|
||||
Short: "Generate completion script",
|
||||
Long: `To load completions:
|
||||
|
||||
### Bash:
|
||||
` + "```shell" + `
|
||||
$ source <(cscli completion bash)
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
|
||||
|
||||
# Linux:
|
||||
|
||||
$ cscli completion bash | sudo tee /etc/bash_completion.d/cscli
|
||||
$ source ~/.bashrc
|
||||
|
||||
# macOS:
|
||||
|
||||
$ cscli completion bash | sudo tee /usr/local/etc/bash_completion.d/cscli
|
||||
|
||||
# Troubleshoot:
|
||||
If you have this error (bash: _get_comp_words_by_ref: command not found), it seems that you need "bash-completion" dependency :
|
||||
|
||||
* Install bash-completion package
|
||||
$ source /etc/profile
|
||||
$ source <(cscli completion bash)
|
||||
` + "```" + `
|
||||
|
||||
### Zsh:
|
||||
` + "```shell" + `
|
||||
# If shell completion is not already enabled in your environment,
|
||||
# you will need to enable it. You can execute the following once:
|
||||
|
||||
$ echo "autoload -U compinit; compinit" >> ~/.zshrc
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
|
||||
$ cscli completion zsh > "${fpath[1]}/_cscli"
|
||||
|
||||
# You will need to start a new shell for this setup to take effect.
|
||||
|
||||
### fish:
|
||||
` + "```shell" + `
|
||||
$ cscli completion fish | source
|
||||
|
||||
# To load completions for each session, execute once:
|
||||
$ cscli completion fish > ~/.config/fish/completions/cscli.fish
|
||||
` + "```" + `
|
||||
### PowerShell:
|
||||
` + "```powershell" + `
|
||||
PS> cscli completion powershell | Out-String | Invoke-Expression
|
||||
|
||||
# To load completions for every new session, run:
|
||||
PS> cscli completion powershell > cscli.ps1
|
||||
# and source this file from your PowerShell profile.
|
||||
` + "```",
|
||||
DisableFlagsInUseLine: true,
|
||||
DisableAutoGenTag: true,
|
||||
ValidArgs: []string{"bash", "zsh", "powershell", "fish"},
|
||||
Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
switch args[0] {
|
||||
case "bash":
|
||||
cmd.Root().GenBashCompletion(os.Stdout)
|
||||
case "zsh":
|
||||
cmd.Root().GenZshCompletion(os.Stdout)
|
||||
case "powershell":
|
||||
cmd.Root().GenPowerShellCompletion(os.Stdout)
|
||||
case "fish":
|
||||
cmd.Root().GenFishCompletion(os.Stdout, true)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
return completionCmd
|
||||
}
|
|
@ -1,158 +1,32 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
/*CliCfg is the cli configuration structure, might be unexported*/
|
||||
type cliConfig struct {
|
||||
configured bool
|
||||
configFolder string `yaml:"cliconfig,omitempty"` /*overload ~/.cscli/*/
|
||||
output string /*output is human, json*/
|
||||
hubFolder string
|
||||
InstallFolder string `yaml:"installdir"` /*/etc/crowdsec/*/
|
||||
BackendPluginFolder string `yaml:"backend"`
|
||||
dbPath string
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func interactiveCfg() error {
|
||||
var err error
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("crowdsec installation directory (default: /etc/crowdsec/config/): ")
|
||||
config.InstallFolder, err = reader.ReadString('\n')
|
||||
config.InstallFolder = strings.Replace(config.InstallFolder, "\n", "", -1) //CRLF to LF (windows)
|
||||
if config.InstallFolder == "" {
|
||||
config.InstallFolder = "/etc/crowdsec/config/"
|
||||
func NewCLIConfig(cfg configGetter) *cliConfig {
|
||||
return &cliConfig{
|
||||
cfg: cfg,
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read input : %v", err.Error())
|
||||
}
|
||||
|
||||
fmt.Print("crowdsec backend plugin directory (default: /etc/crowdsec/plugin/backend): ")
|
||||
config.BackendPluginFolder, err = reader.ReadString('\n')
|
||||
config.BackendPluginFolder = strings.Replace(config.BackendPluginFolder, "\n", "", -1) //CRLF to LF (windows)
|
||||
if config.BackendPluginFolder == "" {
|
||||
config.BackendPluginFolder = "/etc/crowdsec/plugin/backend"
|
||||
}
|
||||
if err != nil {
|
||||
log.Fatalf("failed to read input : %v", err.Error())
|
||||
}
|
||||
if err := writeCfg(); err != nil {
|
||||
log.Fatalf("failed writting configuration file : %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeCfg() error {
|
||||
|
||||
if config.configFolder == "" {
|
||||
return fmt.Errorf("config dir is unset")
|
||||
func (cli *cliConfig) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "config [command]",
|
||||
Short: "Allows to view current config",
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
}
|
||||
|
||||
config.hubFolder = config.configFolder + "/hub/"
|
||||
if _, err := os.Stat(config.hubFolder); os.IsNotExist(err) {
|
||||
cmd.AddCommand(cli.newShowCmd())
|
||||
cmd.AddCommand(cli.newShowYAMLCmd())
|
||||
cmd.AddCommand(cli.newBackupCmd())
|
||||
cmd.AddCommand(cli.newRestoreCmd())
|
||||
cmd.AddCommand(cli.newFeatureFlagsCmd())
|
||||
|
||||
log.Warningf("creating skeleton!")
|
||||
if err := os.MkdirAll(config.hubFolder, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("failed to create missing directory : '%s'", config.hubFolder)
|
||||
}
|
||||
}
|
||||
out := path.Join(config.configFolder, "/config")
|
||||
configYaml, err := yaml.Marshal(&config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshaling config: %s", err)
|
||||
}
|
||||
err = ioutil.WriteFile(out, configYaml, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write to %s : %s", out, err)
|
||||
}
|
||||
log.Infof("wrote config to %s ", out)
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewConfigCmd() *cobra.Command {
|
||||
|
||||
var cmdConfig = &cobra.Command{
|
||||
Use: "config [command] <value>",
|
||||
Short: "Allows to view/edit cscli config",
|
||||
Long: `Allow to configure sqlite path and installation directory.
|
||||
If no commands are specified, config is in interactive mode.`,
|
||||
Example: ` - cscli config show
|
||||
- cscli config prompt`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
}
|
||||
var cmdConfigShow = &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Displays current config",
|
||||
Long: `Displays the current cli configuration.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if config.output == "json" {
|
||||
log.WithFields(log.Fields{
|
||||
"installdir": config.InstallFolder,
|
||||
"cliconfig": path.Join(config.configFolder, "/config"),
|
||||
}).Warning("Current config")
|
||||
} else {
|
||||
x, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to marshal current configuration : %v", err)
|
||||
}
|
||||
fmt.Printf("%s", x)
|
||||
fmt.Printf("#cliconfig: %s", path.Join(config.configFolder, "/config"))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdConfig.AddCommand(cmdConfigShow)
|
||||
var cmdConfigInterctive = &cobra.Command{
|
||||
Use: "prompt",
|
||||
Short: "Prompt for configuration values in an interactive fashion",
|
||||
Long: `Start interactive configuration of cli. It will successively ask for install dir, db path.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := interactiveCfg()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to run interactive config : %s", err)
|
||||
}
|
||||
log.Warningf("Configured, please run update.")
|
||||
},
|
||||
}
|
||||
cmdConfig.AddCommand(cmdConfigInterctive)
|
||||
var cmdConfigInstalldir = &cobra.Command{
|
||||
Use: "installdir [value]",
|
||||
Short: `Configure installation directory`,
|
||||
Long: `Configure the installation directory of crowdsec, such as /etc/crowdsec/config/`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config.InstallFolder = args[0]
|
||||
if err := writeCfg(); err != nil {
|
||||
log.Fatalf("failed writting configuration: %s", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdConfig.AddCommand(cmdConfigInstalldir)
|
||||
|
||||
var cmdConfigBackendFolder = &cobra.Command{
|
||||
Use: "backend [value]",
|
||||
Short: `Configure installation directory`,
|
||||
Long: `Configure the backend plugin directory of crowdsec, such as /etc/crowdsec/plugins/backend`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
config.BackendPluginFolder = args[0]
|
||||
if err := writeCfg(); err != nil {
|
||||
log.Fatalf("failed writting configuration: %s", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdConfig.AddCommand(cmdConfigBackendFolder)
|
||||
|
||||
return cmdConfig
|
||||
return cmd
|
||||
}
|
||||
|
|
240
cmd/crowdsec-cli/config_backup.go
Normal file
240
cmd/crowdsec-cli/config_backup.go
Normal file
|
@ -0,0 +1,240 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
)
|
||||
|
||||
func (cli *cliConfig) backupHub(dirPath string) error {
|
||||
hub, err := require.Hub(cli.cfg(), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, itemType := range cwhub.ItemTypes {
|
||||
clog := log.WithFields(log.Fields{
|
||||
"type": itemType,
|
||||
})
|
||||
|
||||
itemMap := hub.GetItemMap(itemType)
|
||||
if itemMap == nil {
|
||||
clog.Infof("No %s to backup.", itemType)
|
||||
continue
|
||||
}
|
||||
|
||||
itemDirectory := fmt.Sprintf("%s/%s/", dirPath, itemType)
|
||||
if err = os.MkdirAll(itemDirectory, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("error while creating %s: %w", itemDirectory, err)
|
||||
}
|
||||
|
||||
upstreamParsers := []string{}
|
||||
|
||||
for k, v := range itemMap {
|
||||
clog = clog.WithFields(log.Fields{
|
||||
"file": v.Name,
|
||||
})
|
||||
if !v.State.Installed { // only backup installed ones
|
||||
clog.Debugf("[%s]: not installed", k)
|
||||
continue
|
||||
}
|
||||
|
||||
// for the local/tainted ones, we back up the full file
|
||||
if v.State.Tainted || v.State.IsLocal() || !v.State.UpToDate {
|
||||
// we need to backup stages for parsers
|
||||
if itemType == cwhub.PARSERS || itemType == cwhub.POSTOVERFLOWS {
|
||||
fstagedir := fmt.Sprintf("%s%s", itemDirectory, v.Stage)
|
||||
if err = os.MkdirAll(fstagedir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("error while creating stage dir %s: %w", fstagedir, err)
|
||||
}
|
||||
}
|
||||
|
||||
clog.Debugf("[%s]: backing up file (tainted:%t local:%t up-to-date:%t)", k, v.State.Tainted, v.State.IsLocal(), v.State.UpToDate)
|
||||
|
||||
tfile := fmt.Sprintf("%s%s/%s", itemDirectory, v.Stage, v.FileName)
|
||||
if err = CopyFile(v.State.LocalPath, tfile); err != nil {
|
||||
return fmt.Errorf("failed copy %s %s to %s: %w", itemType, v.State.LocalPath, tfile, err)
|
||||
}
|
||||
|
||||
clog.Infof("local/tainted saved %s to %s", v.State.LocalPath, tfile)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
clog.Debugf("[%s]: from hub, just backup name (up-to-date:%t)", k, v.State.UpToDate)
|
||||
clog.Infof("saving, version:%s, up-to-date:%t", v.Version, v.State.UpToDate)
|
||||
upstreamParsers = append(upstreamParsers, v.Name)
|
||||
}
|
||||
// write the upstream items
|
||||
upstreamParsersFname := fmt.Sprintf("%s/upstream-%s.json", itemDirectory, itemType)
|
||||
|
||||
upstreamParsersContent, err := json.MarshalIndent(upstreamParsers, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed marshaling upstream parsers: %w", err)
|
||||
}
|
||||
|
||||
err = os.WriteFile(upstreamParsersFname, upstreamParsersContent, 0o644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to write to %s %s: %w", itemType, upstreamParsersFname, err)
|
||||
}
|
||||
|
||||
clog.Infof("Wrote %d entries for %s to %s", len(upstreamParsers), itemType, upstreamParsersFname)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Backup crowdsec configurations to directory <dirPath>:
|
||||
|
||||
- Main config (config.yaml)
|
||||
- Profiles config (profiles.yaml)
|
||||
- Simulation config (simulation.yaml)
|
||||
- Backup of API credentials (local API and online API)
|
||||
- List of scenarios, parsers, postoverflows and collections that are up-to-date
|
||||
- Tainted/local/out-of-date scenarios, parsers, postoverflows and collections
|
||||
- Acquisition files (acquis.yaml, acquis.d/*.yaml)
|
||||
*/
|
||||
func (cli *cliConfig) backup(dirPath string) error {
|
||||
var err error
|
||||
|
||||
cfg := cli.cfg()
|
||||
|
||||
if dirPath == "" {
|
||||
return errors.New("directory path can't be empty")
|
||||
}
|
||||
|
||||
log.Infof("Starting configuration backup")
|
||||
|
||||
/*if parent directory doesn't exist, bail out. create final dir with Mkdir*/
|
||||
parentDir := filepath.Dir(dirPath)
|
||||
if _, err = os.Stat(parentDir); err != nil {
|
||||
return fmt.Errorf("while checking parent directory %s existence: %w", parentDir, err)
|
||||
}
|
||||
|
||||
if err = os.Mkdir(dirPath, 0o700); err != nil {
|
||||
return fmt.Errorf("while creating %s: %w", dirPath, err)
|
||||
}
|
||||
|
||||
if cfg.ConfigPaths.SimulationFilePath != "" {
|
||||
backupSimulation := filepath.Join(dirPath, "simulation.yaml")
|
||||
if err = CopyFile(cfg.ConfigPaths.SimulationFilePath, backupSimulation); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", cfg.ConfigPaths.SimulationFilePath, backupSimulation, err)
|
||||
}
|
||||
|
||||
log.Infof("Saved simulation to %s", backupSimulation)
|
||||
}
|
||||
|
||||
/*
|
||||
- backup AcquisitionFilePath
|
||||
- backup the other files of acquisition directory
|
||||
*/
|
||||
if cfg.Crowdsec != nil && cfg.Crowdsec.AcquisitionFilePath != "" {
|
||||
backupAcquisition := filepath.Join(dirPath, "acquis.yaml")
|
||||
if err = CopyFile(cfg.Crowdsec.AcquisitionFilePath, backupAcquisition); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", cfg.Crowdsec.AcquisitionFilePath, backupAcquisition, err)
|
||||
}
|
||||
}
|
||||
|
||||
acquisBackupDir := filepath.Join(dirPath, "acquis")
|
||||
if err = os.Mkdir(acquisBackupDir, 0o700); err != nil {
|
||||
return fmt.Errorf("error while creating %s: %w", acquisBackupDir, err)
|
||||
}
|
||||
|
||||
if cfg.Crowdsec != nil && len(cfg.Crowdsec.AcquisitionFiles) > 0 {
|
||||
for _, acquisFile := range cfg.Crowdsec.AcquisitionFiles {
|
||||
/*if it was the default one, it was already backup'ed*/
|
||||
if cfg.Crowdsec.AcquisitionFilePath == acquisFile {
|
||||
continue
|
||||
}
|
||||
|
||||
targetFname, err := filepath.Abs(filepath.Join(acquisBackupDir, filepath.Base(acquisFile)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("while saving %s to %s: %w", acquisFile, acquisBackupDir, err)
|
||||
}
|
||||
|
||||
if err = CopyFile(acquisFile, targetFname); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", acquisFile, targetFname, err)
|
||||
}
|
||||
|
||||
log.Infof("Saved acquis %s to %s", acquisFile, targetFname)
|
||||
}
|
||||
}
|
||||
|
||||
if ConfigFilePath != "" {
|
||||
backupMain := fmt.Sprintf("%s/config.yaml", dirPath)
|
||||
if err = CopyFile(ConfigFilePath, backupMain); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", ConfigFilePath, backupMain, err)
|
||||
}
|
||||
|
||||
log.Infof("Saved default yaml to %s", backupMain)
|
||||
}
|
||||
|
||||
if cfg.API != nil && cfg.API.Server != nil && cfg.API.Server.OnlineClient != nil && cfg.API.Server.OnlineClient.CredentialsFilePath != "" {
|
||||
backupCAPICreds := fmt.Sprintf("%s/online_api_credentials.yaml", dirPath)
|
||||
if err = CopyFile(cfg.API.Server.OnlineClient.CredentialsFilePath, backupCAPICreds); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", cfg.API.Server.OnlineClient.CredentialsFilePath, backupCAPICreds, err)
|
||||
}
|
||||
|
||||
log.Infof("Saved online API credentials to %s", backupCAPICreds)
|
||||
}
|
||||
|
||||
if cfg.API != nil && cfg.API.Client != nil && cfg.API.Client.CredentialsFilePath != "" {
|
||||
backupLAPICreds := fmt.Sprintf("%s/local_api_credentials.yaml", dirPath)
|
||||
if err = CopyFile(cfg.API.Client.CredentialsFilePath, backupLAPICreds); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", cfg.API.Client.CredentialsFilePath, backupLAPICreds, err)
|
||||
}
|
||||
|
||||
log.Infof("Saved local API credentials to %s", backupLAPICreds)
|
||||
}
|
||||
|
||||
if cfg.API != nil && cfg.API.Server != nil && cfg.API.Server.ProfilesPath != "" {
|
||||
backupProfiles := fmt.Sprintf("%s/profiles.yaml", dirPath)
|
||||
if err = CopyFile(cfg.API.Server.ProfilesPath, backupProfiles); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", cfg.API.Server.ProfilesPath, backupProfiles, err)
|
||||
}
|
||||
|
||||
log.Infof("Saved profiles to %s", backupProfiles)
|
||||
}
|
||||
|
||||
if err = cli.backupHub(dirPath); err != nil {
|
||||
return fmt.Errorf("failed to backup hub config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliConfig) newBackupCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: `backup "directory"`,
|
||||
Short: "Backup current config",
|
||||
Long: `Backup the current crowdsec configuration including :
|
||||
|
||||
- Main config (config.yaml)
|
||||
- Simulation config (simulation.yaml)
|
||||
- Profiles config (profiles.yaml)
|
||||
- List of scenarios, parsers, postoverflows and collections that are up-to-date
|
||||
- Tainted/local/out-of-date scenarios, parsers, postoverflows and collections
|
||||
- Backup of API credentials (local API and online API)`,
|
||||
Example: `cscli config backup ./my-backup`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if err := cli.backup(args[0]); err != nil {
|
||||
return fmt.Errorf("failed to backup config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
135
cmd/crowdsec-cli/config_feature_flags.go
Normal file
135
cmd/crowdsec-cli/config_feature_flags.go
Normal file
|
@ -0,0 +1,135 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/fflag"
|
||||
)
|
||||
|
||||
func (cli *cliConfig) featureFlags(showRetired bool) error {
|
||||
green := color.New(color.FgGreen).SprintFunc()
|
||||
red := color.New(color.FgRed).SprintFunc()
|
||||
yellow := color.New(color.FgYellow).SprintFunc()
|
||||
magenta := color.New(color.FgMagenta).SprintFunc()
|
||||
|
||||
printFeature := func(feat fflag.Feature) {
|
||||
nameDesc := feat.Name
|
||||
if feat.Description != "" {
|
||||
nameDesc += ": " + feat.Description
|
||||
}
|
||||
|
||||
status := red("✗")
|
||||
if feat.IsEnabled() {
|
||||
status = green("✓")
|
||||
}
|
||||
|
||||
fmt.Printf("%s %s", status, nameDesc)
|
||||
|
||||
if feat.State == fflag.DeprecatedState {
|
||||
fmt.Printf("\n %s %s", yellow("DEPRECATED"), feat.DeprecationMsg)
|
||||
}
|
||||
|
||||
if feat.State == fflag.RetiredState {
|
||||
fmt.Printf("\n %s %s", magenta("RETIRED"), feat.DeprecationMsg)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
feats := fflag.Crowdsec.GetAllFeatures()
|
||||
|
||||
enabled := []fflag.Feature{}
|
||||
disabled := []fflag.Feature{}
|
||||
retired := []fflag.Feature{}
|
||||
|
||||
for _, feat := range feats {
|
||||
if feat.State == fflag.RetiredState {
|
||||
retired = append(retired, feat)
|
||||
continue
|
||||
}
|
||||
|
||||
if feat.IsEnabled() {
|
||||
enabled = append(enabled, feat)
|
||||
continue
|
||||
}
|
||||
|
||||
disabled = append(disabled, feat)
|
||||
}
|
||||
|
||||
if len(enabled) > 0 {
|
||||
fmt.Println(" --- Enabled features ---")
|
||||
fmt.Println()
|
||||
|
||||
for _, feat := range enabled {
|
||||
printFeature(feat)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if len(disabled) > 0 {
|
||||
fmt.Println(" --- Disabled features ---")
|
||||
fmt.Println()
|
||||
|
||||
for _, feat := range disabled {
|
||||
printFeature(feat)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
fmt.Println("To enable a feature you can: ")
|
||||
fmt.Println(" - set the environment variable CROWDSEC_FEATURE_<uppercase_feature_name> to true")
|
||||
|
||||
featurePath, err := filepath.Abs(csconfig.GetFeatureFilePath(ConfigFilePath))
|
||||
if err != nil {
|
||||
// we already read the file, shouldn't happen
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Printf(" - add the line '- <feature_name>' to the file %s\n", featurePath)
|
||||
fmt.Println()
|
||||
|
||||
if len(enabled) == 0 && len(disabled) == 0 {
|
||||
fmt.Println("However, no feature flag is available in this release.")
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
if showRetired && len(retired) > 0 {
|
||||
fmt.Println(" --- Retired features ---")
|
||||
fmt.Println()
|
||||
|
||||
for _, feat := range retired {
|
||||
printFeature(feat)
|
||||
}
|
||||
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliConfig) newFeatureFlagsCmd() *cobra.Command {
|
||||
var showRetired bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "feature-flags",
|
||||
Short: "Displays feature flag status",
|
||||
Long: `Displays the supported feature flags and their current status.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.featureFlags(showRetired)
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVar(&showRetired, "retired", false, "Show retired features")
|
||||
|
||||
return cmd
|
||||
}
|
273
cmd/crowdsec-cli/config_restore.go
Normal file
273
cmd/crowdsec-cli/config_restore.go
Normal file
|
@ -0,0 +1,273 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
)
|
||||
|
||||
func (cli *cliConfig) restoreHub(dirPath string) error {
|
||||
cfg := cli.cfg()
|
||||
|
||||
hub, err := require.Hub(cfg, require.RemoteHub(cfg), nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, itype := range cwhub.ItemTypes {
|
||||
itemDirectory := fmt.Sprintf("%s/%s/", dirPath, itype)
|
||||
if _, err = os.Stat(itemDirectory); err != nil {
|
||||
log.Infof("no %s in backup", itype)
|
||||
continue
|
||||
}
|
||||
/*restore the upstream items*/
|
||||
upstreamListFN := fmt.Sprintf("%s/upstream-%s.json", itemDirectory, itype)
|
||||
|
||||
file, err := os.ReadFile(upstreamListFN)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error while opening %s: %w", upstreamListFN, err)
|
||||
}
|
||||
|
||||
var upstreamList []string
|
||||
|
||||
err = json.Unmarshal(file, &upstreamList)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error unmarshaling %s: %w", upstreamListFN, err)
|
||||
}
|
||||
|
||||
for _, toinstall := range upstreamList {
|
||||
item := hub.GetItem(itype, toinstall)
|
||||
if item == nil {
|
||||
log.Errorf("Item %s/%s not found in hub", itype, toinstall)
|
||||
continue
|
||||
}
|
||||
|
||||
if err = item.Install(false, false); err != nil {
|
||||
log.Errorf("Error while installing %s : %s", toinstall, err)
|
||||
}
|
||||
}
|
||||
|
||||
/*restore the local and tainted items*/
|
||||
files, err := os.ReadDir(itemDirectory)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed enumerating files of %s: %w", itemDirectory, err)
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
// this was the upstream data
|
||||
if file.Name() == fmt.Sprintf("upstream-%s.json", itype) {
|
||||
continue
|
||||
}
|
||||
|
||||
if itype == cwhub.PARSERS || itype == cwhub.POSTOVERFLOWS {
|
||||
// we expect a stage here
|
||||
if !file.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
stage := file.Name()
|
||||
stagedir := fmt.Sprintf("%s/%s/%s/", cfg.ConfigPaths.ConfigDir, itype, stage)
|
||||
log.Debugf("Found stage %s in %s, target directory : %s", stage, itype, stagedir)
|
||||
|
||||
if err = os.MkdirAll(stagedir, os.ModePerm); err != nil {
|
||||
return fmt.Errorf("error while creating stage directory %s: %w", stagedir, err)
|
||||
}
|
||||
|
||||
// find items
|
||||
ifiles, err := os.ReadDir(itemDirectory + "/" + stage + "/")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed enumerating files of %s: %w", itemDirectory+"/"+stage, err)
|
||||
}
|
||||
|
||||
// finally copy item
|
||||
for _, tfile := range ifiles {
|
||||
log.Infof("Going to restore local/tainted [%s]", tfile.Name())
|
||||
sourceFile := fmt.Sprintf("%s/%s/%s", itemDirectory, stage, tfile.Name())
|
||||
|
||||
destinationFile := fmt.Sprintf("%s%s", stagedir, tfile.Name())
|
||||
if err = CopyFile(sourceFile, destinationFile); err != nil {
|
||||
return fmt.Errorf("failed copy %s %s to %s: %w", itype, sourceFile, destinationFile, err)
|
||||
}
|
||||
|
||||
log.Infof("restored %s to %s", sourceFile, destinationFile)
|
||||
}
|
||||
} else {
|
||||
log.Infof("Going to restore local/tainted [%s]", file.Name())
|
||||
sourceFile := fmt.Sprintf("%s/%s", itemDirectory, file.Name())
|
||||
destinationFile := fmt.Sprintf("%s/%s/%s", cfg.ConfigPaths.ConfigDir, itype, file.Name())
|
||||
|
||||
if err = CopyFile(sourceFile, destinationFile); err != nil {
|
||||
return fmt.Errorf("failed copy %s %s to %s: %w", itype, sourceFile, destinationFile, err)
|
||||
}
|
||||
|
||||
log.Infof("restored %s to %s", sourceFile, destinationFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Restore crowdsec configurations to directory <dirPath>:
|
||||
|
||||
- Main config (config.yaml)
|
||||
- Profiles config (profiles.yaml)
|
||||
- Simulation config (simulation.yaml)
|
||||
- Backup of API credentials (local API and online API)
|
||||
- List of scenarios, parsers, postoverflows and collections that are up-to-date
|
||||
- Tainted/local/out-of-date scenarios, parsers, postoverflows and collections
|
||||
- Acquisition files (acquis.yaml, acquis.d/*.yaml)
|
||||
*/
|
||||
func (cli *cliConfig) restore(dirPath string) error {
|
||||
var err error
|
||||
|
||||
cfg := cli.cfg()
|
||||
|
||||
backupMain := fmt.Sprintf("%s/config.yaml", dirPath)
|
||||
if _, err = os.Stat(backupMain); err == nil {
|
||||
if cfg.ConfigPaths != nil && cfg.ConfigPaths.ConfigDir != "" {
|
||||
if err = CopyFile(backupMain, fmt.Sprintf("%s/config.yaml", cfg.ConfigPaths.ConfigDir)); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", backupMain, cfg.ConfigPaths.ConfigDir, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Now we have config.yaml, we should regenerate config struct to have rights paths etc
|
||||
ConfigFilePath = fmt.Sprintf("%s/config.yaml", cfg.ConfigPaths.ConfigDir)
|
||||
|
||||
log.Debug("Reloading configuration")
|
||||
|
||||
csConfig, _, err = loadConfigFor("config")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to reload configuration: %w", err)
|
||||
}
|
||||
|
||||
cfg = cli.cfg()
|
||||
|
||||
backupCAPICreds := fmt.Sprintf("%s/online_api_credentials.yaml", dirPath)
|
||||
if _, err = os.Stat(backupCAPICreds); err == nil {
|
||||
if err = CopyFile(backupCAPICreds, cfg.API.Server.OnlineClient.CredentialsFilePath); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", backupCAPICreds, cfg.API.Server.OnlineClient.CredentialsFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
backupLAPICreds := fmt.Sprintf("%s/local_api_credentials.yaml", dirPath)
|
||||
if _, err = os.Stat(backupLAPICreds); err == nil {
|
||||
if err = CopyFile(backupLAPICreds, cfg.API.Client.CredentialsFilePath); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", backupLAPICreds, cfg.API.Client.CredentialsFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
backupProfiles := fmt.Sprintf("%s/profiles.yaml", dirPath)
|
||||
if _, err = os.Stat(backupProfiles); err == nil {
|
||||
if err = CopyFile(backupProfiles, cfg.API.Server.ProfilesPath); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", backupProfiles, cfg.API.Server.ProfilesPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
backupSimulation := fmt.Sprintf("%s/simulation.yaml", dirPath)
|
||||
if _, err = os.Stat(backupSimulation); err == nil {
|
||||
if err = CopyFile(backupSimulation, cfg.ConfigPaths.SimulationFilePath); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", backupSimulation, cfg.ConfigPaths.SimulationFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
/*if there is a acquisition dir, restore its content*/
|
||||
if cfg.Crowdsec.AcquisitionDirPath != "" {
|
||||
if err = os.MkdirAll(cfg.Crowdsec.AcquisitionDirPath, 0o700); err != nil {
|
||||
return fmt.Errorf("error while creating %s: %w", cfg.Crowdsec.AcquisitionDirPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// if there was a single one
|
||||
backupAcquisition := fmt.Sprintf("%s/acquis.yaml", dirPath)
|
||||
if _, err = os.Stat(backupAcquisition); err == nil {
|
||||
log.Debugf("restoring backup'ed %s", backupAcquisition)
|
||||
|
||||
if err = CopyFile(backupAcquisition, cfg.Crowdsec.AcquisitionFilePath); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", backupAcquisition, cfg.Crowdsec.AcquisitionFilePath, err)
|
||||
}
|
||||
}
|
||||
|
||||
// if there are files in the acquis backup dir, restore them
|
||||
acquisBackupDir := filepath.Join(dirPath, "acquis", "*.yaml")
|
||||
if acquisFiles, err := filepath.Glob(acquisBackupDir); err == nil {
|
||||
for _, acquisFile := range acquisFiles {
|
||||
targetFname, err := filepath.Abs(cfg.Crowdsec.AcquisitionDirPath + "/" + filepath.Base(acquisFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("while saving %s to %s: %w", acquisFile, targetFname, err)
|
||||
}
|
||||
|
||||
log.Debugf("restoring %s to %s", acquisFile, targetFname)
|
||||
|
||||
if err = CopyFile(acquisFile, targetFname); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", acquisFile, targetFname, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cfg.Crowdsec != nil && len(cfg.Crowdsec.AcquisitionFiles) > 0 {
|
||||
for _, acquisFile := range cfg.Crowdsec.AcquisitionFiles {
|
||||
log.Infof("backup filepath from dir -> %s", acquisFile)
|
||||
|
||||
// if it was the default one, it has already been backed up
|
||||
if cfg.Crowdsec.AcquisitionFilePath == acquisFile {
|
||||
log.Infof("skip this one")
|
||||
continue
|
||||
}
|
||||
|
||||
targetFname, err := filepath.Abs(filepath.Join(acquisBackupDir, filepath.Base(acquisFile)))
|
||||
if err != nil {
|
||||
return fmt.Errorf("while saving %s to %s: %w", acquisFile, acquisBackupDir, err)
|
||||
}
|
||||
|
||||
if err = CopyFile(acquisFile, targetFname); err != nil {
|
||||
return fmt.Errorf("failed copy %s to %s: %w", acquisFile, targetFname, err)
|
||||
}
|
||||
|
||||
log.Infof("Saved acquis %s to %s", acquisFile, targetFname)
|
||||
}
|
||||
}
|
||||
|
||||
if err = cli.restoreHub(dirPath); err != nil {
|
||||
return fmt.Errorf("failed to restore hub config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliConfig) newRestoreCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: `restore "directory"`,
|
||||
Short: `Restore config in backup "directory"`,
|
||||
Long: `Restore the crowdsec configuration from specified backup "directory" including:
|
||||
|
||||
- Main config (config.yaml)
|
||||
- Simulation config (simulation.yaml)
|
||||
- Profiles config (profiles.yaml)
|
||||
- List of scenarios, parsers, postoverflows and collections that are up-to-date
|
||||
- Tainted/local/out-of-date scenarios, parsers, postoverflows and collections
|
||||
- Backup of API credentials (local API and online API)`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
dirPath := args[0]
|
||||
|
||||
if err := cli.restore(dirPath); err != nil {
|
||||
return fmt.Errorf("failed to restore config from %s: %w", dirPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
258
cmd/crowdsec-cli/config_show.go
Normal file
258
cmd/crowdsec-cli/config_show.go
Normal file
|
@ -0,0 +1,258 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"text/template"
|
||||
|
||||
"github.com/antonmedv/expr"
|
||||
"github.com/sanity-io/litter"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/exprhelpers"
|
||||
)
|
||||
|
||||
func (cli *cliConfig) showKey(key string) error {
|
||||
cfg := cli.cfg()
|
||||
|
||||
type Env struct {
|
||||
Config *csconfig.Config
|
||||
}
|
||||
|
||||
opts := []expr.Option{}
|
||||
opts = append(opts, exprhelpers.GetExprOptions(map[string]interface{}{})...)
|
||||
opts = append(opts, expr.Env(Env{}))
|
||||
|
||||
program, err := expr.Compile(key, opts...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
output, err := expr.Run(program, Env{Config: cfg})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch cfg.Cscli.Output {
|
||||
case "human", "raw":
|
||||
// Don't use litter for strings, it adds quotes
|
||||
// that would break compatibility with previous versions
|
||||
switch output.(type) {
|
||||
case string:
|
||||
fmt.Println(output)
|
||||
default:
|
||||
litter.Dump(output)
|
||||
}
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(output, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal configuration: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliConfig) template() string {
|
||||
return `Global:
|
||||
|
||||
{{- if .ConfigPaths }}
|
||||
- Configuration Folder : {{.ConfigPaths.ConfigDir}}
|
||||
- Data Folder : {{.ConfigPaths.DataDir}}
|
||||
- Hub Folder : {{.ConfigPaths.HubDir}}
|
||||
- Simulation File : {{.ConfigPaths.SimulationFilePath}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Common }}
|
||||
- Log Folder : {{.Common.LogDir}}
|
||||
- Log level : {{.Common.LogLevel}}
|
||||
- Log Media : {{.Common.LogMedia}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Crowdsec }}
|
||||
Crowdsec{{if and .Crowdsec.Enable (not (ValueBool .Crowdsec.Enable))}} (disabled){{end}}:
|
||||
- Acquisition File : {{.Crowdsec.AcquisitionFilePath}}
|
||||
- Parsers routines : {{.Crowdsec.ParserRoutinesCount}}
|
||||
{{- if .Crowdsec.AcquisitionDirPath }}
|
||||
- Acquisition Folder : {{.Crowdsec.AcquisitionDirPath}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .Cscli }}
|
||||
cscli:
|
||||
- Output : {{.Cscli.Output}}
|
||||
- Hub Branch : {{.Cscli.HubBranch}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API }}
|
||||
{{- if .API.Client }}
|
||||
API Client:
|
||||
{{- if .API.Client.Credentials }}
|
||||
- URL : {{.API.Client.Credentials.URL}}
|
||||
- Login : {{.API.Client.Credentials.Login}}
|
||||
{{- end }}
|
||||
- Credentials File : {{.API.Client.CredentialsFilePath}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server }}
|
||||
Local API Server{{if and .API.Server.Enable (not (ValueBool .API.Server.Enable))}} (disabled){{end}}:
|
||||
- Listen URL : {{.API.Server.ListenURI}}
|
||||
- Listen Socket : {{.API.Server.ListenSocket}}
|
||||
- Profile File : {{.API.Server.ProfilesPath}}
|
||||
|
||||
{{- if .API.Server.TLS }}
|
||||
{{- if .API.Server.TLS.CertFilePath }}
|
||||
- Cert File : {{.API.Server.TLS.CertFilePath}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server.TLS.KeyFilePath }}
|
||||
- Key File : {{.API.Server.TLS.KeyFilePath}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server.TLS.CACertPath }}
|
||||
- CA Cert : {{.API.Server.TLS.CACertPath}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server.TLS.CRLPath }}
|
||||
- CRL : {{.API.Server.TLS.CRLPath}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server.TLS.CacheExpiration }}
|
||||
- Cache Expiration : {{.API.Server.TLS.CacheExpiration}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server.TLS.ClientVerification }}
|
||||
- Client Verification : {{.API.Server.TLS.ClientVerification}}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server.TLS.AllowedAgentsOU }}
|
||||
{{- range .API.Server.TLS.AllowedAgentsOU }}
|
||||
- Allowed Agents OU : {{.}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .API.Server.TLS.AllowedBouncersOU }}
|
||||
{{- range .API.Server.TLS.AllowedBouncersOU }}
|
||||
- Allowed Bouncers OU : {{.}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
- Trusted IPs:
|
||||
{{- range .API.Server.TrustedIPs }}
|
||||
- {{.}}
|
||||
{{- end }}
|
||||
|
||||
{{- if and .API.Server.OnlineClient .API.Server.OnlineClient.Credentials }}
|
||||
Central API:
|
||||
- URL : {{.API.Server.OnlineClient.Credentials.URL}}
|
||||
- Login : {{.API.Server.OnlineClient.Credentials.Login}}
|
||||
- Credentials File : {{.API.Server.OnlineClient.CredentialsFilePath}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
|
||||
{{- if .DbConfig }}
|
||||
- Database:
|
||||
- Type : {{.DbConfig.Type}}
|
||||
{{- if eq .DbConfig.Type "sqlite" }}
|
||||
- Path : {{.DbConfig.DbPath}}
|
||||
{{- else}}
|
||||
- Host : {{.DbConfig.Host}}
|
||||
- Port : {{.DbConfig.Port}}
|
||||
- User : {{.DbConfig.User}}
|
||||
- DB Name : {{.DbConfig.DbName}}
|
||||
{{- end }}
|
||||
{{- if .DbConfig.MaxOpenConns }}
|
||||
- Max Open Conns : {{.DbConfig.MaxOpenConns}}
|
||||
{{- end }}
|
||||
{{- if ne .DbConfig.DecisionBulkSize 0 }}
|
||||
- Decision Bulk Size : {{.DbConfig.DecisionBulkSize}}
|
||||
{{- end }}
|
||||
{{- if .DbConfig.Flush }}
|
||||
{{- if .DbConfig.Flush.MaxAge }}
|
||||
- Flush age : {{.DbConfig.Flush.MaxAge}}
|
||||
{{- end }}
|
||||
{{- if .DbConfig.Flush.MaxItems }}
|
||||
- Flush size : {{.DbConfig.Flush.MaxItems}}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
`
|
||||
}
|
||||
|
||||
func (cli *cliConfig) show() error {
|
||||
cfg := cli.cfg()
|
||||
|
||||
switch cfg.Cscli.Output {
|
||||
case "human":
|
||||
// The tests on .Enable look funny because the option has a true default which has
|
||||
// not been set yet (we don't really load the LAPI) and go templates don't dereference
|
||||
// pointers in boolean tests. Prefix notation is the cherry on top.
|
||||
funcs := template.FuncMap{
|
||||
// can't use generics here
|
||||
"ValueBool": func(b *bool) bool { return b != nil && *b },
|
||||
}
|
||||
|
||||
tmp, err := template.New("config").Funcs(funcs).Parse(cli.template())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = tmp.Execute(os.Stdout, cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case "json":
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal configuration: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(data))
|
||||
case "raw":
|
||||
data, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal configuration: %w", err)
|
||||
}
|
||||
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliConfig) newShowCmd() *cobra.Command {
|
||||
var key string
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "show",
|
||||
Short: "Displays current config",
|
||||
Long: `Displays the current cli configuration.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if err := cli.cfg().LoadAPIClient(); err != nil {
|
||||
log.Errorf("failed to load API client configuration: %s", err)
|
||||
// don't return, we can still show the configuration
|
||||
}
|
||||
|
||||
if key != "" {
|
||||
return cli.showKey(key)
|
||||
}
|
||||
|
||||
return cli.show()
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&key, "key", "", "", "Display only this value (Config.API.Server.ListenURI)")
|
||||
|
||||
return cmd
|
||||
}
|
26
cmd/crowdsec-cli/config_showyaml.go
Normal file
26
cmd/crowdsec-cli/config_showyaml.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func (cli *cliConfig) showYAML() error {
|
||||
fmt.Println(mergedConfig)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliConfig) newShowYAMLCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "show-yaml",
|
||||
Short: "Displays merged config.yaml + config.yaml.local",
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
return cli.showYAML()
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
441
cmd/crowdsec-cli/console.go
Normal file
441
cmd/crowdsec-cli/console.go
Normal file
|
@ -0,0 +1,441 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/ptr"
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwhub"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
type cliConsole struct {
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLIConsole(cfg configGetter) *cliConsole {
|
||||
return &cliConsole{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliConsole) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "console [action]",
|
||||
Short: "Manage interaction with Crowdsec console (https://app.crowdsec.net)",
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := require.CAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := require.CAPIRegistered(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newEnrollCmd())
|
||||
cmd.AddCommand(cli.newEnableCmd())
|
||||
cmd.AddCommand(cli.newDisableCmd())
|
||||
cmd.AddCommand(cli.newStatusCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newEnrollCmd() *cobra.Command {
|
||||
name := ""
|
||||
overwrite := false
|
||||
tags := []string{}
|
||||
opts := []string{}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "enroll [enroll-key]",
|
||||
Short: "Enroll this instance to https://app.crowdsec.net [requires local API]",
|
||||
Long: `
|
||||
Enroll this instance to https://app.crowdsec.net
|
||||
|
||||
You can get your enrollment key by creating an account on https://app.crowdsec.net.
|
||||
After running this command your will need to validate the enrollment in the webapp.`,
|
||||
Example: fmt.Sprintf(`cscli console enroll YOUR-ENROLL-KEY
|
||||
cscli console enroll --name [instance_name] YOUR-ENROLL-KEY
|
||||
cscli console enroll --name [instance_name] --tags [tag_1] --tags [tag_2] YOUR-ENROLL-KEY
|
||||
cscli console enroll --enable context,manual YOUR-ENROLL-KEY
|
||||
|
||||
valid options are : %s,all (see 'cscli console status' for details)`, strings.Join(csconfig.CONSOLE_CONFIGS, ",")),
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
cfg := cli.cfg()
|
||||
password := strfmt.Password(cfg.API.Server.OnlineClient.Credentials.Password)
|
||||
|
||||
apiURL, err := url.Parse(cfg.API.Server.OnlineClient.Credentials.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not parse CAPI URL: %w", err)
|
||||
}
|
||||
|
||||
hub, err := require.Hub(cfg, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scenarios, err := hub.GetInstalledNamesByType(cwhub.SCENARIOS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get installed scenarios: %w", err)
|
||||
}
|
||||
|
||||
if len(scenarios) == 0 {
|
||||
scenarios = make([]string, 0)
|
||||
}
|
||||
|
||||
enableOpts := []string{csconfig.SEND_MANUAL_SCENARIOS, csconfig.SEND_TAINTED_SCENARIOS}
|
||||
if len(opts) != 0 {
|
||||
for _, opt := range opts {
|
||||
valid := false
|
||||
if opt == "all" {
|
||||
enableOpts = csconfig.CONSOLE_CONFIGS
|
||||
break
|
||||
}
|
||||
for _, availableOpt := range csconfig.CONSOLE_CONFIGS {
|
||||
if opt == availableOpt {
|
||||
valid = true
|
||||
enable := true
|
||||
for _, enabledOpt := range enableOpts {
|
||||
if opt == enabledOpt {
|
||||
enable = false
|
||||
continue
|
||||
}
|
||||
}
|
||||
if enable {
|
||||
enableOpts = append(enableOpts, opt)
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
if !valid {
|
||||
return fmt.Errorf("option %s doesn't exist", opt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c, _ := apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: cli.cfg().API.Server.OnlineClient.Credentials.Login,
|
||||
Password: password,
|
||||
Scenarios: scenarios,
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiURL,
|
||||
VersionPrefix: "v3",
|
||||
})
|
||||
|
||||
resp, err := c.Auth.EnrollWatcher(context.Background(), args[0], name, tags, overwrite)
|
||||
if err != nil {
|
||||
return fmt.Errorf("could not enroll instance: %w", err)
|
||||
}
|
||||
|
||||
if resp.Response.StatusCode == 200 && !overwrite {
|
||||
log.Warning("Instance already enrolled. You can use '--overwrite' to force enroll")
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := cli.setConsoleOpts(enableOpts, true); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, opt := range enableOpts {
|
||||
log.Infof("Enabled %s : %s", opt, csconfig.CONSOLE_CONFIGS_HELP[opt])
|
||||
}
|
||||
|
||||
log.Info("Watcher successfully enrolled. Visit https://app.crowdsec.net to accept it.")
|
||||
log.Info("Please restart crowdsec after accepting the enrollment.")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.StringVarP(&name, "name", "n", "", "Name to display in the console")
|
||||
flags.BoolVarP(&overwrite, "overwrite", "", false, "Force enroll the instance")
|
||||
flags.StringSliceVarP(&tags, "tags", "t", tags, "Tags to display in the console")
|
||||
flags.StringSliceVarP(&opts, "enable", "e", opts, "Enable console options")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newEnableCmd() *cobra.Command {
|
||||
var enableAll bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "enable [option]",
|
||||
Short: "Enable a console option",
|
||||
Example: "sudo cscli console enable tainted",
|
||||
Long: `
|
||||
Enable given information push to the central API. Allows to empower the console`,
|
||||
ValidArgs: csconfig.CONSOLE_CONFIGS,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if enableAll {
|
||||
if err := cli.setConsoleOpts(csconfig.CONSOLE_CONFIGS, true); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("All features have been enabled successfully")
|
||||
} else {
|
||||
if len(args) == 0 {
|
||||
return errors.New("you must specify at least one feature to enable")
|
||||
}
|
||||
if err := cli.setConsoleOpts(args, true); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("%v have been enabled", args)
|
||||
}
|
||||
|
||||
log.Infof(ReloadMessage())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVarP(&enableAll, "all", "a", false, "Enable all console options")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newDisableCmd() *cobra.Command {
|
||||
var disableAll bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "disable [option]",
|
||||
Short: "Disable a console option",
|
||||
Example: "sudo cscli console disable tainted",
|
||||
Long: `
|
||||
Disable given information push to the central API.`,
|
||||
ValidArgs: csconfig.CONSOLE_CONFIGS,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, args []string) error {
|
||||
if disableAll {
|
||||
if err := cli.setConsoleOpts(csconfig.CONSOLE_CONFIGS, false); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("All features have been disabled")
|
||||
} else {
|
||||
if err := cli.setConsoleOpts(args, false); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("%v have been disabled", args)
|
||||
}
|
||||
|
||||
log.Infof(ReloadMessage())
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().BoolVarP(&disableAll, "all", "a", false, "Disable all console options")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) newStatusCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "status",
|
||||
Short: "Shows status of the console options",
|
||||
Example: `sudo cscli console status`,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
consoleCfg := cfg.API.Server.ConsoleConfig
|
||||
switch cfg.Cscli.Output {
|
||||
case "human":
|
||||
cmdConsoleStatusTable(color.Output, *consoleCfg)
|
||||
case "json":
|
||||
out := map[string](*bool){
|
||||
csconfig.SEND_MANUAL_SCENARIOS: consoleCfg.ShareManualDecisions,
|
||||
csconfig.SEND_CUSTOM_SCENARIOS: consoleCfg.ShareCustomScenarios,
|
||||
csconfig.SEND_TAINTED_SCENARIOS: consoleCfg.ShareTaintedScenarios,
|
||||
csconfig.SEND_CONTEXT: consoleCfg.ShareContext,
|
||||
csconfig.CONSOLE_MANAGEMENT: consoleCfg.ConsoleManagement,
|
||||
}
|
||||
data, err := json.MarshalIndent(out, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal configuration: %w", err)
|
||||
}
|
||||
fmt.Println(string(data))
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(os.Stdout)
|
||||
err := csvwriter.Write([]string{"option", "enabled"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
rows := [][]string{
|
||||
{csconfig.SEND_MANUAL_SCENARIOS, strconv.FormatBool(*consoleCfg.ShareManualDecisions)},
|
||||
{csconfig.SEND_CUSTOM_SCENARIOS, strconv.FormatBool(*consoleCfg.ShareCustomScenarios)},
|
||||
{csconfig.SEND_TAINTED_SCENARIOS, strconv.FormatBool(*consoleCfg.ShareTaintedScenarios)},
|
||||
{csconfig.SEND_CONTEXT, strconv.FormatBool(*consoleCfg.ShareContext)},
|
||||
{csconfig.CONSOLE_MANAGEMENT, strconv.FormatBool(*consoleCfg.ConsoleManagement)},
|
||||
}
|
||||
for _, row := range rows {
|
||||
err = csvwriter.Write(row)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
csvwriter.Flush()
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliConsole) dumpConfig() error {
|
||||
serverCfg := cli.cfg().API.Server
|
||||
|
||||
out, err := yaml.Marshal(serverCfg.ConsoleConfig)
|
||||
if err != nil {
|
||||
return fmt.Errorf("while marshaling ConsoleConfig (for %s): %w", serverCfg.ConsoleConfigPath, err)
|
||||
}
|
||||
|
||||
if serverCfg.ConsoleConfigPath == "" {
|
||||
serverCfg.ConsoleConfigPath = csconfig.DefaultConsoleConfigFilePath
|
||||
log.Debugf("Empty console_path, defaulting to %s", serverCfg.ConsoleConfigPath)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(serverCfg.ConsoleConfigPath, out, 0o600); err != nil {
|
||||
return fmt.Errorf("while dumping console config to %s: %w", serverCfg.ConsoleConfigPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliConsole) setConsoleOpts(args []string, wanted bool) error {
|
||||
cfg := cli.cfg()
|
||||
consoleCfg := cfg.API.Server.ConsoleConfig
|
||||
|
||||
for _, arg := range args {
|
||||
switch arg {
|
||||
case csconfig.CONSOLE_MANAGEMENT:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if consoleCfg.ConsoleManagement != nil {
|
||||
if *consoleCfg.ConsoleManagement == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
|
||||
*consoleCfg.ConsoleManagement = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.CONSOLE_MANAGEMENT, wanted)
|
||||
consoleCfg.ConsoleManagement = ptr.Of(wanted)
|
||||
}
|
||||
|
||||
if cfg.API.Server.OnlineClient.Credentials != nil {
|
||||
changed := false
|
||||
if wanted && cfg.API.Server.OnlineClient.Credentials.PapiURL == "" {
|
||||
changed = true
|
||||
cfg.API.Server.OnlineClient.Credentials.PapiURL = types.PAPIBaseURL
|
||||
} else if !wanted && cfg.API.Server.OnlineClient.Credentials.PapiURL != "" {
|
||||
changed = true
|
||||
cfg.API.Server.OnlineClient.Credentials.PapiURL = ""
|
||||
}
|
||||
|
||||
if changed {
|
||||
fileContent, err := yaml.Marshal(cfg.API.Server.OnlineClient.Credentials)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot marshal credentials: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("Updating credentials file: %s", cfg.API.Server.OnlineClient.CredentialsFilePath)
|
||||
|
||||
err = os.WriteFile(cfg.API.Server.OnlineClient.CredentialsFilePath, fileContent, 0o600)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot write credentials file: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
case csconfig.SEND_CUSTOM_SCENARIOS:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if consoleCfg.ShareCustomScenarios != nil {
|
||||
if *consoleCfg.ShareCustomScenarios == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
|
||||
*consoleCfg.ShareCustomScenarios = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CUSTOM_SCENARIOS, wanted)
|
||||
consoleCfg.ShareCustomScenarios = ptr.Of(wanted)
|
||||
}
|
||||
case csconfig.SEND_TAINTED_SCENARIOS:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if consoleCfg.ShareTaintedScenarios != nil {
|
||||
if *consoleCfg.ShareTaintedScenarios == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
|
||||
*consoleCfg.ShareTaintedScenarios = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_TAINTED_SCENARIOS, wanted)
|
||||
consoleCfg.ShareTaintedScenarios = ptr.Of(wanted)
|
||||
}
|
||||
case csconfig.SEND_MANUAL_SCENARIOS:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if consoleCfg.ShareManualDecisions != nil {
|
||||
if *consoleCfg.ShareManualDecisions == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
|
||||
*consoleCfg.ShareManualDecisions = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_MANUAL_SCENARIOS, wanted)
|
||||
consoleCfg.ShareManualDecisions = ptr.Of(wanted)
|
||||
}
|
||||
case csconfig.SEND_CONTEXT:
|
||||
/*for each flag check if it's already set before setting it*/
|
||||
if consoleCfg.ShareContext != nil {
|
||||
if *consoleCfg.ShareContext == wanted {
|
||||
log.Debugf("%s already set to %t", csconfig.SEND_CONTEXT, wanted)
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted)
|
||||
*consoleCfg.ShareContext = wanted
|
||||
}
|
||||
} else {
|
||||
log.Infof("%s set to %t", csconfig.SEND_CONTEXT, wanted)
|
||||
consoleCfg.ShareContext = ptr.Of(wanted)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unknown flag %s", arg)
|
||||
}
|
||||
}
|
||||
|
||||
if err := cli.dumpConfig(); err != nil {
|
||||
return fmt.Errorf("failed writing console config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
49
cmd/crowdsec-cli/console_table.go
Normal file
49
cmd/crowdsec-cli/console_table.go
Normal file
|
@ -0,0 +1,49 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/aquasecurity/table"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/csconfig"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/emoji"
|
||||
)
|
||||
|
||||
func cmdConsoleStatusTable(out io.Writer, consoleCfg csconfig.ConsoleConfig) {
|
||||
t := newTable(out)
|
||||
t.SetRowLines(false)
|
||||
|
||||
t.SetHeaders("Option Name", "Activated", "Description")
|
||||
t.SetHeaderAlignment(table.AlignLeft, table.AlignLeft, table.AlignLeft)
|
||||
|
||||
for _, option := range csconfig.CONSOLE_CONFIGS {
|
||||
activated := emoji.CrossMark
|
||||
|
||||
switch option {
|
||||
case csconfig.SEND_CUSTOM_SCENARIOS:
|
||||
if *consoleCfg.ShareCustomScenarios {
|
||||
activated = emoji.CheckMarkButton
|
||||
}
|
||||
case csconfig.SEND_MANUAL_SCENARIOS:
|
||||
if *consoleCfg.ShareManualDecisions {
|
||||
activated = emoji.CheckMarkButton
|
||||
}
|
||||
case csconfig.SEND_TAINTED_SCENARIOS:
|
||||
if *consoleCfg.ShareTaintedScenarios {
|
||||
activated = emoji.CheckMarkButton
|
||||
}
|
||||
case csconfig.SEND_CONTEXT:
|
||||
if *consoleCfg.ShareContext {
|
||||
activated = emoji.CheckMarkButton
|
||||
}
|
||||
case csconfig.CONSOLE_MANAGEMENT:
|
||||
if *consoleCfg.ConsoleManagement {
|
||||
activated = emoji.CheckMarkButton
|
||||
}
|
||||
}
|
||||
|
||||
t.AddRow(option, activated, csconfig.CONSOLE_CONFIGS_HELP[option])
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
82
cmd/crowdsec-cli/copyfile.go
Normal file
82
cmd/crowdsec-cli/copyfile.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
/*help to copy the file, ioutil doesn't offer the feature*/
|
||||
|
||||
func copyFileContents(src, dst string) (err error) {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
cerr := out.Close()
|
||||
if err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = out.Sync()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
/*copy the file, ioutile doesn't offer the feature*/
|
||||
func CopyFile(sourceSymLink, destinationFile string) error {
|
||||
sourceFile, err := filepath.EvalSymlinks(sourceSymLink)
|
||||
if err != nil {
|
||||
log.Infof("Not a symlink : %s", err)
|
||||
|
||||
sourceFile = sourceSymLink
|
||||
}
|
||||
|
||||
sourceFileStat, err := os.Stat(sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !sourceFileStat.Mode().IsRegular() {
|
||||
// cannot copy non-regular files (e.g., directories,
|
||||
// symlinks, devices, etc.)
|
||||
return fmt.Errorf("copyFile: non-regular source file %s (%q)", sourceFileStat.Name(), sourceFileStat.Mode().String())
|
||||
}
|
||||
|
||||
destinationFileStat, err := os.Stat(destinationFile)
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if !(destinationFileStat.Mode().IsRegular()) {
|
||||
return fmt.Errorf("copyFile: non-regular destination file %s (%q)", destinationFileStat.Name(), destinationFileStat.Mode().String())
|
||||
}
|
||||
|
||||
if os.SameFile(sourceFileStat, destinationFileStat) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err = os.Link(sourceFile, destinationFile); err != nil {
|
||||
err = copyFileContents(sourceFile, destinationFile)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
|
@ -1,375 +1,475 @@
|
|||
//go:build linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"syscall"
|
||||
"unicode"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/cwversion"
|
||||
"github.com/dghubble/sling"
|
||||
"github.com/docker/docker/api/types"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"github.com/docker/docker/api/types/mount"
|
||||
"github.com/docker/docker/client"
|
||||
"github.com/docker/go-connections/nat"
|
||||
"github.com/AlecAivazis/survey/v2"
|
||||
"github.com/pbnjay/memory"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/cmd/crowdsec-cli/require"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/metabase"
|
||||
)
|
||||
|
||||
var (
|
||||
metabaseImage = "metabase/metabase"
|
||||
metabaseDbURI = "https://crowdsec-statics-assets.s3-eu-west-1.amazonaws.com/metabase.db.zip"
|
||||
metabaseDbPath = "/var/lib/crowdsec/data"
|
||||
metabaseUser = "crowdsec@crowdsec.net"
|
||||
metabasePassword string
|
||||
metabaseDBPath string
|
||||
metabaseConfigPath string
|
||||
metabaseConfigFolder = "metabase/"
|
||||
metabaseConfigFile = "metabase.yaml"
|
||||
metabaseImage = "metabase/metabase:v0.46.6.1"
|
||||
/**/
|
||||
metabaseListenAddress = "127.0.0.1"
|
||||
metabaseListenPort = "3000"
|
||||
metabaseContainerID = "/crowdsec-metabase"
|
||||
/*informations needed to setup a random password on user's behalf*/
|
||||
metabaseURI = "http://localhost:3000/api/"
|
||||
metabaseURISession = "session"
|
||||
metabaseURIRescan = "database/2/rescan_values"
|
||||
metabaseURIUpdatepwd = "user/1/password"
|
||||
defaultPassword = "c6cmetabase"
|
||||
defaultEmail = "metabase@crowdsec.net"
|
||||
metabaseContainerID = "crowdsec-metabase"
|
||||
crowdsecGroup = "crowdsec"
|
||||
|
||||
forceYes bool
|
||||
|
||||
// information needed to set up a random password on user's behalf
|
||||
)
|
||||
|
||||
func NewDashboardCmd() *cobra.Command {
|
||||
/* ---- UPDATE COMMAND */
|
||||
var cmdDashboard = &cobra.Command{
|
||||
Use: "dashboard",
|
||||
Short: "Start a dashboard (metabase) container.",
|
||||
Long: `Start a metabase container exposing dashboards and metrics.`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
Example: `cscli dashboard setup
|
||||
cscli dashboard start
|
||||
cscli dashboard stop
|
||||
cscli dashboard setup --force`,
|
||||
}
|
||||
|
||||
var force bool
|
||||
var cmdDashSetup = &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Setup a metabase container.",
|
||||
Long: `Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
Example: `cscli dashboard setup
|
||||
cscli dashboard setup --force
|
||||
cscli dashboard setup -l 0.0.0.0 -p 443
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := downloadMetabaseDB(force); err != nil {
|
||||
log.Fatalf("Failed to download metabase DB : %s", err)
|
||||
}
|
||||
log.Infof("Downloaded metabase DB")
|
||||
if err := createMetabase(); err != nil {
|
||||
log.Fatalf("Failed to start metabase container : %s", err)
|
||||
}
|
||||
log.Infof("Started metabase")
|
||||
newpassword := generatePassword()
|
||||
if err := resetMetabasePassword(newpassword); err != nil {
|
||||
log.Fatalf("Failed to reset password : %s", err)
|
||||
}
|
||||
log.Infof("Setup finished")
|
||||
log.Infof("url : http://%s:%s", metabaseListenAddress, metabaseListenPort)
|
||||
log.Infof("username: %s", defaultEmail)
|
||||
log.Infof("password: %s", newpassword)
|
||||
},
|
||||
}
|
||||
cmdDashSetup.Flags().BoolVarP(&force, "force", "f", false, "Force setup : override existing files.")
|
||||
cmdDashSetup.Flags().StringVarP(&metabaseDbPath, "dir", "d", metabaseDbPath, "Shared directory with metabase container.")
|
||||
cmdDashSetup.Flags().StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
|
||||
cmdDashSetup.Flags().StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
|
||||
cmdDashboard.AddCommand(cmdDashSetup)
|
||||
|
||||
var cmdDashStart = &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start the metabase container.",
|
||||
Long: `Stats the metabase container using docker.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := startMetabase(); err != nil {
|
||||
log.Fatalf("Failed to start metabase container : %s", err)
|
||||
}
|
||||
log.Infof("Started metabase")
|
||||
log.Infof("url : http://%s:%s", metabaseListenAddress, metabaseListenPort)
|
||||
},
|
||||
}
|
||||
cmdDashboard.AddCommand(cmdDashStart)
|
||||
|
||||
var remove bool
|
||||
var cmdDashStop = &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stops the metabase container.",
|
||||
Long: `Stops the metabase container using docker.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := stopMetabase(remove); err != nil {
|
||||
log.Fatalf("Failed to stop metabase container : %s", err)
|
||||
}
|
||||
},
|
||||
}
|
||||
cmdDashStop.Flags().BoolVarP(&remove, "remove", "r", false, "remove (docker rm) container as well.")
|
||||
cmdDashboard.AddCommand(cmdDashStop)
|
||||
return cmdDashboard
|
||||
type cliDashboard struct {
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func downloadMetabaseDB(force bool) error {
|
||||
func NewCLIDashboard(cfg configGetter) *cliDashboard {
|
||||
return &cliDashboard{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
metabaseDBSubpath := path.Join(metabaseDbPath, "metabase.db")
|
||||
func (cli *cliDashboard) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dashboard [command]",
|
||||
Short: "Manage your metabase dashboard container [requires local API]",
|
||||
Long: `Install/Start/Stop/Remove a metabase container exposing dashboard and metrics.
|
||||
Note: This command requires database direct access, so is intended to be run on Local API/master.
|
||||
`,
|
||||
Args: cobra.ExactArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
Example: `
|
||||
cscli dashboard setup
|
||||
cscli dashboard start
|
||||
cscli dashboard stop
|
||||
cscli dashboard remove
|
||||
`,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := require.LAPI(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err := os.Stat(metabaseDBSubpath)
|
||||
if err == nil && !force {
|
||||
log.Printf("%s exists, skip.", metabaseDBSubpath)
|
||||
if err := metabase.TestAvailability(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
metabaseConfigFolderPath := filepath.Join(cfg.ConfigPaths.ConfigDir, metabaseConfigFolder)
|
||||
metabaseConfigPath = filepath.Join(metabaseConfigFolderPath, metabaseConfigFile)
|
||||
if err := os.MkdirAll(metabaseConfigFolderPath, os.ModePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := require.DB(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
/*
|
||||
Old container name was "/crowdsec-metabase" but podman doesn't
|
||||
allow '/' in container name. We do this check to not break
|
||||
existing dashboard setup.
|
||||
*/
|
||||
if !metabase.IsContainerExist(metabaseContainerID) {
|
||||
oldContainerID := fmt.Sprintf("/%s", metabaseContainerID)
|
||||
if metabase.IsContainerExist(oldContainerID) {
|
||||
metabaseContainerID = oldContainerID
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newSetupCmd())
|
||||
cmd.AddCommand(cli.newStartCmd())
|
||||
cmd.AddCommand(cli.newStopCmd())
|
||||
cmd.AddCommand(cli.newShowPasswordCmd())
|
||||
cmd.AddCommand(cli.newRemoveCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newSetupCmd() *cobra.Command {
|
||||
var force bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "setup",
|
||||
Short: "Setup a metabase container.",
|
||||
Long: `Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
Example: `
|
||||
cscli dashboard setup
|
||||
cscli dashboard setup --listen 0.0.0.0
|
||||
cscli dashboard setup -l 0.0.0.0 -p 443 --password <password>
|
||||
`,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if metabaseDBPath == "" {
|
||||
metabaseDBPath = cli.cfg().ConfigPaths.DataDir
|
||||
}
|
||||
|
||||
if metabasePassword == "" {
|
||||
isValid := passwordIsValid(metabasePassword)
|
||||
for !isValid {
|
||||
metabasePassword = generatePassword(16)
|
||||
isValid = passwordIsValid(metabasePassword)
|
||||
}
|
||||
}
|
||||
if err := checkSystemMemory(&forceYes); err != nil {
|
||||
return err
|
||||
}
|
||||
warnIfNotLoopback(metabaseListenAddress)
|
||||
if err := disclaimer(&forceYes); err != nil {
|
||||
return err
|
||||
}
|
||||
dockerGroup, err := checkGroups(&forceYes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = cli.chownDatabase(dockerGroup.Gid); err != nil {
|
||||
return err
|
||||
}
|
||||
mb, err := metabase.SetupMetabase(cli.cfg().API.Server.DbConfig, metabaseListenAddress, metabaseListenPort, metabaseUser, metabasePassword, metabaseDBPath, dockerGroup.Gid, metabaseContainerID, metabaseImage)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mb.DumpConfig(metabaseConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Infof("Metabase is ready")
|
||||
fmt.Println()
|
||||
fmt.Printf("\tURL : '%s'\n", mb.Config.ListenURL)
|
||||
fmt.Printf("\tusername : '%s'\n", mb.Config.Username)
|
||||
fmt.Printf("\tpassword : '%s'\n", mb.Config.Password)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&force, "force", "f", false, "Force setup : override existing files")
|
||||
flags.StringVarP(&metabaseDBPath, "dir", "d", "", "Shared directory with metabase container")
|
||||
flags.StringVarP(&metabaseListenAddress, "listen", "l", metabaseListenAddress, "Listen address of container")
|
||||
flags.StringVar(&metabaseImage, "metabase-image", metabaseImage, "Metabase image to use")
|
||||
flags.StringVarP(&metabaseListenPort, "port", "p", metabaseListenPort, "Listen port of container")
|
||||
flags.BoolVarP(&forceYes, "yes", "y", false, "force yes")
|
||||
// flags.StringVarP(&metabaseUser, "user", "u", "crowdsec@crowdsec.net", "metabase user")
|
||||
flags.StringVar(&metabasePassword, "password", "", "metabase password")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newStartCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "start",
|
||||
Short: "Start the metabase container.",
|
||||
Long: `Stats the metabase container using docker.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
mb, err := metabase.NewMetabase(metabaseConfigPath, metabaseContainerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
warnIfNotLoopback(mb.Config.ListenAddr)
|
||||
if err := disclaimer(&forceYes); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := mb.Container.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start metabase container : %s", err)
|
||||
}
|
||||
log.Infof("Started metabase")
|
||||
log.Infof("url : http://%s:%s", mb.Config.ListenAddr, mb.Config.ListenPort)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().BoolVarP(&forceYes, "yes", "y", false, "force yes")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newStopCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "stop",
|
||||
Short: "Stops the metabase container.",
|
||||
Long: `Stops the metabase container using docker.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if err := metabase.StopContainer(metabaseContainerID); err != nil {
|
||||
return fmt.Errorf("unable to stop container '%s': %s", metabaseContainerID, err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newShowPasswordCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "show-password",
|
||||
Short: "displays password of metabase.",
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
m := metabase.Metabase{}
|
||||
if err := m.LoadConfig(metabaseConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("'%s'", m.Config.Password)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDashboard) newRemoveCmd() *cobra.Command {
|
||||
var force bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "remove",
|
||||
Short: "removes the metabase container.",
|
||||
Long: `removes the metabase container using docker.`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
Example: `
|
||||
cscli dashboard remove
|
||||
cscli dashboard remove --force
|
||||
`,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if !forceYes {
|
||||
var answer bool
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Do you really want to remove crowdsec dashboard? (all your changes will be lost)",
|
||||
Default: true,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return fmt.Errorf("unable to ask to force: %s", err)
|
||||
}
|
||||
if !answer {
|
||||
return fmt.Errorf("user stated no to continue")
|
||||
}
|
||||
}
|
||||
if metabase.IsContainerExist(metabaseContainerID) {
|
||||
log.Debugf("Stopping container %s", metabaseContainerID)
|
||||
if err := metabase.StopContainer(metabaseContainerID); err != nil {
|
||||
log.Warningf("unable to stop container '%s': %s", metabaseContainerID, err)
|
||||
}
|
||||
dockerGroup, err := user.LookupGroup(crowdsecGroup)
|
||||
if err == nil { // if group exist, remove it
|
||||
groupDelCmd, err := exec.LookPath("groupdel")
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to find 'groupdel' command, can't continue")
|
||||
}
|
||||
|
||||
groupDel := &exec.Cmd{Path: groupDelCmd, Args: []string{groupDelCmd, crowdsecGroup}}
|
||||
if err := groupDel.Run(); err != nil {
|
||||
log.Warnf("unable to delete group '%s': %s", dockerGroup, err)
|
||||
}
|
||||
}
|
||||
log.Debugf("Removing container %s", metabaseContainerID)
|
||||
if err := metabase.RemoveContainer(metabaseContainerID); err != nil {
|
||||
log.Warnf("unable to remove container '%s': %s", metabaseContainerID, err)
|
||||
}
|
||||
log.Infof("container %s stopped & removed", metabaseContainerID)
|
||||
}
|
||||
log.Debugf("Removing metabase db %s", cli.cfg().ConfigPaths.DataDir)
|
||||
if err := metabase.RemoveDatabase(cli.cfg().ConfigPaths.DataDir); err != nil {
|
||||
log.Warnf("failed to remove metabase internal db : %s", err)
|
||||
}
|
||||
if force {
|
||||
m := metabase.Metabase{}
|
||||
if err := m.LoadConfig(metabaseConfigPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := metabase.RemoveImageContainer(m.Config.Image); err != nil {
|
||||
if !strings.Contains(err.Error(), "No such image") {
|
||||
return fmt.Errorf("removing docker image: %s", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.BoolVarP(&force, "force", "f", false, "Remove also the metabase image")
|
||||
flags.BoolVarP(&forceYes, "yes", "y", false, "force yes")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func passwordIsValid(password string) bool {
|
||||
hasDigit := false
|
||||
|
||||
for _, j := range password {
|
||||
if unicode.IsDigit(j) {
|
||||
hasDigit = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !hasDigit || len(password) < 6 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func checkSystemMemory(forceYes *bool) error {
|
||||
totMem := memory.TotalMemory()
|
||||
if totMem >= uint64(math.Pow(2, 30)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(metabaseDBSubpath, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create %s : %s", metabaseDBSubpath, err)
|
||||
if !*forceYes {
|
||||
var answer bool
|
||||
|
||||
prompt := &survey.Confirm{
|
||||
Message: "Metabase requires 1-2GB of RAM, your system is below this requirement continue ?",
|
||||
Default: true,
|
||||
}
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return fmt.Errorf("unable to ask about RAM check: %s", err)
|
||||
}
|
||||
|
||||
if !answer {
|
||||
return fmt.Errorf("user stated no to continue")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("GET", metabaseDbURI, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build request to fetch metabase db : %s", err)
|
||||
}
|
||||
//This needs to be removed once we move the zip out of github
|
||||
req.Header.Add("Accept", `application/vnd.github.v3.raw`)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed request to fetch metabase db : %s", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("got http %d while requesting metabase db %s, stop", resp.StatusCode, metabaseDbURI)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed request read while fetching metabase db : %s", err)
|
||||
}
|
||||
|
||||
log.Printf("Got %d bytes archive", len(body))
|
||||
if err := extractMetabaseDB(bytes.NewReader(body)); err != nil {
|
||||
return fmt.Errorf("while extracting zip : %s", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractMetabaseDB(buf *bytes.Reader) error {
|
||||
r, err := zip.NewReader(buf, int64(buf.Len()))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for _, f := range r.File {
|
||||
if strings.Contains(f.Name, "..") {
|
||||
return fmt.Errorf("invalid path '%s' in archive", f.Name)
|
||||
}
|
||||
tfname := fmt.Sprintf("%s/%s", metabaseDbPath, f.Name)
|
||||
log.Debugf("%s -> %d", f.Name, f.UncompressedSize64)
|
||||
if f.UncompressedSize64 == 0 {
|
||||
continue
|
||||
}
|
||||
tfd, err := os.OpenFile(tfname, os.O_RDWR|os.O_TRUNC|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed opening target file '%s' : %s", tfname, err)
|
||||
}
|
||||
rc, err := f.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("while opening zip content %s : %s", f.Name, err)
|
||||
}
|
||||
written, err := io.Copy(tfd, rc)
|
||||
if err == io.EOF {
|
||||
log.Printf("files finished ok")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("while copying content to %s : %s", tfname, err)
|
||||
}
|
||||
log.Infof("written %d bytes to %s", written, tfname)
|
||||
rc.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resetMetabasePassword(newpassword string) error {
|
||||
|
||||
httpctx := sling.New().Base(metabaseURI).Set("User-Agent", fmt.Sprintf("CrowdWatch/%s", cwversion.VersionStr()))
|
||||
|
||||
log.Printf("Waiting for metabase API to be up (can take up to a minute)")
|
||||
for {
|
||||
sessionreq, err := httpctx.New().Post(metabaseURISession).BodyJSON(map[string]string{"username": defaultEmail, "password": defaultPassword}).Request()
|
||||
if err != nil {
|
||||
return fmt.Errorf("api signin: HTTP request creation failed: %s", err)
|
||||
}
|
||||
httpClient := http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := httpClient.Do(sessionreq)
|
||||
if err != nil {
|
||||
fmt.Printf(".")
|
||||
log.Debugf("While waiting for metabase to be up : %s", err)
|
||||
time.Sleep(1 * time.Second)
|
||||
continue
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
fmt.Printf("\n")
|
||||
log.Printf("Metabase API is up")
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("metabase session unable to read API response body: '%s'", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("metabase session http error (%d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
log.Printf("Successfully authenticated")
|
||||
jsonResp := make(map[string]string)
|
||||
err = json.Unmarshal(body, &jsonResp)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unmarshal metabase api response '%s': %s", string(body), err.Error())
|
||||
}
|
||||
log.Debugf("unmarshaled response : %v", jsonResp)
|
||||
httpctx = httpctx.Set("Cookie", fmt.Sprintf("metabase.SESSION=%s", jsonResp["id"]))
|
||||
break
|
||||
}
|
||||
|
||||
/*rescan values*/
|
||||
sessionreq, err := httpctx.New().Post(metabaseURIRescan).Request()
|
||||
if err != nil {
|
||||
return fmt.Errorf("metabase rescan_values http error : %s", err)
|
||||
}
|
||||
httpClient := http.Client{Timeout: 20 * time.Second}
|
||||
resp, err := httpClient.Do(sessionreq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("while trying to do rescan api call to metabase : %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("while reading rescan api call response : %s", err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
return fmt.Errorf("got '%s' (http:%d) while trying to rescan metabase", string(body), resp.StatusCode)
|
||||
}
|
||||
/*update password*/
|
||||
sessionreq, err = httpctx.New().Put(metabaseURIUpdatepwd).BodyJSON(map[string]string{
|
||||
"id": "1",
|
||||
"password": newpassword,
|
||||
"old_password": defaultPassword}).Request()
|
||||
if err != nil {
|
||||
return fmt.Errorf("metabase password change http error : %s", err)
|
||||
}
|
||||
httpClient = http.Client{Timeout: 20 * time.Second}
|
||||
resp, err = httpClient.Do(sessionreq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("while trying to reset metabase password : %s", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, err = ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("while reading from %s: '%s'", metabaseURIUpdatepwd, err)
|
||||
}
|
||||
if resp.StatusCode != 200 {
|
||||
log.Printf("Got %s (http:%d) while trying to reset password.", string(body), resp.StatusCode)
|
||||
log.Printf("Password has probably already been changed.")
|
||||
log.Printf("Use the dashboard install command to reset existing setup.")
|
||||
return fmt.Errorf("got http error %d on %s : %s", resp.StatusCode, metabaseURIUpdatepwd, string(body))
|
||||
}
|
||||
log.Printf("Changed password !")
|
||||
return nil
|
||||
}
|
||||
|
||||
func startMetabase() error {
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
|
||||
if err := cli.ContainerStart(ctx, metabaseContainerID, types.ContainerStartOptions{}); err != nil {
|
||||
return fmt.Errorf("failed while starting %s : %s", metabaseContainerID, err)
|
||||
}
|
||||
log.Warn("Metabase requires 1-2GB of RAM, your system is below this requirement")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func stopMetabase(remove bool) error {
|
||||
log.Printf("Stop docker metabase %s", metabaseContainerID)
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create docker client : %s", err)
|
||||
}
|
||||
var to time.Duration = 20 * time.Second
|
||||
if err := cli.ContainerStop(ctx, metabaseContainerID, &to); err != nil {
|
||||
return fmt.Errorf("failed while stopping %s : %s", metabaseContainerID, err)
|
||||
func warnIfNotLoopback(addr string) {
|
||||
if addr == "127.0.0.1" || addr == "::1" {
|
||||
return
|
||||
}
|
||||
|
||||
if remove {
|
||||
log.Printf("Removing docker metabase %s", metabaseContainerID)
|
||||
if err := cli.ContainerRemove(ctx, metabaseContainerID, types.ContainerRemoveOptions{}); err != nil {
|
||||
return fmt.Errorf("failed remove container %s : %s", metabaseContainerID, err)
|
||||
log.Warnf("You are potentially exposing your metabase port to the internet (addr: %s), please consider using a reverse proxy", addr)
|
||||
}
|
||||
|
||||
func disclaimer(forceYes *bool) error {
|
||||
if !*forceYes {
|
||||
var answer bool
|
||||
|
||||
prompt := &survey.Confirm{
|
||||
Message: "CrowdSec takes no responsibility for the security of your metabase instance. Do you accept these responsibilities ?",
|
||||
Default: true,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return fmt.Errorf("unable to ask to question: %s", err)
|
||||
}
|
||||
|
||||
if !answer {
|
||||
return fmt.Errorf("user stated no to responsibilities")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
log.Warn("CrowdSec takes no responsibility for the security of your metabase instance. You used force yes, so you accept this disclaimer")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func checkGroups(forceYes *bool) (*user.Group, error) {
|
||||
dockerGroup, err := user.LookupGroup(crowdsecGroup)
|
||||
if err == nil {
|
||||
return dockerGroup, nil
|
||||
}
|
||||
|
||||
if !*forceYes {
|
||||
var answer bool
|
||||
|
||||
prompt := &survey.Confirm{
|
||||
Message: fmt.Sprintf("For metabase docker to be able to access SQLite file we need to add a new group called '%s' to the system, is it ok for you ?", crowdsecGroup),
|
||||
Default: true,
|
||||
}
|
||||
|
||||
if err := survey.AskOne(prompt, &answer); err != nil {
|
||||
return dockerGroup, fmt.Errorf("unable to ask to question: %s", err)
|
||||
}
|
||||
|
||||
if !answer {
|
||||
return dockerGroup, fmt.Errorf("unable to continue without creating '%s' group", crowdsecGroup)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
groupAddCmd, err := exec.LookPath("groupadd")
|
||||
if err != nil {
|
||||
return dockerGroup, fmt.Errorf("unable to find 'groupadd' command, can't continue")
|
||||
}
|
||||
|
||||
groupAdd := &exec.Cmd{Path: groupAddCmd, Args: []string{groupAddCmd, crowdsecGroup}}
|
||||
if err := groupAdd.Run(); err != nil {
|
||||
return dockerGroup, fmt.Errorf("unable to add group '%s': %s", dockerGroup, err)
|
||||
}
|
||||
|
||||
return user.LookupGroup(crowdsecGroup)
|
||||
}
|
||||
|
||||
func createMetabase() error {
|
||||
ctx := context.Background()
|
||||
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
|
||||
func (cli *cliDashboard) chownDatabase(gid string) error {
|
||||
cfg := cli.cfg()
|
||||
intID, err := strconv.Atoi(gid)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to start docker client : %s", err)
|
||||
return fmt.Errorf("unable to convert group ID to int: %s", err)
|
||||
}
|
||||
|
||||
log.Printf("Pulling docker image %s", metabaseImage)
|
||||
reader, err := cli.ImagePull(ctx, metabaseImage, types.ImagePullOptions{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pull docker image : %s", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
scanner := bufio.NewScanner(reader)
|
||||
for scanner.Scan() {
|
||||
fmt.Print(".")
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return fmt.Errorf("failed to read imagepull reader: %s", err)
|
||||
}
|
||||
fmt.Print("\n")
|
||||
|
||||
hostConfig := &container.HostConfig{
|
||||
PortBindings: nat.PortMap{
|
||||
"3000/tcp": []nat.PortBinding{
|
||||
{
|
||||
HostIP: metabaseListenAddress,
|
||||
HostPort: metabaseListenPort,
|
||||
},
|
||||
},
|
||||
},
|
||||
Mounts: []mount.Mount{
|
||||
{
|
||||
Type: mount.TypeBind,
|
||||
Source: metabaseDbPath,
|
||||
Target: "/metabase-data",
|
||||
},
|
||||
},
|
||||
}
|
||||
dockerConfig := &container.Config{
|
||||
Image: metabaseImage,
|
||||
Tty: true,
|
||||
Env: []string{"MB_DB_FILE=/metabase-data/metabase.db"},
|
||||
if stat, err := os.Stat(cfg.DbConfig.DbPath); !os.IsNotExist(err) {
|
||||
info := stat.Sys()
|
||||
if err := os.Chown(cfg.DbConfig.DbPath, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
|
||||
return fmt.Errorf("unable to chown sqlite db file '%s': %s", cfg.DbConfig.DbPath, err)
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Creating container")
|
||||
resp, err := cli.ContainerCreate(ctx, dockerConfig, hostConfig, nil, metabaseContainerID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create container : %s", err)
|
||||
}
|
||||
log.Printf("Starting container")
|
||||
if err := cli.ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil {
|
||||
return fmt.Errorf("failed to start docker container : %s", err)
|
||||
if cfg.DbConfig.Type == "sqlite" && cfg.DbConfig.UseWal != nil && *cfg.DbConfig.UseWal {
|
||||
for _, ext := range []string{"-wal", "-shm"} {
|
||||
file := cfg.DbConfig.DbPath + ext
|
||||
if stat, err := os.Stat(file); !os.IsNotExist(err) {
|
||||
info := stat.Sys()
|
||||
if err := os.Chown(file, int(info.(*syscall.Stat_t).Uid), intID); err != nil {
|
||||
return fmt.Errorf("unable to chown sqlite db file '%s': %s", file, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
32
cmd/crowdsec-cli/dashboard_unsupported.go
Normal file
32
cmd/crowdsec-cli/dashboard_unsupported.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
//go:build !linux
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
type cliDashboard struct{
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLIDashboard(cfg configGetter) *cliDashboard {
|
||||
return &cliDashboard{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli cliDashboard) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "dashboard",
|
||||
DisableAutoGenTag: true,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
log.Infof("Dashboard command is disabled on %s", runtime.GOOS)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
523
cmd/crowdsec-cli/decisions.go
Normal file
523
cmd/crowdsec-cli/decisions.go
Normal file
|
@ -0,0 +1,523 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/csv"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/fatih/color"
|
||||
"github.com/go-openapi/strfmt"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/version"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/apiclient"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
var Client *apiclient.ApiClient
|
||||
|
||||
func (cli *cliDecisions) decisionsToTable(alerts *models.GetAlertsResponse, printMachine bool) error {
|
||||
/*here we cheat a bit : to make it more readable for the user, we dedup some entries*/
|
||||
spamLimit := make(map[string]bool)
|
||||
skipped := 0
|
||||
|
||||
for aIdx := 0; aIdx < len(*alerts); aIdx++ {
|
||||
alertItem := (*alerts)[aIdx]
|
||||
newDecisions := make([]*models.Decision, 0)
|
||||
|
||||
for _, decisionItem := range alertItem.Decisions {
|
||||
spamKey := fmt.Sprintf("%t:%s:%s:%s", *decisionItem.Simulated, *decisionItem.Type, *decisionItem.Scope, *decisionItem.Value)
|
||||
if _, ok := spamLimit[spamKey]; ok {
|
||||
skipped++
|
||||
continue
|
||||
}
|
||||
|
||||
spamLimit[spamKey] = true
|
||||
|
||||
newDecisions = append(newDecisions, decisionItem)
|
||||
}
|
||||
|
||||
alertItem.Decisions = newDecisions
|
||||
}
|
||||
|
||||
switch cli.cfg().Cscli.Output {
|
||||
case "raw":
|
||||
csvwriter := csv.NewWriter(os.Stdout)
|
||||
header := []string{"id", "source", "ip", "reason", "action", "country", "as", "events_count", "expiration", "simulated", "alert_id"}
|
||||
|
||||
if printMachine {
|
||||
header = append(header, "machine")
|
||||
}
|
||||
|
||||
err := csvwriter.Write(header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, alertItem := range *alerts {
|
||||
for _, decisionItem := range alertItem.Decisions {
|
||||
raw := []string{
|
||||
fmt.Sprintf("%d", decisionItem.ID),
|
||||
*decisionItem.Origin,
|
||||
*decisionItem.Scope + ":" + *decisionItem.Value,
|
||||
*decisionItem.Scenario,
|
||||
*decisionItem.Type,
|
||||
alertItem.Source.Cn,
|
||||
alertItem.Source.GetAsNumberName(),
|
||||
fmt.Sprintf("%d", *alertItem.EventsCount),
|
||||
*decisionItem.Duration,
|
||||
fmt.Sprintf("%t", *decisionItem.Simulated),
|
||||
fmt.Sprintf("%d", alertItem.ID),
|
||||
}
|
||||
if printMachine {
|
||||
raw = append(raw, alertItem.MachineID)
|
||||
}
|
||||
|
||||
err := csvwriter.Write(raw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
csvwriter.Flush()
|
||||
case "json":
|
||||
if *alerts == nil {
|
||||
// avoid returning "null" in `json"
|
||||
// could be cleaner if we used slice of alerts directly
|
||||
fmt.Println("[]")
|
||||
return nil
|
||||
}
|
||||
|
||||
x, _ := json.MarshalIndent(alerts, "", " ")
|
||||
fmt.Printf("%s", string(x))
|
||||
case "human":
|
||||
if len(*alerts) == 0 {
|
||||
fmt.Println("No active decisions")
|
||||
return nil
|
||||
}
|
||||
|
||||
cli.decisionsTable(color.Output, alerts, printMachine)
|
||||
|
||||
if skipped > 0 {
|
||||
fmt.Printf("%d duplicated entries skipped\n", skipped)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type cliDecisions struct {
|
||||
cfg configGetter
|
||||
}
|
||||
|
||||
func NewCLIDecisions(cfg configGetter) *cliDecisions {
|
||||
return &cliDecisions{
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
func (cli *cliDecisions) NewCommand() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "decisions [action]",
|
||||
Short: "Manage decisions",
|
||||
Long: `Add/List/Delete/Import decisions from LAPI`,
|
||||
Example: `cscli decisions [action] [filter]`,
|
||||
Aliases: []string{"decision"},
|
||||
/*TBD example*/
|
||||
Args: cobra.MinimumNArgs(1),
|
||||
DisableAutoGenTag: true,
|
||||
PersistentPreRunE: func(_ *cobra.Command, _ []string) error {
|
||||
cfg := cli.cfg()
|
||||
if err := cfg.LoadAPIClient(); err != nil {
|
||||
return fmt.Errorf("loading api client: %w", err)
|
||||
}
|
||||
password := strfmt.Password(cfg.API.Client.Credentials.Password)
|
||||
apiurl, err := url.Parse(cfg.API.Client.Credentials.URL)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing api url %s: %w", cfg.API.Client.Credentials.URL, err)
|
||||
}
|
||||
Client, err = apiclient.NewClient(&apiclient.Config{
|
||||
MachineID: cfg.API.Client.Credentials.Login,
|
||||
Password: password,
|
||||
UserAgent: fmt.Sprintf("crowdsec/%s", version.String()),
|
||||
URL: apiurl,
|
||||
VersionPrefix: "v1",
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("creating api client: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.AddCommand(cli.newListCmd())
|
||||
cmd.AddCommand(cli.newAddCmd())
|
||||
cmd.AddCommand(cli.newDeleteCmd())
|
||||
cmd.AddCommand(cli.newImportCmd())
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDecisions) newListCmd() *cobra.Command {
|
||||
var filter = apiclient.AlertsListOpts{
|
||||
ValueEquals: new(string),
|
||||
ScopeEquals: new(string),
|
||||
ScenarioEquals: new(string),
|
||||
OriginEquals: new(string),
|
||||
IPEquals: new(string),
|
||||
RangeEquals: new(string),
|
||||
Since: new(string),
|
||||
Until: new(string),
|
||||
TypeEquals: new(string),
|
||||
IncludeCAPI: new(bool),
|
||||
Limit: new(int),
|
||||
}
|
||||
|
||||
NoSimu := new(bool)
|
||||
contained := new(bool)
|
||||
|
||||
var printMachine bool
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list [options]",
|
||||
Short: "List decisions from LAPI",
|
||||
Example: `cscli decisions list -i 1.2.3.4
|
||||
cscli decisions list -r 1.2.3.0/24
|
||||
cscli decisions list -s crowdsecurity/ssh-bf
|
||||
cscli decisions list --origin lists --scenario list_name
|
||||
`,
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
/*take care of shorthand options*/
|
||||
if err = manageCliDecisionAlerts(filter.IPEquals, filter.RangeEquals, filter.ScopeEquals, filter.ValueEquals); err != nil {
|
||||
return err
|
||||
}
|
||||
filter.ActiveDecisionEquals = new(bool)
|
||||
*filter.ActiveDecisionEquals = true
|
||||
if NoSimu != nil && *NoSimu {
|
||||
filter.IncludeSimulated = new(bool)
|
||||
}
|
||||
/* nullify the empty entries to avoid bad filter */
|
||||
if *filter.Until == "" {
|
||||
filter.Until = nil
|
||||
} else if strings.HasSuffix(*filter.Until, "d") {
|
||||
/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
|
||||
realDuration := strings.TrimSuffix(*filter.Until, "d")
|
||||
days, err := strconv.Atoi(realDuration)
|
||||
if err != nil {
|
||||
printHelp(cmd)
|
||||
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Until)
|
||||
}
|
||||
*filter.Until = fmt.Sprintf("%d%s", days*24, "h")
|
||||
}
|
||||
|
||||
if *filter.Since == "" {
|
||||
filter.Since = nil
|
||||
} else if strings.HasSuffix(*filter.Since, "d") {
|
||||
/*time.ParseDuration support hours 'h' as bigger unit, let's make the user's life easier*/
|
||||
realDuration := strings.TrimSuffix(*filter.Since, "d")
|
||||
days, err := strconv.Atoi(realDuration)
|
||||
if err != nil {
|
||||
printHelp(cmd)
|
||||
return fmt.Errorf("can't parse duration %s, valid durations format: 1d, 4h, 4h15m", *filter.Since)
|
||||
}
|
||||
*filter.Since = fmt.Sprintf("%d%s", days*24, "h")
|
||||
}
|
||||
if *filter.IncludeCAPI {
|
||||
*filter.Limit = 0
|
||||
}
|
||||
if *filter.TypeEquals == "" {
|
||||
filter.TypeEquals = nil
|
||||
}
|
||||
if *filter.ValueEquals == "" {
|
||||
filter.ValueEquals = nil
|
||||
}
|
||||
if *filter.ScopeEquals == "" {
|
||||
filter.ScopeEquals = nil
|
||||
}
|
||||
if *filter.ScenarioEquals == "" {
|
||||
filter.ScenarioEquals = nil
|
||||
}
|
||||
if *filter.IPEquals == "" {
|
||||
filter.IPEquals = nil
|
||||
}
|
||||
if *filter.RangeEquals == "" {
|
||||
filter.RangeEquals = nil
|
||||
}
|
||||
|
||||
if *filter.OriginEquals == "" {
|
||||
filter.OriginEquals = nil
|
||||
}
|
||||
|
||||
if contained != nil && *contained {
|
||||
filter.Contains = new(bool)
|
||||
}
|
||||
|
||||
alerts, _, err := Client.Alerts.List(context.Background(), filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to retrieve decisions: %w", err)
|
||||
}
|
||||
|
||||
err = cli.decisionsToTable(alerts, printMachine)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to print decisions: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().BoolVarP(filter.IncludeCAPI, "all", "a", false, "Include decisions from Central API")
|
||||
cmd.Flags().StringVar(filter.Since, "since", "", "restrict to alerts newer than since (ie. 4h, 30d)")
|
||||
cmd.Flags().StringVar(filter.Until, "until", "", "restrict to alerts older than until (ie. 4h, 30d)")
|
||||
cmd.Flags().StringVarP(filter.TypeEquals, "type", "t", "", "restrict to this decision type (ie. ban,captcha)")
|
||||
cmd.Flags().StringVar(filter.ScopeEquals, "scope", "", "restrict to this scope (ie. ip,range,session)")
|
||||
cmd.Flags().StringVar(filter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
|
||||
cmd.Flags().StringVarP(filter.ValueEquals, "value", "v", "", "restrict to this value (ie. 1.2.3.4,userName)")
|
||||
cmd.Flags().StringVarP(filter.ScenarioEquals, "scenario", "s", "", "restrict to this scenario (ie. crowdsecurity/ssh-bf)")
|
||||
cmd.Flags().StringVarP(filter.IPEquals, "ip", "i", "", "restrict to alerts from this source ip (shorthand for --scope ip --value <IP>)")
|
||||
cmd.Flags().StringVarP(filter.RangeEquals, "range", "r", "", "restrict to alerts from this source range (shorthand for --scope range --value <RANGE>)")
|
||||
cmd.Flags().IntVarP(filter.Limit, "limit", "l", 100, "number of alerts to get (use 0 to remove the limit)")
|
||||
cmd.Flags().BoolVar(NoSimu, "no-simu", false, "exclude decisions in simulation mode")
|
||||
cmd.Flags().BoolVarP(&printMachine, "machine", "m", false, "print machines that triggered decisions")
|
||||
cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDecisions) newAddCmd() *cobra.Command {
|
||||
var (
|
||||
addIP string
|
||||
addRange string
|
||||
addDuration string
|
||||
addValue string
|
||||
addScope string
|
||||
addReason string
|
||||
addType string
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "add [options]",
|
||||
Short: "Add decision to LAPI",
|
||||
Example: `cscli decisions add --ip 1.2.3.4
|
||||
cscli decisions add --range 1.2.3.0/24
|
||||
cscli decisions add --ip 1.2.3.4 --duration 24h --type captcha
|
||||
cscli decisions add --scope username --value foobar
|
||||
`,
|
||||
/*TBD : fix long and example*/
|
||||
Args: cobra.ExactArgs(0),
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
alerts := models.AddAlertsRequest{}
|
||||
origin := types.CscliOrigin
|
||||
capacity := int32(0)
|
||||
leakSpeed := "0"
|
||||
eventsCount := int32(1)
|
||||
empty := ""
|
||||
simulated := false
|
||||
startAt := time.Now().UTC().Format(time.RFC3339)
|
||||
stopAt := time.Now().UTC().Format(time.RFC3339)
|
||||
createdAt := time.Now().UTC().Format(time.RFC3339)
|
||||
|
||||
/*take care of shorthand options*/
|
||||
if err = manageCliDecisionAlerts(&addIP, &addRange, &addScope, &addValue); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if addIP != "" {
|
||||
addValue = addIP
|
||||
addScope = types.Ip
|
||||
} else if addRange != "" {
|
||||
addValue = addRange
|
||||
addScope = types.Range
|
||||
} else if addValue == "" {
|
||||
printHelp(cmd)
|
||||
return errors.New("missing arguments, a value is required (--ip, --range or --scope and --value)")
|
||||
}
|
||||
|
||||
if addReason == "" {
|
||||
addReason = fmt.Sprintf("manual '%s' from '%s'", addType, cli.cfg().API.Client.Credentials.Login)
|
||||
}
|
||||
decision := models.Decision{
|
||||
Duration: &addDuration,
|
||||
Scope: &addScope,
|
||||
Value: &addValue,
|
||||
Type: &addType,
|
||||
Scenario: &addReason,
|
||||
Origin: &origin,
|
||||
}
|
||||
alert := models.Alert{
|
||||
Capacity: &capacity,
|
||||
Decisions: []*models.Decision{&decision},
|
||||
Events: []*models.Event{},
|
||||
EventsCount: &eventsCount,
|
||||
Leakspeed: &leakSpeed,
|
||||
Message: &addReason,
|
||||
ScenarioHash: &empty,
|
||||
Scenario: &addReason,
|
||||
ScenarioVersion: &empty,
|
||||
Simulated: &simulated,
|
||||
// setting empty scope/value broke plugins, and it didn't seem to be needed anymore w/ latest papi changes
|
||||
Source: &models.Source{
|
||||
AsName: empty,
|
||||
AsNumber: empty,
|
||||
Cn: empty,
|
||||
IP: addValue,
|
||||
Range: "",
|
||||
Scope: &addScope,
|
||||
Value: &addValue,
|
||||
},
|
||||
StartAt: &startAt,
|
||||
StopAt: &stopAt,
|
||||
CreatedAt: createdAt,
|
||||
}
|
||||
alerts = append(alerts, &alert)
|
||||
|
||||
_, _, err = Client.Alerts.Add(context.Background(), alerts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Info("Decision successfully added")
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().StringVarP(&addIP, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
|
||||
cmd.Flags().StringVarP(&addRange, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
|
||||
cmd.Flags().StringVarP(&addDuration, "duration", "d", "4h", "Decision duration (ie. 1h,4h,30m)")
|
||||
cmd.Flags().StringVarP(&addValue, "value", "v", "", "The value (ie. --scope username --value foobar)")
|
||||
cmd.Flags().StringVar(&addScope, "scope", types.Ip, "Decision scope (ie. ip,range,username)")
|
||||
cmd.Flags().StringVarP(&addReason, "reason", "R", "", "Decision reason (ie. scenario-name)")
|
||||
cmd.Flags().StringVarP(&addType, "type", "t", "ban", "Decision type (ie. ban,captcha,throttle)")
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli *cliDecisions) newDeleteCmd() *cobra.Command {
|
||||
delFilter := apiclient.DecisionsDeleteOpts{
|
||||
ScopeEquals: new(string),
|
||||
ValueEquals: new(string),
|
||||
TypeEquals: new(string),
|
||||
IPEquals: new(string),
|
||||
RangeEquals: new(string),
|
||||
ScenarioEquals: new(string),
|
||||
OriginEquals: new(string),
|
||||
}
|
||||
|
||||
var delDecisionID string
|
||||
|
||||
var delDecisionAll bool
|
||||
|
||||
contained := new(bool)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "delete [options]",
|
||||
Short: "Delete decisions",
|
||||
DisableAutoGenTag: true,
|
||||
Aliases: []string{"remove"},
|
||||
Example: `cscli decisions delete -r 1.2.3.0/24
|
||||
cscli decisions delete -i 1.2.3.4
|
||||
cscli decisions delete --id 42
|
||||
cscli decisions delete --type captcha
|
||||
cscli decisions delete --origin lists --scenario list_name
|
||||
`,
|
||||
/*TBD : refaire le Long/Example*/
|
||||
PreRunE: func(cmd *cobra.Command, _ []string) error {
|
||||
if delDecisionAll {
|
||||
return nil
|
||||
}
|
||||
if *delFilter.ScopeEquals == "" && *delFilter.ValueEquals == "" &&
|
||||
*delFilter.TypeEquals == "" && *delFilter.IPEquals == "" &&
|
||||
*delFilter.RangeEquals == "" && *delFilter.ScenarioEquals == "" &&
|
||||
*delFilter.OriginEquals == "" && delDecisionID == "" {
|
||||
cmd.Usage()
|
||||
return errors.New("at least one filter or --all must be specified")
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
var err error
|
||||
var decisions *models.DeleteDecisionResponse
|
||||
|
||||
/*take care of shorthand options*/
|
||||
if err = manageCliDecisionAlerts(delFilter.IPEquals, delFilter.RangeEquals, delFilter.ScopeEquals, delFilter.ValueEquals); err != nil {
|
||||
return err
|
||||
}
|
||||
if *delFilter.ScopeEquals == "" {
|
||||
delFilter.ScopeEquals = nil
|
||||
}
|
||||
if *delFilter.OriginEquals == "" {
|
||||
delFilter.OriginEquals = nil
|
||||
}
|
||||
if *delFilter.ValueEquals == "" {
|
||||
delFilter.ValueEquals = nil
|
||||
}
|
||||
if *delFilter.ScenarioEquals == "" {
|
||||
delFilter.ScenarioEquals = nil
|
||||
}
|
||||
if *delFilter.TypeEquals == "" {
|
||||
delFilter.TypeEquals = nil
|
||||
}
|
||||
if *delFilter.IPEquals == "" {
|
||||
delFilter.IPEquals = nil
|
||||
}
|
||||
if *delFilter.RangeEquals == "" {
|
||||
delFilter.RangeEquals = nil
|
||||
}
|
||||
if contained != nil && *contained {
|
||||
delFilter.Contains = new(bool)
|
||||
}
|
||||
|
||||
if delDecisionID == "" {
|
||||
decisions, _, err = Client.Decisions.Delete(context.Background(), delFilter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete decisions: %v", err)
|
||||
}
|
||||
} else {
|
||||
if _, err = strconv.Atoi(delDecisionID); err != nil {
|
||||
return fmt.Errorf("id '%s' is not an integer: %v", delDecisionID, err)
|
||||
}
|
||||
decisions, _, err = Client.Decisions.DeleteOne(context.Background(), delDecisionID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to delete decision: %v", err)
|
||||
}
|
||||
}
|
||||
log.Infof("%s decision(s) deleted", decisions.NbDeleted)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cmd.Flags().SortFlags = false
|
||||
cmd.Flags().StringVarP(delFilter.IPEquals, "ip", "i", "", "Source ip (shorthand for --scope ip --value <IP>)")
|
||||
cmd.Flags().StringVarP(delFilter.RangeEquals, "range", "r", "", "Range source ip (shorthand for --scope range --value <RANGE>)")
|
||||
cmd.Flags().StringVarP(delFilter.TypeEquals, "type", "t", "", "the decision type (ie. ban,captcha)")
|
||||
cmd.Flags().StringVarP(delFilter.ValueEquals, "value", "v", "", "the value to match for in the specified scope")
|
||||
cmd.Flags().StringVarP(delFilter.ScenarioEquals, "scenario", "s", "", "the scenario name (ie. crowdsecurity/ssh-bf)")
|
||||
cmd.Flags().StringVar(delFilter.OriginEquals, "origin", "", fmt.Sprintf("the value to match for the specified origin (%s ...)", strings.Join(types.GetOrigins(), ",")))
|
||||
|
||||
cmd.Flags().StringVar(&delDecisionID, "id", "", "decision id")
|
||||
cmd.Flags().BoolVar(&delDecisionAll, "all", false, "delete all decisions")
|
||||
cmd.Flags().BoolVar(contained, "contained", false, "query decisions contained by range")
|
||||
|
||||
return cmd
|
||||
}
|
280
cmd/crowdsec-cli/decisions_import.go
Normal file
280
cmd/crowdsec-cli/decisions_import.go
Normal file
|
@ -0,0 +1,280 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jszwec/csvutil"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/crowdsecurity/go-cs-lib/ptr"
|
||||
"github.com/crowdsecurity/go-cs-lib/slicetools"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
"github.com/crowdsecurity/crowdsec/pkg/types"
|
||||
)
|
||||
|
||||
// decisionRaw is only used to unmarshall json/csv decisions
|
||||
type decisionRaw struct {
|
||||
Duration string `csv:"duration,omitempty" json:"duration,omitempty"`
|
||||
Scenario string `csv:"reason,omitempty" json:"reason,omitempty"`
|
||||
Scope string `csv:"scope,omitempty" json:"scope,omitempty"`
|
||||
Type string `csv:"type,omitempty" json:"type,omitempty"`
|
||||
Value string `csv:"value" json:"value"`
|
||||
}
|
||||
|
||||
func parseDecisionList(content []byte, format string) ([]decisionRaw, error) {
|
||||
ret := []decisionRaw{}
|
||||
|
||||
switch format {
|
||||
case "values":
|
||||
log.Infof("Parsing values")
|
||||
|
||||
scanner := bufio.NewScanner(bytes.NewReader(content))
|
||||
for scanner.Scan() {
|
||||
value := strings.TrimSpace(scanner.Text())
|
||||
ret = append(ret, decisionRaw{Value: value})
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse values: '%s'", err)
|
||||
}
|
||||
case "json":
|
||||
log.Infof("Parsing json")
|
||||
|
||||
if err := json.Unmarshal(content, &ret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "csv":
|
||||
log.Infof("Parsing csv")
|
||||
|
||||
if err := csvutil.Unmarshal(content, &ret); err != nil {
|
||||
return nil, fmt.Errorf("unable to parse csv: '%s'", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid format '%s', expected one of 'json', 'csv', 'values'", format)
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
|
||||
func (cli *cliDecisions) runImport(cmd *cobra.Command, args []string) error {
|
||||
flags := cmd.Flags()
|
||||
|
||||
input, err := flags.GetString("input")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultDuration, err := flags.GetString("duration")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if defaultDuration == "" {
|
||||
return errors.New("--duration cannot be empty")
|
||||
}
|
||||
|
||||
defaultScope, err := flags.GetString("scope")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if defaultScope == "" {
|
||||
return errors.New("--scope cannot be empty")
|
||||
}
|
||||
|
||||
defaultReason, err := flags.GetString("reason")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if defaultReason == "" {
|
||||
return errors.New("--reason cannot be empty")
|
||||
}
|
||||
|
||||
defaultType, err := flags.GetString("type")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if defaultType == "" {
|
||||
return errors.New("--type cannot be empty")
|
||||
}
|
||||
|
||||
batchSize, err := flags.GetInt("batch")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
format, err := flags.GetString("format")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var (
|
||||
content []byte
|
||||
fin *os.File
|
||||
)
|
||||
|
||||
// set format if the file has a json or csv extension
|
||||
if format == "" {
|
||||
if strings.HasSuffix(input, ".json") {
|
||||
format = "json"
|
||||
} else if strings.HasSuffix(input, ".csv") {
|
||||
format = "csv"
|
||||
}
|
||||
}
|
||||
|
||||
if format == "" {
|
||||
return errors.New("unable to guess format from file extension, please provide a format with --format flag")
|
||||
}
|
||||
|
||||
if input == "-" {
|
||||
fin = os.Stdin
|
||||
input = "stdin"
|
||||
} else {
|
||||
fin, err = os.Open(input)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open %s: %s", input, err)
|
||||
}
|
||||
}
|
||||
|
||||
content, err = io.ReadAll(fin)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to read from %s: %s", input, err)
|
||||
}
|
||||
|
||||
decisionsListRaw, err := parseDecisionList(content, format)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
decisions := make([]*models.Decision, len(decisionsListRaw))
|
||||
|
||||
for i, d := range decisionsListRaw {
|
||||
if d.Value == "" {
|
||||
return fmt.Errorf("item %d: missing 'value'", i)
|
||||
}
|
||||
|
||||
if d.Duration == "" {
|
||||
d.Duration = defaultDuration
|
||||
log.Debugf("item %d: missing 'duration', using default '%s'", i, defaultDuration)
|
||||
}
|
||||
|
||||
if d.Scenario == "" {
|
||||
d.Scenario = defaultReason
|
||||
log.Debugf("item %d: missing 'reason', using default '%s'", i, defaultReason)
|
||||
}
|
||||
|
||||
if d.Type == "" {
|
||||
d.Type = defaultType
|
||||
log.Debugf("item %d: missing 'type', using default '%s'", i, defaultType)
|
||||
}
|
||||
|
||||
if d.Scope == "" {
|
||||
d.Scope = defaultScope
|
||||
log.Debugf("item %d: missing 'scope', using default '%s'", i, defaultScope)
|
||||
}
|
||||
|
||||
decisions[i] = &models.Decision{
|
||||
Value: ptr.Of(d.Value),
|
||||
Duration: ptr.Of(d.Duration),
|
||||
Origin: ptr.Of(types.CscliImportOrigin),
|
||||
Scenario: ptr.Of(d.Scenario),
|
||||
Type: ptr.Of(d.Type),
|
||||
Scope: ptr.Of(d.Scope),
|
||||
Simulated: ptr.Of(false),
|
||||
}
|
||||
}
|
||||
|
||||
if len(decisions) > 1000 {
|
||||
log.Infof("You are about to add %d decisions, this may take a while", len(decisions))
|
||||
}
|
||||
|
||||
for _, chunk := range slicetools.Chunks(decisions, batchSize) {
|
||||
log.Debugf("Processing chunk of %d decisions", len(chunk))
|
||||
importAlert := models.Alert{
|
||||
CreatedAt: time.Now().UTC().Format(time.RFC3339),
|
||||
Scenario: ptr.Of(fmt.Sprintf("import %s: %d IPs", input, len(chunk))),
|
||||
|
||||
Message: ptr.Of(""),
|
||||
Events: []*models.Event{},
|
||||
Source: &models.Source{
|
||||
Scope: ptr.Of(""),
|
||||
Value: ptr.Of(""),
|
||||
},
|
||||
StartAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
|
||||
StopAt: ptr.Of(time.Now().UTC().Format(time.RFC3339)),
|
||||
Capacity: ptr.Of(int32(0)),
|
||||
Simulated: ptr.Of(false),
|
||||
EventsCount: ptr.Of(int32(len(chunk))),
|
||||
Leakspeed: ptr.Of(""),
|
||||
ScenarioHash: ptr.Of(""),
|
||||
ScenarioVersion: ptr.Of(""),
|
||||
Decisions: chunk,
|
||||
}
|
||||
|
||||
_, _, err = Client.Alerts.Add(context.Background(), models.AddAlertsRequest{&importAlert})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
log.Infof("Imported %d decisions", len(decisions))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (cli *cliDecisions) newImportCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "import [options]",
|
||||
Short: "Import decisions from a file or pipe",
|
||||
Long: "expected format:\n" +
|
||||
"csv : any of duration,reason,scope,type,value, with a header line\n" +
|
||||
"json :" + "`{" + `"duration" : "24h", "reason" : "my_scenario", "scope" : "ip", "type" : "ban", "value" : "x.y.z.z"` + "}`",
|
||||
Args: cobra.NoArgs,
|
||||
DisableAutoGenTag: true,
|
||||
Example: `decisions.csv:
|
||||
duration,scope,value
|
||||
24h,ip,1.2.3.4
|
||||
|
||||
$ cscli decisions import -i decisions.csv
|
||||
|
||||
decisions.json:
|
||||
[{"duration" : "4h", "scope" : "ip", "type" : "ban", "value" : "1.2.3.4"}]
|
||||
|
||||
The file format is detected from the extension, but can be forced with the --format option
|
||||
which is required when reading from standard input.
|
||||
|
||||
Raw values, standard input:
|
||||
|
||||
$ echo "1.2.3.4" | cscli decisions import -i - --format values
|
||||
`,
|
||||
RunE: cli.runImport,
|
||||
}
|
||||
|
||||
flags := cmd.Flags()
|
||||
flags.SortFlags = false
|
||||
flags.StringP("input", "i", "", "Input file")
|
||||
flags.StringP("duration", "d", "4h", "Decision duration: 1h,4h,30m")
|
||||
flags.String("scope", types.Ip, "Decision scope: ip,range,username")
|
||||
flags.StringP("reason", "R", "manual", "Decision reason: <scenario-name>")
|
||||
flags.StringP("type", "t", "ban", "Decision type: ban,captcha,throttle")
|
||||
flags.Int("batch", 0, "Split import in batches of N decisions")
|
||||
flags.String("format", "", "Input format: 'json', 'csv' or 'values' (each line is a value, no headers)")
|
||||
|
||||
cmd.MarkFlagRequired("input")
|
||||
|
||||
return cmd
|
||||
}
|
50
cmd/crowdsec-cli/decisions_table.go
Normal file
50
cmd/crowdsec-cli/decisions_table.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"github.com/crowdsecurity/crowdsec/pkg/models"
|
||||
)
|
||||
|
||||
func (cli *cliDecisions) decisionsTable(out io.Writer, alerts *models.GetAlertsResponse, printMachine bool) {
|
||||
t := newTable(out)
|
||||
t.SetRowLines(false)
|
||||
|
||||
header := []string{"ID", "Source", "Scope:Value", "Reason", "Action", "Country", "AS", "Events", "expiration", "Alert ID"}
|
||||
if printMachine {
|
||||
header = append(header, "Machine")
|
||||
}
|
||||
|
||||
t.SetHeaders(header...)
|
||||
|
||||
for _, alertItem := range *alerts {
|
||||
for _, decisionItem := range alertItem.Decisions {
|
||||
if *alertItem.Simulated {
|
||||
*decisionItem.Type = fmt.Sprintf("(simul)%s", *decisionItem.Type)
|
||||
}
|
||||
|
||||
row := []string{
|
||||
strconv.Itoa(int(decisionItem.ID)),
|
||||
*decisionItem.Origin,
|
||||
*decisionItem.Scope + ":" + *decisionItem.Value,
|
||||
*decisionItem.Scenario,
|
||||
*decisionItem.Type,
|
||||
alertItem.Source.Cn,
|
||||
alertItem.Source.GetAsNumberName(),
|
||||
strconv.Itoa(int(*alertItem.EventsCount)),
|
||||
*decisionItem.Duration,
|
||||
strconv.Itoa(int(alertItem.ID)),
|
||||
}
|
||||
|
||||
if printMachine {
|
||||
row = append(row, alertItem.MachineID)
|
||||
}
|
||||
|
||||
t.AddRow(row...)
|
||||
}
|
||||
}
|
||||
|
||||
t.Render()
|
||||
}
|
51
cmd/crowdsec-cli/doc.go
Normal file
51
cmd/crowdsec-cli/doc.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/cobra/doc"
|
||||
)
|
||||
|
||||
type cliDoc struct{}
|
||||
|
||||
func NewCLIDoc() *cliDoc {
|
||||
return &cliDoc{}
|
||||
}
|
||||
|
||||
func (cli cliDoc) NewCommand(rootCmd *cobra.Command) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "doc",
|
||||
Short: "Generate the documentation in `./doc/`. Directory must exist.",
|
||||
Args: cobra.ExactArgs(0),
|
||||
Hidden: true,
|
||||
DisableAutoGenTag: true,
|
||||
RunE: func(_ *cobra.Command, _ []string) error {
|
||||
if err := doc.GenMarkdownTreeCustom(rootCmd, "./doc/", cli.filePrepender, cli.linkHandler); err != nil {
|
||||
return fmt.Errorf("failed to generate cobra doc: %s", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func (cli cliDoc) filePrepender(filename string) string {
|
||||
const header = `---
|
||||
id: %s
|
||||
title: %s
|
||||
---
|
||||
`
|
||||
|
||||
name := filepath.Base(filename)
|
||||
base := strings.TrimSuffix(name, filepath.Ext(name))
|
||||
|
||||
return fmt.Sprintf(header, base, strings.ReplaceAll(base, "_", " "))
|
||||
}
|
||||
|
||||
func (cli cliDoc) linkHandler(name string) string {
|
||||
return fmt.Sprintf("/cscli/%s", name)
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
## cscli
|
||||
|
||||
cscli allows you to manage crowdsec
|
||||
|
||||
### Synopsis
|
||||
|
||||
cscli is the main command to interact with your crowdsec service, scenarios & db.
|
||||
It is meant to allow you to manage bans, parsers/scenarios/etc, api and generally manage you crowdsec setup.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
View/Add/Remove bans:
|
||||
- cscli ban list
|
||||
- cscli ban add ip 1.2.3.4 24h 'go away'
|
||||
- cscli ban del 1.2.3.4
|
||||
|
||||
View/Add/Upgrade/Remove scenarios and parsers:
|
||||
- cscli list
|
||||
- cscli install collection crowdsec/linux-web
|
||||
- cscli remove scenario crowdsec/ssh_enum
|
||||
- cscli upgrade --all
|
||||
|
||||
API interaction:
|
||||
- cscli api pull
|
||||
- cscli api register
|
||||
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--debug Set logging to debug.
|
||||
--info Set logging to info.
|
||||
--warning Set logging to warning.
|
||||
--error Set logging to error.
|
||||
-h, --help help for cscli
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli api](cscli_api.md) - Crowdsec API interaction
|
||||
* [cscli backup](cscli_backup.md) - Backup or restore configuration (api, parsers, scenarios etc.) to/from directory
|
||||
* [cscli ban](cscli_ban.md) - Manage bans/mitigations
|
||||
* [cscli config](cscli_config.md) - Allows to view/edit cscli config
|
||||
* [cscli dashboard](cscli_dashboard.md) - Start a dashboard (metabase) container.
|
||||
* [cscli inspect](cscli_inspect.md) - Inspect configuration(s)
|
||||
* [cscli install](cscli_install.md) - Install configuration(s) from hub
|
||||
* [cscli list](cscli_list.md) - List enabled configs
|
||||
* [cscli metrics](cscli_metrics.md) - Display crowdsec prometheus metrics.
|
||||
* [cscli remove](cscli_remove.md) - Remove/disable configuration(s)
|
||||
* [cscli update](cscli_update.md) - Fetch available configs from hub
|
||||
* [cscli upgrade](cscli_upgrade.md) - Upgrade configuration(s)
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,49 +0,0 @@
|
|||
## cscli api
|
||||
|
||||
Crowdsec API interaction
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Allow to register your machine into crowdsec API to send and receive signal.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
|
||||
cscli api register # Register to Crowdsec API
|
||||
cscli api pull # Pull malevolant IPs from Crowdsec API
|
||||
cscli api reset # Reset your machines credentials
|
||||
cscli api enroll # Enroll your machine to the user account you created on Crowdsec backend
|
||||
cscli api credentials # Display your API credentials
|
||||
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for api
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli](cscli.md) - cscli allows you to manage crowdsec
|
||||
* [cscli api credentials](cscli_api_credentials.md) - Display api credentials
|
||||
* [cscli api enroll](cscli_api_enroll.md) - Associate your machine to an existing crowdsec user
|
||||
* [cscli api pull](cscli_api_pull.md) - Pull crowdsec API TopX
|
||||
* [cscli api register](cscli_api_register.md) - Register on Crowdsec API
|
||||
* [cscli api reset](cscli_api_reset.md) - Reset password on CrowdSec API
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,40 +0,0 @@
|
|||
## cscli api credentials
|
||||
|
||||
Display api credentials
|
||||
|
||||
### Synopsis
|
||||
|
||||
Display api credentials
|
||||
|
||||
```
|
||||
cscli api credentials [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli api credentials
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for credentials
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli api](cscli_api.md) - Crowdsec API interaction
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,41 +0,0 @@
|
|||
## cscli api enroll
|
||||
|
||||
Associate your machine to an existing crowdsec user
|
||||
|
||||
### Synopsis
|
||||
|
||||
Enrolling your machine into your user account will allow for more accurate lists and threat detection. See website to create user account.
|
||||
|
||||
```
|
||||
cscli api enroll [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli api enroll -u 1234567890ffff
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for enroll
|
||||
-u, --user string User ID (required)
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli api](cscli_api.md) - Crowdsec API interaction
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,40 +0,0 @@
|
|||
## cscli api pull
|
||||
|
||||
Pull crowdsec API TopX
|
||||
|
||||
### Synopsis
|
||||
|
||||
Pulls a list of malveolent IPs relevant to your situation and add them into the local ban database.
|
||||
|
||||
```
|
||||
cscli api pull [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli api pull
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for pull
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli api](cscli_api.md) - Crowdsec API interaction
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,41 +0,0 @@
|
|||
## cscli api register
|
||||
|
||||
Register on Crowdsec API
|
||||
|
||||
### Synopsis
|
||||
|
||||
This command will register your machine to crowdsec API to allow you to receive list of malveolent IPs.
|
||||
The printed machine_id and password should be added to your api.yaml file.
|
||||
|
||||
```
|
||||
cscli api register [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli api register
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for register
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli api](cscli_api.md) - Crowdsec API interaction
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,40 +0,0 @@
|
|||
## cscli api reset
|
||||
|
||||
Reset password on CrowdSec API
|
||||
|
||||
### Synopsis
|
||||
|
||||
Attempts to reset your credentials to the API.
|
||||
|
||||
```
|
||||
cscli api reset [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli api reset
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for reset
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli api](cscli_api.md) - Crowdsec API interaction
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,39 +0,0 @@
|
|||
## cscli backup
|
||||
|
||||
Backup or restore configuration (api, parsers, scenarios etc.) to/from directory
|
||||
|
||||
### Synopsis
|
||||
|
||||
This command is here to help you save and/or restore crowdsec configurations to simple replication
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli backup save ./my-backup
|
||||
cscli backup restore ./my-backup
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for backup
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli](cscli.md) - cscli allows you to manage crowdsec
|
||||
* [cscli backup restore](cscli_backup_restore.md) - Restore configuration (api, parsers, scenarios etc.) from directory
|
||||
* [cscli backup save](cscli_backup_save.md) - Backup configuration (api, parsers, scenarios etc.) to directory
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,49 +0,0 @@
|
|||
## cscli backup restore
|
||||
|
||||
Restore configuration (api, parsers, scenarios etc.) from directory
|
||||
|
||||
### Synopsis
|
||||
|
||||
restore command will try to restore all saved information from <directory> to yor local setup, including :
|
||||
|
||||
- Installation of up-to-date scenarios/parsers/... via cscli
|
||||
|
||||
- Restauration of tainted/local/out-of-date scenarios/parsers/... file
|
||||
|
||||
- Restauration of API credentials (if the existing ones aren't working)
|
||||
|
||||
- Restauration of acqusition configuration
|
||||
|
||||
|
||||
```
|
||||
cscli backup restore <directory> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli backup restore ./my-backup
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for restore
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli backup](cscli_backup.md) - Backup or restore configuration (api, parsers, scenarios etc.) to/from directory
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,50 +0,0 @@
|
|||
## cscli backup save
|
||||
|
||||
Backup configuration (api, parsers, scenarios etc.) to directory
|
||||
|
||||
### Synopsis
|
||||
|
||||
backup command will try to save all relevant informations to crowdsec config, including :
|
||||
|
||||
- List of scenarios, parsers, postoverflows and collections that are up-to-date
|
||||
|
||||
- Actual backup of tainted/local/out-of-date scenarios, parsers, postoverflows and collections
|
||||
|
||||
- Backup of API credentials
|
||||
|
||||
- Backup of acqusition configuration
|
||||
|
||||
|
||||
|
||||
```
|
||||
cscli backup save <directory> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli backup save ./my-backup
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for save
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli backup](cscli_backup.md) - Backup or restore configuration (api, parsers, scenarios etc.) to/from directory
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,38 +0,0 @@
|
|||
## cscli ban
|
||||
|
||||
Manage bans/mitigations
|
||||
|
||||
### Synopsis
|
||||
|
||||
This is the main interaction point with local ban database for humans.
|
||||
|
||||
You can add/delete/list or flush current bans in your local ban DB.
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
--db string Set path to SQLite DB.
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
-h, --help help for ban
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli](cscli.md) - cscli allows you to manage crowdsec
|
||||
* [cscli ban add](cscli_ban_add.md) - Adds a ban against a given ip/range for the provided duration
|
||||
* [cscli ban del](cscli_ban_del.md) - Delete bans from db
|
||||
* [cscli ban flush](cscli_ban_flush.md) - Fush ban DB
|
||||
* [cscli ban list](cscli_ban_list.md) - List local or api bans/remediations
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,46 +0,0 @@
|
|||
## cscli ban add
|
||||
|
||||
Adds a ban against a given ip/range for the provided duration
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Allows to add a ban against a specific ip or range target for a specific duration.
|
||||
|
||||
The duration argument can be expressed in seconds(s), minutes(m) or hours (h).
|
||||
|
||||
See [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) for more informations.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli ban add ip 1.2.3.4 24h "scan"
|
||||
cscli ban add range 1.2.3.0/24 24h "the whole range"
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for add
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban](cscli_ban.md) - Manage bans/mitigations
|
||||
* [cscli ban add ip](cscli_ban_add_ip.md) - Adds the specific ip to the ban db
|
||||
* [cscli ban add range](cscli_ban_add_range.md) - Adds the specific ip to the ban db
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,42 +0,0 @@
|
|||
## cscli ban add ip
|
||||
|
||||
Adds the specific ip to the ban db
|
||||
|
||||
### Synopsis
|
||||
|
||||
Duration must be [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration), expressed in s/m/h.
|
||||
|
||||
```
|
||||
cscli ban add ip <target> <duration> <reason> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli ban add ip 1.2.3.4 12h "the scan"
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for ip
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban add](cscli_ban_add.md) - Adds a ban against a given ip/range for the provided duration
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,42 +0,0 @@
|
|||
## cscli ban add range
|
||||
|
||||
Adds the specific ip to the ban db
|
||||
|
||||
### Synopsis
|
||||
|
||||
Duration must be [time.ParseDuration](https://golang.org/pkg/time/#ParseDuration) compatible, expressed in s/m/h.
|
||||
|
||||
```
|
||||
cscli ban add range <target> <duration> <reason> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli ban add range 1.2.3.0/24 12h "the whole range"
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for range
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban add](cscli_ban_add.md) - Adds a ban against a given ip/range for the provided duration
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,41 +0,0 @@
|
|||
## cscli ban del
|
||||
|
||||
Delete bans from db
|
||||
|
||||
### Synopsis
|
||||
|
||||
The removal of the bans can be applied on a single IP address or directly on a IP range.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli ban del ip 1.2.3.4
|
||||
cscli ban del range 1.2.3.0/24
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for del
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban](cscli_ban.md) - Manage bans/mitigations
|
||||
* [cscli ban del ip](cscli_ban_del_ip.md) - Delete bans for given ip from db
|
||||
* [cscli ban del range](cscli_ban_del_range.md) - Delete bans for given ip from db
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,42 +0,0 @@
|
|||
## cscli ban del ip
|
||||
|
||||
Delete bans for given ip from db
|
||||
|
||||
### Synopsis
|
||||
|
||||
Delete bans for given ip from db
|
||||
|
||||
```
|
||||
cscli ban del ip <target> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli ban del ip 1.2.3.4
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for ip
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban del](cscli_ban_del.md) - Delete bans from db
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,42 +0,0 @@
|
|||
## cscli ban del range
|
||||
|
||||
Delete bans for given ip from db
|
||||
|
||||
### Synopsis
|
||||
|
||||
Delete bans for given ip from db
|
||||
|
||||
```
|
||||
cscli ban del range <target> [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli ban del range 1.2.3.0/24
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for range
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban del](cscli_ban_del.md) - Delete bans from db
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,42 +0,0 @@
|
|||
## cscli ban flush
|
||||
|
||||
Fush ban DB
|
||||
|
||||
### Synopsis
|
||||
|
||||
Fush ban DB
|
||||
|
||||
```
|
||||
cscli ban flush [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli ban flush
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for flush
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban](cscli_ban.md) - Manage bans/mitigations
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,50 +0,0 @@
|
|||
## cscli ban list
|
||||
|
||||
List local or api bans/remediations
|
||||
|
||||
### Synopsis
|
||||
|
||||
List the bans, by default only local decisions.
|
||||
|
||||
If --all/-a is specified, api-provided bans will be displayed too.
|
||||
|
||||
Time can be specified with --at and support a variety of date formats:
|
||||
- Jan 2 15:04:05
|
||||
- Mon Jan 02 15:04:05.000000 2006
|
||||
- 2006-01-02T15:04:05Z07:00
|
||||
- 2006/01/02
|
||||
- 2006/01/02 15:04
|
||||
- 2006-01-02
|
||||
- 2006-01-02 15:04
|
||||
|
||||
|
||||
```
|
||||
cscli ban list [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-a, --all List as well bans received from API
|
||||
--at string List bans at given time
|
||||
-h, --help help for list
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--db string Set path to SQLite DB.
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--remediation string Set specific remediation type : ban|slow|captcha (default "ban")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli ban](cscli_ban.md) - Manage bans/mitigations
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,42 +0,0 @@
|
|||
## cscli config
|
||||
|
||||
Allows to view/edit cscli config
|
||||
|
||||
### Synopsis
|
||||
|
||||
Allow to configure sqlite path and installation directory.
|
||||
If no commands are specified, config is in interactive mode.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
- cscli config show
|
||||
- cscli config prompt
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for config
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli](cscli.md) - cscli allows you to manage crowdsec
|
||||
* [cscli config backend](cscli_config_backend.md) - Configure installation directory
|
||||
* [cscli config installdir](cscli_config_installdir.md) - Configure installation directory
|
||||
* [cscli config prompt](cscli_config_prompt.md) - Prompt for configuration values in an interactive fashion
|
||||
* [cscli config show](cscli_config_show.md) - Displays current config
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,34 +0,0 @@
|
|||
## cscli config backend
|
||||
|
||||
Configure installation directory
|
||||
|
||||
### Synopsis
|
||||
|
||||
Configure the backend plugin directory of crowdsec, such as /etc/crowdsec/plugins/backend
|
||||
|
||||
```
|
||||
cscli config backend [value] [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for backend
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli config](cscli_config.md) - Allows to view/edit cscli config
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,34 +0,0 @@
|
|||
## cscli config installdir
|
||||
|
||||
Configure installation directory
|
||||
|
||||
### Synopsis
|
||||
|
||||
Configure the installation directory of crowdsec, such as /etc/crowdsec/config/
|
||||
|
||||
```
|
||||
cscli config installdir [value] [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for installdir
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli config](cscli_config.md) - Allows to view/edit cscli config
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,34 +0,0 @@
|
|||
## cscli config prompt
|
||||
|
||||
Prompt for configuration values in an interactive fashion
|
||||
|
||||
### Synopsis
|
||||
|
||||
Start interactive configuration of cli. It will successively ask for install dir, db path.
|
||||
|
||||
```
|
||||
cscli config prompt [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for prompt
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli config](cscli_config.md) - Allows to view/edit cscli config
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,34 +0,0 @@
|
|||
## cscli config show
|
||||
|
||||
Displays current config
|
||||
|
||||
### Synopsis
|
||||
|
||||
Displays the current cli configuration.
|
||||
|
||||
```
|
||||
cscli config show [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for show
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli config](cscli_config.md) - Allows to view/edit cscli config
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,42 +0,0 @@
|
|||
## cscli dashboard
|
||||
|
||||
Start a dashboard (metabase) container.
|
||||
|
||||
### Synopsis
|
||||
|
||||
Start a metabase container exposing dashboards and metrics.
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli dashboard setup
|
||||
cscli dashboard start
|
||||
cscli dashboard stop
|
||||
cscli dashboard setup --force
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for dashboard
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli](cscli.md) - cscli allows you to manage crowdsec
|
||||
* [cscli dashboard setup](cscli_dashboard_setup.md) - Setup a metabase container.
|
||||
* [cscli dashboard start](cscli_dashboard_start.md) - Start the metabase container.
|
||||
* [cscli dashboard stop](cscli_dashboard_stop.md) - Stops the metabase container.
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,47 +0,0 @@
|
|||
## cscli dashboard setup
|
||||
|
||||
Setup a metabase container.
|
||||
|
||||
### Synopsis
|
||||
|
||||
Perform a metabase docker setup, download standard dashboards, create a fresh user and start the container
|
||||
|
||||
```
|
||||
cscli dashboard setup [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli dashboard setup
|
||||
cscli dashboard setup --force
|
||||
cscli dashboard setup -l 0.0.0.0 -p 443
|
||||
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-d, --dir string Shared directory with metabase container. (default "/var/lib/crowdsec/data")
|
||||
-f, --force Force setup : override existing files.
|
||||
-h, --help help for setup
|
||||
-l, --listen string Listen address of container (default "127.0.0.1")
|
||||
-p, --port string Listen port of container (default "3000")
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli dashboard](cscli_dashboard.md) - Start a dashboard (metabase) container.
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,34 +0,0 @@
|
|||
## cscli dashboard start
|
||||
|
||||
Start the metabase container.
|
||||
|
||||
### Synopsis
|
||||
|
||||
Stats the metabase container using docker.
|
||||
|
||||
```
|
||||
cscli dashboard start [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for start
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli dashboard](cscli_dashboard.md) - Start a dashboard (metabase) container.
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,35 +0,0 @@
|
|||
## cscli dashboard stop
|
||||
|
||||
Stops the metabase container.
|
||||
|
||||
### Synopsis
|
||||
|
||||
Stops the metabase container using docker.
|
||||
|
||||
```
|
||||
cscli dashboard stop [flags]
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for stop
|
||||
-r, --remove remove (docker rm) container as well.
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli dashboard](cscli_dashboard.md) - Start a dashboard (metabase) container.
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,47 +0,0 @@
|
|||
## cscli inspect
|
||||
|
||||
Inspect configuration(s)
|
||||
|
||||
### Synopsis
|
||||
|
||||
|
||||
Inspect give you full detail about local installed configuration.
|
||||
|
||||
[type] must be parser, scenario, postoverflow, collection.
|
||||
|
||||
[config_name] must be a valid config name from [Crowdsec Hub](https://hub.crowdsec.net) or locally installed.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli inspect parser crowdsec/xxx
|
||||
cscli inspect collection crowdsec/xxx
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for inspect
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli](cscli.md) - cscli allows you to manage crowdsec
|
||||
* [cscli inspect collection](cscli_inspect_collection.md) - Inspect given collection
|
||||
* [cscli inspect parser](cscli_inspect_parser.md) - Inspect given log parser
|
||||
* [cscli inspect postoverflow](cscli_inspect_postoverflow.md) - Inspect given postoverflow parser
|
||||
* [cscli inspect scenario](cscli_inspect_scenario.md) - Inspect given scenario
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
|
@ -1,40 +0,0 @@
|
|||
## cscli inspect collection
|
||||
|
||||
Inspect given collection
|
||||
|
||||
### Synopsis
|
||||
|
||||
Inspect given collection from hub
|
||||
|
||||
```
|
||||
cscli inspect collection [config] [flags]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```
|
||||
cscli inspect collection crowdsec/xxx
|
||||
```
|
||||
|
||||
### Options
|
||||
|
||||
```
|
||||
-h, --help help for collection
|
||||
```
|
||||
|
||||
### Options inherited from parent commands
|
||||
|
||||
```
|
||||
-c, --config-dir string Configuration directory to use. (default "/etc/crowdsec/cscli/")
|
||||
--debug Set logging to debug.
|
||||
--error Set logging to error.
|
||||
--info Set logging to info.
|
||||
-o, --output string Output format : human, json, raw. (default "human")
|
||||
--warning Set logging to warning.
|
||||
```
|
||||
|
||||
### SEE ALSO
|
||||
|
||||
* [cscli inspect](cscli_inspect.md) - Inspect configuration(s)
|
||||
|
||||
###### Auto generated by spf13/cobra on 15-May-2020
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue