mirror of
https://github.com/Security-Onion-Solutions/securityonion.git
synced 2026-10-24 15:08:10 +02:00
Compare commits
863
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a24fe8b61 | ||
|
|
fed0c5739d | ||
|
|
d352bc0384 | ||
|
|
d1114a0dae | ||
|
|
f4b301d71c | ||
|
|
f7dbfba178 | ||
|
|
0a628bb7e7 | ||
|
|
bd6647e775 | ||
|
|
da2c19188a | ||
|
|
90b3d37be6 | ||
|
|
fcd2f67076 | ||
|
|
a9cdd17694 | ||
|
|
b32aaac290 | ||
|
|
1aee3f28dc | ||
|
|
678cb0d5b2 | ||
|
|
31c5190a1f | ||
|
|
4ce7a06abe | ||
|
|
ba95b9bbc2 | ||
|
|
43475452b3 | ||
|
|
99322cf26a | ||
|
|
117548757f | ||
|
|
22bda63847 | ||
|
|
2a4611df45 | ||
|
|
89f8bcd19f | ||
|
|
563269cbac | ||
|
|
523c39d4f2 | ||
|
|
b4557e973c | ||
|
|
0f53a7e0bc | ||
|
|
8de8ba811a | ||
|
|
d122ee7fea | ||
|
|
9732e1c639 | ||
|
|
53f9ebcd46 | ||
|
|
47d74f1ae1 | ||
|
|
855716846a | ||
|
|
8e35d70595 | ||
|
|
e4625cfcae | ||
|
|
eb803dce0e | ||
|
|
a06f08217a | ||
|
|
29d27cf255 | ||
|
|
235a60e587 | ||
|
|
a8f7c46b0d | ||
|
|
26d895ccb7 | ||
|
|
b43efc458f | ||
|
|
21222ff119 | ||
|
|
88fa7e7fb4 | ||
|
|
efe0581892 | ||
|
|
72f60fcaa9 | ||
|
|
2684a5ca95 | ||
|
|
bcee63bde5 | ||
|
|
6ce89eb323 | ||
|
|
ebab4b0d90 | ||
|
|
db60c27da2 | ||
|
|
f4defdfde0 | ||
|
|
36652e8f23 | ||
|
|
7bef194540 | ||
|
|
e2bf2837fe | ||
|
|
06704dad22 | ||
|
|
47fe0758d0 | ||
|
|
b71fd93f9d | ||
|
|
d9eff9aa9e | ||
|
|
d8884dbd99 | ||
|
|
6c0d4c15e8 | ||
|
|
2e2f62f265 | ||
|
|
b7a11a525c | ||
|
|
8eef95ea3e | ||
|
|
65e261475d | ||
|
|
bbc28c88b7 | ||
|
|
edaacf79a7 | ||
|
|
ecc643cd33 | ||
|
|
08aaf7948e | ||
|
|
bb57545d08 | ||
|
|
0f7adbbecc | ||
|
|
aeb4fe8f50 | ||
|
|
f3aa39c5a4 | ||
|
|
24077ba974 | ||
|
|
b3567405f9 | ||
|
|
1fc5bb7afa | ||
|
|
1f1d3ded41 | ||
|
|
1e86be11b2 | ||
|
|
f4518e2620 | ||
|
|
a9f7ffc3fe | ||
|
|
b018277d68 | ||
|
|
3be603e203 | ||
|
|
84cd966736 | ||
|
|
fee401a912 | ||
|
|
496b61966f | ||
|
|
52037314be | ||
|
|
9c12c10f96 | ||
|
|
9fc9be2cc9 | ||
|
|
7245843a3c | ||
|
|
a1d17417ea | ||
|
|
bee03d5bae | ||
|
|
56e3e44d04 | ||
|
|
32d1274b80 | ||
|
|
1624e8c094 | ||
|
|
3f3f091a7f | ||
|
|
cb48909578 | ||
|
|
3057775770 | ||
|
|
e4e8b90b9c | ||
|
|
223ace6ff3 | ||
|
|
66e7863336 | ||
|
|
8f253d17a6 | ||
|
|
9652a2053b | ||
|
|
191ee159ef | ||
|
|
a8bfe955a5 | ||
|
|
bd354abe83 | ||
|
|
37782fb45c | ||
|
|
cf3a4ebc27 | ||
|
|
c49008a413 | ||
|
|
fcbea1a1c4 | ||
|
|
6736f9c3a0 | ||
|
|
8f14e96215 | ||
|
|
86ae51b2b5 | ||
|
|
2cde9abec2 | ||
|
|
57ce2cea0d | ||
|
|
ff7555d93b | ||
|
|
65b84026a8 | ||
|
|
721b1d6207 | ||
|
|
a74046fc24 | ||
|
|
ea539f8679 | ||
|
|
54bb0e2c12 | ||
|
|
67b4d82f62 | ||
|
|
30574fdbb9 | ||
|
|
64771e9e1d | ||
|
|
01ca33b90a | ||
|
|
599d19215b | ||
|
|
ee671e7ec9 | ||
|
|
42d429a11e | ||
|
|
332a5d11bc | ||
|
|
42a62c90a5 | ||
|
|
fb7e065590 | ||
|
|
bce6b0c1fe | ||
|
|
b9ba7df80c | ||
|
|
97fddc0719 | ||
|
|
a5deee1444 | ||
|
|
3585ccca79 | ||
|
|
dd035beec4 | ||
|
|
0dbb7803ef | ||
|
|
30deb00277 | ||
|
|
192363bc2f | ||
|
|
3d8f86883a | ||
|
|
96bef89ba9 | ||
|
|
a244640539 | ||
|
|
fdb975fdef | ||
|
|
f8401bef37 | ||
|
|
ca96a15091 | ||
|
|
1bac9a218e | ||
|
|
4786d359fb | ||
|
|
d771fbc444 | ||
|
|
85ab4c69e5 | ||
|
|
cb8e576d6b | ||
|
|
fae1754fec | ||
|
|
d33eb70af6 | ||
|
|
f45dcfdf73 | ||
|
|
62da505ea7 | ||
|
|
7e5b6f276f | ||
|
|
376d29e376 | ||
|
|
665772adb8 | ||
|
|
094b4d5e86 | ||
|
|
3a3667996c | ||
|
|
dfa6f0b454 | ||
|
|
9f6679c043 | ||
|
|
fb7d162de1 | ||
|
|
0ee8aa8079 | ||
|
|
a8785870af | ||
|
|
00f948e4d2 | ||
|
|
f6ab92fc24 | ||
|
|
5e9fd4a45b | ||
|
|
e7f54b49c4 | ||
|
|
a127ef5714 | ||
|
|
fcb889a30c | ||
|
|
99e1d83358 | ||
|
|
60052e0910 | ||
|
|
99c3c7f8aa | ||
|
|
cef1dcfcee | ||
|
|
cec3f7ed57 | ||
|
|
f566a8965d | ||
|
|
905cc1c0dd | ||
|
|
12744353fb | ||
|
|
52fc0cb828 | ||
|
|
247d9cdb34 | ||
|
|
088b761190 | ||
|
|
5c3a69d742 | ||
|
|
c1f256e630 | ||
|
|
2dcc81ea7d | ||
|
|
356da00395 | ||
|
|
d2ff29b7a8 | ||
|
|
7bdaf9338e | ||
|
|
35f545a858 | ||
|
|
dff3d76efd | ||
|
|
6f3f58bd70 | ||
|
|
d62c53fc92 | ||
|
|
2f2187f714 | ||
|
|
6c37bc1f9b | ||
|
|
c9a041ddb4 | ||
|
|
de3306e73c | ||
|
|
4b74e2c320 | ||
|
|
3744c0bd6c | ||
|
|
563b9d7c3b | ||
|
|
ec91f9b830 | ||
|
|
7f3f99880f | ||
|
|
3e7f508620 | ||
|
|
c4555a5514 | ||
|
|
d4d63fa60a | ||
|
|
dcb931b97c | ||
|
|
8e6b16bde0 | ||
|
|
63692aa1a0 | ||
|
|
2663ca87a2 | ||
|
|
a337a3e4f6 | ||
|
|
ea502e29d0 | ||
|
|
af222eed08 | ||
|
|
83e55ab0f3 | ||
|
|
ff82cc32a0 | ||
|
|
721d6483f0 | ||
|
|
a88562a348 | ||
|
|
64d7383233 | ||
|
|
7400e3dffa | ||
|
|
792b801086 | ||
|
|
3991e485c0 | ||
|
|
a87a910585 | ||
|
|
ba0dd38f4e | ||
|
|
b3467854a8 | ||
|
|
9ebf93cc26 | ||
|
|
d69234146e | ||
|
|
706d46b395 | ||
|
|
539389c78e | ||
|
|
65e81d3b3a | ||
|
|
546462c77f | ||
|
|
a7ddb7a975 | ||
|
|
fe4f7ad2f7 | ||
|
|
ee1d2167e8 | ||
|
|
2d0ea48c39 | ||
|
|
23d92316c1 | ||
|
|
668ab447a2 | ||
|
|
e5346af068 | ||
|
|
9762523849 | ||
|
|
e8ab6433ab | ||
|
|
9c20ef60f4 | ||
|
|
a2a4d9314d | ||
|
|
6abf382ea8 | ||
|
|
5d36d00dec | ||
|
|
f1f672892e | ||
|
|
a8053e2c9d | ||
|
|
36833fdad1 | ||
|
|
a92d10a1e3 | ||
|
|
d3da6b3939 | ||
|
|
e998a21b4d | ||
|
|
d77760c268 | ||
|
|
4f7ad76d5b | ||
|
|
41dba204c3 | ||
|
|
12bb16e89c | ||
|
|
45f1a1b8b1 | ||
|
|
23a9daf7a2 | ||
|
|
d3c6fdce7e | ||
|
|
c9642489d3 | ||
|
|
7b080797b4 | ||
|
|
d94c16eea1 | ||
|
|
3e3d409c42 | ||
|
|
a45ca12076 | ||
|
|
2e141a1ad7 | ||
|
|
4de8f0208f | ||
|
|
4aabf7d638 | ||
|
|
812310088e | ||
|
|
4e17390cfa | ||
|
|
57d629683d | ||
|
|
3666b5b0de | ||
|
|
7f64f143d7 | ||
|
|
162c66a705 | ||
|
|
14d11cc180 | ||
|
|
112fcf7804 | ||
|
|
120b426a79 | ||
|
|
72fd754a92 | ||
|
|
d5fddafa6a | ||
|
|
0c83d4e1fe | ||
|
|
baca444a7e | ||
|
|
4f5af93b38 | ||
|
|
b109ca4e9b | ||
|
|
e5969a12aa | ||
|
|
4d97b562eb | ||
|
|
f2d81cea3f | ||
|
|
6bd3c414bb | ||
|
|
16f958dac0 | ||
|
|
a57ff5f89e | ||
|
|
e2513daddc | ||
|
|
01a873b2d9 | ||
|
|
19957d9530 | ||
|
|
e4c14a9294 | ||
|
|
445ae58919 | ||
|
|
382dee1d06 | ||
|
|
f4aa9932ff | ||
|
|
4871098278 | ||
|
|
7b32c73da8 | ||
|
|
334978ad92 | ||
|
|
a21186ccce | ||
|
|
f77fad8087 | ||
|
|
963e475d1a | ||
|
|
ac46636196 | ||
|
|
f3d8bae13d | ||
|
|
894d323323 | ||
|
|
26eb8c3c18 | ||
|
|
4e1935f8a0 | ||
|
|
c4c1464b2a | ||
|
|
8d168e9661 | ||
|
|
f542d1e7ce | ||
|
|
b104f0955a | ||
|
|
89f4950521 | ||
|
|
387781c629 | ||
|
|
011749ad09 | ||
|
|
ad78e84ccd | ||
|
|
c4295b4e0a | ||
|
|
c2075ddafb | ||
|
|
4886034fef | ||
|
|
48a7d66964 | ||
|
|
6876b25280 | ||
|
|
30f3bddb8b | ||
|
|
811b799b0b | ||
|
|
aaea6dbd58 | ||
|
|
8095b82841 | ||
|
|
141116f550 | ||
|
|
f6d3cbe08d | ||
|
|
9e7e6edae0 | ||
|
|
6f61e7c901 | ||
|
|
cc2bfc26e2 | ||
|
|
073e32520b | ||
|
|
5867b50720 | ||
|
|
8a16ead33d | ||
|
|
f9b154ccef | ||
|
|
3503d0c33d | ||
|
|
517538a9a7 | ||
|
|
23c74f1727 | ||
|
|
b76f9d022e | ||
|
|
02318f065c | ||
|
|
f958212bea | ||
|
|
376607d292 | ||
|
|
186bf86e99 | ||
|
|
bd70dd53fb | ||
|
|
be7d8a2aa7 | ||
|
|
5178d5fd0e | ||
|
|
fee62ab976 | ||
|
|
618712469e | ||
|
|
8b488f9226 | ||
|
|
1657480d31 | ||
|
|
63d4061500 | ||
|
|
405dc52587 | ||
|
|
e42f7cd6fc | ||
|
|
8167ae3282 | ||
|
|
2cd889782d | ||
|
|
87a5639643 | ||
|
|
ed533efb7b | ||
|
|
5af6c56996 | ||
|
|
99e9fc1c3b | ||
|
|
e5de499bcc | ||
|
|
7d17784e96 | ||
|
|
f0bbbf37d8 | ||
|
|
6fc0fd954c | ||
|
|
566f90a0c0 | ||
|
|
89e6a746c8 | ||
|
|
52885e28c5 | ||
|
|
fbeac25ee9 | ||
|
|
8a3f5d0f81 | ||
|
|
4e856f02da | ||
|
|
f6a2758321 | ||
|
|
0b078c4804 | ||
|
|
2959dc9564 | ||
|
|
8b0759866e | ||
|
|
6fa0d327cb | ||
|
|
9a71f64a35 | ||
|
|
40c02b3149 | ||
|
|
5fd5df54b4 | ||
|
|
3394e9aab7 | ||
|
|
3766f74102 | ||
|
|
c04a30785f | ||
|
|
ca4d22a5fe | ||
|
|
ea199aee55 | ||
|
|
5a57bbe4de | ||
|
|
1f44e98681 | ||
|
|
9a313d1966 | ||
|
|
85d7f6bebc | ||
|
|
2a4a7307f7 | ||
|
|
f8de176f4b | ||
|
|
dffe0d3780 | ||
|
|
d131d167de | ||
|
|
8a8f2c4a33 | ||
|
|
7f6014096b | ||
|
|
70af3cec53 | ||
|
|
57b7d59387 | ||
|
|
ef83450107 | ||
|
|
032d792331 | ||
|
|
66a1141b84 | ||
|
|
0cac761edc | ||
|
|
db91ce981d | ||
|
|
bd8e5a63db | ||
|
|
18212cad0d | ||
|
|
9975d36b4f | ||
|
|
8e9e221196 | ||
|
|
1fe7726aff | ||
|
|
83cf1f0793 | ||
|
|
3310e19ee4 | ||
|
|
07d6b2cfdd | ||
|
|
89afea876a | ||
|
|
1243a25bd3 | ||
|
|
8675296393 | ||
|
|
23f04e2866 | ||
|
|
76f6947f36 | ||
|
|
92a55386c6 | ||
|
|
e7352eb841 | ||
|
|
795aa898a3 | ||
|
|
69d77382f1 | ||
|
|
dc9b4f3ce5 | ||
|
|
87b9276c79 | ||
|
|
99118f9bed | ||
|
|
24b75b4a2b | ||
|
|
395bd627f1 | ||
|
|
868b217549 | ||
|
|
c33db9d00f | ||
|
|
e88eb65a44 | ||
|
|
dc8c80633b | ||
|
|
f441d98e71 | ||
|
|
895aa18486 | ||
|
|
2a6cc58306 | ||
|
|
ee36f5f84c | ||
|
|
9217670bab | ||
|
|
a3f586cf88 | ||
|
|
670d2b2757 | ||
|
|
3b8459c6ec | ||
|
|
52574e21c6 | ||
|
|
a330bea25e | ||
|
|
33c24cd136 | ||
|
|
12f4447875 | ||
|
|
576c7bfedd | ||
|
|
b3b7ecdded | ||
|
|
0af020b6c3 | ||
|
|
339a5af4a3 | ||
|
|
da94788255 | ||
|
|
7952c274c4 | ||
|
|
67a9abadf2 | ||
|
|
94f31e1356 | ||
|
|
435e2b4182 | ||
|
|
d0edfd2131 | ||
|
|
13ebde61bd | ||
|
|
fa2ae1b87f | ||
|
|
5bf9751adf | ||
|
|
3effdbc91e | ||
|
|
30312b93a6 | ||
|
|
a9c03e39bb | ||
|
|
8836529496 | ||
|
|
b09c3776b7 | ||
|
|
dfdb1fbaeb | ||
|
|
4d34470b84 | ||
|
|
b0b022c3ad | ||
|
|
27c1c35e62 | ||
|
|
f45631af3a | ||
|
|
61aa963a2d | ||
|
|
81c8d54589 | ||
|
|
4f3b57f495 | ||
|
|
84228a819b | ||
|
|
81ebea0451 | ||
|
|
8e2753aeb8 | ||
|
|
698a746d6d | ||
|
|
d71e80cf66 | ||
|
|
a9f9d8bd0d | ||
|
|
953fdee3af | ||
|
|
e2e3e690ca | ||
|
|
323491f58e | ||
|
|
96fcc0ec38 | ||
|
|
bcc60a4ae0 | ||
|
|
b77103aa9f | ||
|
|
63a2e20698 | ||
|
|
22d5c96bd5 | ||
|
|
28fdd1eb6f | ||
|
|
d0bea2ebcb | ||
|
|
62c01a9756 | ||
|
|
b143e1e577 | ||
|
|
16149df71f | ||
|
|
6a18f35020 | ||
|
|
aa58225e8f | ||
|
|
8e33d0e1e9 | ||
|
|
acf48db915 | ||
|
|
3daed551df | ||
|
|
4456bde1c8 | ||
|
|
4a6c675223 | ||
|
|
a769d4c680 | ||
|
|
f68e3e47a1 | ||
|
|
b81257bf45 | ||
|
|
1a423a2434 | ||
|
|
95cae4c734 | ||
|
|
596471e140 | ||
|
|
d10f21399c | ||
|
|
ae1ddf3817 | ||
|
|
ea73216f4e | ||
|
|
1ee555957a | ||
|
|
43f72c1f9f | ||
|
|
9031c1fd22 | ||
|
|
ae6a705ce1 | ||
|
|
80c39d612c | ||
|
|
c505160480 | ||
|
|
d9f6cde4e1 | ||
|
|
b1273573ed | ||
|
|
6c42c419e2 | ||
|
|
f03f0155f4 | ||
|
|
f23652397c | ||
|
|
07d3b148b5 | ||
|
|
780d9faf0d | ||
|
|
d2fe51d5fe | ||
|
|
0cc94980af | ||
|
|
b8bf684077 | ||
|
|
f083db67e4 | ||
|
|
4741cc92bd | ||
|
|
46655860e9 | ||
|
|
289ddda5e8 | ||
|
|
83aaa76f98 | ||
|
|
f905afbc6f | ||
|
|
bd5e77afc5 | ||
|
|
944e773759 | ||
|
|
33a116357d | ||
|
|
cf456dc58c | ||
|
|
9aa9ea3255 | ||
|
|
3ba96da3b7 | ||
|
|
f0712bd780 | ||
|
|
448668a72e | ||
|
|
f088a27159 | ||
|
|
9f5a9616a5 | ||
|
|
27c7702325 | ||
|
|
8c306eb37d | ||
|
|
e536ffa363 | ||
|
|
eb82f9ea9d | ||
|
|
d7aa7ab228 | ||
|
|
fe0b68d24c | ||
|
|
6ad345730b | ||
|
|
9580976ba2 | ||
|
|
ac907ba45f | ||
|
|
f957954abf | ||
|
|
cb3631da81 | ||
|
|
f5d63f585e | ||
|
|
13f8be40b5 | ||
|
|
9ee90a5bc0 | ||
|
|
ca85c5d900 | ||
|
|
2d653b6f1b | ||
|
|
34fee25b0c | ||
|
|
1d3d98f759 | ||
|
|
a767c79641 | ||
|
|
61e72c89e4 | ||
|
|
d9fb7313f9 | ||
|
|
7ca2313255 | ||
|
|
534f0e639d | ||
|
|
559465b407 | ||
|
|
f9c2579261 | ||
|
|
33699a914b | ||
|
|
0c2d8f8973 | ||
|
|
8c17ae0f66 | ||
|
|
f2996fb888 | ||
|
|
3c533cccbc | ||
|
|
79da9f9f2c | ||
|
|
99a027589b | ||
|
|
68a82a425b | ||
|
|
f54939b444 | ||
|
|
d86a3c5cc9 | ||
|
|
86edc5aaba | ||
|
|
d48a22e37e | ||
|
|
9a70a06b3b | ||
|
|
526d739b3b | ||
|
|
68d783e760 | ||
|
|
1e9b6b0975 | ||
|
|
2131e7d450 | ||
|
|
2a2d853ac4 | ||
|
|
5abd6de4b5 | ||
|
|
bb8ae91d91 | ||
|
|
93ffce98d7 | ||
|
|
5599cce22c | ||
|
|
b2a82fec29 | ||
|
|
613eca52fc | ||
|
|
79987f3659 | ||
|
|
bf609a112e | ||
|
|
0b4a4de609 | ||
|
|
ad376d2a43 | ||
|
|
0834998cca | ||
|
|
473f93f0ee | ||
|
|
16055c4d88 | ||
|
|
6393d08e86 | ||
|
|
7cc2e045fb | ||
|
|
6955ee73bf | ||
|
|
c0272ddb81 | ||
|
|
d72219c586 | ||
|
|
ffd34d4e0e | ||
|
|
aa78978740 | ||
|
|
75d4f5e496 | ||
|
|
89a28d2cfe | ||
|
|
c1d187599b | ||
|
|
d87313db27 | ||
|
|
141a61f5b5 | ||
|
|
901cbf03e4 | ||
|
|
b485be4602 | ||
|
|
7d13007aa9 | ||
|
|
d7a1b67095 | ||
|
|
6c8997b28a | ||
|
|
52791204e4 | ||
|
|
58f1d08ebe | ||
|
|
d0aa33a255 | ||
|
|
730c828bec | ||
|
|
74b50f6009 | ||
|
|
e89c820b65 | ||
|
|
9ac05a6ad1 | ||
|
|
24ee3318bc | ||
|
|
ce566ba174 | ||
|
|
2635a60a8c | ||
|
|
244a73b7a2 | ||
|
|
e45ad45d73 | ||
|
|
1189621ec5 | ||
|
|
d2524a593f | ||
|
|
f2ab2354fd | ||
|
|
64731c73ba | ||
|
|
024fece607 | ||
|
|
249b126312 | ||
|
|
8e38bff0c3 | ||
|
|
b9f2d56932 | ||
|
|
03fa01a705 | ||
|
|
450eacca41 | ||
|
|
b7a13899f7 | ||
|
|
6f273d7d97 | ||
|
|
fabecb8288 | ||
|
|
907f699721 | ||
|
|
e7a7047f71 | ||
|
|
b4e5171415 | ||
|
|
b328820c01 | ||
|
|
936295f1c4 | ||
|
|
61ca60a94c | ||
|
|
638aca97c8 | ||
|
|
74a5c895e8 | ||
|
|
84decc1db6 | ||
|
|
d56bf01823 | ||
|
|
d29267d9c2 | ||
|
|
72327285b2 | ||
|
|
cc7a237457 | ||
|
|
b068ad2b35 | ||
|
|
b103f412b5 | ||
|
|
ef79c63858 | ||
|
|
01fb1aa156 | ||
|
|
f19bdd7aae | ||
|
|
f637dc62d1 | ||
|
|
081f6fa1fb | ||
|
|
d6d90d84cd | ||
|
|
125610ed42 | ||
|
|
306b0af4d0 | ||
|
|
492ae80da7 | ||
|
|
4a2177c827 | ||
|
|
006ac31109 | ||
|
|
7d4d6a0756 | ||
|
|
66c0a662fc | ||
|
|
49a643fff4 | ||
|
|
e1d830da76 | ||
|
|
778cc055ea | ||
|
|
e847c46129 | ||
|
|
499f7102bd | ||
|
|
932deab751 | ||
|
|
1281f0ee37 | ||
|
|
4bc19f91ce | ||
|
|
f774334b6c | ||
|
|
4990d0ddea | ||
|
|
3e49322220 | ||
|
|
ecb92d43fc | ||
|
|
3b714db0bf | ||
|
|
f17da4e68b | ||
|
|
04cfc22e3f | ||
|
|
dceed421ae | ||
|
|
652ac5d61f | ||
|
|
f888a2ba6b | ||
|
|
8a1ee02335 | ||
|
|
192f6cfe13 | ||
|
|
5bca81d833 | ||
|
|
1c6574c694 | ||
|
|
b701664e04 | ||
|
|
bc64f1431d | ||
|
|
2203037ce7 | ||
|
|
77a4ad877e | ||
|
|
702b3585cc | ||
|
|
86966d2778 | ||
|
|
7fcace34c4 | ||
|
|
9541024eb7 | ||
|
|
ce3ad3a895 | ||
|
|
3a4b7b50de | ||
|
|
0d166ef732 | ||
|
|
f7d2994f8b | ||
|
|
39d0947102 | ||
|
|
8f0757606d | ||
|
|
0a8f2e01a0 | ||
|
|
4546d7bc52 | ||
|
|
0085d9a353 | ||
|
|
2f01ce3b23 | ||
|
|
71b19c1b5f | ||
|
|
82e55ae87f | ||
|
|
3e02001544 | ||
|
|
17849d8758 | ||
|
|
82f70bb53a | ||
|
|
2dcded6cca | ||
|
|
d3d30a587c | ||
|
|
8ca59e6f0c | ||
|
|
82dac82d15 | ||
|
|
288a823edf | ||
|
|
f9e3d30a71 | ||
|
|
9cec79b299 | ||
|
|
c86399327b | ||
|
|
034711d148 | ||
|
|
fa8162de02 | ||
|
|
33abc429d1 | ||
|
|
b22585ca90 | ||
|
|
9f2ca7012f | ||
|
|
21aeb68188 | ||
|
|
81e60ec5bf | ||
|
|
199c2746f1 | ||
|
|
8eca465ef6 | ||
|
|
a45e59239f | ||
|
|
2ad0bcab7c | ||
|
|
070d150420 | ||
|
|
90ecbe90d8 | ||
|
|
813fa03dc3 | ||
|
|
02381fbbe9 | ||
|
|
0722b681b1 | ||
|
|
564815e836 | ||
|
|
88b30adf7f | ||
|
|
b6acf3b522 | ||
|
|
ba55468da8 | ||
|
|
cdd217283d | ||
|
|
810a582717 | ||
|
|
a6948e8dcb | ||
|
|
5f35554fdc | ||
|
|
0ecc7ae594 | ||
|
|
fdfca469cc | ||
|
|
5f2ec76ba8 | ||
|
|
b015c8ff14 | ||
|
|
7e70870a9e | ||
|
|
eadad6c163 | ||
|
|
22b32a16dd | ||
|
|
22f869734e | ||
|
|
398bc9e4ed | ||
|
|
72dbb69a1c | ||
|
|
339959d1c0 | ||
|
|
d5c0ec4404 | ||
|
|
e616b4c120 | ||
|
|
f240a99e22 | ||
|
|
614f32c5e0 | ||
|
|
cd6707a566 | ||
|
|
edd207a9d5 | ||
|
|
724d76965f | ||
|
|
dbf4fb66a4 | ||
|
|
5f28e9b191 | ||
|
|
01bd3b6e06 | ||
|
|
1abfd77351 | ||
|
|
06a555fafb | ||
|
|
81c0f2b464 | ||
|
|
d5dc28e526 | ||
|
|
7411031e11 | ||
|
|
247091766c | ||
|
|
7f93110d68 | ||
|
|
05f6503d61 | ||
|
|
a149ea7e8f | ||
|
|
bb71e44614 | ||
|
|
84197fb33b | ||
|
|
89a6e7c0dd | ||
|
|
a902f667ba | ||
|
|
f72c30abd0 | ||
|
|
37e9257698 | ||
|
|
72105f1f2f | ||
|
|
ee89b78751 | ||
|
|
33ef138866 | ||
|
|
71da27dc8e | ||
|
|
80bf07ffd8 | ||
|
|
b69e50542a | ||
|
|
3ecd19d085 | ||
|
|
b6a3d1889c | ||
|
|
1cb34b089c | ||
|
|
1537ba5031 | ||
|
|
8225d41661 | ||
|
|
ee437265fc | ||
|
|
affede7f0a | ||
|
|
97366c0496 | ||
|
|
3f46caaf02 | ||
|
|
f3181b204a | ||
|
|
dd39db4584 | ||
|
|
759880a800 | ||
|
|
f5cd90d139 | ||
|
|
31383bd9d0 | ||
|
|
ebb93b4fa7 | ||
|
|
21076af01e | ||
|
|
f11e9da83a | ||
|
|
0fddcd8fe7 | ||
|
|
927eba566c | ||
|
|
af9330a9dd | ||
|
|
b3fbd5c7a4 | ||
|
|
5228668be0 | ||
|
|
7d07f3c8fe | ||
|
|
d9a9029ce5 | ||
|
|
9fe53d9ccc | ||
|
|
f7b80f5931 | ||
|
|
f11d315fea | ||
|
|
2013bf9e30 | ||
|
|
a2ffb92b8d | ||
|
|
8b6d11b118 | ||
|
|
ba00ae8a7b | ||
|
|
470b3bd4da | ||
|
|
c124186989 | ||
|
|
d24808ff98 | ||
|
|
7d22f7bd58 | ||
|
|
88582c94e8 | ||
|
|
cefbe01333 | ||
|
|
76a6997de2 | ||
|
|
16a4a42faf | ||
|
|
0e4623c728 | ||
|
|
d598e20fbb | ||
|
|
8b0d4b2195 | ||
|
|
cf414423b1 | ||
|
|
0405a66c72 | ||
|
|
da7c2995b0 | ||
|
|
696a1a729c | ||
|
|
5fa7006f11 | ||
|
|
5634aed679 | ||
|
|
a232cd89cc | ||
|
|
dd40e44530 | ||
|
|
47d226e189 | ||
|
|
440537140b | ||
|
|
29e13b2c0b | ||
|
|
2006a07637 | ||
|
|
abcad9fde0 | ||
|
|
a43947cca5 | ||
|
|
f51de6569f | ||
|
|
b0584a4dc5 | ||
|
|
08f34d408f | ||
|
|
6298397534 | ||
|
|
a0cf0489d6 | ||
|
|
9ccd0acb4f | ||
|
|
1ffdcab3be | ||
|
|
da1045e052 | ||
|
|
55be1f1119 | ||
|
|
9272afa9e5 | ||
|
|
378d1ec81b | ||
|
|
c1b1452bd9 | ||
|
|
cdbacdcd7e | ||
|
|
6b8a6267da | ||
|
|
89e49d0bf3 | ||
|
|
2dfa83dd7d | ||
|
|
f0b67a415a | ||
|
|
b87af8ea3d | ||
|
|
46e38d39bb | ||
|
|
81afbd32d4 | ||
|
|
e9c4f40735 | ||
|
|
61bdfb1a4b | ||
|
|
9ec4a26f97 | ||
|
|
358a2e6d3f | ||
|
|
762e73faf5 | ||
|
|
ef3cfc8722 | ||
|
|
28d31f4840 | ||
|
|
2166bb749a | ||
|
|
868cd11874 | ||
|
|
7356f3affd | ||
|
|
dd56e7f1ac | ||
|
|
075b592471 | ||
|
|
51a3c04c3d | ||
|
|
1a8aae3039 | ||
|
|
8101bc4941 | ||
|
|
51e0ca2602 | ||
|
|
664f3fd18a | ||
|
|
dc2598d5cf | ||
|
|
d7e971a0fc | ||
|
|
613d31c8a6 |
No files matched your search
@@ -11,6 +11,9 @@ body:
|
|||||||
-
|
-
|
||||||
- 3.0.0
|
- 3.0.0
|
||||||
- 3.1.0
|
- 3.1.0
|
||||||
|
- 3.2.0
|
||||||
|
- 3.3.0
|
||||||
|
- 3.4.0
|
||||||
- Other (please provide detail below)
|
- Other (please provide detail below)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -5,6 +5,11 @@ on:
|
|||||||
paths:
|
paths:
|
||||||
- "salt/sensoroni/files/analyzers/**"
|
- "salt/sensoroni/files/analyzers/**"
|
||||||
- "salt/manager/tools/sbin/**"
|
- "salt/manager/tools/sbin/**"
|
||||||
|
- "salt/_beacons/**"
|
||||||
|
- "salt/elastalert/files/modules/**"
|
||||||
|
- "salt/telegraf/tools/sbin_jinja/**"
|
||||||
|
- "salt/telegraf/defaults.yaml"
|
||||||
|
- "salt/telegraf/soc_telegraf.yaml"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
@@ -14,7 +19,7 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
python-version: ["3.14"]
|
python-version: ["3.14"]
|
||||||
python-code-path: ["salt/sensoroni/files/analyzers", "salt/manager/tools/sbin"]
|
python-code-path: ["salt/sensoroni/files/analyzers", "salt/manager/tools/sbin", "salt/_beacons", "salt/elastalert/files/modules/so"]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -33,3 +38,25 @@ jobs:
|
|||||||
- name: Test with pytest
|
- name: Test with pytest
|
||||||
run: |
|
run: |
|
||||||
PYTHONPATH=${{ matrix.python-code-path }} pytest ${{ matrix.python-code-path }} --cov=${{ matrix.python-code-path }} --doctest-modules --cov-report=term --cov-fail-under=100 --cov-config=pytest.ini
|
PYTHONPATH=${{ matrix.python-code-path }} pytest ${{ matrix.python-code-path }} --cov=${{ matrix.python-code-path }} --doctest-modules --cov-report=term --cov-fail-under=100 --cov-config=pytest.ini
|
||||||
|
|
||||||
|
telegraf-collector:
|
||||||
|
# so-container-stats is a jinja template rather than an importable module, so it gets its
|
||||||
|
# own job: the test renders it the way salt does, then drives it with a faked docker engine
|
||||||
|
# and cgroup tree. No container runtime is needed.
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
- name: Set up Python
|
||||||
|
uses: actions/setup-python@v3
|
||||||
|
with:
|
||||||
|
python-version: "3.14"
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python -m pip install flake8 pytest jinja2 pyyaml
|
||||||
|
- name: Lint with flake8
|
||||||
|
run: |
|
||||||
|
flake8 salt/telegraf/tools/sbin_jinja/so-container-stats_test.py --config=pytest.ini
|
||||||
|
- name: Test with pytest
|
||||||
|
run: |
|
||||||
|
pytest salt/telegraf/tools/sbin_jinja/so-container-stats_test.py -v
|
||||||
+11
-11
@@ -1,17 +1,17 @@
|
|||||||
### 3.0.0-20260331 ISO image released on 2026/03/31
|
### 3.3.0-20260911 ISO image released on 2026/09/11
|
||||||
|
|
||||||
|
|
||||||
### Download and Verify
|
### Download and Verify
|
||||||
|
|
||||||
3.0.0-20260331 ISO image:
|
3.3.0-20260911 ISO image:
|
||||||
https://download.securityonion.net/file/securityonion/securityonion-3.0.0-20260331.iso
|
https://download.securityonion.net/file/securityonion/securityonion-3.3.0-20260911.iso
|
||||||
|
|
||||||
MD5: ECD318A1662A6FDE0EF213F5A9BD4B07
|
MD5: 12B18433D3A2198A185892FF79CF638F
|
||||||
SHA1: E55BE314440CCF3392DC0B06BC5E270B43176D9C
|
SHA1: 2B3C2E1FA7A78ED1F956E7EDCC12E32593C14EEE
|
||||||
SHA256: 7FC47405E335CBE5C2B6C51FE7AC60248F35CBE504907B8B5A33822B23F8F4D5
|
SHA256: 0938C73B76CE30EC9E4394D312C79EA7CAC721B6818541697279A6221F7D870D
|
||||||
|
|
||||||
Signature for ISO image:
|
Signature for ISO image:
|
||||||
https://github.com/Security-Onion-Solutions/securityonion/raw/3/main/sigs/securityonion-3.0.0-20260331.iso.sig
|
https://github.com/Security-Onion-Solutions/securityonion/raw/3/main/sigs/securityonion-3.3.0-20260911.iso.sig
|
||||||
|
|
||||||
Signing key:
|
Signing key:
|
||||||
https://raw.githubusercontent.com/Security-Onion-Solutions/securityonion/3/main/KEYS
|
https://raw.githubusercontent.com/Security-Onion-Solutions/securityonion/3/main/KEYS
|
||||||
@@ -25,22 +25,22 @@ wget https://raw.githubusercontent.com/Security-Onion-Solutions/securityonion/3/
|
|||||||
|
|
||||||
Download the signature file for the ISO:
|
Download the signature file for the ISO:
|
||||||
```
|
```
|
||||||
wget https://github.com/Security-Onion-Solutions/securityonion/raw/3/main/sigs/securityonion-3.0.0-20260331.iso.sig
|
wget https://github.com/Security-Onion-Solutions/securityonion/raw/3/main/sigs/securityonion-3.3.0-20260911.iso.sig
|
||||||
```
|
```
|
||||||
|
|
||||||
Download the ISO image:
|
Download the ISO image:
|
||||||
```
|
```
|
||||||
wget https://download.securityonion.net/file/securityonion/securityonion-3.0.0-20260331.iso
|
wget https://download.securityonion.net/file/securityonion/securityonion-3.3.0-20260911.iso
|
||||||
```
|
```
|
||||||
|
|
||||||
Verify the downloaded ISO image using the signature file:
|
Verify the downloaded ISO image using the signature file:
|
||||||
```
|
```
|
||||||
gpg --verify securityonion-3.0.0-20260331.iso.sig securityonion-3.0.0-20260331.iso
|
gpg --verify securityonion-3.3.0-20260911.iso.sig securityonion-3.3.0-20260911.iso
|
||||||
```
|
```
|
||||||
|
|
||||||
The output should show "Good signature" and the Primary key fingerprint should match what's shown below:
|
The output should show "Good signature" and the Primary key fingerprint should match what's shown below:
|
||||||
```
|
```
|
||||||
gpg: Signature made Mon 30 Mar 2026 06:22:14 PM EDT using RSA key ID FE507013
|
gpg: Signature made Fri 11 Sep 2026 11:23:56 AM EDT using RSA key ID FE507013
|
||||||
gpg: Good signature from "Security Onion Solutions, LLC <info@securityonionsolutions.com>"
|
gpg: Good signature from "Security Onion Solutions, LLC <info@securityonionsolutions.com>"
|
||||||
gpg: WARNING: This key is not certified with a trusted signature!
|
gpg: WARNING: This key is not certified with a trusted signature!
|
||||||
gpg: There is no indication that the signature belongs to the owner.
|
gpg: There is no indication that the signature belongs to the owner.
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# This script adds sensors/nodes/etc to the nodes tab
|
|
||||||
default_salt_dir=/opt/so/saltstack/default
|
|
||||||
local_salt_dir=/opt/so/saltstack/local
|
|
||||||
TYPE=$1
|
|
||||||
NAME=$2
|
|
||||||
IPADDRESS=$3
|
|
||||||
CPUS=$4
|
|
||||||
GUID=$5
|
|
||||||
MANINT=$6
|
|
||||||
ROOTFS=$7
|
|
||||||
NSM=$8
|
|
||||||
MONINT=$9
|
|
||||||
#NODETYPE=$10
|
|
||||||
#HOTNAME=$11
|
|
||||||
|
|
||||||
echo "Seeing if this host is already in here. If so delete it"
|
|
||||||
if grep -q $NAME "$local_salt_dir/pillar/data/$TYPE.sls"; then
|
|
||||||
echo "Node Already Present - Let's re-add it"
|
|
||||||
awk -v blah=" $NAME:" 'BEGIN{ print_flag=1 }
|
|
||||||
{
|
|
||||||
if( $0 ~ blah )
|
|
||||||
{
|
|
||||||
print_flag=0;
|
|
||||||
next
|
|
||||||
}
|
|
||||||
if( $0 ~ /^ [a-zA-Z0-9]+:$/ )
|
|
||||||
{
|
|
||||||
print_flag=1;
|
|
||||||
}
|
|
||||||
if ( print_flag == 1 )
|
|
||||||
print $0
|
|
||||||
|
|
||||||
} ' $local_salt_dir/pillar/data/$TYPE.sls > $local_salt_dir/pillar/data/tmp.$TYPE.sls
|
|
||||||
mv $local_salt_dir/pillar/data/tmp.$TYPE.sls $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
echo "Deleted $NAME from the tab. Now adding it in again with updated info"
|
|
||||||
fi
|
|
||||||
echo " $NAME:" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
echo " ip: $IPADDRESS" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
echo " manint: $MANINT" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
echo " totalcpus: $CPUS" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
echo " guid: $GUID" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
echo " rootfs: $ROOTFS" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
echo " nsmfs: $NSM" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
if [ $TYPE == 'sensorstab' ]; then
|
|
||||||
echo " monint: bond0" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
fi
|
|
||||||
if [ $TYPE == 'evaltab' ] || [ $TYPE == 'standalonetab' ]; then
|
|
||||||
echo " monint: bond0" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
if [ ! $10 ]; then
|
|
||||||
salt-call state.apply utility queue=True
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
if [ $TYPE == 'nodestab' ]; then
|
|
||||||
salt-call state.apply elasticsearch queue=True
|
|
||||||
# echo " nodetype: $NODETYPE" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
# echo " hotname: $HOTNAME" >> $local_salt_dir/pillar/data/$TYPE.sls
|
|
||||||
fi
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
elasticsearch:
|
|
||||||
index_settings:
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# Per-minion Telegraf Postgres credentials. so-telegraf-cred on the manager is
|
||||||
|
# the single writer; it mutates /opt/so/saltstack/local/pillar/telegraf/creds.sls
|
||||||
|
# under flock. Pillar_roots order (local before default) means the populated
|
||||||
|
# copy shadows this default on any real grid; this file exists so the pillar
|
||||||
|
# key is always defined on fresh installs and when no minions have creds yet.
|
||||||
|
telegraf:
|
||||||
|
postgres_creds: {}
|
||||||
+23
-3
@@ -3,6 +3,8 @@ base:
|
|||||||
- ca
|
- ca
|
||||||
- global.soc_global
|
- global.soc_global
|
||||||
- global.adv_global
|
- global.adv_global
|
||||||
|
- salt.soc_salt
|
||||||
|
- salt.adv_salt
|
||||||
- docker.soc_docker
|
- docker.soc_docker
|
||||||
- docker.adv_docker
|
- docker.adv_docker
|
||||||
- influxdb.token
|
- influxdb.token
|
||||||
@@ -17,6 +19,7 @@ base:
|
|||||||
- sensoroni.adv_sensoroni
|
- sensoroni.adv_sensoroni
|
||||||
- telegraf.soc_telegraf
|
- telegraf.soc_telegraf
|
||||||
- telegraf.adv_telegraf
|
- telegraf.adv_telegraf
|
||||||
|
- telegraf.creds
|
||||||
- versionlock.soc_versionlock
|
- versionlock.soc_versionlock
|
||||||
- versionlock.adv_versionlock
|
- versionlock.adv_versionlock
|
||||||
- soc.license
|
- soc.license
|
||||||
@@ -38,6 +41,9 @@ base:
|
|||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
||||||
- elasticsearch.auth
|
- elasticsearch.auth
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %}
|
||||||
|
- postgres.auth
|
||||||
|
{% endif %}
|
||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
||||||
- kibana.secrets
|
- kibana.secrets
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -60,6 +66,8 @@ base:
|
|||||||
- redis.adv_redis
|
- redis.adv_redis
|
||||||
- influxdb.soc_influxdb
|
- influxdb.soc_influxdb
|
||||||
- influxdb.adv_influxdb
|
- influxdb.adv_influxdb
|
||||||
|
- postgres.soc_postgres
|
||||||
|
- postgres.adv_postgres
|
||||||
- elasticsearch.nodes
|
- elasticsearch.nodes
|
||||||
- elasticsearch.soc_elasticsearch
|
- elasticsearch.soc_elasticsearch
|
||||||
- elasticsearch.adv_elasticsearch
|
- elasticsearch.adv_elasticsearch
|
||||||
@@ -97,10 +105,12 @@ base:
|
|||||||
- node_data.ips
|
- node_data.ips
|
||||||
- secrets
|
- secrets
|
||||||
- healthcheck.eval
|
- healthcheck.eval
|
||||||
- elasticsearch.index_templates
|
|
||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
||||||
- elasticsearch.auth
|
- elasticsearch.auth
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %}
|
||||||
|
- postgres.auth
|
||||||
|
{% endif %}
|
||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
||||||
- kibana.secrets
|
- kibana.secrets
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -126,6 +136,8 @@ base:
|
|||||||
- redis.adv_redis
|
- redis.adv_redis
|
||||||
- influxdb.soc_influxdb
|
- influxdb.soc_influxdb
|
||||||
- influxdb.adv_influxdb
|
- influxdb.adv_influxdb
|
||||||
|
- postgres.soc_postgres
|
||||||
|
- postgres.adv_postgres
|
||||||
- backup.soc_backup
|
- backup.soc_backup
|
||||||
- backup.adv_backup
|
- backup.adv_backup
|
||||||
- zeek.soc_zeek
|
- zeek.soc_zeek
|
||||||
@@ -142,10 +154,12 @@ base:
|
|||||||
- logstash.nodes
|
- logstash.nodes
|
||||||
- logstash.soc_logstash
|
- logstash.soc_logstash
|
||||||
- logstash.adv_logstash
|
- logstash.adv_logstash
|
||||||
- elasticsearch.index_templates
|
|
||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
||||||
- elasticsearch.auth
|
- elasticsearch.auth
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %}
|
||||||
|
- postgres.auth
|
||||||
|
{% endif %}
|
||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
||||||
- kibana.secrets
|
- kibana.secrets
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -160,6 +174,8 @@ base:
|
|||||||
- redis.adv_redis
|
- redis.adv_redis
|
||||||
- influxdb.soc_influxdb
|
- influxdb.soc_influxdb
|
||||||
- influxdb.adv_influxdb
|
- influxdb.adv_influxdb
|
||||||
|
- postgres.soc_postgres
|
||||||
|
- postgres.adv_postgres
|
||||||
- elasticsearch.nodes
|
- elasticsearch.nodes
|
||||||
- elasticsearch.soc_elasticsearch
|
- elasticsearch.soc_elasticsearch
|
||||||
- elasticsearch.adv_elasticsearch
|
- elasticsearch.adv_elasticsearch
|
||||||
@@ -256,10 +272,12 @@ base:
|
|||||||
'*_import':
|
'*_import':
|
||||||
- node_data.ips
|
- node_data.ips
|
||||||
- secrets
|
- secrets
|
||||||
- elasticsearch.index_templates
|
|
||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/elasticsearch/auth.sls') %}
|
||||||
- elasticsearch.auth
|
- elasticsearch.auth
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/postgres/auth.sls') %}
|
||||||
|
- postgres.auth
|
||||||
|
{% endif %}
|
||||||
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
{% if salt['file.file_exists']('/opt/so/saltstack/local/pillar/kibana/secrets.sls') %}
|
||||||
- kibana.secrets
|
- kibana.secrets
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -285,6 +303,8 @@ base:
|
|||||||
- redis.adv_redis
|
- redis.adv_redis
|
||||||
- influxdb.soc_influxdb
|
- influxdb.soc_influxdb
|
||||||
- influxdb.adv_influxdb
|
- influxdb.adv_influxdb
|
||||||
|
- postgres.soc_postgres
|
||||||
|
- postgres.adv_postgres
|
||||||
- zeek.soc_zeek
|
- zeek.soc_zeek
|
||||||
- zeek.adv_zeek
|
- zeek.adv_zeek
|
||||||
- bpf.soc_bpf
|
- bpf.soc_bpf
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# Custom salt beacon that watches hand-edited directories under
|
||||||
|
# /opt/so/saltstack/local/salt/ for changes and emits a beacon event per changed
|
||||||
|
# directory. This replaces the stock salt `inotify` beacon, which leaks a kernel
|
||||||
|
# inotify instance every time the minion rebuilds the beacon loader's __context__
|
||||||
|
# (orphaning the old pyinotify.Notifier without closing it) until
|
||||||
|
# fs.inotify.max_user_instances is exhausted and the beacon dies with EMFILE.
|
||||||
|
# Polling holds zero inotify instances, so the leak is impossible, and it keeps
|
||||||
|
# firing during state runs (no blackout).
|
||||||
|
#
|
||||||
|
# Detection is poll-based with a per-directory fingerprint persisted to
|
||||||
|
# WATERMARK_DIR: each pass walks the directory and hashes every file's
|
||||||
|
# (relpath, st_mtime_ns, st_size), which catches content writes, additions,
|
||||||
|
# moves, and deletions. A change in the digest emits one event; an unchanged
|
||||||
|
# digest emits nothing. This makes it self-healing (a missed poll simply catches
|
||||||
|
# up on the next one).
|
||||||
|
#
|
||||||
|
# Each emitted event carries the watched directory path under the configured tag
|
||||||
|
# (e.g. salt/beacon/<minion>/local_files_beacon/zeek); the push_files reactor
|
||||||
|
# looks the tag up in salt/reactor/pillar_push_map.yaml and writes a push intent,
|
||||||
|
# after which the existing so-push-drainer / orch.push_batch pipeline takes over
|
||||||
|
# unchanged.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
WATERMARK_DIR = '/opt/so/state'
|
||||||
|
|
||||||
|
# Temp/editor files that should not trigger a push. Mirrors the exclude regexes
|
||||||
|
# the inotify beacon used. Matched against the full pathname.
|
||||||
|
EXCLUDES = [
|
||||||
|
re.compile(r'\.sw[a-z]$'),
|
||||||
|
re.compile(r'~$'),
|
||||||
|
re.compile(r'/4913$'),
|
||||||
|
re.compile(r'/\.#'),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate(config):
|
||||||
|
return True, 'valid'
|
||||||
|
|
||||||
|
|
||||||
|
def _paths_from_config(config):
|
||||||
|
# The beacon config arrives as a list of single-key dicts (salt beacon style).
|
||||||
|
# Merge it and return the {dir: tag} mapping under the 'paths' key.
|
||||||
|
merged = {}
|
||||||
|
if isinstance(config, list):
|
||||||
|
for item in config:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
merged.update(item)
|
||||||
|
elif isinstance(config, dict):
|
||||||
|
merged = config
|
||||||
|
paths = merged.get('paths', {})
|
||||||
|
return paths if isinstance(paths, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _excluded(pathname):
|
||||||
|
for pattern in EXCLUDES:
|
||||||
|
if pattern.search(pathname):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _fingerprint(directory):
|
||||||
|
# Stat-only walk; hash each file's (relpath, mtime_ns, size). Returns a hex
|
||||||
|
# digest, or the digest of an empty tree if the directory does not exist.
|
||||||
|
h = hashlib.sha1()
|
||||||
|
if os.path.isdir(directory):
|
||||||
|
entries = []
|
||||||
|
for root, dirs, files in os.walk(directory):
|
||||||
|
# zkg packages are git clones; .git churn would fire a state apply on its own.
|
||||||
|
dirs[:] = [d for d in dirs if d != '.git']
|
||||||
|
for name in files:
|
||||||
|
full = os.path.join(root, name)
|
||||||
|
if _excluded(full):
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
st = os.stat(full)
|
||||||
|
except OSError:
|
||||||
|
continue
|
||||||
|
rel = os.path.relpath(full, directory)
|
||||||
|
entries.append('%s\0%d\0%d' % (rel, st.st_mtime_ns, st.st_size))
|
||||||
|
for line in sorted(entries):
|
||||||
|
h.update(line.encode('utf-8', 'surrogateescape'))
|
||||||
|
h.update(b'\n')
|
||||||
|
return h.hexdigest()
|
||||||
|
|
||||||
|
|
||||||
|
def _watermark_file(tag, directory):
|
||||||
|
# Keyed by directory: zeek/policy and zeek/zkg share the tag `zeek`.
|
||||||
|
scope = hashlib.sha1(directory.encode('utf-8', 'surrogateescape')).hexdigest()[:12]
|
||||||
|
return os.path.join(WATERMARK_DIR, 'local_files_beacon_%s_%s.hash' % (tag, scope))
|
||||||
|
|
||||||
|
|
||||||
|
def _read_watermark(tag, directory):
|
||||||
|
try:
|
||||||
|
with open(_watermark_file(tag, directory), 'r') as f:
|
||||||
|
return (f.read() or '').strip() or None
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_watermark(tag, directory, digest):
|
||||||
|
path = _watermark_file(tag, directory)
|
||||||
|
try:
|
||||||
|
os.makedirs(WATERMARK_DIR, exist_ok=True)
|
||||||
|
tmp = path + '.tmp'
|
||||||
|
with open(tmp, 'w') as f:
|
||||||
|
f.write(digest)
|
||||||
|
os.rename(tmp, path)
|
||||||
|
except OSError:
|
||||||
|
log.exception('local_files_beacon: failed to persist watermark to %s', path)
|
||||||
|
|
||||||
|
|
||||||
|
def beacon(config):
|
||||||
|
retval = []
|
||||||
|
|
||||||
|
for directory, tag in _paths_from_config(config).items():
|
||||||
|
digest = _fingerprint(directory)
|
||||||
|
previous = _read_watermark(tag, directory)
|
||||||
|
|
||||||
|
# First run / missing watermark: seed the digest and emit nothing so a
|
||||||
|
# fresh host does not fire a spurious fleetwide push.
|
||||||
|
if previous is None:
|
||||||
|
_write_watermark(tag, directory, digest)
|
||||||
|
continue
|
||||||
|
|
||||||
|
if digest != previous:
|
||||||
|
_write_watermark(tag, directory, digest)
|
||||||
|
retval.append({'tag': tag, 'path': directory})
|
||||||
|
log.info('local_files_beacon: change detected in %s, emitting %s', directory, tag)
|
||||||
|
|
||||||
|
return retval
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import local_files_beacon
|
||||||
|
|
||||||
|
|
||||||
|
class TestRulesBeacon(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Isolate all on-disk state (watermarks and the dirs we fingerprint) in a
|
||||||
|
# throwaway tree, and point WATERMARK_DIR at it so the real read/write
|
||||||
|
# helpers run against actual files.
|
||||||
|
self.tmpdir = tempfile.mkdtemp()
|
||||||
|
self.state = os.path.join(self.tmpdir, 'state')
|
||||||
|
patcher = patch.object(local_files_beacon, 'WATERMARK_DIR', self.state)
|
||||||
|
patcher.start()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
def _make_dir(self, name, files=None):
|
||||||
|
path = os.path.join(self.tmpdir, name)
|
||||||
|
os.makedirs(path, exist_ok=True)
|
||||||
|
for fname, content in (files or {}).items():
|
||||||
|
with open(os.path.join(path, fname), 'w') as f:
|
||||||
|
f.write(content)
|
||||||
|
return path
|
||||||
|
|
||||||
|
# -- trivial contract -------------------------------------------------
|
||||||
|
|
||||||
|
def test_virtual_returns_true(self):
|
||||||
|
self.assertTrue(local_files_beacon.__virtual__())
|
||||||
|
|
||||||
|
def test_validate_returns_valid(self):
|
||||||
|
self.assertEqual(local_files_beacon.validate({}), (True, 'valid'))
|
||||||
|
|
||||||
|
# -- _paths_from_config -----------------------------------------------
|
||||||
|
|
||||||
|
def test_paths_from_config_list_of_dicts(self):
|
||||||
|
config = [{'interval': 10}, {'paths': {'/a': 'suricata', '/b': 'strelka'}}]
|
||||||
|
self.assertEqual(
|
||||||
|
local_files_beacon._paths_from_config(config),
|
||||||
|
{'/a': 'suricata', '/b': 'strelka'},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_paths_from_config_plain_dict(self):
|
||||||
|
self.assertEqual(
|
||||||
|
local_files_beacon._paths_from_config({'paths': {'/a': 'suricata'}}),
|
||||||
|
{'/a': 'suricata'},
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_paths_from_config_skips_non_dict_items(self):
|
||||||
|
self.assertEqual(local_files_beacon._paths_from_config(['bogus', 42]), {})
|
||||||
|
|
||||||
|
def test_paths_from_config_paths_not_a_dict(self):
|
||||||
|
self.assertEqual(local_files_beacon._paths_from_config({'paths': 'nope'}), {})
|
||||||
|
|
||||||
|
def test_paths_from_config_unexpected_type(self):
|
||||||
|
self.assertEqual(local_files_beacon._paths_from_config('nonsense'), {})
|
||||||
|
|
||||||
|
# -- _excluded --------------------------------------------------------
|
||||||
|
|
||||||
|
def test_excluded_matches_temp_and_editor_files(self):
|
||||||
|
for pathname in ('/rules/foo.swp', '/rules/foo~', '/rules/4913', '/rules/.#foo'):
|
||||||
|
self.assertTrue(local_files_beacon._excluded(pathname), pathname)
|
||||||
|
|
||||||
|
def test_excluded_allows_real_rule_files(self):
|
||||||
|
self.assertFalse(local_files_beacon._excluded('/rules/suricata.rules'))
|
||||||
|
|
||||||
|
# -- _fingerprint -----------------------------------------------------
|
||||||
|
|
||||||
|
def test_fingerprint_missing_dir_is_empty_tree_digest(self):
|
||||||
|
missing = os.path.join(self.tmpdir, 'does-not-exist')
|
||||||
|
self.assertEqual(local_files_beacon._fingerprint(missing), hashlib.sha1().hexdigest())
|
||||||
|
|
||||||
|
def test_fingerprint_changes_when_content_changes(self):
|
||||||
|
d = self._make_dir('rules', {'a.rules': 'alert'})
|
||||||
|
before = local_files_beacon._fingerprint(d)
|
||||||
|
with open(os.path.join(d, 'a.rules'), 'w') as f:
|
||||||
|
f.write('alert tcp any any -> any any') # different size
|
||||||
|
self.assertNotEqual(local_files_beacon._fingerprint(d), before)
|
||||||
|
|
||||||
|
def test_fingerprint_ignores_excluded_files(self):
|
||||||
|
d = self._make_dir('rules', {'a.rules': 'alert'})
|
||||||
|
before = local_files_beacon._fingerprint(d)
|
||||||
|
with open(os.path.join(d, 'a.rules.swp'), 'w') as f:
|
||||||
|
f.write('editor swap')
|
||||||
|
self.assertEqual(local_files_beacon._fingerprint(d), before)
|
||||||
|
|
||||||
|
def test_fingerprint_skips_unstatable_entries(self):
|
||||||
|
# A dangling symlink appears in os.walk's file list but os.stat raises
|
||||||
|
# OSError, exercising the except-continue path.
|
||||||
|
d = self._make_dir('rules', {'a.rules': 'alert'})
|
||||||
|
good = local_files_beacon._fingerprint(d)
|
||||||
|
os.symlink(os.path.join(d, 'missing-target'), os.path.join(d, 'broken.link'))
|
||||||
|
self.assertEqual(local_files_beacon._fingerprint(d), good)
|
||||||
|
|
||||||
|
def test_fingerprint_prunes_git_metadata(self):
|
||||||
|
# zkg packages are git clones, so the watched tree carries .git.
|
||||||
|
d = self._make_dir('zkg', {'pkg.zeek': 'print 1;'})
|
||||||
|
before = local_files_beacon._fingerprint(d)
|
||||||
|
git_dir = os.path.join(d, 'pkg', '.git', 'refs', 'heads')
|
||||||
|
os.makedirs(git_dir)
|
||||||
|
with open(os.path.join(git_dir, 'main'), 'w') as f:
|
||||||
|
f.write('0' * 40)
|
||||||
|
self.assertEqual(local_files_beacon._fingerprint(d), before)
|
||||||
|
|
||||||
|
def test_fingerprint_still_sees_worktree_next_to_git(self):
|
||||||
|
d = self._make_dir('zkg', {'pkg.zeek': 'print 1;'})
|
||||||
|
os.makedirs(os.path.join(d, 'pkg', '.git'))
|
||||||
|
before = local_files_beacon._fingerprint(d)
|
||||||
|
with open(os.path.join(d, 'pkg', 'scripts.zeek'), 'w') as f:
|
||||||
|
f.write('print 2;')
|
||||||
|
self.assertNotEqual(local_files_beacon._fingerprint(d), before)
|
||||||
|
|
||||||
|
# -- _read_watermark / _write_watermark -------------------------------
|
||||||
|
|
||||||
|
def test_watermark_round_trip(self):
|
||||||
|
local_files_beacon._write_watermark('suricata', '/rules/suricata', 'deadbeef')
|
||||||
|
self.assertEqual(
|
||||||
|
local_files_beacon._read_watermark('suricata', '/rules/suricata'), 'deadbeef')
|
||||||
|
|
||||||
|
def test_read_watermark_missing_returns_none(self):
|
||||||
|
self.assertIsNone(local_files_beacon._read_watermark('suricata', '/rules/suricata'))
|
||||||
|
|
||||||
|
def test_read_watermark_empty_file_returns_none(self):
|
||||||
|
os.makedirs(self.state, exist_ok=True)
|
||||||
|
with open(local_files_beacon._watermark_file('suricata', '/rules/suricata'), 'w') as f:
|
||||||
|
f.write('')
|
||||||
|
self.assertIsNone(local_files_beacon._read_watermark('suricata', '/rules/suricata'))
|
||||||
|
|
||||||
|
def test_write_watermark_swallows_oserror(self):
|
||||||
|
with patch.object(local_files_beacon.os, 'makedirs', side_effect=OSError):
|
||||||
|
local_files_beacon._write_watermark('suricata', '/rules/suricata', 'deadbeef')
|
||||||
|
self.assertIsNone(local_files_beacon._read_watermark('suricata', '/rules/suricata'))
|
||||||
|
|
||||||
|
def test_watermark_file_differs_per_directory_within_one_tag(self):
|
||||||
|
# zeek/policy and zeek/zkg share the tag 'zeek'.
|
||||||
|
self.assertNotEqual(
|
||||||
|
local_files_beacon._watermark_file('zeek', '/local/zeek/policy'),
|
||||||
|
local_files_beacon._watermark_file('zeek', '/local/zeek/zkg'),
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_watermarks_are_independent_within_one_tag(self):
|
||||||
|
local_files_beacon._write_watermark('zeek', '/local/zeek/policy', 'policyhash')
|
||||||
|
local_files_beacon._write_watermark('zeek', '/local/zeek/zkg', 'zkghash')
|
||||||
|
self.assertEqual(
|
||||||
|
local_files_beacon._read_watermark('zeek', '/local/zeek/policy'), 'policyhash')
|
||||||
|
self.assertEqual(
|
||||||
|
local_files_beacon._read_watermark('zeek', '/local/zeek/zkg'), 'zkghash')
|
||||||
|
|
||||||
|
# -- beacon -----------------------------------------------------------
|
||||||
|
|
||||||
|
def _config(self, mapping):
|
||||||
|
return [{'paths': mapping}]
|
||||||
|
|
||||||
|
def test_beacon_seeds_first_run_and_emits_nothing(self):
|
||||||
|
with patch.object(local_files_beacon, '_fingerprint', return_value='hash1'), \
|
||||||
|
patch.object(local_files_beacon, '_read_watermark', return_value=None), \
|
||||||
|
patch.object(local_files_beacon, '_write_watermark') as mock_write:
|
||||||
|
result = local_files_beacon.beacon(self._config({'/rules/suricata': 'suricata'}))
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
mock_write.assert_called_once_with('suricata', '/rules/suricata', 'hash1')
|
||||||
|
|
||||||
|
def test_beacon_emits_on_change(self):
|
||||||
|
with patch.object(local_files_beacon, '_fingerprint', return_value='newhash'), \
|
||||||
|
patch.object(local_files_beacon, '_read_watermark', return_value='oldhash'), \
|
||||||
|
patch.object(local_files_beacon, '_write_watermark') as mock_write:
|
||||||
|
result = local_files_beacon.beacon(self._config({'/rules/suricata': 'suricata'}))
|
||||||
|
self.assertEqual(result, [{'tag': 'suricata', 'path': '/rules/suricata'}])
|
||||||
|
mock_write.assert_called_once_with('suricata', '/rules/suricata', 'newhash')
|
||||||
|
|
||||||
|
def test_beacon_no_change_emits_nothing(self):
|
||||||
|
with patch.object(local_files_beacon, '_fingerprint', return_value='samehash'), \
|
||||||
|
patch.object(local_files_beacon, '_read_watermark', return_value='samehash'), \
|
||||||
|
patch.object(local_files_beacon, '_write_watermark') as mock_write:
|
||||||
|
result = local_files_beacon.beacon(self._config({'/rules/suricata': 'suricata'}))
|
||||||
|
self.assertEqual(result, [])
|
||||||
|
mock_write.assert_not_called()
|
||||||
|
|
||||||
|
def test_beacon_end_to_end_with_real_files(self):
|
||||||
|
# Exercise the full stack (real fingerprint + real watermark files) across
|
||||||
|
# two poll passes: first seeds silently, second fires after a write.
|
||||||
|
d = self._make_dir('rules', {'a.rules': 'alert'})
|
||||||
|
config = self._config({d: 'suricata'})
|
||||||
|
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), []) # seed pass
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), []) # unchanged pass
|
||||||
|
|
||||||
|
with open(os.path.join(d, 'b.rules'), 'w') as f:
|
||||||
|
f.write('alert tcp any any -> any any')
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), [{'tag': 'suricata', 'path': d}])
|
||||||
|
|
||||||
|
def test_beacon_two_dirs_one_tag_do_not_flap(self):
|
||||||
|
# Tag-keyed watermarks would clobber each other and emit on every pass.
|
||||||
|
policy = self._make_dir('zeek/policy', {'intel.dat': '#fields\tindicator'})
|
||||||
|
zkg = self._make_dir('zeek/zkg', {'README': 'place packages here'})
|
||||||
|
config = self._config({policy: 'zeek', zkg: 'zeek'})
|
||||||
|
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), []) # seed pass
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), []) # idle
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), []) # still idle
|
||||||
|
|
||||||
|
with open(os.path.join(policy, 'intel.dat'), 'a') as f:
|
||||||
|
f.write('\nevil.com\tIntel::DOMAIN\tsource\n')
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), [{'tag': 'zeek', 'path': policy}])
|
||||||
|
self.assertEqual(local_files_beacon.beacon(config), []) # quiet again
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# Custom salt beacon that watches the SOC audit_settings table in postgres for
|
||||||
|
# new settings changes and emits a beacon event per new row. This replaces the
|
||||||
|
# inotify watch on /opt/so/saltstack/local/pillar -- instead of monitoring pillar
|
||||||
|
# files on disk, we monitor the securityonion.audit_settings table that SOC writes to.
|
||||||
|
#
|
||||||
|
# Detection is poll-based with a monotonic `id` watermark persisted to
|
||||||
|
# WATERMARK_FILE: each pass selects rows with id greater than the last id seen,
|
||||||
|
# which makes it self-healing (a missed poll simply catches up on the next one).
|
||||||
|
#
|
||||||
|
# Each emitted event carries setting_id and node_id; the push_pillar reactor maps
|
||||||
|
# setting_id -> app via pillar_push_map.yaml and writes a push intent, after which
|
||||||
|
# the existing so-push-drainer / orch.push_batch pipeline takes over unchanged.
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
WATERMARK_FILE = '/opt/so/state/postgres_pillar_beacon_watch.id'
|
||||||
|
CONTAINER = 'so-postgres'
|
||||||
|
DATABASE = 'securityonion'
|
||||||
|
|
||||||
|
# Unaligned, tuples-only psql output with a field separator that cannot appear in
|
||||||
|
# an id/setting_id/node_id, so we can split each row reliably.
|
||||||
|
FIELD_SEP = '\x1f'
|
||||||
|
|
||||||
|
|
||||||
|
def __virtual__():
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def validate(config):
|
||||||
|
return True, 'valid'
|
||||||
|
|
||||||
|
|
||||||
|
def _read_watermark():
|
||||||
|
# Returns the last processed id, or None if the watermark has not been seeded.
|
||||||
|
try:
|
||||||
|
with open(WATERMARK_FILE, 'r') as f:
|
||||||
|
return int((f.read() or '').strip())
|
||||||
|
except (IOError, ValueError):
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _write_watermark(value):
|
||||||
|
try:
|
||||||
|
os.makedirs(os.path.dirname(WATERMARK_FILE), exist_ok=True)
|
||||||
|
tmp = WATERMARK_FILE + '.tmp'
|
||||||
|
with open(tmp, 'w') as f:
|
||||||
|
f.write(str(int(value)))
|
||||||
|
os.rename(tmp, WATERMARK_FILE)
|
||||||
|
except OSError:
|
||||||
|
log.exception('postgres_pillar_beacon: failed to persist watermark to %s', WATERMARK_FILE)
|
||||||
|
|
||||||
|
|
||||||
|
def _query(sql):
|
||||||
|
# Run a query against securityonion inside the so-postgres container over the unix
|
||||||
|
# socket (trust auth, no password). Returns stdout on success, or None on any
|
||||||
|
# failure so the caller can no-op and retry on the next interval.
|
||||||
|
cmd = [
|
||||||
|
'docker', 'exec', CONTAINER,
|
||||||
|
'psql', '-U', 'postgres', '-d', DATABASE,
|
||||||
|
'-tA', '-F', FIELD_SEP, '-c', sql,
|
||||||
|
]
|
||||||
|
try:
|
||||||
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
log.warning('postgres_pillar_beacon: psql timed out')
|
||||||
|
return None
|
||||||
|
except Exception:
|
||||||
|
log.exception('postgres_pillar_beacon: failed to exec psql')
|
||||||
|
return None
|
||||||
|
if result.returncode != 0:
|
||||||
|
log.warning('postgres_pillar_beacon: psql failed (rc=%s): %s',
|
||||||
|
result.returncode, (result.stderr or '').strip())
|
||||||
|
return None
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def beacon(config): # noqa: C901
|
||||||
|
retval = []
|
||||||
|
|
||||||
|
watermark = _read_watermark()
|
||||||
|
|
||||||
|
# First run / missing watermark: seed to the current MAX(id) and emit nothing
|
||||||
|
# so we never replay the entire settings history into a fleetwide push.
|
||||||
|
if watermark is None:
|
||||||
|
seed = _query('SELECT COALESCE(MAX(id), 0) FROM audit_settings;')
|
||||||
|
if seed is None:
|
||||||
|
return retval # postgres not ready yet; retry next interval
|
||||||
|
try:
|
||||||
|
_write_watermark(int((seed or '0').strip() or 0))
|
||||||
|
except ValueError:
|
||||||
|
log.warning('postgres_pillar_beacon: could not parse MAX(id) seed: %r', seed)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
rows = _query(
|
||||||
|
"SELECT id, setting_id, COALESCE(node_id, '') FROM audit_settings "
|
||||||
|
"WHERE id > %d ORDER BY id;" % watermark
|
||||||
|
)
|
||||||
|
if rows is None:
|
||||||
|
return retval
|
||||||
|
|
||||||
|
max_id = watermark
|
||||||
|
for line in rows.splitlines():
|
||||||
|
# Do NOT str.strip() the whole line: Python treats the \x1f field
|
||||||
|
# separator (and \x1c-\x1e) as whitespace, so stripping would eat an
|
||||||
|
# empty trailing node_id field and make the row look malformed.
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
parts = line.split(FIELD_SEP)
|
||||||
|
if len(parts) < 3:
|
||||||
|
log.warning('postgres_pillar_beacon: skipping malformed row: %r', line)
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
row_id = int(parts[0])
|
||||||
|
except ValueError:
|
||||||
|
log.warning('postgres_pillar_beacon: skipping row with non-int id: %r', line)
|
||||||
|
continue
|
||||||
|
setting_id = parts[1]
|
||||||
|
node_id = parts[2]
|
||||||
|
retval.append({
|
||||||
|
'tag': 'audit_settings',
|
||||||
|
'id': row_id,
|
||||||
|
'setting_id': setting_id,
|
||||||
|
'node_id': node_id,
|
||||||
|
})
|
||||||
|
if row_id > max_id:
|
||||||
|
max_id = row_id
|
||||||
|
|
||||||
|
if max_id > watermark:
|
||||||
|
_write_watermark(max_id)
|
||||||
|
log.info('postgres_pillar_beacon: emitted %d change(s), watermark %d -> %d',
|
||||||
|
len(retval), watermark, max_id)
|
||||||
|
|
||||||
|
return retval
|
||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import patch
|
||||||
|
|
||||||
|
import postgres_pillar_beacon
|
||||||
|
|
||||||
|
|
||||||
|
class TestPostgresPillarBeacon(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# Point WATERMARK_FILE at a throwaway dir so the real read/write helpers
|
||||||
|
# (and their os.makedirs/os.rename) run against actual files, then clean
|
||||||
|
# it all up in tearDown.
|
||||||
|
self.tmpdir = tempfile.mkdtemp()
|
||||||
|
self.watermark = os.path.join(self.tmpdir, 'state', 'watch.id')
|
||||||
|
patcher = patch.object(postgres_pillar_beacon, 'WATERMARK_FILE', self.watermark)
|
||||||
|
patcher.start()
|
||||||
|
self.addCleanup(patcher.stop)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
shutil.rmtree(self.tmpdir, ignore_errors=True)
|
||||||
|
|
||||||
|
# -- trivial contract -------------------------------------------------
|
||||||
|
|
||||||
|
def test_virtual_returns_true(self):
|
||||||
|
self.assertTrue(postgres_pillar_beacon.__virtual__())
|
||||||
|
|
||||||
|
def test_validate_returns_valid(self):
|
||||||
|
self.assertEqual(postgres_pillar_beacon.validate({}), (True, 'valid'))
|
||||||
|
|
||||||
|
# -- _read_watermark --------------------------------------------------
|
||||||
|
|
||||||
|
def test_read_watermark_valid(self):
|
||||||
|
postgres_pillar_beacon._write_watermark(42)
|
||||||
|
self.assertEqual(postgres_pillar_beacon._read_watermark(), 42)
|
||||||
|
|
||||||
|
def test_read_watermark_missing_file_returns_none(self):
|
||||||
|
# tmp watermark file was never created
|
||||||
|
self.assertIsNone(postgres_pillar_beacon._read_watermark())
|
||||||
|
|
||||||
|
def test_read_watermark_garbage_returns_none(self):
|
||||||
|
os.makedirs(os.path.dirname(self.watermark), exist_ok=True)
|
||||||
|
with open(self.watermark, 'w') as f:
|
||||||
|
f.write('nope')
|
||||||
|
self.assertIsNone(postgres_pillar_beacon._read_watermark())
|
||||||
|
|
||||||
|
# -- _write_watermark -------------------------------------------------
|
||||||
|
|
||||||
|
def test_write_watermark_round_trip(self):
|
||||||
|
postgres_pillar_beacon._write_watermark(7)
|
||||||
|
with open(self.watermark) as f:
|
||||||
|
self.assertEqual(f.read(), '7')
|
||||||
|
|
||||||
|
def test_write_watermark_swallows_oserror(self):
|
||||||
|
with patch.object(postgres_pillar_beacon.os, 'makedirs', side_effect=OSError):
|
||||||
|
# Must not raise; failure is logged and the beacon retries next pass.
|
||||||
|
postgres_pillar_beacon._write_watermark(5)
|
||||||
|
self.assertFalse(os.path.exists(self.watermark))
|
||||||
|
|
||||||
|
# -- _query -----------------------------------------------------------
|
||||||
|
|
||||||
|
def test_query_success_returns_stdout_and_builds_argv(self):
|
||||||
|
completed = subprocess.CompletedProcess(args=[], returncode=0, stdout='rows', stderr='')
|
||||||
|
with patch.object(postgres_pillar_beacon.subprocess, 'run', return_value=completed) as mock_run:
|
||||||
|
result = postgres_pillar_beacon._query('SELECT 1;')
|
||||||
|
self.assertEqual(result, 'rows')
|
||||||
|
argv = mock_run.call_args[0][0]
|
||||||
|
self.assertEqual(argv[:5], ['docker', 'exec', 'so-postgres', 'psql', '-U'])
|
||||||
|
self.assertIn('SELECT 1;', argv)
|
||||||
|
self.assertFalse(mock_run.call_args[1].get('shell', False))
|
||||||
|
|
||||||
|
def test_query_timeout_returns_none(self):
|
||||||
|
with patch.object(postgres_pillar_beacon.subprocess, 'run',
|
||||||
|
side_effect=subprocess.TimeoutExpired(cmd='psql', timeout=30)):
|
||||||
|
self.assertIsNone(postgres_pillar_beacon._query('SELECT 1;'))
|
||||||
|
|
||||||
|
def test_query_generic_exception_returns_none(self):
|
||||||
|
with patch.object(postgres_pillar_beacon.subprocess, 'run', side_effect=Exception('boom')):
|
||||||
|
self.assertIsNone(postgres_pillar_beacon._query('SELECT 1;'))
|
||||||
|
|
||||||
|
def test_query_nonzero_returncode_returns_none(self):
|
||||||
|
completed = subprocess.CompletedProcess(args=[], returncode=1, stdout='', stderr='bad')
|
||||||
|
with patch.object(postgres_pillar_beacon.subprocess, 'run', return_value=completed):
|
||||||
|
self.assertIsNone(postgres_pillar_beacon._query('SELECT 1;'))
|
||||||
|
|
||||||
|
# -- beacon: first run / seeding --------------------------------------
|
||||||
|
|
||||||
|
def test_beacon_seeds_when_postgres_not_ready(self):
|
||||||
|
with patch.object(postgres_pillar_beacon, '_read_watermark', return_value=None), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_query', return_value=None), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_write_watermark') as mock_write:
|
||||||
|
self.assertEqual(postgres_pillar_beacon.beacon({}), [])
|
||||||
|
mock_write.assert_not_called()
|
||||||
|
|
||||||
|
def test_beacon_seeds_to_max_id_and_emits_nothing(self):
|
||||||
|
with patch.object(postgres_pillar_beacon, '_read_watermark', return_value=None), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_query', return_value='7\n'), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_write_watermark') as mock_write:
|
||||||
|
self.assertEqual(postgres_pillar_beacon.beacon({}), [])
|
||||||
|
mock_write.assert_called_once_with(7)
|
||||||
|
|
||||||
|
def test_beacon_seed_unparseable_is_swallowed(self):
|
||||||
|
with patch.object(postgres_pillar_beacon, '_read_watermark', return_value=None), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_query', return_value='abc'), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_write_watermark') as mock_write:
|
||||||
|
self.assertEqual(postgres_pillar_beacon.beacon({}), [])
|
||||||
|
mock_write.assert_not_called()
|
||||||
|
|
||||||
|
# -- beacon: steady state ---------------------------------------------
|
||||||
|
|
||||||
|
def test_beacon_query_failure_returns_empty(self):
|
||||||
|
with patch.object(postgres_pillar_beacon, '_read_watermark', return_value=10), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_query', return_value=None), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_write_watermark') as mock_write:
|
||||||
|
self.assertEqual(postgres_pillar_beacon.beacon({}), [])
|
||||||
|
mock_write.assert_not_called()
|
||||||
|
|
||||||
|
def test_beacon_emits_events_and_advances_watermark(self):
|
||||||
|
sep = postgres_pillar_beacon.FIELD_SEP
|
||||||
|
rows = '11%s5%snode1\n12%s6%s\n' % (sep, sep, sep, sep)
|
||||||
|
with patch.object(postgres_pillar_beacon, '_read_watermark', return_value=10), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_query', return_value=rows), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_write_watermark') as mock_write:
|
||||||
|
result = postgres_pillar_beacon.beacon({})
|
||||||
|
self.assertEqual(result, [
|
||||||
|
{'tag': 'audit_settings', 'id': 11, 'setting_id': '5', 'node_id': 'node1'},
|
||||||
|
{'tag': 'audit_settings', 'id': 12, 'setting_id': '6', 'node_id': ''},
|
||||||
|
])
|
||||||
|
mock_write.assert_called_once_with(12)
|
||||||
|
|
||||||
|
def test_beacon_skips_malformed_blank_and_noninteger_rows(self):
|
||||||
|
sep = postgres_pillar_beacon.FIELD_SEP
|
||||||
|
rows = (
|
||||||
|
'\n' # blank line -> skipped
|
||||||
|
'13%s7\n' # too few fields -> skipped
|
||||||
|
'abc%s8%snodeX\n' # non-integer id -> skipped
|
||||||
|
'14%s9%snodeY\n' # the one good row
|
||||||
|
) % (sep, sep, sep, sep, sep)
|
||||||
|
with patch.object(postgres_pillar_beacon, '_read_watermark', return_value=10), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_query', return_value=rows), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_write_watermark') as mock_write:
|
||||||
|
result = postgres_pillar_beacon.beacon({})
|
||||||
|
self.assertEqual(result, [
|
||||||
|
{'tag': 'audit_settings', 'id': 14, 'setting_id': '9', 'node_id': 'nodeY'},
|
||||||
|
])
|
||||||
|
mock_write.assert_called_once_with(14)
|
||||||
|
|
||||||
|
def test_beacon_no_new_rows_does_not_advance_watermark(self):
|
||||||
|
with patch.object(postgres_pillar_beacon, '_read_watermark', return_value=10), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_query', return_value=''), \
|
||||||
|
patch.object(postgres_pillar_beacon, '_write_watermark') as mock_write:
|
||||||
|
self.assertEqual(postgres_pillar_beacon.beacon({}), [])
|
||||||
|
mock_write.assert_not_called()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
+18
-19
@@ -3,31 +3,30 @@ import logging
|
|||||||
|
|
||||||
def status():
|
def status():
|
||||||
|
|
||||||
cmd = "runuser -l zeek -c '/opt/zeek/bin/zeekctl status'"
|
cmd = "runuser -l zeek -c '/opt/zeek/bin/zeekctl status'"
|
||||||
retval = __salt__['docker.run']('so-zeek', cmd)
|
retval = __salt__['docker.run']('so-zeek', cmd) # noqa: F821
|
||||||
logging.info('zeekctl_module: zeekctl.status retval: %s' % retval)
|
logging.info('zeekctl_module: zeekctl.status retval: %s' % retval)
|
||||||
|
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
def beacon(config):
|
def beacon(config):
|
||||||
|
|
||||||
retval = []
|
retval = []
|
||||||
|
|
||||||
is_enabled = __salt__['healthcheck.is_enabled']()
|
is_enabled = __salt__['healthcheck.is_enabled']() # noqa: F821
|
||||||
logging.info('zeek_beacon: healthcheck_is_enabled: %s' % is_enabled)
|
logging.info('zeek_beacon: healthcheck_is_enabled: %s' % is_enabled)
|
||||||
|
|
||||||
if is_enabled:
|
if is_enabled:
|
||||||
zeekstatus = status().lower().split(' ')
|
zeekstatus = status().lower().split(' ')
|
||||||
logging.info('zeek_beacon: zeekctl.status: %s' % str(zeekstatus))
|
logging.info('zeek_beacon: zeekctl.status: %s' % str(zeekstatus))
|
||||||
if 'stopped' in zeekstatus or 'crashed' in zeekstatus or 'error' in zeekstatus or 'error:' in zeekstatus:
|
if 'stopped' in zeekstatus or 'crashed' in zeekstatus or 'error' in zeekstatus or 'error:' in zeekstatus:
|
||||||
zeek_restart = True
|
zeek_restart = True
|
||||||
else:
|
else:
|
||||||
zeek_restart = False
|
zeek_restart = False
|
||||||
|
|
||||||
__salt__['telegraf.send']('healthcheck zeek_restart=%s' % str(zeek_restart))
|
__salt__['telegraf.send']('healthcheck zeek_restart=%s' % str(zeek_restart)) # noqa: F821
|
||||||
retval.append({'zeek_restart': zeek_restart})
|
retval.append({'zeek_restart': zeek_restart})
|
||||||
logging.info('zeek_beacon: retval: %s' % str(retval))
|
logging.info('zeek_beacon: retval: %s' % str(retval))
|
||||||
|
|
||||||
return retval
|
|
||||||
|
|
||||||
|
return retval
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
import zeek
|
||||||
|
|
||||||
|
ZEEKCTL_CMD = "runuser -l zeek -c '/opt/zeek/bin/zeekctl status'"
|
||||||
|
|
||||||
|
|
||||||
|
class TestZeekBeacon(unittest.TestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
# zeek.py relies on the __salt__ dunder that Salt injects at load time.
|
||||||
|
# Nothing defines it under test, so we attach a dict of mock loader
|
||||||
|
# functions to the module and remove it again afterwards.
|
||||||
|
self.salt = {
|
||||||
|
'docker.run': MagicMock(return_value='Zeek is running'),
|
||||||
|
'healthcheck.is_enabled': MagicMock(return_value=True),
|
||||||
|
'telegraf.send': MagicMock(),
|
||||||
|
}
|
||||||
|
zeek.__salt__ = self.salt
|
||||||
|
self.addCleanup(lambda: delattr(zeek, '__salt__'))
|
||||||
|
|
||||||
|
# -- status -----------------------------------------------------------
|
||||||
|
|
||||||
|
def test_status_runs_zeekctl_and_returns_output(self):
|
||||||
|
self.salt['docker.run'].return_value = 'Zeek is running'
|
||||||
|
result = zeek.status()
|
||||||
|
self.assertEqual(result, 'Zeek is running')
|
||||||
|
self.salt['docker.run'].assert_called_once_with('so-zeek', ZEEKCTL_CMD)
|
||||||
|
|
||||||
|
# -- beacon -----------------------------------------------------------
|
||||||
|
|
||||||
|
def test_beacon_disabled_returns_empty_and_skips_telegraf(self):
|
||||||
|
self.salt['healthcheck.is_enabled'].return_value = False
|
||||||
|
self.assertEqual(zeek.beacon({}), [])
|
||||||
|
self.salt['telegraf.send'].assert_not_called()
|
||||||
|
|
||||||
|
def test_beacon_running_reports_no_restart(self):
|
||||||
|
self.salt['docker.run'].return_value = 'Zeek is running'
|
||||||
|
self.assertEqual(zeek.beacon({}), [{'zeek_restart': False}])
|
||||||
|
self.salt['telegraf.send'].assert_called_once_with('healthcheck zeek_restart=False')
|
||||||
|
|
||||||
|
def test_beacon_unhealthy_status_triggers_restart(self):
|
||||||
|
# Each of these status tokens should flag a restart (the or-chain in beacon).
|
||||||
|
for status_text in ('Zeek is stopped', 'Zeek crashed', 'Zeek error state', 'Zeek error:'):
|
||||||
|
with self.subTest(status=status_text):
|
||||||
|
self.salt['docker.run'].return_value = status_text
|
||||||
|
self.salt['telegraf.send'].reset_mock()
|
||||||
|
self.assertEqual(zeek.beacon({}), [{'zeek_restart': True}])
|
||||||
|
self.salt['telegraf.send'].assert_called_once_with('healthcheck zeek_restart=True')
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
@@ -29,19 +29,23 @@
|
|||||||
'manager',
|
'manager',
|
||||||
'nginx',
|
'nginx',
|
||||||
'influxdb',
|
'influxdb',
|
||||||
|
'postgres',
|
||||||
|
'postgres.auth',
|
||||||
'soc',
|
'soc',
|
||||||
'kratos',
|
'kratos',
|
||||||
'hydra',
|
'hydra',
|
||||||
'elasticfleet',
|
'elasticfleet',
|
||||||
'elastic-fleet-package-registry',
|
'elasticfleet.manager',
|
||||||
'utility'
|
'elasticsearch.cluster',
|
||||||
|
'elastic-fleet-package-registry'
|
||||||
] %}
|
] %}
|
||||||
|
|
||||||
{% set sensor_states = [
|
{% set sensor_states = [
|
||||||
'suricata',
|
'suricata',
|
||||||
'healthcheck',
|
'healthcheck',
|
||||||
'tcpreplay',
|
'tcpreplay',
|
||||||
'zeek'
|
'zeek',
|
||||||
|
'strelka'
|
||||||
] %}
|
] %}
|
||||||
|
|
||||||
{% set kafka_states = [
|
{% set kafka_states = [
|
||||||
@@ -76,33 +80,33 @@
|
|||||||
),
|
),
|
||||||
'so-heavynode': (
|
'so-heavynode': (
|
||||||
sensor_states +
|
sensor_states +
|
||||||
['elasticagent', 'elasticsearch', 'logstash', 'redis', 'nginx']
|
['elasticagent', 'elasticsearch', 'elasticsearch.cluster', 'logstash', 'redis', 'nginx']
|
||||||
),
|
),
|
||||||
'so-idh': (
|
'so-idh': (
|
||||||
['idh']
|
['idh']
|
||||||
),
|
),
|
||||||
'so-import': (
|
'so-import': (
|
||||||
manager_states +
|
manager_states +
|
||||||
sensor_states | reject('equalto', 'healthcheck') | list +
|
sensor_states | reject('equalto', 'strelka') | reject('equalto', 'healthcheck') | list +
|
||||||
['elasticsearch', 'elasticsearch.auth', 'kibana', 'kibana.secrets', 'logstash.ssl']
|
['elasticsearch', 'elasticsearch.auth', 'kibana', 'kibana.secrets', 'logstash.ssl', 'strelka.manager']
|
||||||
),
|
),
|
||||||
'so-manager': (
|
'so-manager': (
|
||||||
manager_states +
|
manager_states +
|
||||||
['salt.cloud', 'libvirt.packages', 'libvirt.ssh.users'] +
|
['salt.cloud', 'libvirt.packages', 'libvirt.ssh.users', 'strelka.manager'] +
|
||||||
stig_states +
|
stig_states +
|
||||||
kafka_states +
|
kafka_states +
|
||||||
elastic_stack_states
|
elastic_stack_states
|
||||||
),
|
),
|
||||||
'so-managerhype': (
|
'so-managerhype': (
|
||||||
manager_states +
|
manager_states +
|
||||||
['salt.cloud', 'hypervisor', 'libvirt'] +
|
['salt.cloud', 'strelka.manager', 'hypervisor', 'libvirt'] +
|
||||||
stig_states +
|
stig_states +
|
||||||
kafka_states +
|
kafka_states +
|
||||||
elastic_stack_states
|
elastic_stack_states
|
||||||
),
|
),
|
||||||
'so-managersearch': (
|
'so-managersearch': (
|
||||||
manager_states +
|
manager_states +
|
||||||
['salt.cloud', 'libvirt.packages', 'libvirt.ssh.users'] +
|
['salt.cloud', 'libvirt.packages', 'libvirt.ssh.users', 'strelka.manager'] +
|
||||||
stig_states +
|
stig_states +
|
||||||
kafka_states +
|
kafka_states +
|
||||||
elastic_stack_states
|
elastic_stack_states
|
||||||
|
|||||||
@@ -32,3 +32,4 @@ so_config_backup:
|
|||||||
- daymonth: '*'
|
- daymonth: '*'
|
||||||
- month: '*'
|
- month: '*'
|
||||||
- dayweek: '*'
|
- dayweek: '*'
|
||||||
|
|
||||||
@@ -25,9 +25,11 @@ if [ ! -f $BACKUPFILE ]; then
|
|||||||
# Create empty backup file
|
# Create empty backup file
|
||||||
tar -cf $BACKUPFILE -T /dev/null
|
tar -cf $BACKUPFILE -T /dev/null
|
||||||
|
|
||||||
# Loop through all paths defined in global.sls, and append them to backup file
|
# Loop through all paths defined in global.sls, and append them to backup file if they exist
|
||||||
{%- for LOCATION in BACKUPLOCATIONS %}
|
{%- for LOCATION in BACKUPLOCATIONS %}
|
||||||
tar -rf $BACKUPFILE "${EXCLUSIONS[@]}" {{ LOCATION }}
|
if [[ -d {{ LOCATION }} || -f {{ LOCATION }} ]]; then
|
||||||
|
tar -rf $BACKUPFILE "${EXCLUSIONS[@]}" {{ LOCATION }}
|
||||||
|
fi
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
|
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -54,6 +54,20 @@ x509_signing_policies:
|
|||||||
- extendedKeyUsage: serverAuth
|
- extendedKeyUsage: serverAuth
|
||||||
- days_valid: 820
|
- days_valid: 820
|
||||||
- copypath: /etc/pki/issued_certs/
|
- copypath: /etc/pki/issued_certs/
|
||||||
|
postgres:
|
||||||
|
- minions: '*'
|
||||||
|
- signing_private_key: /etc/pki/ca.key
|
||||||
|
- signing_cert: /etc/pki/ca.crt
|
||||||
|
- C: US
|
||||||
|
- ST: Utah
|
||||||
|
- L: Salt Lake City
|
||||||
|
- basicConstraints: "critical CA:false"
|
||||||
|
- keyUsage: "critical keyEncipherment"
|
||||||
|
- subjectKeyIdentifier: hash
|
||||||
|
- authorityKeyIdentifier: keyid,issuer:always
|
||||||
|
- extendedKeyUsage: serverAuth
|
||||||
|
- days_valid: 820
|
||||||
|
- copypath: /etc/pki/issued_certs/
|
||||||
elasticfleet:
|
elasticfleet:
|
||||||
- minions: '*'
|
- minions: '*'
|
||||||
- signing_private_key: /etc/pki/ca.key
|
- signing_private_key: /etc/pki/ca.key
|
||||||
|
|||||||
+45
-5
@@ -117,25 +117,61 @@ elastic_curl_config:
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
|
||||||
|
# A non-root owner here can chmod the directory and replace any script in it, including
|
||||||
|
# the root-owned ones. 555 is the mode the filesystem RPM ships; root ignores it anyway.
|
||||||
|
usr_sbin_perms:
|
||||||
|
file.directory:
|
||||||
|
- name: /usr/sbin
|
||||||
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 555
|
||||||
|
|
||||||
common_sbin:
|
common_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://common/tools/sbin
|
- source: salt://common/tools/sbin
|
||||||
- user: 939
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
- show_changes: False
|
- show_changes: False
|
||||||
|
- require:
|
||||||
|
- file: usr_sbin_perms
|
||||||
{% if GLOBALS.role == 'so-heavynode' %}
|
{% if GLOBALS.role == 'so-heavynode' %}
|
||||||
- exclude_pat:
|
- exclude_pat:
|
||||||
- so-pcap-import
|
- so-pcap-import
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
# Pin physical NIC names by MAC (run-once) so a kernel upgrade can't renumber the
|
||||||
|
# interfaces SO binds by name. The marker keeps it a one-time setup; an admin can
|
||||||
|
# pre-create the marker to opt out.
|
||||||
|
pin_nic_names:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-nic-pin
|
||||||
|
- unless: 'test -e /opt/so/state/nic_names_pinned'
|
||||||
|
- require:
|
||||||
|
- file: common_sbin
|
||||||
|
- file: statedir
|
||||||
|
|
||||||
|
# Once a node is actually running UEK8, the stock EL9 (RHCK) kernel packages are dead weight.
|
||||||
|
# They can't be removed any earlier -- dnf protects the running kernel -- so the cleanup waits
|
||||||
|
# for the reboot, which makes the highstate the natural place to catch it: fresh installs
|
||||||
|
# reboot at the end of setup, and upgraded nodes reboot whenever the admin schedules it.
|
||||||
|
# so-kernel-upgrade --cleanup checks rpm before touching dnf, so this costs an rpm query on
|
||||||
|
# every highstate after the first pass. The package list lives in the script only, so there
|
||||||
|
# is nothing here to drift out of sync with it.
|
||||||
|
remove_stock_kernel:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-kernel-upgrade --cleanup
|
||||||
|
- onlyif: 'uname -r | grep -qE "^6\.[0-9]+.*uek"'
|
||||||
|
- require:
|
||||||
|
- file: common_sbin
|
||||||
|
|
||||||
common_sbin_jinja:
|
common_sbin_jinja:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://common/tools/sbin_jinja
|
- source: salt://common/tools/sbin_jinja
|
||||||
- user: 939
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
- template: jinja
|
- template: jinja
|
||||||
- show_changes: False
|
- show_changes: False
|
||||||
@@ -148,6 +184,8 @@ so-status_script:
|
|||||||
file.managed:
|
file.managed:
|
||||||
- name: /usr/sbin/so-status
|
- name: /usr/sbin/so-status
|
||||||
- source: salt://common/tools/sbin/so-status
|
- source: salt://common/tools/sbin/so-status
|
||||||
|
- user: root
|
||||||
|
- group: root
|
||||||
- mode: 755
|
- mode: 755
|
||||||
|
|
||||||
{% if GLOBALS.is_sensor %}
|
{% if GLOBALS.is_sensor %}
|
||||||
@@ -179,9 +217,11 @@ sostatus_log:
|
|||||||
- replace: False
|
- replace: False
|
||||||
|
|
||||||
# Install sostatus check cron. This is used to populate Grid.
|
# Install sostatus check cron. This is used to populate Grid.
|
||||||
|
# telegraf reads status.log on the same minute boundary this runs, so write aside and rename
|
||||||
|
# rather than truncating the file it is reading
|
||||||
so-status_check_cron:
|
so-status_check_cron:
|
||||||
cron.present:
|
cron.present:
|
||||||
- name: '/usr/sbin/so-status -j > /opt/so/log/sostatus/status.log 2>&1'
|
- name: '/usr/sbin/so-status -j > /opt/so/log/sostatus/status.log.tmp 2>&1; mv -f /opt/so/log/sostatus/status.log.tmp /opt/so/log/sostatus/status.log'
|
||||||
- identifier: so-status_check_cron
|
- identifier: so-status_check_cron
|
||||||
- user: root
|
- user: root
|
||||||
- minute: '*/1'
|
- minute: '*/1'
|
||||||
|
|||||||
@@ -18,47 +18,61 @@ copy_so-common_common_tools_sbin:
|
|||||||
- name: /opt/so/saltstack/default/salt/common/tools/sbin/so-common
|
- name: /opt/so/saltstack/default/salt/common/tools/sbin/so-common
|
||||||
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-common
|
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-common
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-image-common_common_tools_sbin:
|
copy_so-image-common_common_tools_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /opt/so/saltstack/default/salt/common/tools/sbin/so-image-common
|
- name: /opt/so/saltstack/default/salt/common/tools/sbin/so-image-common
|
||||||
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-image-common
|
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-image-common
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_soup_manager_tools_sbin:
|
copy_soup_manager_tools_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/soup
|
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/soup
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/soup
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/soup
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-firewall_manager_tools_sbin:
|
copy_so-firewall_manager_tools_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/so-firewall
|
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/so-firewall
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-firewall
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-firewall
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-yaml_manager_tools_sbin:
|
copy_so-yaml_manager_tools_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/so-yaml.py
|
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/so-yaml.py
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-yaml.py
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-yaml.py
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-repo-sync_manager_tools_sbin:
|
copy_so-repo-sync_manager_tools_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/so-repo-sync
|
- name: /opt/so/saltstack/default/salt/manager/tools/sbin/so-repo-sync
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-repo-sync
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-repo-sync
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_bootstrap-salt_manager_tools_sbin:
|
copy_bootstrap-salt_manager_tools_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /opt/so/saltstack/default/salt/salt/scripts/bootstrap-salt.sh
|
- name: /opt/so/saltstack/default/salt/salt/scripts/bootstrap-salt.sh
|
||||||
- source: {{UPDATE_DIR}}/salt/salt/scripts/bootstrap-salt.sh
|
- source: {{UPDATE_DIR}}/salt/salt/scripts/bootstrap-salt.sh
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 644
|
||||||
|
|
||||||
# This section is used to put the new script in place so that it can be called during soup.
|
# This section is used to put the new script in place so that it can be called during soup.
|
||||||
# It is faster than calling the states that normally manage them to put them in place.
|
# It is faster than calling the states that normally manage them to put them in place.
|
||||||
@@ -67,46 +81,60 @@ copy_so-common_sbin:
|
|||||||
- name: /usr/sbin/so-common
|
- name: /usr/sbin/so-common
|
||||||
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-common
|
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-common
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-image-common_sbin:
|
copy_so-image-common_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /usr/sbin/so-image-common
|
- name: /usr/sbin/so-image-common
|
||||||
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-image-common
|
- source: {{UPDATE_DIR}}/salt/common/tools/sbin/so-image-common
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_soup_sbin:
|
copy_soup_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /usr/sbin/soup
|
- name: /usr/sbin/soup
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/soup
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/soup
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-firewall_sbin:
|
copy_so-firewall_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /usr/sbin/so-firewall
|
- name: /usr/sbin/so-firewall
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-firewall
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-firewall
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-yaml_sbin:
|
copy_so-yaml_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /usr/sbin/so-yaml.py
|
- name: /usr/sbin/so-yaml.py
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-yaml.py
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-yaml.py
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_so-repo-sync_sbin:
|
copy_so-repo-sync_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /usr/sbin/so-repo-sync
|
- name: /usr/sbin/so-repo-sync
|
||||||
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-repo-sync
|
- source: {{UPDATE_DIR}}/salt/manager/tools/sbin/so-repo-sync
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
|
||||||
copy_bootstrap-salt_sbin:
|
copy_bootstrap-salt_sbin:
|
||||||
file.copy:
|
file.copy:
|
||||||
- name: /usr/sbin/bootstrap-salt.sh
|
- name: /usr/sbin/bootstrap-salt.sh
|
||||||
- source: {{UPDATE_DIR}}/salt/salt/scripts/bootstrap-salt.sh
|
- source: {{UPDATE_DIR}}/salt/salt/scripts/bootstrap-salt.sh
|
||||||
- force: True
|
- force: True
|
||||||
- preserve: True
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
@@ -142,6 +142,11 @@ check_elastic_license() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
check_elasticsearch_responsive() {
|
||||||
|
retry 3 15 "so-elasticsearch-query / --output /dev/null --fail" ||
|
||||||
|
fail "Elasticsearch is not responding. Please review Elasticsearch logs /opt/so/log/elasticsearch/securityonion.log for more details. Additionally, consider running so-elasticsearch-troubleshoot."
|
||||||
|
}
|
||||||
|
|
||||||
check_salt_master_status() {
|
check_salt_master_status() {
|
||||||
local count=0
|
local count=0
|
||||||
local attempts="${1:- 10}"
|
local attempts="${1:- 10}"
|
||||||
@@ -235,7 +240,8 @@ copy_new_files() {
|
|||||||
cd $UPDATE_DIR
|
cd $UPDATE_DIR
|
||||||
rsync -a salt $DEFAULT_SALT_DIR/ --delete "${EXCLUDE_ARGS[@]}"
|
rsync -a salt $DEFAULT_SALT_DIR/ --delete "${EXCLUDE_ARGS[@]}"
|
||||||
rsync -a pillar $DEFAULT_SALT_DIR/ --delete "${EXCLUDE_ARGS[@]}"
|
rsync -a pillar $DEFAULT_SALT_DIR/ --delete "${EXCLUDE_ARGS[@]}"
|
||||||
chown -R socore:socore $DEFAULT_SALT_DIR/
|
# Root-executed code; SOC only needs to read it. Local dirs stay socore-owned.
|
||||||
|
chown -R root:root $DEFAULT_SALT_DIR/
|
||||||
cd /tmp
|
cd /tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +292,20 @@ download_and_verify() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# check if container with name is running and optionally stop it
|
||||||
|
docker_check_running() {
|
||||||
|
# show running containers, only names
|
||||||
|
if docker ps --format '{{.Names}}' | grep -q "^so-${1}$"; then
|
||||||
|
if [[ "$2" == "--stop" ]]; then
|
||||||
|
docker stop "so-${1}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
elastic_license() {
|
elastic_license() {
|
||||||
|
|
||||||
read -r -d '' message <<- EOM
|
read -r -d '' message <<- EOM
|
||||||
@@ -399,6 +419,19 @@ is_single_node_grid() {
|
|||||||
grep "role: so-" /etc/salt/grains | grep -E "eval|standalone|import" &> /dev/null
|
grep "role: so-" /etc/salt/grains | grep -E "eval|standalone|import" &> /dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remove_elasticsearch_index_template() {
|
||||||
|
local template_name=$1
|
||||||
|
local reason=${2:-"Removing index template for upgrade"}
|
||||||
|
|
||||||
|
# check if template exists
|
||||||
|
if so-elasticsearch-query "_index_template/$template_name" --fail --retry 3 --retry-delay 5 >/dev/null 2>&1; then
|
||||||
|
echo "Removing Elasticsearch index template: $template_name ($reason)"
|
||||||
|
if ! so-elasticsearch-query "_index_template/$template_name" -XDELETE --fail --retry 3 --retry-delay 5; then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
initialize_elasticsearch_indices() {
|
initialize_elasticsearch_indices() {
|
||||||
local index_names=$1
|
local index_names=$1
|
||||||
local default_entry=${2:-'{"@timestamp":"0"}'}
|
local default_entry=${2:-'{"@timestamp":"0"}'}
|
||||||
@@ -583,42 +616,6 @@ run_check_net_err() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_salt_minion() {
|
|
||||||
local minion="$1"
|
|
||||||
local max_wait="${2:-30}"
|
|
||||||
local interval="${3:-2}"
|
|
||||||
local logfile="${4:-'/dev/stdout'}"
|
|
||||||
local elapsed=0
|
|
||||||
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - Waiting for salt-minion '$minion' to be ready..."
|
|
||||||
|
|
||||||
while [ $elapsed -lt $max_wait ]; do
|
|
||||||
# Check if service is running
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - Check if salt-minion service is running"
|
|
||||||
if ! systemctl is-active --quiet salt-minion; then
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - salt-minion service not running (elapsed: ${elapsed}s)"
|
|
||||||
sleep $interval
|
|
||||||
elapsed=$((elapsed + interval))
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - salt-minion service is running"
|
|
||||||
|
|
||||||
# Check if minion responds to ping
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - Check if $minion responds to ping"
|
|
||||||
if salt "$minion" test.ping --timeout=3 --out=json 2>> "$logfile" | grep -q "true"; then
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - salt-minion '$minion' is connected and ready!"
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - Waiting... (${elapsed}s / ${max_wait}s)"
|
|
||||||
sleep $interval
|
|
||||||
elapsed=$((elapsed + interval))
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "$(date '+%a %d %b %Y %H:%M:%S.%6N') - ERROR: salt-minion '$minion' not ready after $max_wait seconds"
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
salt_minion_count() {
|
salt_minion_count() {
|
||||||
local MINIONDIR="/opt/so/saltstack/local/pillar/minions"
|
local MINIONDIR="/opt/so/saltstack/local/pillar/minions"
|
||||||
MINIONCOUNT=$(ls -la $MINIONDIR/*.sls | grep -v adv_ | wc -l)
|
MINIONCOUNT=$(ls -la $MINIONDIR/*.sls | grep -v adv_ | wc -l)
|
||||||
@@ -683,7 +680,7 @@ systemctl_func() {
|
|||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "${echo_action^}ing $service_name service at $(date +"%T.%6N")"
|
echo "${echo_action^}ing $service_name service at $(date +"%T.%6N")"
|
||||||
systemctl $action $service_name && echo "Successfully ${echo_action}ed $service_name." || echo "Failed to $action $service_name."
|
systemctl $action $service_name && echo "Successfully ${echo_action}ed $service_name at $(date +"%T.%6N")." || echo "Failed to $action $service_name at $(date +"%T.%6N")."
|
||||||
echo ""
|
echo ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import sys
|
|||||||
import subprocess
|
import subprocess
|
||||||
import os
|
import os
|
||||||
import json
|
import json
|
||||||
|
import tempfile
|
||||||
|
|
||||||
sys.path.append('/opt/saltstack/salt/lib/python3.10/site-packages/')
|
sys.path.append('/opt/saltstack/salt/lib/python3.10/site-packages/')
|
||||||
import salt.config
|
import salt.config
|
||||||
@@ -17,6 +18,21 @@ import salt.loader
|
|||||||
__opts__ = salt.config.minion_config('/etc/salt/minion')
|
__opts__ = salt.config.minion_config('/etc/salt/minion')
|
||||||
__grains__ = salt.loader.grains(__opts__)
|
__grains__ = salt.loader.grains(__opts__)
|
||||||
|
|
||||||
|
def write_atomic(path, value):
|
||||||
|
# telegraf reads these files on its own schedule; replacing them by rename means it never
|
||||||
|
# reads a truncated file and reports an empty value as if it were real
|
||||||
|
directory = os.path.dirname(path)
|
||||||
|
handle, temp = tempfile.mkstemp(dir=directory)
|
||||||
|
try:
|
||||||
|
with os.fdopen(handle, 'w') as f:
|
||||||
|
f.write(str(value))
|
||||||
|
os.chmod(temp, 0o644)
|
||||||
|
os.replace(temp, path)
|
||||||
|
except Exception:
|
||||||
|
os.path.exists(temp) and os.unlink(temp)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
def check_needs_restarted():
|
def check_needs_restarted():
|
||||||
osfam = __grains__['os_family']
|
osfam = __grains__['os_family']
|
||||||
val = '0'
|
val = '0'
|
||||||
@@ -34,8 +50,7 @@ def check_needs_restarted():
|
|||||||
else:
|
else:
|
||||||
fail("Unsupported OS")
|
fail("Unsupported OS")
|
||||||
|
|
||||||
with open(outfile, 'w') as f:
|
write_atomic(outfile, val)
|
||||||
f.write(val)
|
|
||||||
|
|
||||||
def check_for_fps():
|
def check_for_fps():
|
||||||
feat = 'fps'
|
feat = 'fps'
|
||||||
@@ -56,8 +71,7 @@ def check_for_fps():
|
|||||||
# Unknown, so assume 0
|
# Unknown, so assume 0
|
||||||
fps = 0
|
fps = 0
|
||||||
|
|
||||||
with open('/opt/so/log/sostatus/fps_enabled', 'w') as f:
|
write_atomic('/opt/so/log/sostatus/fps_enabled', fps)
|
||||||
f.write(str(fps))
|
|
||||||
|
|
||||||
def check_for_lks():
|
def check_for_lks():
|
||||||
feat = 'Lks'
|
feat = 'Lks'
|
||||||
@@ -80,8 +94,7 @@ def check_for_lks():
|
|||||||
lks = 1
|
lks = 1
|
||||||
if lks:
|
if lks:
|
||||||
break
|
break
|
||||||
with open('/opt/so/log/sostatus/lks_enabled', 'w') as f:
|
write_atomic('/opt/so/log/sostatus/lks_enabled', lks)
|
||||||
f.write(str(lks))
|
|
||||||
|
|
||||||
def fail(msg):
|
def fail(msg):
|
||||||
print(msg, file=sys.stderr)
|
print(msg, file=sys.stderr)
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ container_list() {
|
|||||||
"so-hydra"
|
"so-hydra"
|
||||||
"so-nginx"
|
"so-nginx"
|
||||||
"so-pcaptools"
|
"so-pcaptools"
|
||||||
|
"so-postgres"
|
||||||
"so-soc"
|
"so-soc"
|
||||||
"so-suricata"
|
"so-suricata"
|
||||||
"so-telegraf"
|
"so-telegraf"
|
||||||
@@ -55,8 +56,11 @@ container_list() {
|
|||||||
"so-logstash"
|
"so-logstash"
|
||||||
"so-nginx"
|
"so-nginx"
|
||||||
"so-pcaptools"
|
"so-pcaptools"
|
||||||
|
"so-postgres"
|
||||||
"so-redis"
|
"so-redis"
|
||||||
"so-soc"
|
"so-soc"
|
||||||
|
"so-strelka-backend"
|
||||||
|
"so-strelka-manager"
|
||||||
"so-suricata"
|
"so-suricata"
|
||||||
"so-telegraf"
|
"so-telegraf"
|
||||||
"so-zeek"
|
"so-zeek"
|
||||||
@@ -184,8 +188,27 @@ update_docker_containers() {
|
|||||||
if [ -z "$HOSTNAME" ]; then
|
if [ -z "$HOSTNAME" ]; then
|
||||||
HOSTNAME=$(hostname)
|
HOSTNAME=$(hostname)
|
||||||
fi
|
fi
|
||||||
docker tag $CONTAINER_REGISTRY/$IMAGEREPO/$image $HOSTNAME:5000/$IMAGEREPO/$image >> "$LOG_FILE" 2>&1
|
docker tag $CONTAINER_REGISTRY/$IMAGEREPO/$image $HOSTNAME:5000/$IMAGEREPO/$image >> "$LOG_FILE" 2>&1 || {
|
||||||
docker push $HOSTNAME:5000/$IMAGEREPO/$image >> "$LOG_FILE" 2>&1
|
echo "Unable to tag $image" >> "$LOG_FILE" 2>&1
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
# Push to the embedded registry via a registry-to-registry copy. Avoids
|
||||||
|
# `docker push`, which on Docker 29.x with the containerd image store
|
||||||
|
# represents freshly-pulled images as an index whose layer content
|
||||||
|
# isn't reachable through the push path. The local `docker tag` above
|
||||||
|
# is preserved so so-image-pull's `:5000` existence check still works.
|
||||||
|
# Pin to the digest already gpg-verified above so we copy exactly the
|
||||||
|
# bytes we approved.
|
||||||
|
local VERIFIED_REF
|
||||||
|
VERIFIED_REF=$(echo "$DOCKERINSPECT" | jq -r ".[0].RepoDigests[] | select(. | contains(\"$CONTAINER_REGISTRY\"))" | head -n 1)
|
||||||
|
if [ -z "$VERIFIED_REF" ] || [ "$VERIFIED_REF" = "null" ]; then
|
||||||
|
echo "Unable to determine verified digest for $image" >> "$LOG_FILE" 2>&1
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
docker buildx imagetools create --tag $HOSTNAME:5000/$IMAGEREPO/$image "$VERIFIED_REF" >> "$LOG_FILE" 2>&1 || {
|
||||||
|
echo "Unable to copy $image to embedded registry" >> "$LOG_FILE" 2>&1
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "There is a problem downloading the $image image. Details: " >> "$LOG_FILE" 2>&1
|
echo "There is a problem downloading the $image image. Details: " >> "$LOG_FILE" 2>&1
|
||||||
|
|||||||
Executable
+317
@@ -0,0 +1,317 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
#
|
||||||
|
# so-kernel-upgrade — install the UEK8 (6.x) kernel, make it the boot default, and once the
|
||||||
|
# node is running it, remove the stock EL9 kernel.
|
||||||
|
#
|
||||||
|
# Security Onion is moving off the EL9 stock kernel (RHCK, 5.14) and UEK7 (5.15) onto UEK8
|
||||||
|
# (6.x). Four things have to happen, and the tool has to drive each one:
|
||||||
|
#
|
||||||
|
# 1. Populate. The manager mirrors the UEK8 packages into /nsm/kernelrepo via so-repo-sync,
|
||||||
|
# and serves them to the grid over https://<manager>/kernelrepo. Until that sync runs the
|
||||||
|
# repo is valid but EMPTY -- dnf resolves it happily and installs nothing, with no error.
|
||||||
|
# 2. Install. A node on RHCK has no kernel-uek* package at all, so there is nothing for
|
||||||
|
# 'dnf update' to upgrade. A node on UEK7 does have kernel-uek installed, so
|
||||||
|
# 'dnf install kernel-uek' reports "Nothing to do" and exits 0 without installing 6.x.
|
||||||
|
# Both cases need an explicit install of the UEK8 NEVRA.
|
||||||
|
# 3. Boot it. Whether a newly installed UEK8 kernel becomes the boot default depends on the
|
||||||
|
# RUNNING kernel's flavor. kernel-install/grubby (with UPDATEDEFAULT=yes) only auto-promote
|
||||||
|
# within the running kernel's flavor lineage:
|
||||||
|
# - From UEK7 (5.x, kernel-uek) the install stays in the kernel-uek lineage and IS
|
||||||
|
# auto-promoted, so no grubby change is needed -- just make sure the repo is populated
|
||||||
|
# and install UEK8.
|
||||||
|
# - From the stock EL9 kernel (RHCK, 5.14, no UEK) it is a flavor CROSS that is NOT
|
||||||
|
# auto-promoted, so the box keeps booting RHCK until grubby is told otherwise.
|
||||||
|
# This tool inspects the running kernel and only runs 'grubby --set-default' for RHCK.
|
||||||
|
# 4. Clean up. Once the node is actually RUNNING UEK8 the stock kernel packages are dead
|
||||||
|
# weight -- disk in /boot and a stale GRUB entry. They cannot come off any earlier:
|
||||||
|
# dnf's protect_running_kernel refuses to erase the booted kernel-core, so the removal
|
||||||
|
# has to wait for the reboot. Waiting is also the safer sequencing on its own terms --
|
||||||
|
# the node has proven it comes up on UEK8 before its fallback is deleted. That is why
|
||||||
|
# the removal does not happen in the uek7 branch either, where dnf would allow it.
|
||||||
|
#
|
||||||
|
# Every one of those failure modes is silent by default. This tool handles each case and fails
|
||||||
|
# loudly when it cannot, rather than reporting success while changing nothing.
|
||||||
|
#
|
||||||
|
# Invocation: with no arguments it drives the whole sequence for whatever kernel the node is
|
||||||
|
# on. With --cleanup it does the step 4 removal ONLY, and no-ops on a node that isn't running
|
||||||
|
# UEK8 yet -- that is the form the common highstate calls (remove_stock_kernel in
|
||||||
|
# salt/common/init.sls) so the cleanup lands grid-wide after each node reboots.
|
||||||
|
#
|
||||||
|
# Manager vs minion: only the manager owns /nsm/kernelrepo, so only the manager can populate
|
||||||
|
# it. If the repo is empty here, a manager runs so-repo-sync itself; a minion has no way to
|
||||||
|
# fix it and exits non-zero telling the admin to sync the manager first.
|
||||||
|
#
|
||||||
|
# Idempotent: an already-installed, already-default UEK8 kernel is left alone. It only sets
|
||||||
|
# the boot default; it does NOT reboot -- the admin reboots the node on their own schedule.
|
||||||
|
|
||||||
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
|
# Client-side repo id (what dnf enables on this node, from repo/client/oracle.sls) vs the
|
||||||
|
# reposync-side section in repodownload.conf that the manager mirrors from (mirrors the
|
||||||
|
# securityonion/securityonionsync split for the main repo).
|
||||||
|
KERNEL_REPO="securityonionkernel"
|
||||||
|
KERNEL_REPO_SYNC="securityonionkernelsync"
|
||||||
|
KERNEL_PKG="kernel-uek"
|
||||||
|
KERNEL_REPO_DIR="/nsm/kernelrepo"
|
||||||
|
REPOSYNC_CONF="/opt/so/conf/reposync/repodownload.conf"
|
||||||
|
GLOBAL_PILLAR="/opt/so/saltstack/local/pillar/global/soc_global.sls"
|
||||||
|
|
||||||
|
# Stock EL9 (RHCK) kernel packages, removed only once the node is running UEK8 (see step 4
|
||||||
|
# in the header). Left deliberately narrow: UEK7 kernel-uek builds age out on their own via
|
||||||
|
# installonly_limit=3, and kernel-devel/kernel-headers are not touched.
|
||||||
|
RHCK_PKGS="kernel kernel-core kernel-modules kernel-modules-core kernel-tools kernel-tools-libs"
|
||||||
|
|
||||||
|
log() { echo "[so-kernel-upgrade] $*"; }
|
||||||
|
die() { echo "[so-kernel-upgrade] ERROR: $*" >&2; exit 1; }
|
||||||
|
|
||||||
|
command -v grubby >/dev/null 2>&1 || die "grubby not found"
|
||||||
|
command -v dnf >/dev/null 2>&1 || die "dnf not found"
|
||||||
|
|
||||||
|
ARCH="$(rpm -E '%{_arch}')"
|
||||||
|
|
||||||
|
is_airgap() {
|
||||||
|
[ -f "$GLOBAL_PILLAR" ] && grep -q 'airgap: *[Tt]rue' "$GLOBAL_PILLAR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Newest installed UEK8 (6.x) kernel known to the bootloader. UEK8 vmlinuz paths look like
|
||||||
|
# /boot/vmlinuz-6.12.0-204.92.4.2.el9uek.x86_64; UEK7 (5.15) and RHCK (5.14) won't match.
|
||||||
|
find_uek8() {
|
||||||
|
grubby --info=ALL 2>/dev/null \
|
||||||
|
| sed -n 's/^kernel="\(.*\)"$/\1/p' \
|
||||||
|
| grep -E '/vmlinuz-6\.[0-9]+.*uek' \
|
||||||
|
| sort -V | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Classify the RUNNING kernel (uname -r) -- this, not what's installed, is what decides whether
|
||||||
|
# a UEK8 install auto-promotes to the boot default:
|
||||||
|
# uek8 6.x UEK already on the target line; nothing to do
|
||||||
|
# uek7 5.x UEK a UEK8 install stays in the kernel-uek lineage and auto-promotes (no grubby)
|
||||||
|
# rhck 5.14 EL9 crossing into the UEK flavor does NOT auto-promote (needs grubby --set-default)
|
||||||
|
running_flavor() {
|
||||||
|
case "$(uname -r)" in
|
||||||
|
6.*uek*) echo uek8 ;;
|
||||||
|
*uek*) echo uek7 ;;
|
||||||
|
*) echo rhck ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
# Newest UEK8 kernel-uek NEVRA offered by the kernel repo, empty if the repo has none.
|
||||||
|
# Restricted to the kernel repo so a UEK7 kernel-uek in the main repo can't be picked up,
|
||||||
|
# and filtered to 6.x so we never "succeed" by reinstalling the 5.15 we already have.
|
||||||
|
uek8_available() {
|
||||||
|
dnf -q repoquery --disablerepo='*' --enablerepo="$KERNEL_REPO" \
|
||||||
|
--arch="$ARCH" --latest-limit=1 \
|
||||||
|
--qf '%{name}-%{evr}.%{arch}\n' "$KERNEL_PKG" 2>/dev/null \
|
||||||
|
| grep -E "^${KERNEL_PKG}-6\." | tail -1
|
||||||
|
}
|
||||||
|
|
||||||
|
kernelrepo_rpm_count() {
|
||||||
|
find "$KERNEL_REPO_DIR" -maxdepth 1 -name '*.rpm' 2>/dev/null | wc -l
|
||||||
|
}
|
||||||
|
|
||||||
|
# The kernel repo starts life as valid-but-empty (kernelrepo_init_empty in
|
||||||
|
# salt/manager/init.sls) and is filled by so-repo-sync. During a soup, so-repo-sync runs
|
||||||
|
# BEFORE the highstate deploys the [securityonionkernelsync] section into repodownload.conf, so
|
||||||
|
# the first kernel-aware soup leaves the repo empty until the next nightly sync.
|
||||||
|
sync_kernel_repo() {
|
||||||
|
if is_airgap; then
|
||||||
|
log "airgap install: $KERNEL_REPO_DIR is populated from the airgap ISO, not by so-repo-sync."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if ! grep -q "^\[${KERNEL_REPO_SYNC}\]" "$REPOSYNC_CONF" 2>/dev/null; then
|
||||||
|
log "$REPOSYNC_CONF has no [${KERNEL_REPO_SYNC}] section -- run a highstate to deploy it."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "populating $KERNEL_REPO_DIR with so-repo-sync (mirrors upstream; can take several minutes)"
|
||||||
|
su socore -c '/usr/sbin/so-repo-sync' || { log "so-repo-sync failed"; return 1; }
|
||||||
|
|
||||||
|
dnf -q clean expire-cache >/dev/null 2>&1
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make the kernel repo actually able to serve a UEK8 package, or fail trying.
|
||||||
|
ensure_kernel_repo() {
|
||||||
|
# The repo is assigned by the repo.client highstate, and only once NICs are pinned by MAC
|
||||||
|
# (/opt/so/state/nic_names_pinned) so the kernel swap can't renumber interfaces SO binds
|
||||||
|
# by name. skip_if_unavailable=1 means a broken repo is silently ignored, so check first.
|
||||||
|
if ! dnf -q repolist --enabled 2>/dev/null | awk '{print $1}' | grep -qx "$KERNEL_REPO"; then
|
||||||
|
log "repo '$KERNEL_REPO' is not enabled on this node."
|
||||||
|
log "Run a highstate first; the repo is skipped until /opt/so/state/nic_names_pinned"
|
||||||
|
log "exists (run so-nic-pin) and this node's salt matches the version this release ships."
|
||||||
|
die "kernel repo unavailable"
|
||||||
|
fi
|
||||||
|
|
||||||
|
[ -n "$(uek8_available)" ] && return 0
|
||||||
|
|
||||||
|
log "repo '$KERNEL_REPO' is enabled but offers no UEK8 $KERNEL_PKG package"
|
||||||
|
|
||||||
|
if ! is_manager_node; then
|
||||||
|
log "This is a minion; it consumes the kernel repo from the manager and cannot populate it."
|
||||||
|
log "On the manager, run: su socore -c /usr/sbin/so-repo-sync"
|
||||||
|
log "then re-run this script here."
|
||||||
|
die "manager's kernel repo is empty"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "this is a manager and $KERNEL_REPO_DIR holds $(kernelrepo_rpm_count) rpm(s)"
|
||||||
|
sync_kernel_repo || die "could not populate $KERNEL_REPO_DIR"
|
||||||
|
|
||||||
|
[ -n "$(uek8_available)" ] \
|
||||||
|
|| die "so-repo-sync completed but $KERNEL_REPO still offers no UEK8 $KERNEL_PKG"
|
||||||
|
}
|
||||||
|
|
||||||
|
reboot_notice() {
|
||||||
|
[ "$(uname -r)" = "$(basename "$1" | sed 's/^vmlinuz-//')" ] && return 0
|
||||||
|
log "REBOOT REQUIRED to start using the UEK8 kernel (currently running $(uname -r))."
|
||||||
|
# The stock kernel can't be removed until it stops being the running one, so say when
|
||||||
|
# that will happen rather than leaving the admin to wonder if it was missed.
|
||||||
|
[ -n "$(rhck_installed)" ] \
|
||||||
|
&& log "The stock EL9 kernel is left in place until then; it is removed by the next highstate after the reboot."
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep future kernel updates on the UEK line rather than falling back to RHCK. Oracle ships
|
||||||
|
# /etc/sysconfig/kernel; only rewrite it when it's actually pointing somewhere else.
|
||||||
|
set_default_kernel_conf() {
|
||||||
|
if [ -f /etc/sysconfig/kernel ] && ! grep -q '^DEFAULTKERNEL=kernel-uek-core$' /etc/sysconfig/kernel; then
|
||||||
|
log "setting DEFAULTKERNEL=kernel-uek-core in /etc/sysconfig/kernel"
|
||||||
|
sed -i 's/^DEFAULTKERNEL=.*/DEFAULTKERNEL=kernel-uek-core/' /etc/sysconfig/kernel
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Which of RHCK_PKGS are actually installed, one per line. rpm -qa treats each argument as a
|
||||||
|
# name glob and prints only what it finds, so a package that was never installed (or is
|
||||||
|
# already gone) simply doesn't appear -- no "not installed" noise and no non-zero exit.
|
||||||
|
rhck_installed() {
|
||||||
|
rpm -qa $RHCK_PKGS 2>/dev/null
|
||||||
|
}
|
||||||
|
|
||||||
|
# Remove the stock EL9 kernel. Only ever called once the running kernel is UEK8. The rpm
|
||||||
|
# check above is the idempotency guard, so this is a cheap no-op on every highstate after
|
||||||
|
# the first one -- it costs an rpm query, not a dnf transaction.
|
||||||
|
remove_rhck() {
|
||||||
|
local installed; installed="$(rhck_installed)"
|
||||||
|
if [ -z "$installed" ]; then
|
||||||
|
log "no stock EL9 (RHCK) kernel packages installed; nothing to remove."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "running UEK8; removing the stock EL9 (RHCK) kernel packages:"
|
||||||
|
echo "$installed" | sed 's/^/[so-kernel-upgrade] /'
|
||||||
|
dnf -y remove $RHCK_PKGS || die "failed to remove the stock EL9 kernel packages"
|
||||||
|
|
||||||
|
installed="$(rhck_installed)"
|
||||||
|
[ -z "$installed" ] || die "dnf reported success but these remain: $(echo $installed)"
|
||||||
|
log "stock EL9 kernel packages removed."
|
||||||
|
}
|
||||||
|
|
||||||
|
# Make sure a UEK8 kernel is installed, leaving its boot entry in INSTALLED_UEK8. If one is
|
||||||
|
# already present we leave the repo alone -- it may be disabled or empty and we don't need it
|
||||||
|
# just to flip the boot default. Otherwise install the explicit NEVRA, not the bare package
|
||||||
|
# name: on a UEK7 node 'dnf install kernel-uek' sees 5.15 already present, prints "Nothing to
|
||||||
|
# do" and exits 0 without installing 6.x.
|
||||||
|
ensure_uek8_installed() {
|
||||||
|
INSTALLED_UEK8="$(find_uek8)"
|
||||||
|
if [ -n "$INSTALLED_UEK8" ]; then
|
||||||
|
log "UEK8 kernel already installed: $INSTALLED_UEK8"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
ensure_kernel_repo
|
||||||
|
local nevra; nevra="$(uek8_available)"
|
||||||
|
log "installing $nevra from $KERNEL_REPO"
|
||||||
|
dnf -y install "$nevra" || die "failed to install $nevra"
|
||||||
|
|
||||||
|
INSTALLED_UEK8="$(find_uek8)"
|
||||||
|
[ -n "$INSTALLED_UEK8" ] || die "$nevra installed but no 6.x UEK boot entry appeared -- check 'grubby --info=ALL'"
|
||||||
|
log "installed UEK8 kernel: $INSTALLED_UEK8"
|
||||||
|
}
|
||||||
|
|
||||||
|
# --cleanup does step 4 and nothing else. It exits 0 rather than failing on a node that
|
||||||
|
# isn't on UEK8 yet: the highstate gates on 'uname -r' before calling this, and a state that
|
||||||
|
# fails whenever that gate races would be worse than one that says what it's waiting for.
|
||||||
|
case "$1" in
|
||||||
|
"")
|
||||||
|
;;
|
||||||
|
--cleanup)
|
||||||
|
if [ "$(running_flavor)" != uek8 ]; then
|
||||||
|
log "not running a UEK8 kernel yet (currently $(uname -r)); leaving the stock EL9 kernel in place."
|
||||||
|
log "Run so-kernel-upgrade with no arguments to install UEK8, then reboot."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
set_default_kernel_conf
|
||||||
|
remove_rhck
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Usage: so-kernel-upgrade [--cleanup]" >&2
|
||||||
|
echo " (no arguments) install UEK8, make it the boot default, clean up once it's running" >&2
|
||||||
|
echo " --cleanup remove the stock EL9 kernel; no-op unless already running UEK8" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
case "$(running_flavor)" in
|
||||||
|
uek8)
|
||||||
|
# Already on the 6.x UEK line. A plain 'dnf update' keeps this node current within the
|
||||||
|
# lineage and auto-promotes newer builds, so there is no install or grubby work left --
|
||||||
|
# only the step 4 cleanup, which this is the first point in the sequence that can run it.
|
||||||
|
log "already running a UEK8 kernel ($(uname -r)); no kernel install needed."
|
||||||
|
set_default_kernel_conf
|
||||||
|
remove_rhck
|
||||||
|
;;
|
||||||
|
|
||||||
|
uek7)
|
||||||
|
# On a 5.x UEK kernel. Installing UEK8 stays inside the kernel-uek lineage, so dnf/grubby
|
||||||
|
# (UPDATEDEFAULT=yes) auto-promote it and we do NOT touch grubby. A node still on UEK7
|
||||||
|
# usually means the kernel repo was empty when it last updated, so populate it and install.
|
||||||
|
log "running UEK7 kernel ($(uname -r)); the kernel repo was likely not yet populated when"
|
||||||
|
log "this node last updated. Populating it and installing UEK8 -- the update stays on the"
|
||||||
|
log "kernel-uek line, so it becomes the boot default automatically (no grubby change needed)."
|
||||||
|
set_default_kernel_conf
|
||||||
|
ensure_uek8_installed
|
||||||
|
|
||||||
|
now="$(grubby --default-kernel 2>/dev/null)"
|
||||||
|
if [ "$now" = "$INSTALLED_UEK8" ]; then
|
||||||
|
log "boot default auto-promoted to UEK8 kernel: $INSTALLED_UEK8"
|
||||||
|
else
|
||||||
|
log "WARNING: expected the UEK8 kernel to auto-promote but the default is still"
|
||||||
|
log "'${now:-unknown}'. Run 'grubby --set-default=$INSTALLED_UEK8' to force it."
|
||||||
|
fi
|
||||||
|
reboot_notice "$INSTALLED_UEK8"
|
||||||
|
;;
|
||||||
|
|
||||||
|
rhck)
|
||||||
|
# On the stock EL9 kernel (5.14, no UEK installed). Crossing from RHCK into the UEK flavor
|
||||||
|
# does NOT auto-promote -- kernel-install/grubby only auto-promote within the running
|
||||||
|
# kernel's flavor lineage -- so after installing we must set the boot default explicitly.
|
||||||
|
log "running stock EL9 (RHCK) kernel ($(uname -r)); installing UEK8 and setting it as the"
|
||||||
|
log "boot default explicitly (a RHCK->UEK flavor change does not auto-promote)."
|
||||||
|
set_default_kernel_conf
|
||||||
|
ensure_uek8_installed
|
||||||
|
target="$INSTALLED_UEK8"
|
||||||
|
|
||||||
|
current="$(grubby --default-kernel 2>/dev/null)"
|
||||||
|
if [ "$current" = "$target" ]; then
|
||||||
|
log "UEK8 kernel is already the boot default: $target"
|
||||||
|
reboot_notice "$target"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "current default kernel: ${current:-unknown}"
|
||||||
|
log "switching boot default to UEK8 kernel: $target"
|
||||||
|
grubby --set-default="$target" || die "grubby --set-default failed for $target"
|
||||||
|
|
||||||
|
# Verify the change actually took before claiming success.
|
||||||
|
now="$(grubby --default-kernel 2>/dev/null)"
|
||||||
|
[ "$now" = "$target" ] || die "default kernel is still '${now:-unknown}' after set-default"
|
||||||
|
|
||||||
|
log "boot default is now $target"
|
||||||
|
reboot_notice "$target"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -132,6 +132,9 @@ if [[ $EXCLUDE_STARTUP_ERRORS == 'Y' ]]; then
|
|||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|HTTP 404: Not Found" # Salt loops until Kratos returns 200, during startup Kratos may not be ready
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|HTTP 404: Not Found" # Salt loops until Kratos returns 200, during startup Kratos may not be ready
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Cancelling deferred write event maybeFenceReplicas because the event queue is now closed" # Kafka controller log during shutdown/restart
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Cancelling deferred write event maybeFenceReplicas because the event queue is now closed" # Kafka controller log during shutdown/restart
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Redis may have been restarted" # Redis likely restarted by salt
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Redis may have been restarted" # Redis likely restarted by salt
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|file already closed" # Go logging race condition during container restart
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|relation \"audit_settings\" does not exist" # salt checking for changes before SOC starts
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Error in plugin: elasticsearch: Unable to retrieve master node information" # expected error while ES is upgrading/electing a master
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
|
if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
|
||||||
@@ -152,6 +155,8 @@ if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
|
|||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|id.orig_h" # false positive (zeek test data)
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|id.orig_h" # false positive (zeek test data)
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|emerging-all.rules" # false positive (error in rulename)
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|emerging-all.rules" # false positive (error in rulename)
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|invalid query input" # false positive (Invalid user input in hunt query)
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|invalid query input" # false positive (Invalid user input in hunt query)
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|no data available for the requested dates" # false positive (pcap cypress test submits a job with an empty time frame)
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|no job processor" # false positive (same empty-time-frame job on import nodes, where no pcap processor runs)
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|example" # false positive (example test data)
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|example" # false positive (example test data)
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|status 200" # false positive (request successful, contained error string in content)
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|status 200" # false positive (request successful, contained error string in content)
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|app_layer.error" # false positive (suricata 7) in stats.log e.g. app_layer.error.imap.parser | Total | 0
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|app_layer.error" # false positive (suricata 7) in stats.log e.g. app_layer.error.imap.parser | Total | 0
|
||||||
@@ -165,6 +170,14 @@ if [[ $EXCLUDE_FALSE_POSITIVE_ERRORS == 'Y' ]]; then
|
|||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|upgrading component template" # false positive (elasticsearch index or template names contain 'error')
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|upgrading component template" # false positive (elasticsearch index or template names contain 'error')
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|upgrading composable template" # false positive (elasticsearch composable template names contain 'error')
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|upgrading composable template" # false positive (elasticsearch composable template names contain 'error')
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Error while parsing document for index \[.ds-logs-kratos-so-.*object mapping for \[file\]" # false positive (mapping error occuring BEFORE kratos index has rolled over in 2.4.210)
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Error while parsing document for index \[.ds-logs-kratos-so-.*object mapping for \[file\]" # false positive (mapping error occuring BEFORE kratos index has rolled over in 2.4.210)
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|No such container" # false positive (telegraf trying to run stats on an old container)
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|passwords do not match" # false positive (automated hydra test)
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Request did not pass preprocessing" # expected WARN log lines indicating invalid auth header
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing or invalid authorization header for bearer token" # expected WARN log lines indicating invalid auth header
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Unexpected authorization header" # expected WARN log lines indicating invalid auth header
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Missing ory_kratos_session cookie" # expected WARN log lines indicating invalid auth header
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Static assets preprocessor only supports GET and HEAD requests" # expected WARN log lines indicating invalid auth header
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|respondError" # respondError is a function name, output via http middleware as standard request logging
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
||||||
@@ -227,8 +240,9 @@ if [[ $EXCLUDE_KNOWN_ERRORS == 'Y' ]]; then
|
|||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|from NIC checksum offloading" # zeek reporter.log
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|from NIC checksum offloading" # zeek reporter.log
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|marked for removal" # docker container getting recycled
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|marked for removal" # docker container getting recycled
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|tcp 127.0.0.1:6791: bind: address already in use" # so-elastic-fleet agent restarting. Seen starting w/ 8.18.8 https://github.com/elastic/kibana/issues/201459
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|tcp 127.0.0.1:6791: bind: address already in use" # so-elastic-fleet agent restarting. Seen starting w/ 8.18.8 https://github.com/elastic/kibana/issues/201459
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|TransformTask\] \[logs-(tychon|aws_billing|microsoft_defender_endpoint).*user so_kibana lacks the required permissions \[logs-\1" # Known issue with 3 integrations using kibana_system role vs creating unique api creds with proper permissions.
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|TransformTask\] \[logs-.*user so_kibana lacks the required permissions" # Known issue with integrations starting transform jobs that are explicitly not allowed to start as a system user
|
||||||
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|manifest unknown" # appears in so-dockerregistry log for so-tcpreplay following docker upgrade to 29.2.1-1
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|manifest unknown" # so-dockerregistry logs a tag lookup miss during image copy; not tied to one docker version
|
||||||
|
EXCLUDED_ERRORS="$EXCLUDED_ERRORS|Could not index event to Elasticsearch.*\"version\" => \"9.0.8\"" # Expected during Elastic upgrade temporarily, as policies referencing older pipelines are updated
|
||||||
fi
|
fi
|
||||||
|
|
||||||
RESULT=0
|
RESULT=0
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# so-nic-pin — pin physical NIC names by permanent MAC via classic by-MAC udev
|
||||||
|
# rules, so a kernel upgrade can't renumber them.
|
||||||
|
#
|
||||||
|
# Security Onion binds its management and monitor interfaces BY NAME in pillar
|
||||||
|
# (host:mainint, sensor:mainint, and bond0 is built on a specific physical NIC).
|
||||||
|
# A kernel upgrade can change the kernel/systemd-udevd predictable-naming output
|
||||||
|
# and renumber those NICs (e.g. enp1s0 -> enp2s0), which breaks the grid: the
|
||||||
|
# pillar references a name that no longer exists and bond/bridge bring-up fails.
|
||||||
|
#
|
||||||
|
# This writes /etc/udev/rules.d/70-persistent-net.rules pinning each PHYSICAL NIC
|
||||||
|
# to its CURRENT name by its PERMANENT MAC, freezing the names across future kernel
|
||||||
|
# changes. It only writes the rules file; it does NOT live-trigger a rename (the
|
||||||
|
# rules apply on the next boot/kernel, and a live rename would be disruptive).
|
||||||
|
#
|
||||||
|
# Run-once: gated by the drop file /opt/so/state/nic_names_pinned. If the marker is
|
||||||
|
# present the script does nothing, so an admin can pre-create it to opt out. Invoked
|
||||||
|
# from the common state on every highstate; the marker keeps it a one-time setup.
|
||||||
|
|
||||||
|
NET_RULES_FILE="/etc/udev/rules.d/70-persistent-net.rules"
|
||||||
|
MARKER="/opt/so/state/nic_names_pinned"
|
||||||
|
|
||||||
|
log() { echo -e "[so-nic-pin] $*"; }
|
||||||
|
|
||||||
|
# Echo "<name> <permanent-mac>" for every PHYSICAL NIC. A physical NIC is backed by a
|
||||||
|
# real device (has device/driver), which excludes bond0/sobridge/docker0/veth*/lo whose
|
||||||
|
# MACs are dynamic and must never be pinned. The PERMANENT MAC is used (ethtool -P, with
|
||||||
|
# fallbacks), not the current one: an enslaved bond member's current MAC is rewritten to
|
||||||
|
# the bond's, so matching on it would be wrong/ambiguous.
|
||||||
|
physical_nics() {
|
||||||
|
local path n mac
|
||||||
|
for path in /sys/class/net/*; do
|
||||||
|
n="${path##*/}"
|
||||||
|
[ "$n" = "lo" ] && continue
|
||||||
|
[ -e "${path}/device/driver" ] || continue # real device only
|
||||||
|
mac="$(ethtool -P "$n" 2>/dev/null | awk '/Permanent address/{print $NF}')"
|
||||||
|
case "$mac" in ""|00:00:00:00:00:00) mac="$(cat "${path}/bonding_slave/perm_hwaddr" 2>/dev/null)" ;; esac
|
||||||
|
case "$mac" in ""|00:00:00:00:00:00) mac="$(cat "${path}/address" 2>/dev/null)" ;; esac
|
||||||
|
case "$mac" in ""|00:00:00:00:00:00) continue ;; esac
|
||||||
|
echo "$n $mac"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Turn "<name> <mac>" lines on stdin into classic by-MAC persistent-net udev rules.
|
||||||
|
render_net_rules() {
|
||||||
|
echo "# Generated by so-nic-pin: pin NIC names by MAC so kernel upgrades can't renumber them."
|
||||||
|
echo "# Security Onion binds its management/monitor interfaces by name; do not hand-edit."
|
||||||
|
local n mac
|
||||||
|
while read -r n mac; do
|
||||||
|
[ -n "$n" ] || continue
|
||||||
|
printf 'SUBSYSTEM=="net", ACTION=="add", DRIVERS=="?*", ATTR{address}=="%s", NAME="%s"\n' \
|
||||||
|
"$mac" "$n"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
[ "$(id -u)" -eq 0 ] || exit 0 # salt runs us as root; bail quietly otherwise
|
||||||
|
[ -e "${MARKER}" ] && exit 0 # run-once guard (mirrors the state's unless)
|
||||||
|
|
||||||
|
nics="$(physical_nics)"
|
||||||
|
if [ -z "${nics}" ]; then
|
||||||
|
log "no physical NICs detected — nothing to pin (will retry on next highstate)"
|
||||||
|
exit 0 # do NOT drop the marker; let it retry later
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "pinning physical NICs by permanent MAC:"
|
||||||
|
echo "${nics}" | sed 's/^/ /'
|
||||||
|
|
||||||
|
[ -f "${NET_RULES_FILE}" ] && cp -f "${NET_RULES_FILE}" "${NET_RULES_FILE}.bak"
|
||||||
|
echo "${nics}" | render_net_rules > "${NET_RULES_FILE}" || {
|
||||||
|
log "ERROR: failed to write ${NET_RULES_FILE}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mkdir -p "$(dirname "${MARKER}")" && touch "${MARKER}"
|
||||||
|
log "wrote ${NET_RULES_FILE} ($(grep -c '^SUBSYSTEM' "${NET_RULES_FILE}") NIC(s) pinned); dropped ${MARKER}"
|
||||||
@@ -5,27 +5,41 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Usage: so-restart kibana | playbook
|
|
||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
if [ $# -ge 1 ]; then
|
usage() {
|
||||||
|
echo "Usage: $0 <component> [args]"
|
||||||
|
echo ""
|
||||||
|
echo "Supported args:"
|
||||||
|
echo " --force | -f Force stop all Salt jobs before starting component."
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 kibana Restart Kibana"
|
||||||
|
echo " $0 kibana --force Force stop all Salt jobs before restarting Kibana"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
echo $banner
|
if [[ $# -lt 1 ]]; then
|
||||||
printf "Restarting $1...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n"
|
usage
|
||||||
echo $banner
|
|
||||||
|
|
||||||
if [ "$2" = "--force" ]; then
|
|
||||||
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
|
||||||
salt-call saltutil.kill_all_jobs
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
"elastic-fleet") docker stop so-elastic-fleet && docker rm so-elastic-fleet && salt-call state.apply elasticfleet queue=True;;
|
|
||||||
*) docker stop so-$1 ; docker rm so-$1 ; salt-call state.apply $1 queue=True;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo -e "\nPlease provide an argument by running like so-restart $component, or by using the component-specific script.\nEx. so-restart logstash, or so-logstash-restart\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#shellcheck disable=SC2154
|
||||||
|
echo "$banner"
|
||||||
|
printf "Restarting %s...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n" "$1"
|
||||||
|
echo "$banner"
|
||||||
|
if [[ "$2" = "--force" ]] || [[ "$2" = "-f" ]]; then
|
||||||
|
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
||||||
|
salt-call saltutil.kill_all_jobs
|
||||||
|
fi
|
||||||
|
case $1 in
|
||||||
|
"elastic-fleet"|"elasticfleet")
|
||||||
|
docker_check_running "elastic-fleet" "--stop"
|
||||||
|
docker rm "so-elastic-fleet" 2> /dev/null
|
||||||
|
salt-call state.apply elasticfleet queue=True
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
docker_check_running "$1" "--stop"
|
||||||
|
docker rm "so-${1}" 2> /dev/null
|
||||||
|
salt-call state.apply "$1" queue=True
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -8,21 +8,37 @@
|
|||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
|
||||||
SENSOR_DIR='/nsm'
|
SENSOR_DIR="${SENSOR_DIR:-/nsm}"
|
||||||
CRIT_DISK_USAGE=90
|
CRIT_DISK_USAGE=90
|
||||||
CUR_USAGE=$(df -P $SENSOR_DIR | tail -1 | awk '{print $5}' | tr -d %)
|
LOG="${LOG:-/opt/so/log/sensor_clean.log}"
|
||||||
LOG="/opt/so/log/sensor_clean.log"
|
LOCK="${LOCK:-/var/tmp/so-sensor-clean.lock}"
|
||||||
TODAY=$(date -u "+%Y-%m-%d")
|
MAX_PASSES=100
|
||||||
|
|
||||||
|
ZEEK_LOGS="$SENSOR_DIR/zeek/logs"
|
||||||
|
STRELKA_FILES="$SENSOR_DIR/strelka/processed"
|
||||||
|
SURICATA_LOGS="$SENSOR_DIR/suricata"
|
||||||
|
PCAPS="$SENSOR_DIR/pcapout"
|
||||||
|
|
||||||
|
log() {
|
||||||
|
echo "$(date) - $*" >>"$LOG"
|
||||||
|
}
|
||||||
|
|
||||||
|
disk_usage() {
|
||||||
|
df -P "$SENSOR_DIR" | tail -1 | awk '{print $5}' | tr -d %
|
||||||
|
}
|
||||||
|
|
||||||
|
disk_avail() {
|
||||||
|
df -P "$SENSOR_DIR" | tail -1 | awk '{print $4}'
|
||||||
|
}
|
||||||
|
|
||||||
|
# sets REMOVED=1 if anything was actually deleted
|
||||||
clean() {
|
clean() {
|
||||||
## find the oldest Zeek logs directory
|
## find the oldest Zeek logs directory
|
||||||
OLDEST_DIR=$(ls /nsm/zeek/logs/ | grep -v "current" | grep -v "stats" | grep -v "packetloss" | grep -v "zeek_clean" | sort | head -n 1)
|
OLDEST_DIR=$(ls "$ZEEK_LOGS" 2>/dev/null | grep -v "current" | grep -v "stats" | grep -v "packetloss" | grep -v "zeek_clean" | sort | head -n 1)
|
||||||
if [ -z "$OLDEST_DIR" -o "$OLDEST_DIR" == ".." -o "$OLDEST_DIR" == "." ]; then
|
if [ -n "$OLDEST_DIR" ]; then
|
||||||
echo "$(date) - No old Zeek logs available to clean up in /nsm/zeek/logs/" >>$LOG
|
log "Removing directory: $ZEEK_LOGS/$OLDEST_DIR"
|
||||||
#exit 0
|
rm -rf "$ZEEK_LOGS/$OLDEST_DIR"
|
||||||
else
|
REMOVED=1
|
||||||
echo "$(date) - Removing directory: /nsm/zeek/logs/$OLDEST_DIR" >>$LOG
|
|
||||||
rm -rf /nsm/zeek/logs/"$OLDEST_DIR"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
## Remarking for now, as we are moving extracted files to /nsm/strelka/processed
|
## Remarking for now, as we are moving extracted files to /nsm/strelka/processed
|
||||||
@@ -42,44 +58,74 @@ clean() {
|
|||||||
# done
|
# done
|
||||||
#fi
|
#fi
|
||||||
|
|
||||||
|
## Clean up Zeek extracted files processed by Strelka
|
||||||
|
OLDEST_STRELKA=$(find "$STRELKA_FILES" -type f -printf '%T+ %p\n' 2>/dev/null | sort -n | head -n 1)
|
||||||
|
if [ -n "$OLDEST_STRELKA" ]; then
|
||||||
|
OLDEST_STRELKA_DATE=$(echo $OLDEST_STRELKA | awk '{print $1}' | cut -d+ -f1)
|
||||||
|
log "Removing extracted files for $OLDEST_STRELKA_DATE"
|
||||||
|
REMOVED=1
|
||||||
|
find "$STRELKA_FILES" -type f -printf '%T+ %p\n' 2>/dev/null | grep $OLDEST_STRELKA_DATE | awk '{print $2}' | while read FILE; do
|
||||||
|
log "Removing file: $FILE"
|
||||||
|
rm -f "$FILE"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
## Clean up Suricata log files
|
## Clean up Suricata log files
|
||||||
SURICATA_LOGS='/nsm/suricata'
|
OLDEST_SURICATA=$(find "$SURICATA_LOGS" -type f -printf '%T+ %p\n' 2>/dev/null | sort -n | head -n 1)
|
||||||
OLDEST_SURICATA=$(find $SURICATA_LOGS -type f -printf '%T+ %p\n' | sort -n | head -n 1)
|
if [ -n "$OLDEST_SURICATA" ]; then
|
||||||
if [[ -z "$OLDEST_SURICATA" ]] || [[ "$OLDEST_SURICATA" == ".." ]] || [[ "$OLDEST_SURICATA" == "." ]]; then
|
|
||||||
echo "$(date) - No old files available to clean up in $SURICATA_LOGS" >>$LOG
|
|
||||||
else
|
|
||||||
OLDEST_SURICATA_DATE=$(echo $OLDEST_SURICATA | awk '{print $1}' | cut -d+ -f1)
|
OLDEST_SURICATA_DATE=$(echo $OLDEST_SURICATA | awk '{print $1}' | cut -d+ -f1)
|
||||||
OLDEST_SURICATA_FILE=$(echo $OLDEST_SURICATA | awk '{print $2}')
|
log "Removing logs for $OLDEST_SURICATA_DATE"
|
||||||
echo "$(date) - Removing logs for $OLDEST_SURICATA_DATE" >>$LOG
|
REMOVED=1
|
||||||
find $SURICATA_LOGS -type f -printf '%T+ %p\n' | grep $OLDEST_SURICATA_DATE | awk '{print $2}' | while read FILE; do
|
find "$SURICATA_LOGS" -type f -printf '%T+ %p\n' 2>/dev/null | grep $OLDEST_SURICATA_DATE | awk '{print $2}' | while read FILE; do
|
||||||
echo "$(date) - Removing file: $FILE" >>$LOG
|
log "Removing file: $FILE"
|
||||||
rm -f "$FILE"
|
rm -f "$FILE"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
## Clean up extracted pcaps
|
## Clean up extracted pcaps
|
||||||
PCAPS='/nsm/pcapout'
|
OLDEST_PCAP=$(find "$PCAPS" -type f -printf '%T+ %p\n' 2>/dev/null | sort -n | head -n 1)
|
||||||
OLDEST_PCAP=$(find $PCAPS -type f -printf '%T+ %p\n' | sort -n | head -n 1)
|
if [ -n "$OLDEST_PCAP" ]; then
|
||||||
if [ -z "$OLDEST_PCAP" -o "$OLDEST_PCAP" == ".." -o "$OLDEST_PCAP" == "." ]; then
|
|
||||||
echo "$(date) - No old files available to clean up in $PCAPS" >>$LOG
|
|
||||||
else
|
|
||||||
OLDEST_PCAP_DATE=$(echo $OLDEST_PCAP | awk '{print $1}' | cut -d+ -f1)
|
OLDEST_PCAP_DATE=$(echo $OLDEST_PCAP | awk '{print $1}' | cut -d+ -f1)
|
||||||
OLDEST_PCAP_FILE=$(echo $OLDEST_PCAP | awk '{print $2}')
|
log "Removing extracted files for $OLDEST_PCAP_DATE"
|
||||||
echo "$(date) - Removing extracted files for $OLDEST_PCAP_DATE" >>$LOG
|
REMOVED=1
|
||||||
find $PCAPS -type f -printf '%T+ %p\n' | grep $OLDEST_PCAP_DATE | awk '{print $2}' | while read FILE; do
|
find "$PCAPS" -type f -printf '%T+ %p\n' 2>/dev/null | grep $OLDEST_PCAP_DATE | awk '{print $2}' | while read FILE; do
|
||||||
echo "$(date) - Removing file: $FILE" >>$LOG
|
log "Removing file: $FILE"
|
||||||
rm -f "$FILE"
|
rm -f "$FILE"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Check to see if we are already running
|
# Only one instance at a time; the lock is the fd, so it releases on any exit
|
||||||
NUM_RUNNING=$(pgrep -cf "/bin/bash /usr/sbin/so-sensor-clean")
|
exec 9>"$LOCK" || exit 1
|
||||||
[ "$NUM_RUNNING" -gt 1 ] && echo "$(date) - $NUM_RUNNING sensor clean script processes running...exiting." >>$LOG && exit 0
|
if ! flock -n 9; then
|
||||||
|
log "another so-sensor-clean is already running (lock $LOCK held); exiting"
|
||||||
if [ "$CUR_USAGE" -gt "$CRIT_DISK_USAGE" ]; then
|
exit 0
|
||||||
while [ "$CUR_USAGE" -gt "$CRIT_DISK_USAGE" ]; do
|
|
||||||
clean
|
|
||||||
CUR_USAGE=$(df -P $SENSOR_DIR | tail -1 | awk '{print $5}' | tr -d %)
|
|
||||||
done
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
CUR_USAGE=$(disk_usage)
|
||||||
|
[ "$CUR_USAGE" -gt "$CRIT_DISK_USAGE" ] || exit 0
|
||||||
|
|
||||||
|
log "$SENSOR_DIR at ${CUR_USAGE}% (threshold ${CRIT_DISK_USAGE}%); starting cleanup"
|
||||||
|
|
||||||
|
PASS=0
|
||||||
|
while [ "$CUR_USAGE" -gt "$CRIT_DISK_USAGE" ]; do
|
||||||
|
PASS=$((PASS + 1))
|
||||||
|
if [ "$PASS" -gt "$MAX_PASSES" ]; then
|
||||||
|
log "stopping after $MAX_PASSES passes; $SENSOR_DIR still at ${CUR_USAGE}%"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
|
||||||
|
REMOVED=0
|
||||||
|
BEFORE=$(disk_avail)
|
||||||
|
clean
|
||||||
|
CUR_USAGE=$(disk_usage)
|
||||||
|
|
||||||
|
if [ "$REMOVED" -eq 0 ]; then
|
||||||
|
log "nothing left to remove in $ZEEK_LOGS, $STRELKA_FILES, $SURICATA_LOGS, $PCAPS; $SENSOR_DIR still at ${CUR_USAGE}% - space is consumed outside of NSM cleanup scope"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
if [ "$(disk_avail)" -le "$BEFORE" ]; then
|
||||||
|
log "pass $PASS freed no space; $SENSOR_DIR still at ${CUR_USAGE}% - stopping until next run"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -5,27 +5,54 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
|
||||||
# Usage: so-start all | kibana | playbook
|
|
||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
if [ $# -ge 1 ]; then
|
usage() {
|
||||||
echo $banner
|
echo "Usage: $0 <component> [args]"
|
||||||
printf "Starting $1...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n"
|
echo ""
|
||||||
echo $banner
|
echo "Supported args:"
|
||||||
|
echo " --force | -f Force stop all Salt jobs before starting component."
|
||||||
|
echo ""
|
||||||
|
echo "Examples:"
|
||||||
|
echo " $0 kibana Start Kibana"
|
||||||
|
echo " $0 kibana --force Force stop all Salt jobs before starting Kibana"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
if [ "$2" = "--force" ]; then
|
if [[ $# -lt 1 ]]; then
|
||||||
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
usage
|
||||||
salt-call saltutil.kill_all_jobs
|
|
||||||
fi
|
|
||||||
|
|
||||||
case $1 in
|
|
||||||
"all") salt-call state.highstate queue=True;;
|
|
||||||
"elastic-fleet") if docker ps | grep -q so-$1; then printf "\n$1 is already running!\n\n"; else docker rm so-$1 >/dev/null 2>&1 ; salt-call state.apply elasticfleet queue=True; fi ;;
|
|
||||||
*) if docker ps | grep -E -q '^so-$1$'; then printf "\n$1 is already running\n\n"; else docker rm so-$1 >/dev/null 2>&1 ; salt-call state.apply $1 queue=True; fi ;;
|
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo -e "\nPlease provide an argument by running like so-start $component, or by using the component-specific script.\nEx. so-start logstash, or so-logstash-start\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#shellcheck disable=SC2154
|
||||||
|
echo "$banner"
|
||||||
|
printf "Starting %s...\n\nThis could take a while if another Salt job is running. \nRun this command with --force to stop all Salt jobs before proceeding.\n" "$1"
|
||||||
|
echo "$banner"
|
||||||
|
if [[ "$2" = "--force" ]] || [[ "$2" == "-f" ]]; then
|
||||||
|
printf "\nForce-stopping all Salt jobs before proceeding\n\n"
|
||||||
|
salt-call saltutil.kill_all_jobs
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$1" in
|
||||||
|
"all")
|
||||||
|
salt-call state.highstate queue=True
|
||||||
|
;;
|
||||||
|
"elastic-fleet"|"elasticfleet")
|
||||||
|
if docker_check_running "elastic-fleet"; then
|
||||||
|
printf "\nso-%s is already running!\n\n" "elastic-fleet"
|
||||||
|
/usr/sbin/so-status
|
||||||
|
else
|
||||||
|
docker rm "so-elastic-fleet" 2> /dev/null
|
||||||
|
salt-call state.apply elasticfleet queue=True
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
if docker_check_running "$1"; then
|
||||||
|
printf "\nso-%s is already running\n\n" "$1"
|
||||||
|
/usr/sbin/so-status
|
||||||
|
else
|
||||||
|
docker rm "so-${1}" 2> /dev/null
|
||||||
|
salt-call state.apply "$1" queue=True
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -74,13 +74,13 @@ def output(options, console, code, data):
|
|||||||
summary = { "status_code": code, "containers": data }
|
summary = { "status_code": code, "containers": data }
|
||||||
print(json.dumps(summary))
|
print(json.dumps(summary))
|
||||||
elif "-q" not in options:
|
elif "-q" not in options:
|
||||||
if code == 2:
|
if code == 99:
|
||||||
console.print(" [bold yellow]:hourglass: [bold white]System appears to be starting. No highstate has completed since the system was restarted.")
|
|
||||||
elif code == 99:
|
|
||||||
console.print(" [bold red]:exclamation: [bold white]Installation does not appear to be complete. A highstate has not fully completed.")
|
console.print(" [bold red]:exclamation: [bold white]Installation does not appear to be complete. A highstate has not fully completed.")
|
||||||
elif code == 100:
|
elif code == 100:
|
||||||
console.print(" [bold red]:exclamation: [bold white]Installation encountered errors.")
|
console.print(" [bold red]:exclamation: [bold white]Installation encountered errors.")
|
||||||
else:
|
else:
|
||||||
|
if code == 2:
|
||||||
|
console.print(" [bold yellow]:hourglass: [bold white]System appears to be starting. No highstate has completed since the system was restarted. Container status is shown below.")
|
||||||
table = Table(title = "Security Onion Status", show_edge = False, safe_box = True, box = box.MINIMAL)
|
table = Table(title = "Security Onion Status", show_edge = False, safe_box = True, box = box.MINIMAL)
|
||||||
table.add_column("Container", justify="right", style="white", no_wrap=True)
|
table.add_column("Container", justify="right", style="white", no_wrap=True)
|
||||||
table.add_column("Status", justify="left", style="green", no_wrap=True)
|
table.add_column("Status", justify="left", style="green", no_wrap=True)
|
||||||
@@ -154,8 +154,14 @@ def check_status(options, console):
|
|||||||
code = check_installation_status(options, console)
|
code = check_installation_status(options, console)
|
||||||
if code == 0:
|
if code == 0:
|
||||||
code = check_system_status(options, console)
|
code = check_system_status(options, console)
|
||||||
if code == 0:
|
# Containers now start on boot without a highstate, so gather/display their
|
||||||
code, container_list = check_container_status(options, console)
|
# status even when the system is still "starting" (code 2). Keep the starting
|
||||||
|
# code as the exit/status_code so SOC keeps showing the "restarting" message
|
||||||
|
# on the Grid until a highstate completes.
|
||||||
|
if code == 0 or code == 2:
|
||||||
|
container_code, container_list = check_container_status(options, console)
|
||||||
|
if code == 0:
|
||||||
|
code = container_code
|
||||||
output(options, console, code, container_list)
|
output(options, console, code, container_list)
|
||||||
return code
|
return code
|
||||||
|
|
||||||
@@ -180,4 +186,3 @@ def main():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
||||||
@@ -5,21 +5,33 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
# shellcheck disable=SC1091
|
||||||
|
|
||||||
# Usage: so-stop kibana | playbook | thehive
|
|
||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
if [ $# -ge 1 ]; then
|
usage() {
|
||||||
echo $banner
|
echo "Usage: $0 <component>"
|
||||||
printf "Stopping $1...\n"
|
echo ""
|
||||||
echo $banner
|
echo "Examples:"
|
||||||
|
echo " $0 kibana Stop Kibana"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
case $1 in
|
if [[ $# -lt 1 ]]; then
|
||||||
*) docker stop so-$1 ; docker rm so-$1 ;;
|
usage
|
||||||
esac
|
|
||||||
else
|
|
||||||
echo -e "\nPlease provide an argument by running like so-stop $component, or by using the component-specific script.\nEx. so-stop logstash, or so-logstash-stop\n"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
#shellcheck disable=SC2154
|
||||||
|
echo "$banner"
|
||||||
|
printf "Stopping %s...\n" "$1"
|
||||||
|
echo "$banner"
|
||||||
|
case $1 in
|
||||||
|
"elasticfleet"|"elastic-fleet")
|
||||||
|
docker_check_running "elastic-fleet" "--stop"
|
||||||
|
docker rm "so-elastic-fleet" 2> /dev/null
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
docker_check_running "$1" "--stop"
|
||||||
|
docker rm "so-${1}" 2> /dev/null
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@@ -63,7 +63,8 @@ function status {
|
|||||||
function pcapinfo() {
|
function pcapinfo() {
|
||||||
PCAP=$1
|
PCAP=$1
|
||||||
ARGS=$2
|
ARGS=$2
|
||||||
docker run --rm -v "$PCAP:/input.pcap" --entrypoint capinfos {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-pcaptools:{{ VERSION }} /input.pcap -ae $ARGS
|
docker run --rm -v "$PCAP:/input.pcap" --entrypoint capinfos {{ MANAGER }}:5000/{{ IMAGEREPO }}/so-pcaptools:{{ VERSION }} /input.pcap -ae $ARGS |\
|
||||||
|
sed 's/First packet/Earliest packet/g' | sed 's/Last packet/Latest packet/g'
|
||||||
}
|
}
|
||||||
|
|
||||||
function pcapfix() {
|
function pcapfix() {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@
|
|||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
|
||||||
software_raid=("SOSMN" "SOSMN-DE02" "SOSSNNV" "SOSSNNV-DE02" "SOS10k-DE02" "SOS10KNV" "SOS10KNV-DE02" "SOS10KNV-DE02" "SOS2000-DE02" "SOS-GOFAST-LT-DE02" "SOS-GOFAST-MD-DE02" "SOS-GOFAST-HV-DE02")
|
software_raid=("SOSMN" "SOSMN-DE02" "SOSSNNV" "SOSSNNV-DE02" "SOS10k-DE02" "SOS10KNV" "SOS10KNV-DE02" "SOS10KNV-DE02" "SOS2000-DE02" "SOS-GOFAST-LT-DE02" "SOS-GOFAST-MD-DE02" "SOS-GOFAST-HV-DE02" "HVGUEST")
|
||||||
hardware_raid=("SOS1000" "SOS1000F" "SOSSN7200" "SOS5000" "SOS4000")
|
hardware_raid=("SOS1000" "SOS1000F" "SOSSN7200" "SOS5000" "SOS4000")
|
||||||
|
|
||||||
{%- if salt['grains.get']('sosmodel', '') %}
|
{%- if salt['grains.get']('sosmodel', '') %}
|
||||||
@@ -87,6 +87,11 @@ check_boss_raid() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
check_software_raid() {
|
check_software_raid() {
|
||||||
|
if [[ ! -f /proc/mdstat ]]; then
|
||||||
|
SWRAID=0
|
||||||
|
return
|
||||||
|
fi
|
||||||
|
|
||||||
SWRC=$(grep "_" /proc/mdstat)
|
SWRC=$(grep "_" /proc/mdstat)
|
||||||
if [[ -n $SWRC ]]; then
|
if [[ -n $SWRC ]]; then
|
||||||
# RAID is failed in some way
|
# RAID is failed in some way
|
||||||
@@ -107,7 +112,9 @@ if [[ "$is_hwraid" == "true" ]]; then
|
|||||||
fi
|
fi
|
||||||
if [[ "$is_softwareraid" == "true" ]]; then
|
if [[ "$is_softwareraid" == "true" ]]; then
|
||||||
check_software_raid
|
check_software_raid
|
||||||
check_boss_raid
|
if [ "$model" != "HVGUEST" ]; then
|
||||||
|
check_boss_raid
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sum=$(($SWRAID + $BOSSRAID + $HWRAID))
|
sum=$(($SWRAID + $BOSSRAID + $HWRAID))
|
||||||
@@ -118,4 +125,6 @@ else
|
|||||||
RAIDSTATUS=1
|
RAIDSTATUS=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "nsmraid=$RAIDSTATUS" > /opt/so/log/raid/status.log
|
# telegraf reads this file; write aside and rename so it never sees a half-written file
|
||||||
|
echo "nsmraid=$RAIDSTATUS" > /opt/so/log/raid/status.log.tmp
|
||||||
|
mv -f /opt/so/log/raid/status.log.tmp /opt/so/log/raid/status.log
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
{% import_yaml 'salt/minion.defaults.yaml' as SALT_MINION_DEFAULTS -%}
|
|
||||||
|
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
#
|
#
|
||||||
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
@@ -7,7 +5,7 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
{% from 'salt/schedule.map.jinja' import SCHEDULEMERGED %}
|
||||||
|
|
||||||
# this script checks the time the file /opt/so/log/salt/state-apply-test was last modified and restarts the salt-minion service if it is outside a threshold date/time
|
# this script checks the time the file /opt/so/log/salt/state-apply-test was last modified and restarts the salt-minion service if it is outside a threshold date/time
|
||||||
# the file is modified via file.touch using a scheduled job healthcheck.salt-minion.state-apply-test that runs a state.apply.
|
# the file is modified via file.touch using a scheduled job healthcheck.salt-minion.state-apply-test that runs a state.apply.
|
||||||
@@ -20,12 +18,14 @@
|
|||||||
|
|
||||||
QUIET=false
|
QUIET=false
|
||||||
UPTIME_REQ=1800 #in seconds, how long the box has to be up before considering restarting salt-minion due to /opt/so/log/salt/state-apply-test not being touched
|
UPTIME_REQ=1800 #in seconds, how long the box has to be up before considering restarting salt-minion due to /opt/so/log/salt/state-apply-test not being touched
|
||||||
|
HIGHSTATE_UPTIME_REQ=900 #in seconds; if the box has been up this long and no highstate has completed since boot, force one
|
||||||
CURRENT_TIME=$(date +%s)
|
CURRENT_TIME=$(date +%s)
|
||||||
SYSTEM_START_TIME=$(date -d "$(</proc/uptime awk '{print $1}') seconds ago" +%s)
|
SYSTEM_START_TIME=$(date -d "$(</proc/uptime awk '{print $1}') seconds ago" +%s)
|
||||||
LAST_HIGHSTATE_END=$([ -e "/opt/so/log/salt/lasthighstate" ] && date -r /opt/so/log/salt/lasthighstate +%s || echo 0)
|
LAST_HIGHSTATE_END=$([ -e "/opt/so/log/salt/lasthighstate" ] && date -r /opt/so/log/salt/lasthighstate +%s || echo 0)
|
||||||
LAST_HEALTHCHECK_STATE_APPLY=$([ -e "/opt/so/log/salt/state-apply-test" ] && date -r /opt/so/log/salt/state-apply-test +%s || echo 0)
|
LAST_HEALTHCHECK_STATE_APPLY=$([ -e "/opt/so/log/salt/state-apply-test" ] && date -r /opt/so/log/salt/state-apply-test +%s || echo 0)
|
||||||
# SETTING THRESHOLD TO ANYTHING UNDER 600 seconds may cause a lot of salt-minion restarts since the job to touch the file occurs every 5-8 minutes by default
|
# SETTING THRESHOLD TO ANYTHING UNDER 600 seconds may cause a lot of salt-minion restarts since the job to touch the file occurs every 5-8 minutes by default
|
||||||
THRESHOLD={{SALT_MINION_DEFAULTS.salt.minion.check_threshold}} #within how many seconds the file /opt/so/log/salt/state-apply-test must have been touched/modified before the salt minion is restarted
|
# THRESHOLD is derived from the salt schedule highstate interval + 1 hour, so the minion-check grace period tracks the schedule automatically.
|
||||||
|
THRESHOLD=$(( ({{ SCHEDULEMERGED.highstate_interval_minutes }} + 60) * 60 )) #within how many seconds the file /opt/so/log/salt/state-apply-test must have been touched/modified before the salt minion is restarted
|
||||||
THRESHOLD_DATE=$((LAST_HEALTHCHECK_STATE_APPLY+THRESHOLD))
|
THRESHOLD_DATE=$((LAST_HEALTHCHECK_STATE_APPLY+THRESHOLD))
|
||||||
|
|
||||||
logCmd() {
|
logCmd() {
|
||||||
@@ -77,24 +77,50 @@ done
|
|||||||
|
|
||||||
log "running so-salt-minion-check"
|
log "running so-salt-minion-check"
|
||||||
|
|
||||||
|
RESTARTED=false
|
||||||
|
|
||||||
|
# Check 1 (minion-restart-check): if the minion has stopped applying states (the
|
||||||
|
# state-apply-test healthcheck file has gone stale), restart the salt-minion service.
|
||||||
if [ $CURRENT_TIME -ge $((SYSTEM_START_TIME+$UPTIME_REQ)) ]; then
|
if [ $CURRENT_TIME -ge $((SYSTEM_START_TIME+$UPTIME_REQ)) ]; then
|
||||||
if [ $THRESHOLD_DATE -le $CURRENT_TIME ]; then
|
if [ $THRESHOLD_DATE -le $CURRENT_TIME ]; then
|
||||||
log "salt-minion is unable to apply states" E
|
log "[minion-restart-check] salt-minion is unable to apply states; restarting salt-minion" E
|
||||||
log "/opt/so/log/salt/healthcheck-state-apply not touched by required date: `date -d @$THRESHOLD_DATE`, last touched: `date -d @$LAST_HEALTHCHECK_STATE_APPLY`" I
|
log "[minion-restart-check] state-apply-test not touched by required date `date -d @$THRESHOLD_DATE`, last touched `date -d @$LAST_HEALTHCHECK_STATE_APPLY`" I
|
||||||
log "last highstate completed at `date -d @$LAST_HIGHSTATE_END`" I
|
log "[minion-restart-check] last highstate completed at `date -d @$LAST_HIGHSTATE_END`" I
|
||||||
log "checking if any jobs are running" I
|
log "[minion-restart-check] checking if any jobs are running" I
|
||||||
logCmd "salt-call --local saltutil.running" I
|
logCmd "salt-call --local saltutil.running" I
|
||||||
log "ensure salt.minion-state-apply-test is enabled" I
|
log "[minion-restart-check] ensure salt.minion-state-apply-test is enabled" I
|
||||||
logCmd "salt-call state.enable salt.minion-state-apply-test" I
|
logCmd "salt-call state.enable salt.minion-state-apply-test" I
|
||||||
log "ensure highstate is enabled" I
|
log "[minion-restart-check] ensure highstate is enabled" I
|
||||||
logCmd "salt-call state.enable highstate" I
|
logCmd "salt-call state.enable highstate" I
|
||||||
log "killing all salt-minion processes" I
|
log "[minion-restart-check] killing all salt-minion processes" I
|
||||||
logCmd "pkill -9 -ef /usr/bin/salt-minion" I
|
logCmd "pkill -9 -ef /usr/bin/salt-minion" I
|
||||||
log "starting salt-minion service" I
|
log "[minion-restart-check] starting salt-minion service" I
|
||||||
logCmd "systemctl start salt-minion" I
|
logCmd "systemctl start salt-minion" I
|
||||||
|
log "[minion-restart-check] waiting for salt-minion to become ready, then applying highstate in the background (queued)" I
|
||||||
|
nohup bash -c '/usr/sbin/so-salt-minion-wait; salt-call state.highstate queue=True' >> "/opt/so/log/salt/so-salt-minion-check" 2>&1 &
|
||||||
|
RESTARTED=true
|
||||||
else
|
else
|
||||||
log "/opt/so/log/salt/healthcheck-state-apply last touched: `date -d @$LAST_HEALTHCHECK_STATE_APPLY` must be touched by `date -d @$THRESHOLD_DATE` to avoid salt-minion restart" I
|
log "[minion-restart-check] healthy: state-apply-test last touched `date -d @$LAST_HEALTHCHECK_STATE_APPLY`, must go stale past `date -d @$THRESHOLD_DATE` to trigger a salt-minion restart" I
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
log "system uptime only $((CURRENT_TIME-SYSTEM_START_TIME)) seconds does not meet $UPTIME_REQ second requirement." I
|
log "[minion-restart-check] skipped: system uptime $((CURRENT_TIME-SYSTEM_START_TIME))s is below the ${UPTIME_REQ}s minimum required before a salt-minion restart" I
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check 2 (boot-highstate-check): if the host has been up long enough but no highstate
|
||||||
|
# has completed since this boot, force one. This recovers a host whose boot highstate
|
||||||
|
# (so-boot-highstate.service) failed or was skipped, even while the minion is otherwise
|
||||||
|
# healthy (touching state-apply-test). We deliberately do NOT enable highstate here: if
|
||||||
|
# soup has disabled it during an upgrade, Salt will refuse the highstate and we avoid
|
||||||
|
# forcing one mid-upgrade.
|
||||||
|
if $RESTARTED; then
|
||||||
|
log "[boot-highstate-check] skipped: minion-restart-check already queued a highstate this run" I
|
||||||
|
elif [ $CURRENT_TIME -lt $((SYSTEM_START_TIME+HIGHSTATE_UPTIME_REQ)) ]; then
|
||||||
|
log "[boot-highstate-check] skipped: system uptime $((CURRENT_TIME-SYSTEM_START_TIME))s is below the ${HIGHSTATE_UPTIME_REQ}s minimum required before forcing a highstate" I
|
||||||
|
elif [ $LAST_HIGHSTATE_END -ge $SYSTEM_START_TIME ]; then
|
||||||
|
log "[boot-highstate-check] healthy: a highstate completed at `date -d @$LAST_HIGHSTATE_END`, after this boot at `date -d @$SYSTEM_START_TIME`" I
|
||||||
|
elif salt-call --local saltutil.running 2>/dev/null | grep -q 'state.highstate'; then
|
||||||
|
log "[boot-highstate-check] no highstate has completed since boot, but one is already running; skipping" I
|
||||||
|
else
|
||||||
|
log "[boot-highstate-check] no highstate has completed since boot after $((CURRENT_TIME-SYSTEM_START_TIME))s uptime; applying highstate" E
|
||||||
|
nohup bash -c 'salt-call state.highstate -l info queue=True' >> "/opt/so/log/salt/so-salt-minion-check" 2>&1 &
|
||||||
fi
|
fi
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
docker:
|
docker:
|
||||||
range: '172.17.1.0/24'
|
range: '172.17.1.0/24'
|
||||||
gateway: '172.17.1.1'
|
gateway: '172.17.1.1'
|
||||||
|
networks:
|
||||||
|
sobridge: {}
|
||||||
|
soauth:
|
||||||
|
range: '172.17.2.0/24'
|
||||||
|
gateway: '172.17.2.1'
|
||||||
|
manager_only: True
|
||||||
ulimits:
|
ulimits:
|
||||||
- name: nofile
|
- name: nofile
|
||||||
soft: 1048576
|
soft: 1048576
|
||||||
@@ -58,18 +64,18 @@ docker:
|
|||||||
ulimits: []
|
ulimits: []
|
||||||
'so-kratos':
|
'so-kratos':
|
||||||
final_octet: 28
|
final_octet: 28
|
||||||
|
networks: ['soauth']
|
||||||
port_bindings:
|
port_bindings:
|
||||||
- 0.0.0.0:4433:4433
|
- 0.0.0.0:4433:4433
|
||||||
- 0.0.0.0:4434:4434
|
|
||||||
custom_bind_mounts: []
|
custom_bind_mounts: []
|
||||||
extra_hosts: []
|
extra_hosts: []
|
||||||
extra_env: []
|
extra_env: []
|
||||||
ulimits: []
|
ulimits: []
|
||||||
'so-hydra':
|
'so-hydra':
|
||||||
final_octet: 30
|
final_octet: 30
|
||||||
|
networks: ['soauth']
|
||||||
port_bindings:
|
port_bindings:
|
||||||
- 0.0.0.0:4444:4444
|
- 0.0.0.0:4444:4444
|
||||||
- 0.0.0.0:4445:4445
|
|
||||||
custom_bind_mounts: []
|
custom_bind_mounts: []
|
||||||
extra_hosts: []
|
extra_hosts: []
|
||||||
extra_env: []
|
extra_env: []
|
||||||
@@ -128,12 +134,55 @@ docker:
|
|||||||
ulimits: []
|
ulimits: []
|
||||||
'so-soc':
|
'so-soc':
|
||||||
final_octet: 34
|
final_octet: 34
|
||||||
|
networks: ['sobridge', 'soauth']
|
||||||
port_bindings:
|
port_bindings:
|
||||||
- 0.0.0.0:9822:9822
|
- 0.0.0.0:9822:9822
|
||||||
custom_bind_mounts: []
|
custom_bind_mounts: []
|
||||||
extra_hosts: []
|
extra_hosts: []
|
||||||
extra_env: []
|
extra_env: []
|
||||||
ulimits: []
|
ulimits: []
|
||||||
|
'so-strelka-backend':
|
||||||
|
final_octet: 36
|
||||||
|
custom_bind_mounts: []
|
||||||
|
extra_hosts: []
|
||||||
|
extra_env: []
|
||||||
|
ulimits: []
|
||||||
|
'so-strelka-filestream':
|
||||||
|
final_octet: 37
|
||||||
|
custom_bind_mounts: []
|
||||||
|
extra_hosts: []
|
||||||
|
extra_env: []
|
||||||
|
ulimits: []
|
||||||
|
'so-strelka-frontend':
|
||||||
|
final_octet: 38
|
||||||
|
port_bindings:
|
||||||
|
- 0.0.0.0:57314:57314
|
||||||
|
custom_bind_mounts: []
|
||||||
|
extra_hosts: []
|
||||||
|
extra_env: []
|
||||||
|
ulimits: []
|
||||||
|
'so-strelka-manager':
|
||||||
|
final_octet: 39
|
||||||
|
custom_bind_mounts: []
|
||||||
|
extra_hosts: []
|
||||||
|
extra_env: []
|
||||||
|
ulimits: []
|
||||||
|
'so-strelka-gatekeeper':
|
||||||
|
final_octet: 40
|
||||||
|
port_bindings:
|
||||||
|
- 0.0.0.0:6381:6379
|
||||||
|
custom_bind_mounts: []
|
||||||
|
extra_hosts: []
|
||||||
|
extra_env: []
|
||||||
|
ulimits: []
|
||||||
|
'so-strelka-coordinator':
|
||||||
|
final_octet: 41
|
||||||
|
port_bindings:
|
||||||
|
- 0.0.0.0:6380:6379
|
||||||
|
custom_bind_mounts: []
|
||||||
|
extra_hosts: []
|
||||||
|
extra_env: []
|
||||||
|
ulimits: []
|
||||||
'so-elastalert':
|
'so-elastalert':
|
||||||
final_octet: 42
|
final_octet: 42
|
||||||
custom_bind_mounts: []
|
custom_bind_mounts: []
|
||||||
@@ -195,3 +244,11 @@ docker:
|
|||||||
extra_hosts: []
|
extra_hosts: []
|
||||||
extra_env: []
|
extra_env: []
|
||||||
ulimits: []
|
ulimits: []
|
||||||
|
'so-postgres':
|
||||||
|
final_octet: 47
|
||||||
|
port_bindings:
|
||||||
|
- 0.0.0.0:5432:5432
|
||||||
|
custom_bind_mounts: []
|
||||||
|
extra_hosts: []
|
||||||
|
extra_env: []
|
||||||
|
ulimits: []
|
||||||
@@ -1,8 +1,26 @@
|
|||||||
{% import_yaml 'docker/defaults.yaml' as DOCKERDEFAULTS %}
|
{% import_yaml 'docker/defaults.yaml' as DOCKERDEFAULTS %}
|
||||||
{% set DOCKERMERGED = salt['pillar.get']('docker', DOCKERDEFAULTS.docker, merge=True) %}
|
{% set DOCKERMERGED = salt['pillar.get']('docker', DOCKERDEFAULTS.docker, merge=True) %}
|
||||||
{% set RANGESPLIT = DOCKERMERGED.range.split('.') %}
|
|
||||||
{% set FIRSTTHREE = RANGESPLIT[0] ~ '.' ~ RANGESPLIT[1] ~ '.' ~ RANGESPLIT[2] ~ '.' %}
|
{% if DOCKERMERGED.networks.sobridge is not mapping %}
|
||||||
|
{% do DOCKERMERGED.networks.update({'sobridge': {}}) %}
|
||||||
|
{% endif %}
|
||||||
|
{% do DOCKERMERGED.networks['sobridge'].update({'range': DOCKERMERGED.range, 'gateway': DOCKERMERGED.gateway}) %}
|
||||||
|
|
||||||
|
{% for netname, net in DOCKERMERGED.networks.items() %}
|
||||||
|
{% set RANGESPLIT = net.range.split('.') %}
|
||||||
|
{% do net.update({'prefix': RANGESPLIT[0] ~ '.' ~ RANGESPLIT[1] ~ '.' ~ RANGESPLIT[2] ~ '.'}) %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{% for container, vals in DOCKERMERGED.containers.items() %}
|
{% for container, vals in DOCKERMERGED.containers.items() %}
|
||||||
{% do DOCKERMERGED.containers[container].update({'ip': FIRSTTHREE ~ DOCKERMERGED.containers[container].final_octet}) %}
|
{% set CONTAINER_NETS = vals.get('networks', ['sobridge']) %}
|
||||||
|
{% set IPS = {} %}
|
||||||
|
{% for netname in CONTAINER_NETS %}
|
||||||
|
{% do IPS.update({netname: DOCKERMERGED.networks[netname].prefix ~ vals.final_octet}) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% do DOCKERMERGED.containers[container].update({
|
||||||
|
'networks': CONTAINER_NETS,
|
||||||
|
'ips': IPS,
|
||||||
|
'network': CONTAINER_NETS[0],
|
||||||
|
'ip': IPS[CONTAINER_NETS[0]]
|
||||||
|
}) %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
+14
-10
@@ -18,10 +18,10 @@ dockergroup:
|
|||||||
dockerheldpackages:
|
dockerheldpackages:
|
||||||
pkg.installed:
|
pkg.installed:
|
||||||
- pkgs:
|
- pkgs:
|
||||||
- containerd.io: 2.2.1-1.el9
|
- containerd.io: 2.3.6-1.el9
|
||||||
- docker-ce: 3:29.2.1-1.el9
|
- docker-ce: 3:29.8.1-1.el9
|
||||||
- docker-ce-cli: 1:29.2.1-1.el9
|
- docker-ce-cli: 1:29.8.1-1.el9
|
||||||
- docker-ce-rootless-extras: 29.2.1-1.el9
|
- docker-ce-rootless-extras: 29.8.1-1.el9
|
||||||
- hold: True
|
- hold: True
|
||||||
- update_holds: True
|
- update_holds: True
|
||||||
|
|
||||||
@@ -71,15 +71,19 @@ dockerreserveports:
|
|||||||
- source: salt://common/files/99-reserved-ports.conf
|
- source: salt://common/files/99-reserved-ports.conf
|
||||||
- name: /etc/sysctl.d/99-reserved-ports.conf
|
- name: /etc/sysctl.d/99-reserved-ports.conf
|
||||||
|
|
||||||
sos_docker_net:
|
{% for NETNAME, NETWORK in DOCKERMERGED.networks.items() %}
|
||||||
|
{% if not NETWORK.get('manager_only') or GLOBALS.get('is_manager', False) %}
|
||||||
|
sos_docker_net_{{ NETNAME }}:
|
||||||
docker_network.present:
|
docker_network.present:
|
||||||
- name: sobridge
|
- name: {{ NETNAME }}
|
||||||
- subnet: {{ DOCKERMERGED.range }}
|
- subnet: {{ NETWORK.range }}
|
||||||
- gateway: {{ DOCKERMERGED.gateway }}
|
- gateway: {{ NETWORK.gateway }}
|
||||||
- options:
|
- options:
|
||||||
com.docker.network.bridge.name: 'sobridge'
|
com.docker.network.bridge.name: '{{ NETNAME }}'
|
||||||
com.docker.network.driver.mtu: '1500'
|
com.docker.network.driver.mtu: '1500'
|
||||||
com.docker.network.bridge.enable_ip_masquerade: 'true'
|
com.docker.network.bridge.enable_ip_masquerade: 'true'
|
||||||
com.docker.network.bridge.enable_icc: 'true'
|
com.docker.network.bridge.enable_icc: 'true'
|
||||||
com.docker.network.bridge.host_binding_ipv4: '0.0.0.0'
|
com.docker.network.bridge.host_binding_ipv4: '0.0.0.0'
|
||||||
- unless: ip l | grep sobridge
|
- unless: ip l | grep {{ NETNAME }}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
@@ -7,6 +7,40 @@ docker:
|
|||||||
description: Default docker IP range for containers.
|
description: Default docker IP range for containers.
|
||||||
helpLink: docker
|
helpLink: docker
|
||||||
advanced: True
|
advanced: True
|
||||||
|
networks:
|
||||||
|
sobridge:
|
||||||
|
description: |
|
||||||
|
The default docker network, carrying most containers. Its range and gateway are taken
|
||||||
|
from the docker.range and docker.gateway settings above rather than set here.
|
||||||
|
helpLink: docker
|
||||||
|
readonly: True
|
||||||
|
advanced: True
|
||||||
|
global: True
|
||||||
|
soauth:
|
||||||
|
range:
|
||||||
|
description: |
|
||||||
|
IP range for the soauth docker network, an isolated network for the authentication
|
||||||
|
services, so that the Kratos and Hydra admin APIs are only reachable from the
|
||||||
|
containers placed on it.
|
||||||
|
helpLink: docker
|
||||||
|
readonly: True
|
||||||
|
advanced: True
|
||||||
|
global: True
|
||||||
|
gateway:
|
||||||
|
description: Gateway for the soauth docker network.
|
||||||
|
helpLink: docker
|
||||||
|
readonly: True
|
||||||
|
advanced: True
|
||||||
|
global: True
|
||||||
|
manager_only:
|
||||||
|
description: |
|
||||||
|
Limits the soauth network to grid members running the authentication containers,
|
||||||
|
instead of creating it on every node.
|
||||||
|
helpLink: docker
|
||||||
|
readonly: True
|
||||||
|
advanced: True
|
||||||
|
global: True
|
||||||
|
forcedType: bool
|
||||||
ulimits:
|
ulimits:
|
||||||
description: |
|
description: |
|
||||||
Default ulimit settings applied to all containers via the Docker daemon. Each entry specifies a resource name (e.g. nofile, memlock, core, nproc) with soft and hard limits. Individual container ulimits override these defaults. Valid resource names include: cpu, fsize, data, stack, core, rss, nproc, nofile, memlock, as, locks, sigpending, msgqueue, nice, rtprio, rttime.
|
Default ulimit settings applied to all containers via the Docker daemon. Each entry specifies a resource name (e.g. nofile, memlock, core, nproc) with soft and hard limits. Individual container ulimits override these defaults. Valid resource names include: cpu, fsize, data, stack, core, rss, nproc, nofile, memlock, as, locks, sigpending, msgqueue, nice, rtprio, rttime.
|
||||||
@@ -34,6 +68,16 @@ docker:
|
|||||||
readonly: True
|
readonly: True
|
||||||
advanced: True
|
advanced: True
|
||||||
global: True
|
global: True
|
||||||
|
networks:
|
||||||
|
description: |
|
||||||
|
Docker networks this container is attached to. The first entry is the container's
|
||||||
|
primary network and determines the address its published ports are forwarded to.
|
||||||
|
Defaults to sobridge when unset.
|
||||||
|
helpLink: docker
|
||||||
|
readonly: True
|
||||||
|
advanced: True
|
||||||
|
global: True
|
||||||
|
forcedType: "[]string"
|
||||||
port_bindings:
|
port_bindings:
|
||||||
description: List of port bindings for the container.
|
description: List of port bindings for the container.
|
||||||
helpLink: docker
|
helpLink: docker
|
||||||
@@ -89,6 +133,12 @@ docker:
|
|||||||
so-redis: *dockerOptions
|
so-redis: *dockerOptions
|
||||||
so-sensoroni: *dockerOptions
|
so-sensoroni: *dockerOptions
|
||||||
so-soc: *dockerOptions
|
so-soc: *dockerOptions
|
||||||
|
so-strelka-backend: *dockerOptions
|
||||||
|
so-strelka-filestream: *dockerOptions
|
||||||
|
so-strelka-frontend: *dockerOptions
|
||||||
|
so-strelka-manager: *dockerOptions
|
||||||
|
so-strelka-gatekeeper: *dockerOptions
|
||||||
|
so-strelka-coordinator: *dockerOptions
|
||||||
so-elastalert: *dockerOptions
|
so-elastalert: *dockerOptions
|
||||||
so-elastic-fleet-package-registry: *dockerOptions
|
so-elastic-fleet-package-registry: *dockerOptions
|
||||||
so-idh: *dockerOptions
|
so-idh: *dockerOptions
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
prune_images:
|
prune_images:
|
||||||
cmd.run:
|
cmd.run:
|
||||||
- name: so-docker-prune
|
- name: so-docker-prune
|
||||||
- order: last
|
- onlyif: command -v /usr/sbin/so-docker-prune >/dev/null 2>&1
|
||||||
|
- order: 9000
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ elastalert:
|
|||||||
- gid: 933
|
- gid: 933
|
||||||
- home: /opt/so/conf/elastalert
|
- home: /opt/so/conf/elastalert
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elastalogdir:
|
elastalogdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
@@ -33,8 +34,8 @@ elastalert_sbin:
|
|||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://elastalert/tools/sbin
|
- source: salt://elastalert/tools/sbin
|
||||||
- user: 933
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
|
|
||||||
#elastalert_sbin_jinja:
|
#elastalert_sbin_jinja:
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ elastalert:
|
|||||||
buffer_time:
|
buffer_time:
|
||||||
minutes: 10
|
minutes: 10
|
||||||
old_query_limit:
|
old_query_limit:
|
||||||
minutes: 5
|
minutes: 1440
|
||||||
es_port: 9200
|
es_port: 9200
|
||||||
es_conn_timeout: 55
|
es_conn_timeout: 55
|
||||||
max_query_size: 5000
|
max_query_size: 5000
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ wait_for_elasticsearch:
|
|||||||
so-elastalert:
|
so-elastalert:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastalert:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastalert:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: elastalert
|
- hostname: elastalert
|
||||||
- name: so-elastalert
|
- name: so-elastalert
|
||||||
- user: so-elastalert
|
- user: so-elastalert
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
import types
|
||||||
|
|
||||||
|
# stand-ins when ElastAlert isn't installed (CI)
|
||||||
|
try:
|
||||||
|
import elastalert.alerts # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
class Alerter:
|
||||||
|
def __init__(self, rule):
|
||||||
|
self.rule = rule
|
||||||
|
|
||||||
|
class DateTimeEncoder(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
return obj.isoformat() if hasattr(obj, 'isoformat') else json.JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
class EAException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def lookup_es_key(doc, term):
|
||||||
|
for part in term.split('.'):
|
||||||
|
if not isinstance(doc, dict) or part not in doc:
|
||||||
|
return None
|
||||||
|
doc = doc[part]
|
||||||
|
return doc
|
||||||
|
|
||||||
|
def ts_to_dt(value):
|
||||||
|
return value if isinstance(value, datetime) else datetime.fromisoformat(value)
|
||||||
|
|
||||||
|
def elasticsearch_client(conf):
|
||||||
|
return None # tests set the alerter's client
|
||||||
|
|
||||||
|
alerts = types.ModuleType('elastalert.alerts')
|
||||||
|
alerts.Alerter = Alerter
|
||||||
|
alerts.DateTimeEncoder = DateTimeEncoder
|
||||||
|
util = types.ModuleType('elastalert.util')
|
||||||
|
util.EAException = EAException
|
||||||
|
util.elastalert_logger = logging.getLogger('elastalert')
|
||||||
|
util.lookup_es_key = lookup_es_key
|
||||||
|
util.ts_to_dt = ts_to_dt
|
||||||
|
util.elasticsearch_client = elasticsearch_client
|
||||||
|
package = types.ModuleType('elastalert')
|
||||||
|
package.alerts = alerts
|
||||||
|
package.util = util
|
||||||
|
sys.modules.update({'elastalert': package, 'elastalert.alerts': alerts, 'elastalert.util': util})
|
||||||
|
|
||||||
|
# stand-ins when elasticsearch-py isn't installed (CI)
|
||||||
|
try:
|
||||||
|
import elasticsearch.exceptions # noqa: F401
|
||||||
|
except ImportError:
|
||||||
|
|
||||||
|
class ElasticsearchException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class TransportError(ElasticsearchException):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConnectionError(TransportError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class ConflictError(TransportError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class RequestError(TransportError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
exceptions = types.ModuleType('elasticsearch.exceptions')
|
||||||
|
for cls in (ElasticsearchException, TransportError, ConnectionError, ConflictError, RequestError):
|
||||||
|
setattr(exceptions, cls.__name__, cls)
|
||||||
|
es_package = types.ModuleType('elasticsearch')
|
||||||
|
es_package.exceptions = exceptions
|
||||||
|
sys.modules.update({'elasticsearch': es_package, 'elasticsearch.exceptions': exceptions})
|
||||||
@@ -5,59 +5,265 @@
|
|||||||
# https://securityonion.net/license; you may not use this file except in compliance with the
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
# Elastic License 2.0.
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
from datetime import datetime, timezone
|
||||||
from time import gmtime, strftime
|
import hashlib
|
||||||
import requests,json
|
import ipaddress
|
||||||
from elastalert.alerts import Alerter
|
import json
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
|
||||||
import urllib3
|
import urllib3
|
||||||
|
from elasticsearch.exceptions import ConflictError, ElasticsearchException, RequestError
|
||||||
|
from elastalert.alerts import Alerter, DateTimeEncoder
|
||||||
|
from elastalert.util import EAException, elastalert_logger, elasticsearch_client, lookup_es_key, ts_to_dt
|
||||||
|
|
||||||
|
# grid runs verify_certs: false; also quiets ElastAlert's own queries
|
||||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
|
||||||
|
ALERT_INDEX = 'logs-detections.alerts-so'
|
||||||
|
# ES error text kept in logs and alerts
|
||||||
|
ERROR_TEXT_LIMIT = 500
|
||||||
|
# a match missing backend columns (window_start, count, @timestamp); the alert is still written
|
||||||
|
MATCH_ERRORS = (KeyError, TypeError, ValueError)
|
||||||
|
|
||||||
|
|
||||||
class SecurityOnionESAlerter(Alerter):
|
class SecurityOnionESAlerter(Alerter):
|
||||||
"""
|
"""
|
||||||
Use matched data to create alerts in Elasticsearch.
|
Use matched data to create alerts in Elasticsearch.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
required_options = set(['detection_title', 'sigma_level'])
|
required_options = {'detection_title', 'sigma_level'}
|
||||||
optional_fields = ['sigma_category', 'sigma_product', 'sigma_service']
|
optional_fields = ['sigma_category', 'sigma_product', 'sigma_service', 'sigma_correlation']
|
||||||
|
|
||||||
|
# count column and default summary per type; stored alert data, so not localized
|
||||||
|
CORRELATION_COUNTS = {
|
||||||
|
'event_count': ('event_count', '%count% events'),
|
||||||
|
'value_count': ('value_count', '%count% distinct values'),
|
||||||
|
'temporal': ('event_type_count', '%count% correlated rules matched'),
|
||||||
|
'value_sum': ('value_sum', 'total %count%'),
|
||||||
|
'value_avg': ('value_avg', 'average %count%'),
|
||||||
|
'value_percentile': ('value_percentile', 'percentile %count%'),
|
||||||
|
'value_median': ('value_median', 'median %count%'),
|
||||||
|
}
|
||||||
|
PLACEHOLDER = re.compile(r'%([^%\s]+)%')
|
||||||
|
# group-by fields copied into ECS related.*
|
||||||
|
RELATED_USERS = {'user.name', 'winlog.event_data.TargetUserName', 'winlog.event_data.SubjectUserName'}
|
||||||
|
RELATED_HOSTS = {'host.name', 'host.hostname', 'winlog.computer_name'}
|
||||||
|
|
||||||
|
def __init__(self, rule):
|
||||||
|
super().__init__(rule)
|
||||||
|
# uses the grid's TLS, auth and timeout settings
|
||||||
|
self.es = elasticsearch_client(rule)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_correlation(self):
|
||||||
|
return bool(self.rule.get('sigma_correlation'))
|
||||||
|
|
||||||
|
def query_keys(self):
|
||||||
|
"""compound_query_key holds the list; query_key is flattened to a string."""
|
||||||
|
if self.rule.get('compound_query_key'):
|
||||||
|
return self.rule['compound_query_key']
|
||||||
|
if self.rule.get('query_key'):
|
||||||
|
return [self.rule['query_key']]
|
||||||
|
return []
|
||||||
|
|
||||||
|
def alert_id(self, match):
|
||||||
|
"""Stable id: window end + group values for correlations, source _id otherwise; random without one."""
|
||||||
|
if self.is_correlation:
|
||||||
|
# ungrouped rows have a hashed _id that changes with the count
|
||||||
|
values = ''.join(f"|{lookup_es_key(match, k)}" for k in self.query_keys())
|
||||||
|
key = f"{self.rule['detection_public_id']}|{ts_to_dt(match['@timestamp']).isoformat()}{values}"
|
||||||
|
elif match.get('_id'):
|
||||||
|
key = f"{self.rule['detection_public_id']}|{match['_id']}"
|
||||||
|
else:
|
||||||
|
return uuid.uuid4().hex
|
||||||
|
|
||||||
|
return hashlib.sha256(key.encode('utf-8')).hexdigest()
|
||||||
|
|
||||||
|
def group(self, match):
|
||||||
|
"""Group-by values, joined like ElastAlert's realert key."""
|
||||||
|
return ', '.join(str(lookup_es_key(match, k)) for k in self.query_keys())
|
||||||
|
|
||||||
|
def related_bucket(self, key):
|
||||||
|
if key == 'ip' or key.endswith('.ip'):
|
||||||
|
return 'ip'
|
||||||
|
if key in self.RELATED_USERS or key.endswith('.user.name'):
|
||||||
|
return 'user'
|
||||||
|
if key in self.RELATED_HOSTS:
|
||||||
|
return 'hosts'
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def valid_ip(value):
|
||||||
|
try:
|
||||||
|
ipaddress.ip_address(value)
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def related(self, match):
|
||||||
|
"""ECS related.* from group-by values; skips invalid IPs."""
|
||||||
|
related = {}
|
||||||
|
for key in self.query_keys():
|
||||||
|
bucket = self.related_bucket(key)
|
||||||
|
if not bucket:
|
||||||
|
continue
|
||||||
|
# original spellings of a lowercased group
|
||||||
|
value = lookup_es_key(match, f"{key}_spellings")
|
||||||
|
if value is None:
|
||||||
|
value = lookup_es_key(match, key)
|
||||||
|
for v in value if isinstance(value, list) else [value]:
|
||||||
|
if v is None or (bucket == 'ip' and not self.valid_ip(str(v))):
|
||||||
|
continue
|
||||||
|
# dict: ordered and deduped
|
||||||
|
related.setdefault(bucket, {})[str(v)] = None
|
||||||
|
return {bucket: list(values) for bucket, values in related.items()}
|
||||||
|
|
||||||
|
def event_data(self, match):
|
||||||
|
"""The match minus the compound query_key field, which ES would map by its last part."""
|
||||||
|
if not self.rule.get('compound_query_key'):
|
||||||
|
return match
|
||||||
|
return {k: v for k, v in match.items() if k != self.rule['query_key']}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_value(value):
|
||||||
|
if isinstance(value, list):
|
||||||
|
shown = ', '.join(str(v) for v in value[:3])
|
||||||
|
return shown if len(value) <= 3 else f"{shown} and {len(value) - 3} more"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_count(value):
|
||||||
|
if isinstance(value, float) and not value.is_integer():
|
||||||
|
return f"{value:,.2f}"
|
||||||
|
if isinstance(value, (int, float)):
|
||||||
|
return f"{int(value):,}"
|
||||||
|
return str(value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def format_duration(seconds):
|
||||||
|
for unit, size in (('hour', 3600), ('minute', 60)):
|
||||||
|
if seconds >= 2 * size:
|
||||||
|
return f"{seconds // size} {unit}s"
|
||||||
|
return f"{seconds} second{'' if seconds == 1 else 's'}"
|
||||||
|
|
||||||
|
def summary(self, match):
|
||||||
|
"""One-line correlation summary."""
|
||||||
|
column, label = self.CORRELATION_COUNTS.get(self.rule['sigma_correlation'], (None, '%count%'))
|
||||||
|
start = ts_to_dt(match['window_start'])
|
||||||
|
end = ts_to_dt(match['@timestamp'])
|
||||||
|
values = {
|
||||||
|
'count': self.format_count(match.get(column)),
|
||||||
|
'start': start.strftime('%Y-%m-%d %H:%M:%S UTC'),
|
||||||
|
'end': end.strftime('%Y-%m-%d %H:%M:%S UTC'),
|
||||||
|
'duration': self.format_duration(int((end - start).total_seconds())),
|
||||||
|
}
|
||||||
|
|
||||||
|
template = self.rule.get('summary_template')
|
||||||
|
if not template:
|
||||||
|
groups = ', '.join(f"{k} %{k}%" for k in self.query_keys())
|
||||||
|
template = f"{label} for {groups} in %duration%" if groups else f"{label} in %duration%"
|
||||||
|
|
||||||
|
def fill(m):
|
||||||
|
if m[1] in values:
|
||||||
|
return values[m[1]]
|
||||||
|
value = lookup_es_key(match, m[1])
|
||||||
|
# unknown placeholders stay visible so typos show
|
||||||
|
return m[0] if value is None else self.format_value(value)
|
||||||
|
|
||||||
|
return self.PLACEHOLDER.sub(fill, template)
|
||||||
|
|
||||||
def alert(self, matches):
|
def alert(self, matches):
|
||||||
for match in matches:
|
for match in matches:
|
||||||
timestamp = strftime("%Y-%m-%d"'T'"%H:%M:%S"'.000Z', gmtime())
|
try:
|
||||||
headers = {"Content-Type": "application/json"}
|
alert_id = self.alert_id(match)
|
||||||
|
except MATCH_ERRORS as e:
|
||||||
|
elastalert_logger.warning("Writing alert for rule %s without a stable id, so a retry may duplicate it: %r",
|
||||||
|
self.rule['detection_public_id'], e)
|
||||||
|
alert_id = uuid.uuid4().hex
|
||||||
|
try:
|
||||||
|
self.write(alert_id, self.payload(match))
|
||||||
|
except ElasticsearchException as e:
|
||||||
|
# EAException makes ElastAlert retry
|
||||||
|
raise EAException(f"Unable to write the alert to Elasticsearch: {str(e)[:ERROR_TEXT_LIMIT]}") from e
|
||||||
|
|
||||||
creds = None
|
def payload(self, match):
|
||||||
if 'es_username' in self.rule and 'es_password' in self.rule:
|
rule_info = {
|
||||||
creds = (self.rule['es_username'], self.rule['es_password'])
|
"name": self.rule['detection_title'],
|
||||||
|
"uuid": self.rule['detection_public_id']
|
||||||
|
}
|
||||||
|
|
||||||
# Start building the rule dict
|
# Add optional fields if they are present in the rule
|
||||||
rule_info = {
|
for field in self.optional_fields:
|
||||||
"name": self.rule['detection_title'],
|
rule_key = field.split('_')[-1] # Assumes field format "sigma_<key>"
|
||||||
"uuid": self.rule['detection_public_id']
|
if field in self.rule:
|
||||||
}
|
rule_info[rule_key] = self.rule[field]
|
||||||
|
|
||||||
# Add optional fields if they are present in the rule
|
event_info = {
|
||||||
for field in self.optional_fields:
|
"kind": "alert",
|
||||||
rule_key = field.split('_')[-1] # Assumes field format "sigma_<key>"
|
"severity": self.rule['event.severity'],
|
||||||
if field in self.rule:
|
"module": self.rule['event.module'],
|
||||||
rule_info[rule_key] = self.rule[field]
|
"dataset": self.rule['event.dataset'],
|
||||||
|
"severity_label": self.rule['sigma_level']
|
||||||
|
}
|
||||||
|
|
||||||
# Construct the payload with the conditional rule_info
|
payload = {
|
||||||
payload = {
|
"tags": ["alert"],
|
||||||
"tags": "alert",
|
"rule": rule_info,
|
||||||
"rule": rule_info,
|
"event": event_info,
|
||||||
"event": {
|
"sigma_level": self.rule['sigma_level'],
|
||||||
"severity": self.rule['event.severity'],
|
"event_data": self.event_data(match),
|
||||||
"module": self.rule['event.module'],
|
"@timestamp": datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%S.000Z')
|
||||||
"dataset": self.rule['event.dataset'],
|
}
|
||||||
"severity_label": self.rule['sigma_level']
|
|
||||||
},
|
if self.is_correlation:
|
||||||
"sigma_level": self.rule['sigma_level'],
|
keys = self.query_keys()
|
||||||
"event_data": match,
|
try:
|
||||||
"@timestamp": timestamp
|
# built before any is added, so a failure adds none
|
||||||
}
|
reason = self.summary(match)
|
||||||
url = f"https://{self.rule['es_host']}:{self.rule['es_port']}/logs-detections.alerts-so/_doc/"
|
labels = {"correlation_group_by": ', '.join(keys), "correlation_group": self.group(match)} if keys else None
|
||||||
requests.post(url, data=json.dumps(payload), headers=headers, verify=False, auth=creds)
|
related = self.related(match)
|
||||||
|
except MATCH_ERRORS as e:
|
||||||
|
elastalert_logger.warning("Writing alert for rule %s without its correlation summary: %r",
|
||||||
|
self.rule['detection_public_id'], e)
|
||||||
|
else:
|
||||||
|
payload["event"]["reason"] = reason
|
||||||
|
if labels:
|
||||||
|
payload["labels"] = labels
|
||||||
|
if related:
|
||||||
|
payload["related"] = related
|
||||||
|
|
||||||
|
return payload
|
||||||
|
|
||||||
|
def write(self, alert_id, payload):
|
||||||
|
try:
|
||||||
|
self.create(alert_id, payload)
|
||||||
|
except RequestError as e:
|
||||||
|
# mapping rejections come from event_data; retry it as text
|
||||||
|
rejection = str(e)[:ERROR_TEXT_LIMIT]
|
||||||
|
try:
|
||||||
|
self.create(alert_id, self.without_event_data(payload, rejection))
|
||||||
|
except RequestError as again:
|
||||||
|
elastalert_logger.error("Dropping alert %s for rule %s, rejected by Elasticsearch even without its event data: %s; first rejection: %s",
|
||||||
|
alert_id, self.rule['detection_public_id'], str(again)[:ERROR_TEXT_LIMIT], rejection)
|
||||||
|
return
|
||||||
|
elastalert_logger.warning("Stored alert %s for rule %s with its event data as text, rejected by Elasticsearch: %s",
|
||||||
|
alert_id, self.rule['detection_public_id'], rejection)
|
||||||
|
|
||||||
|
def create(self, alert_id, payload):
|
||||||
|
try:
|
||||||
|
self.es.create(index=ALERT_INDEX, id=alert_id, body=payload)
|
||||||
|
except ConflictError:
|
||||||
|
pass # a repeat id is already stored
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def without_event_data(payload, rejection):
|
||||||
|
"""Moves event_data to event.original; the tag keeps Fleet's final pipeline from dropping it."""
|
||||||
|
fallback = {k: v for k, v in payload.items() if k != 'event_data'}
|
||||||
|
fallback['event'] = dict(payload['event'], original=json.dumps(payload['event_data'], cls=DateTimeEncoder))
|
||||||
|
fallback['error'] = {'message': f"event_data rejected by Elasticsearch: {rejection}"}
|
||||||
|
fallback['tags'] = payload['tags'] + ['preserve_original_event']
|
||||||
|
return fallback
|
||||||
|
|
||||||
def get_info(self):
|
def get_info(self):
|
||||||
return {'type': 'SecurityOnionESAlerter'}
|
return {'type': 'SecurityOnionESAlerter'}
|
||||||
@@ -0,0 +1,260 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
import copy
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
import importlib.util
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
|
from elasticsearch.exceptions import ConflictError, ConnectionError, RequestError
|
||||||
|
|
||||||
|
spec = importlib.util.spec_from_file_location('securityonion_es', os.path.join(os.path.dirname(__file__), 'securityonion-es.py'))
|
||||||
|
es = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(es)
|
||||||
|
|
||||||
|
BASE_RULE = {
|
||||||
|
'detection_title': 'Many Failed Network Logons To One Host From One Source',
|
||||||
|
'detection_public_id': '35a42db6-6629-45af-b8aa-e1fa33c28ef5',
|
||||||
|
'sigma_level': 'medium',
|
||||||
|
'sigma_correlation': 'event_count',
|
||||||
|
'event.severity': 3,
|
||||||
|
'event.module': 'sigma',
|
||||||
|
'event.dataset': 'sigma.alert',
|
||||||
|
'summary_template': '%count% failed network logons to %host.name% from %source.ip% in %duration%',
|
||||||
|
}
|
||||||
|
|
||||||
|
PLAIN_RULE = {k: v for k, v in BASE_RULE.items() if k not in ('sigma_correlation', 'summary_template')}
|
||||||
|
|
||||||
|
|
||||||
|
def correlation_match():
|
||||||
|
return {
|
||||||
|
'event_count': 3561,
|
||||||
|
'window_start': '2026-09-30T18:05:10+00:00',
|
||||||
|
'@timestamp': '2026-09-30T18:07:53+00:00',
|
||||||
|
'host': {'name': 'host-01'},
|
||||||
|
'source': {'ip': '192.0.2.10'},
|
||||||
|
'_id': '6d1c',
|
||||||
|
'num_hits': 1,
|
||||||
|
'num_matches': 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestSecurityOnionESAlerter(unittest.TestCase):
|
||||||
|
|
||||||
|
def creates(self, rule, match, effects=None):
|
||||||
|
"""Run alert(); return (body, id) of each create."""
|
||||||
|
alerter = es.SecurityOnionESAlerter(rule)
|
||||||
|
alerter.es = MagicMock()
|
||||||
|
alerter.es.create.side_effect = effects
|
||||||
|
alerter.alert([match])
|
||||||
|
calls = alerter.es.create.call_args_list
|
||||||
|
self.assertTrue(all(c.kwargs['index'] == 'logs-detections.alerts-so' for c in calls))
|
||||||
|
return [(json.loads(json.dumps(c.kwargs['body'], cls=es.DateTimeEncoder)), c.kwargs['id']) for c in calls]
|
||||||
|
|
||||||
|
def send(self, rule, match):
|
||||||
|
"""Run alert(); return the payload it wrote and its id."""
|
||||||
|
(payload, alert_id), = self.creates(rule, match)
|
||||||
|
return payload, alert_id
|
||||||
|
|
||||||
|
def test_compound_query_key_left_out_of_event_data(self):
|
||||||
|
rule = dict(BASE_RULE, compound_query_key=['host.name', 'source.ip'], query_key='host.name,source.ip')
|
||||||
|
match = correlation_match()
|
||||||
|
match['host.name,source.ip'] = 'host-01, 192.0.2.10'
|
||||||
|
original = copy.deepcopy(match)
|
||||||
|
|
||||||
|
payload, _ = self.send(rule, match)
|
||||||
|
|
||||||
|
self.assertNotIn('host.name,source.ip', payload['event_data'])
|
||||||
|
self.assertEqual(payload['event_data']['host'], {'name': 'host-01'})
|
||||||
|
self.assertEqual(payload['event_data']['source'], {'ip': '192.0.2.10'})
|
||||||
|
self.assertEqual(payload['rule'], {'name': BASE_RULE['detection_title'], 'uuid': BASE_RULE['detection_public_id'], 'correlation': 'event_count'})
|
||||||
|
self.assertEqual(payload['labels'], {'correlation_group_by': 'host.name, source.ip', 'correlation_group': 'host-01, 192.0.2.10'})
|
||||||
|
self.assertEqual(payload['related'], {'hosts': ['host-01'], 'ip': ['192.0.2.10']})
|
||||||
|
self.assertEqual(payload['event']['kind'], 'alert')
|
||||||
|
self.assertEqual(payload['event']['reason'], '3,561 failed network logons to host-01 from 192.0.2.10 in 2 minutes')
|
||||||
|
# ElastAlert reuses the match
|
||||||
|
self.assertEqual(match, original)
|
||||||
|
|
||||||
|
def test_single_query_key(self):
|
||||||
|
rule = dict(BASE_RULE, query_key='user.name', summary_template='%count% failed SOC logins for %user.name%')
|
||||||
|
match = {'event_count': 3, 'window_start': '2026-09-30T16:49:52+00:00', '@timestamp': '2026-09-30T16:50:00+00:00',
|
||||||
|
'user': {'name': 'user@example.invalid'}}
|
||||||
|
|
||||||
|
payload, _ = self.send(rule, match)
|
||||||
|
|
||||||
|
self.assertEqual(payload['labels'], {'correlation_group_by': 'user.name', 'correlation_group': 'user@example.invalid'})
|
||||||
|
self.assertEqual(payload['related'], {'user': ['user@example.invalid']})
|
||||||
|
self.assertEqual(payload['event']['reason'], '3 failed SOC logins for user@example.invalid')
|
||||||
|
self.assertEqual(payload['event_data'], match)
|
||||||
|
|
||||||
|
def test_related_buckets(self):
|
||||||
|
rule = dict(BASE_RULE, compound_query_key=['winlog.event_data.TargetUserName', 'source.ip', 'dns.highest_registered_domain'],
|
||||||
|
query_key='winlog.event_data.TargetUserName,source.ip,dns.highest_registered_domain')
|
||||||
|
match = correlation_match()
|
||||||
|
match.update({'winlog': {'event_data': {'TargetUserName': ['admin1', 'svc', 'admin1']}},
|
||||||
|
'source': {'ip': 'not-an-ip'}, 'dns': {'highest_registered_domain': 'example.com'}})
|
||||||
|
|
||||||
|
payload, _ = self.send(rule, match)
|
||||||
|
|
||||||
|
# deduped; invalid IP skipped; domain stays in the group only
|
||||||
|
self.assertEqual(payload['related'], {'user': ['admin1', 'svc']})
|
||||||
|
self.assertEqual(payload['labels']['correlation_group'], "['admin1', 'svc', 'admin1'], not-an-ip, example.com")
|
||||||
|
|
||||||
|
payload, _ = self.send(dict(BASE_RULE, query_key='dns.highest_registered_domain'), match)
|
||||||
|
|
||||||
|
self.assertNotIn('related', payload)
|
||||||
|
|
||||||
|
def test_related_uses_original_spellings(self):
|
||||||
|
rule = dict(BASE_RULE, query_key='user.name', summary_template=None)
|
||||||
|
match = {'event_count': 3, 'window_start': '2026-09-30T16:49:52+00:00', '@timestamp': '2026-09-30T16:50:00+00:00',
|
||||||
|
'user': {'name': 'admin', 'name_spellings': ['Admin', 'admin', 'ADMIN']}}
|
||||||
|
|
||||||
|
payload, _ = self.send(rule, match)
|
||||||
|
|
||||||
|
# the group shows the lowercased value; related.user finds every spelling
|
||||||
|
self.assertEqual(payload['labels']['correlation_group'], 'admin')
|
||||||
|
self.assertEqual(payload['related'], {'user': ['Admin', 'admin', 'ADMIN']})
|
||||||
|
self.assertEqual(payload['event']['reason'], '3 events for user.name admin in 8 seconds')
|
||||||
|
|
||||||
|
def test_plain_rule_has_no_correlation_fields(self):
|
||||||
|
# a query_key alone (e.g. from an override) isn't a correlation
|
||||||
|
rule = dict(PLAIN_RULE, query_key='user.name')
|
||||||
|
# ElastAlert parses @timestamp for EQL hits
|
||||||
|
match = {'@timestamp': datetime(2026, 9, 30, 16, 50, tzinfo=timezone.utc), '_id': 'abc',
|
||||||
|
'process': {'name': 'whoami.exe'}, 'user': {'name': 'user'}}
|
||||||
|
|
||||||
|
payload, alert_id = self.send(rule, match)
|
||||||
|
|
||||||
|
alerter = es.SecurityOnionESAlerter(rule)
|
||||||
|
self.assertEqual(payload['rule'], {'name': PLAIN_RULE['detection_title'], 'uuid': PLAIN_RULE['detection_public_id']})
|
||||||
|
self.assertNotIn('labels', payload)
|
||||||
|
self.assertNotIn('related', payload)
|
||||||
|
self.assertNotIn('reason', payload['event'])
|
||||||
|
self.assertEqual(payload['event']['kind'], 'alert')
|
||||||
|
self.assertEqual(payload['event_data'], dict(match, **{'@timestamp': '2026-09-30T16:50:00+00:00'}))
|
||||||
|
self.assertEqual(alert_id, alerter.alert_id(match))
|
||||||
|
self.assertNotEqual(alerter.alert_id(match), alerter.alert_id(dict(match, _id='abd')))
|
||||||
|
self.assertNotEqual(alerter.alert_id(match), es.SecurityOnionESAlerter(dict(rule, detection_public_id='other')).alert_id(match))
|
||||||
|
# without an _id, never deduplicated
|
||||||
|
self.assertNotEqual(alerter.alert_id({'_id': None}), alerter.alert_id({'_id': None}))
|
||||||
|
|
||||||
|
def test_ungrouped_correlation_id_ignores_row_hash(self):
|
||||||
|
rule = dict(BASE_RULE, summary_template=None)
|
||||||
|
first = {k: v for k, v in correlation_match().items() if k not in ('host', 'source')}
|
||||||
|
# ES|QL hashes the row into _id, so a later count changes it
|
||||||
|
later = dict(first, event_count=3600, _id='9f2a')
|
||||||
|
|
||||||
|
payload, alert_id = self.send(rule, first)
|
||||||
|
|
||||||
|
alerter = es.SecurityOnionESAlerter(rule)
|
||||||
|
self.assertEqual(alerter.alert_id(first), alerter.alert_id(later))
|
||||||
|
self.assertNotEqual(alerter.alert_id(first), alerter.alert_id(dict(first, **{'@timestamp': '2026-09-30T18:09:00+00:00'})))
|
||||||
|
self.assertEqual(alert_id, alerter.alert_id(first))
|
||||||
|
self.assertEqual(payload['event']['reason'], '3,561 events in 2 minutes')
|
||||||
|
self.assertNotIn('labels', payload)
|
||||||
|
|
||||||
|
def test_temporal_count_column(self):
|
||||||
|
rule = dict(BASE_RULE, sigma_correlation='temporal', summary_template=None)
|
||||||
|
match = {'event_type_count': 2, 'window_start': '2026-09-30T16:49:52+00:00', '@timestamp': '2026-09-30T16:50:00+00:00'}
|
||||||
|
|
||||||
|
payload, _ = self.send(rule, match)
|
||||||
|
|
||||||
|
self.assertEqual(payload['event']['reason'], '2 correlated rules matched in 8 seconds')
|
||||||
|
|
||||||
|
def test_grouped_correlation_id_unchanged(self):
|
||||||
|
"""Ids of alerts already written must not change."""
|
||||||
|
rule = dict(BASE_RULE, compound_query_key=['host.name', 'source.ip'], query_key='host.name,source.ip')
|
||||||
|
key = f"{BASE_RULE['detection_public_id']}|2026-09-30T18:07:53+00:00|host-01|192.0.2.10"
|
||||||
|
|
||||||
|
self.assertEqual(es.SecurityOnionESAlerter(rule).alert_id(correlation_match()), es.hashlib.sha256(key.encode()).hexdigest())
|
||||||
|
|
||||||
|
def test_rejected_event_data_is_kept_as_text(self):
|
||||||
|
rule = dict(BASE_RULE, query_key='user.name', summary_template='%count% failed SOC logins for %user.name%')
|
||||||
|
match = {'event_count': 3, 'window_start': '2026-09-30T16:49:52+00:00', '@timestamp': '2026-09-30T16:50:00+00:00',
|
||||||
|
'user': {'name': 'user@example.invalid'}}
|
||||||
|
rejected = RequestError(400, 'document_parsing_exception', {})
|
||||||
|
|
||||||
|
(first, _), (second, _) = self.creates(rule, match, [rejected, None])
|
||||||
|
|
||||||
|
self.assertIn('event_data', first)
|
||||||
|
self.assertNotIn('event_data', second)
|
||||||
|
self.assertEqual(json.loads(second['event']['original']), match)
|
||||||
|
self.assertEqual(second['tags'], ['alert', 'preserve_original_event'])
|
||||||
|
self.assertEqual(first['tags'], ['alert'])
|
||||||
|
self.assertTrue(second['error']['message'].startswith('event_data rejected by Elasticsearch: '))
|
||||||
|
self.assertIn('document_parsing_exception', second['error']['message'])
|
||||||
|
# everything else carries over
|
||||||
|
self.assertEqual({k: v for k, v in second['event'].items() if k != 'original'}, first['event'])
|
||||||
|
changed = ('event_data', 'event', 'error', 'tags')
|
||||||
|
self.assertEqual({k: v for k, v in second.items() if k not in changed}, {k: v for k, v in first.items() if k not in changed})
|
||||||
|
|
||||||
|
def test_rejected_dates_are_kept_as_text(self):
|
||||||
|
match = {'@timestamp': datetime(2026, 9, 30, 16, 50, tzinfo=timezone.utc), '_id': 'abc'}
|
||||||
|
|
||||||
|
_, (second, _) = self.creates(PLAIN_RULE, match, [RequestError(400, 'document_parsing_exception', {}), None])
|
||||||
|
|
||||||
|
self.assertEqual(json.loads(second['event']['original']), {'@timestamp': '2026-09-30T16:50:00+00:00', '_id': 'abc'})
|
||||||
|
|
||||||
|
def test_rejected_twice_is_dropped_without_retry(self):
|
||||||
|
rejected = RequestError(400, 'document_parsing_exception', {})
|
||||||
|
match = {'@timestamp': '2026-09-30T16:50:00+00:00', '_id': 'abc'}
|
||||||
|
|
||||||
|
with self.assertLogs('elastalert', 'ERROR') as logs:
|
||||||
|
self.assertEqual(len(self.creates(PLAIN_RULE, match, [rejected, rejected])), 2)
|
||||||
|
|
||||||
|
self.assertIn('Dropping alert', logs.output[0])
|
||||||
|
|
||||||
|
def test_write_failure_is_retried(self):
|
||||||
|
match = {'@timestamp': '2026-09-30T16:50:00+00:00', '_id': 'abc'}
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(es.EAException, 'Unable to write the alert to Elasticsearch'):
|
||||||
|
self.creates(PLAIN_RULE, match, [ConnectionError('N/A', 'refused', None)])
|
||||||
|
|
||||||
|
def test_repeat_id_is_ignored(self):
|
||||||
|
match = {'@timestamp': '2026-09-30T16:50:00+00:00', '_id': 'abc'}
|
||||||
|
|
||||||
|
self.assertEqual(len(self.creates(PLAIN_RULE, match, [ConflictError(409, 'version_conflict_engine_exception', {})])), 1)
|
||||||
|
|
||||||
|
def test_correlation_fields_are_optional(self):
|
||||||
|
rule = dict(BASE_RULE, query_key='source.ip')
|
||||||
|
# no window_start: the summary cannot be built
|
||||||
|
match = {k: v for k, v in correlation_match().items() if k != 'window_start'}
|
||||||
|
|
||||||
|
with self.assertLogs('elastalert', 'WARNING') as logs:
|
||||||
|
payload, _ = self.send(rule, match)
|
||||||
|
|
||||||
|
self.assertIn('without its correlation summary', logs.output[0])
|
||||||
|
self.assertEqual(payload['event']['kind'], 'alert')
|
||||||
|
self.assertNotIn('reason', payload['event'])
|
||||||
|
self.assertNotIn('labels', payload)
|
||||||
|
self.assertNotIn('related', payload)
|
||||||
|
|
||||||
|
def test_unstable_id_still_writes(self):
|
||||||
|
# no @timestamp: the window end is unknown
|
||||||
|
match = {k: v for k, v in correlation_match().items() if k not in ('@timestamp', 'window_start')}
|
||||||
|
|
||||||
|
with self.assertLogs('elastalert', 'WARNING') as logs:
|
||||||
|
payload, alert_id = self.send(BASE_RULE, match)
|
||||||
|
|
||||||
|
self.assertIn('without a stable id', logs.output[0])
|
||||||
|
self.assertEqual(len(alert_id), 32)
|
||||||
|
self.assertEqual(payload['event_data'], match)
|
||||||
|
|
||||||
|
def test_summary_formats_values(self):
|
||||||
|
rule = dict(BASE_RULE, sigma_correlation='value_avg', query_key='source.ip',
|
||||||
|
summary_template='%count% for %source.ip% to %destination.port% %no.such.field%')
|
||||||
|
match = {'value_avg': 2.5, 'window_start': '2026-09-30T16:49:52+00:00', '@timestamp': '2026-09-30T16:50:00+00:00',
|
||||||
|
'source': {'ip': '192.0.2.10'}, 'destination': {'port': [22, 80, 443, 8080, 8443]}}
|
||||||
|
|
||||||
|
payload, _ = self.send(rule, match)
|
||||||
|
|
||||||
|
self.assertEqual(payload['event']['reason'], '2.50 for 192.0.2.10 to 22, 80, 443 and 2 more %no.such.field%')
|
||||||
|
self.assertEqual(es.SecurityOnionESAlerter.format_count('n/a'), 'n/a')
|
||||||
|
|
||||||
|
def test_get_info(self):
|
||||||
|
self.assertEqual(es.SecurityOnionESAlerter(PLAIN_RULE).get_info(), {'type': 'SecurityOnionESAlerter'})
|
||||||
@@ -120,7 +120,7 @@ elastalert:
|
|||||||
helpLink: elastalert
|
helpLink: elastalert
|
||||||
old_query_limit:
|
old_query_limit:
|
||||||
minutes:
|
minutes:
|
||||||
description: Amount of time in minutes between queries to start at the most recently run query.
|
description: How long ElastAlert can be down, in minutes, and still resume each rule where it stopped. After a longer outage, rules restart from now and skip the gap.
|
||||||
global: True
|
global: True
|
||||||
helpLink: elastalert
|
helpLink: elastalert
|
||||||
es_conn_timeout:
|
es_conn_timeout:
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ elastic-agent-pr:
|
|||||||
- gid: 948
|
- gid: 948
|
||||||
- home: /opt/so/conf/elastic-fleet-pr
|
- home: /opt/so/conf/elastic-fleet-pr
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ include:
|
|||||||
so-elastic-fleet-package-registry:
|
so-elastic-fleet-package-registry:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-fleet-package-registry:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-fleet-package-registry:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-elastic-fleet-package-registry
|
- name: so-elastic-fleet-package-registry
|
||||||
- hostname: Fleet-package-reg-{{ GLOBALS.hostname }}
|
- hostname: Fleet-package-reg-{{ GLOBALS.hostname }}
|
||||||
- detach: True
|
- detach: True
|
||||||
@@ -51,6 +52,16 @@ so-elastic-fleet-package-registry:
|
|||||||
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
- {{ ULIMIT.name }}={{ ULIMIT.soft }}:{{ ULIMIT.hard }}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
wait_for_so-elastic-fleet-package-registry:
|
||||||
|
http.wait_for_successful_query:
|
||||||
|
- name: "http://localhost:8080/health"
|
||||||
|
- status: 200
|
||||||
|
- wait_for: 300
|
||||||
|
- request_interval: 15
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elastic-fleet-package-registry
|
||||||
|
|
||||||
delete_so-elastic-fleet-package-registry_so-status.disabled:
|
delete_so-elastic-fleet-package-registry_so-status.disabled:
|
||||||
file.uncomment:
|
file.uncomment:
|
||||||
- name: /opt/so/conf/so-status/so-status.conf
|
- name: /opt/so/conf/so-status/so-status.conf
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ elastic-agent:
|
|||||||
- gid: 949
|
- gid: 949
|
||||||
- home: /opt/so/conf/elastic-agent
|
- home: /opt/so/conf/elastic-agent
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticagentconfdir:
|
elasticagentconfdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
@@ -39,8 +40,8 @@ elasticagent_sbin_jinja:
|
|||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://elasticagent/tools/sbin_jinja
|
- source: salt://elasticagent/tools/sbin_jinja
|
||||||
- user: 949
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
- template: jinja
|
- template: jinja
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ include:
|
|||||||
so-elastic-agent:
|
so-elastic-agent:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-elastic-agent
|
- name: so-elastic-agent
|
||||||
- hostname: {{ GLOBALS.hostname }}
|
- hostname: {{ GLOBALS.hostname }}
|
||||||
- detach: True
|
- detach: True
|
||||||
|
|||||||
@@ -26,13 +26,14 @@ elastic-fleet:
|
|||||||
- gid: 947
|
- gid: 947
|
||||||
- home: /opt/so/conf/elastic-fleet
|
- home: /opt/so/conf/elastic-fleet
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticfleet_sbin:
|
elasticfleet_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://elasticfleet/tools/sbin
|
- source: salt://elasticfleet/tools/sbin
|
||||||
- user: 947
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
- show_changes: False
|
- show_changes: False
|
||||||
|
|
||||||
@@ -40,8 +41,8 @@ elasticfleet_sbin_jinja:
|
|||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://elasticfleet/tools/sbin_jinja
|
- source: salt://elasticfleet/tools/sbin_jinja
|
||||||
- user: 947
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
- template: jinja
|
- template: jinja
|
||||||
- exclude_pat:
|
- exclude_pat:
|
||||||
@@ -81,8 +82,8 @@ eapackageupgrade:
|
|||||||
file.managed:
|
file.managed:
|
||||||
- name: /usr/sbin/so-elastic-fleet-package-upgrade
|
- name: /usr/sbin/so-elastic-fleet-package-upgrade
|
||||||
- source: salt://elasticfleet/tools/sbin_jinja/so-elastic-fleet-package-upgrade
|
- source: salt://elasticfleet/tools/sbin_jinja/so-elastic-fleet-package-upgrade
|
||||||
- user: 947
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- mode: 755
|
- mode: 755
|
||||||
- template: jinja
|
- template: jinja
|
||||||
|
|
||||||
@@ -173,7 +174,7 @@ eaoptionalintegrationsdir:
|
|||||||
|
|
||||||
{% for minion in node_data %}
|
{% for minion in node_data %}
|
||||||
{% set role = node_data[minion]["role"] %}
|
{% set role = node_data[minion]["role"] %}
|
||||||
{% if role in [ "eval","fleet","heavynode","import","manager", "managerhype", "managersearch","standalone" ] %}
|
{% if role in [ "eval","fleet","import","manager", "managerhype", "managersearch","standalone" ] %}
|
||||||
{% set optional_integrations = ELASTICFLEETMERGED.optional_integrations %}
|
{% set optional_integrations = ELASTICFLEETMERGED.optional_integrations %}
|
||||||
{% set integration_keys = optional_integrations.keys() %}
|
{% set integration_keys = optional_integrations.keys() %}
|
||||||
fleet_server_integrations_{{ minion }}:
|
fleet_server_integrations_{{ minion }}:
|
||||||
|
|||||||
@@ -0,0 +1,122 @@
|
|||||||
|
{# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
or more contributor license agreements. Licensed under the Elastic License 2.0; you may not use
|
||||||
|
this file except in compliance with the Elastic License 2.0. #}
|
||||||
|
|
||||||
|
|
||||||
|
{% import_json '/opt/so/state/esfleet_content_package_components.json' as ADDON_CONTENT_PACKAGE_COMPONENTS %}
|
||||||
|
{% import_json '/opt/so/state/esfleet_component_templates.json' as INSTALLED_COMPONENT_TEMPLATES %}
|
||||||
|
{% import_yaml 'elasticfleet/defaults.yaml' as ELASTICFLEETDEFAULTS %}
|
||||||
|
|
||||||
|
{% set CORE_ESFLEET_PACKAGES = ELASTICFLEETDEFAULTS.get('elasticfleet', {}).get('packages', {}) %}
|
||||||
|
{% set ADDON_CONTENT_INTEGRATION_DEFAULTS = {} %}
|
||||||
|
|
||||||
|
{% for pkg in ADDON_CONTENT_PACKAGE_COMPONENTS %}
|
||||||
|
{% if pkg.name in CORE_ESFLEET_PACKAGES %}
|
||||||
|
{# skip core content packages #}
|
||||||
|
{% elif pkg.name not in CORE_ESFLEET_PACKAGES %}
|
||||||
|
{# generate defaults for each content package #}
|
||||||
|
{% if pkg.dataStreams is defined and pkg.dataStreams is not none and pkg.dataStreams | length > 0%}
|
||||||
|
{% for pattern in pkg.dataStreams %}
|
||||||
|
{# in ES 9.3.2 'input' type integrations no longer create default component templates and instead they wait for user input during 'integration' setup (fleet ui config)
|
||||||
|
title: generic is an artifact of that and is not in use #}
|
||||||
|
{% if pattern.title == "generic" %}
|
||||||
|
{% continue %}
|
||||||
|
{% endif %}
|
||||||
|
{% if "metrics-" in pattern.name %}
|
||||||
|
{% set integration_type = "metrics-" %}
|
||||||
|
{% elif "logs-" in pattern.name %}
|
||||||
|
{% set integration_type = "logs-" %}
|
||||||
|
{% else %}
|
||||||
|
{% set integration_type = "" %}
|
||||||
|
{% endif %}
|
||||||
|
{# on content integrations the component name is user defined at the time it is added to an agent policy #}
|
||||||
|
{% set component_name = pattern.title %}
|
||||||
|
{% set index_pattern = pattern.name %}
|
||||||
|
{# component_name_x maintains the functionality of merging local pillar changes with generated 'defaults' via SOC UI #}
|
||||||
|
{% set component_name_x = component_name.replace(".","_x_") %}
|
||||||
|
{# pillar overrides/merge expects the key names to follow the naming in elasticsearch/defaults.yaml eg. so-logs-1password_x_item_usages . The _x_ is replaced later on in elasticsearch/template.map.jinja #}
|
||||||
|
{% set integration_key = "so-" ~ integration_type ~ pkg.name + '_x_' ~ component_name_x %}
|
||||||
|
{# Default integration settings #}
|
||||||
|
{% set integration_defaults = {
|
||||||
|
"index_sorting": false,
|
||||||
|
"index_template": {
|
||||||
|
"composed_of": [integration_type ~ component_name ~ "@package", integration_type ~ component_name ~ "@custom", "so-fleet_integrations.ip_mappings-1", "so-fleet_globals-1", "so-fleet_agent_id_verification-1"],
|
||||||
|
"data_stream": {
|
||||||
|
"allow_custom_routing": false,
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"ignore_missing_component_templates": [integration_type ~ component_name ~ "@custom"],
|
||||||
|
"index_patterns": [index_pattern],
|
||||||
|
"priority": 501,
|
||||||
|
"template": {
|
||||||
|
"settings": {
|
||||||
|
"index": {
|
||||||
|
"lifecycle": {"name": "so-" ~ integration_type ~ component_name ~ "-logs"},
|
||||||
|
"number_of_replicas": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"phases": {
|
||||||
|
"cold": {
|
||||||
|
"actions": {
|
||||||
|
"allocate":{
|
||||||
|
"number_of_replicas": ""
|
||||||
|
},
|
||||||
|
"set_priority": {"priority": 0}
|
||||||
|
},
|
||||||
|
"min_age": "60d"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"actions": {
|
||||||
|
"delete": {}
|
||||||
|
},
|
||||||
|
"min_age": "365d"
|
||||||
|
},
|
||||||
|
"hot": {
|
||||||
|
"actions": {
|
||||||
|
"rollover": {
|
||||||
|
"max_age": "30d",
|
||||||
|
"max_primary_shard_size": "50gb"
|
||||||
|
},
|
||||||
|
"forcemerge":{
|
||||||
|
"max_num_segments": ""
|
||||||
|
},
|
||||||
|
"shrink":{
|
||||||
|
"max_primary_shard_size": "",
|
||||||
|
"method": "COUNT",
|
||||||
|
"number_of_shards": ""
|
||||||
|
},
|
||||||
|
"set_priority": {"priority": 100}
|
||||||
|
},
|
||||||
|
"min_age": "0ms"
|
||||||
|
},
|
||||||
|
"warm": {
|
||||||
|
"actions": {
|
||||||
|
"allocate": {
|
||||||
|
"number_of_replicas": ""
|
||||||
|
},
|
||||||
|
"forcemerge": {
|
||||||
|
"max_num_segments": ""
|
||||||
|
},
|
||||||
|
"shrink":{
|
||||||
|
"max_primary_shard_size": "",
|
||||||
|
"method": "COUNT",
|
||||||
|
"number_of_shards": ""
|
||||||
|
},
|
||||||
|
"set_priority": {"priority": 50}
|
||||||
|
},
|
||||||
|
"min_age": "30d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} %}
|
||||||
|
|
||||||
|
|
||||||
|
{% do ADDON_CONTENT_INTEGRATION_DEFAULTS.update({integration_key: integration_defaults}) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% else %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
+17
-102
@@ -17,65 +17,19 @@ include:
|
|||||||
- logstash.ssl
|
- logstash.ssl
|
||||||
- elasticfleet.config
|
- elasticfleet.config
|
||||||
- elasticfleet.sostatus
|
- elasticfleet.sostatus
|
||||||
|
{%- if GLOBALS.role != "so-fleet" %}
|
||||||
|
- elasticfleet.manager
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
{% if grains.role not in ['so-fleet'] %}
|
{% if GLOBALS.role != "so-fleet" %}
|
||||||
# Wait for Elasticsearch to be ready - no reason to try running Elastic Fleet server if ES is not ready
|
# Wait for Elasticsearch to be ready - no reason to try running Elastic Fleet server if ES is not ready
|
||||||
wait_for_elasticsearch_elasticfleet:
|
wait_for_elasticsearch_elasticfleet:
|
||||||
cmd.run:
|
cmd.run:
|
||||||
- name: so-elasticsearch-wait
|
- name: so-elasticsearch-wait
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
# If enabled, automatically update Fleet Logstash Outputs
|
{% if GLOBALS.role == "so-fleet" %}
|
||||||
{% if ELASTICFLEETMERGED.config.server.enable_auto_configuration and grains.role not in ['so-import', 'so-eval', 'so-fleet'] %}
|
|
||||||
so-elastic-fleet-auto-configure-logstash-outputs:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-outputs-update
|
|
||||||
- retry:
|
|
||||||
attempts: 4
|
|
||||||
interval: 30
|
|
||||||
|
|
||||||
{# Separate from above in order to catch elasticfleet-logstash.crt changes and force update to fleet output policy #}
|
|
||||||
so-elastic-fleet-auto-configure-logstash-outputs-force:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-outputs-update --certs
|
|
||||||
- retry:
|
|
||||||
attempts: 4
|
|
||||||
interval: 30
|
|
||||||
- onchanges:
|
|
||||||
- x509: etc_elasticfleet_logstash_crt
|
|
||||||
- x509: elasticfleet_kafka_crt
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
# If enabled, automatically update Fleet Server URLs & ES Connection
|
|
||||||
{% if ELASTICFLEETMERGED.config.server.enable_auto_configuration and grains.role not in ['so-fleet'] %}
|
|
||||||
so-elastic-fleet-auto-configure-server-urls:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-urls-update
|
|
||||||
- retry:
|
|
||||||
attempts: 4
|
|
||||||
interval: 30
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
# Automatically update Fleet Server Elasticsearch URLs & Agent Artifact URLs
|
|
||||||
{% if grains.role not in ['so-fleet'] %}
|
|
||||||
so-elastic-fleet-auto-configure-elasticsearch-urls:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-es-url-update
|
|
||||||
- retry:
|
|
||||||
attempts: 4
|
|
||||||
interval: 30
|
|
||||||
|
|
||||||
so-elastic-fleet-auto-configure-artifact-urls:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-artifacts-url-update
|
|
||||||
- retry:
|
|
||||||
attempts: 4
|
|
||||||
interval: 30
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
# Sync Elastic Agent artifacts to Fleet Node
|
# Sync Elastic Agent artifacts to Fleet Node
|
||||||
{% if grains.role in ['so-fleet'] %}
|
|
||||||
elasticagent_syncartifacts:
|
elasticagent_syncartifacts:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /nsm/elastic-fleet/artifacts/beats
|
- name: /nsm/elastic-fleet/artifacts/beats
|
||||||
@@ -88,6 +42,7 @@ elasticagent_syncartifacts:
|
|||||||
so-elastic-fleet:
|
so-elastic-fleet:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent:{{ GLOBALS.so_version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- name: so-elastic-fleet
|
- name: so-elastic-fleet
|
||||||
- hostname: FleetServer-{{ GLOBALS.hostname }}
|
- hostname: FleetServer-{{ GLOBALS.hostname }}
|
||||||
- detach: True
|
- detach: True
|
||||||
@@ -147,59 +102,19 @@ so-elastic-fleet:
|
|||||||
- file: trusttheca
|
- file: trusttheca
|
||||||
- x509: etc_elasticfleet_key
|
- x509: etc_elasticfleet_key
|
||||||
- x509: etc_elasticfleet_crt
|
- x509: etc_elasticfleet_crt
|
||||||
|
|
||||||
|
wait_for_so-elastic-fleet:
|
||||||
|
http.wait_for_successful_query:
|
||||||
|
- name: "https://localhost:8220/api/status"
|
||||||
|
- ssl: True
|
||||||
|
- verify_ssl: False
|
||||||
|
- status: 200
|
||||||
|
- wait_for: 300
|
||||||
|
- request_interval: 15
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elastic-fleet
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if GLOBALS.role != "so-fleet" %}
|
|
||||||
so-elastic-fleet-package-statefile:
|
|
||||||
file.managed:
|
|
||||||
- name: /opt/so/state/elastic_fleet_packages.txt
|
|
||||||
- contents: {{ELASTICFLEETMERGED.packages}}
|
|
||||||
|
|
||||||
so-elastic-fleet-package-upgrade:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-package-upgrade
|
|
||||||
- retry:
|
|
||||||
attempts: 3
|
|
||||||
interval: 10
|
|
||||||
- onchanges:
|
|
||||||
- file: /opt/so/state/elastic_fleet_packages.txt
|
|
||||||
|
|
||||||
so-elastic-fleet-integrations:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-integration-policy-load
|
|
||||||
- retry:
|
|
||||||
attempts: 3
|
|
||||||
interval: 10
|
|
||||||
|
|
||||||
so-elastic-agent-grid-upgrade:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-agent-grid-upgrade
|
|
||||||
- retry:
|
|
||||||
attempts: 12
|
|
||||||
interval: 5
|
|
||||||
|
|
||||||
so-elastic-fleet-integration-upgrade:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-integration-upgrade
|
|
||||||
- retry:
|
|
||||||
attempts: 3
|
|
||||||
interval: 10
|
|
||||||
|
|
||||||
{# Optional integrations script doesn't need the retries like so-elastic-fleet-integration-upgrade which loads the default integrations #}
|
|
||||||
so-elastic-fleet-addon-integrations:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elastic-fleet-optional-integrations-load
|
|
||||||
|
|
||||||
{% if ELASTICFLEETMERGED.config.defend_filters.enable_auto_configuration %}
|
|
||||||
so-elastic-defend-manage-filters-file-watch:
|
|
||||||
cmd.run:
|
|
||||||
- name: python3 /sbin/so-elastic-defend-manage-filters.py -c /opt/so/conf/elasticsearch/curl.config -d /opt/so/conf/elastic-fleet/defend-exclusions/disabled-filters.yaml -i /nsm/securityonion-resources/event_filters/ -i /opt/so/conf/elastic-fleet/defend-exclusions/rulesets/custom-filters/ &>> /opt/so/log/elasticfleet/elastic-defend-manage-filters.log
|
|
||||||
- onchanges:
|
|
||||||
- file: elasticdefendcustom
|
|
||||||
- file: elasticdefenddisabled
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
delete_so-elastic-fleet_so-status.disabled:
|
delete_so-elastic-fleet_so-status.disabled:
|
||||||
file.uncomment:
|
file.uncomment:
|
||||||
- name: /opt/so/conf/so-status/so-status.conf
|
- name: /opt/so/conf/so-status/so-status.conf
|
||||||
|
|||||||
+9
-2
@@ -9,16 +9,22 @@
|
|||||||
"namespace": "so",
|
"namespace": "so",
|
||||||
"description": "Zeek Import logs",
|
"description": "Zeek Import logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/import/*/zeek/logs/*.log"
|
"/nsm/import/*/zeek/logs/*.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "import",
|
"data_stream.dataset": "import",
|
||||||
"pipeline": "",
|
"pipeline": "",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -34,7 +40,8 @@
|
|||||||
"fingerprint_length": "64",
|
"fingerprint_length": "64",
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "kratos-logs",
|
"name": "kratos-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Kratos logs",
|
"description": "Kratos logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/kratos/kratos.log"
|
"/opt/so/log/kratos/kratos.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "kratos",
|
"data_stream.dataset": "kratos",
|
||||||
"pipeline": "kratos",
|
"pipeline": "kratos",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -48,10 +54,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,16 +9,22 @@
|
|||||||
"namespace": "so",
|
"namespace": "so",
|
||||||
"description": "Zeek logs",
|
"description": "Zeek logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/zeek/logs/current/*.log"
|
"/nsm/zeek/logs/current/*.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "zeek",
|
"data_stream.dataset": "zeek",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
"exclude_files": ["({%- endraw -%}{{ ELASTICFLEETMERGED.logging.zeek.excluded | join('|') }}{%- raw -%})(\\..+)?\\.log$"],
|
"exclude_files": ["({%- endraw -%}{{ ELASTICFLEETMERGED.logging.zeek.excluded | join('|') }}{%- raw -%})(\\..+)?\\.log$"],
|
||||||
@@ -30,10 +36,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
"package": {
|
"package": {
|
||||||
"name": "endpoint",
|
"name": "endpoint",
|
||||||
"title": "Elastic Defend",
|
"title": "Elastic Defend",
|
||||||
"version": "9.0.2",
|
"version": "9.4.1",
|
||||||
"requires_root": true
|
"requires_root": true
|
||||||
},
|
},
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
|
|||||||
@@ -6,21 +6,23 @@
|
|||||||
"name": "agent-monitor",
|
"name": "agent-monitor",
|
||||||
"namespace": "",
|
"namespace": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
|
"policy_id": "so-grid-nodes_general",
|
||||||
"policy_ids": [
|
"policy_ids": [
|
||||||
"so-grid-nodes_general"
|
"so-grid-nodes_general"
|
||||||
],
|
],
|
||||||
"output_id": null,
|
|
||||||
"vars": {},
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/agents/agent-monitor.log"
|
"/opt/so/log/agents/agent-monitor.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "agentmonitor",
|
"data_stream.dataset": "agentmonitor",
|
||||||
"pipeline": "elasticagent.monitor",
|
"pipeline": "elasticagent.monitor",
|
||||||
"parsers": "",
|
"parsers": "",
|
||||||
@@ -34,15 +36,16 @@
|
|||||||
"ignore_older": "72h",
|
"ignore_older": "72h",
|
||||||
"clean_inactive": -1,
|
"clean_inactive": -1,
|
||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": true,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": 64,
|
"file_identity_native": true,
|
||||||
"file_identity_native": false,
|
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"force": true
|
||||||
}
|
}
|
||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "hydra-logs",
|
"name": "hydra-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Hydra logs",
|
"description": "Hydra logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/hydra/hydra.log"
|
"/opt/so/log/hydra/hydra.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "hydra",
|
"data_stream.dataset": "hydra",
|
||||||
"pipeline": "hydra",
|
"pipeline": "hydra",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -34,10 +40,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "idh-logs",
|
"name": "idh-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "IDH integration",
|
"description": "IDH integration",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/idh/opencanary.log"
|
"/nsm/idh/opencanary.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "idh",
|
"data_stream.dataset": "idh",
|
||||||
"pipeline": "common",
|
"pipeline": "common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -31,10 +37,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,32 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "import-evtx-logs",
|
"name": "import-evtx-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Import Windows EVTX logs",
|
"description": "Import Windows EVTX logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/import/*/evtx/*.json"
|
"/nsm/import/*/evtx/*.json"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "import",
|
"data_stream.dataset": "import",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
"exclude_files": [
|
"exclude_files": [
|
||||||
"\\.gz$"
|
"\\.gz$"
|
||||||
],
|
],
|
||||||
"include_files": [],
|
"include_files": [],
|
||||||
"processors": "- dissect:\n tokenizer: \"/nsm/import/%{import.id}/evtx/%{import.file}\"\n field: \"log.file.path\"\n target_prefix: \"\"\n- decode_json_fields:\n fields: [\"message\"]\n target: \"\"\n- drop_fields:\n fields: [\"host\"]\n ignore_missing: true\n- add_fields:\n target: data_stream\n fields:\n type: logs\n dataset: system.security\n- add_fields:\n target: event\n fields:\n dataset: system.security\n module: system\n imported: true\n- add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.security-2.6.1\n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-Sysmon/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.sysmon_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.sysmon_operational\n module: windows\n imported: true\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.sysmon_operational-3.1.2\n- if:\n equals:\n winlog.channel: 'Application'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.application\n - add_fields:\n target: event\n fields:\n dataset: system.application\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.application-2.6.1\n- if:\n equals:\n winlog.channel: 'System'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: system.system\n - add_fields:\n target: event\n fields:\n dataset: system.system\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-system.system-2.6.1\n \n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-PowerShell/Operational'\n then: \n - add_fields:\n target: data_stream\n fields:\n dataset: windows.powershell_operational\n - add_fields:\n target: event\n fields:\n dataset: windows.powershell_operational\n module: windows\n - add_fields:\n target: \"@metadata\"\n fields:\n pipeline: logs-windows.powershell_operational-3.1.2\n- add_fields:\n target: data_stream\n fields:\n dataset: import",
|
"processors": "- dissect:\n tokenizer: \"/nsm/import/%{import.id}/evtx/%{import.file}\"\n field: \"log.file.path\"\n target_prefix: \"\"\n- decode_json_fields:\n fields: [\"message\"]\n target: \"\"\n- add_fields:\n target: event\n fields:\n dataset: windows.forwarded\n module: windows\n imported: true\n- add_fields:\n target: \"@metadata\"\n fields:\n pipeline: import.evtx\n- if:\n equals:\n winlog.channel: 'Security'\n then: \n - add_fields:\n target: event\n fields:\n dataset: system.security\n module: system\n- if:\n equals:\n winlog.channel: 'Windows PowerShell'\n then: \n - add_fields:\n target: event\n fields:\n dataset: windows.powershell\n module: windows\n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-Sysmon/Operational'\n then: \n - add_fields:\n target: event\n fields:\n dataset: windows.sysmon_operational\n module: windows\n imported: true\n- if:\n equals:\n winlog.channel: 'Application'\n then: \n - add_fields:\n target: event\n fields:\n dataset: system.application\n module: system\n- if:\n equals:\n winlog.channel: 'System'\n then: \n - add_fields:\n target: event\n fields:\n dataset: system.system\n module: system\n \n- if:\n equals:\n winlog.channel: 'Microsoft-Windows-PowerShell/Operational'\n then: \n - add_fields:\n target: event\n fields:\n dataset: windows.powershell_operational\n module: windows\n- add_fields:\n target: data_stream\n fields:\n type: logs\n dataset: import",
|
||||||
"tags": [
|
"tags": [
|
||||||
"import"
|
"import"
|
||||||
],
|
],
|
||||||
@@ -33,10 +39,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "import-suricata-logs",
|
"name": "import-suricata-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Import Suricata logs",
|
"description": "Import Suricata logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/import/*/suricata/eve*.json"
|
"/nsm/import/*/suricata/eve*.json"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "import",
|
"data_stream.dataset": "import",
|
||||||
"pipeline": "suricata.common",
|
"pipeline": "suricata.common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -32,10 +38,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,14 +4,18 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "rita-logs",
|
"name": "rita-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "RITA Logs",
|
"description": "RITA Logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
@@ -19,6 +23,8 @@
|
|||||||
"/nsm/rita/exploded-dns.csv",
|
"/nsm/rita/exploded-dns.csv",
|
||||||
"/nsm/rita/long-connections.csv"
|
"/nsm/rita/long-connections.csv"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "rita",
|
"data_stream.dataset": "rita",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
"exclude_files": [
|
"exclude_files": [
|
||||||
@@ -33,10 +39,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "so-ip-mappings",
|
"name": "so-ip-mappings",
|
||||||
|
"namespace": "so",
|
||||||
"description": "IP Description mappings",
|
"description": "IP Description mappings",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/custom-mappings/ip-descriptions.csv"
|
"/nsm/custom-mappings/ip-descriptions.csv"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "hostnamemappings",
|
"data_stream.dataset": "hostnamemappings",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
"exclude_files": [
|
"exclude_files": [
|
||||||
@@ -32,10 +38,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "soc-auth-sync-logs",
|
"name": "soc-auth-sync-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Security Onion - Elastic Auth Sync - Logs",
|
"description": "Security Onion - Elastic Auth Sync - Logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/soc/sync.log"
|
"/opt/so/log/soc/sync.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "soc",
|
"data_stream.dataset": "soc",
|
||||||
"pipeline": "common",
|
"pipeline": "common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -31,10 +37,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,20 +4,26 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "soc-detections-logs",
|
"name": "soc-detections-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Security Onion Console - Detections Logs",
|
"description": "Security Onion Console - Detections Logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/soc/detections_runtime-status_sigma.log",
|
"/opt/so/log/soc/detections_runtime-status_sigma.log",
|
||||||
"/opt/so/log/soc/detections_runtime-status_yara.log"
|
"/opt/so/log/soc/detections_runtime-status_yara.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "soc",
|
"data_stream.dataset": "soc",
|
||||||
"pipeline": "common",
|
"pipeline": "common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -35,10 +41,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "soc-salt-relay-logs",
|
"name": "soc-salt-relay-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Security Onion - Salt Relay - Logs",
|
"description": "Security Onion - Salt Relay - Logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/soc/salt-relay.log"
|
"/opt/so/log/soc/salt-relay.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "soc",
|
"data_stream.dataset": "soc",
|
||||||
"pipeline": "common",
|
"pipeline": "common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -33,10 +39,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "soc-sensoroni-logs",
|
"name": "soc-sensoroni-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Security Onion - Sensoroni - Logs",
|
"description": "Security Onion - Sensoroni - Logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/sensoroni/sensoroni.log"
|
"/opt/so/log/sensoroni/sensoroni.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "soc",
|
"data_stream.dataset": "soc",
|
||||||
"pipeline": "common",
|
"pipeline": "common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -31,10 +37,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "soc-server-logs",
|
"name": "soc-server-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Security Onion Console Logs",
|
"description": "Security Onion Console Logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/opt/so/log/soc/sensoroni-server.log"
|
"/opt/so/log/soc/sensoroni-server.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "soc",
|
"data_stream.dataset": "soc",
|
||||||
"pipeline": "common",
|
"pipeline": "common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -33,10 +39,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "strelka-logs",
|
"name": "strelka-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Strelka Logs",
|
"description": "Strelka Logs",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/strelka/log/strelka.log"
|
"/nsm/strelka/log/strelka.log"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "strelka",
|
"data_stream.dataset": "strelka",
|
||||||
"pipeline": "strelka.file",
|
"pipeline": "strelka.file",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -31,10 +37,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,19 +4,25 @@
|
|||||||
"version": ""
|
"version": ""
|
||||||
},
|
},
|
||||||
"name": "suricata-logs",
|
"name": "suricata-logs",
|
||||||
|
"namespace": "so",
|
||||||
"description": "Suricata integration",
|
"description": "Suricata integration",
|
||||||
"policy_id": "so-grid-nodes_general",
|
"policy_id": "so-grid-nodes_general",
|
||||||
"namespace": "so",
|
"policy_ids": [
|
||||||
|
"so-grid-nodes_general"
|
||||||
|
],
|
||||||
|
"vars": {},
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"filestream-filestream": {
|
"filestream-filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"streams": {
|
"streams": {
|
||||||
"filestream.generic": {
|
"filestream.filestream": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"vars": {
|
"vars": {
|
||||||
"paths": [
|
"paths": [
|
||||||
"/nsm/suricata/eve*.json"
|
"/nsm/suricata/eve*.json"
|
||||||
],
|
],
|
||||||
|
"compression_gzip": false,
|
||||||
|
"use_logs_stream": false,
|
||||||
"data_stream.dataset": "suricata",
|
"data_stream.dataset": "suricata",
|
||||||
"pipeline": "suricata.common",
|
"pipeline": "suricata.common",
|
||||||
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
"parsers": "#- ndjson:\n# target: \"\"\n# message_key: msg\n#- multiline:\n# type: count\n# count_lines: 3\n",
|
||||||
@@ -31,10 +37,10 @@
|
|||||||
"harvester_limit": 0,
|
"harvester_limit": 0,
|
||||||
"fingerprint": false,
|
"fingerprint": false,
|
||||||
"fingerprint_offset": 0,
|
"fingerprint_offset": 0,
|
||||||
"fingerprint_length": "64",
|
|
||||||
"file_identity_native": true,
|
"file_identity_native": true,
|
||||||
"exclude_lines": [],
|
"exclude_lines": [],
|
||||||
"include_lines": []
|
"include_lines": [],
|
||||||
|
"delete_enabled": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
{# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
or more contributor license agreements. Licensed under the Elastic License 2.0; you may not use
|
||||||
|
this file except in compliance with the Elastic License 2.0. #}
|
||||||
|
|
||||||
|
|
||||||
|
{% import_json '/opt/so/state/esfleet_input_package_components.json' as ADDON_INPUT_PACKAGE_COMPONENTS %}
|
||||||
|
{% import_json '/opt/so/state/esfleet_component_templates.json' as INSTALLED_COMPONENT_TEMPLATES %}
|
||||||
|
{% import_yaml 'elasticfleet/defaults.yaml' as ELASTICFLEETDEFAULTS %}
|
||||||
|
|
||||||
|
{% set CORE_ESFLEET_PACKAGES = ELASTICFLEETDEFAULTS.get('elasticfleet', {}).get('packages', {}) %}
|
||||||
|
{% set ADDON_INPUT_INTEGRATION_DEFAULTS = {} %}
|
||||||
|
|
||||||
|
{% for pkg in ADDON_INPUT_PACKAGE_COMPONENTS %}
|
||||||
|
{% if pkg.name in CORE_ESFLEET_PACKAGES %}
|
||||||
|
{# skip core input packages #}
|
||||||
|
{% elif pkg.name not in CORE_ESFLEET_PACKAGES %}
|
||||||
|
{# generate defaults for each input package #}
|
||||||
|
{% if pkg.dataStreams is defined and pkg.dataStreams is not none and pkg.dataStreams | length > 0 %}
|
||||||
|
{% for pattern in pkg.dataStreams %}
|
||||||
|
{# in ES 9.3.2 'input' type integrations no longer create default component templates and instead they wait for user input during 'integration' setup (fleet ui config)
|
||||||
|
title: generic is an artifact of that and is not in use #}
|
||||||
|
{% if pattern.title == "generic" %}
|
||||||
|
{% continue %}
|
||||||
|
{% endif %}
|
||||||
|
{% if "metrics-" in pattern.name %}
|
||||||
|
{% set integration_type = "metrics-" %}
|
||||||
|
{% elif "logs-" in pattern.name %}
|
||||||
|
{% set integration_type = "logs-" %}
|
||||||
|
{% else %}
|
||||||
|
{% set integration_type = "" %}
|
||||||
|
{% endif %}
|
||||||
|
{# on input integrations the component name is user defined at the time it is added to an agent policy #}
|
||||||
|
{% set component_name = pattern.title %}
|
||||||
|
{% set index_pattern = pattern.name %}
|
||||||
|
{# component_name_x maintains the functionality of merging local pillar changes with generated 'defaults' via SOC UI #}
|
||||||
|
{% set component_name_x = component_name.replace(".","_x_") %}
|
||||||
|
{# pillar overrides/merge expects the key names to follow the naming in elasticsearch/defaults.yaml eg. so-logs-1password_x_item_usages . The _x_ is replaced later on in elasticsearch/template.map.jinja #}
|
||||||
|
{% set integration_key = "so-" ~ integration_type ~ pkg.name + '_x_' ~ component_name_x %}
|
||||||
|
{# Default integration settings #}
|
||||||
|
{% set integration_defaults = {
|
||||||
|
"index_sorting": false,
|
||||||
|
"index_template": {
|
||||||
|
"composed_of": [integration_type ~ component_name ~ "@package", integration_type ~ component_name ~ "@custom", "so-fleet_integrations.ip_mappings-1", "so-fleet_globals-1", "so-fleet_agent_id_verification-1"],
|
||||||
|
"data_stream": {
|
||||||
|
"allow_custom_routing": false,
|
||||||
|
"hidden": false
|
||||||
|
},
|
||||||
|
"ignore_missing_component_templates": [integration_type ~ component_name ~ "@custom"],
|
||||||
|
"index_patterns": [index_pattern],
|
||||||
|
"priority": 501,
|
||||||
|
"template": {
|
||||||
|
"settings": {
|
||||||
|
"index": {
|
||||||
|
"lifecycle": {"name": "so-" ~ integration_type ~ component_name ~ "-logs"},
|
||||||
|
"number_of_replicas": 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"policy": {
|
||||||
|
"phases": {
|
||||||
|
"cold": {
|
||||||
|
"actions": {
|
||||||
|
"allocate":{
|
||||||
|
"number_of_replicas": ""
|
||||||
|
},
|
||||||
|
"set_priority": {"priority": 0}
|
||||||
|
},
|
||||||
|
"min_age": "60d"
|
||||||
|
},
|
||||||
|
"delete": {
|
||||||
|
"actions": {
|
||||||
|
"delete": {}
|
||||||
|
},
|
||||||
|
"min_age": "365d"
|
||||||
|
},
|
||||||
|
"hot": {
|
||||||
|
"actions": {
|
||||||
|
"rollover": {
|
||||||
|
"max_age": "30d",
|
||||||
|
"max_primary_shard_size": "50gb"
|
||||||
|
},
|
||||||
|
"forcemerge":{
|
||||||
|
"max_num_segments": ""
|
||||||
|
},
|
||||||
|
"shrink":{
|
||||||
|
"max_primary_shard_size": "",
|
||||||
|
"method": "COUNT",
|
||||||
|
"number_of_shards": ""
|
||||||
|
},
|
||||||
|
"set_priority": {"priority": 100}
|
||||||
|
},
|
||||||
|
"min_age": "0ms"
|
||||||
|
},
|
||||||
|
"warm": {
|
||||||
|
"actions": {
|
||||||
|
"allocate": {
|
||||||
|
"number_of_replicas": ""
|
||||||
|
},
|
||||||
|
"forcemerge": {
|
||||||
|
"max_num_segments": ""
|
||||||
|
},
|
||||||
|
"shrink":{
|
||||||
|
"max_primary_shard_size": "",
|
||||||
|
"method": "COUNT",
|
||||||
|
"number_of_shards": ""
|
||||||
|
},
|
||||||
|
"set_priority": {"priority": 50}
|
||||||
|
},
|
||||||
|
"min_age": "30d"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} %}
|
||||||
|
|
||||||
|
|
||||||
|
{% do ADDON_INPUT_INTEGRATION_DEFAULTS.update({integration_key: integration_defaults}) %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
@@ -10,6 +10,15 @@
|
|||||||
{% set AGENT_STATUS = salt['service.available']('elastic-agent') %}
|
{% set AGENT_STATUS = salt['service.available']('elastic-agent') %}
|
||||||
{% set AGENT_EXISTS = salt['file.file_exists']('/opt/Elastic/Agent/elastic-agent') %}
|
{% set AGENT_EXISTS = salt['file.file_exists']('/opt/Elastic/Agent/elastic-agent') %}
|
||||||
|
|
||||||
|
so-elastic-agent-install:
|
||||||
|
file.managed:
|
||||||
|
- name: /usr/sbin/so-elastic-agent-install
|
||||||
|
- source: salt://elasticfleet/tools/sbin/so-elastic-agent-install
|
||||||
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
|
- show_changes: False
|
||||||
|
|
||||||
{% if not AGENT_STATUS or not AGENT_EXISTS %}
|
{% if not AGENT_STATUS or not AGENT_EXISTS %}
|
||||||
|
|
||||||
pull_agent_installer:
|
pull_agent_installer:
|
||||||
@@ -21,11 +30,9 @@ pull_agent_installer:
|
|||||||
|
|
||||||
run_installer:
|
run_installer:
|
||||||
cmd.run:
|
cmd.run:
|
||||||
- name: ./so-elastic-agent_linux_amd64 -token={{ GRIDNODETOKEN }} -force
|
- name: /usr/sbin/so-elastic-agent-install "{{ GRIDNODETOKEN }}"
|
||||||
- cwd: /opt/so
|
- require:
|
||||||
- retry:
|
- file: pull_agent_installer
|
||||||
attempts: 3
|
|
||||||
interval: 20
|
|
||||||
|
|
||||||
cleanup_agent_installer:
|
cleanup_agent_installer:
|
||||||
file.absent:
|
file.absent:
|
||||||
|
|||||||
@@ -16,7 +16,6 @@
|
|||||||
'awsfirehose.metrics': 'aws.cloudwatch',
|
'awsfirehose.metrics': 'aws.cloudwatch',
|
||||||
'cribl.logs': 'cribl',
|
'cribl.logs': 'cribl',
|
||||||
'cribl.metrics': 'cribl',
|
'cribl.metrics': 'cribl',
|
||||||
'sentinel_one_cloud_funnel.logins': 'sentinel_one_cloud_funnel.login',
|
|
||||||
'azure_application_insights.app_insights': 'azure.app_insights',
|
'azure_application_insights.app_insights': 'azure.app_insights',
|
||||||
'azure_application_insights.app_state': 'azure.app_state',
|
'azure_application_insights.app_state': 'azure.app_state',
|
||||||
'azure_billing.billing': 'azure.billing',
|
'azure_billing.billing': 'azure.billing',
|
||||||
@@ -31,17 +30,14 @@
|
|||||||
'azure_metrics.monitor': 'azure.monitor',
|
'azure_metrics.monitor': 'azure.monitor',
|
||||||
'azure_metrics.storage_account': 'azure.storage_account',
|
'azure_metrics.storage_account': 'azure.storage_account',
|
||||||
'azure_openai.metrics': 'azure.open_ai',
|
'azure_openai.metrics': 'azure.open_ai',
|
||||||
'beat.state': 'beats.stack_monitoring.state',
|
|
||||||
'beat.stats': 'beats.stack_monitoring.stats',
|
|
||||||
'enterprisesearch.health': 'enterprisesearch.stack_monitoring.health',
|
|
||||||
'enterprisesearch.stats': 'enterprisesearch.stack_monitoring.stats',
|
|
||||||
'kibana.cluster_actions': 'kibana.stack_monitoring.cluster_actions',
|
'kibana.cluster_actions': 'kibana.stack_monitoring.cluster_actions',
|
||||||
'kibana.cluster_rules': 'kibana.stack_monitoring.cluster_rules',
|
'kibana.cluster_rules': 'kibana.stack_monitoring.cluster_rules',
|
||||||
'kibana.node_actions': 'kibana.stack_monitoring.node_actions',
|
'kibana.node_actions': 'kibana.stack_monitoring.node_actions',
|
||||||
'kibana.node_rules': 'kibana.stack_monitoring.node_rules',
|
'kibana.node_rules': 'kibana.stack_monitoring.node_rules',
|
||||||
'kibana.stats': 'kibana.stack_monitoring.stats',
|
'kibana.stats': 'kibana.stack_monitoring.stats',
|
||||||
'kibana.status': 'kibana.stack_monitoring.status',
|
'kibana.status': 'kibana.stack_monitoring.status',
|
||||||
'logstash.node_cel': 'logstash.stack_monitoring.node',
|
'logstash.node': 'logstash.stack_monitoring.node',
|
||||||
|
'logstash.node_cel': 'logstash.node',
|
||||||
'logstash.node_stats': 'logstash.stack_monitoring.node_stats',
|
'logstash.node_stats': 'logstash.stack_monitoring.node_stats',
|
||||||
'synthetics.browser': 'synthetics-browser',
|
'synthetics.browser': 'synthetics-browser',
|
||||||
'synthetics.browser_network': 'synthetics-browser.network',
|
'synthetics.browser_network': 'synthetics-browser.network',
|
||||||
@@ -59,8 +55,8 @@
|
|||||||
{# skip core integrations #}
|
{# skip core integrations #}
|
||||||
{% elif pkg.name not in CORE_ESFLEET_PACKAGES %}
|
{% elif pkg.name not in CORE_ESFLEET_PACKAGES %}
|
||||||
{# generate defaults for each integration #}
|
{# generate defaults for each integration #}
|
||||||
{% if pkg.es_index_patterns is defined and pkg.es_index_patterns is not none %}
|
{% if pkg.dataStreams is defined and pkg.dataStreams is not none and pkg.dataStreams | length > 0 %}
|
||||||
{% for pattern in pkg.es_index_patterns %}
|
{% for pattern in pkg.dataStreams %}
|
||||||
{% if "metrics-" in pattern.name %}
|
{% if "metrics-" in pattern.name %}
|
||||||
{% set integration_type = "metrics-" %}
|
{% set integration_type = "metrics-" %}
|
||||||
{% elif "logs-" in pattern.name %}
|
{% elif "logs-" in pattern.name %}
|
||||||
@@ -75,44 +71,27 @@
|
|||||||
{% if component_name in WEIRD_INTEGRATIONS %}
|
{% if component_name in WEIRD_INTEGRATIONS %}
|
||||||
{% set component_name = WEIRD_INTEGRATIONS[component_name] %}
|
{% set component_name = WEIRD_INTEGRATIONS[component_name] %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{# create duplicate of component_name, so we can split generics from @custom component templates in the index template below and overwrite the default @package when needed
|
|
||||||
eg. having to replace unifiedlogs.generic@package with filestream.generic@package, but keep the ability to customize unifiedlogs.generic@custom and its ILM policy #}
|
|
||||||
{% set custom_component_name = component_name %}
|
|
||||||
|
|
||||||
{# duplicate integration_type to assist with sometimes needing to overwrite component templates with 'logs-filestream.generic@package' (there is no metrics-filestream.generic@package) #}
|
|
||||||
{% set generic_integration_type = integration_type %}
|
|
||||||
|
|
||||||
{# component_name_x maintains the functionality of merging local pillar changes with generated 'defaults' via SOC UI #}
|
{# component_name_x maintains the functionality of merging local pillar changes with generated 'defaults' via SOC UI #}
|
||||||
{% set component_name_x = component_name.replace(".","_x_") %}
|
{% set component_name_x = component_name.replace(".","_x_") %}
|
||||||
{# pillar overrides/merge expects the key names to follow the naming in elasticsearch/defaults.yaml eg. so-logs-1password_x_item_usages . The _x_ is replaced later on in elasticsearch/template.map.jinja #}
|
{# pillar overrides/merge expects the key names to follow the naming in elasticsearch/defaults.yaml eg. so-logs-1password_x_item_usages . The _x_ is replaced later on in elasticsearch/template.map.jinja #}
|
||||||
{% set integration_key = "so-" ~ integration_type ~ component_name_x %}
|
{% set integration_key = "so-" ~ integration_type ~ component_name_x %}
|
||||||
|
|
||||||
{# if its a .generic template make sure that a .generic@package for the integration exists. Else default to logs-filestream.generic@package #}
|
|
||||||
{% if ".generic" in component_name and integration_type ~ component_name ~ "@package" not in INSTALLED_COMPONENT_TEMPLATES %}
|
|
||||||
{# these generic templates by default are directed to index_pattern of 'logs-generic-*', overwrite that here to point to eg gcp_pubsub.generic-* #}
|
|
||||||
{% set index_pattern = integration_type ~ component_name ~ "-*" %}
|
|
||||||
{# includes use of .generic component template, but it doesn't exist in installed component templates. Redirect it to filestream.generic@package #}
|
|
||||||
{% set component_name = "filestream.generic" %}
|
|
||||||
{% set generic_integration_type = "logs-" %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{# Default integration settings #}
|
{# Default integration settings #}
|
||||||
{% set integration_defaults = {
|
{% set integration_defaults = {
|
||||||
"index_sorting": false,
|
"index_sorting": false,
|
||||||
"index_template": {
|
"index_template": {
|
||||||
"composed_of": [generic_integration_type ~ component_name ~ "@package", integration_type ~ custom_component_name ~ "@custom", "so-fleet_integrations.ip_mappings-1", "so-fleet_globals-1", "so-fleet_agent_id_verification-1"],
|
"composed_of": [integration_type ~ component_name ~ "@package", integration_type ~ component_name ~ "@custom", "so-fleet_integrations.ip_mappings-1", "so-fleet_globals-1", "so-fleet_agent_id_verification-1"],
|
||||||
"data_stream": {
|
"data_stream": {
|
||||||
"allow_custom_routing": false,
|
"allow_custom_routing": false,
|
||||||
"hidden": false
|
"hidden": false
|
||||||
},
|
},
|
||||||
"ignore_missing_component_templates": [integration_type ~ custom_component_name ~ "@custom"],
|
"ignore_missing_component_templates": [integration_type ~ component_name ~ "@custom"],
|
||||||
"index_patterns": [index_pattern],
|
"index_patterns": [index_pattern],
|
||||||
"priority": 501,
|
"priority": 501,
|
||||||
"template": {
|
"template": {
|
||||||
"settings": {
|
"settings": {
|
||||||
"index": {
|
"index": {
|
||||||
"lifecycle": {"name": "so-" ~ integration_type ~ custom_component_name ~ "-logs"},
|
"lifecycle": {"name": "so-" ~ integration_type ~ component_name ~ "-logs"},
|
||||||
"number_of_replicas": 0
|
"number_of_replicas": 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
{% from 'allowed_states.map.jinja' import allowed_states %}
|
||||||
|
{% if sls in allowed_states %}
|
||||||
|
{% from 'elasticfleet/map.jinja' import ELASTICFLEETMERGED %}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- elasticfleet.config
|
||||||
|
- kibana.enabled
|
||||||
|
|
||||||
|
# If enabled, automatically update Fleet Logstash Outputs
|
||||||
|
{% if ELASTICFLEETMERGED.config.server.enable_auto_configuration %}
|
||||||
|
{% if grains.role not in ['so-import', 'so-eval']%}
|
||||||
|
so-elastic-fleet-auto-configure-logstash-outputs:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-outputs-update
|
||||||
|
- retry:
|
||||||
|
attempts: 4
|
||||||
|
interval: 30
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# If enabled, automatically update Fleet Server URLs & ES Connection
|
||||||
|
so-elastic-fleet-auto-configure-server-urls:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-urls-update
|
||||||
|
- retry:
|
||||||
|
attempts: 4
|
||||||
|
interval: 30
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# Automatically update Fleet Server Elasticsearch URLs & Agent Artifact URLs
|
||||||
|
so-elastic-fleet-auto-configure-elasticsearch-urls:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-es-url-update
|
||||||
|
- retry:
|
||||||
|
attempts: 4
|
||||||
|
interval: 30
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
|
||||||
|
so-elastic-fleet-auto-configure-artifact-urls:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-artifacts-url-update
|
||||||
|
- retry:
|
||||||
|
attempts: 4
|
||||||
|
interval: 30
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
|
||||||
|
so-elastic-fleet-package-statefile:
|
||||||
|
file.managed:
|
||||||
|
- name: /opt/so/state/elastic_fleet_packages.txt
|
||||||
|
- contents: {{ELASTICFLEETMERGED.packages}}
|
||||||
|
|
||||||
|
so-elastic-fleet-package-upgrade:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-package-upgrade
|
||||||
|
- retry:
|
||||||
|
attempts: 3
|
||||||
|
interval: 30
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
|
||||||
|
# initial so-elasticsearch-templates run is earlier, but it can skip over templates that have component templates not yet installed to avoid elasticsearch rejecting the template.
|
||||||
|
so-elasticsearch-templates-after-fleet-packages:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-templates-load
|
||||||
|
- cwd: /opt/so
|
||||||
|
- unless: test -f /opt/so/state/estemplates.txt
|
||||||
|
- require:
|
||||||
|
- cmd: so-elastic-fleet-package-upgrade
|
||||||
|
|
||||||
|
so-elastic-fleet-integration-upgrade:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-integration-upgrade
|
||||||
|
- retry:
|
||||||
|
attempts: 3
|
||||||
|
interval: 10
|
||||||
|
- require:
|
||||||
|
- cmd: so-elastic-fleet-package-upgrade
|
||||||
|
|
||||||
|
so-elastic-fleet-integrations:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-integration-policy-load
|
||||||
|
- retry:
|
||||||
|
attempts: 3
|
||||||
|
interval: 10
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
|
||||||
|
so-elastic-agent-grid-upgrade:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-agent-grid-upgrade
|
||||||
|
- retry:
|
||||||
|
attempts: 12
|
||||||
|
interval: 5
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
|
||||||
|
{# Optional integrations script doesn't need the retries like so-elastic-fleet-integration-upgrade which loads the default integrations #}
|
||||||
|
so-elastic-fleet-addon-integrations:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elastic-fleet-optional-integrations-load
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
- cmd: so-elasticsearch-templates-after-fleet-packages
|
||||||
|
|
||||||
|
{% if ELASTICFLEETMERGED.config.defend_filters.enable_auto_configuration %}
|
||||||
|
so-elastic-defend-manage-filters-file-watch:
|
||||||
|
cmd.run:
|
||||||
|
- name: python3 /sbin/so-elastic-defend-manage-filters.py -c /opt/so/conf/elasticsearch/curl.config -d /opt/so/conf/elastic-fleet/defend-exclusions/disabled-filters.yaml -i /nsm/securityonion-resources/event_filters/ -i /opt/so/conf/elastic-fleet/defend-exclusions/rulesets/custom-filters/ &>> /opt/so/log/elasticfleet/elastic-defend-manage-filters.log
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-kibana
|
||||||
|
- onchanges:
|
||||||
|
- file: elasticdefendcustom
|
||||||
|
- file: elasticdefenddisabled
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{{sls}}_state_not_allowed:
|
||||||
|
test.fail_without_changes:
|
||||||
|
- name: {{sls}}_state_not_allowed
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
. /usr/sbin/so-elastic-fleet-common
|
||||||
|
|
||||||
|
|
||||||
|
# passed in as arg from elasticfleet/install_agent_grid.sls, else pulled from pillar later
|
||||||
|
GRIDNODETOKEN="$1"
|
||||||
|
LOGFILE="/opt/so/SO-Elastic-Agent_Installer_Health.log"
|
||||||
|
|
||||||
|
check_agent_health() {
|
||||||
|
timeout=300
|
||||||
|
interval=10
|
||||||
|
start=$SECONDS
|
||||||
|
|
||||||
|
while (( SECONDS - start < timeout )); do
|
||||||
|
agent_status=$(elastic-agent status 2>&1)
|
||||||
|
echo -e "\n$(date)\n$agent_status\n" >> "$LOGFILE"
|
||||||
|
if echo "$agent_status" | grep -A1 'elastic-agent$' | grep -q 'status: (HEALTHY)'; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
echo "The Elastic Agent is not yet healthy. Waiting for ${interval} seconds before checking again..."
|
||||||
|
sleep "$interval"
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "The Elastic Agent did not become healthy within ${timeout} seconds"
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall_agent() {
|
||||||
|
if command -v elastic-agent >/dev/null 2>&1; then
|
||||||
|
elastic-agent uninstall -f
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ -z "$GRIDNODETOKEN" ]]; then
|
||||||
|
noderole=$(so-yaml.py get -r /etc/salt/grains role)
|
||||||
|
if [[ "$noderole" == "so-heavynode" ]]; then
|
||||||
|
GRIDNODETOKEN=$(salt-call pillar.get global:fleet_grid_enrollment_token_heavy --out=newline_values_only)
|
||||||
|
else
|
||||||
|
GRIDNODETOKEN=$(salt-call pillar.get global:fleet_grid_enrollment_token_general --out=newline_values_only)
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -z "$GRIDNODETOKEN" ]]; then
|
||||||
|
echo "Unable to determine Elastic Fleet enrollment token. Exiting."
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -x /opt/so/so-elastic-agent_linux_amd64 ]]; then
|
||||||
|
echo "Downloading so-elastic-agent installer... This could take a while if another Salt job is running."
|
||||||
|
|
||||||
|
# When running outside of elasticfleet/install_agent_grid.sls we need to download the installer independently.
|
||||||
|
# PYTHONWARNINGS="ignore" to avoid messages like the following when running salt-call:
|
||||||
|
# '/opt/saltstack/salt/lib/python3.10/site-packages/salt/transport/base.py:129: TransportWarning: Unclosed transport! <salt.transport.zeromq.RequestClient object at 0x7fc5f0ee7a30>
|
||||||
|
# File "/bin/salt-call", line 12, in <module>
|
||||||
|
# sys.exit(salt_call())'
|
||||||
|
|
||||||
|
PYTHONWARNINGS="ignore" salt-call state.single file.managed name=/opt/so/so-elastic-agent_linux_amd64 source=salt://elasticfleet/files/so_agent-installers/so-elastic-agent_linux_amd64 mode=755 makedirs=True queue=True
|
||||||
|
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -x /opt/so/so-elastic-agent_linux_amd64 ]]; then
|
||||||
|
attempts=0
|
||||||
|
cd /opt/so/ || exit 1
|
||||||
|
|
||||||
|
truncate -s 0 "$LOGFILE"
|
||||||
|
|
||||||
|
uninstall_agent
|
||||||
|
|
||||||
|
while [[ $attempts -lt 3 ]]; do
|
||||||
|
if ./so-elastic-agent_linux_amd64 -token="$GRIDNODETOKEN" -force && echo "Verifying Elastic Agent health..." && check_agent_health; then
|
||||||
|
rm -f /opt/so/so-elastic-agent_linux_amd64
|
||||||
|
elastic-agent status
|
||||||
|
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
attempts=$((attempts + 1))
|
||||||
|
|
||||||
|
if [[ $attempts -lt 3 ]]; then
|
||||||
|
echo "Unable to verify Elastic Agent health... Retrying in 20 seconds..."
|
||||||
|
sleep 20
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
uninstall_agent
|
||||||
|
rm -f /opt/so/so-elastic-agent_linux_amd64
|
||||||
|
echo "The so-elastic-agent installer failed after 3 attempts. Exiting."
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Unable to locate so-elastic-agent installer. Exiting."
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
@@ -30,15 +30,151 @@ fleet_api() {
|
|||||||
curl -sK /opt/so/conf/elasticsearch/curl.config -L "localhost:5601/api/fleet/${QUERYPATH}" "$@" --retry 3 --retry-delay 10 --fail 2>/dev/null
|
curl -sK /opt/so/conf/elasticsearch/curl.config -L "localhost:5601/api/fleet/${QUERYPATH}" "$@" --retry 3 --retry-delay 10 --fail 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
elastic_fleet_require_agent_policy() {
|
||||||
|
local AGENT_POLICY=$1
|
||||||
|
local POLICY_JSON
|
||||||
|
|
||||||
|
if ! POLICY_JSON=$(fleet_api "agent_policies/$AGENT_POLICY") || [ -z "$POLICY_JSON" ]; then
|
||||||
|
echo "Error: Agent policy '$AGENT_POLICY' was not found or is not visible in the current Kibana space." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! jq -e '.item.package_policies | type == "array"' <<<"$POLICY_JSON" >/dev/null 2>&1; then
|
||||||
|
echo "Error: Agent policy '$AGENT_POLICY' was not found or is not visible in the current Kibana space." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "$POLICY_JSON"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Print the single active enrollment token for POLICY_ID.
|
||||||
|
# Exit 1: retryable (API failure, invalid response, no active token)
|
||||||
|
# Exit 2: multiple active tokens - Shouldn't get into this state without manual intervention
|
||||||
|
elastic_fleet_active_enrollment_token() {
|
||||||
|
local POLICY_ID=$1
|
||||||
|
local RESP TOKEN_COUNT API_KEY
|
||||||
|
|
||||||
|
if ! RESP=$(fleet_api "enrollment_api_keys?perPage=100" -H 'kbn-xsrf: true' -H 'Content-Type: application/json'); then
|
||||||
|
echo "Error: Failed to retrieve enrollment tokens for agent policy '$POLICY_ID'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! jq -e '.list' <<<"$RESP" >/dev/null 2>&1; then
|
||||||
|
echo "Error: Invalid enrollment token response for agent policy '$POLICY_ID'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
TOKEN_COUNT=$(jq --arg pid "$POLICY_ID" '[.list[] | select(.policy_id == $pid and .active == true)] | length' <<<"$RESP")
|
||||||
|
|
||||||
|
if [ "${TOKEN_COUNT:-0}" -eq 0 ]; then
|
||||||
|
echo "Error: No active enrollment token found for agent policy '$POLICY_ID'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$TOKEN_COUNT" -gt 1 ]; then
|
||||||
|
echo "Error: Found $TOKEN_COUNT active enrollment tokens for agent policy '$POLICY_ID'; expected exactly one." >&2
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
API_KEY=$(jq -r --arg pid "$POLICY_ID" '.list[] | select(.policy_id == $pid and .active == true) | .api_key' <<<"$RESP")
|
||||||
|
echo "$API_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Max number of concurrent Fleet write jobs (create/update). Override via env if needed.
|
||||||
|
MAX_FLEET_JOBS=${MAX_FLEET_JOBS:-10}
|
||||||
|
|
||||||
|
# Block until fewer than MAX_FLEET_JOBS background jobs are running.
|
||||||
|
elastic_fleet_throttle() {
|
||||||
|
while (( $(jobs -rp | wc -l) >= MAX_FLEET_JOBS )); do
|
||||||
|
wait -n || true
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
# Load every integration JSON in a directory into a single agent policy.
|
||||||
|
# The agent policy is fetched ONCE (not per file), and the create/update writes
|
||||||
|
# are dispatched as throttled background jobs.
|
||||||
|
# $1 AGENT_POLICY - the agent policy id/name to load integrations into
|
||||||
|
# $2 DIR - directory of integration *.json files
|
||||||
|
# $3 LABEL - human-readable label for log output
|
||||||
|
# $4 SKIP_CREATE_NAME - (optional) integration name to skip when creating (still updated if present)
|
||||||
|
# Returns 1 if the policy cannot be fetched or if any integration failed to create/update.
|
||||||
|
elastic_fleet_load_integrations_dir() {
|
||||||
|
local AGENT_POLICY=$1
|
||||||
|
local DIR=$2
|
||||||
|
local LABEL=$3
|
||||||
|
local SKIP_CREATE_NAME=$4
|
||||||
|
local POLICY_JSON FAIL_FILE OUT_DIR INTEGRATION NAME ID i
|
||||||
|
|
||||||
|
FAIL_FILE=$(mktemp)
|
||||||
|
# Each job buffers its full output (header + API response) into its own file so the
|
||||||
|
# parent can print them grouped and in submission order after concurrent writes finish.
|
||||||
|
OUT_DIR=$(mktemp -d)
|
||||||
|
i=0
|
||||||
|
|
||||||
|
# Fetch the agent policy a single time; we look up integration ids locally below.
|
||||||
|
if ! POLICY_JSON=$(elastic_fleet_require_agent_policy "$AGENT_POLICY"); then
|
||||||
|
rm -f "$FAIL_FILE"
|
||||||
|
rm -rf "$OUT_DIR"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for INTEGRATION in "$DIR"/*.json; do
|
||||||
|
[ -e "$INTEGRATION" ] || continue
|
||||||
|
NAME=$(jq -r .name "$INTEGRATION")
|
||||||
|
ID=$(jq -r --arg n "$NAME" '.item.package_policies[]? | select(.name==$n) | .id' <<<"$POLICY_JSON")
|
||||||
|
|
||||||
|
elastic_fleet_throttle
|
||||||
|
{
|
||||||
|
local RESP
|
||||||
|
if [ -n "$ID" ]; then
|
||||||
|
printf "\n\n%s - Updating integration %s\n" "$LABEL" "$NAME"
|
||||||
|
if ! RESP=$(elastic_fleet_integration_update "$ID" "@$INTEGRATION"); then
|
||||||
|
flock 9; echo "update ${INTEGRATION##*/}" >&9
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$RESP"
|
||||||
|
elif [ -n "$SKIP_CREATE_NAME" ] && [ "$NAME" == "$SKIP_CREATE_NAME" ]; then
|
||||||
|
printf "\n\n%s - Skipping creation of %s\n" "$LABEL" "$NAME"
|
||||||
|
else
|
||||||
|
printf "\n\n%s - Creating integration %s\n" "$LABEL" "$NAME"
|
||||||
|
if ! RESP=$(elastic_fleet_integration_create "@$INTEGRATION"); then
|
||||||
|
flock 9; echo "create ${INTEGRATION##*/}" >&9
|
||||||
|
fi
|
||||||
|
printf '%s\n' "$RESP"
|
||||||
|
fi
|
||||||
|
} >"$OUT_DIR/$(printf '%03d' "$i")" 9>>"$FAIL_FILE" &
|
||||||
|
i=$((i+1))
|
||||||
|
done
|
||||||
|
wait || true
|
||||||
|
|
||||||
|
# Emit per-integration output grouped and in submission order (glob sorts numerically).
|
||||||
|
cat "$OUT_DIR"/* 2>/dev/null
|
||||||
|
rm -rf "$OUT_DIR"
|
||||||
|
|
||||||
|
local rc=0
|
||||||
|
if [ -s "$FAIL_FILE" ]; then
|
||||||
|
printf "\n%s: failed integrations:\n" "$LABEL"
|
||||||
|
cat "$FAIL_FILE"
|
||||||
|
rc=1
|
||||||
|
fi
|
||||||
|
rm -f "$FAIL_FILE"
|
||||||
|
return $rc
|
||||||
|
}
|
||||||
|
|
||||||
elastic_fleet_integration_check() {
|
elastic_fleet_integration_check() {
|
||||||
|
|
||||||
AGENT_POLICY=$1
|
AGENT_POLICY=$1
|
||||||
|
|
||||||
JSON_STRING=$2
|
JSON_STRING=$2
|
||||||
|
|
||||||
NAME=$(jq -r .name $JSON_STRING)
|
NAME=$(jq -r .name "$JSON_STRING")
|
||||||
|
INTEGRATION_ID=""
|
||||||
|
|
||||||
INTEGRATION_ID=$(/usr/sbin/so-elastic-fleet-agent-policy-view "$AGENT_POLICY" | jq -r '.item.package_policies[] | select(.name=="'"$NAME"'") | .id')
|
local POLICY_JSON
|
||||||
|
if ! POLICY_JSON=$(elastic_fleet_require_agent_policy "$AGENT_POLICY"); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INTEGRATION_ID=$(jq -r --arg n "$NAME" '.item.package_policies[]? | select(.name==$n) | .id' <<<"$POLICY_JSON")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +182,9 @@ elastic_fleet_integration_create() {
|
|||||||
|
|
||||||
JSON_STRING=$1
|
JSON_STRING=$1
|
||||||
|
|
||||||
if ! fleet_api "package_policies" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPOST -d "$JSON_STRING"; then
|
# --retry-all-errors so transient 409 conflicts (concurrent writes to the same agent
|
||||||
|
# policy) are retried; curl --retry alone does not retry 409.
|
||||||
|
if ! fleet_api "package_policies" --retry-all-errors -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPOST -d "$JSON_STRING"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -58,7 +196,16 @@ elastic_fleet_integration_remove() {
|
|||||||
|
|
||||||
NAME=$2
|
NAME=$2
|
||||||
|
|
||||||
INTEGRATION_ID=$(/usr/sbin/so-elastic-fleet-agent-policy-view "$AGENT_POLICY" | jq -r '.item.package_policies[] | select(.name=="'"$NAME"'") | .id')
|
local POLICY_JSON
|
||||||
|
if ! POLICY_JSON=$(elastic_fleet_require_agent_policy "$AGENT_POLICY"); then
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INTEGRATION_ID=$(jq -r --arg n "$NAME" '.item.package_policies[]? | select(.name==$n) | .id' <<<"$POLICY_JSON")
|
||||||
|
if [ -z "$INTEGRATION_ID" ]; then
|
||||||
|
echo "Error: Integration '$NAME' was not found in agent policy '$AGENT_POLICY'." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
JSON_STRING=$( jq -n \
|
JSON_STRING=$( jq -n \
|
||||||
--arg INTEGRATIONID "$INTEGRATION_ID" \
|
--arg INTEGRATIONID "$INTEGRATION_ID" \
|
||||||
@@ -77,7 +224,9 @@ elastic_fleet_integration_update() {
|
|||||||
|
|
||||||
JSON_STRING=$2
|
JSON_STRING=$2
|
||||||
|
|
||||||
if ! fleet_api "package_policies/$UPDATE_ID" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPUT -d "$JSON_STRING"; then
|
# --retry-all-errors so transient 409 conflicts (concurrent writes to the same agent
|
||||||
|
# policy) are retried; curl --retry alone does not retry 409.
|
||||||
|
if ! fleet_api "package_policies/$UPDATE_ID" --retry-all-errors -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -XPUT -d "$JSON_STRING"; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
@@ -135,9 +284,33 @@ elastic_fleet_bulk_package_install() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
elastic_fleet_installed_packages() {
|
elastic_fleet_get_package_list_by_type() {
|
||||||
if ! fleet_api "epm/packages/installed?perPage=500"; then
|
if ! output=$(fleet_api "epm/packages"); then
|
||||||
return 1
|
return 1
|
||||||
|
else
|
||||||
|
is_integration=$(jq '[.items[] | select(.type=="integration") | .name ]' <<< "$output")
|
||||||
|
is_input=$(jq '[.items[] | select(.type=="input") | .name ]' <<< "$output")
|
||||||
|
is_content=$(jq '[.items[] | select(.type=="content") | .name ]' <<< "$output")
|
||||||
|
jq -n --argjson is_integration "${is_integration:-[]}" \
|
||||||
|
--argjson is_input "${is_input:-[]}" \
|
||||||
|
--argjson is_content "${is_content:-[]}" \
|
||||||
|
'{"integration": $is_integration,"input": $is_input, "content": $is_content}'
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
elastic_fleet_installed_packages_components() {
|
||||||
|
package_type=${1,,}
|
||||||
|
if [[ "$package_type" != "integration" && "$package_type" != "input" && "$package_type" != "content" ]]; then
|
||||||
|
echo "Error: Invalid package type ${package_type}. Valid types are 'integration', 'input', or 'content'."
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
packages_by_type=$(elastic_fleet_get_package_list_by_type)
|
||||||
|
packages=$(jq --arg package_type "$package_type" '.[$package_type]' <<< "$packages_by_type")
|
||||||
|
|
||||||
|
if ! output=$(fleet_api "epm/packages/installed?perPage=500"); then
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
jq -c --argjson packages "$packages" '[.items[] | select(.name | IN($packages[])) | {name: .name, dataStreams: .dataStreams}]' <<< "$output"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,7 +389,7 @@ elastic_fleet_policy_create() {
|
|||||||
--arg DESC "$DESC" \
|
--arg DESC "$DESC" \
|
||||||
--arg TIMEOUT $TIMEOUT \
|
--arg TIMEOUT $TIMEOUT \
|
||||||
--arg FLEETSERVER "$FLEETSERVER" \
|
--arg FLEETSERVER "$FLEETSERVER" \
|
||||||
'{"name": $NAME,"id":$NAME,"description":$DESC,"namespace":"default","monitoring_enabled":["logs"],"inactivity_timeout":$TIMEOUT,"has_fleet_server":$FLEETSERVER}'
|
'{"name": $NAME,"id":$NAME,"description":$DESC,"namespace":"default","monitoring_enabled":["logs"],"inactivity_timeout":$TIMEOUT,"has_fleet_server":$FLEETSERVER,"advanced_settings":{"agent_logging_level": "warning"}}'
|
||||||
)
|
)
|
||||||
# Create Fleet Policy
|
# Create Fleet Policy
|
||||||
if ! fleet_api "agent_policies" -XPOST -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d "$JSON_STRING"; then
|
if ! fleet_api "agent_policies" -XPOST -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d "$JSON_STRING"; then
|
||||||
|
|||||||
@@ -13,7 +13,10 @@ ERROR=false
|
|||||||
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/elastic-defend/*.json
|
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/elastic-defend/*.json
|
||||||
do
|
do
|
||||||
printf "\n\nInitial Endpoints Policy - Loading $INTEGRATION\n"
|
printf "\n\nInitial Endpoints Policy - Loading $INTEGRATION\n"
|
||||||
elastic_fleet_integration_check "endpoints-initial" "$INTEGRATION"
|
if ! elastic_fleet_integration_check "endpoints-initial" "$INTEGRATION"; then
|
||||||
|
ERROR=true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
if [ -n "$INTEGRATION_ID" ]; then
|
if [ -n "$INTEGRATION_ID" ]; then
|
||||||
printf "\n\nIntegration $NAME exists - Upgrading integration policy\n"
|
printf "\n\nIntegration $NAME exists - Upgrading integration policy\n"
|
||||||
if ! elastic_fleet_integration_policy_upgrade "$INTEGRATION_ID"; then
|
if ! elastic_fleet_integration_policy_upgrade "$INTEGRATION_ID"; then
|
||||||
|
|||||||
+20
-5
@@ -7,20 +7,35 @@
|
|||||||
. /usr/sbin/so-elastic-fleet-common
|
. /usr/sbin/so-elastic-fleet-common
|
||||||
|
|
||||||
# Get all the fleet policies
|
# Get all the fleet policies
|
||||||
json_output=$(curl -s -K /opt/so/conf/elasticsearch/curl.config -L -X GET "localhost:5601/api/fleet/agent_policies" -H 'kbn-xsrf: true')
|
if ! json_output=$(fleet_api "agent_policies" -H 'kbn-xsrf: true'); then
|
||||||
|
echo "Error: Failed to retrieve Fleet agent policies." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! jq -e '.items' <<<"$json_output" >/dev/null 2>&1; then
|
||||||
|
echo "Error: Invalid Fleet agent policies response." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Extract the IDs that start with "FleetServer_"
|
# Extract the IDs that start with "FleetServer_"
|
||||||
POLICY=$(echo "$json_output" | jq -r '.items[] | select(.id | startswith("FleetServer_")) | .id')
|
POLICY=$(jq -r '.items[] | select(.id | startswith("FleetServer_")) | .id' <<<"$json_output")
|
||||||
|
|
||||||
# Iterate over each ID in the POLICY variable
|
# Iterate over each ID in the POLICY variable
|
||||||
for POLICYNAME in $POLICY; do
|
for POLICYNAME in $POLICY; do
|
||||||
printf "\nUpdating Policy: $POLICYNAME\n"
|
printf "\nUpdating Policy: $POLICYNAME\n"
|
||||||
|
|
||||||
# First get the Integration ID
|
if ! POLICY_JSON=$(elastic_fleet_require_agent_policy "$POLICYNAME"); then
|
||||||
INTEGRATION_ID=$(/usr/sbin/so-elastic-fleet-agent-policy-view "$POLICYNAME" | jq -r '.item.package_policies[] | select(.package.name == "fleet_server") | .id')
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
INTEGRATION_ID=$(jq -r '.item.package_policies[]? | select(.package.name == "fleet_server") | .id' <<<"$POLICY_JSON")
|
||||||
|
if [ -z "$INTEGRATION_ID" ]; then
|
||||||
|
echo "Error: fleet_server integration was not found in agent policy '$POLICYNAME'." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
# Modify the default integration policy to update the policy_id and an with the correct naming
|
# Modify the default integration policy to update the policy_id and an with the correct naming
|
||||||
UPDATED_INTEGRATION_POLICY=$(jq --arg policy_id "$POLICYNAME" --arg name "fleet_server-$POLICYNAME" '
|
UPDATED_INTEGRATION_POLICY=$(jq --arg policy_id "$POLICYNAME" --arg name "fleet_server-$POLICYNAME" '
|
||||||
.policy_id = $policy_id |
|
.policy_id = $policy_id |
|
||||||
.name = $name' /opt/so/conf/elastic-fleet/integrations/fleet-server/fleet-server.json)
|
.name = $name' /opt/so/conf/elastic-fleet/integrations/fleet-server/fleet-server.json)
|
||||||
|
|
||||||
|
|||||||
@@ -9,108 +9,45 @@
|
|||||||
RETURN_CODE=0
|
RETURN_CODE=0
|
||||||
|
|
||||||
if [ ! -f /opt/so/state/eaintegrations.txt ]; then
|
if [ ! -f /opt/so/state/eaintegrations.txt ]; then
|
||||||
# First, check for any package upgrades
|
|
||||||
/usr/sbin/so-elastic-fleet-package-upgrade
|
|
||||||
|
|
||||||
# Second, update Fleet Server policies
|
# update Fleet Server policies
|
||||||
/usr/sbin/so-elastic-fleet-integration-policy-elastic-fleet-server
|
/usr/sbin/so-elastic-fleet-integration-policy-elastic-fleet-server
|
||||||
|
|
||||||
# Third, configure Elastic Defend Integration seperately
|
# configure Elastic Defend Integration separately
|
||||||
/usr/sbin/so-elastic-fleet-integration-policy-elastic-defend
|
/usr/sbin/so-elastic-fleet-integration-policy-elastic-defend
|
||||||
|
|
||||||
|
# Each group fetches its agent policy once and dispatches create/update writes concurrently.
|
||||||
|
|
||||||
# Initial Endpoints
|
# Initial Endpoints
|
||||||
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/endpoints-initial/*.json; do
|
elastic_fleet_load_integrations_dir "endpoints-initial" \
|
||||||
printf "\n\nInitial Endpoints Policy - Loading $INTEGRATION\n"
|
/opt/so/conf/elastic-fleet/integrations/endpoints-initial "Initial Endpoints Policy" || RETURN_CODE=1
|
||||||
elastic_fleet_integration_check "endpoints-initial" "$INTEGRATION"
|
|
||||||
if [ -n "$INTEGRATION_ID" ]; then
|
|
||||||
printf "\n\nIntegration $NAME exists - Updating integration\n"
|
|
||||||
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
|
|
||||||
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "\n\nIntegration does not exist - Creating integration\n"
|
|
||||||
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
|
|
||||||
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Grid Nodes - General
|
# Grid Nodes - General
|
||||||
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/grid-nodes_general/*.json; do
|
elastic_fleet_load_integrations_dir "so-grid-nodes_general" \
|
||||||
printf "\n\nGrid Nodes Policy_General - Loading $INTEGRATION\n"
|
/opt/so/conf/elastic-fleet/integrations/grid-nodes_general "Grid Nodes Policy_General" || RETURN_CODE=1
|
||||||
elastic_fleet_integration_check "so-grid-nodes_general" "$INTEGRATION"
|
|
||||||
if [ -n "$INTEGRATION_ID" ]; then
|
|
||||||
printf "\n\nIntegration $NAME exists - Updating integration\n"
|
|
||||||
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
|
|
||||||
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "\n\nIntegration does not exist - Creating integration\n"
|
|
||||||
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
|
|
||||||
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Grid Nodes - Heavy
|
# Grid Nodes - Heavy
|
||||||
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations/grid-nodes_heavy/*.json; do
|
elastic_fleet_load_integrations_dir "so-grid-nodes_heavy" \
|
||||||
printf "\n\nGrid Nodes Policy_Heavy - Loading $INTEGRATION\n"
|
/opt/so/conf/elastic-fleet/integrations/grid-nodes_heavy "Grid Nodes Policy_Heavy" || RETURN_CODE=1
|
||||||
elastic_fleet_integration_check "so-grid-nodes_heavy" "$INTEGRATION"
|
|
||||||
if [ -n "$INTEGRATION_ID" ]; then
|
|
||||||
printf "\n\nIntegration $NAME exists - Updating integration\n"
|
|
||||||
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
|
|
||||||
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "\n\nIntegration does not exist - Creating integration\n"
|
|
||||||
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
|
|
||||||
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
# Fleet Server - Optional integrations
|
# Fleet Server - Optional integrations (adds integration configuration to a given FleetServer_ policy)
|
||||||
for INTEGRATION in /opt/so/conf/elastic-fleet/integrations-optional/FleetServer*/*.json; do
|
for FLEET_DIR in /opt/so/conf/elastic-fleet/integrations-optional/FleetServer*/; do
|
||||||
if ! [ "$INTEGRATION" == "/opt/so/conf/elastic-fleet/integrations-optional/FleetServer*/*.json" ]; then
|
[ -d "$FLEET_DIR" ] || continue
|
||||||
FLEET_POLICY=`echo "$INTEGRATION"| cut -d'/' -f7`
|
INTEGRATIONS=("${FLEET_DIR%/}"/*.json)
|
||||||
printf "\n\nFleet Server Policy - Loading $INTEGRATION\n"
|
[ -e "${INTEGRATIONS[0]}" ] || continue
|
||||||
elastic_fleet_integration_check "$FLEET_POLICY" "$INTEGRATION"
|
|
||||||
if [ -n "$INTEGRATION_ID" ]; then
|
FLEET_POLICY=$(basename "$FLEET_DIR")
|
||||||
printf "\n\nIntegration $NAME exists - Updating integration\n"
|
elastic_fleet_load_integrations_dir "$FLEET_POLICY" \
|
||||||
if ! elastic_fleet_integration_update "$INTEGRATION_ID" "@$INTEGRATION"; then
|
"${FLEET_DIR%/}" "Fleet Server Policy" "elasticsearch-logs" || RETURN_CODE=1
|
||||||
echo -e "\nFailed to update integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
printf "\n\nIntegration does not exist - Creating integration\n"
|
|
||||||
if [ "$NAME" != "elasticsearch-logs" ]; then
|
|
||||||
if ! elastic_fleet_integration_create "@$INTEGRATION"; then
|
|
||||||
echo -e "\nFailed to create integration for ${INTEGRATION##*/}"
|
|
||||||
RETURN_CODE=1
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
|
|
||||||
# Only create the state file if all policies were created/updated successfully
|
# Only create the state file if all policies were created/updated successfully
|
||||||
if [[ "$RETURN_CODE" != "1" ]]; then
|
if [[ $RETURN_CODE -eq 0 ]]; then
|
||||||
touch /opt/so/state/eaintegrations.txt
|
touch /opt/so/state/eaintegrations.txt
|
||||||
|
else
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
exit $RETURN_CODE
|
echo "Fleet integration policies already loaded."
|
||||||
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -22,15 +22,22 @@ NUM_RUNNING=$(pgrep -cf "/bin/bash /sbin/so-elastic-agent-gen-installers")
|
|||||||
|
|
||||||
for i in {1..30}
|
for i in {1..30}
|
||||||
do
|
do
|
||||||
ENROLLMENTOKEN=$(curl -K /opt/so/conf/elasticsearch/curl.config -L "localhost:5601/api/fleet/enrollment_api_keys?perPage=100" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' | jq .list | jq -r -c '.[] | select(.policy_id | contains("endpoints-initial")) | .api_key')
|
ENROLLMENTOKEN=$(elastic_fleet_active_enrollment_token "endpoints-initial")
|
||||||
|
TOKEN_RC=$?
|
||||||
|
if [ "$TOKEN_RC" -eq 2 ]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
FLEETHOST=$(curl -K /opt/so/conf/elasticsearch/curl.config 'http://localhost:5601/api/fleet/fleet_server_hosts/grid-default' | jq -r '.item.host_urls[]' | paste -sd ',')
|
FLEETHOST=$(curl -K /opt/so/conf/elasticsearch/curl.config 'http://localhost:5601/api/fleet/fleet_server_hosts/grid-default' | jq -r '.item.host_urls[]' | paste -sd ',')
|
||||||
if [[ $FLEETHOST ]] && [[ $ENROLLMENTOKEN ]]; then break; else sleep 10; fi
|
if [[ -n "$FLEETHOST" ]] && [[ -n "$ENROLLMENTOKEN" ]]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 10
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -z $FLEETHOST ]] || [[ -z $ENROLLMENTOKEN ]]; then
|
if [[ -z "$FLEETHOST" ]] || [[ -z "$ENROLLMENTOKEN" ]]; then
|
||||||
printf "\nFleet Host URL, Enrollment Token or Elastic Version empty - exiting..."
|
printf "\nFleet Host URL, Enrollment Token or Elastic Version empty - exiting..."
|
||||||
printf "\nFleet Host: $FLEETHOST, Enrollment Token: $ENROLLMENTOKEN\n"
|
printf "\nFleet Host: $FLEETHOST, Enrollment Token: $ENROLLMENTOKEN\n"
|
||||||
exit
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
OSARCH=( "linux-x86_64" "windows-x86_64" "darwin-x86_64" "darwin-aarch64" )
|
OSARCH=( "linux-x86_64" "windows-x86_64" "darwin-x86_64" "darwin-aarch64" )
|
||||||
@@ -62,31 +69,60 @@ do
|
|||||||
done
|
done
|
||||||
|
|
||||||
GOTARGETOS=( "linux" "windows" "darwin" "darwin/arm64" )
|
GOTARGETOS=( "linux" "windows" "darwin" "darwin/arm64" )
|
||||||
GOARCH="amd64"
|
|
||||||
printf "\n### Generating OS packages using the cleaned up tarballs"
|
printf "\n### Generating OS packages using the cleaned up tarballs"
|
||||||
for GOOS in "${GOTARGETOS[@]}"
|
for GOOS in "${GOTARGETOS[@]}"; do
|
||||||
do
|
GOARCH="amd64"
|
||||||
if [[ $GOOS == 'darwin/arm64' ]]; then GOOS="darwin" && GOARCH="arm64"; fi
|
if [[ $GOOS == 'darwin/arm64' ]]; then GOOS="darwin" && GOARCH="arm64"; fi
|
||||||
printf "\n\n### Generating $GOOS/$GOARCH Installer...\n"
|
printf "\n\n### Generating $GOOS/$GOARCH Installer...\n"
|
||||||
docker run -e CGO_ENABLED=0 -e GOOS=$GOOS -e GOARCH=$GOARCH \
|
if ! docker run -e CGO_ENABLED=0 -e GOOS=$GOOS -e GOARCH=$GOARCH \
|
||||||
--mount type=bind,source=/etc/pki/tls/certs/,target=/workspace/files/cert/ \
|
--mount type=bind,source=/etc/pki/tls/certs/,target=/workspace/files/cert/ \
|
||||||
--mount type=bind,source=/nsm/elastic-agent-workspace/,target=/workspace/files/elastic-agent/ \
|
--mount type=bind,source=/nsm/elastic-agent-workspace/,target=/workspace/files/elastic-agent/ \
|
||||||
--mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/,target=/output/ \
|
--mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/,target=/output/ \
|
||||||
{{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent-builder:{{ GLOBALS.so_version }} go build -ldflags "-X main.fleetHostURLsList=$FLEETHOST -X main.enrollmentToken=$ENROLLMENTOKEN" -o /output/so-elastic-agent_${GOOS}_${GOARCH}
|
{{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent-builder:{{ GLOBALS.so_version }} go build -ldflags "-X main.fleetHostURLsList=$FLEETHOST -X main.enrollmentToken=$ENROLLMENTOKEN" -o /output/so-elastic-agent_${GOOS}_${GOARCH}; then
|
||||||
|
printf "\n### ERROR: Failed to generate $GOOS/$GOARCH installer. Exiting...\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
printf "\n### $GOOS/$GOARCH Installer Generated...\n"
|
printf "\n### $GOOS/$GOARCH Installer Generated...\n"
|
||||||
done
|
done
|
||||||
|
|
||||||
printf "\n\n### Generating MSI...\n"
|
printf "\n\n### Generating MSI...\n"
|
||||||
cp /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/so-elastic-agent_windows_amd64 /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/so-elastic-agent_windows_amd64.exe
|
cp /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64 /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64.exe
|
||||||
docker run \
|
if ! docker run \
|
||||||
--mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/,target=/output/ -w /output \
|
--mount type=bind,source=/opt/so/saltstack/local/salt/elasticfleet/files/,target=/output/ -w /output \
|
||||||
{{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent-builder:{{ GLOBALS.so_version }} wixl -o so-elastic-agent_windows_amd64_msi --arch x64 /workspace/so-elastic-agent.wxs
|
{{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elastic-agent-builder:{{ GLOBALS.so_version }} wixl -o so-elastic-agent_windows_amd64_msi --arch x64 /workspace/so-elastic-agent.wxs; then
|
||||||
|
printf "\n### ERROR: Failed to generate MSI. Exiting...\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
printf "\n### MSI Generated...\n"
|
printf "\n### MSI Generated...\n"
|
||||||
|
|
||||||
|
# Verify installers were created
|
||||||
|
for GOOS in "${GOTARGETOS[@]}"; do
|
||||||
|
GOARCH="amd64"
|
||||||
|
if [[ $GOOS == 'darwin/arm64' ]]; then GOOS="darwin"; GOARCH="arm64"; fi
|
||||||
|
if [[ ! -f /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_${GOOS}_${GOARCH} ]]; then
|
||||||
|
printf "\n### ERROR: Installer for %s/%s was not generated. Exiting...\n" "$GOOS" "$GOARCH"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
# After verifying new installer was generated, move it to so_agent-installers directory
|
||||||
|
mv /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_${GOOS}_${GOARCH} /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/
|
||||||
|
done
|
||||||
|
|
||||||
|
# Verify MSI installer
|
||||||
|
if [[ ! -f /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64_msi ]]; then
|
||||||
|
printf "\n### ERROR: Installer MSI was not generated. Exiting...\n"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
# After verifying new installer MSI was generated, move it to so_agent-installers directory
|
||||||
|
mv /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64_msi /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/
|
||||||
|
fi
|
||||||
|
|
||||||
printf "\n### Cleaning up temp files \n"
|
printf "\n### Cleaning up temp files \n"
|
||||||
rm -rf /nsm/elastic-agent-workspace
|
rm -rf /nsm/elastic-agent-workspace
|
||||||
rm -rf /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/so-elastic-agent_windows_amd64.exe
|
rm -rf /opt/so/saltstack/local/salt/elasticfleet/files/so-elastic-agent_windows_amd64.exe
|
||||||
|
|
||||||
printf "\n### Copying so_agent-installers to /nsm/elastic-fleet/ for nginx.\n"
|
printf "\n### Copying so_agent-installers to /nsm/elastic-fleet/ for nginx.\n"
|
||||||
\cp -vr /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/ /nsm/elastic-fleet/
|
\cp -vr /opt/so/saltstack/local/salt/elasticfleet/files/so_agent-installers/ /nsm/elastic-fleet/
|
||||||
chmod 644 /nsm/elastic-fleet/so_agent-installers/*
|
chmod 644 /nsm/elastic-fleet/so_agent-installers/*
|
||||||
|
|
||||||
|
# if we got here all installers have been generated successfully
|
||||||
|
exit 0
|
||||||
@@ -5,7 +5,13 @@
|
|||||||
# this file except in compliance with the Elastic License 2.0.
|
# this file except in compliance with the Elastic License 2.0.
|
||||||
|
|
||||||
. /usr/sbin/so-common
|
. /usr/sbin/so-common
|
||||||
|
. /usr/sbin/so-elastic-fleet-common
|
||||||
{%- import_yaml 'elasticsearch/defaults.yaml' as ELASTICSEARCHDEFAULTS %}
|
{%- import_yaml 'elasticsearch/defaults.yaml' as ELASTICSEARCHDEFAULTS %}
|
||||||
|
{%- import_yaml 'elasticfleet/defaults.yaml' as ELASTICFLEETDEFAULTS %}
|
||||||
|
{# Optionally override Elasticsearch version for Elastic Agent patch releases #}
|
||||||
|
{%- if ELASTICFLEETDEFAULTS.elasticfleet.patch_version is defined %}
|
||||||
|
{%- do ELASTICSEARCHDEFAULTS.elasticsearch.update({'version': ELASTICFLEETDEFAULTS.elasticfleet.patch_version}) %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
# Only run on Managers
|
# Only run on Managers
|
||||||
if ! is_manager_node; then
|
if ! is_manager_node; then
|
||||||
@@ -14,13 +20,10 @@ if ! is_manager_node; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Get current list of Grid Node Agents that need to be upgraded
|
# Get current list of Grid Node Agents that need to be upgraded
|
||||||
RAW_JSON=$(curl -K /opt/so/conf/elasticsearch/curl.config -L "http://localhost:5601/api/fleet/agents?perPage=20&page=1&kuery=NOT%20agent.version%3A%20{{ELASTICSEARCHDEFAULTS.elasticsearch.version}}%20AND%20policy_id%3A%20so-grid-nodes_%2A&showInactive=false&getStatusSummary=true" --retry 3 --retry-delay 30 --fail 2>/dev/null)
|
if ! RAW_JSON=$(fleet_api "agents?perPage=20&page=1&kuery=NOT%20agent.version%3A%20{{ELASTICSEARCHDEFAULTS.elasticsearch.version | urlencode }}%20AND%20policy_id%3A%20so-grid-nodes_%2A&showInactive=false&getStatusSummary=true" -H 'kbn-xsrf: true' -H 'Content-Type: application/json'); then
|
||||||
|
|
||||||
# Check to make sure that the server responded with good data - else, bail from script
|
printf "Failed to query for current Grid Agents...\n"
|
||||||
CHECKSUM=$(jq -r '.page' <<< "$RAW_JSON")
|
exit 1
|
||||||
if [ "$CHECKSUM" -ne 1 ]; then
|
|
||||||
printf "Failed to query for current Grid Agents...\n"
|
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Generate list of Node Agents that need updates
|
# Generate list of Node Agents that need updates
|
||||||
@@ -31,10 +34,12 @@ if [ "$OUTDATED_LIST" != '[]' ]; then
|
|||||||
printf "Initiating upgrades for $AGENTNUMBERS Agents to Elastic {{ELASTICSEARCHDEFAULTS.elasticsearch.version}}...\n\n"
|
printf "Initiating upgrades for $AGENTNUMBERS Agents to Elastic {{ELASTICSEARCHDEFAULTS.elasticsearch.version}}...\n\n"
|
||||||
|
|
||||||
# Generate updated JSON payload
|
# Generate updated JSON payload
|
||||||
JSON_STRING=$(jq -n --arg ELASTICVERSION {{ELASTICSEARCHDEFAULTS.elasticsearch.version}} --arg UPDATELIST $OUTDATED_LIST '{"version": $ELASTICVERSION,"agents": $UPDATELIST }')
|
JSON_STRING=$(jq -n --arg ELASTICVERSION "{{ELASTICSEARCHDEFAULTS.elasticsearch.version}}" --argjson UPDATELIST "$OUTDATED_LIST" '{"version": $ELASTICVERSION,"agents": $UPDATELIST }')
|
||||||
|
|
||||||
# Update Node Agents
|
# Update Node Agents
|
||||||
curl -K /opt/so/conf/elasticsearch/curl.config -L -X POST "http://localhost:5601/api/fleet/agents/bulk_upgrade" -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d "$JSON_STRING"
|
if ! fleet_api "agents/bulk_upgrade" -XPOST -H 'kbn-xsrf: true' -H 'Content-Type: application/json' -d "$JSON_STRING"; then
|
||||||
|
printf "Failed to initiate Agent upgrades...\n"
|
||||||
|
fi
|
||||||
else
|
else
|
||||||
printf "No Agents need updates... Exiting\n\n"
|
printf "No Agents need updates... Exiting\n\n"
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -23,73 +23,90 @@ if [ $? -ne 0 ]; then
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
default_packages=({% for pkg in SUPPORTED_PACKAGES %}"{{ pkg }}"{% if not loop.last %} {% endif %}{% endfor %})
|
default_packages=({% for pkg in SUPPORTED_PACKAGES %}"{{ pkg }}"{% if not loop.last %} {% endif %}{% endfor %})
|
||||||
|
# JSON array of the default packages, used by the jq filter below.
|
||||||
|
default_packages_json=$(printf '%s\n' "${default_packages[@]}" | jq -R . | jq -s '.')
|
||||||
|
|
||||||
|
# Output lock (serializes concurrent job output) and failure file (one marker line per
|
||||||
|
# failed integration). Mirrors the pattern used by elastic_fleet_load_integrations_dir.
|
||||||
|
OUTPUT_LOCK=$(mktemp)
|
||||||
|
FAIL_FILE=$(mktemp)
|
||||||
|
trap 'rm -f "$OUTPUT_LOCK" "$FAIL_FILE"' EXIT
|
||||||
|
|
||||||
|
# Cache of package name -> latest available version, so the same package is only looked up
|
||||||
|
# once instead of once per (policy, integration).
|
||||||
|
declare -A LATEST_VERSION_CACHE
|
||||||
|
|
||||||
ERROR=false
|
|
||||||
for AGENT_POLICY in $agent_policies; do
|
for AGENT_POLICY in $agent_policies; do
|
||||||
if ! integrations=$(elastic_fleet_integration_policy_names "$AGENT_POLICY"); then
|
# Fetch the agent policy a single time; package name/version and integration id are all
|
||||||
|
# extracted locally below instead of re-fetching the same policy per integration.
|
||||||
|
if ! POLICY_JSON=$(fleet_api "agent_policies/$AGENT_POLICY"); then
|
||||||
# this script upgrades default integration packages, exit 1 and let salt handle retrying
|
# this script upgrades default integration packages, exit 1 and let salt handle retrying
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
for INTEGRATION in $integrations; do
|
|
||||||
if ! [[ "$INTEGRATION" == "elastic-defend-endpoints" ]] && ! [[ "$INTEGRATION" == "fleet_server-"* ]]; then
|
# One jq pass emits name/package.name/package.version/id for every eligible integration.
|
||||||
# Get package name so we know what package to look for when checking the current and latest available version
|
# The endpoint/fleet_server skips and the default-package gate are applied here in jq.
|
||||||
if ! PACKAGE_NAME=$(elastic_fleet_integration_policy_package_name "$AGENT_POLICY" "$INTEGRATION"); then
|
# $defaults (not $def, a jq reserved keyword) holds the default package list.
|
||||||
|
while IFS=$'\t' read -r INTEGRATION PACKAGE_NAME PACKAGE_VERSION INTEGRATION_ID; do
|
||||||
|
[ -n "$INTEGRATION" ] || continue
|
||||||
|
|
||||||
|
# Look up the latest available version once per package, then memoize it.
|
||||||
|
if [[ -z "${LATEST_VERSION_CACHE[$PACKAGE_NAME]+set}" ]]; then
|
||||||
|
if ! AVAILABLE_VERSION=$(elastic_fleet_package_latest_version_check "$PACKAGE_NAME"); then
|
||||||
|
echo "Error: Failed getting latest version for $PACKAGE_NAME"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
|
LATEST_VERSION_CACHE[$PACKAGE_NAME]=$AVAILABLE_VERSION
|
||||||
if [[ " ${default_packages[@]} " =~ " $PACKAGE_NAME " ]]; then
|
|
||||||
{%- endif %}
|
|
||||||
# Get currently installed version of package
|
|
||||||
attempt=0
|
|
||||||
max_attempts=3
|
|
||||||
while [ $attempt -lt $max_attempts ]; do
|
|
||||||
if PACKAGE_VERSION=$(elastic_fleet_integration_policy_package_version "$AGENT_POLICY" "$INTEGRATION") && AVAILABLE_VERSION=$(elastic_fleet_package_latest_version_check "$PACKAGE_NAME"); then
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
attempt=$((attempt + 1))
|
|
||||||
done
|
|
||||||
if [ $attempt -eq $max_attempts ]; then
|
|
||||||
echo "Error: Failed getting $PACKAGE_VERSION or $AVAILABLE_VERSION"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Get integration ID
|
|
||||||
if ! INTEGRATION_ID=$(elastic_fleet_integration_id "$AGENT_POLICY" "$INTEGRATION"); then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$PACKAGE_VERSION" != "$AVAILABLE_VERSION" ]]; then
|
|
||||||
# Dry run of the upgrade
|
|
||||||
echo ""
|
|
||||||
echo "Current $PACKAGE_NAME package version ($PACKAGE_VERSION) is not the same as the latest available package ($AVAILABLE_VERSION)..."
|
|
||||||
echo "Upgrading $INTEGRATION..."
|
|
||||||
echo "Starting dry run..."
|
|
||||||
if ! DRYRUN_OUTPUT=$(elastic_fleet_integration_policy_dryrun_upgrade "$INTEGRATION_ID"); then
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
DRYRUN_ERRORS=$(echo "$DRYRUN_OUTPUT" | jq .[].hasErrors)
|
|
||||||
|
|
||||||
# If no errors with dry run, proceed with actual upgrade
|
|
||||||
if [[ "$DRYRUN_ERRORS" == "false" ]]; then
|
|
||||||
echo "No errors detected. Proceeding with upgrade..."
|
|
||||||
if ! elastic_fleet_integration_policy_upgrade "$INTEGRATION_ID"; then
|
|
||||||
echo "Error: Upgrade failed for $PACKAGE_NAME with integration ID '$INTEGRATION_ID'."
|
|
||||||
ERROR=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "Errors detected during dry run for $PACKAGE_NAME policy upgrade..."
|
|
||||||
ERROR=true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
|
|
||||||
fi
|
|
||||||
{%- endif %}
|
|
||||||
fi
|
fi
|
||||||
done
|
AVAILABLE_VERSION=${LATEST_VERSION_CACHE[$PACKAGE_NAME]}
|
||||||
|
|
||||||
|
if [[ "$PACKAGE_VERSION" != "$AVAILABLE_VERSION" ]]; then
|
||||||
|
# Dry run, then (if clean) the actual upgrade, dispatched as a throttled background
|
||||||
|
# job. Each job builds its full log into one block, then flushes it under a single
|
||||||
|
# shared lock (OUTPUT_LOCK) so concurrent jobs never interleave on stdout; a failed
|
||||||
|
# job also appends a marker line to FAIL_FILE while holding that same lock.
|
||||||
|
elastic_fleet_throttle
|
||||||
|
{
|
||||||
|
block=$'\n'"Current $PACKAGE_NAME package version ($PACKAGE_VERSION) is not the same as the latest available package ($AVAILABLE_VERSION)..."$'\n'
|
||||||
|
block+="Upgrading $INTEGRATION..."$'\n'"Starting dry run..."$'\n'
|
||||||
|
fail=""
|
||||||
|
if ! DRYRUN_OUTPUT=$(elastic_fleet_integration_policy_dryrun_upgrade "$INTEGRATION_ID"); then
|
||||||
|
block+="Error: Failed to complete dry run for '$INTEGRATION_ID'."$'\n'
|
||||||
|
fail="dryrun $INTEGRATION"
|
||||||
|
elif [[ "$(jq .[].hasErrors <<<"$DRYRUN_OUTPUT")" == "false" ]]; then
|
||||||
|
block+="No errors detected. Proceeding with upgrade..."$'\n'
|
||||||
|
if ! elastic_fleet_integration_policy_upgrade "$INTEGRATION_ID"; then
|
||||||
|
block+="Error: Upgrade failed for $PACKAGE_NAME with integration ID '$INTEGRATION_ID'."$'\n'
|
||||||
|
fail="upgrade $INTEGRATION"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
block+="Errors detected during dry run for $PACKAGE_NAME policy upgrade..."$'\n'
|
||||||
|
fail="dryrun-errors $INTEGRATION"
|
||||||
|
fi
|
||||||
|
{
|
||||||
|
flock 9
|
||||||
|
printf '%s' "$block"
|
||||||
|
[ -n "$fail" ] && printf '%s\n' "$fail" >>"$FAIL_FILE"
|
||||||
|
} 9>>"$OUTPUT_LOCK"
|
||||||
|
} &
|
||||||
|
fi
|
||||||
|
done < <(jq -r --argjson defaults "$default_packages_json" '
|
||||||
|
.item.package_policies[]
|
||||||
|
| select(.name != "elastic-defend-endpoints")
|
||||||
|
| select(.name | startswith("fleet_server-") | not)
|
||||||
|
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
|
||||||
|
| select(.package.name | IN($defaults[]))
|
||||||
|
{%- endif %}
|
||||||
|
| [.name, .package.name, .package.version, .id] | @tsv
|
||||||
|
' <<<"$POLICY_JSON")
|
||||||
done
|
done
|
||||||
if [[ "$ERROR" == "true" ]]; then
|
|
||||||
|
# Barrier: wait for every dispatched dry-run/upgrade job to finish.
|
||||||
|
wait
|
||||||
|
|
||||||
|
if [ -s "$FAIL_FILE" ]; then
|
||||||
|
printf '\nFailed integration upgrades:\n'
|
||||||
|
cat "$FAIL_FILE"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
echo
|
echo
|
||||||
@@ -16,9 +16,10 @@
|
|||||||
STATE_FILE_SUCCESS=/opt/so/state/estemplates.txt
|
STATE_FILE_SUCCESS=/opt/so/state/estemplates.txt
|
||||||
INSTALLED_PACKAGE_LIST=/tmp/esfleet_installed_packages.json
|
INSTALLED_PACKAGE_LIST=/tmp/esfleet_installed_packages.json
|
||||||
BULK_INSTALL_PACKAGE_LIST=/tmp/esfleet_bulk_install.json
|
BULK_INSTALL_PACKAGE_LIST=/tmp/esfleet_bulk_install.json
|
||||||
BULK_INSTALL_PACKAGE_TMP=/tmp/esfleet_bulk_install_tmp.json
|
|
||||||
BULK_INSTALL_OUTPUT=/opt/so/state/esfleet_bulk_install_results.json
|
BULK_INSTALL_OUTPUT=/opt/so/state/esfleet_bulk_install_results.json
|
||||||
PACKAGE_COMPONENTS=/opt/so/state/esfleet_package_components.json
|
INTEGRATION_PACKAGE_COMPONENTS=/opt/so/state/esfleet_package_components.json
|
||||||
|
INPUT_PACKAGE_COMPONENTS=/opt/so/state/esfleet_input_package_components.json
|
||||||
|
CONTENT_PACKAGE_COMPONENTS=/opt/so/state/esfleet_content_package_components.json
|
||||||
COMPONENT_TEMPLATES=/opt/so/state/esfleet_component_templates.json
|
COMPONENT_TEMPLATES=/opt/so/state/esfleet_component_templates.json
|
||||||
|
|
||||||
PENDING_UPDATE=false
|
PENDING_UPDATE=false
|
||||||
@@ -27,29 +28,6 @@ PENDING_UPDATE=false
|
|||||||
# Requiring some level of manual Elastic Stack configuration before installation
|
# Requiring some level of manual Elastic Stack configuration before installation
|
||||||
EXCLUDED_INTEGRATIONS=('apm')
|
EXCLUDED_INTEGRATIONS=('apm')
|
||||||
|
|
||||||
version_conversion(){
|
|
||||||
version=$1
|
|
||||||
echo "$version" | awk -F '.' '{ printf("%d%03d%03d\n", $1, $2, $3); }'
|
|
||||||
}
|
|
||||||
|
|
||||||
compare_versions() {
|
|
||||||
version1=$1
|
|
||||||
version2=$2
|
|
||||||
|
|
||||||
# Convert versions to numbers
|
|
||||||
num1=$(version_conversion "$version1")
|
|
||||||
num2=$(version_conversion "$version2")
|
|
||||||
|
|
||||||
# Compare using bc
|
|
||||||
if (( $(echo "$num1 < $num2" | bc -l) )); then
|
|
||||||
echo "less"
|
|
||||||
elif (( $(echo "$num1 > $num2" | bc -l) )); then
|
|
||||||
echo "greater"
|
|
||||||
else
|
|
||||||
echo "equal"
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
IFS=$'\n'
|
IFS=$'\n'
|
||||||
agent_policies=$(elastic_fleet_agent_policy_ids)
|
agent_policies=$(elastic_fleet_agent_policy_ids)
|
||||||
if [ $? -ne 0 ]; then
|
if [ $? -ne 0 ]; then
|
||||||
@@ -61,23 +39,23 @@ default_packages=({% for pkg in SUPPORTED_PACKAGES %}"{{ pkg }}"{% if not loop.l
|
|||||||
|
|
||||||
in_use_integrations=()
|
in_use_integrations=()
|
||||||
|
|
||||||
|
# Fetch each agent policy once; its package_policies[] already contain both the integration name
|
||||||
|
# and the .package.name, so extract all non-default package names locally in a single jq instead
|
||||||
|
# of re-fetching the same policy per integration.
|
||||||
|
default_packages_json=$(printf '%s\n' "${default_packages[@]}" | jq -R . | jq -s '.')
|
||||||
for AGENT_POLICY in $agent_policies; do
|
for AGENT_POLICY in $agent_policies; do
|
||||||
|
|
||||||
if ! integrations=$(elastic_fleet_integration_policy_names "$AGENT_POLICY"); then
|
if ! policy_json=$(fleet_api "agent_policies/$AGENT_POLICY"); then
|
||||||
# skip the agent policy if we can't get required info, let salt retry. Integrations loaded by this script are non-default integrations.
|
# skip the agent policy if we can't get required info, let salt retry. Integrations loaded by this script are non-default integrations.
|
||||||
echo "Skipping $AGENT_POLICY.. "
|
echo "Skipping $AGENT_POLICY.. "
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
for INTEGRATION in $integrations; do
|
# non-default integrations that are in-use in any policy
|
||||||
if ! PACKAGE_NAME=$(elastic_fleet_integration_policy_package_name "$AGENT_POLICY" "$INTEGRATION"); then
|
while IFS= read -r PACKAGE_NAME; do
|
||||||
echo "Not adding $INTEGRATION, couldn't get package name"
|
[ -n "$PACKAGE_NAME" ] && in_use_integrations+=("$PACKAGE_NAME")
|
||||||
continue
|
done < <(jq -r --argjson defaults "$default_packages_json" \
|
||||||
fi
|
'.item.package_policies[].package.name | select(. as $n | ($defaults | index($n)) | not)' \
|
||||||
# non-default integrations that are in-use in any policy
|
<<<"$policy_json")
|
||||||
if ! [[ " ${default_packages[@]} " =~ " $PACKAGE_NAME " ]]; then
|
|
||||||
in_use_integrations+=("$PACKAGE_NAME")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ -f $STATE_FILE_SUCCESS ]]; then
|
if [[ -f $STATE_FILE_SUCCESS ]]; then
|
||||||
@@ -88,72 +66,55 @@ if [[ -f $STATE_FILE_SUCCESS ]]; then
|
|||||||
rm -f $INSTALLED_PACKAGE_LIST
|
rm -f $INSTALLED_PACKAGE_LIST
|
||||||
echo $latest_package_list | jq '{packages: [.items[] | {name: .name, latest_version: .version, installed_version: .installationInfo.version, subscription: .conditions.elastic.subscription }]}' >> $INSTALLED_PACKAGE_LIST
|
echo $latest_package_list | jq '{packages: [.items[] | {name: .name, latest_version: .version, installed_version: .installationInfo.version, subscription: .conditions.elastic.subscription }]}' >> $INSTALLED_PACKAGE_LIST
|
||||||
|
|
||||||
while read -r package; do
|
# Build the bulk install list and the per-package status messages with two jq passes
|
||||||
# get package details
|
# instead of a per-package bash loop. The old loop forked ~10 processes per package
|
||||||
package_name=$(echo "$package" | jq -r '.name')
|
# (5 jq + awk/bc for the version compare) and re-parsed/rewrote a growing JSON file on
|
||||||
latest_version=$(echo "$package" | jq -r '.latest_version')
|
# every add (O(n^2)). Selection and messages below are identical to that logic.
|
||||||
installed_version=$(echo "$package" | jq -r '.installed_version')
|
SUB={% if SUB %}true{% else %}false{% endif %}
|
||||||
subscription=$(echo "$package" | jq -r '.subscription')
|
AUTOUP={% if AUTO_UPGRADE_INTEGRATIONS %}true{% else %}false{% endif %}
|
||||||
bulk_package=$(echo "$package" | jq '{name: .name, version: .latest_version}' )
|
EXCLUDED_JSON=$(printf '%s\n' "${EXCLUDED_INTEGRATIONS[@]}" | jq -R 'select(length>0)' | jq -s '.')
|
||||||
|
INUSE_JSON=$(printf '%s\n' "${in_use_integrations[@]}" | jq -R 'select(length>0)' | jq -s 'unique')
|
||||||
|
|
||||||
if [[ ! "${EXCLUDED_INTEGRATIONS[@]}" =~ "$package_name" ]]; then
|
# vnum replicates the previous version_conversion (%d%03d%03d of the first three dotted
|
||||||
{% if not SUB %}
|
# fields); needs() replicates the excluded/subscription/installed/upgrade/in-use logic.
|
||||||
if [[ "$subscription" != "basic" && "$subscription" != "null" && -n "$subscription" ]]; then
|
JQ_DECISION='
|
||||||
# pass over integrations that require non-basic elastic license
|
def vnum:
|
||||||
echo "$package_name integration requires an Elastic license of $subscription or greater... skipping"
|
[ (split(".")|.[0:3][] | gsub("[^0-9].*";"") | (if .=="" then "0" else . end) | tonumber) ]
|
||||||
continue
|
| (.[0]//0)*1000000 + (.[1]//0)*1000 + (.[2]//0);
|
||||||
else
|
def needs($sub;$autoup;$excluded;$inuse):
|
||||||
if [[ "$installed_version" == "null" || -z "$installed_version" ]]; then
|
.name as $n
|
||||||
echo "$package_name is not installed... Adding to next update."
|
| ($n | IN($excluded[]) | not)
|
||||||
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
|
and ( $sub or (.subscription==null or .subscription=="basic" or .subscription=="") )
|
||||||
|
and ( (.installed_version==null or .installed_version=="")
|
||||||
|
or ( ((.latest_version|vnum) > (.installed_version|vnum))
|
||||||
|
and ( $autoup or ($n | IN($inuse[]) | not) ) ) );'
|
||||||
|
|
||||||
PENDING_UPDATE=true
|
JQ_ARGS=(--argjson sub "$SUB" --argjson autoup "$AUTOUP" --argjson excluded "$EXCLUDED_JSON" --argjson inuse "$INUSE_JSON")
|
||||||
else
|
|
||||||
results=$(compare_versions "$latest_version" "$installed_version")
|
|
||||||
if [ $results == "greater" ]; then
|
|
||||||
{#- When auto_upgrade_integrations is false, skip upgrading in_use_integrations #}
|
|
||||||
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
|
|
||||||
if ! [[ " ${in_use_integrations[@]} " =~ " $package_name " ]]; then
|
|
||||||
{%- endif %}
|
|
||||||
echo "$package_name is at version $installed_version latest version is $latest_version... Adding to next update."
|
|
||||||
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
|
|
||||||
|
|
||||||
PENDING_UPDATE=true
|
# (a) Per-package status messages (parity with the previous echo output).
|
||||||
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
|
jq -r "${JQ_ARGS[@]}" "$JQ_DECISION"'
|
||||||
else
|
.packages[]
|
||||||
echo "skipping available upgrade for in use integration - $package_name."
|
| .name as $n
|
||||||
fi
|
| if ($n|IN($excluded[])) then "Skipping \($n)..."
|
||||||
{%- endif %}
|
elif (($sub|not) and (.subscription!=null and .subscription!="basic" and .subscription!="")) then
|
||||||
fi
|
"\($n) integration requires an Elastic license of \(.subscription) or greater... skipping"
|
||||||
fi
|
elif (.installed_version==null or .installed_version=="") then
|
||||||
fi
|
"\($n) is not installed... Adding to next update."
|
||||||
{% else %}
|
elif ((.latest_version|vnum) > (.installed_version|vnum)) then
|
||||||
if [[ "$installed_version" == "null" || -z "$installed_version" ]]; then
|
(if ($autoup or ($n|IN($inuse[])|not))
|
||||||
echo "$package_name is not installed... Adding to next update."
|
then "\($n) is at version \(.installed_version) latest version is \(.latest_version)... Adding to next update."
|
||||||
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
|
else "skipping available upgrade for in use integration - \($n)." end)
|
||||||
PENDING_UPDATE=true
|
else empty end
|
||||||
else
|
' "$INSTALLED_PACKAGE_LIST"
|
||||||
results=$(compare_versions "$latest_version" "$installed_version")
|
|
||||||
if [ $results == "greater" ]; then
|
# (b) The bulk install list, built in a single pass.
|
||||||
{#- When auto_upgrade_integrations is false, skip upgrading in_use_integrations #}
|
jq "${JQ_ARGS[@]}" "$JQ_DECISION"'
|
||||||
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
|
{packages: [ .packages[] | select(needs($sub;$autoup;$excluded;$inuse)) | {name, version: .latest_version} ]}
|
||||||
if ! [[ " ${in_use_integrations[@]} " =~ " $package_name " ]]; then
|
' "$INSTALLED_PACKAGE_LIST" > "$BULK_INSTALL_PACKAGE_LIST"
|
||||||
{%- endif %}
|
|
||||||
echo "$package_name is at version $installed_version latest version is $latest_version... Adding to next update."
|
if jq -e '.packages | length > 0' "$BULK_INSTALL_PACKAGE_LIST" >/dev/null; then
|
||||||
jq --argjson package "$bulk_package" '.packages += [$package]' $BULK_INSTALL_PACKAGE_LIST > $BULK_INSTALL_PACKAGE_TMP && mv $BULK_INSTALL_PACKAGE_TMP $BULK_INSTALL_PACKAGE_LIST
|
PENDING_UPDATE=true
|
||||||
PENDING_UPDATE=true
|
fi
|
||||||
{%- if not AUTO_UPGRADE_INTEGRATIONS %}
|
|
||||||
else
|
|
||||||
echo "skipping available upgrade for in use integration - $package_name."
|
|
||||||
fi
|
|
||||||
{%- endif %}
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
{% endif %}
|
|
||||||
else
|
|
||||||
echo "Skipping $package_name..."
|
|
||||||
fi
|
|
||||||
done <<< "$(jq -c '.packages[]' "$INSTALLED_PACKAGE_LIST")"
|
|
||||||
|
|
||||||
if [ "$PENDING_UPDATE" = true ]; then
|
if [ "$PENDING_UPDATE" = true ]; then
|
||||||
# Run chunked install of packages
|
# Run chunked install of packages
|
||||||
@@ -179,10 +140,13 @@ if [[ -f $STATE_FILE_SUCCESS ]]; then
|
|||||||
else
|
else
|
||||||
echo "Elastic integrations don't appear to need installation/updating..."
|
echo "Elastic integrations don't appear to need installation/updating..."
|
||||||
fi
|
fi
|
||||||
# Write out file for generating index/component/ilm templates
|
# Write out file for generating index/component/ilm templates, keeping each package type separate
|
||||||
if latest_installed_package_list=$(elastic_fleet_installed_packages); then
|
for package_type in "INTEGRATION" "INPUT" "CONTENT"; do
|
||||||
echo $latest_installed_package_list | jq '[.items[] | {name: .name, es_index_patterns: .dataStreams}]' > $PACKAGE_COMPONENTS
|
if latest_installed_package_list=$(elastic_fleet_installed_packages_components "$package_type"); then
|
||||||
fi
|
outfile="${package_type}_PACKAGE_COMPONENTS"
|
||||||
|
echo $latest_installed_package_list > "${!outfile}"
|
||||||
|
fi
|
||||||
|
done
|
||||||
if retry 3 1 "so-elasticsearch-query / --fail --output /dev/null"; then
|
if retry 3 1 "so-elasticsearch-query / --fail --output /dev/null"; then
|
||||||
# Refresh installed component template list
|
# Refresh installed component template list
|
||||||
latest_component_templates_list=$(so-elasticsearch-query _component_template | jq '.component_templates[] | .name' | jq -s '.')
|
latest_component_templates_list=$(so-elasticsearch-query _component_template | jq '.component_templates[] | .name' | jq -s '.')
|
||||||
|
|||||||
@@ -235,6 +235,16 @@ function update_kafka_outputs() {
|
|||||||
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
# Compare the current Elastic Fleet certificate against what is on disk
|
||||||
|
POLICY_CERT_SHA=$(jq -r '.item.ssl.certificate' <<< $RAW_JSON | openssl x509 -noout -sha256 -fingerprint)
|
||||||
|
DISK_CERT_SHA=$(openssl x509 -in /etc/pki/elasticfleet-logstash.crt -noout -sha256 -fingerprint)
|
||||||
|
|
||||||
|
if [[ "$POLICY_CERT_SHA" != "$DISK_CERT_SHA" ]]; then
|
||||||
|
printf "Certificate on disk doesn't match certificate in policy - forcing update\n"
|
||||||
|
UPDATE_CERTS=true
|
||||||
|
FORCE_UPDATE=true
|
||||||
|
fi
|
||||||
|
|
||||||
# Sort & hash the new list of Logstash Outputs
|
# Sort & hash the new list of Logstash Outputs
|
||||||
NEW_LIST_JSON=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${NEW_LIST[@]}")
|
NEW_LIST_JSON=$(jq --compact-output --null-input '$ARGS.positional' --args -- "${NEW_LIST[@]}")
|
||||||
NEW_HASH=$(sha256sum <<< "$NEW_LIST_JSON" | awk '{print $1}')
|
NEW_HASH=$(sha256sum <<< "$NEW_LIST_JSON" | awk '{print $1}')
|
||||||
|
|||||||
@@ -8,18 +8,74 @@
|
|||||||
|
|
||||||
. /usr/sbin/so-elastic-fleet-common
|
. /usr/sbin/so-elastic-fleet-common
|
||||||
|
|
||||||
|
PKG_LOAD_FAILURES=0
|
||||||
|
PKG_LOAD_FAILURES_NAMES=()
|
||||||
|
PKG_UPGRADED=0
|
||||||
|
|
||||||
|
cleanup_elasticsearch_fleet_transforms() {
|
||||||
|
local transforms transform_id attempt
|
||||||
|
|
||||||
|
if ! transforms=$(so-elasticsearch-query "_transform/logs-elasticsearch.index_pivot-default-*" --retry 1 --retry-delay 5); then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
while IFS= read -r transform_id; do
|
||||||
|
[ -n "$transform_id" ] || continue
|
||||||
|
for attempt in {1..3}; do
|
||||||
|
if so-elasticsearch-query "_transform/$transform_id?force=true" -XDELETE --fail --retry 1 --retry-delay 5; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 5
|
||||||
|
done
|
||||||
|
done < <(jq -r '.transforms[]?.id' <<< "$transforms")
|
||||||
|
}
|
||||||
|
|
||||||
{%- for PACKAGE in SUPPORTED_PACKAGES %}
|
{%- for PACKAGE in SUPPORTED_PACKAGES %}
|
||||||
echo "Upgrading {{ PACKAGE }} package..."
|
if INSTALLED_VERSION=$(elastic_fleet_package_version_check "{{ PACKAGE }}") && LATEST_VERSION=$(elastic_fleet_package_latest_version_check "{{ PACKAGE }}"); then
|
||||||
if VERSION=$(elastic_fleet_package_latest_version_check "{{ PACKAGE }}"); then
|
|
||||||
if ! elastic_fleet_package_install "{{ PACKAGE }}" "$VERSION"; then
|
if [ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]; then
|
||||||
# exit 1 on failure to upgrade a default package, allow salt to handle retries
|
echo "{{ PACKAGE }} integration version $INSTALLED_VERSION is already at the reported latest version $LATEST_VERSION, skipping upgrade."
|
||||||
echo -e "\nERROR: Failed to upgrade $PACKAGE to version: $VERSION"
|
else
|
||||||
exit 1
|
{%- if PACKAGE == 'elasticsearch' %}
|
||||||
|
cleanup_elasticsearch_fleet_transforms
|
||||||
|
{%- endif %}
|
||||||
|
echo "Upgrading {{ PACKAGE }} package from $INSTALLED_VERSION to version $LATEST_VERSION..."
|
||||||
|
if ! elastic_fleet_package_install "{{ PACKAGE }}" "$LATEST_VERSION"; then
|
||||||
|
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
||||||
|
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
||||||
|
# check that package has upgraded to the expected version after install command
|
||||||
|
elif ! LATEST_VERSION=$(elastic_fleet_package_latest_version_check "{{ PACKAGE }}"); then
|
||||||
|
echo "ERROR: Failed to get latest version information for integration {{ PACKAGE }} after upgrade attempt"
|
||||||
|
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
||||||
|
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
||||||
|
elif INSTALLED_VERSION=$(elastic_fleet_package_version_check "{{ PACKAGE }}") && [ "$INSTALLED_VERSION" == "$LATEST_VERSION" ]; then
|
||||||
|
echo "{{ PACKAGE }} integration upgraded to version $LATEST_VERSION."
|
||||||
|
PKG_UPGRADED=$((PKG_UPGRADED + 1))
|
||||||
|
else
|
||||||
|
echo "ERROR: {{ PACKAGE }} integration still at ${INSTALLED_VERSION:-unknown}; expected $LATEST_VERSION"
|
||||||
|
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
||||||
|
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo -e "\nERROR: Failed to get version information for integration $PACKAGE"
|
echo "ERROR: Failed to get version information for integration {{ PACKAGE }}"
|
||||||
|
PKG_LOAD_FAILURES=$((PKG_LOAD_FAILURES + 1))
|
||||||
|
PKG_LOAD_FAILURES_NAMES+=("{{ PACKAGE }}")
|
||||||
fi
|
fi
|
||||||
echo
|
|
||||||
{%- endfor %}
|
{%- endfor %}
|
||||||
echo
|
|
||||||
/usr/sbin/so-elasticsearch-templates-load
|
if [ $PKG_UPGRADED -gt 0 ]; then
|
||||||
|
echo "Elasticsearch template statefiles cleared after $PKG_UPGRADED package upgrade(s), so templates can reload."
|
||||||
|
rm -f /opt/so/state/estemplates.txt /opt/so/state/addon_estemplates.txt
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ $PKG_LOAD_FAILURES -gt 0 ]; then
|
||||||
|
echo "ERROR: Failed to upgrade $PKG_LOAD_FAILURES package(s):"
|
||||||
|
for PKG in "${PKG_LOAD_FAILURES_NAMES[@]}"; do
|
||||||
|
echo " - $PKG"
|
||||||
|
done
|
||||||
|
# exit 1 on failure to upgrade a default package, allow salt to handle retries
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
echo "Successfully upgraded all packages."
|
||||||
|
fi
|
||||||
@@ -181,6 +181,9 @@ if ! elastic_fleet_policy_create "so-grid-nodes_heavy" "SO Grid Nodes - Heavy No
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Check for package upgrades
|
||||||
|
so-elastic-fleet-package-upgrade
|
||||||
|
|
||||||
# Load Integrations for default policies
|
# Load Integrations for default policies
|
||||||
so-elastic-fleet-integration-policy-load
|
so-elastic-fleet-integration-policy-load
|
||||||
|
|
||||||
@@ -199,26 +202,9 @@ fi
|
|||||||
### Finalization ###
|
### Finalization ###
|
||||||
|
|
||||||
# Query for Enrollment Tokens for default policies
|
# Query for Enrollment Tokens for default policies
|
||||||
if ENDPOINTSENROLLMENTOKEN_RAW=$(fleet_api "enrollment_api_keys" -H 'kbn-xsrf: true' -H 'Content-Type: application/json'); then
|
ENDPOINTSENROLLMENTOKEN=$(elastic_fleet_active_enrollment_token "endpoints-initial") || exit 1
|
||||||
ENDPOINTSENROLLMENTOKEN=$(echo "$ENDPOINTSENROLLMENTOKEN_RAW" | jq .list | jq -r -c '.[] | select(.policy_id | contains("endpoints-initial")) | .api_key')
|
GRIDNODESENROLLMENTOKENGENERAL=$(elastic_fleet_active_enrollment_token "so-grid-nodes_general") || exit 1
|
||||||
else
|
GRIDNODESENROLLMENTOKENHEAVY=$(elastic_fleet_active_enrollment_token "so-grid-nodes_heavy") || exit 1
|
||||||
echo -e "\nFailed to query for Endpoints enrollment token"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if GRIDNODESENROLLMENTOKENGENERAL_RAW=$(fleet_api "enrollment_api_keys" -H 'kbn-xsrf: true' -H 'Content-Type: application/json'); then
|
|
||||||
GRIDNODESENROLLMENTOKENGENERAL=$(echo "$GRIDNODESENROLLMENTOKENGENERAL_RAW" | jq .list | jq -r -c '.[] | select(.policy_id | contains("so-grid-nodes_general")) | .api_key')
|
|
||||||
else
|
|
||||||
echo -e "\nFailed to query for Grid nodes - General enrollment token"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if GRIDNODESENROLLMENTOKENHEAVY_RAW=$(fleet_api "enrollment_api_keys" -H 'kbn-xsrf: true' -H 'Content-Type: application/json'); then
|
|
||||||
GRIDNODESENROLLMENTOKENHEAVY=$(echo "$GRIDNODESENROLLMENTOKENHEAVY_RAW" | jq .list | jq -r -c '.[] | select(.policy_id | contains("so-grid-nodes_heavy")) | .api_key')
|
|
||||||
else
|
|
||||||
echo -e "\nFailed to query for Grid nodes - Heavy enrollment token"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Store needed data in minion pillar
|
# Store needed data in minion pillar
|
||||||
pillar_file=/opt/so/saltstack/local/pillar/minions/{{ GLOBALS.minion_id }}.sls
|
pillar_file=/opt/so/saltstack/local/pillar/minions/{{ GLOBALS.minion_id }}.sls
|
||||||
@@ -241,11 +227,37 @@ printf '%s\n'\
|
|||||||
"" >> "$global_pillar_file"
|
"" >> "$global_pillar_file"
|
||||||
|
|
||||||
# Call Elastic-Fleet Salt State
|
# Call Elastic-Fleet Salt State
|
||||||
printf "\nApplying elasticfleet state"
|
printf "\nApplying elasticfleet state\n"
|
||||||
salt-call state.apply elasticfleet queue=True
|
for state_attempt in {1..3}; do
|
||||||
|
if salt-call state.apply elasticfleet queue=True; then
|
||||||
|
break
|
||||||
|
elif [[ $state_attempt -lt 3 ]]; then
|
||||||
|
printf "\nElasticfleet state did not complete successfully... Attempt (%s/3). Retrying...\n" "$state_attempt"
|
||||||
|
sleep 10
|
||||||
|
else
|
||||||
|
printf "\nFailure(s) in elasticfleet state... Exiting...\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf "\nRunning so-elastic-agent-gen-installers\n"
|
||||||
# Generate installers & install Elastic Agent on the node
|
# Generate installers & install Elastic Agent on the node
|
||||||
so-elastic-agent-gen-installers
|
for agent_gen_attempt in {1..3}; do
|
||||||
printf "\nApplying elasticfleet.install_agent_grid state"
|
if so-elastic-agent-gen-installers; then
|
||||||
salt-call state.apply elasticfleet.install_agent_grid queue=True
|
break
|
||||||
exit 0
|
elif [[ $agent_gen_attempt -lt 3 ]]; then
|
||||||
|
printf "\nUnable to generate Elastic Agent installers... Attempt (%s/3). Retrying...\n" "$agent_gen_attempt"
|
||||||
|
sleep 10
|
||||||
|
else
|
||||||
|
printf "\nFailed to generate Elastic Agent installers after 3 attempts. Exiting...\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
printf "\nApplying elasticfleet.install_agent_grid state\n"
|
||||||
|
if ! salt-call state.apply elasticfleet.install_agent_grid queue=True; then
|
||||||
|
printf "\nFailure(s) in elasticfleet.install_agent_grid state... Exiting...\n"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
printf "\nElastic Fleet setup completed successfully\n"
|
||||||
@@ -0,0 +1,201 @@
|
|||||||
|
# Copyright Security Onion Solutions LLC and/or licensed to Security Onion Solutions LLC under one
|
||||||
|
# or more contributor license agreements. Licensed under the Elastic License 2.0 as shown at
|
||||||
|
# https://securityonion.net/license; you may not use this file except in compliance with the
|
||||||
|
# Elastic License 2.0.
|
||||||
|
|
||||||
|
{% from 'allowed_states.map.jinja' import allowed_states %}
|
||||||
|
{% if sls in allowed_states %}
|
||||||
|
{% from 'vars/globals.map.jinja' import GLOBALS %}
|
||||||
|
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCHMERGED %}
|
||||||
|
{% from 'elasticsearch/template.map.jinja' import ES_INDEX_SETTINGS, SO_MANAGED_INDICES %}
|
||||||
|
{% if GLOBALS.role != 'so-heavynode' %}
|
||||||
|
{% from 'elasticsearch/template.map.jinja' import ALL_ADDON_SETTINGS, ADDON_INDICES %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
include:
|
||||||
|
- elasticsearch.enabled
|
||||||
|
|
||||||
|
escomponenttemplates:
|
||||||
|
file.recurse:
|
||||||
|
- name: /opt/so/conf/elasticsearch/templates/component
|
||||||
|
- source: salt://elasticsearch/templates/component
|
||||||
|
- user: 930
|
||||||
|
- group: 939
|
||||||
|
- clean: True
|
||||||
|
- onchanges_in:
|
||||||
|
- file: so-elasticsearch-templates-reload
|
||||||
|
- show_changes: False
|
||||||
|
|
||||||
|
# Clean up legacy and non-SO managed templates from the elasticsearch/templates/index/ directory
|
||||||
|
so_index_template_dir:
|
||||||
|
file.directory:
|
||||||
|
- name: /opt/so/conf/elasticsearch/templates/index
|
||||||
|
- clean: True
|
||||||
|
{%- if SO_MANAGED_INDICES %}
|
||||||
|
- require:
|
||||||
|
{%- for index in SO_MANAGED_INDICES %}
|
||||||
|
- file: so_index_template_{{index}}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
|
||||||
|
{% if GLOBALS.role != "so-heavynode" %}
|
||||||
|
# Clean up legacy and non-SO managed templates from the elasticsearch/templates/addon-index/ directory
|
||||||
|
addon_index_template_dir:
|
||||||
|
file.directory:
|
||||||
|
- name: /opt/so/conf/elasticsearch/templates/addon-index
|
||||||
|
- clean: True
|
||||||
|
{%- if ADDON_INDICES %}
|
||||||
|
- require:
|
||||||
|
{%- for index in ADDON_INDICES %}
|
||||||
|
- file: addon_index_template_{{index}}
|
||||||
|
{%- endfor %}
|
||||||
|
{%- endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# Auto-generate index templates for SO managed indices (directly defined in elasticsearch/defaults.yaml)
|
||||||
|
# These index templates are for the core SO datasets and are always required
|
||||||
|
{% for index, settings in ES_INDEX_SETTINGS.items() %}
|
||||||
|
{% if settings.index_template is defined %}
|
||||||
|
so_index_template_{{index}}:
|
||||||
|
file.managed:
|
||||||
|
- name: /opt/so/conf/elasticsearch/templates/index/{{ index }}-template.json
|
||||||
|
- source: salt://elasticsearch/base-template.json.jinja
|
||||||
|
- defaults:
|
||||||
|
TEMPLATE_CONFIG: {{ settings.index_template }}
|
||||||
|
- template: jinja
|
||||||
|
- onchanges_in:
|
||||||
|
- file: so-elasticsearch-templates-reload
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
|
{% if GLOBALS.role != "so-heavynode" %}
|
||||||
|
# Auto-generate optional index templates for integration | input | content packages
|
||||||
|
# These index templates are not used by default (until user adds package to an agent policy).
|
||||||
|
# Pre-configured with standard defaults, and incorporated into SOC configuration for user customization.
|
||||||
|
{% for index,settings in ALL_ADDON_SETTINGS.items() %}
|
||||||
|
{% if settings.index_template is defined %}
|
||||||
|
addon_index_template_{{index}}:
|
||||||
|
file.managed:
|
||||||
|
- name: /opt/so/conf/elasticsearch/templates/addon-index/{{ index }}-template.json
|
||||||
|
- source: salt://elasticsearch/base-template.json.jinja
|
||||||
|
- defaults:
|
||||||
|
TEMPLATE_CONFIG: {{ settings.index_template }}
|
||||||
|
- template: jinja
|
||||||
|
- show_changes: False
|
||||||
|
- onchanges_in:
|
||||||
|
- file: addon-elasticsearch-templates-reload
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if GLOBALS.role in GLOBALS.manager_roles %}
|
||||||
|
so-es-cluster-settings:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-cluster-settings
|
||||||
|
- cwd: /opt/so
|
||||||
|
- template: jinja
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elasticsearch
|
||||||
|
- file: elasticsearch_sbin_jinja
|
||||||
|
- http: wait_for_so-elasticsearch
|
||||||
|
|
||||||
|
so-elasticsearch-system-indices-patch:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-system-indices-patch
|
||||||
|
- require:
|
||||||
|
- http: wait_for_so-elasticsearch
|
||||||
|
- file: so-elasticsearch-system-indices-patch-script
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
# heavynodes will only load ILM policies for SO managed indices. (Indicies defined in elasticsearch/defaults.yaml)
|
||||||
|
so-elasticsearch-ilm-policy-load:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-ilm-policy-load
|
||||||
|
- cwd: /opt/so
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elasticsearch
|
||||||
|
- file: so-elasticsearch-ilm-policy-load-script
|
||||||
|
- onchanges:
|
||||||
|
- file: so-elasticsearch-ilm-policy-load-script
|
||||||
|
|
||||||
|
so-elasticsearch-templates-reload:
|
||||||
|
file.absent:
|
||||||
|
- name: /opt/so/state/estemplates.txt
|
||||||
|
|
||||||
|
addon-elasticsearch-templates-reload:
|
||||||
|
file.absent:
|
||||||
|
- name: /opt/so/state/addon_estemplates.txt
|
||||||
|
|
||||||
|
# so-elasticsearch-templates-load will have its first successful run during the 'so-elastic-fleet-setup' script
|
||||||
|
so-elasticsearch-templates:
|
||||||
|
cmd.run:
|
||||||
|
{%- if GLOBALS.role == "so-heavynode" %}
|
||||||
|
- name: /usr/sbin/so-elasticsearch-templates-load --heavynode
|
||||||
|
{%- else %}
|
||||||
|
- name: /usr/sbin/so-elasticsearch-templates-load
|
||||||
|
{%- endif %}
|
||||||
|
- cwd: /opt/so
|
||||||
|
- template: jinja
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elasticsearch
|
||||||
|
- file: elasticsearch_sbin_jinja
|
||||||
|
|
||||||
|
so-elasticsearch-dlm-apply:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-dlm-apply
|
||||||
|
- cwd: /opt/so
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elasticsearch
|
||||||
|
- file: elasticsearch_sbin_jinja
|
||||||
|
- cmd: so-elasticsearch-templates
|
||||||
|
- retry:
|
||||||
|
attempts: 3
|
||||||
|
interval: 10
|
||||||
|
|
||||||
|
so-elasticsearch-pipelines:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-pipelines {{ GLOBALS.hostname }}
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elasticsearch
|
||||||
|
- file: so-elasticsearch-pipelines-script
|
||||||
|
|
||||||
|
so-elasticsearch-roles-load:
|
||||||
|
cmd.run:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-roles-load
|
||||||
|
- cwd: /opt/so
|
||||||
|
- template: jinja
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elasticsearch
|
||||||
|
- file: elasticsearch_sbin_jinja
|
||||||
|
|
||||||
|
{% if grains.role in ['so-managersearch', 'so-manager', 'so-managerhype'] %}
|
||||||
|
{% set ap = "absent" %}
|
||||||
|
{% endif %}
|
||||||
|
{% if grains.role in ['so-eval', 'so-standalone', 'so-heavynode'] %}
|
||||||
|
{# Remove so-elasticsearch-indices-delete script when using DLM #}
|
||||||
|
{% if ELASTICSEARCHMERGED.index_clean and ELASTICSEARCHMERGED.data_retention_method == "ILM" %}
|
||||||
|
{% set ap = "present" %}
|
||||||
|
{% else %}
|
||||||
|
{% set ap = "absent" %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if grains.role in ['so-eval', 'so-standalone', 'so-managersearch', 'so-heavynode', 'so-manager'] %}
|
||||||
|
so-elasticsearch-indices-delete:
|
||||||
|
cron.{{ap}}:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-indices-delete > /opt/so/log/elasticsearch/cron-elasticsearch-indices-delete.log 2>&1
|
||||||
|
- identifier: so-elasticsearch-indices-delete
|
||||||
|
- user: root
|
||||||
|
- minute: '*/5'
|
||||||
|
- hour: '*'
|
||||||
|
- daymonth: '*'
|
||||||
|
- month: '*'
|
||||||
|
- dayweek: '*'
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% else %}
|
||||||
|
|
||||||
|
{{sls}}_state_not_allowed:
|
||||||
|
test.fail_without_changes:
|
||||||
|
- name: {{sls}}_state_not_allowed
|
||||||
|
|
||||||
|
{% endif %}
|
||||||
@@ -32,24 +32,35 @@ elasticsearch:
|
|||||||
- gid: 930
|
- gid: 930
|
||||||
- home: /opt/so/conf/elasticsearch
|
- home: /opt/so/conf/elasticsearch
|
||||||
- createhome: False
|
- createhome: False
|
||||||
|
- shell: /sbin/nologin
|
||||||
|
|
||||||
elasticsearch_sbin:
|
elasticsearch_sbin:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://elasticsearch/tools/sbin
|
- source: salt://elasticsearch/tools/sbin
|
||||||
- user: 930
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
- exclude_pat:
|
- exclude_pat:
|
||||||
- so-elasticsearch-pipelines # exclude this because we need to watch it for changes, we sync it in another state
|
- so-elasticsearch-pipelines # exclude this because we need to watch it for changes, we sync it in another state
|
||||||
|
- so-elasticsearch-system-indices-patch
|
||||||
|
- show_changes: False
|
||||||
|
|
||||||
|
so-elasticsearch-system-indices-patch-script:
|
||||||
|
file.managed:
|
||||||
|
- name: /usr/sbin/so-elasticsearch-system-indices-patch
|
||||||
|
- source: salt://elasticsearch/tools/sbin/so-elasticsearch-system-indices-patch
|
||||||
|
- user: root
|
||||||
|
- group: root
|
||||||
|
- mode: 755
|
||||||
- show_changes: False
|
- show_changes: False
|
||||||
|
|
||||||
elasticsearch_sbin_jinja:
|
elasticsearch_sbin_jinja:
|
||||||
file.recurse:
|
file.recurse:
|
||||||
- name: /usr/sbin
|
- name: /usr/sbin
|
||||||
- source: salt://elasticsearch/tools/sbin_jinja
|
- source: salt://elasticsearch/tools/sbin_jinja
|
||||||
- user: 939
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- file_mode: 755
|
- file_mode: 755
|
||||||
- template: jinja
|
- template: jinja
|
||||||
- exclude_pat:
|
- exclude_pat:
|
||||||
@@ -62,18 +73,20 @@ so-elasticsearch-ilm-policy-load-script:
|
|||||||
file.managed:
|
file.managed:
|
||||||
- name: /usr/sbin/so-elasticsearch-ilm-policy-load
|
- name: /usr/sbin/so-elasticsearch-ilm-policy-load
|
||||||
- source: salt://elasticsearch/tools/sbin_jinja/so-elasticsearch-ilm-policy-load
|
- source: salt://elasticsearch/tools/sbin_jinja/so-elasticsearch-ilm-policy-load
|
||||||
- user: 930
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- mode: 754
|
- mode: 754
|
||||||
- template: jinja
|
- template: jinja
|
||||||
|
- defaults:
|
||||||
|
GLOBALS: {{ GLOBALS }}
|
||||||
- show_changes: False
|
- show_changes: False
|
||||||
|
|
||||||
so-elasticsearch-pipelines-script:
|
so-elasticsearch-pipelines-script:
|
||||||
file.managed:
|
file.managed:
|
||||||
- name: /usr/sbin/so-elasticsearch-pipelines
|
- name: /usr/sbin/so-elasticsearch-pipelines
|
||||||
- source: salt://elasticsearch/tools/sbin/so-elasticsearch-pipelines
|
- source: salt://elasticsearch/tools/sbin/so-elasticsearch-pipelines
|
||||||
- user: 930
|
- user: root
|
||||||
- group: 939
|
- group: root
|
||||||
- mode: 754
|
- mode: 754
|
||||||
- show_changes: False
|
- show_changes: False
|
||||||
|
|
||||||
@@ -91,6 +104,13 @@ estemplatedir:
|
|||||||
- group: 939
|
- group: 939
|
||||||
- makedirs: True
|
- makedirs: True
|
||||||
|
|
||||||
|
esaddontemplatedir:
|
||||||
|
file.directory:
|
||||||
|
- name: /opt/so/conf/elasticsearch/templates/addon-index
|
||||||
|
- user: 930
|
||||||
|
- group: 939
|
||||||
|
- makedirs: True
|
||||||
|
|
||||||
esrolesdir:
|
esrolesdir:
|
||||||
file.directory:
|
file.directory:
|
||||||
- name: /opt/so/conf/elasticsearch/roles
|
- name: /opt/so/conf/elasticsearch/roles
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
+17
-125
@@ -10,8 +10,6 @@
|
|||||||
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCH_NODES %}
|
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCH_NODES %}
|
||||||
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCH_SEED_HOSTS %}
|
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCH_SEED_HOSTS %}
|
||||||
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCHMERGED %}
|
{% from 'elasticsearch/config.map.jinja' import ELASTICSEARCHMERGED %}
|
||||||
{% set TEMPLATES = salt['pillar.get']('elasticsearch:templates', {}) %}
|
|
||||||
{% from 'elasticsearch/template.map.jinja' import ES_INDEX_SETTINGS %}
|
|
||||||
|
|
||||||
include:
|
include:
|
||||||
- ca
|
- ca
|
||||||
@@ -19,10 +17,14 @@ include:
|
|||||||
- elasticsearch.ssl
|
- elasticsearch.ssl
|
||||||
- elasticsearch.config
|
- elasticsearch.config
|
||||||
- elasticsearch.sostatus
|
- elasticsearch.sostatus
|
||||||
|
{%- if GLOBALS.role != "so-searchnode" %}
|
||||||
|
- elasticsearch.cluster
|
||||||
|
{%- endif%}
|
||||||
|
|
||||||
so-elasticsearch:
|
so-elasticsearch:
|
||||||
docker_container.running:
|
docker_container.running:
|
||||||
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elasticsearch:{{ ELASTICSEARCHMERGED.version }}
|
- image: {{ GLOBALS.registry_host }}:5000/{{ GLOBALS.image_repo }}/so-elasticsearch:{{ ELASTICSEARCHMERGED.version }}
|
||||||
|
- restart_policy: unless-stopped
|
||||||
- hostname: elasticsearch
|
- hostname: elasticsearch
|
||||||
- name: so-elasticsearch
|
- name: so-elasticsearch
|
||||||
- user: elasticsearch
|
- user: elasticsearch
|
||||||
@@ -101,134 +103,24 @@ so-elasticsearch:
|
|||||||
- cmd: auth_users_roles_inode
|
- cmd: auth_users_roles_inode
|
||||||
- cmd: auth_users_inode
|
- cmd: auth_users_inode
|
||||||
|
|
||||||
|
wait_for_so-elasticsearch:
|
||||||
|
http.wait_for_successful_query:
|
||||||
|
- name: "https://localhost:9200/"
|
||||||
|
- username: 'so_elastic'
|
||||||
|
- password: '{{ ELASTICSEARCHMERGED.auth.users.so_elastic_user.pass }}'
|
||||||
|
- ssl: True
|
||||||
|
- verify_ssl: False
|
||||||
|
- status: 200
|
||||||
|
- wait_for: 300
|
||||||
|
- request_interval: 15
|
||||||
|
- require:
|
||||||
|
- docker_container: so-elasticsearch
|
||||||
|
|
||||||
delete_so-elasticsearch_so-status.disabled:
|
delete_so-elasticsearch_so-status.disabled:
|
||||||
file.uncomment:
|
file.uncomment:
|
||||||
- name: /opt/so/conf/so-status/so-status.conf
|
- name: /opt/so/conf/so-status/so-status.conf
|
||||||
- regex: ^so-elasticsearch$
|
- regex: ^so-elasticsearch$
|
||||||
|
|
||||||
{% if GLOBALS.role != "so-searchnode" %}
|
|
||||||
escomponenttemplates:
|
|
||||||
file.recurse:
|
|
||||||
- name: /opt/so/conf/elasticsearch/templates/component
|
|
||||||
- source: salt://elasticsearch/templates/component
|
|
||||||
- user: 930
|
|
||||||
- group: 939
|
|
||||||
- clean: True
|
|
||||||
- onchanges_in:
|
|
||||||
- file: so-elasticsearch-templates-reload
|
|
||||||
- show_changes: False
|
|
||||||
|
|
||||||
# Auto-generate templates from defaults file
|
|
||||||
{% for index, settings in ES_INDEX_SETTINGS.items() %}
|
|
||||||
{% if settings.index_template is defined %}
|
|
||||||
es_index_template_{{index}}:
|
|
||||||
file.managed:
|
|
||||||
- name: /opt/so/conf/elasticsearch/templates/index/{{ index }}-template.json
|
|
||||||
- source: salt://elasticsearch/base-template.json.jinja
|
|
||||||
- defaults:
|
|
||||||
TEMPLATE_CONFIG: {{ settings.index_template }}
|
|
||||||
- template: jinja
|
|
||||||
- show_changes: False
|
|
||||||
- onchanges_in:
|
|
||||||
- file: so-elasticsearch-templates-reload
|
|
||||||
{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
|
|
||||||
{% if TEMPLATES %}
|
|
||||||
# Sync custom templates to /opt/so/conf/elasticsearch/templates
|
|
||||||
{% for TEMPLATE in TEMPLATES %}
|
|
||||||
es_template_{{TEMPLATE.split('.')[0] | replace("/","_") }}:
|
|
||||||
file.managed:
|
|
||||||
- source: salt://elasticsearch/templates/index/{{TEMPLATE}}
|
|
||||||
{% if 'jinja' in TEMPLATE.split('.')[-1] %}
|
|
||||||
- name: /opt/so/conf/elasticsearch/templates/index/{{TEMPLATE.split('/')[1] | replace(".jinja", "")}}
|
|
||||||
- template: jinja
|
|
||||||
{% else %}
|
|
||||||
- name: /opt/so/conf/elasticsearch/templates/index/{{TEMPLATE.split('/')[1]}}
|
|
||||||
{% endif %}
|
|
||||||
- user: 930
|
|
||||||
- group: 939
|
|
||||||
- show_changes: False
|
|
||||||
- onchanges_in:
|
|
||||||
- file: so-elasticsearch-templates-reload
|
|
||||||
{% endfor %}
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if GLOBALS.role in GLOBALS.manager_roles %}
|
|
||||||
so-es-cluster-settings:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elasticsearch-cluster-settings
|
|
||||||
- cwd: /opt/so
|
|
||||||
- template: jinja
|
|
||||||
- require:
|
|
||||||
- docker_container: so-elasticsearch
|
|
||||||
- file: elasticsearch_sbin_jinja
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
so-elasticsearch-ilm-policy-load:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elasticsearch-ilm-policy-load
|
|
||||||
- cwd: /opt/so
|
|
||||||
- require:
|
|
||||||
- docker_container: so-elasticsearch
|
|
||||||
- file: so-elasticsearch-ilm-policy-load-script
|
|
||||||
- onchanges:
|
|
||||||
- file: so-elasticsearch-ilm-policy-load-script
|
|
||||||
|
|
||||||
so-elasticsearch-templates-reload:
|
|
||||||
file.absent:
|
|
||||||
- name: /opt/so/state/estemplates.txt
|
|
||||||
|
|
||||||
so-elasticsearch-templates:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elasticsearch-templates-load
|
|
||||||
- cwd: /opt/so
|
|
||||||
- template: jinja
|
|
||||||
- require:
|
|
||||||
- docker_container: so-elasticsearch
|
|
||||||
- file: elasticsearch_sbin_jinja
|
|
||||||
|
|
||||||
so-elasticsearch-pipelines:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elasticsearch-pipelines {{ GLOBALS.hostname }}
|
|
||||||
- require:
|
|
||||||
- docker_container: so-elasticsearch
|
|
||||||
- file: so-elasticsearch-pipelines-script
|
|
||||||
|
|
||||||
so-elasticsearch-roles-load:
|
|
||||||
cmd.run:
|
|
||||||
- name: /usr/sbin/so-elasticsearch-roles-load
|
|
||||||
- cwd: /opt/so
|
|
||||||
- template: jinja
|
|
||||||
- require:
|
|
||||||
- docker_container: so-elasticsearch
|
|
||||||
- file: elasticsearch_sbin_jinja
|
|
||||||
|
|
||||||
{% if grains.role in ['so-managersearch', 'so-manager', 'so-managerhype'] %}
|
|
||||||
{% set ap = "absent" %}
|
|
||||||
{% endif %}
|
|
||||||
{% if grains.role in ['so-eval', 'so-standalone', 'so-heavynode'] %}
|
|
||||||
{% if ELASTICSEARCHMERGED.index_clean %}
|
|
||||||
{% set ap = "present" %}
|
|
||||||
{% else %}
|
|
||||||
{% set ap = "absent" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endif %}
|
|
||||||
{% if grains.role in ['so-eval', 'so-standalone', 'so-managersearch', 'so-heavynode', 'so-manager'] %}
|
|
||||||
so-elasticsearch-indices-delete:
|
|
||||||
cron.{{ap}}:
|
|
||||||
- name: /usr/sbin/so-elasticsearch-indices-delete > /opt/so/log/elasticsearch/cron-elasticsearch-indices-delete.log 2>&1
|
|
||||||
- identifier: so-elasticsearch-indices-delete
|
|
||||||
- user: root
|
|
||||||
- minute: '*/5'
|
|
||||||
- hour: '*'
|
|
||||||
- daymonth: '*'
|
|
||||||
- month: '*'
|
|
||||||
- dayweek: '*'
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
|
|
||||||
{{sls}}_state_not_allowed:
|
{{sls}}_state_not_allowed:
|
||||||
|
|||||||
@@ -63,7 +63,8 @@
|
|||||||
{ "set": { "if": "ctx.event?.dataset != null && !ctx.event.dataset.contains('.')", "field": "event.dataset", "value": "{{event.module}}.{{event.dataset}}" } },
|
{ "set": { "if": "ctx.event?.dataset != null && !ctx.event.dataset.contains('.')", "field": "event.dataset", "value": "{{event.module}}.{{event.dataset}}" } },
|
||||||
{ "split": { "if": "ctx.event?.dataset != null && ctx.event.dataset.contains('.')", "field": "event.dataset", "separator": "\\.", "target_field": "dataset_tag_temp" } },
|
{ "split": { "if": "ctx.event?.dataset != null && ctx.event.dataset.contains('.')", "field": "event.dataset", "separator": "\\.", "target_field": "dataset_tag_temp" } },
|
||||||
{ "append": { "if": "ctx.dataset_tag_temp != null", "field": "tags", "value": "{{dataset_tag_temp.1}}" } },
|
{ "append": { "if": "ctx.dataset_tag_temp != null", "field": "tags", "value": "{{dataset_tag_temp.1}}" } },
|
||||||
{ "grok": { "if": "ctx.http?.response?.status_code != null", "field": "http.response.status_code", "patterns": ["%{NUMBER:http.response.status_code:long} %{GREEDYDATA}"]} },
|
{ "grok": { "if": "ctx.http?.response?.status_code instanceof String", "field": "http.response.status_code", "patterns": ["%{NUMBER:http.response.status_code:long}(?:\\s+%{GREEDYDATA})?"], "ignore_failure": true } },
|
||||||
|
{ "convert": { "if": "ctx.http?.response?.status_code != null && !(ctx.http.response.status_code instanceof Number)", "field": "http.response.status_code", "type": "long", "ignore_failure": true } },
|
||||||
{ "set": { "if": "ctx?.metadata?.kafka != null" , "field": "kafka.id", "value": "{{metadata.kafka.partition}}{{metadata.kafka.offset}}{{metadata.kafka.timestamp}}", "ignore_failure": true } },
|
{ "set": { "if": "ctx?.metadata?.kafka != null" , "field": "kafka.id", "value": "{{metadata.kafka.partition}}{{metadata.kafka.offset}}{{metadata.kafka.timestamp}}", "ignore_failure": true } },
|
||||||
{ "remove": { "field": [ "message2", "type", "fields", "category", "module", "dataset", "dataset_tag_temp", "event.dataset_temp" ], "ignore_missing": true, "ignore_failure": true } },
|
{ "remove": { "field": [ "message2", "type", "fields", "category", "module", "dataset", "dataset_tag_temp", "event.dataset_temp" ], "ignore_missing": true, "ignore_failure": true } },
|
||||||
{ "pipeline": { "name": "global@custom", "ignore_missing_pipeline": true, "description": "[Fleet] Global pipeline for all data streams" } }
|
{ "pipeline": { "name": "global@custom", "ignore_missing_pipeline": true, "description": "[Fleet] Global pipeline for all data streams" } }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"version": 3,
|
"version": 4,
|
||||||
"_meta": {
|
"_meta": {
|
||||||
"managed_by": "securityonion",
|
"managed_by": "securityonion",
|
||||||
"managed": true
|
"managed": true
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
"if": "ctx.tags != null && ctx.tags.contains('import')",
|
"if": "ctx.tags != null && ctx.tags.contains('import') && ctx._index != null && ctx._index.startsWith('logs-import-')",
|
||||||
"override": true,
|
"override": true,
|
||||||
"field": "data_stream.dataset",
|
"field": "data_stream.dataset",
|
||||||
"value": "import"
|
"value": "import"
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
"if": "ctx.tags != null && ctx.tags.contains('import')",
|
"if": "ctx.tags != null && ctx.tags.contains('import') && ctx._index != null && ctx._index.startsWith('logs-import-')",
|
||||||
"override": true,
|
"override": true,
|
||||||
"field": "data_stream.namespace",
|
"field": "data_stream.namespace",
|
||||||
"value": "so"
|
"value": "so"
|
||||||
@@ -177,12 +177,110 @@
|
|||||||
"description": "Extract IPs from Elastic Agent events (host.ip) and adds them to related.ip"
|
"description": "Extract IPs from Elastic Agent events (host.ip) and adds them to related.ip"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"trim": {
|
||||||
|
"if": "ctx.event?.dataset == 'system.auth'",
|
||||||
|
"description": "Grok leaves a leading space on 'invalid user' names (elastic/integrations#12174) and, before 2.23.2, sudo padding",
|
||||||
|
"field": "user.name",
|
||||||
|
"ignore_missing": true,
|
||||||
|
"ignore_failure": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"trim": {
|
||||||
|
"if": "ctx.event?.dataset == 'system.auth'",
|
||||||
|
"description": "Appended from the untrimmed user.name",
|
||||||
|
"field": "related.user",
|
||||||
|
"ignore_missing": true,
|
||||||
|
"ignore_failure": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"if": "ctx.event?.dataset == 'system.auth' && ctx.related?.user instanceof List",
|
||||||
|
"description": "Dedupe after trimming",
|
||||||
|
"source": "ctx.related.user = new ArrayList(new LinkedHashSet(ctx.related.user));",
|
||||||
|
"ignore_failure": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Snapshot event.ingested into _tmp.event_ingested_pre_fleet before .fleet_final_pipeline-1 overwrites it with ES ingest time",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx.event?.ingested != null && ctx.event?.created == null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ctx.putIfAbsent('_tmp', [:]); ctx._tmp.event_ingested_pre_fleet = ctx.event.ingested;"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": ".fleet_final_pipeline-1",
|
"name": ".fleet_final_pipeline-1",
|
||||||
"ignore_missing_pipeline": true
|
"ignore_missing_pipeline": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Calculate time from Elastic Agent to Logstash.",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx._tmp?.logstash_from_agent != null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ZonedDateTime start = ctx._tmp.event_ingested_pre_fleet != null ? ZonedDateTime.parse(ctx._tmp.event_ingested_pre_fleet) : ZonedDateTime.parse(ctx['@timestamp']); ctx.event.putIfAbsent('ingestion', [:]); ctx.event.ingestion.latency_elasticagent_to_logstash = ChronoUnit.SECONDS.between(start, ZonedDateTime.parse(ctx._tmp.logstash_from_agent));"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Calculate time from Logstash to Redis",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx._tmp?.logstash_from_agent != null && ctx._tmp?.logstash_to_redis != null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ctx.event.putIfAbsent('ingestion', [:]); ctx.event.ingestion.latency_logstash_to_redis = ChronoUnit.SECONDS.between(ZonedDateTime.parse(ctx._tmp.logstash_from_agent), ZonedDateTime.parse(ctx._tmp.logstash_to_redis));"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Calculate time message spends in redis queue (logstash delay in pulling event).",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx._tmp?.logstash_to_redis != null && ctx._tmp?.logstash_from_redis != null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ctx.event.putIfAbsent('ingestion', [:]); ctx.event.ingestion.latency_redis_to_logstash = ChronoUnit.SECONDS.between(ZonedDateTime.parse(ctx._tmp.logstash_to_redis), ZonedDateTime.parse(ctx._tmp.logstash_from_redis));"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Calculate time from Logstash to Elasticsearch (after read from Redis).",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx._tmp?.logstash_from_redis != null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ctx.event.putIfAbsent('ingestion', [:]); ctx.event.ingestion.latency_logstash_to_elasticsearch = ChronoUnit.SECONDS.between(ZonedDateTime.parse(ctx._tmp.logstash_from_redis), metadata().now);"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Calculate time from Elastic Agent to Kafka.",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx._tmp?.logstash_from_kafka != null && ctx._tmp?.logstash_from_agent == null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ZonedDateTime start = ctx._tmp.event_ingested_pre_fleet != null ? ZonedDateTime.parse(ctx._tmp.event_ingested_pre_fleet) : ZonedDateTime.parse(ctx['@timestamp']); ctx.event.putIfAbsent('ingestion', [:]); ctx.event.ingestion.latency_elasticagent_to_kafka = ChronoUnit.SECONDS.between(start, ZonedDateTime.parse(ctx._tmp.logstash_from_kafka));"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Calculate time message spends in Kafka queue (logstash delay in pulling event).",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx._tmp?.logstash_from_kafka != null && ctx.metadata?.kafka?.timestamp != null && ctx._tmp?.logstash_from_agent == null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ctx.event.putIfAbsent('ingestion', [:]); ctx.event.ingestion.latency_kafka_queue = ChronoUnit.SECONDS.between(ZonedDateTime.ofInstant(Instant.ofEpochMilli(Long.parseLong(ctx.metadata.kafka.timestamp.toString())), ZoneId.of('UTC')), ZonedDateTime.parse(ctx._tmp.logstash_from_kafka));"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"script": {
|
||||||
|
"description": "Calculate time from Logstash to Elasticsearch (after read from Kafka).",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx._tmp?.logstash_from_kafka != null && ctx._tmp?.logstash_from_agent == null",
|
||||||
|
"ignore_failure": true,
|
||||||
|
"source": "ctx.event.putIfAbsent('ingestion', [:]); ctx.event.ingestion.latency_kafka_to_elasticsearch = ChronoUnit.SECONDS.between(ZonedDateTime.parse(ctx._tmp.logstash_from_kafka), metadata().now);"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"remove": {
|
"remove": {
|
||||||
"field": "event.agent_id_status",
|
"field": "event.agent_id_status",
|
||||||
@@ -202,7 +300,8 @@
|
|||||||
"event.dataset_temp",
|
"event.dataset_temp",
|
||||||
"dataset_tag_temp",
|
"dataset_tag_temp",
|
||||||
"module_temp",
|
"module_temp",
|
||||||
"datastream_dataset_temp"
|
"datastream_dataset_temp",
|
||||||
|
"_tmp"
|
||||||
],
|
],
|
||||||
"ignore_missing": true,
|
"ignore_missing": true,
|
||||||
"ignore_failure": true
|
"ignore_failure": true
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{
|
||||||
|
"description" : "import.evtx: normalize imported EVTX and reroute to logs-<dataset>-import",
|
||||||
|
"processors" : [
|
||||||
|
{ "script": {
|
||||||
|
"description": "Host from the event, not the importing node",
|
||||||
|
"lang": "painless",
|
||||||
|
"source": "Map host = ['os': ['type': 'windows', 'family': 'windows', 'platform': 'windows']]; def cn = ctx.winlog?.computer_name; if (cn != null && cn.toString().length() > 0) { String name = cn.toString(); int dot = name.indexOf('.'); if (dot > 0) { name = name.substring(0, dot); } host.put('hostname', name); host.put('name', name.toLowerCase()); } ctx.host = host;"
|
||||||
|
} },
|
||||||
|
{ "script": {
|
||||||
|
"description": "String event IDs, as Winlogbeat sends",
|
||||||
|
"lang": "painless",
|
||||||
|
"source": "if (ctx.winlog?.event_id != null) { ctx.winlog.event_id = ctx.winlog.event_id.toString(); } if (ctx.event?.code != null) { ctx.event.code = ctx.event.code.toString(); }"
|
||||||
|
} },
|
||||||
|
{ "script": {
|
||||||
|
"description": "Unnamed <Data> to param1..N, as Winlogbeat",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx.winlog?.event_data?.Data instanceof Map && ctx.winlog.event_data.Data['#text'] != null",
|
||||||
|
"source": "def t = ctx.winlog.event_data.Data['#text']; List vals = t instanceof List ? t : [t]; for (int i = 0; i < vals.size(); i++) { ctx.winlog.event_data['param' + (i + 1)] = vals.get(i); } ctx.winlog.event_data.remove('Data');"
|
||||||
|
} },
|
||||||
|
{ "script": {
|
||||||
|
"description": "String values and LF line endings, as Winlogbeat",
|
||||||
|
"lang": "painless",
|
||||||
|
"if": "ctx.winlog?.event_data instanceof Map || ctx.winlog?.user_data instanceof Map",
|
||||||
|
"source": "String lf = String.valueOf((char) 10); String crlf = String.valueOf((char) 13) + lf; for (def key : ['event_data', 'user_data']) { def m = ctx.winlog[key]; if (!(m instanceof Map)) { continue; } for (def e : m.entrySet()) { def v = e.getValue(); if (v instanceof String) { e.setValue(v.replace(crlf, lf)); } else if (v instanceof Number || v instanceof Boolean) { e.setValue(v.toString()); } } }"
|
||||||
|
} },
|
||||||
|
{ "set": { "description": "event.kind, as Winlogbeat", "field": "event.kind", "value": "event", "override": false } },
|
||||||
|
{ "set": { "field": "data_stream.dataset", "copy_from": "event.dataset", "override": true, "ignore_empty_value": true } },
|
||||||
|
{ "set": { "field": "data_stream.namespace", "value": "import", "override": true } },
|
||||||
|
{ "reroute": { "dataset": "{{data_stream.dataset}}", "namespace": "{{data_stream.namespace}}" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
+74
-13
@@ -10,24 +10,28 @@
|
|||||||
"processors": [
|
"processors": [
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_ecs_version_f5923549",
|
||||||
"field": "ecs.version",
|
"field": "ecs.version",
|
||||||
"value": "8.17.0"
|
"value": "8.17.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_observer_vendor_ad9d35cc",
|
||||||
"field": "observer.vendor",
|
"field": "observer.vendor",
|
||||||
"value": "netgate"
|
"value": "netgate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_observer_type_5dddf3ba",
|
||||||
"field": "observer.type",
|
"field": "observer.type",
|
||||||
"value": "firewall"
|
"value": "firewall"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rename": {
|
"rename": {
|
||||||
|
"tag": "rename_message_to_event_original_56a77271",
|
||||||
"field": "message",
|
"field": "message",
|
||||||
"target_field": "event.original",
|
"target_field": "event.original",
|
||||||
"ignore_missing": true,
|
"ignore_missing": true,
|
||||||
@@ -36,12 +40,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_event_kind_de80643c",
|
||||||
"field": "event.kind",
|
"field": "event.kind",
|
||||||
"value": "event"
|
"value": "event"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_event_timezone_4ca44cac",
|
||||||
"field": "event.timezone",
|
"field": "event.timezone",
|
||||||
"value": "{{{_tmp.tz_offset}}}",
|
"value": "{{{_tmp.tz_offset}}}",
|
||||||
"if": "ctx._tmp?.tz_offset != null && ctx._tmp?.tz_offset != 'local'"
|
"if": "ctx._tmp?.tz_offset != null && ctx._tmp?.tz_offset != 'local'"
|
||||||
@@ -49,6 +55,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"grok": {
|
"grok": {
|
||||||
|
"tag": "grok_event_original_27d9c8c7",
|
||||||
"description": "Parse syslog header",
|
"description": "Parse syslog header",
|
||||||
"field": "event.original",
|
"field": "event.original",
|
||||||
"patterns": [
|
"patterns": [
|
||||||
@@ -72,6 +79,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": {
|
"date": {
|
||||||
|
"tag": "date__tmp_timestamp8601_to_timestamp_6ac9d3ce",
|
||||||
"if": "ctx._tmp.timestamp8601 != null",
|
"if": "ctx._tmp.timestamp8601 != null",
|
||||||
"field": "_tmp.timestamp8601",
|
"field": "_tmp.timestamp8601",
|
||||||
"target_field": "@timestamp",
|
"target_field": "@timestamp",
|
||||||
@@ -82,6 +90,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"date": {
|
"date": {
|
||||||
|
"tag": "date__tmp_timestamp_to_timestamp_f21e536e",
|
||||||
"if": "ctx.event?.timezone != null && ctx._tmp?.timestamp != null",
|
"if": "ctx.event?.timezone != null && ctx._tmp?.timestamp != null",
|
||||||
"field": "_tmp.timestamp",
|
"field": "_tmp.timestamp",
|
||||||
"target_field": "@timestamp",
|
"target_field": "@timestamp",
|
||||||
@@ -95,6 +104,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"grok": {
|
"grok": {
|
||||||
|
"tag": "grok_process_name_cef3d489",
|
||||||
"description": "Set Event Provider",
|
"description": "Set Event Provider",
|
||||||
"field": "process.name",
|
"field": "process.name",
|
||||||
"patterns": [
|
"patterns": [
|
||||||
@@ -107,71 +117,83 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-firewall",
|
"tag": "pipeline_e16851a7",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-firewall",
|
||||||
"if": "ctx.event.provider == 'filterlog'"
|
"if": "ctx.event.provider == 'filterlog'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-openvpn",
|
"tag": "pipeline_828590b5",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-openvpn",
|
||||||
"if": "ctx.event.provider == 'openvpn'"
|
"if": "ctx.event.provider == 'openvpn'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-ipsec",
|
"tag": "pipeline_9d37039c",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-ipsec",
|
||||||
"if": "ctx.event.provider == 'charon'"
|
"if": "ctx.event.provider == 'charon'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-dhcp",
|
"tag": "pipeline_ad56bbca",
|
||||||
"if": "[\"dhcpd\", \"dhclient\", \"dhcp6c\"].contains(ctx.event.provider)"
|
"name": "logs-pfsense.log-1.25.4-dhcp",
|
||||||
|
"if": "[\"dhcpd\", \"dhclient\", \"dhcp6c\", \"dnsmasq-dhcp\"].contains(ctx.event.provider)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-unbound",
|
"tag": "pipeline_dd85553d",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-unbound",
|
||||||
"if": "ctx.event.provider == 'unbound'"
|
"if": "ctx.event.provider == 'unbound'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-haproxy",
|
"tag": "pipeline_720ed255",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-haproxy",
|
||||||
"if": "ctx.event.provider == 'haproxy'"
|
"if": "ctx.event.provider == 'haproxy'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-php-fpm",
|
"tag": "pipeline_456beba5",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-php-fpm",
|
||||||
"if": "ctx.event.provider == 'php-fpm'"
|
"if": "ctx.event.provider == 'php-fpm'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-squid",
|
"tag": "pipeline_a0d89375",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-squid",
|
||||||
"if": "ctx.event.provider == 'squid'"
|
"if": "ctx.event.provider == 'squid'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-snort",
|
"tag": "pipeline_c2f1ed55",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-snort",
|
||||||
"if": "ctx.event.provider == 'snort'"
|
"if": "ctx.event.provider == 'snort'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "logs-pfsense.log-1.23.1-suricata",
|
"tag":"pipeline_33db1c9e",
|
||||||
|
"name": "logs-pfsense.log-1.25.4-suricata",
|
||||||
"if": "ctx.event.provider == 'suricata'"
|
"if": "ctx.event.provider == 'suricata'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"drop": {
|
"drop": {
|
||||||
"if": "![\"filterlog\", \"openvpn\", \"charon\", \"dhcpd\", \"dhclient\", \"dhcp6c\", \"unbound\", \"haproxy\", \"php-fpm\", \"squid\", \"snort\", \"suricata\"].contains(ctx.event?.provider)"
|
"tag": "drop_9d7c46f8",
|
||||||
|
"if": "![\"filterlog\", \"openvpn\", \"charon\", \"dhcpd\", \"dnsmasq-dhcp\", \"dhclient\", \"dhcp6c\", \"unbound\", \"haproxy\", \"php-fpm\", \"squid\", \"snort\", \"suricata\"].contains(ctx.event?.provider)"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"append": {
|
"append": {
|
||||||
|
"tag": "append_event_category_4780a983",
|
||||||
"field": "event.category",
|
"field": "event.category",
|
||||||
"value": "network",
|
"value": "network",
|
||||||
"if": "ctx.network != null"
|
"if": "ctx.network != null"
|
||||||
@@ -179,6 +201,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"convert": {
|
"convert": {
|
||||||
|
"tag": "convert_source_address_to_source_ip_f5632a20",
|
||||||
"field": "source.address",
|
"field": "source.address",
|
||||||
"target_field": "source.ip",
|
"target_field": "source.ip",
|
||||||
"type": "ip",
|
"type": "ip",
|
||||||
@@ -188,6 +211,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"convert": {
|
"convert": {
|
||||||
|
"tag": "convert_destination_address_to_destination_ip_f1388f0c",
|
||||||
"field": "destination.address",
|
"field": "destination.address",
|
||||||
"target_field": "destination.ip",
|
"target_field": "destination.ip",
|
||||||
"type": "ip",
|
"type": "ip",
|
||||||
@@ -197,6 +221,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_network_type_1f1d940a",
|
||||||
"field": "network.type",
|
"field": "network.type",
|
||||||
"value": "ipv6",
|
"value": "ipv6",
|
||||||
"if": "ctx.source?.ip != null && ctx.source.ip.contains(\":\")"
|
"if": "ctx.source?.ip != null && ctx.source.ip.contains(\":\")"
|
||||||
@@ -204,6 +229,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_network_type_69deca38",
|
||||||
"field": "network.type",
|
"field": "network.type",
|
||||||
"value": "ipv4",
|
"value": "ipv4",
|
||||||
"if": "ctx.source?.ip != null && ctx.source.ip.contains(\".\")"
|
"if": "ctx.source?.ip != null && ctx.source.ip.contains(\".\")"
|
||||||
@@ -211,6 +237,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"geoip": {
|
"geoip": {
|
||||||
|
"tag": "geoip_source_ip_to_source_geo_da2e41b2",
|
||||||
"field": "source.ip",
|
"field": "source.ip",
|
||||||
"target_field": "source.geo",
|
"target_field": "source.geo",
|
||||||
"ignore_missing": true
|
"ignore_missing": true
|
||||||
@@ -218,6 +245,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"geoip": {
|
"geoip": {
|
||||||
|
"tag": "geoip_destination_ip_to_destination_geo_ab5e2968",
|
||||||
"field": "destination.ip",
|
"field": "destination.ip",
|
||||||
"target_field": "destination.geo",
|
"target_field": "destination.geo",
|
||||||
"ignore_missing": true
|
"ignore_missing": true
|
||||||
@@ -225,6 +253,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"geoip": {
|
"geoip": {
|
||||||
|
"tag": "geoip_source_ip_to_source_as_28d69883",
|
||||||
"ignore_missing": true,
|
"ignore_missing": true,
|
||||||
"database_file": "GeoLite2-ASN.mmdb",
|
"database_file": "GeoLite2-ASN.mmdb",
|
||||||
"field": "source.ip",
|
"field": "source.ip",
|
||||||
@@ -237,6 +266,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"geoip": {
|
"geoip": {
|
||||||
|
"tag": "geoip_destination_ip_to_destination_as_8a007787",
|
||||||
"database_file": "GeoLite2-ASN.mmdb",
|
"database_file": "GeoLite2-ASN.mmdb",
|
||||||
"field": "destination.ip",
|
"field": "destination.ip",
|
||||||
"target_field": "destination.as",
|
"target_field": "destination.as",
|
||||||
@@ -249,6 +279,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rename": {
|
"rename": {
|
||||||
|
"tag": "rename_source_as_asn_to_source_as_number_a917047d",
|
||||||
"field": "source.as.asn",
|
"field": "source.as.asn",
|
||||||
"target_field": "source.as.number",
|
"target_field": "source.as.number",
|
||||||
"ignore_missing": true
|
"ignore_missing": true
|
||||||
@@ -256,6 +287,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rename": {
|
"rename": {
|
||||||
|
"tag": "rename_source_as_organization_name_to_source_as_organization_name_f1362d0b",
|
||||||
"field": "source.as.organization_name",
|
"field": "source.as.organization_name",
|
||||||
"target_field": "source.as.organization.name",
|
"target_field": "source.as.organization.name",
|
||||||
"ignore_missing": true
|
"ignore_missing": true
|
||||||
@@ -263,6 +295,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rename": {
|
"rename": {
|
||||||
|
"tag": "rename_destination_as_asn_to_destination_as_number_3b459fcd",
|
||||||
"field": "destination.as.asn",
|
"field": "destination.as.asn",
|
||||||
"target_field": "destination.as.number",
|
"target_field": "destination.as.number",
|
||||||
"ignore_missing": true
|
"ignore_missing": true
|
||||||
@@ -270,6 +303,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rename": {
|
"rename": {
|
||||||
|
"tag": "rename_destination_as_organization_name_to_destination_as_organization_name_814bd459",
|
||||||
"field": "destination.as.organization_name",
|
"field": "destination.as.organization_name",
|
||||||
"target_field": "destination.as.organization.name",
|
"target_field": "destination.as.organization.name",
|
||||||
"ignore_missing": true
|
"ignore_missing": true
|
||||||
@@ -277,12 +311,14 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"community_id": {
|
"community_id": {
|
||||||
|
"tag": "community_id_d2308e7a",
|
||||||
"target_field": "network.community_id",
|
"target_field": "network.community_id",
|
||||||
"ignore_failure": true
|
"ignore_failure": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"grok": {
|
"grok": {
|
||||||
|
"tag": "grok_observer_ingress_interface_name_968018d3",
|
||||||
"field": "observer.ingress.interface.name",
|
"field": "observer.ingress.interface.name",
|
||||||
"patterns": [
|
"patterns": [
|
||||||
"%{DATA}.%{NONNEGINT:observer.ingress.vlan.id}"
|
"%{DATA}.%{NONNEGINT:observer.ingress.vlan.id}"
|
||||||
@@ -293,6 +329,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_network_vlan_id_efd4d96a",
|
||||||
"field": "network.vlan.id",
|
"field": "network.vlan.id",
|
||||||
"copy_from": "observer.ingress.vlan.id",
|
"copy_from": "observer.ingress.vlan.id",
|
||||||
"ignore_empty_value": true
|
"ignore_empty_value": true
|
||||||
@@ -300,6 +337,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"append": {
|
"append": {
|
||||||
|
"tag": "append_related_ip_c1a6356b",
|
||||||
"field": "related.ip",
|
"field": "related.ip",
|
||||||
"value": "{{{destination.ip}}}",
|
"value": "{{{destination.ip}}}",
|
||||||
"allow_duplicates": false,
|
"allow_duplicates": false,
|
||||||
@@ -308,6 +346,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"append": {
|
"append": {
|
||||||
|
"tag": "append_related_ip_8121c591",
|
||||||
"field": "related.ip",
|
"field": "related.ip",
|
||||||
"value": "{{{source.ip}}}",
|
"value": "{{{source.ip}}}",
|
||||||
"allow_duplicates": false,
|
"allow_duplicates": false,
|
||||||
@@ -316,6 +355,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"append": {
|
"append": {
|
||||||
|
"tag": "append_related_ip_53b62ed8",
|
||||||
"field": "related.ip",
|
"field": "related.ip",
|
||||||
"value": "{{{source.nat.ip}}}",
|
"value": "{{{source.nat.ip}}}",
|
||||||
"allow_duplicates": false,
|
"allow_duplicates": false,
|
||||||
@@ -324,6 +364,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"append": {
|
"append": {
|
||||||
|
"tag": "append_related_hosts_6f162628",
|
||||||
"field": "related.hosts",
|
"field": "related.hosts",
|
||||||
"value": "{{{destination.domain}}}",
|
"value": "{{{destination.domain}}}",
|
||||||
"if": "ctx.destination?.domain != null"
|
"if": "ctx.destination?.domain != null"
|
||||||
@@ -331,6 +372,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"append": {
|
"append": {
|
||||||
|
"tag": "append_related_user_c036eec2",
|
||||||
"field": "related.user",
|
"field": "related.user",
|
||||||
"value": "{{{user.name}}}",
|
"value": "{{{user.name}}}",
|
||||||
"if": "ctx.user?.name != null"
|
"if": "ctx.user?.name != null"
|
||||||
@@ -338,6 +380,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"set": {
|
"set": {
|
||||||
|
"tag": "set_network_direction_cb1e3125",
|
||||||
"field": "network.direction",
|
"field": "network.direction",
|
||||||
"value": "{{{network.direction}}}bound",
|
"value": "{{{network.direction}}}bound",
|
||||||
"if": "ctx.network?.direction != null && ctx.network?.direction =~ /^(in|out)$/"
|
"if": "ctx.network?.direction != null && ctx.network?.direction =~ /^(in|out)$/"
|
||||||
@@ -345,6 +388,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"remove": {
|
"remove": {
|
||||||
|
"tag": "remove_a82e20f2",
|
||||||
"field": [
|
"field": [
|
||||||
"_tmp"
|
"_tmp"
|
||||||
],
|
],
|
||||||
@@ -353,11 +397,21 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"script": {
|
"script": {
|
||||||
|
"tag": "script_a7f2c062",
|
||||||
"lang": "painless",
|
"lang": "painless",
|
||||||
"description": "This script processor iterates over the whole document to remove fields with null values.",
|
"description": "This script processor iterates over the whole document to remove fields with null values.",
|
||||||
"source": "void handleMap(Map map) {\n for (def x : map.values()) {\n if (x instanceof Map) {\n handleMap(x);\n } else if (x instanceof List) {\n handleList(x);\n }\n }\n map.values().removeIf(v -> v == null || (v instanceof String && v == \"-\"));\n}\nvoid handleList(List list) {\n for (def x : list) {\n if (x instanceof Map) {\n handleMap(x);\n } else if (x instanceof List) {\n handleList(x);\n }\n }\n}\nhandleMap(ctx);\n"
|
"source": "void handleMap(Map map) {\n for (def x : map.values()) {\n if (x instanceof Map) {\n handleMap(x);\n } else if (x instanceof List) {\n handleList(x);\n }\n }\n map.values().removeIf(v -> v == null || (v instanceof String && v == \"-\"));\n}\nvoid handleList(List list) {\n for (def x : list) {\n if (x instanceof Map) {\n handleMap(x);\n } else if (x instanceof List) {\n handleList(x);\n }\n }\n}\nhandleMap(ctx);\n"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"append": {
|
||||||
|
"tag": "append_preserve_original_event_on_error",
|
||||||
|
"field": "tags",
|
||||||
|
"value": "preserve_original_event",
|
||||||
|
"allow_duplicates": false,
|
||||||
|
"if": "ctx.error?.message != null"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"pipeline": {
|
"pipeline": {
|
||||||
"name": "global@custom",
|
"name": "global@custom",
|
||||||
@@ -405,7 +459,14 @@
|
|||||||
{
|
{
|
||||||
"append": {
|
"append": {
|
||||||
"field": "error.message",
|
"field": "error.message",
|
||||||
"value": "{{{ _ingest.on_failure_message }}}"
|
"value": "Processor '{{{ _ingest.on_failure_processor_type }}}' {{#_ingest.on_failure_processor_tag}}with tag '{{{ _ingest.on_failure_processor_tag }}}' {{/_ingest.on_failure_processor_tag}}in pipeline '{{{ _ingest.pipeline }}}' failed with message '{{{ _ingest.on_failure_message }}}'"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"append": {
|
||||||
|
"field": "tags",
|
||||||
|
"value": "preserve_original_event",
|
||||||
|
"allow_duplicates": false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File renamed without changes.
@@ -5,7 +5,8 @@
|
|||||||
{ "rename": { "field": "message2.proto", "target_field": "network.transport", "ignore_missing": true } },
|
{ "rename": { "field": "message2.proto", "target_field": "network.transport", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.app_proto", "target_field": "network.protocol", "ignore_missing": true } },
|
{ "rename": { "field": "message2.app_proto", "target_field": "network.protocol", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.fileinfo.filename", "target_field": "file.name", "ignore_missing": true } },
|
{ "rename": { "field": "message2.fileinfo.filename", "target_field": "file.name", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.fileinfo.gaps", "target_field": "file.bytes.missing", "ignore_missing": true } },
|
{ "rename": { "field": "message2.fileinfo.gaps", "target_field": "suricata.fileinfo.gaps", "ignore_missing": true } },
|
||||||
|
{ "set": { "if": "ctx.suricata?.fileinfo?.gaps == false", "field": "file.bytes.missing", "value": 0 } },
|
||||||
{ "rename": { "field": "message2.fileinfo.magic", "target_field": "file.mime_type", "ignore_missing": true } },
|
{ "rename": { "field": "message2.fileinfo.magic", "target_field": "file.mime_type", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.fileinfo.md5", "target_field": "hash.md5", "ignore_missing": true } },
|
{ "rename": { "field": "message2.fileinfo.md5", "target_field": "hash.md5", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.fileinfo.sha1", "target_field": "hash.sha1", "ignore_missing": true } },
|
{ "rename": { "field": "message2.fileinfo.sha1", "target_field": "hash.sha1", "ignore_missing": true } },
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
{
|
||||||
|
"description": "zeek.ja4d",
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"set": {
|
||||||
|
"field": "event.dataset",
|
||||||
|
"value": "ja4d"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"remove": {
|
||||||
|
"field": [
|
||||||
|
"host"
|
||||||
|
],
|
||||||
|
"ignore_failure": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"field": "message",
|
||||||
|
"target_field": "message2",
|
||||||
|
"ignore_failure": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rename": {
|
||||||
|
"field": "message2.ja4d",
|
||||||
|
"target_field": "hash.ja4d",
|
||||||
|
"ignore_missing": true,
|
||||||
|
"if": "ctx?.message2?.ja4d != null && ctx.message2.ja4d.length() > 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rename": {
|
||||||
|
"field": "message2.client_mac",
|
||||||
|
"target_field": "host.mac",
|
||||||
|
"ignore_missing": true,
|
||||||
|
"if": "ctx?.message2?.client_mac != null && ctx.message2.client_mac.length() > 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rename": {
|
||||||
|
"field": "message2.hostname",
|
||||||
|
"target_field": "host.hostname",
|
||||||
|
"ignore_missing": true,
|
||||||
|
"if": "ctx?.message2?.hostname != null && ctx.message2.hostname.length() > 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rename": {
|
||||||
|
"field": "message2.requested_ip",
|
||||||
|
"target_field": "dhcp.requested_address",
|
||||||
|
"ignore_missing": true,
|
||||||
|
"if": "ctx?.message2?.requested_ip != null && ctx.message2.requested_ip.length() > 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"rename": {
|
||||||
|
"field": "message2.vendor_class_id",
|
||||||
|
"target_field": "zeek.ja4d.vendor_class_id",
|
||||||
|
"ignore_missing": true,
|
||||||
|
"if": "ctx?.message2?.vendor_class_id != null && ctx.message2.vendor_class_id.length() > 0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"pipeline": {
|
||||||
|
"name": "zeek.common"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@
|
|||||||
{ "remove": { "field": ["host"], "ignore_failure": true } },
|
{ "remove": { "field": ["host"], "ignore_failure": true } },
|
||||||
{ "json": { "field": "message", "target_field": "message2", "ignore_failure": true } },
|
{ "json": { "field": "message", "target_field": "message2", "ignore_failure": true } },
|
||||||
{ "rename": { "field": "message2.version", "target_field": "ssl.version", "ignore_missing": true } },
|
{ "rename": { "field": "message2.version", "target_field": "ssl.version", "ignore_missing": true } },
|
||||||
|
{ "set": { "description": "Set transport for the community_id processor", "if": "ctx.ssl?.version == null || !ctx.ssl.version.startsWith('DTLS')", "field": "network.transport", "value": "tcp", "ignore_failure": true } },
|
||||||
{ "rename": { "field": "message2.cipher", "target_field": "ssl.cipher", "ignore_missing": true } },
|
{ "rename": { "field": "message2.cipher", "target_field": "ssl.cipher", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.curve", "target_field": "ssl.curve", "ignore_missing": true } },
|
{ "rename": { "field": "message2.curve", "target_field": "ssl.curve", "ignore_missing": true } },
|
||||||
{ "rename": { "field": "message2.server_name", "target_field": "ssl.server_name", "ignore_missing": true } },
|
{ "rename": { "field": "message2.server_name", "target_field": "ssl.server_name", "ignore_missing": true } },
|
||||||
|
|||||||
Loaded 100 of 371 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user