From bdda640da17bc86081e06a9a84506e804c45b1eb Mon Sep 17 00:00:00 2001 From: Mark Stemm Date: Wed, 27 Mar 2019 15:50:39 -0700 Subject: [PATCH] Flag excess drops (#561) * Make stats file interval configurable New argument --stats_interval= controls the interval at which statistics are written to the stats file. The default is 5000 ms (5 sec) which matches the prior hardcoded interval. The stats interval is triggered via signals, so an interval below ~250ms will probably interfere with falco's behavior. * Add ability to emit general purpose messages A new method falco_outputs::handle_msg allows emitting generic messages that have a "rule", message, and output fields, but aren't exactly tied to any event and aren't passed through an event formatter. This allows falco to emit "events" based on internal checks like kernel buffer overflow detection. * Clean up newline handling for logging Log messages from falco_logger::log may or may not have trailing newlines. Handle both by always adding a newline to stderr logs and always removing any newline from syslog logs. * Add method to get sequence from subkey New variant of get_sequence that allows fetching a list of items from a key + subkey, for example: key: subkey: - list - items - here Both use a shared method get_sequence_from_node(). * Monitor syscall event drops + optional actions Start actively monitoring the kernel buffer for syscall event drops, which are visible in scap_stats.n_drops, and add the ability to take actions when events are dropped. The -v (verbose) and -s (stats filename) arguments also print out information on dropped events, but they were only printed/logged without any actions. In falco config you can specify one or more of the following actions to take when falco notes system call drops: - ignore (do nothing) - log a critical message - emit an "internal" falco alert. It looks like any other alert with a time, "rule", message, and output fields but is not related to any rule in falco_rules.yaml/other rules files. - exit falco (the idea being that the restart would be monitored elsewhere). A new module syscall_event_drop_mgr is called for every event and collects scap stats every second. If in the prior second there were drops, perform_actions() handles the actions. To prevent potential flooding in high drop rate environments, actions are goverened by a token bucket with a rate of 1 actions per 30 seconds, with a max burst of 10 seconds. We might tune this later based on experience in busy environments. This might be considered a fix for https://github.com/falcosecurity/falco/issues/545. It doesn't specifically flag falco rules alerts when there are drops, but does make it easier to notice when there are drops. * Add unit test for syscall event drop detection Add unit tests for syscall event drop detection. First, add an optional config option that artifically increments the drop count every second. (This is only used for testing). Then add test cases for each of the following: - No dropped events: should not see any log messages or alerts. - ignore action: should note the drops but not log messages or alert. - log action: should only see log messages for the dropped events. - alert action: should only see alerts for the dropped events. - exit action: should see log message noting the dropped event and exit with rc=1 A new trace file ping_sendto.scap has 10 seconds worth of events to allow the periodic tracking of drops to kick in. --- falco.yaml | 20 ++++ test/confs/drops_alert.yaml | 11 ++ test/confs/drops_exit.yaml | 11 ++ test/confs/drops_ignore.yaml | 11 ++ test/confs/drops_log.yaml | 11 ++ test/confs/drops_none.yaml | 11 ++ test/falco_test.py | 46 ++++++-- test/falco_tests.yaml | 71 ++++++++++++- test/trace_files/ping_sendto.scap | Bin 0 -> 28075 bytes userspace/falco/CMakeLists.txt | 1 + userspace/falco/configuration.cpp | 37 +++++++ userspace/falco/configuration.h | 48 +++++++-- userspace/falco/event_drops.cpp | 157 ++++++++++++++++++++++++++++ userspace/falco/event_drops.h | 78 ++++++++++++++ userspace/falco/falco.cpp | 39 ++++++- userspace/falco/falco_outputs.cpp | 82 ++++++++++++++- userspace/falco/falco_outputs.h | 11 ++ userspace/falco/logger.cpp | 27 ++++- userspace/falco/lua/output.lua | 6 ++ userspace/falco/statsfilewriter.cpp | 6 +- userspace/falco/statsfilewriter.h | 2 +- 21 files changed, 658 insertions(+), 28 deletions(-) create mode 100644 test/confs/drops_alert.yaml create mode 100644 test/confs/drops_exit.yaml create mode 100644 test/confs/drops_ignore.yaml create mode 100644 test/confs/drops_log.yaml create mode 100644 test/confs/drops_none.yaml create mode 100644 test/trace_files/ping_sendto.scap create mode 100644 userspace/falco/event_drops.cpp create mode 100644 userspace/falco/event_drops.h diff --git a/falco.yaml b/falco.yaml index b455bebd..9d90e463 100644 --- a/falco.yaml +++ b/falco.yaml @@ -63,6 +63,26 @@ priority: debug # buffered. Defaults to false buffered_outputs: false +# Falco uses a shared buffer between the kernel and userspace to pass +# system call information. When falco detects that this buffer is +# full and system calls have been dropped, it can take one or more of +# the following actions: +# - "ignore": do nothing. If an empty list is provided, ignore is assumed. +# - "log": log a CRITICAL message noting that the buffer was full. +# - "alert": emit a falco alert noting that the buffer was full. +# - "exit": exit falco with a non-zero rc. +# +# The rate at which log/alert messages are emitted is governed by a +# token bucket. The rate corresponds to one message every 30 seconds +# with a burst of 10 messages. + +syscall_event_drops: + actions: + - log + - alert + rate: .03333 + max_burst: 10 + # A throttling mechanism implemented as a token bucket limits the # rate of falco notifications. This throttling is controlled by the following configuration # options: diff --git a/test/confs/drops_alert.yaml b/test/confs/drops_alert.yaml new file mode 100644 index 00000000..9c71aa58 --- /dev/null +++ b/test/confs/drops_alert.yaml @@ -0,0 +1,11 @@ +syscall_event_drops: + actions: + - alert + rate: .03333 + max_burst: 10 + simulate_drops: true + +stdout_output: + enabled: true + +log_stderr: true diff --git a/test/confs/drops_exit.yaml b/test/confs/drops_exit.yaml new file mode 100644 index 00000000..0cf52c75 --- /dev/null +++ b/test/confs/drops_exit.yaml @@ -0,0 +1,11 @@ +syscall_event_drops: + actions: + - exit + rate: .03333 + max_burst: 10 + simulate_drops: true + +stdout_output: + enabled: true + +log_stderr: true diff --git a/test/confs/drops_ignore.yaml b/test/confs/drops_ignore.yaml new file mode 100644 index 00000000..1f7f5358 --- /dev/null +++ b/test/confs/drops_ignore.yaml @@ -0,0 +1,11 @@ +syscall_event_drops: + actions: + - ignore + rate: .03333 + max_burst: 10 + simulate_drops: true + +stdout_output: + enabled: true + +log_stderr: true diff --git a/test/confs/drops_log.yaml b/test/confs/drops_log.yaml new file mode 100644 index 00000000..0e5df214 --- /dev/null +++ b/test/confs/drops_log.yaml @@ -0,0 +1,11 @@ +syscall_event_drops: + actions: + - log + rate: .03333 + max_burst: 10 + simulate_drops: true + +stdout_output: + enabled: true + +log_stderr: true diff --git a/test/confs/drops_none.yaml b/test/confs/drops_none.yaml new file mode 100644 index 00000000..e2d28c83 --- /dev/null +++ b/test/confs/drops_none.yaml @@ -0,0 +1,11 @@ +syscall_event_drops: + actions: + - log + rate: .03333 + max_burst: 10 + simulate_drops: false + +stdout_output: + enabled: true + +log_stderr: true diff --git a/test/falco_test.py b/test/falco_test.py index ef21eb2c..a202e4d0 100644 --- a/test/falco_test.py +++ b/test/falco_test.py @@ -37,7 +37,31 @@ class FalcoTest(Test): self.falcodir = self.params.get('falcodir', '/', default=os.path.join(self.basedir, '../build')) self.stdout_contains = self.params.get('stdout_contains', '*', default='') + + if not isinstance(self.stdout_contains, list): + self.stdout_contains = [self.stdout_contains] + self.stderr_contains = self.params.get('stderr_contains', '*', default='') + + if not isinstance(self.stderr_contains, list): + self.stderr_contains = [self.stderr_contains] + + self.stdout_not_contains = self.params.get('stdout_not_contains', '*', default='') + + if not isinstance(self.stdout_not_contains, list): + if self.stdout_not_contains == '': + self.stdout_not_contains = [] + else: + self.stdout_not_contains = [self.stdout_not_contains] + + self.stderr_not_contains = self.params.get('stderr_not_contains', '*', default='') + + if not isinstance(self.stderr_not_contains, list): + if self.stderr_not_contains == '': + self.stderr_not_contains = [] + else: + self.stderr_not_contains = [self.stderr_not_contains] + self.exit_status = self.params.get('exit_status', '*', default=0) self.should_detect = self.params.get('detect', '*', default=False) self.trace_file = self.params.get('trace_file', '*', default='') @@ -389,15 +413,25 @@ class FalcoTest(Test): res = self.falco_proc.run(timeout=180, sig=9) - if self.stderr_contains != '': - match = re.search(self.stderr_contains, res.stderr) + for pattern in self.stderr_contains: + match = re.search(pattern, res.stderr) if match is None: - self.fail("Stderr of falco process did not contain content matching {}".format(self.stderr_contains)) + self.fail("Stderr of falco process did not contain content matching {}".format(pattern)) - if self.stdout_contains != '': - match = re.search(self.stdout_contains, res.stdout) + for pattern in self.stdout_contains: + match = re.search(pattern, res.stdout) if match is None: - self.fail("Stdout of falco process '{}' did not contain content matching {}".format(res.stdout, self.stdout_contains)) + self.fail("Stdout of falco process '{}' did not contain content matching {}".format(res.stdout, pattern)) + + for pattern in self.stderr_not_contains: + match = re.search(pattern, res.stderr) + if match is not None: + self.fail("Stderr of falco process contained content matching {} when it should have not".format(pattern)) + + for pattern in self.stdout_not_contains: + match = re.search(pattern, res.stdout) + if match is not None: + self.fail("Stdout of falco process '{}' did contain content matching {} when it should have not".format(res.stdout, pattern)) if res.exit_status != self.exit_status: self.error("Falco command \"{}\" exited with unexpected return value {} (!= {})".format( diff --git a/test/falco_tests.yaml b/test/falco_tests.yaml index ba0f00f0..7d057d0c 100644 --- a/test/falco_tests.yaml +++ b/test/falco_tests.yaml @@ -770,4 +770,73 @@ trace_files: !mux stderr_contains: Rules require engine version 9999999, but engine version is rules_file: - rules/engine_version_mismatch.yaml - trace_file: trace_files/cat_write.scap \ No newline at end of file + trace_file: trace_files/cat_write.scap + + monitor_syscall_drops_none: + exit_status: 0 + rules_file: + - rules/single_rule.yaml + conf_file: confs/drops_none.yaml + trace_file: trace_files/ping_sendto.scap + stderr_not_contains: + - "event drop detected: 9 occurrences" + - "num times actions taken: 9" + - "Falco internal: syscall event drop" + stdout_not_contains: + - "Falco internal: syscall event drop" + + monitor_syscall_drops_ignore: + exit_status: 0 + rules_file: + - rules/single_rule.yaml + conf_file: confs/drops_ignore.yaml + trace_file: trace_files/ping_sendto.scap + stderr_contains: + - "event drop detected: 9 occurrences" + - "num times actions taken: 9" + stderr_not_contains: + - "Falco internal: syscall event drop" + stdout_not_contains: + - "Falco internal: syscall event drop" + + monitor_syscall_drops_log: + exit_status: 0 + rules_file: + - rules/single_rule.yaml + conf_file: confs/drops_log.yaml + trace_file: trace_files/ping_sendto.scap + stderr_contains: + - "event drop detected: 9 occurrences" + - "num times actions taken: 9" + - "Falco internal: syscall event drop" + stdout_not_contains: + - "Falco internal: syscall event drop" + + monitor_syscall_drops_alert: + exit_status: 0 + rules_file: + - rules/single_rule.yaml + conf_file: confs/drops_alert.yaml + trace_file: trace_files/ping_sendto.scap + stderr_contains: + - "event drop detected: 9 occurrences" + - "num times actions taken: 9" + stderr_not_contains: + - "Falco internal: syscall event drop" + stdout_contains: + - "Falco internal: syscall event drop" + + monitor_syscall_drops_exit: + exit_status: 1 + rules_file: + - rules/single_rule.yaml + conf_file: confs/drops_exit.yaml + trace_file: trace_files/ping_sendto.scap + stderr_contains: + - "event drop detected: 1 occurrences" + - "num times actions taken: 1" + - "Falco internal: syscall event drop" + - "Exiting." + stdout_not_contains: + - "Falco internal: syscall event drop" + diff --git a/test/trace_files/ping_sendto.scap b/test/trace_files/ping_sendto.scap new file mode 100644 index 0000000000000000000000000000000000000000..939de43639f944e1d559eb785a028bd75bee5d1b GIT binary patch literal 28075 zcmb??WmsIx(rywGNP-1-2=4A4+}+*X-66qYaJPZr0S0$>8EkNOcXz$)v(GvE`|kaF z=SMx$@2alq>a|vPKX26{0DOM;uOHM=E37+-miDf zX>FVhHE4P^GCggu0^3|~T4 zInSZkFhYdBgCT?--_KAo@5+l`ikq7X3i9t=XX2zG)eB{1_bUfYEh^V4*NeP3_15zZ zfNxCR$I}?Bv8ES9-s;*)%!GOc{nJ;`xPBd$L??wz8kFj2Ro47^1%Bnf@BIQzABeC` zO`4POb@L2dQC%B1O77~dz@if^k?0wCdZ??WU-LufT|d>&iDq&{Z|u9tW!&Z%F=`-) zC@mlF*F{AslPtJ?Vb>AMoO@rutv%_?$eKt;$i-Ezx5*G3rOq;al9CBk zu+0qrOk@)ud*i&o$crHjxlVB{j7zOkceHZ@b<4Prr<`1+kTok-iZd}`Nfa_i z?ceKrw#~Aj?H`HP%ZW@>BJ^Kf20OCx0u5{?1Q$(T(FJb?BJ0u;03ufK zU#h#Iku!(UyW%CB3L?KXgXdN?<77=QdOtk3876g;u`aAbcLy1kaR6zxN4}4o)rXJS zsa{@Lrj|Lr3@eeb%9!o26qMIFNeuY44n*w~2@cAlGYt{Bo6e54TirWwr9x70j`Q5r z%Vw~36IAp(3FbCO+7U`LKb=@H|uD{UxCv)?Xfv(k>F9Vz*Sj2NSd6WG!T1~c8O zmY&(xwgHib0`R{9@7X$5EfohcSn9#a21vE~IO}$-Maqh#h!G;@`h?JnAN;Jui?lQN zRjTLR@9BfuueY|q@!8H3l4bSfLUODrvcZjTFEepqW6L_l1#99X6deoqQXK@p5(zph zO?fLCb!scMj#a8S+QPcIn{7!#X^68-cgYd!nV5x=xaN+kLn4@gzg&f=PxPif2{k;v z_li<8N3b#`wXksr)fww(r#0?HX1x74;zE@y3qeC;P)^by+>A45FOsdkFpiVi2@lG2 zZC&q#yc@!g;<5sqX?NNE@pV!zZdjY0O!TjJHsGN;{il?#2y;8$CQUuy>O+h$`M z!5iL8z?Bo-Plj984!YG`X`3mE<240Um%9T2pL@U*Gj8s*BbQNX6_PmLl~^cnEnp6=R3kUviS{e!wz@|Uw$ShC~wD#kz=6^G~sYan9nIGYjuY>(a zlAOWPZCb#SOc^pi2S4r_kujzF!REn}4=q2gW)^i~dy7*MCzJ!LKJsUYnHM_uT+xw` zHECV4mDlH|VTTWQ^On9z(!2Nb*uJJ3h3-vO9!>io&Y^-5GDi5z)1~{wtift>UKcZw zOc3*-otfFhQh#K<;LvD`61|*W zrbgokzO53C$)H4u`p&HVu0m!3v(!a3^)WO>;oWuqd@y_Zr-_IpdDepS0as5*Q@l#Jg!7q2__oK@FVokB1&I651S{R5eRicJegc4z8Y-r zzGJ201~n`jUQ_%$Uaw?h%y6Zp^#r60HxI~v&%V$YOW$uf?8%evc4ln~n;+g=%{c^{ zMuh>Z>KHeh!E<>Y+OmAkj9&Ux_wv(+OAI6BcrJnSA#vt!?ZaPuQ@zVlBsH-z#piAL zoPzZ>#Y&d$e?dtj3?!N-52D|>(D>pprsG`YI zPYwqurdH{gLXIVn*r&6CM4lGQ(ljx8yQtT(X z{t;Kvt1O;(4`Dsm0Y)g##9`T)IFFATc^MLD`#=1sWcwZw5&NCy1x+$5g44uFrA#w$ zG4b%SFmX>fg(owHs>|g<7PNA0N@L|g%q+m{k$9Qd%FOfkkA}gUg_s7r3D5&`GCnM7 z*3bhO=6&kFiyhl!CFg6fcCl<(m7H6MoCng<28K;AgQXLhQzuiI;PWNXqOo}83KWyD z%}g;P5Eex=1@E~RHpvt@Jt^_2601e9^L%2W=2i~nc1-8p6o;iv(nizbK@9uearr;` zi#Tdh`{URM7W*U=a$b;76>vqurtgXwj}kR4L+vJEy=|1t{m=r$R|&pdpyr?5?vZ#n zm*QdYS`yT=!Cyzq88_5qzvFFwT{hwX}HDws1Fh>fU~jg@s4J<31#8 zSabXEa^Bz;rkE#{pk!>$z_`NC#K<;>VHfQJm<;d$tjib`|+&f!+MZh>#*c-#*7Kr_jy&tS|Gta~$cbLP_3l zqZ;;X&v}r@VXF4fR7kI;I3C>0hRe}3_Dr|7&4&)n09m-zUu1VtU7D=E0A@yI^VkWD z{r=}q?ZmYd&ymqc7ots{7bMDO5gppS4oRQpTCPw8XLVDrDm76)&>K;R;GH-!7DgT3 zBu}}~8uz$u6=a^T{AwE)8db>%B@TO8ad)=sc~Pzv>$O8(Q|p4f(`|S?(=%xfty~2N zz&)*ed=m93Cp8_oEqKSf`@%ZqVo|3hh?}*1z#H!FMQHQd9`uPta!%*vek!q&ajS8#|RE_ZtNcB)TY$t1Bd&I&K z>x%Wn1JDM2rmM+$?kaO!(2F zn#84*GwM=aZXo6vWWYInf4^N=wHEz)oJlZo;7vfcL-UV3zlK6~r1}AQ!yGqf^DBFz zQ6yfmJ{WhgsE|_cuw%909Cqi(En6-f4n~3k>RJD3Y=E#44@N7mk%Qp4Sg@G6SX7Tb zMXW3BajamJxe7GeqjS@rKKa6C{ZZpWnrlE)2zhs_+E!y8o&kCs>7t|s3R?yIEgV8G z@R3vgW8ClB1$2N*&4ebri01kXtsW|^bXxKHtnQ|SitwW6USGC54i+|VPq6T~Av=83 zRpbh4^xOy^7t_S5)#JOBf{;mb7~=Pv3VINCi16xn8-qr4tm)q?N5ALpjL_l=@)*rZ z4a49RkfWlmqkB#*9hz{CRZJ`|BU||v9o|w&6!1&eOAi4?XMWvQ0Tj zWxz#|KtDc^TTk>ZXmM-i&gO;RTX$}#?crg|T=lzP=+CBvdUF-STn20Y8It3 zl#V;j;iHmKsj?;4%F2wLR*{xiw%p69gQ_CBQ+oXgHK8A;+}7L_PTrkHULtHwxV=Et z8GnYVMy>96r(I295Iq{p%(4(C1-8lDl@y)biY_AgPxHrQM&WaSq9O!!Y0-KWC3=r8rurf0Wn& zxw*~~^cJdErL+9zY^)k7E6EG%2Bz8^yIQt)rE0Yu8OzRPv|8Rv_;s_vt1C7~P-)ZMW$H z{6d{x|K=5`c`o>C?$QdCNIfCP%OXm$W_wfD<$Krr8$Mpd*^T4mvgIFid9aI*`=9h+ z98Ud_|`OVBCGmbDE$M30Ky^;zqblyTa9P@`>laVYSAyYBWFDa8NS-P_WazexW@+zb?W!L zcs?K>a}8J(7+x#V>|6`lw-rE>8Xjy1)FZK(jj>>B%5D?_BJI1wqv z#Uu9m7(h<#HPjy$tKvOMr7=DDd7#OAU@&2v(>ystP?goU#a4!miqqer5U8z-B;;#oR)w(}+x{0wr(yS~{+P+v`$$Yh#x|@C~<I=-mv}kiZ|Lej3EsGbVZhL4J1Cvi4W4X~y5N&~Xb-w0& zbdEVCgpc^G;ZkJ5yd7TUe(|f@)SXOVDRxKx;{J-SF2l|DFF{8cV?UUHbY{{@TjReK zf;N;Zd4`~B80T|U5WeF{7J!blCkD}vkHz-6NT1f1`w!P;x`A5Z>|HmvuY9o1mtI88ZNT#r z_2kzf67@!4vY0rz%5@R2;l0A`Mi*2`Wr4iSQxY=%qNnP&x20r#t@4jm>zaTh+)H|L zXy@Bf0u4)f$aQgHTA>MfAAr-lR!++pK;RFnyYTc6R+ya9d9b@Zs9&221dC;ZS;~B`*w|EowlPx*_yw zP=j9Uu$tq)A$rZ~8n_p$hP9rdjQ^jFx|;zi@quw zRA9FUZ%u-oeNkE>Dj$r01`6IA5?Y}Ru?VM=_t?QZN!kZ6crOkL(-AhzXRzKcZz@;m zuL_T%6SZJwArDyV4}K*;lwHcm!2`kvsN&t842oE9VU= zWyWhEhEtXk$nhBMzJ{I#o`$OPci$$hv|t!pf8a5;qF;ZT1bj4H`4M(6@<+h|SGSPA zjEwwZ(&X_KuoR2xy8tGmc(kSdvMoM9lql$$8Av63`}p%B8Z(Rlb*Z^6jHkL=s@G*q z%JV+mdLFg}5dl9ugjsb^BFvAG#nZfHS6;ivA^18@=$ZIAWSyOkNJr(5z2HUFaFOv{lRl}@zc{satuTd&~+?``Ne?aD3`td67rt*f+l~x&Vt1K}v zYOU7#kkF%)<>%gO2=eoc2(k~}udEI&$vF1fJqbF#E+yY0i858g9~av~7X*rN1Ue+( zS(J05lcCgYqFZ{_p=nJx^)_$yQQ!LKIU3qbG=aMIpUE7m4$Rl28R1X11kMW2}oC}yIBT})bZO+DYIoC1xjqzd9-jlS-roMJL^|5GaZ(N>g z2~N`1y@2x7=V1C!x>7{Jb|xF4X{X8Vdtx$Av_tJlZGa9&ftoNiaz%(3ZKJ9~-=#Lc zTq^@FcZsz($9`>TK$!0DC_i0*r~0nQhgsV^?Z?kkdtx(tR+{Sz9{yHGA&WX?Dhgsx zQ3W@-6$grUTF(%fEhOyMBG!}AtKIdK4yOLo+$3Iom0a~}%&9%2p#T+Y&_gHoZL?hW zIjBOC)~lmfwDKTVK9@iTACy*@>}4DI2&?kHE;{@f?@xv)NrAe%!oHz~y4gKlcUM)< z|H~?|I;(8@86;b@SK;o=^2yRLjoISBX&m(w6VR z>P*Bq#?HvamKmy+sP?#^quKLtPa0s!)3H^1=H)F>1=?LF{ZioLEb-7*29r#JYhMhg zJ5kFTa!fX_sh_!tZFlQGB)^jZNFM|_uhlbSYHbd7i&4KL5~ z-3wU^;TG{WI^y!OlMfr4hhxr)pI%L6)AX1?{`2`kUGBm2`A5W=(-0Kp?oWO!olt)s zVNiJX7Wk9a7)#z>Kx6lmkXz=0V&2Wtgcw92ZXVDT9IG!eI7v>L%n2+=RjqWfhaT>e z=u=L3vSeo8exXEdN01Zsr0KPTuE9kTY_`WR2z8P<`pXMbo%YJ%nBxfj_e*61ww4jk z4E94Lc>1Sj-?n9qt92P$iIsO*FcivaP18f8{`k?`3>uyeoED#h&J5c~VflDz_}s?P zz(F`)?n&XZU>zt2B7!ffmcJQ}~9opP9TL!YEC_v8u4n{ds@F7veu(7T3FC!UD%^H@=m z(H+0Z!Grd#Jq??*K?&~o6WfMi3Cv(;UD+CWW5x5LBj*v=kuk?2m+u%ZeS^DK*BNwN z;J{QgIK-yL2`~2X-F>^qoj5t)BV%>6jF^KyQ_RR}n1qAA8D16Lfeh)Ls-t-_Qqo&M zvFtYY_@tyDpeAPh_7t=9$t;hNzg-sT593coy$h@l&hs5sSH7LRFf19 zHI>O+uiM0CP!`$NzD%HZ5%?KCTjeZph&{5$s*$9@S(`t5Dx0vW$xUv6((ak6IPk|O%%4!yqcHchD@B8|GWy-Q9 zv}$xVCY~uhDh&N?yOKAR=&3DN#xj|$$n=WMp0}&dTq#Ba#zVfsd1w!OiokDgnYjV5J+X8zCP#bkg8HN?F0gGwzr z!_W{78wMYY%Vh==(%x>D6OYsS%U=x5figu(KGxiv_MH^AX?q!|ugc!CeM>aWF}bb^ zTBo-70GjiH2mZmPN51ddsXj3B#!5UCkL{b=Q(=6H*})it(GvymVh$2y>%6;Adp5CL z&#oflabcY+Ej-OUJ?~SG?{sr4@=gmS&`SDOSWk!W>Dc#027qXael_dN#0uVFL~W1Z zO>!`gHQEN_0Nf4fj~{vp*N zM-RyTU5VZPD?zGAmCVA7pBfx8vVfF+%--}9qi){EOVnWSk~*J!Z`H3=d?3*-pP1@c zj36mt|I~kzd^GrwgNMgBgqXHN_>Jfp)5ga?#N$laJLL4HK(d=3 zl5MQS6F}bNB@%C!Seo;qi2bHViRK2d<&>sJ-C$LD^>4r~)B_D1iWzHy-elyo)) zIn=7~N^M%QaC{tbzVi93io5a0)eWgVDTPwA@sq>S&Wm(5US*2GYZwh7dZb@)12vHU z*JjVddM1*re1O!dRH9-SIwPlrp!~wG66{mw0K;XYDyA_{GT_JNawbm>B>MF8(Ad#F z;&C6FW6`n>D4?h>8GaaMzI6+4Re9+M1AwQkTO80~yzpYFM`D?mXJ0wot$;pira@#% zx-wH+&TE-6d0scrcEd)Txb^Fg0dVVAibCEmS9Ati+a+7$o%aKq?zT;v$p&Aq)2ITaatJ zj>wVilAy4+iIZ#W_Rx%QuX|Lpd?LU0DDzZXzWJ3GHt6cG6{^~c^WnAqjRlRa_%{n0 zOvefLl4-sunTiPneuKyY53Wg}gKcrc#V}T;j3zDL_)A1BCK4WBqI3`V+nb2>yZoEx z@kuPF8LFfMjd{57eJ^K_ABnFTU{ieSPyI;CKoalH2WrgVlfM~B{optD|0V2T|067x zU<%9~!&HB_A=2pn3MX!i}b){|7=v8kQKI_qrbPIFw&&ryw8|&P1GpWFHS)Dp4 zH}OFyTinW|O0j~fPRfiOcsN-(u##Eno^wmLUD9leTlf(W=p~9AK+9fV>snVoY%-)u znrdZWYfu+6jMdL(%kUt&+5ZrIB0<0q{BSIv<={SsaE&e2cO)OpXn`G%&fS>U{bxwD zyPSFNN+x<1rwErDVa~w=VemxabTJ;EK@oR`ZNy>fwfWga^jEG|WSogS!IAo_Za`CP z|ASI6c(f@6!wuxu@h4%=AZ61r_3jv|&zR2lk&sv=YL(C0xOCYoGL@(uq`)B&{T!Sa zvC3y|tf5&RHrH@;5@k!QvV;fdi7;MCp~t**>KjWjNb*>a$UG&?H5;JYi_zgyq7s-F zcgi)$MT`iJWEt8WXUr|1jl$ZjprLnvjEDta{D|_b==+{>#BSEXI3Ix* z%RS=R-c;xoE{#h!-?JfjBRl-`NlYjoN&A6sUmwi7=GyDho~3d?t3ci>;@pBBesPRgYaAw`HGO-rJhGU_muKpAlAO z=70dC=2oee20V>`(O4Qejm?LaW%p_%-LV1QKI?>F(f){eOzstZb`cy{0yfjdc}9a_ zSNlT*%Vn^T4$IVtRcJv6(&L@R75=9FRGA8P`24i2kPU7vJ)0@6b;xg@`~W++ZJ50$wdDg{xSi!V%R}vl>+81YnZSruD{TnuxZNw#Y zh!HYycP#$wa~9Bd4I~$a^48z~S-iCgPpv=zA%a&{FY=7g=o?kggzE#3>l!6p_l)xy zvDw9>LyJ9#+J%eh8w~ZYFsn1c46W;P;?jf!Y};4hA|!QpI^MC{aW1i^VB#D!lIqru z!p`*GG~KDm6SOPN^jbb)fdE-~hZ~6Hu^=@c}N56|x93;7%@1b#j=) zt^Wnmk8o+|AVMD+3l2IvJkU(_SO2wOs(iJgY>0f#5herHRs~rg55^6NbLjo1UMTvq z7efx%p0Ci54JwRlRA#g`k(P4Hy=Sif=Ey%XrR4nHht&J_ba$>_uB@D>y&JuaLRRe} zKFUaVFYRXFOq@t|*KdMqe?OX+s>Wt&dJ%!5&pM9kc!3G77ENdHG^kvg09@&FUlY$`$zz!G=a>6Qv z7EdH}196#5E7^`*sqcJ2>IAGcw{sX0WPvV3yW1?{L2UE4lEKEOOu-Yp?{cn++ z3Mg*U{soX6U_8j-^KAUfm$veoFASIUUF($Yc`@J!eW_bJjxL_HA9S0^n%4gm+h5OV zRrDy&Xus^wiw?+@RUQiAPGfY0-A(s^@&GrlT6tLl`Gsn8hAw52yT>tf5eQ zS!yPi-V4m7wWUIf8t{pQ@YRM_N4S3BnlS1>P_u`A2i`n6b00g+r!7WDBq|cg@sdl> z7j^@o1F!ZJG+pK!fM#9c53gF$`ISw*lkWKIjO*~|K$s(5KDO+FAufJ@m^@F3W2HH# z?l=DR$KSF8k@dG)%d73;xcfNyPQz#R5b-QWr}m&x585^-d>AiV!n4rMHV!9~J1}A0 z5Kd8A1QdC%t0Dr;f`rE1HM2Q+ev!y&UMS+xC5)`(THeS1iWV9mstXfB2CCpo(8FgT znB#|UVk%XH8$NlT%Sq7l-+hrm@_h{9e_}CsqmJ@Wa%(@o?{d;3-zuvO8?@g@A4!xlwL*1$IYR7U zF+INQnH@pI`AOubAja=U9|R~}bn^)MCb;#ulGtkZG^qGCdPk_E^WPF=G&T%Ur@DMJ zt?s>5%B4L&^WjbRd8HtV4q!3Wa^*o=LI-OPAD1|8d3tS4X6cl@{Cl|GLYS zf=u9iKdGj6rBR_~)VB!Gc*xE-E4q8qnv^st(hPeqg)=d&& z1ou4kHu7f>&wtzTKl9B-{}V%&u4mE;ff}FK8NqtstwHH=*)S1`M2=S9VQ1D~!gJvCgfqBKf3MYRIQ{=#c>7X$o4(BFpok zCTwr-j%qed_IUzUcb+rrS7^q*Wn6~W+)7cQhjMF%^*im(y#$6LJ1sxG7|BF?>VSN7J|Do0Vh9o8H=@!7LQgi6r2Vgz5CBp`+Hc7XBuJwOQ*Q9hJ zwrXQ8HB0yF|EFE<3|*47-S?k1f$lyWI(36p5@o>jR|L!@eP}=9d}eibB&~mtb|d2; z<~#D=lv#GVnSXoAJ*9dx1WPa2^2xc8QN48SXJ=#^dHn>|6P)~QnF$wk3EErcX8d+b zl`BA?M%0yN@^2-0(>KZ;ZB*ZV%zvp`8VjUq-?0YG>~8<&GNN;XckrLsvUK+0bYUGf zcKRHRDl|L)HCuM2&d>nf&z)QUjDPj7@i$uxsjQd%ZR_r|PnQf%x9q>2>hXP28QdFJ z#^wK-qTE^wYGCIp^c0?6fg8)z-yy#~&3!ein3Gwo`Ddcq)%W4}FgAScQu%VT|807x zzT6&K?~}u0QW3^~uv>Cfj)1~njeeE?^qtgt!-8^lmrUN$W`Ey@Wve%6z2AG_G;SOG zHyG1N&W3z(9jH^eG1oZlMHf{9HlE0s|G~kZxW+a(D##c=syW}q^2G$4Pr~z80jWn) zkhgnP+(`b&nKDx{Rq)WicD0OWt%rL~JU%xj!xcesPZUC0t1p}%R)PFB1pJM@M+I#S z88rJi(h>02OUUG#&mke3`;xwKA)a1+8+mTddw*@}_<$bqS9>`-BHP%`*f-|mHP8p! z%I?@aQa7Gzr}`5>x(C>n3?^41!#a>7@eC0~c|O5tjIcDWMwynQ!b`4W4lL)-L`qp94iB}4!vbA>gY#_Tu$yui$=(>%CB zLp*6>-MKHLHk>q098UTwDk|dYHVQE+swX#gM%)VY9}=g-q9&IVo%lcHGcnoYLZIm0 z>=$GxA$EjbBV+x9J`c+}OE~d}Rb)FrFPElN5>|MPDjD^s62V|E=#UK_zk6dg@{Sw# zbqhbXZv#lBg98RxvKZ4ThLlPSW>rc_45@D+2D|QQq9(7q}o zuFI`~O=6aeJ>I#ya54WZ0(G=mRCV-|)}`W|^EkX)<9T5~Nt&5-qyW*f{8BNgk zZ!e_%h4?}v#krAScHH=j16@Am{lKC87L+!j;>mbL4&opgO6{aB3Ce!3av!bl_=rz& zf{R?zsPrGU!v0!;7X;I zplljBJdmK1a zb`RdlFy83HD1zC^NXhpg=dnvFSPR6JxFT@f;pF-mJ^3YK6zF8Y^(%<@=7{sJs_`skXW3Yez=EbhFd&Hc~qm-uQMaJA!H>r%8YZEHW%p*Zui(Ixz7b_YU({ zG}6C?)S=N-EdweyO4v$sLgQK)PXE3qFY*bN1YF(H(#*pA3R5zzR-$oP?_k2^ydaYq z?qWmd9!?~|A%7FOPY32fy^v4ex&Zf-6)sk_5N}_eg|z0;mMTK-CO3Th=x-k&emT1$ zrz>v7<^^uTa{Nj?Ij?>m2goLEbI;h}@i5|p*ey2c&~K=l%9mDXpi|U!AzVDrrIPSf z{q?5;<#;zk|2EV4B1{BW`{_nVv>3PDXKp~V`4*d5#8)HNJ`Vp8o*Jv&XKbLVMi-^h zVSX8JS+ux;8$ApQ+`_2Gu(KPH&Dc$ISrgB*DkKVt(HSFbb}mr%uZrCf?G`1#gvYT+ zg5(;B>XqX$_ZevDp50&($8Z0_1)f9*FwENxUnuM006Av}c5kY&xQX0AuUtk^M!GnM z16WQxZKXbaQ7Mc&(%fCPOha`Mq6^JtaMYU_TM=LDtsppP_&B+Q@w4(;nZz>OX_?#P z8h-+y-Iatdtv$_F$KREt=IXE!L6BG`whCKCH23qjPDWjGcfZTI%W>IV;jSf{bQ*^B zyPjH|7M!y?jo~)W)HqNWl4!h?<%uO7B*NU)*XaHKxA2YF zB!zN~`m7`H>?+c8B7+cds*jIS7)-evTwrQXwzBE44|Z6EmDWOio@hLy9T+TUso;B$ zEDN*Qq82c4m=WrP#@Z%25jc3QagPG^yEx`d^z8H^3~v|Ij18jJW+P9wSFg|P*lkaUPubC)z9) znhK=M+ec%xOF;|0xn*9?IOMrup2e8XuItL}|KKC{jBhbr<}$rvd3{qdm!fiGWkA5L z3RyYDI%TG>i9NRNqHiHMY0sjw>8xheo#tXx=SR61zYgU2aD6Tw=~Ba?XG9;jUNNa7 zaCxcj^+!I#j$SG@o*2FK2>p3DDZ9LEH^MafSIG z^(`a`M7wkTHr2{BJ0FE#NY{v-({ue0%dCi^PrqkEsO%LzIS=QORy#X|&h7goIVsQS zDPH@$5PC&$3!UgWr}!uQ8v0iVBV1Jy?>vEV*l~aKBuhVBYg$GoMyF`jmI=v}3c5I2 zzExWC)`m~lGAXIQIyx~o#!MW)$dvWpTNVZNTB+AnlNYmpqr0O}c5GCW!Y|H>eBi)4|-Bd$!ENKTfV-I{kH_Z=OVGU zm!Z2bTa+5RajK@oQjY0uf;}l?t(T(W7yIoV8_kMwu0cogV=O|CYt%O$A@SMityl{Z zEbZp)v5DJ)74DwqsJ|gg4|8shFErtHNaNcHSHNw*TpTk03Ep!2LsIzyeO$xqY$nNQ zZTuhQs(t5%3{C}b%8aP%`B2ANx~vMX`_|>ER!?uKbETn;am^xpQadbAY%J=!&$yRL zHuXr*Fv4A^WMD%MLrwBgkS2^*|Nhk_lq9O%y>la8zt)#i-M7q@z(ma-nyA~s;AW@< zQchp3=D!WKOcaK=s$_;-{(k6|L&AGs%tbZTJ@27a78!A5M#DrcV4E~pd;`gj?uZJ7 z{{Zy_#dI~Ll(~A0EN_cs-1o5YniL5g{#G+qcuN`B82WR2yBtLV_w{b{AuiN^8^`vW z1doRfo=tm}W>3cjFM@*9%E`r7?yx28%;&`LMs4g&hDG;K-(_giN zRq27DkMOcTm_T~<0oUZ;aSqu~TmvRKup?bv>sR|kS@$9?)*uJJQE~_zzA{{%bKz9O z((vygL;$3^M)|4lbt_srih_U1WtJ=FHb`Zp%Y~$-Qy0e~5j4SrJLc*T$bin7%`yAm zoRXPLOusG%NKZ4#Y*$?kC@#SiBy%=4K^8f7=f)fm>}w*v zWx3}eYmb+BYYukJ+TY&`9r^t-CFZ!2?(bh*IUMEKE}FNd>mRhoZF~N4%v511A$UjF zPqS+>AC#GyR2e5Lp^hf)KkbEgcP^n5udKc^w+Y%dwB3A41iduyY_}6qURZWWAW&lYY=5&X=r-6hhX>)O6! zpl5NAG#!snvXM1qp%$!lmCwk?gZcFRs_`RN*ZnFIii*>*$*vmYybB`61)za4l=(f3 zZU}D=9e={9A9jVHfy~``0k_n4DT2%a6WCfPOXKbO+0S#@mF@dtfV{)PuRJcISZ5|= z37I}Frt!KuXU8hq3B3KIy)Ol6ZR( z>A7SD4^)T>Xeq`QUur|KLinNO<_@y+zHZ}H6Yuifp`RgsieuCBWPG?4WH=3j&Bz*~ z`n@5M$xD37XBq)7wmMt4?@Jk;te^}~OM8LoszA~$#^^W36Fj1p?Q6!vq(QCOVR|_a zzEL!06r~k0pylelNdCfhX$c@r_tU4WP2c_0_a%oj0maM?==gE(+-pqIQ0b?5{$g9$ZkNX#2#Cc> z+xN$VkT?lv{0w}vDbLOCb5nV$s*+>gzKB;{jM$uK-cGDvXG={}$+bvy?8UucyGXoU zSP;8pX|e0iuXJfLi&xT3KelI{-HD)n1Y1MN!PtQzosjp{66K=pn(LBIDXarU}z#uZJiijG~A=i zmdx0lZ*AcDS6PZ`<+L=ZRC6A>V_oX(+$f%wC1CXG)a!W0Y6UJzPfbhBo$C2h(Xws8 zLW$cF_gCtJ>#fwcHHi1*J_WqFV)7U?Po!FI?Iq5#vtmkF!rjuxY1tx0HTpiKz-y|B-UGH%PC z+izjq%zMN#uz7n|lOBz=D2aMJLtmUFVjc9wm5STK=7svdcR;Zo3uWUr8o)vhgos%P@ znk8F0EV`h^5HNG6qP|k1{&_Nx+cZ#4#e2!59OxP0`aDeP$znx(-11- z`~BgpcoMg^RGqioCoO@e@~H}$L_k#O9-Li?K{`mCxl7eKW`nAgcG8_eYvu;wmc=_` zvInTV!N$D*-cj0X1q+0)Xh%L^lopM3iTnu=nB34ArE2$Ld**3OJ6ak6;}TX@Dtc)9 zxF9rOT2v0PIygF(TzdO%1VO_LYBtQ<&veFYx$0-?pX%k&IHzhia8Y?THv3rp0)RjE zx!wk)cW!jB->vk=*tcM_pWR6=;m;yn;Dkir(B6S-yc=FsKiG15_v^+@^w9H&@N7fZ zsNacCeDm}|S!GmXaKq&=_u4=`l-t}$CT#+hYRW2WD1#Q`Ezh1Ej!B&% z--CR7xeZKi=A5%vPzIfRbe$Zz)>D{Ht;Y9RE)zBEj}oRFGn%8)Na?E)y9BxGEd&rq z9G|f19bf-s<+Hc1RnNxaXC6PAEV(g28`8TCTg1mH7r?f)@yiD@)x~SIaCUO;NrYdX zmMG#^^DxggoWF!;R<`YM;&-`2tUVVImh0}&?PggsCM@~nIxrt8v3*e-kG%7lcU~Q5 z%-YTw<9+ttr)sPTRbwsgpbwD`oibmY*2mt{a&!;Y9_-QV&h{V0B&;$wz-t@P#q3TF z0sER2^NVfymN)R0x)D4J!;`R(9fr#kM${#G*Ip$0kGcqsK;?NfSd4f58OE7sQ3n7i zQE3DlS*GaMKck-f!b{-XWr>|eCL%1xd1+kxxW=`Nhq!g^9X@!{y^xrU7y1y;q1^qI z00A?bAV8t8EPa&qWLQT`+ke`R-m`G)@j0^Z_A!m@#6o6imepZ(vJ^*6t)DkLtl3<~ zWob8ogYETsA9(~~$Z_95ql~(!b6;aYN^9o(LfN{9k^DW&T1qw|Q%me9x5Rh8k8@>Ho4*4@H=Iq}TV0)qz<^LXloUOG zOfW6-zQ`R_uG0bb&9mo}(Jy~k#2Dczj}oWmji;)7ZAdC+RiY3Xu2X$^uu)oJdqLI? zs#hRrzB?eRN3hknoS6M}#OtfvR^2{bmzqv9D#xs0o30G&HW+OGn1Dq#S$7dZSCZOI zIeCH>BK*wf^@|Ce!=>ZSzQi<&qd1JaAgoyi#jTmlRuIKaJIpKm#iye4c$}=ZUk>QQ z&{ZOT6o6Jx9dvJBpiP%F@0ub#e#&h-kt|F70wXn~09>X!uFwJH;f9Z+8HUqEX>1(v zRbkT7#VBB?QVbt)ikLMUjLe1P29_CTc*b!zhB%taJ!PK)SS0LI@5B zXegstDWamH(xtbcLMS3tA)!WUlt2Om5+DJ-6N)k_GrxPk=l*k_`$v-V(h(=R$NZNBSv@rjARB}?Ry&#gE5TQ`K743w5cJUB zp3Kq7yO!=535_!w-;2JVXx(QV^1e}D&K^b1j@P`Vf=qmE==?Si-AVDyh}kfS0It1N-1Nz4gE4}e5b5hEq2`3>lM;!_ zCo=^<&-EYtxjYF0W>5Vs3ppK=3e5RX>rO2{W@OSB4&~Q{uZr!ZkkkUoX^M z1s2+99Au0kCs&652?G1Or`lKY6C8f~`{gr%Znv^d z^Tcq?8A!0%TGm3+H!`rhtfd@os6?b(bMx6*AR_##A;j;uFE7lDUI`=@`3)1L^hG>- zR9t)~&NL)QJEU)KtlT#?DlaiLSFD@+`?NyAz6*y9ck~rKF84lkbH27{_Qri_!)^PO zx$nKbSD<_yf*Z6}43+58e;k*szbC9aB|$}Hf<(QozusTl;rSuBS%d>;w6fzpZxbFl zCv7ErLLF+Lt32; z6MByY6X`X#V<*oS?~YJBpsQ@Jez6{ZoYt4VwejdNzhp&rDr3=HS|_@KZsW0YbP=4VF9X4<<|9;Y_(h8_I4m#9|T7l z`s_}CXT66jzj=SP;oNVd39=lbv3vM@G3V;}U(W7f+DdR31>}5gFmf!ued_K<6<+q< z&SSoh+Uz#CDc*{8W1o*vG{mtn@z5C~30}It;^wY^&y>RhCj6n4q3-8rKHA&&`S7H4 zJwH=#tZL5PyFj}1cmVj*$JDyhb`P)bnWp8&&IZf!{v}!TFGth7XuswHkFq_^{b;Cim+)?p)!=lvE1~c$)-6 z!uHaOD}nMg9p&nUDVc~SrS@Xasy(+K*<-2_?VY52f;4eOEEGo3=X)g}blm;VF_e=3 z{c)vvawt~hf%ZK#-1g1gE&d_OY+pP*I6_f%!ZqIBg?#T81_%1=-DS;{E*#je%&yg& zs#qM7B+Fhc$`r3ooAY{~0dnT>LyJ2K?l^XzKJ>;FJ+dox>%;KQ-Z}z&XZbn3ga>-R zPuEAc2ed|>oj4k5r0lPapng8*+44XRi!dYmwpgauz1tf~=;PuV-Vnn%xPMEg*Xt~T zGyJA*t$A4MiOYu`#MSQ_={>{=clTkGyOr&?G?`KGJ#w}lZw%ek8>J8TjA`XhHP)Y~ z-OyNhB#;rTY&CpHo~^f_Wc#^#{7zAhUO^5odr$WJ(~<-BUfk`i_h&-ci$?-|@1v~U_pu~+w?cUh92dL$`E8MSI3FV($` zFSTXbUJCelYdjOP$txCB5x8HF(GzruH;cw%eiC9Oe35V=mqwB2hP zGe0R%ZhrK)Jv10uWB%N>w>j?J2aq@)SPt{`b)wtU-P)N1Y8KIsm+;cg7fXsKT95dN zsUo*X(sxorVDW=j%c{Ow*RXYPJ7?s-bV6Y5V{R);+$%dR+pZ8RTEmWw@H`eN$@KMl zcb%t{_-vT{pdEX<<{38r8V5zCOEm?fPE!su`H4yOvzuH-ib4JF7|>C&r1|Z(8}CQS z&Yw#8lfkKxrTu?gh~yZVmeNzhebQDHCw|1e@RvU+XDHxL6^Nzo59&SDKG#WMoC?CG5?dwe)A76&gp|%u%$8mdrdWTQKh#@ z!h8OR0w4OKY%ulIwA=rctAg}9`^Tp+VGFJWQGT>u)5~Hr0;LYnQSohX5aI1UY8`Z9 zBGrP-4k;{DN+LNw?b)+Bp!PjPYI9vGb-$pooqI}x0W5h4%U@<}H&E}8QX_$2?=;AG z*+A4eIA1&G(+z=fO$A8L%ydvP%|`EEL=Mn#sx$jLKluM~u&ueRSpeU zEqgS1s1Awh&U7qH@u?+^J=;z7dw@0eT=!nK>egJY%W?KpQvbfs7<0(I`43#_5+xW< z*wph}$?4SHIj>?yltwq?_?Yc3_SO=+**VZ$emCmF3)O}}%k&{9RBUSPZp*k?4xK4q z^6*^#4xNCMGUk9Be}YviXHIKefzy7fx!w@+NM1{@Sz=#~WV%a|Ujenh$5&a(w7tU~ zcM3bxF?eS8$+7Mh+XSYO(K|1!`MxCi3g&3WDXI3jhqrNV>OMV(%Yp*VPc6P?XQ-nn|jYmM8$kPV185t z;`5AKzSj4(I5Ep~*&Pp05^S+UwgqUtwd~RlwC&&O+QZ$?pt7hJ!Ij%MVq2m@JxJEFoxHtlHLu?kN7io z;`M=Z`i8HIj;QLl(?zbv{xLN*ejHj?%Shx;qJ+*%s51fz#K|IhU1f2F>_Kl+*h{!XtkTb4G;49 z_`S8H-}#>5JCkDD|9GCjl@j|PFq@|VWY%3{=)jCe=j(&KKoFk&d>yme@K0hU&7Ow2kP%fBo3(e>!}$UO}M>#qO3Z?<<)Z)9G}H+ zuRgGsscq{iy?LlQF;uMQsz7X3z2jTDuj6cg3DkqqTv=wsOIOqDIv!UT4)uVvRU%LD z(z*1?ktyQj?zlqwSA@-)QjhqM?BQk4Tb3(dR_P2 zGou-E*M|$g%CvfP$NAC&g>w~NXAU!iB3|Dh-$Gve);ktN_;nw$ZY-nji`@A9gHrL@ zk*sIONwwwkA9D!l#f-h1@iLyKWE)+}d|jpeKp#CD^El`QyZf=Zb;tFQDd}s@g0&p? z1c-kcyfTr1&)vuy6Vrh;>g0n!>RY8}*%9~tI@3{lo1O^Qxzl{#_vDNUZ_6ILry14f zHm)}+AB3+HR-U^#oHUp=-X98J?_q@NW6A99Ku4$q^`)c@hgw0U9I z%2v*Zrk>Dw&3OwoBnw#Mv65iVL>!dB)>ApefZERIFv;Tket`FoW71gpb1D z&0`_(SUFm#yMMu~O$@`PV8(`fcF4a(PNBG#yW~8Y?|4iTnv_j+%c-aTA)I-0Iw)AP6cx1NG|NDE!I>)=9nfm{S^$GA-*U2wXsbU1|+pR0*%b> z!-4q;<*%NCy^(DnB1bc5 zd?csx6078UF)&*z&mW|dKn-Lx%gfME>)>}XDaaC#ln9E~p!iERuW8zwz%DSt96g74 zlgpsJjk%n?uno^eC=Sp?yRxJDYwguXw1DEyFK}Y;)p_{LbuuUBfE-M2F)fs{=LhJ>n(-6Jux8XdDU z!%5vaZQP7fkkoJu!5!>$QO+F8tS50WX z)DtLzndDq4-U!taJ*NXpS}>7;d${i1Wi^}KjpE{aB`s%Y-dLVdsb2t=3h32`rJeD% zYP9U@+Tp)|e=%$*2X0fYkudG_;uxoF;_-c|qZEX5 z_PZo1T@#xasG8X}ZG#m?*+sD>&R1gw0R~Zm1LmK)`$Jl77B*!i(Xm12(89qx#$bU| zrlSna?allLFhk!}=z7e8FD%8k)LhnFcLNvkkbK(}UiF z=2I@6C(UU9hz%kQ(H7+ST{H{k+FCX!nZ;^wY_5PZS2$V{o*TP;Ug z&%8Bac%GBwFU1qClFBOR#UD)}g&XdN%H^AtJTDPBND9pE(=P&e$^ILyaS|`#;A1>K z6DCJfspCcfOhh|mK{{LdeY;ZwNlx8C>S+tJ^l%J@9t0Z+0onr&8zBqLWg!Byv5{O- z!|kXKGxOX8rnPrXJ~A0v`gC`=C7K&&R15-^x5>sU1Tx;wMSYlhDVGxRDo zb8B9hShKMiLd)UapbTAZenbhbcUxh7R=sK3^rt5Ab)aW@>!eS|uQO(SDe1bwYDw_U zc5Hm}BzJ4DrX%k5hG3tWhHY)RJR zaSd%=oQ!7_y>#*S;8uC6^t-lfN>y8OvlRa-lb<91hiJYdFqgNLKAiV0_lSV1%Rhin zrqH_$_4mXoyP}%KBZ8L0Uo&b}ND-JAz!Vd_{I-Q4l)O{}yya-wpPO>G+901|WgqGB z_&j5M{G%^c{dm;amT{c!;a`gCtrV~VF^Pw6D`;Ffj<(pXcxI`d?;77O<+25~YkAUd zh5rG#h&j$k1&uECci)rep&y|$tmkm$t6;RT2m|u)VEgV(BhBMr$42=StK6-9n3FkN zQW?a?RB*2Glvq}NMz1h&R7KV{yEtrtI7LLXynN>D*Bv(m)~0@Z zvFv6s{#7PFNB$4dd`DoF2cD_O<{v-^u&B5*zl_zgWkiusvNO-zmgHYx^}~!jNC;qx zT)*;$QCvy0tb?(gcbw?(Y1YTjhnvUyABaBS3zp z8|@@5LOzFqN{+fcS}H+kQpi#+JQ&H9Nmmzsm1iD?5KxdpROt8QMahn2dc|xeYK13& zd1di-Admjd-@Szx_gi^$+UUOp56y+zLG{+oV!tzNJaPf_uEG~vzhAjd9OTH|Yq)KYJB=_mdC0VNj*7Wi3 zbsrY^k!@4OVYq41-`ZAm$PMVJNuaUTY@?OjH71cP(@iUHfZQ#M0?mnXSo)DO4@pc0 zzTP6_g%s=KA0jWyu)d1WV5J<|7`(!`WtwY@oksIP76MLw72K$pi^qDfE$NFWbN333 zHU9;(5Io@xz^=K~NqHD}l|$N4bF0W9+GYn(m|tWud7T zhjGv&vFQt=nF|_Kecqh)1DQS4T!}=};xNV1Cgui{xq?T|N5{ee}Cx(e}MozRgP9D_e;fGAzVov*bER|d>ydLlZn4p*A?4U$^1lzClKqUramDATzq5c zVpW$%)0(vbH5ZZ6O zWD^A_bi}bu088o6I3GNA<^aD%g?~6ii9TA67%C2{tALC>J+Fgnn$rc;vz~A@qc=TC zUCg3l-@S1B|1Ik)(5`6OnxgmXWxe%(cv(-J!a*a7TWr9<-svAzf*&u z2#QRj54S*>({+bO;#ivu*5ja;z|#EFUeav}D?-Pvp;Su+^qsMB2HF(yKrX*(ZueS( zvu3eZ=KQt}e&1=b&P7WocYuFfPFL-7SiFiD-?nTz1T2*QI@y2E05}dU>Ctz7lx6e^ zqP|PS;yMN#hoG$E(7)3t0z*l4_G|EE3C{@_Aa49)+$Z=ZLGm zO}s8vPjB9@dq?4xy8P&o z3KPn(W+Ye8!kqDCN-pk&D=4ja{_Z4Qppq5A%3xKa@X?Rt!$o1LLMgazV&NiY)}-Bw zG30~z*2o!85$Y3!2kl(}EzwaeA&H-tbuqFUDOKIRmw9G4d_5(vQJdQc} zmH}WJnpZe1Fg<6!PKB3*g}b8=6pFbQryL4XilmKGiozfZOi|p}6bVepI0e+%6HoS9 z4INOcQR+uptK-iVW+7ju?3!5^*1`Bld+)FRoisngvCO8G8(IE~DOFTyo3FfBoQndS z{b0F-Xay#72~GrTH91$@eTV&*qgT;+aReR-T7`FqEpBYE=sS%odb)w)$1Kjb2Diy* zXS_KNE~`j1!`j(>iANCbMgh0Nu+bQ3c-BTIBI1f(u`MLSQ1vB8GOzCuQS-Dp|)V#;MM~yKMV_uPoLjC1DJxQhX zSi}NE7|iM>-uMm_3)T)|%4q7xUv~5vgl~r>fDJum^bR%QKPt>}oK)?_grk7OR$jnx zA=p|Ci-av{4IQ(B#Bx`w`f@@NGs;%*_^}GV?tMUnewGiP{~7t{@B&tXuM4^>{k<9` ze4NH24C!Ty4T<;i!K;m`#X942JOa9r(PoPpkl(OliAZPH0%s1hC&QYk^-Urk3{{vQ xFbgZ9T|u+!6Qq^xS&3lOqY^rj_!}@p@Ln<;R|1SnA`8y5fl4=KMKajd{XZpb`dk12 literal 0 HcmV?d00001 diff --git a/userspace/falco/CMakeLists.txt b/userspace/falco/CMakeLists.txt index 8d225b05..74e35e86 100644 --- a/userspace/falco/CMakeLists.txt +++ b/userspace/falco/CMakeLists.txt @@ -36,6 +36,7 @@ add_executable(falco configuration.cpp logger.cpp falco_outputs.cpp + event_drops.cpp statsfilewriter.cpp falco.cpp "${PROJECT_SOURCE_DIR}/../sysdig/userspace/sysdig/fields_info.cpp" diff --git a/userspace/falco/configuration.cpp b/userspace/falco/configuration.cpp index bb959e27..bbc7588e 100644 --- a/userspace/falco/configuration.cpp +++ b/userspace/falco/configuration.cpp @@ -182,6 +182,43 @@ void falco_configuration::init(string conf_filename, list &cmdline_optio m_webserver_k8s_audit_endpoint = m_config->get_scalar("webserver", "k8s_audit_endpoint", "/k8s_audit"); m_webserver_ssl_enabled = m_config->get_scalar("webserver", "ssl_enabled", false); m_webserver_ssl_certificate = m_config->get_scalar("webserver", "ssl_certificate","/etc/falco/falco.pem"); + + std::list syscall_event_drop_acts; + m_config->get_sequence(syscall_event_drop_acts, "syscall_event_drops", "actions"); + + for(std::string &act : syscall_event_drop_acts) + { + if(act == "ignore") + { + m_syscall_evt_drop_actions.insert(syscall_evt_drop_mgr::ACT_IGNORE); + } + else if (act == "log") + { + m_syscall_evt_drop_actions.insert(syscall_evt_drop_mgr::ACT_LOG); + } + else if (act == "alert") + { + m_syscall_evt_drop_actions.insert(syscall_evt_drop_mgr::ACT_ALERT); + } + else if (act == "exit") + { + m_syscall_evt_drop_actions.insert(syscall_evt_drop_mgr::ACT_EXIT); + } + else + { + throw invalid_argument("Error reading config file (" + m_config_file + "): syscall event drop action " + act + " must be one of \"ignore\", \"log\", \"alert\", or \"exit\""); + } + } + + if(m_syscall_evt_drop_actions.empty()) + { + m_syscall_evt_drop_actions.insert(syscall_evt_drop_mgr::ACT_IGNORE); + } + + m_syscall_evt_drop_rate = m_config->get_scalar("syscall_event_drops", "rate", 0.3333); + m_syscall_evt_drop_max_burst = m_config->get_scalar("syscall_event_drops", "max_burst", 10); + + m_syscall_evt_simulate_drops = m_config->get_scalar("syscall_event_drops", "simulate_drops", false); } void falco_configuration::read_rules_file_directory(const string &path, list &rules_filenames) diff --git a/userspace/falco/configuration.h b/userspace/falco/configuration.h index 0fb6700a..854ca9b3 100644 --- a/userspace/falco/configuration.h +++ b/userspace/falco/configuration.h @@ -26,8 +26,10 @@ limitations under the License. #include #include #include +#include #include +#include "event_drops.h" #include "falco_outputs.h" class yaml_configuration @@ -133,25 +135,50 @@ public: // called with the last variadic arg (where the sequence is expected to be found) template - void get_sequence(T& ret, const std::string& name) + void get_sequence_from_node(T& ret, const YAML::Node &node) { - YAML::Node child_node = m_root[name]; - if(child_node.IsDefined()) + if(node.IsDefined()) { - if(child_node.IsSequence()) + if(node.IsSequence()) { - for(const YAML::Node& item : child_node) + for(const YAML::Node& item : node) { ret.insert(ret.end(), item.as()); } } - else if(child_node.IsScalar()) + else if(node.IsScalar()) { - ret.insert(ret.end(), child_node.as()); + ret.insert(ret.end(), node.as()); } } } + // called with the last variadic arg (where the sequence is expected to be found) + template + void get_sequence(T& ret, const std::string& name) + { + return get_sequence_from_node(ret, m_root[name]); + } + + // called with the last variadic arg (where the sequence is expected to be found) + template + void get_sequence(T& ret, const std::string& key, const std::string &subkey) + { + try + { + auto node = m_root[key]; + if (node.IsDefined()) + { + return get_sequence_from_node(ret, node[subkey]); + } + } + catch (const YAML::BadConversion& ex) + { + std::cerr << "Cannot read config file (" + m_path + "): wrong type at key " + key + "\n"; + throw; + } + } + private: YAML::Node m_root; }; @@ -184,6 +211,13 @@ class falco_configuration std::string m_webserver_k8s_audit_endpoint; bool m_webserver_ssl_enabled; std::string m_webserver_ssl_certificate; + std::set m_syscall_evt_drop_actions; + double m_syscall_evt_drop_rate; + double m_syscall_evt_drop_max_burst; + + // Only used for testing + bool m_syscall_evt_simulate_drops; + private: void init_cmdline_options(std::list &cmdline_options); diff --git a/userspace/falco/event_drops.cpp b/userspace/falco/event_drops.cpp new file mode 100644 index 00000000..99d29016 --- /dev/null +++ b/userspace/falco/event_drops.cpp @@ -0,0 +1,157 @@ +/* +Copyright (C) 2013-2019 Draios Inc dba Sysdig. + +This file is part of sysdig. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ + +#include "event_drops.h" + +syscall_evt_drop_mgr::syscall_evt_drop_mgr() + : m_num_syscall_evt_drops(0), + m_num_actions(0), + m_inspector(NULL), + m_outputs(NULL), + m_next_check_ts(0), + m_simulate_drops(false) +{ +} + +syscall_evt_drop_mgr::~syscall_evt_drop_mgr() +{ +} + +void syscall_evt_drop_mgr::init(sinsp *inspector, + falco_outputs *outputs, + std::set &actions, + double rate, + double max_tokens, + bool simulate_drops) +{ + m_inspector = inspector; + m_outputs = outputs; + m_actions = actions; + m_bucket.init(rate, max_tokens); + + m_inspector->get_capture_stats(&m_last_stats); + + m_simulate_drops = simulate_drops; +} + +bool syscall_evt_drop_mgr::process_event(sinsp_evt *evt) +{ + if(m_next_check_ts == 0) + { + m_next_check_ts = evt->get_ts() + ONE_SECOND_IN_NS; + } + + if(m_next_check_ts < evt->get_ts()) + { + scap_stats stats, delta; + + m_next_check_ts = evt->get_ts() + ONE_SECOND_IN_NS; + + m_inspector->get_capture_stats(&stats); + + // NOTE: only computing delta for interesting stats (evts/drops) + delta.n_evts = stats.n_evts - m_last_stats.n_evts; + delta.n_drops = stats.n_drops - m_last_stats.n_drops; + + m_last_stats = stats; + + if(m_simulate_drops) + { + falco_logger::log(LOG_INFO, "Simulating syscall event drop"); + delta.n_drops++; + } + + if(delta.n_drops > 0) + { + m_num_syscall_evt_drops++; + + // There were new drops in the last second. If + // the token bucket allows, perform actions. + if(m_bucket.claim(1, evt->get_ts())) + { + m_num_actions++; + + return perform_actions(evt->get_ts(), delta); + } + else + { + falco_logger::log(LOG_DEBUG, "Syscall event drop but token bucket depleted, skipping actions"); + } + } + } + + return true; +} + +void syscall_evt_drop_mgr::print_stats() +{ + fprintf(stderr, "Syscall event drop monitoring:\n"); + fprintf(stderr, " - event drop detected: %lu occurrences\n", m_num_syscall_evt_drops); + fprintf(stderr, " - num times actions taken: %lu\n", m_num_actions); +} + +bool syscall_evt_drop_mgr::perform_actions(uint64_t now, scap_stats &delta) +{ + std::string rule = "Falco internal: syscall event drop"; + std::string msg = rule + ". " + std::to_string(delta.n_drops) + " system calls dropped in last second."; + + std::map output_fields; + + output_fields["n_evts"] = std::to_string(delta.n_evts); + output_fields["n_drops"] = std::to_string(delta.n_drops); + bool should_exit = false; + + for(auto &act : m_actions) + { + switch(act) + { + case ACT_IGNORE: + break; + + case ACT_LOG: + falco_logger::log(LOG_ERR, msg); + break; + + case ACT_ALERT: + m_outputs->handle_msg(now, + falco_outputs::PRIORITY_CRITICAL, + msg, + rule, + output_fields); + break; + + case ACT_EXIT: + should_exit = true; + break; + + default: + falco_logger::log(LOG_ERR, "Ignoring unknown action " + std::to_string(int(act))); + break; + } + } + + if(should_exit) + { + falco_logger::log(LOG_CRIT, msg); + falco_logger::log(LOG_CRIT, "Exiting."); + return false; + } + + return true; +} diff --git a/userspace/falco/event_drops.h b/userspace/falco/event_drops.h new file mode 100644 index 00000000..96549c6b --- /dev/null +++ b/userspace/falco/event_drops.h @@ -0,0 +1,78 @@ +/* +Copyright (C) 2016-2019 Draios Inc dba Sysdig. + +This file is part of falco. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +*/ +#pragma once + +#include + +#include +#include + +#include "logger.h" +#include "falco_outputs.h" + +class syscall_evt_drop_mgr +{ +public: + + // The possible actions that this class can take upon + // detecting a syscall event drop. + enum action + { + ACT_IGNORE = 0, + ACT_LOG, + ACT_ALERT, + ACT_EXIT, + }; + + syscall_evt_drop_mgr(); + virtual ~syscall_evt_drop_mgr(); + + void init(sinsp *inspector, + falco_outputs *outputs, + std::set &actions, + double rate, + double max_tokens, + bool simulate_drops); + + // Call this for every event. The class will take care of + // periodically measuring the scap stats, looking for syscall + // event drops, and performing any actions. + // + // Returns whether event processing should continue or stop (with an error). + bool process_event(sinsp_evt *evt); + + void print_stats(); + +protected: + + // Perform all configured actions. + bool perform_actions(uint64_t now, scap_stats &delta); + + uint64_t m_num_syscall_evt_drops; + uint64_t m_num_actions; + sinsp *m_inspector; + falco_outputs *m_outputs; + std::set m_actions; + token_bucket m_bucket; + uint64_t m_next_check_ts; + scap_stats m_last_stats; + bool m_simulate_drops; +}; + + diff --git a/userspace/falco/falco.cpp b/userspace/falco/falco.cpp index 88ebbc8d..8341146f 100644 --- a/userspace/falco/falco.cpp +++ b/userspace/falco/falco.cpp @@ -40,6 +40,7 @@ limitations under the License. #include "chisel.h" #include "sysdig.h" +#include "event_drops.h" #include "configuration.h" #include "falco_engine.h" #include "config_falco.h" @@ -132,6 +133,9 @@ static void usage() " from multiple files/directories.\n" " -s If specified, write statistics related to falco's reading/processing of events\n" " to this file. (Only useful in live mode).\n" + " --stats_interval When using -s , write statistics every ms.\n" + " (This uses signals, so don't recommend intervals below 200 ms)\n" + " defaults to 5000 (5 seconds)\n" " -S , --snaplen=\n" " Capture the first bytes of each I/O buffer.\n" " By default, the first 80 bytes are captured. Use this\n" @@ -215,9 +219,13 @@ static std::string read_file(std::string filename) uint64_t do_inspect(falco_engine *engine, falco_outputs *outputs, sinsp* inspector, + falco_configuration &config, + syscall_evt_drop_mgr &sdropmgr, uint64_t duration_to_tot_ns, string &stats_filename, - bool all_events) + uint64_t stats_interval, + bool all_events, + int &result) { uint64_t num_evts = 0; int32_t rc; @@ -225,11 +233,18 @@ uint64_t do_inspect(falco_engine *engine, StatsFileWriter writer; uint64_t duration_start = 0; + sdropmgr.init(inspector, + outputs, + config.m_syscall_evt_drop_actions, + config.m_syscall_evt_drop_rate, + config.m_syscall_evt_drop_max_burst, + config.m_syscall_evt_simulate_drops); + if (stats_filename != "") { string errstr; - if (!writer.init(inspector, stats_filename, 5, errstr)) + if (!writer.init(inspector, stats_filename, stats_interval, errstr)) { throw falco_exception(errstr); } @@ -285,6 +300,12 @@ uint64_t do_inspect(falco_engine *engine, } } + if(!sdropmgr.process_event(ev)) + { + result = EXIT_FAILURE; + break; + } + if(!ev->falco_consider() && !all_events) { continue; @@ -375,6 +396,7 @@ int falco_init(int argc, char **argv) sinsp_evt::param_fmt event_buffer_format = sinsp_evt::PF_NORMAL; falco_engine *engine = NULL; falco_outputs *outputs = NULL; + syscall_evt_drop_mgr sdropmgr; int op; int long_index = 0; string trace_filename; @@ -388,6 +410,7 @@ int falco_init(int argc, char **argv) string describe_rule = ""; list validate_rules_filenames; string stats_filename = ""; + uint64_t stats_interval = 5000; bool verbose = false; bool names_only = false; bool all_events = false; @@ -432,6 +455,7 @@ int falco_init(int argc, char **argv) {"print", required_argument, 0, 'p' }, {"pidfile", required_argument, 0, 'P' }, {"snaplen", required_argument, 0, 'S' }, + {"stats_interval", required_argument, 0}, {"support", no_argument, 0}, {"unbuffered", no_argument, 0, 'U' }, {"version", no_argument, 0, 0 }, @@ -588,6 +612,10 @@ int falco_init(int argc, char **argv) list_flds_source = optarg; } } + else if (string(long_options[long_index].name) == "stats_interval") + { + stats_interval = atoi(optarg); + } else if (string(long_options[long_index].name) == "support") { print_support = true; @@ -1061,9 +1089,13 @@ int falco_init(int argc, char **argv) num_evts = do_inspect(engine, outputs, inspector, + config, + sdropmgr, uint64_t(duration_to_tot*ONE_SECOND_IN_NS), stats_filename, - all_events); + stats_interval, + all_events, + result); duration = ((double)clock()) / CLOCKS_PER_SEC - duration; @@ -1085,6 +1117,7 @@ int falco_init(int argc, char **argv) inspector->close(); engine->print_stats(); + sdropmgr.print_stats(); webserver.stop(); } catch(exception &e) diff --git a/userspace/falco/falco_outputs.cpp b/userspace/falco/falco_outputs.cpp index c348b525..fa444da6 100644 --- a/userspace/falco/falco_outputs.cpp +++ b/userspace/falco/falco_outputs.cpp @@ -36,7 +36,8 @@ const static struct luaL_reg ll_falco_outputs [] = falco_outputs::falco_outputs(falco_engine *engine) : m_falco_engine(engine), m_initialized(false), - m_buffered(true) + m_buffered(true), + m_json_output(false) { } @@ -77,6 +78,8 @@ void falco_outputs::init(bool json_output, throw falco_exception("No inspector provided"); } + m_json_output = json_output; + falco_common::init(m_lua_main_filename.c_str(), FALCO_SOURCE_LUA_DIR); // Note that falco_formats is added to both the lua state used @@ -162,6 +165,81 @@ void falco_outputs::handle_event(gen_event *ev, string &rule, string &source, } +void falco_outputs::handle_msg(uint64_t now, + falco_common::priority_type priority, + std::string &msg, + std::string &rule, + std::map &output_fields) +{ + std::string full_msg; + + if(m_json_output) + { + nlohmann::json jmsg; + + // Convert the time-as-nanoseconds to a more json-friendly ISO8601. + time_t evttime = now/1000000000; + char time_sec[20]; // sizeof "YYYY-MM-DDTHH:MM:SS" + char time_ns[12]; // sizeof ".sssssssssZ" + string iso8601evttime; + + strftime(time_sec, sizeof(time_sec), "%FT%T", gmtime(&evttime)); + snprintf(time_ns, sizeof(time_ns), ".%09luZ", now % 1000000000); + iso8601evttime = time_sec; + iso8601evttime += time_ns; + + jmsg["output"] = msg; + jmsg["priority"] = "Critical"; + jmsg["rule"] = rule; + jmsg["time"] = iso8601evttime; + jmsg["output_fields"] = output_fields; + + full_msg = jmsg.dump(); + } + else + { + std::string timestr; + bool first = true; + + sinsp_utils::ts_to_string(now, ×tr, false, true); + full_msg = timestr + ": " + falco_common::priority_names[LOG_CRIT] + " " + msg + "("; + for(auto &pair : output_fields) + { + if(first) + { + first = false; + } + else + { + full_msg += " "; + } + full_msg += pair.first + "=" + pair.second; + } + full_msg += ")"; + } + + lua_getglobal(m_ls, m_lua_output_msg.c_str()); + + if(lua_isfunction(m_ls, -1)) + { + lua_pushstring(m_ls, full_msg.c_str()); + lua_pushstring(m_ls, falco_common::priority_names[priority].c_str()); + lua_pushnumber(m_ls, priority); + + if(lua_pcall(m_ls, 3, 0, 0) != 0) + { + const char* lerr = lua_tostring(m_ls, -1); + string err = "Error invoking function output: " + string(lerr); + throw falco_exception(err); + } + } + else + { + throw falco_exception("No function " + m_lua_output_msg + " found in lua compiler module"); + } + +} + void falco_outputs::reopen_outputs() { lua_getglobal(m_ls, m_lua_output_reopen.c_str()); @@ -215,4 +293,4 @@ int falco_outputs::handle_http(lua_State *ls) slist1 = NULL; } return 1; -} \ No newline at end of file +} diff --git a/userspace/falco/falco_outputs.h b/userspace/falco/falco_outputs.h index 888c0183..51be58a2 100644 --- a/userspace/falco/falco_outputs.h +++ b/userspace/falco/falco_outputs.h @@ -20,6 +20,7 @@ limitations under the License. #pragma once #include +#include extern "C" { #include "lua.h" @@ -66,6 +67,13 @@ public: void handle_event(gen_event *ev, std::string &rule, std::string &source, falco_common::priority_type priority, std::string &format); + // Send a generic message to all outputs. Not necessarily associated with any event. + void handle_msg(uint64_t now, + falco_common::priority_type priority, + std::string &msg, + std::string &rule, + std::map &output_fields); + void reopen_outputs(); static int handle_http(lua_State *ls); @@ -81,8 +89,11 @@ private: bool m_buffered; + bool m_json_output; + std::string m_lua_add_output = "add_output"; std::string m_lua_output_event = "output_event"; + std::string m_lua_output_msg = "output_msg"; std::string m_lua_output_cleanup = "output_cleanup"; std::string m_lua_output_reopen = "output_reopen"; std::string m_lua_main_filename = "output.lua"; diff --git a/userspace/falco/logger.cpp b/userspace/falco/logger.cpp index b4bd86ce..1442b085 100644 --- a/userspace/falco/logger.cpp +++ b/userspace/falco/logger.cpp @@ -93,22 +93,39 @@ int falco_logger::syslog(lua_State *ls) { bool falco_logger::log_stderr = true; bool falco_logger::log_syslog = true; -void falco_logger::log(int priority, const string msg) { +void falco_logger::log(int priority, const string msg) +{ if(priority > falco_logger::level) { return; } - if (falco_logger::log_syslog) { - ::syslog(priority, "%s", msg.c_str()); + string copy = msg; + + if (falco_logger::log_syslog) + { + // Syslog output should not have any trailing newline + if(copy.back() == '\n') + { + copy.pop_back(); + } + + ::syslog(priority, "%s", copy.c_str()); } - if (falco_logger::log_stderr) { + if (falco_logger::log_stderr) + { + // log output should always have a trailing newline + if(copy.back() != '\n') + { + copy.push_back('\n'); + } + std::time_t result = std::time(nullptr); string tstr = std::asctime(std::localtime(&result)); tstr = tstr.substr(0, 24);// remove trailling newline - fprintf(stderr, "%s: %s", tstr.c_str(), msg.c_str()); + fprintf(stderr, "%s: %s", tstr.c_str(), copy.c_str()); } } diff --git a/userspace/falco/lua/output.lua b/userspace/falco/lua/output.lua index a5f65e6f..60092165 100644 --- a/userspace/falco/lua/output.lua +++ b/userspace/falco/lua/output.lua @@ -172,6 +172,12 @@ function output_event(event, rule, source, priority, priority_num, format) end end +function output_msg(msg, priority, priority_num) + for index,o in ipairs(outputs) do + o.output(priority, priority_num, msg, o.options) + end +end + function output_cleanup() formats.free_formatters() for index,o in ipairs(outputs) do diff --git a/userspace/falco/statsfilewriter.cpp b/userspace/falco/statsfilewriter.cpp index 971df5ce..98ca8ff5 100644 --- a/userspace/falco/statsfilewriter.cpp +++ b/userspace/falco/statsfilewriter.cpp @@ -42,7 +42,7 @@ StatsFileWriter::~StatsFileWriter() m_output.close(); } -bool StatsFileWriter::init(sinsp *inspector, string &filename, uint32_t interval_sec, string &errstr) +bool StatsFileWriter::init(sinsp *inspector, string &filename, uint32_t interval_msec, string &errstr) { struct itimerval timer; struct sigaction handler; @@ -60,8 +60,8 @@ bool StatsFileWriter::init(sinsp *inspector, string &filename, uint32_t interval return false; } - timer.it_value.tv_sec = interval_sec; - timer.it_value.tv_usec = 0; + timer.it_value.tv_sec = 0; + timer.it_value.tv_usec = interval_msec * 1000; timer.it_interval = timer.it_value; if (setitimer(ITIMER_REAL, &timer, NULL) == -1) { diff --git a/userspace/falco/statsfilewriter.h b/userspace/falco/statsfilewriter.h index 1227ffdb..dc35b0f6 100644 --- a/userspace/falco/statsfilewriter.h +++ b/userspace/falco/statsfilewriter.h @@ -35,7 +35,7 @@ public: // Returns success as bool. On false fills in errstr. bool init(sinsp *inspector, std::string &filename, - uint32_t interval_sec, + uint32_t interval_msec, string &errstr); // Should be called often (like for each event in a sinsp