From c6e6462e2f4561d786bb108dbf1c031d36293869 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Thu, 24 Jun 2021 00:25:31 +0300 Subject: [PATCH] [Security Solution] [Cases] Swimlane Connector for Cases (#100086) (#103165) Co-authored-by: Josh Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Christos Nasikas Co-authored-by: Jonathan Buttner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Steph Milovic Co-authored-by: Josh Co-authored-by: Xavier Mouligneau <189600+XavierM@users.noreply.github.com> Co-authored-by: Jonathan Buttner Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com> --- docs/management/action-types.asciidoc | 4 + .../connectors/action-types/swimlane.asciidoc | 105 ++++ .../connectors/images/swimlane-connector.png | Bin 0 -> 74730 bytes .../images/swimlane-params-test.png | Bin 0 -> 175258 bytes docs/management/connectors/index.asciidoc | 1 + x-pack/plugins/actions/README.md | 145 ++++-- .../server/builtin_action_types/index.test.ts | 1 + .../server/builtin_action_types/index.ts | 2 + .../server/builtin_action_types/jira/index.ts | 4 +- .../builtin_action_types/jira/schema.ts | 8 - .../builtin_action_types/jira/service.test.ts | 12 +- .../server/builtin_action_types/jira/types.ts | 10 +- .../builtin_action_types/resilient/schema.ts | 8 - .../builtin_action_types/servicenow/schema.ts | 8 - .../builtin_action_types/swimlane/api.test.ts | 142 ++++++ .../builtin_action_types/swimlane/api.ts | 60 +++ .../swimlane/helpers.test.ts | 90 ++++ .../builtin_action_types/swimlane/helpers.ts | 58 +++ .../builtin_action_types/swimlane/index.ts | 116 +++++ .../builtin_action_types/swimlane/mocks.ts | 124 +++++ .../builtin_action_types/swimlane/schema.ts | 75 +++ .../swimlane/service.test.ts | 434 ++++++++++++++++ .../builtin_action_types/swimlane/service.ts | 196 +++++++ .../swimlane/translations.ts | 20 + .../builtin_action_types/swimlane/types.ts | 123 +++++ .../swimlane/validators.ts | 28 + x-pack/plugins/actions/server/index.ts | 1 - x-pack/plugins/actions/server/types.ts | 2 +- .../server/usage/actions_usage_collector.ts | 1 + x-pack/plugins/cases/README.md | 2 +- .../cases/common/api/connectors/index.ts | 14 +- .../cases/common/api/connectors/mappings.ts | 7 +- .../cases/common/api/connectors/swimlane.ts | 21 + x-pack/plugins/cases/common/constants.ts | 16 +- .../cases/public/common/shared_imports.ts | 2 + .../components/case_view/index.test.tsx | 11 +- .../public/components/case_view/index.tsx | 7 +- .../components/configure_cases/index.tsx | 8 +- .../components/configure_cases/utils.ts | 13 +- .../components/connector_selector/form.tsx | 12 +- .../components/connectors/fields_form.tsx | 3 +- .../public/components/connectors/index.ts | 3 + .../components/connectors/jira/index.ts | 4 +- .../public/components/connectors/mock.ts | 18 + .../components/connectors/resilient/index.ts | 4 +- .../components/connectors/servicenow/index.ts | 10 +- .../connectors/swimlane/case_fields.test.tsx | 53 ++ .../connectors/swimlane/case_fields.tsx | 48 ++ .../components/connectors/swimlane/index.ts | 25 + .../connectors/swimlane/translations.ts | 42 ++ .../connectors/swimlane/validator.test.ts | 60 +++ .../connectors/swimlane/validator.ts | 39 ++ .../public/components/connectors/types.ts | 3 +- .../components/create/connector.test.tsx | 52 +- .../public/components/create/connector.tsx | 53 +- .../public/components/create/form.test.tsx | 6 + .../public/components/create/form_context.tsx | 33 +- .../cases/public/components/create/schema.tsx | 4 +- .../components/edit_connector/index.tsx | 10 +- .../plugins/cases/public/components/types.ts | 10 + .../plugins/cases/public/components/utils.ts | 43 ++ .../containers/use_get_action_license.tsx | 3 +- .../plugins/cases/server/client/cases/get.ts | 1 - .../cases/server/client/cases/utils.ts | 1 + .../server/connectors/case/index.test.ts | 14 +- .../cases/server/connectors/case/schema.ts | 26 +- .../server/connectors/case/validators.ts | 3 +- .../cases/server/connectors/factory.ts | 4 +- .../server/connectors/swimlane/format.test.ts | 21 + .../server/connectors/swimlane/format.ts | 15 + .../cases/server/connectors/swimlane/index.ts | 15 + .../server/connectors/swimlane/mapping.ts | 28 + .../cases/server/connectors/swimlane/types.ts | 13 + .../security_solution/common/constants.ts | 1 + .../schema/xpack_plugins.json | 6 + .../components/builtin_action_types/index.ts | 2 + .../jira/jira_connectors.test.tsx | 2 +- .../builtin_action_types/jira/jira_params.tsx | 6 + .../resilient/resilient_connectors.test.tsx | 2 +- .../resilient/resilient_params.tsx | 1 + .../servicenow/servicenow_connectors.test.tsx | 2 +- .../builtin_action_types/swimlane/api.test.ts | 145 ++++++ .../builtin_action_types/swimlane/api.ts | 65 +++ .../builtin_action_types/swimlane/helpers.ts | 62 +++ .../builtin_action_types/swimlane/index.ts | 8 + .../builtin_action_types/swimlane/logo.tsx | 53 ++ .../builtin_action_types/swimlane/mocks.ts | 61 +++ .../swimlane/steps/index.ts | 9 + .../swimlane/steps/swimlane_connection.tsx | 201 ++++++++ .../swimlane/steps/swimlane_fields.tsx | 313 ++++++++++++ .../swimlane/swimlane.test.tsx | 219 ++++++++ .../swimlane/swimlane.tsx | 106 ++++ .../swimlane/swimlane_connectors.test.tsx | 319 ++++++++++++ .../swimlane/swimlane_connectors.tsx | 103 ++++ .../swimlane/swimlane_params.test.tsx | 137 +++++ .../swimlane/swimlane_params.tsx | 159 ++++++ .../swimlane/translations.ts | 282 ++++++++++ .../builtin_action_types/swimlane/types.ts | 56 ++ .../swimlane/use_get_application.test.tsx | 180 +++++++ .../swimlane/use_get_application.tsx | 82 +++ .../actions/builtin_action_types/swimlane.ts | 91 ++++ .../basic/tests/actions/index.ts | 1 + .../alerting_api_integration/common/config.ts | 1 + .../actions_simulators/server/plugin.ts | 6 + .../server/swimlane_simulation.ts | 39 ++ .../actions/builtin_action_types/swimlane.ts | 482 ++++++++++++++++++ .../tests/actions/index.ts | 1 + .../case_api_integration/common/config.ts | 1 + .../common/config.ts | 1 + x-pack/test/functional_with_es_ssl/config.ts | 1 + 110 files changed, 5531 insertions(+), 233 deletions(-) create mode 100644 docs/management/connectors/action-types/swimlane.asciidoc create mode 100644 docs/management/connectors/images/swimlane-connector.png create mode 100644 docs/management/connectors/images/swimlane-params-test.png create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts create mode 100644 x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts create mode 100644 x-pack/plugins/cases/common/api/connectors/swimlane.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/index.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts create mode 100644 x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts create mode 100644 x-pack/plugins/cases/public/components/types.ts create mode 100644 x-pack/plugins/cases/public/components/utils.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/format.test.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/format.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/index.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/mapping.ts create mode 100644 x-pack/plugins/cases/server/connectors/swimlane/types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx create mode 100644 x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx create mode 100644 x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts create mode 100644 x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts create mode 100644 x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts diff --git a/docs/management/action-types.asciidoc b/docs/management/action-types.asciidoc index 65b600d4b7281f..3d3d7aeb2d777e 100644 --- a/docs/management/action-types.asciidoc +++ b/docs/management/action-types.asciidoc @@ -43,6 +43,10 @@ a| <> | Send a message to a Slack channel or user. +a| <> + +| Create an incident in Swimlane. + a| <> | Send a request to a web service. diff --git a/docs/management/connectors/action-types/swimlane.asciidoc b/docs/management/connectors/action-types/swimlane.asciidoc new file mode 100644 index 00000000000000..88447bb496a860 --- /dev/null +++ b/docs/management/connectors/action-types/swimlane.asciidoc @@ -0,0 +1,105 @@ +[role="xpack"] +[[swimlane-action-type]] +=== Swimlane connector and action +++++ +Swimlane +++++ + +The Swimlane connector uses the https://swimlane.com/knowledge-center/docs/developer-guide/rest-api/[Swimlane REST API] to create Swimlane records. + +[float] +[[swimlane-connector-configuration]] +==== Connector configuration + +Swimlane connectors have the following configuration properties. + +Name:: The name of the connector. The name is used to identify a connector in the **Stack Management** UI connector listing, and in the connector list when configuring an action. +URL:: Swimlane instance URL. +Application ID:: Swimlane application ID. +API token:: Swimlane API authentication token for HTTP Basic authentication. + +[float] +[[Preconfigured-swimlane-configuration]] +==== Preconfigured connector type + +[source,text] +-- + my-swimlane: + name: preconfigured-swimlane-connector-type + actionTypeId: .swimlane + config: + apiUrl: https://elastic.swimlaneurl.us + appId: app-id + mappings: + alertIdConfig: + fieldType: text + id: agp4s + key: alert-id + name: Alert ID + caseIdConfig: + fieldType: text + id: ae1mi + key: case-id + name: Case ID + caseNameConfig: + fieldType: text + id: anxnr + key: case-name + name: Case Name + commentsConfig: + fieldType: comments + id: au18d + key: comments + name: Comments + descriptionConfig: + fieldType: text + id: ae1gd + key: description + name: Description + ruleNameConfig: + fieldType: text + id: avfsl + key: rule-name + name: Rule Name + severityConfig: + fieldType: text + id: a71ik + key: severity + name: severity + secrets: + apiToken: tokenkeystorevalue +-- + +Config defines information for the connector type. + +`apiUrl`:: An address that corresponds to *URL*. +`appId`:: A key that corresponds to *Application ID*. + +Secrets defines sensitive information for the connector type. + +`apiToken`:: A string that corresponds to *API Token*. Should be stored in the <>. + +[float] +[[define-swimlane-ui]] +==== Define connector in Stack Management + +Define Swimlane connector properties. + +[role="screenshot"] +image::management/connectors/images/swimlane-connector.png[Swimlane connector] + +Test Swimlane action parameters. + +[role="screenshot"] +image::management/connectors/images/swimlane-params-test.png[Swimlane params test] + +[float] +[[swimlane-action-configuration]] +==== Action configuration + +Swimlane actions have the following configuration properties. + +Comments:: Additional information for the client, such as how to troubleshoot the issue. +Severity:: The severity of the incident. + +NOTE: Alert ID and Rule Name are filled automatically. Specifically, Alert ID is set to `{{alert.id}}` and Rule Name to `{{rule.name}}`. \ No newline at end of file diff --git a/docs/management/connectors/images/swimlane-connector.png b/docs/management/connectors/images/swimlane-connector.png new file mode 100644 index 0000000000000000000000000000000000000000..520c35d00381bd8d21a0accff1ba56cc1145ffc4 GIT binary patch literal 74730 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 literal 0 HcmV?d00001 diff --git a/docs/management/connectors/images/swimlane-params-test.png b/docs/management/connectors/images/swimlane-params-test.png new file mode 100644 index 0000000000000000000000000000000000000000..c0e02c2c7b18f2fa4118e10d31d0a7c6447d4c19 GIT binary patch literal 175258 zcma&O1z1$=w+BiybO?w;4PDYA-O@@U4IbW5p#O_uW z_b>K3e^41(Leb}iiad&ByJXcSUNwX|luCY+fB_|+R`oT3N&Zl?vsU3NWR)GcU*T8= z-iW5gq?w8(8SRP{%2bI_B>~>MoiE1-@31tLBy1ousm(nKAF2;464qImk))LD0?WlE z23Hed3U68(((3N*)b7MzcE2v5*C`O6$o{+A1?{u?X9=pVHJJwi>60c(CKZ1CR`BaQ zIWSi7SAl}1!tY)3$XUYs*has9)0v{xsQgwLCxZGy_C=h6c2Ax>ITURN5kE5NYBrLl zQ!KLecMZ!BorhQ!O(^V(B0T#9nv@uj8p-iTpO>$M49e8FxwLUn3VXhks?puOD6)qX zk&VN;$Y4vaJ%q_M%U^Z8LiP`$jiilaKzY-}63GtX>9WQCq4YW(*?;$|5J4`lc2pDP zsFedm{ew*1|Y6Mf&4w$~`4-|=w|2A)%i(d_spE!rR+2ipB&MEPBiAtV4Z zHM#k#<`AAVt-D5V0Dd_GE;dD5($l)$_N7eJYJ2Xkjh&EXwXhZXl-B#mRB#COMr&K{ zX@rr~@d|dV%d-E}Y$vAy|0Oae+9U*y*@v(amHCA}th||u$Mf6FT_jjd2)vpg6rpm$ zFDzPEzK_oDp!^qQC14-~;OD%9^ic3}4+awX_1vF&eda&;U~w(duJU5S+b&sMHPal) zmFpkEnZ9}W-0vTTc+%(pZs_6Q%y-%fj5qow|JCuY&u{yLq(9QI_~8qol20!MI796R zinp3!nio}b?6Yx3Se^T2`@L=yj-lUT0j{aRrT@(JRCSK=?Hhk)+z-M3<0t(`*pGbG}slV;AS zF7l(-H`g?FefW7CZaEtrm%9gf1A!MgBn%~OlhC`b98q~f*mcjmgNWL|KT#%i;V}w} zsA2QXnAL9u`MCxoHc5`E^S%a4b$>Ve&XXiSe{&2!a*Rt5!KcQmMc0kJ&Zc&_2l-(8 zn>QQlt%{M#b10kx@Y>1_gWe+1C+2>S5e9F!F)uYQFRu{}tx;5{^NZ#^ucO5NIE0j; zGAPcnFzR?b8i(p-VQ*Qachw`P-bg>8Dy{j8nx9?zKroTXgkDO(<$fS5D8($Yqy36( z_{k*|^u-(Z0|O0WV)Of-&vv12g!qhMI+|&%i8T=>erZ*^ayLs4qRspA!wE@%(9*~e zKGS~m=Y$k}=D_y$BO8+7jSkKrSNn_~Ma~ulFM(8wOe}y*ih|*RIK5W{Z0;P zFU{R)wZjj7?;Jk6RK6vh5A&iN1c|{Req!|zDc53xC3Yn1B*i6FB#1J! z*&Wyw*;Oi-D_*geOuJVoRA`xsj~3B=MFBgBKQ?3-X2FdFucbS?BQlps{LH8ij!04ads#e1IJ?w=Uft!KWc@YikEZfC_iUaTL$ ze&AL7f>${l)mEPgrU}AXzMEDL9cJiGSkEnawz{;21Rjuhc7WJRIf6M7bj!y!Kgmaw z1ld%V{B~|QvRPq#79bfZS=j}SA*V{pvLAs`jM@T^q-QaGb$yG+7O&RM`)l_#8*2!2 zOriyg8T$@f6^kruidEypZ6kbJ%M97b$1u?l(W=bcHRGOdaqMwU1)cBb-wWqEyw5J= zSDjJKR%I$Js`9iCmxDn|ID4-aa_6_kBzmGu$W_95`Hu6; z0_}=UUT@Sfax+FTo@jx!=Buacjy35ui))5vTV~g**{d5Iovfc%TiG|Q30>gtm2{@J zt%PL9XOEVRmyPrc)6eA(?_^aKIeouSIAG(4OZkw(#s7|<#DmY{#N*=Ter;njex<7C z;EEaOdm>9Xt2EfFuVwIq^*u;j7m$t`U| zxp-yRvw4Q;ZQrj3(U>X;E4hrO8{Zh4*smDd+plhpr607Zwtd#y))RV3?dY(3$C+47 zYts}#8XiQT$g*xyFMZp4If8WRCy7*tJx`55%_v)+sGmq7YbrVZmN-Q->6Vm|!a!N- zW-?08D2y+~^edf|g#>vfWwwRv&Wq>VmV4FVN5m^3?F(N&u<1_wl|4hFUYFtJm9)cl z8uqalc9ps=Ivto*wZyHW_KlJMHq@tgGJd3e?0ac;nFQnYi|$w1+lOA|Zv$nwW$%p~ zkYW8ZTNtr4lo(x;h@u*3Z$xCw`@_HA&wiP0a4~h+atZv+y-au%)I}AO{mwFmQzDpR zPtiTQAZeH(EZQ=jv=pf{z+LN7-b=`m{z3BoCnPoXN2-(Q@hI#_d3I2?)>lgbkFHZA ze_j8k-e#ji!}8u&iCk9et!f2o8iwR$Y=%|6C11;|WjbaLYj`%~*VlUMI2T;*^R5o? zGF}>M8rNIzw{!^jKPV|?$Wt)X)^u78Wwl*?99kZyw3Hrax>E0D;#XPLR8T*5p|#qo zNzw9Tys7b0TQ417OYY$)f)rsESC_C_uk|ZW=Oe4|Y4f={%z@Vvme~)+Wz^j&M;q)% zxVyQpYR^kIO9Rg0c2o);G#OtuX=7=YR1N3YO0ET>-J+?yMzAQD)349fAIa|f(N|UJ zXO){@T0}P|HLL#fY^!qBBHQceKGU^s={$N*$MLgM`9bJ{%a7^&TG#lYqBVBou=*^! zi_5wA3%^~H9EXCr()f}peUeG)g^T5hQyr__i0K8}<{QC1tM^UH#2F+ni$u1wCLAVE zc4i0^2&U0RiA_yT&3G^CMhA+fhrM!iB5u7%cBm3%huGx39jp7bXtGX!rmGQnKju}9r_?nX3ZqAZE@pw3G z%#+X3Hx@fIo%#&<<_YZHPtNR5d0y&vwxUE}6VG|{y4@ZJ?RIU1`^%chN~e6|&-dKF zuFqvFI5`fnVdB=7(mr>0x&0*uit*#K336 zhcUm=aBw3xYc^PSl77)KlA^%Rx087N?X+e$WG=qFSYGGgY~$=gx>{ksIU5RrAmJNQ zb0QJcpaOJis~v&C_v4qGkk7}r+=7b&CCBxgkYuRQmc(VYg%kmUJmhs9@R4 z-w^NG8Z`FUPS!ci*v6bgiZ`2k2VQ2VP`Oy%OvCs0jc9jwI+efltzZXvMO_e-L0HKz zUI)@_o^OAVI`pL($X@6>>fn*nUqtt;e?URcjhLI((9{C_k6;5;NyB&Vpy+^W5EL9V z7z!S^f(G9F&;He?4Mv#z&_5P7WesWorO=&B)BiOvaB4fk1d|^$ocd-iZCDIq(-B8335p z+)PYPPEL$YY>Za6MocVRTwF}dtW2z|48Ri%b}p9oy3P!icI5x=nO)!vv`0DA_E!Oz0M!O8ot2LGR1|2yRWX{!2vnsPET|KFzn=hpwbsj{7ct%#Kc zFsMEM{}Sv!jsNe>|1{)f`m^@`vlsu~=zskR>@+_zFVp`}jUSoE#Q|`Pp$JXh$SMKf z0Gs`J!MXq+H2?kvuAyahzLS{ELO}^aNxl(Ma)#cWbDEVoBk4FA@K|{5bgpePV<0B1 z?-w8;rf*Pe9xO+p@d8&k`dbnaE7?xA+?R$6s>GYdyZ(z!!j0M@JFN@ti97Cy!2YZ7 zvgD0nl7<`4CjPo}Vz-y(Ac!{<%o87rV=$7~)HpgiI!C0C)9r)a9@3{9q@qwrC#Wvm z5c0dLk_cU19*y|;{rdIGH9b!{ngU7xiJ|`0FD44SySqDKayLzW!o>ARep|zZB3?$lx zfT7cBtFf@LF>BMah@YAe(973n*60XhF{-da8>L_&d>kAK{&+^M;nL=8#gM#kG0vRx zt{7?+<-bw|Qv}9QH}y>Z`%Iw$`Kwo1;IkUrwQfDV2BErCX3=1raWlKnC&>!zb;+A% zFdR5yRnrK;cRIlj37Y~lr6-;#(F`*$=DUb6jIeJs-&33cbiqQsNO;R75TsQnpM6e8 zs054XQKa3FXYffjjllcgXh|Cw-z=XQH&ZV}0~T-Y+m-mQ-xFD}Tpd zXA%Mz3N?5Y^?j$W0#vM3XILa;?B_e)%Nr6K^2#!Dog)t9; z5DVR4gy~8Cwvyl9y|K>yPvQ2jb0g(2K!rZ3XR7dlCL>vBTi4~}Pm8d(;7d&F=csw& ziX$O;w*wp3XFKa)KY%;Iq4NG)!^kQUCAFWu}2f4kl z4}+XCDf|sKjBZZ|?!UzPGc1f4jj@MP?8B4{izyLbPB@!>=LfzBy}vf}hjC0xJ!7#D z2bjp0PLu*tUTX6fx(S}5_~G39uLp&iUC!3KDE2^;NXyAZ<}_UEy=BWgf5B#m+IP0; z`&Ws2+YEvq)5|543nLqa6*l8EhVv7XkPyW0X(WsIOKWdLXfqn)y{uya544t@iDI*F zi!o8YVw}cv&oI@e{w4zuM^H5i%p70Rb}<6_^=!3GD<$NW7)G8)*4uBhTE4D--86C- z)5z3mLKtCO5O^t-B{FE|eoMemG znA2gzN)7-Dhm#lo)+751GkPa^BO{KX{xG!)huEWo!iN0d{2cv8|DuL4dEi5%KK09P z{6t8z<1}V?hRHo5{}&{IT*J@8d+B;$rzCP%@mC^zqT_niZJI(U9rsf^=KinH(F13m zjnCd9RKbyjPYDS%& zBC7*H-7-uC=x&^3?hCq z%3$2spl9vBK@l%7!9EaZ-B5Q#%it%a2(t%Xsd=JEWWw6tZ`_*MDjDhXBgX17+2(3>vS~1^$YBm$J=@68HqILT8hV&}l5LgCJxi(3QtuFndwEnUO-NVa92f zDiEC{s@?@gfBBMS`5>j`PV`4jTL<^2K`sZzA3@{6%d_HG|RfqI^wx+*geYP?5&U zYC<7OzfzAjj#fI}vgCc4e42QeKe4RQ5vjvT(l1OW(##4_kd)6iR#r|Z05nAb`pxt4 z9$ep1@PCqry}d=783|7>WYaF>EJ|1?l(xlmGnQVZe5K#BLMS#@uXowU*mv`K^jjcU-S&WukH0hL8rNhEZ zfDk@y@w~|w{Hz!Xco}Fc6cE%Ir<(LIyxp>+G{T1a$)z_i!iea|PlNi<%hXZe_9!{1gOm`ZSW{sV+`lpp6oq%M=h@y96oi`Y4HEQJsU)6Db>Lh5cq+Vk2l{wyeRbdb=*Ol zPLdQf&K7>!a2B*j%DElr=-Al8Ne6Z?JLSha+S<$14O-EpVllzZm%=Ze-UM!VCBdB6 z9&l+T`>7`&VO%i(&2E{i_&w>5ae`a-9`dUl1?#U|AW-YucBH4tDGyn+yE-;tcqvLGyId?5TZo(Z0UzAA=tBLY@r4mb0s!>$fhCo+k~ zSuFfCCJ{k=Qhu4IEt=18(0mk1&}mr6fahEQ2C$e?rARZ%I=|Wli9K_Z!lL1qPI#aN zK$y~So66RBMVScElQ;k=LI$WfF`;tG4hoa^ zHMQakFNR*4m;U9->8wE@gs-Ndi2-`JGHNQ&Xksbl?A`f7clN1X=~}dm@CBkku{L=i z21!N3=uWY646h3e;WQqj=F6nIGwU2S^C}C{+rqTF*2ozS_709hbB!Rt=6kms&^Wl*I@)n`DdkCixWTP z6{pItIA-o~yPICx8NREsNXLGP_dc7!SZ3pg6+@>bDA><@Tco)Phx0oxC?SQo?}a6l zIxXv?++JBBZwq;jDvl&9oOU~-@>Mhy0Bf3 z()TR#(3A=GLAD|mnk-YX`o`vzTxi-*>!9BoPp&mRx)5Q`MU+if;W%At;>&wuE@Ot< z0s1Wo?YU!fTy`}0h@5yAdS6>Lnx@9|{1MekZLB==)B2S9i|=NK95)3E7iw0TlgM0b)f!A|EC$7mmjy+Cg}`4Hx*c__ zjs3HhhiD^jk}j?F#FG#x2IuiX>#pA6DNx{SoqrT|p-n7q6+J+J@2+mWS#56OAlwf7 z1eR1~(0iSZPk%m7h)K5y7(iW{EGzm83lE#~1GscFT6K=myHmSOuxXM7muknDLy%@G zK5%3W{3A*TGYkeD5Jn(WB-a1DGpSqry6L+dE5(IP^@>*{yi(5Oc`FnICjKm)y|1Nwltr!xB90*^j*j-5`eng^hArrS}Ps&Mn|EfGO!L;0NeuGkL znIeskvisTv86=Iv#Ml$}GPKk!@T>*SPZu{`0A?Q^1e%)}{Pc(!^jjI44`NTuo^$W} z^E!RoZmkY!lF`YWu(6PO}M5=M$DqZb*1`!Rxi`q!0r=Sde9^;2p!KGEf zPyoGjPV^9y#Pjg8g8oFogjTK8Z{G1}S@au+SCnVLy(cOOSNA!g^3+=b`EOiyhGlm( z8^~hU5?!!}=-umm8mqHccDC22YFu9T^%T-^Z|kh6x*|z^?`MmZXqbS2BYO&Y0phk7 z;bg5t-R}HJwkHWY?#>v!3Z+GDc{IBjURN|%2oBSGrS3QnNp}yT>|qBdCfRv3g3);U z>58$e#;b1S{Iv)H$F45L?9&^=fha9x+?O=)DC_a@QcX1X_xJmj=@SC3rxK%995ghO znL2`kmM$x2+lF^T=^gr8rE9j!za{THk)BbEdTCspY{dK!MV2PDn4UVcblRVd9Wxsb zuI+eX(bqGQJT^UTRWbXsA3r!yl4M=Q+7OcP{qlVjOl}cc!%lp3!;&a!2oExiJ1P zuRoBi{9qAVd{~MzXf9mGpjoYYlT>@}Ha;;Su3llz81#%f4iSU6^Y#kGV5gGEcK6ce zXvDK>s?;dP)5EPUNrAKcBAetf5mA8yfgWxYu+Ttkriwx5^zm}G*^WsH_T440KeF(` zI?o-)jYRK)av8FKScUW3yL-f|?VTgrwQc39Vx0mBtZ%1#&V1HLW`dj_7B^V0>T)Y5 z_9q*Y+-EmL8s5P(oe{h)-FvQJu&2(xwo}~hH_Gta5%+B#=FW}Dh{49ed0N(qD7a}t zj{o{+c7|lkbWN9q8oWB2QC}R1=T};4*%aHE0preZe1Wgb##O-_uCme8vW7}C`+P1; zYUP3$ub(qsOY#%Enq1)+_)Z%WN^s(jClgoF5Rq5^!~dJ8^p{c#6``r`Y9njgV`jrU zt^B2~d+IZuVg@noVHsWv{G^R3LppoPKdGGLsccAraZUHZ6apAy5dPv3+p5J0;M)$K#~Azo#jm zw~8`+dzYnfQV3pNS-Ct=*=UTVQ;8Q5N9R%5S<~XF>N+Ff$h3*MSi6Ovz_b|)&Q!=a zJgMAJ2zo@ookP6AggFS2mzuTqTO}+FIV`4fdt)hr7JkzEvo#HCNqmjsZ(v$lUbtsA z;h}TO($?-==mF;OYbD_{C?a$zMfcNQBdvPllSFHMi4#SMvfbBiXh<3hqpSp!xf!H9s)lj>| z;77M%uh^^O)!2hE#E5v`VC3PZ}EWxirD`8Ur=cm4s0T%0C66uhw@;q6Jd) zATN}mb>F--t-lJOiN~Rhp5e}sl7(I>O~I2oiL;mu=!f&OR5N)bCc%UQf!rRX2XH{# zc~Vl!8TY}Hxz^UROCC}8F_JONQK$6BKQe;WBbjS~*p3{B*}T+v;Q}U=2<~D|E{@ag ztclzFCX<-NovQ=UG%x)7@k81?;b(z?Hi~kJ{I88CUumW#okfo{J;>xK&{_nX6^#l1w+6kSL8eaKV|Gm)*paTx4Ofb1Q`Cv6@}cf92fq;3zd>$Ut`5FlyXlApKl2 ze0Qp^N4(9-<9Icf^~2!CI$w)jq(;3H1^@2^^382lC=~Rv{51zpY^T==ivx07( zfm6EwVQ6MYfSE@cV5(0_4%!p&D24l_K;B_t@}922B5*1@yfXpC$b6683qIQ>r0JN<_u}ST#sL$M29oo2AGqcCG^_>wyGBf zfyoG&<@BwFu{O(ZeuDABN+Rg&mU7!QdNCSvR?8yc7KTKQgdHUCndt#7r2HJErvy2G z!%~$TNFdpA_9+T(4zh&$m*|8?*E%ee@JWYx7bq6{SZJuy8{IOG?M=s;nvQlCzjxvx zV|;m`@M~szjGjU=n&R3diU`wkrc!IW@kU#fxXWU})}Y6f+&_B;p*$Hzxa#?Bv5z-1 z1?luZ`}xfpa7z1vf|m;us6C*TQQLRuyW07b1&zby3N~9xdtC7ge(HACoA-RSOf-X; z3@>qisng&)@5E6IigB;eJp|9bi&L7uq6|}pPE$wO50P(eJyv&JfVn!6Y!7{*)nDhh zSNnxlg;e1a-A0Hw1?2YbLQ7$XsADLwf5J({^3u}d1-tFnI>U%F(}v3X6!gX%`T zJEVCJdZ`Baj-*Ac+572Y-(A!(hMnWKmGiru6sc+p2%h5t`LT`9x*{1vZsET)mEis8 zpynPfQW#0c81@cKGwH@GCaT#qM;i}M212}oagHbE8;HF&w)tl&%}cceN|krcOHI#A zcMQnw3$ipkikIrH*~w0-JWtl&<;oq6$=w+W-^_pyB?V08^89;lfHYjBoX;b;#R+2m zH0UEjQ4X!UeIvfsd@oR5Mv|yP6U!9;)>wZ2B>|!e@$5wQ3uRqh)v8pti_;Ixm%m^o z<4Myq=&pY7zP;3^8=1tz3^CHI+G_UGk229KA*W(O1nDV zh_wT(MJ7OfZkDX<)^d;j$wO#M+`)Ipu{Eo&S=SqkAVlmOa@n8DReRiouQDl3ieN-_ zw|U`n=3N}N@@?CEB<9tXvl~dV44FCvyTm-Ik_Kp#;rExHZs{|1fjM*5AqstL!2hxBtce=qt*(rQOYe}}(y(QVWap|`;$K6#2O*FLFbuxEcYm?Ad zd)@HTRyQJhA`(CGQ2DPfbbtUx9>cwL!J-}y@&}EqcISkF+-AS_or8|_0=28ny0VVb(Wx_Bh)PaPy`3JTFD>a$?M!{{X*bK1DNn~eo6XT z(sV`Wk46kyAmn;tY~eb|q3v|Xz@zyY<`HXeu?>dqp)Bd+CTNj2%PWZ)u_K!eE~beM=`N&cX>8pbiMu9&(^SfxX{&U9 zB+%J!JMH^|WfisZjBf2NWb7<>97JU2^E}#y+Y|~^Cb%kj-C7bkiRDH9bevSJ&#|hJWhnyR1;Ywy#@Zh?O%SNhpGQcgl%a$-;bGCDA>B~ zHk{)6!a{J*!)d*ks+4ng?KWIHs=^3^ZBJA(%|pXI6ACU0z1P zUcZVJGmwBz(}Ea&&p$72kXJn4pD_Y9*v;{PZm!nxb(Wv?6qV?UU-@@VErCF zYmEIRGFL1MU`QQ>a-LGqqqA~R(j!Oew^0Eb`e+=CK2%&J8-ztMSEcOo@$6NLV93=uyw+fXRJ1BKt z?l|66SL0x6H`)asWAJb!XWMH-KAbV;)>!Uw1t67#uqK&YG z?q;&qL(J(`^8EmQGE9r?%|-0%!nwencD|{{G8;!i%?80hAUSGvy1AK$+A}63zCYNi zW%b%vsOinU?mb^lu2`U{Vz}XO7O}%T!rrCwc@mew`+Qo_ljlsdPGtt{JF`T;+kt@g z1KxO{!ww$f&h#ILm)mJ)>0BXOmJmy)uY2TulaOAOSw~x@xAnt4?6K(?$86*FL**Tn zTA88E4Dob;i9l~;{ z;M|LFAhp?*A-N*PYTgNn7yu7n+#8%p(W?IRc0mKERGu4?xt*`v+xTc2<49icjzcG& zR%|R%m>n#^-abJqHgY}xW#!?P3d6S|_YK1iO2CEqk1WEwJYAs~2c!++YYn`I2Ju8} zmXcLQt#(HsLVD9)>HYJ~(Q>JImvEgpn%&%Lm!d&)>VU-nipAwnrWuCXPveq3_GJaZ zYQQKl8O#aVbIa+D=|DFzo2In>vsY2+Vm)7N&ePA84(p7UW-*!0WRjc=I{K3;?`j?Q zDH>cI*=B%b&HlLiD;acJW@Ix#=fwkoHl4)bi8;JD@54fM2mOitxqU|5v}?Qrju@d7 zJ|r9R6*vDiT{56Tfwd$cc|TeF4uhV{Zo_Wkr9giI=~|BfS||V(Y@T1~V(FFb-59|C z?4LmSTtZTh1=ulrbm4T}F+hRE{AAR!L@^%(yh=25nCk^XuPNCiH@dmZWp!NF>USlQ zNB3NP9jRUH1UWt0Uv7<5HO-mrD83?|ZJMa;Q=iIna8s*rt*Gec1(0s3a*rp&@-AS# zjE7`*p-Md2W&Op*HgkMO=$(|9C(HDC{YTN@&fqnHsY*5ag@U8X-K!lYvsjned~uGg z>C70l^KtJ%MG4bJcTS814l}wG3~*}7-jBr-HGYYA4Sm~RkI?Pt#UtDs_)no;Y6|5ht%BaSbb4h)HnfK`$Ty(`sMcb7TC*Wwnch5C`#CM0Y zhdx_y9}^SIVdK5(*h3T=HX0(G^0{e>N?bDwd#n3c+jQyjo4l%oEIH=qOE}o zf@(54+r1{8VI0+BIk!JoN9}Sfuu;j!2=7;AITsBxfa`F>sF>|ePseFn#piq|Sb1=n zVqph((?Fue=&TY5S4qVfGMwtwdF{@f1Onm>wkGfZ>e3kfLmEEdXr1>f^0)%AuK(I$ z7b>{E2%dBd2^?h>C}sw4nkF)(Ho$}&kuCG> zuJt#&2w#JbgM9L&y3QPcq7ebJjOOUVzF=|xtWis{(H~FTz;8Q491kOWhMg?7u3R7| zWZ3t8{alL=UJ}U3;2NY=`Uav)uc_qMHS~Mm-(@(MT|h6d(oWY#rA!qQbPT9aiTx7Ws7K*4l<6ckV4s z%50!OW_*vsf_sQ^y8H*$Ts;>=NpTL(E3I6!#&wKgea?vg#N2!>WsS%4}mBW!(RKDKE>wY$~(kT_co~+yPNYX z3GQ+$v&vWo6?vIteuKC|M6R-p$S{I&i63u*3dXLF(#0cIV}q6XlIt9L!~#!RRxC6Z z|5t-}?H+nce!n!K@mlLvrd9v(MOq-e#RV=w-fGv)YgWe)Fe-6~&iQH`qV}gr^muHR zGrr`@bX~rWir$n_px-A zUK@0rGj|>6l5sD;*_haQZS-roh$dE^-}<8%CY^nll10gU6SO=|=drzI&IL=jT>vlJ zbO!V&(KT#PI7bw|Gge5h*GRY}XM`hsY(Ab*g8Jh2+mO}*P_AVy_n0JV!YMT_0aY3L z!SZ@@LdqtD`Ke-eT}eDuJzYnc%ZHhDfamvSCeU$5lQMABi!?^WiCx`V?NDUuMfe#4 z5#y;2X(hZLJPJO*WVBF-Z8o zIPURuZC9A}dG;fcoNd!4@iIb{^MyuRvoVoXo;5?(O6l88 zWY)ODo%Vw9!Vb!b`v$B-@j+7}1>pFyC+;saG0nNT1NjUFww>=7=La+Ch%xN_oi@5)eh7KYmM|pVR2w~^~3xCc*^3}6*&%dMek^@MGif<^7qKiLM(s~ z>U{oE-I>8?r!IOtkW@(=H5=?q>o1Z!8>5S6FneEPvl3u_ZxPZVAJ~2s^#Zd|eGkoo zp$fJ=Q|r7f7$=HM6s2dXY(szabK0M%Ks-OGa!yksF1?B!k9sesPS|f&;TBx__g#Q? z3!{o}Kvs3ypIl}pI~?5_QOKI5XY?q7I9L>9U>STi>lB479L7!} zR>+{eEPN#)hZU+mgO#jzj>RNtwVkM-y0c5U(elW&sjPQNFX;wR2P(}bEM0c?xR4q84heZ)Sn0N0k{8_X7Q^wKZux$)n{q&Dy_& z@^T&w^JQ6Ffk!(`>ExBu?(x%+zI`11CC!vWE6&q5b?}o>_b&QdhTR$=W734FVsO#W z{hPM+J?J!69v1}yQsu^v87HEKf}CdO5y!d*5h7=ybG7zy$$Yf|dBs}UOpS!}I^i$O zCUCO2+0QZ((8pcWRUEL+oPa!mA#t`Z>Rw-WECanjfAT(Si*(XKA9R{)6QFCPV)Ke? zh_G_5{Tvk+%=!zqqRB*U;}u6Q@>B$DTRLACs&{6moZ8g{%3}dRur+#>)wSbTvbGGB z2+QFLY3h|B!gDx3bV8iRMUIcc{y8RCNY`E4cTlG9tL^D+AL5UeU&k(rb@m$ITfx`j z!>+!J&nM`XMFex^-L8&fhbw(oru7|($7{JO7VvZ|=-RJP@YM7BRsc$mEkpsqte&jC zrf(BYPI76tJ+6Z*MSK~q?5q}kmFu${&!_@M$1Lg+@@&VtGo$&nxT`!y)x$~bx2bkG zEbV8gGXj9HW%q(J~M0CP0cnmYH-`7X48vEB|h-S=$+?=Wgfa;l- zVviN>SmQz<_Uu7l5`u&-RyS8tC2HbMM3XNhA$_;eoGRijF2^_)*Iff-R@ag%J^-(? zPmg-eRPG&nhK#3yU6ktHzk9)bYPa>!)ohFe8av62lCLlwLsQKHVt zxY_(wi*$;MP;bjqcu#uSNYBG0oM$)EF*3Dg#w}qvcKXOys)}GXWwdI0YR?D?;?c*B zR;{Qz35r!NCgZ&a$uYtntDMMr@y5f$qYWqyjCwHpoag;B!HO(5?=C3FqCHe?xX$W9 zqR9csDC$o}3K18(?M*v;<1(vsXY>4FA@`L>h4D&|K<(7>uJ+o&^*@GyTi=mRM65dd zf~_$DtG(Y$l*@nY0@<0?Kn#QVdU)33kReO)(6!!V5rT%Q-+MXSvJ+00Hx~_oI!|1_} zUv*~`vOqbS=Jf6}wWc#ZuX{?n_1@TTWgA~7DtTyBQ@!gK3WCm^Dg!#AzO>i;Hzoww zNB;@)aE{79W{oW8LqY4}xL#VYQ1f2d+eUdTl4s_F<&Xs_fWPDKz z*{HagYx2@j@=njfRKDoT_ZI-_5yB2FMnM&2MioxA&hn#oqaYCh3-CnmH#W(v&=-{X z=bp(~*`bH9&Ry_-oDRC*b|PDMwn&=r)Og-+)|Bp7E4lvmMa6=OVOPf9)ML;DN2b!ehm&sV zoOYeg{S!fVa)_J_(!0Due{e$;H9DP)_Eh~7a60NU2oqE6%|L4nU|z+IjuW_ZAdC~7 z9-6*-y6E_~JTxES=~0P}ZzPr<)=gd%7G~p4qN>z2zm_z^N7>&~kBHHKlSH5SeRKA} zK#elijmsMf$lJnVj;~6iqd)$33hPZ4lwzn@R@R#O5)dMFzs>meI@4HzfkOZ33Q0i- z9h{~ZCISM&wg}1?jc_bvy+4VEZhvQN0##N}wT~l8Y3%;z(@Tb6#=^k)$)^&;c{2yS zR{!av^U>;rztR$*DRzC_I)EKE@B!yhJ&UT-uU40l7@8pA9++ z@`@p)f7`P;7&=&c{6FQxTq(_r5Y)q?7(-9VuL`ehKi42XJ$-z5d?9?XpFi!L&_QGN zKTOr-U;kz59~A^KkeIaVFDJjsd;x&^gv%3D%XCAG7@VL`{vUd+YJSasx~dq02@Rcv z9|o12tq6n`t{5((zKnkQHCn*285I2V41A2SKLigMe_DSHf?!_3#6Ni8Up-$7E|G!U zu?2mcV>=RjL@Wd~pIhS#8NX!%1IH8T_+E9_*lmRG9CV$Kdwa(tVPd*YAv&CJ1EG=+ zIgeGJ0wtk(1wzJffKz~lvt|#6`Pmj4riF$&9G3$E3B^tE348=hnXs+Def;7*7}CgC z{K-2W1sLrL@&4cp1X}5ET4(s@yg1|l48_7b7H?ya^(flK|nm=R(-?8NVIS?U*2@3=B z;~=;C8NfN1$iVvKATC1BVT743Ki5B!@cJzUJf?c3=1c>Jc?isLQ?kQt8U$+C&^V@g zWQuiw+~`eQESg{nLT~_?PZK=;#0cl7eTe1qND1v{Wf;?7c@h#5fNzv_`~!uceB=PK z1q9RfJCQ*`Pf{YE|MTwqs8Th9{YrLy4>^GqxdKxi)M#5vhV!$z+Bw>NWPXY8r?g_e zpFX{z4A_paAtWSxxh*RwC}{5K`3LTo(*WIgd$|ecceKKD68A_S9x&*Nq2M&&fY7N2 z)F1jSJ%>P0o3u2!W4xIeJs6U4{~4N=gT0Bdwdlw;&u)mPro67nh-igp!j4cRaFuPV&@1Uw-}qA$+4od<@_`Ru&X=n&i?LBEVL6 zE-qaE3~ZlCp~W!UF?p**0Ywe)lUOfP(f4a9$@qIt`L`xFa?{H0UbH^o4^d1 z>{0%)%p)ygU`(mxfx2r_NN87aa|1wRO<>rik0Exzd5iWM*>>1}PD=?AVgda3Zvy#b z15IQZ>aRX~4<{)`d08e-k?ZJ$a5xsbYx^~WjU`}^DEa@{U~IM?7$9dpV0C(f}qpHiNXJ@L!f-Dkv7ReNT61O4oP6Ee1S$R zpGm^1s34(Y=9Z5f_&Snm4)kW$_OQ~%gD%E|Ku&`P@Ry|ltd)3!_|Kdv2zd+hmIX}3 z$W87oXpRl=!8;xD?PPy%=56zuM~?UWmW5_-$N|n}0+jId^qL3za54bslP#c=Lz3*x zJgQ{>&w>sOC(HlGav0F>KVmR>%Kl?t4FpQ>kE4HtH*ay|fKyCiSbj$*H{x9y zDf}K{_qS##guKqGqZlux<9Zb`kLjaX^blMw57W%&>y?|VfKx7%@;~0lAEVAY%FpgL z(Lkp)gj9_I$+ns2YaWllRwZETw-3fUA1?Y7Xk6r*PuGjeB=OU4^e6qv8%wVDe#=lD zYEe)_84t$sWDH1V+^y28HIH@Y*Z1oQ(g}M(3nVtaV7Pga5qQlNRH@`@$_Jp|k7hFA z7Qs~4B;+JaVq=D@y9zVj-wfW9ipbx{_Zd;nvH7j-=K!VaU2+lsC}#kVzXcfWqvsO0 z+t`4a!R2Qb+lzUvh>Z!4>^S?KYafcYUz_!szFN+f8r0eEOm&>IytcSh^SWncYT6!b z+^f?0Juonk#@L8^e%GKg%G+exlfo2%he_=S0RgfwODNGRY>ot4Agq*z@l8SPcum5H zil{ivOFcD0iPZWcYT<0SFVOb<+T*SzH-_?y5gSF=ZKB^vFn}t4E3Y2msOB*51nA+8 zjzC(mxyv__6~YgOxBB1s?PPL_b;=JOM`m;7+&o*wFNX*Z zb{wNj2M5k`Rw-(5Uy&0WU<%(WLAN-fjOgl}DvkFYOy@&J@>bM>rm7q@NvH)_xy_Cr zjMxh8lkOrM0WQJxTkV}l`uDR0+<`&s0_Qf zwO9rVQ;GpWP0VGR!iHe5{p$k9MQa3&?m8T;Qv(%fe5Kf4&4a=2wS#Ng3Nv?)BOgZ; z!E~I|K{H0&W6XISPA|fM&U@?~#0}7Vg3gru6R_2#8P$a>a;g=mSV%49-q@pkQ>;~Zah`|BJ5(jxIbzIcqxNu99R`Dkm>J#leCuf>aBzCD&1 z7Mn~;E#g5BL%PKJ?$Y!os={R%AVs>FJw&Jp0OPMIHe2)9*U=eRZw!$8HH)ItDmIC| z_Tz)}580daDvkc#@TGYJU#x=5#=aaxL(Fu!pJD#{`>0}Tj6v}>M$chmT^P`kR7CrH z$&6-EZ=9isE7Fb8Dp^=@F7ZvWEw6)CN6&Sm^Yz)GIeySQZ7oRoREKsEpZ2l+&Zxas znN>lOkh`qSL?!cnS>M|%+1PNxo9%5ec_W>kjMCwvB1@yqLTbqsFUMHwlI}kvv>YnV z9WxW&b4PPH{QP>a4&nAYed~jZo^grDY!F91Ek~?}m@6n51g=hE!jAP*cTQTD3O8&) zH#;S-O{}-}VL%W$!=p#iT&A70JrD!wRQ;i8NPvW|vn~1xAX{-E@G~gTRpBy`F2WRG zcXcAU&0Vmg2l6Qs?E1CzR1**df{CGUn!eOqR1WQ>oPVlg>Tvn+?7cX z`!&mbjLoMbm!EHi2t0~!WmBv@XEa~|No_TzSjL;hx8uShgw=wZ2?Y7h>Gt9v2d0>a zoLz{7gk*T}uKG zNjML&Pf(t1$%k4c`6STrf%=R?^bz#)6Z@mvZ7~nwP40+KlFWtA%e0GZr^{ogS%@k5 zky)vToZrpXTlT5c>96_A){t0uDXLc;;(`eS)%ey=udq92s5+|=#652hjNKE3JGo@e zQ?GxJpe4ohQ0Nha?8tgu2+RI-Iq|~pt(%Yz^HkE24lCK=dAY@S*zlXeNso@ z?(m57L8NPjv8xuXkC(&x^p-*C%COgXnP>ack>hTtPJ2aJco8~pjQ-Uwz5ur3yixbIesakRvLD!P0o~UTHX~Fx1 zDkt2onKY{Ns+D93j~7cgN73EhVaQIYGpb*Of;T76_5G@!-DY@1vB{XMQbn9@kI2Io zrKCrPPfbO0I`dJYh(zrRjp5?_+xN=eTsH7!=BQQHEh1D9ePY@8oH;w>7@CNt9J}a0 zYzT#X>w*lnc5_IQMEGA$#jBO%vle}xV~FGU9nrU5dt$Wml==Ci21n=8xH@Lq!Apeg zU-zX<$7d+-323l>M4AdWMa6B%B(|OSyA`hVF0SdSeo>~Vvz5k~DOrweuYeH-tW>KE zz4v>BFsWic%PR&>jV2DylU~p#`uxo!3LhNn;ru9{R3Dou%YnD4=v>OK4|aExAiiQ# zwtZ7qzCb^z7umpIvTa;jOEYx)&I7mhjhbI>x3lOba#<$r9Udk+4{x^Ryd0~|h4yA= z?Vz`B^3=_PYH$dnx2;f30xo)?%NJekflf@gXSzs=ltHf)t)Kxp7T1P(U7TYtx*^%O z6+hWC0mC_!r90M-bjqzSK1E(1O_G|`*ptQJq}h}5pv`02i=+{QxjTJjR>(fb#qN%A z=PB;$cNAYnvxg2CpkJ42yesrF4mrJEU59V?g+hB`|z2Rl9{jsNoWC&`J_gnHr)jgN-K>r+i}%|hUrmOohBxA{>_0fw53oN3G$mfEcu(g zfzsnVvz{YGN}0HE*1QYWtvh3W^;<|^oV2niw0<{^QCNz%>2_>^8K!O8hv8Y!6GfN> zpDH`zLStdFV5=uFD!#|`E~pqUcIjj)Y#hLo1iZBqT?U6tI!AxM$nVM#uLwbD-iD(* z%M=X@ki?zU0o&%XyDzo6itT@Pvppwrv8u-w>Q;McZ6MaaxXrN26kLoSUfcdgXR^G- zWX+B6KrKC0FZU5tPdS-&a>PogYa}{rYt*>9_doPFS!TnT8MDS@{rKLXAjA`5;mP*1 z#O51vuxE{FgHNLIC_bvAa^YepyF$a%L-G^voH*+nN*u}hJx1lZZ+yEaV(T4Amg^m%u_61Z)EiCm=0MKFLN(0 zxt1uZ-I*HCQ?ZzTSZ|hcY*Xi&DTM(gz64_AD;}?o2;mymj~J(?pLo356BvS6wI8W@ zd8LH1tBt5+w6fG4WV^RTG(qAtS)#c+R=!ej1@O?;dSVKp`CB^&-6&iNKGj#xv6s4b zM7wi>+WnN>b9%c4klmF<&L>MMpE^pD5#HOjI$2Uh^=2b69k-qYUkSDlt+btn^rt&Z zZ67oR`Y^_@#PW4Ce_I=O%~Y&wUXUI*Kt32q5f-5ABAH%KM)G7~0t-=E0%1_yk`*SI z7?v#ekdIF`0kSaApr>t>ZPB^Wzr^mGVlL?_yINtrTT!El@W%FH#J&bdO6rE*u#KG$DIr$S4;r^@I*y@I&c-ZAqW|KT}nTMuCnURadS>c)Qg_N!X zm{!02d2mFxIzMU>t2aM@Z%iSA=7N1_5ko@5n#+w*9&r&Kk~$Wf_2RIP zHMz_>4FQnKbN7e=hIHl3yT$gAVXWA!uLEEw-H}S$cx#^|yABqhv7l4y`1sIfxi`1Q zy=Go+LFVn1<*s71AqWfS2VO~ZB7hWy-E;7j=_p*SteM!gs>RD~4pXb9KJD8n;)7o+ zrf_xX#;Jt3KnH@V$Lmvh)rfK$1`?Cum1S>hQmtQE@UY!>o)V`t6G$bGF}1x+pd&xZ zWL^Xz;2!FI>z?(r(0pgRaMFiw`K2suZnZ;0}Wu``Mu=evMfnvZ36B{CX zPPp*Z`m|!9kUH%Cp?P`Q_u)mtIU>wtGQ4P_(kF{v?!e@6#P+kWyTXhcJT{gZIwKuH z#5gmdR1I?Q!n)2yoeHz~y+in;j^Q^v)+2HaN|)l*IRWO$&aRTz!}bfWz`$g-cume- zu_rDQc=0@oZPJ6{7l~R+bFWVbe_+)uefl=+ZbpS*(LDf4j%5=k&WKG8FY_!~8BwOY z0C#rMRU4?>^E5t*%yXr_^Fw(9e~>7g%RRM_&mBEXxBp`Q^8V~zgH2HcueKvp568z;)QNe^BzINCMEB_@qk<6Tn1AjoKj& zJN?4gsZ&F{5L5hih5y6cW*nYRC@KJP{iKfkfneA~bRFC#NV}rf-RT7(thz5iIEa7Z zjbHDpH|JCxXvsKEL&pS>?Z7%nL`Z`T@5b&x<5(nvoEd0YGbGN39%d6m06XVv zAi_4~m(3((=$rdwJ*)wH-^a)2plTT9nV`ze0>51c`mdnA8Rh%zRO1z~W(w2X;EeFD zRkx-$cbKq-$y5ci(&A zcA?Rgk~eOaNpWdpeJCSnFz>M3=OwnqRSn)z5!Jw#Cw|()v!}Q<*9P0=Bbo5dK=G9t zqS5egE0loq1SfTuQ;{2UGB7N2^vO}F%H<29*zqDhvi+*JVcXsW~tw(FN@7AGA8du-~YV@4f+Ny^SBn)|6r^ z{P0-j`t7&@jCtWyTsBuhfX&^iygwrehIQ}W;5#Gdkq^O&U zASG#WRG3Ok!QP>H{Bnv@;$Wx05Jsf(EtwEAh;(66N{JjpA|2#n3mzZlURb;u804$X zE)nQ!wCi4DMhu8#GVWYMSSmp7JbDVyj5fRb9up7TzYG$Yjp^-~xa@7vvH8lq3J2MeioCG?-WdVJ zw*kJD5#R89kQ0g`vN%~CfydSEn>joBF9E1YrfFVidgIfw+)d}c*YS~z3RZRb$6^3$ z|8zyn;zFY*>6`hB};cwj;q^jXYCn18b`W+fYs<4lwH=;y|E|H@j zFuuQj(EcR|lZkab`PifX#n5V5ZJg6Wf2Sd6YmLLaQ-mAxfOYsMtTAC=YhWX8XZO2L z{ej1OOI7;Ehb?oj(EP?*{FQuXM^&^x*>FLgz#|mIpdkj(02kP}E4S3KmTdtp8(r&{ z;XGyY%v+){AQPlrKofSGU-A~K_Ah0F!d1C+k@a0FL8_G+Co7JSDNg~mT&tZMB3eqJ zD}Kr$8f#;DsV}FBhdgxRjBn=XS9^`+S6uQ(6zhbEki5!4sQCbist2;`xPj((ULP*Q z&+fuyh0_mjwp$JaGe&^KmYw^hp*{e$b{@`!sB2*_9bvJifVpsiKvrp5Iv|)zz-443 zw_7vOW4hQ>KN?{160F%oEU(5twokh8PBy@`*xe(XH6vW60ejpB;lLcS!GQB9HTn zc;vA=$c*?vT+|MZ-M*(O>dpQTV$RCrMLuHW!)^RHxHxro9p+~jC@%CJIjiSz8~#)d zL=#L3za~tP^yP;Zn|D}^KfCzK`_TIoTvrU^W0|&AR;qScHoVbQ2j6U4&7^wjESj^P zHH?3k_ef4_f13hT&}GciUab0KtA8U(%q-ba)a_vB4lrB==ZCC4i*> zC>7i3db&wZpViOQ>HEqjRD*7_Jb``bE;us>#EF@h7A3eBPa&t(cf(xe`h5K>%apl1 zcXcc>@On}N&68|uX!u`-*KW0Uu*2~D+36!xug3Z*Pulgo*V*b@mAE%`IPaC4X`4Lp z%lgh2L^j#lBhvd%}UhSKs=reoZ_4o(?2|BaTaIZrJApt>fYt zIR!aF&h}(BU2u3T{UV3$CLV2-Ga>Wl2JNLSmyj4P@>ga@PTpiZa<3NgTF%=zWrT$QG zr=*;eN=P)33w<-tMV(-+j)X>p#oBj_7G8GM4zn>G=jDE3$5PyYT~pF$yja0wXT@57 z9GpX9a(3Owh>T`|U`c4kN&=6!(#iJz&#Pv3Behy|meYkX?sdH)83Hpi8B$yGOGT!A zzp@tgo5ea76z1#RqBCX7DuP+GzPx}ji8xZ77zpFcOCrRCnC?%Si59~ zElypK1wH+p4)S+(T?k8F{JJAFIY2?SAf5P>vBJAs&9!c=C#qtu25mn7b6=3%o7>9z zHxGNN7Q)IJzxr6=R+ZaqTIZ;f;_!OSii6FEr#`BZM7IR%VJ2n?$;We7UAZdTozsdue<#!`6W{hg?m0L6ZHGJcuK3IGQuJA624zDe%qfxCWk0B{vL?s3m=Spe%n7I@MG()JF8 z#5@#X-l!i?;kuM8G4&&(K!4KT8-P=ZBHS6lzipa)5a98yJrEWBDEgVU4*XDB zFx2Xt`mrbx0Dg+PlEwfKq${v<>n7r;Cm;ucW-2(l#`bEC{IudsN=FdYz>$N4OF*v- zO0cgC#XR_NjKW>+4xODNf8}?I5!z!WO*_#1wiL^lsR(NP zP}lz3=lt$3oMGB4eu|#}<7fC2;N*U3@tXPIM{pZ0E(#PXR2CS4{N^F2SBA zz{xaBwV@y5HqJ#ZR-OF|G6b%bouQjsB{yN}kyY5jrpB`O`+6zFJ64L_d)9wHb8 z2hM_y@!35;KVN`x!ps5Q8B_JBg1Ws z{(X(?4=<4@K-+;`!rDi9t!vISi^`BOcU*$bvhDW7O`3Pe7n12^lyi z)w9_k`Wy;{zLb{_&M8&Kz7Gw(Wo+Or_4MgpgND#m@Pje{_Ey=v|Hjz| z>Tl?r-wFu}AgZ4N2g)Pk#?c5kP`)(%xaa%=V7eN=u98~;bPP4vA>p6?z0A0cWGnIs z=P(1}-1}s9O(BLQ20ZtvtMX+$GU<2V3&l%cW3+J_f368yg`S-#eRT?W8O2Hf;Lq0p zfL0@lCKGxpdi!;9Nxdd82zmtoyH@eqO0+Bo4Z*cwqP#gtYq93$mqToK@4BipR zC&Wo3wP&U{``|^=lYtD*h+43ISe2lR=)@n!Cj5ol#t}ZVUh44hAlCl}A9$P5mg_AZ z(SOj}=WI%e&w8UK0bt7vwz z>(}XKW@dgX0pj|P{RTIY=Rtk`WBJHfa9QY0KJ+OsD?r{&P12)&3-b4}J+R9)r_->IXGUnUi`;Pydf%f z{cPXlUr7*n18vf~VXXb`zwQFr94;jv3_bc95!2x*WSazibK%rY;UOTtTK}gvh`uVL z^x1r#moUWQF0|nlc}l76efHJ-_YC7o8_utUP+O~2xTuFM7BGPh!#@iG=fUR}^B!Sk z2+fM>mhIsvJbw-1@;|N)rv_JC`0f=VW8%iYR*ezo>6!mIV}8trnxZ*`CU|LW?U(+r zF|WYR-2R9aFZjcO*nIDojFW~i{KuY%BqB6c?Ys=v zKh-3h2SA9oDJdTs85srBf=P7QvVU0jM_iYS{)n$1Kiuu5HM$<`}Z5GNLJ4_S^s-q{8I5&uMmJE zs+8Ppe71Q0y>HU_etI}*A|8)egLw$9k6)cQuSWULd4rtFRbB6n*isnv?43V|iC*k$ zx1X{)9e8$BFXhA^PK{sKMeuB}?iQ+NzyI*$(R-)oKAW7Fl++wAjVf9EPfOX81$&@8 z6vji~JtP+$^uN+N>o4G;f&B&j6KEr1@kD{QFa99K&45!Ws_@~8OT4!I<6!`9-Cu$M zENAQp41P%YLP4INv)xCj|4%JV)@`q)3(E|ZmK*Ric!+!#3 z^}i1}R(1cVk4jD@!J_%E1=yi-rWFv-4)#V? zR(Od2ds_$kYQhX6iSbXO3mT5uT6|8V=rv9_#GVD@=9M+vcgT#}A~lUc{8*3ZzsSv$ zbA|ov5c;3J2bLFT9)=$`GA_Fr6zYb>W{s!Mp_0kxKJuz6ci_RWIbR?Bs^3v9k|(j$ zT^DKl$X-@3uXg*t+8j|kLNc<0s%hw>y|LyJE?>t?-4o1ibXt9(c=h9%truAnTkUF9 z1ccrp3g<*~|54GRenen95ZP~^0j1dd{rghW<)QG#r6mi_-67YfPoMfl?G<_ul`7k< zAi1|NkP0sh87YH#onTpMLA<6mv~zxjMe)C1H-FI8kVfNy*{sUrT0mVd#*Q*TgxHRy zq6dZ@V*FIM$U9=%7CjND>g zJ@FEQc{d(LJ-=4*-}N_M)af24$UFvIaq^s2Ye#*^oNw-sr`n_!Cw7Bk9NH++Gz66% zD);{DQgEgTMsJc)lmRc|g#Hrill#-_hm)YWVZ)rdi(Mj-oMcHr^_&);e-63lsirsn zCwxNac=YV64^tftq)>0EG4seyJj~GdXjM|0{li6VV>s2+D>T=ygIN{wboj{z)o_|N z#wP6>_9C4k|NZz7Tm~P(^;+4J`L$8kx4kL7VftXDciZ4@__el0!RIQBTj(9LJ5QMw z0Z+Ry>zS7ZBzk&Jg@L$)x@QDJT!iO!sc;Rby+f9R7MP zZgY9m8vilNgOH9%n$PHr7vQO)4wrSS9MnB9YaAU7`bHY}jGM1;8BN^I)yvCv4?fTS z(}rIJ8d+`iMn+(dUd*lQ7=ZVnbuGg<4-7{}2TW2&G*-R#eA-!{)3=4qRset-5q`(r z{3KO6pHP}8F#Qd%eect2b|*QUU@_p_Jtfc#VDH6GvnopZ7P#yN}jV$^XDP&>SD{;7}{U>J1{=csbC>h=? zK$eD~j>w|5ry z067>FKZrc$i08}z{0vJP5&byL8|qf0KE#% zO#P&wc(UK_#m|0(1d!V^0(elInM%&WCW#E0JdL855q(?p)r4u%I96r@HXT+lGLk3k z%Xy*kf9z6%{$n8_p?NTBD+Aw}c#@gV<_DhDG*Y#wG`GpL>nj(I@CUomL%KOYat^aB za=gdyU=pV=T@WXKhg%@^#dHYJz?8R1@cl(qFNhDl1IWDg%r>=oU6ahIYycZyLB%1M zo%a9FasJ1B|NEqUqdiqK$b|G^+9dR()}52JT*IYV`jIb(1N-wa@QEV8gNsj}=BJRz zE4NK&LM`|m47W7O?H8fX|6_~{S0&Au~E&w=Y7j#*5cw)9HH2x{&#)EJJ8q-oAtfW?z zRoRip-n*mi9!MmgEwwv3CT=d5B_^a})+=vw>rf$a?(3^J4!f)T{N4w_V1j_mdc%ni zpzn$UoPSDIBZavTy6&#aPwc?Ni!nf8zZ+Zv*qXKYowuUFU`q*yi8u*RCud}ky%D#i z6deURMg;xyy2D0G%!`gby9fE~E%jv-&jp!w$9+7+9_;W|3`r&n@MKBPG?KLFuIbi! zC}sUxFv1-5$t<}7X<}gqMRJ^A88$rZ1AAMnqBn{An`*0ELHC>>uO4)y%KM|1ij-Pw zX>5vwW8RHl_mcbW?b`8u_y@zHuq*$BpyRAod}Y6P&b*!dHCO%oXt}AJ6VYpOUog97 zT?WxzkSUTZ-I$|YzvKQiZyineue)kSazhklvv>JOpt(?)Tb-$})s^WtQ_O^n~CGl^YESQ%taILsJH@VWo zQ>ZfC9Q{@)nk}!$TO&h~2es^0fmt8th=R*U%^MUwDnjoh;qGtAff=)zK6U20ZMb z^#elfsTY3W)T{+dX%wkwlF$ghSQYR}6|sk=XBeBz<@7ChBzBWX9u5g|nsrLE33Y5J zzoinax2kcRgg)n0Lv4dve8RwAf)$#R#-&x#X^jVmS{0$F)S0&-3GLl_HRzT~mX-sr z4Efp7X6HOATxlij{1n)R!Da5w?ctF3vn}nf z1A^vS*a|PmYvRoOo@JVg87}Go&sogbv|I>gj^rS%U7&-V7AypRrIpS)eA;~nE%jJ#ghh`VS1 z2&`h*xT$#~(|ED){fgsF!>~O=izJmy7Fs7htKNf`n&Z<22D2c6y8`DgF>|{@5Va+e zSDx#N6iDc+s!v&oo!AI>)Ehe+I{0et=4UXPj_vZk-=nP}N>$tMYY`9XSF1~N8|P6z zSqwVak8NmXdqCFX#k1-DNECGsPV{&?((A=a=^ZYMP34L#5?|k(+CfCt_;#UUj$!&x zM}b~)f0%cJQNh!vHy-zR5)xQLi?S6=E5FqayEg%V$v{l~0TOkYt_ekq>OHNIZmKNX zRK5_2_s6?g$tuiBGu?@G9<71K&3EEF&;`f_&qa+Y4k?nNLRZ%a2+~H#6S)(A&{O7N z_5!l9Xn0(!MPlSmi|4Nfx7yh5{oc^iPM2;drj>~e&RbU3^TtI}IjC!fo*iL{ZV{eF zA_WBdes*Q$3kD7Qfg!sGtA|rNmL_Y0rkz_w`eCs%2Gmjax;FBM7B>9s+&b>7)y4))z*wekH52p#-X)c$LmYEQh?kHj@WyavNZk&T9;|>M3t7y}Q!Cuk;=P_& zRgO)~kr14AD=L&RM_eBWG^0Bn&w>7y(RyTC-oYbw0f@xr!r|FYaQM;R7Zj7S;yQYd zhRmmSrBr^}u6a=yU_muE4)rd1`9RmZ!X-6`-6p)s8!j~R4sFRU3hZ|-GA%ZD6qVZx ziZ}9A%KgIyP$a&;RczAdm8#iO==T{`qqqe)>8Me8!+)_V(>C1Y_ZZYo?42Z*6G`Q` z$rA1Q#2cki8tq%`P>a#2 z9fn#HJ}bOnEtr9u(nvL%@ZHPyc?C-!it`|E4fJ0|Qk12HR`gu&KL!{cxf$_*9Ubjb zFRl8MnLz&u8~qy7ZP!T1bn?lD`wEk$(GHkxH=nywR(pW|mzCArwC_oiS9uLF3i9Z) z{%o$zFzQ-$aglHmDM8p^gEv{V5=r^D&sN5JdRk^-C-|D1Ue950j)+|b;-~)Y`W%-0b$u76U zQk;Qq>`<+i+8wM>zobGH>*(^Q3NWh(!=mn?O6~T^@6=r(p*nb$z-OCXq@FFQz+;1) z$ek0cvFd^h-EuDaJyz!#wzD)e4)fF8Sgz(W-C}BtJAWDookn zv{{u#15$Ava`1h&G**tBBO16q+(Dd@Qfm`cIuyJvb-SsAq9E9?nabdWj#^-YQ#T`m zkL-fc;Z3`81^Uqy2WVdS6ZIawYNz-sUasNepX^4%F>^zj`VJ3T#;JTqbF1C1uix<6 z6mwV5vE?$Ds@*!I@ol|UFsV~+gC31m5~1Me5gz|K%d@Ts_HnjZDOx3?r8@7ekJnDl zRaJCu`)Q~er2Ph3$FzNm)yXr=`9|7r8$=M~*ra&;QH^C(GnY%LaqrrZj|bRSoS9Mk z>q5tOb5q|dF^0x*nNp8P38zAQ^}*;5Myn5ct~Ba!1CsTy66!Q~^&QjwLn^M6j&qXv zc(I_8fP0uRzlYtDn;XKLimB{SfB8c1{llsYjp^=jqv2#&ilK@U zmYMy$Ec9dIqu<8yBHP??8By8|Tg6wXxE?58Q2%6*>)_1qegI8!T=64RP2hoDTTD8Q zxNcx7tCa)p9lC71a}2W=9fRgWyVkfp8~W5Xbeei9mAp5QWjZO|Q)tH_i>8?AqaKf{ zk@4f=AdU&k{w4Q2$KRBz4%chCs`F@Ffzz?o#}w6R+)dd%@J%2UQ@RmW-ZHXi)&BDt z(P9T(7PU(bqFSjyWtm;t5mV?+v4+5Hv^SRI7iw{DQG)=)^6xT*A^YX@0M#7j3EwB& ztJtesZJBM|VSE@c@%Uj#3g`yYq)b`K?q+Hc%uMMxC)~kRI?bn>UIs@o(SG<@R18CRXY!(^%Lre+-)E_c!v4dq6dRg6 z$?LHm`N89#9LK{F=|RkHvvIQG+mWRfx6a<<`^2HLv{#Z@8^V&W>|3Z?b>G(#XBISR zu*I?KM;4pXxDq7iW8AcB3T!v{vUElihda<~nRxyHw;kotBSHh0cmDLeFI~H}LYGKh ztxBkf#Dh?`VfwQMwpvKFI)6MhE!7uktoX+I#!q1O zLv~@kpTGQGdNM6B<*7+?m15D~MHG!{0^HN(u%SGS82%C}sXLce1m_Umch9DXJ9RN0 z(mvL{x@AqKal2B%X5s@U5;d{`49-9{NgSI*>ZWy~p0NDITkb^PsqBq)6Qh^uL%3@9 zcW;m(RFtf4OzXK$&X+$>nt8>CdT=})K(sYFTX>kE7P!mxv!LDv(82pt#JH>sTB_h= z!YyegLk8t#Mfa97nYnhbGwgYb>+ZurnqDQUNauHy+6Zg2o+Nf2D`<(n&7q6xs!)L7 zZ!{d@_wa`+V+{k2eB1XY4xFDyM=G;JYlV#9Ix%h6j_ zb=$9erpND5eR;04P>ETWRY6MAZmWv1$=%KeZn zZgEv;35;D)sY~cQep%hp;$7nqQ#;j-t@Z;B(~et%fcyT|&A1UaZmZQ_KY|?QT5TQn zTCc7hzPm-35&^OUg86GXJ~@-#6Bk`%E)xjHBP^e(Z>}tZnU?C_(V2PHg91TpI<-Gn zps&OB8^NjKKEkGLspiGnp!m(#y#LW)j0Iezi=+v$Rx(ic2d^4oGzetoa znt5%f?$dymvnFer1}IBIEjzgJNdGVi{dmCiLpH>nG-ohqRroHGOBd zl9KVkn1*CQm+*=M%<(J`hhCl~rGYcduB zl&fli)y+V3XCwt#C>5U=WgB02B7$L$V$p9CaC4|u=wGN%i)7rxK@!&k=vBxS1$n9h zOsUVj=^X)+aCEpPup&g??OJGHw*C28r{T4!f!p=cn%cf5G2cu6Dahe0HT>o2o^8ne zO0W43H`w&sVb-y%+O#*@r}w2OU+s}p6$mk%U00*kI)zWRp8WF1IxI~9i)-354=}Q~ zI+Dc}oE=X(tUR@s^_VGneV7XLZPs>wHTUpEU!HBL6y8|*Ub|0}WqX2CsJPq$=7dS! zzfYL37)3$WZLnFjvpzn}+Ebl2jPyh}n4;3~VB`;!MK9G3jnu*=;DrEDdvl?_h8R5| zB^whN$sjA8tIA+HkKnP{*nTEDdOBNVcj!b{Ln$ET#|M$Qtt38t03>Hnpnz<2Ws!{R zb+D5)QTdkE>0tDWoyOuw7Mc}Llgs@~yP?HsbJ3+-*i`;?50I+af}*uANhNJ;@Z~%2 z`Kr>^OW3n?{co-3|ND!807nqyEI);(z9_{^G!?IZrD^MT2f`SB5U(Iv#uCX2bM?td z@F;qk+cMXF6JxYi&GRs8Nn&bbWWdR9ti(L3uBIH7K^Rg^&qJ$QVe_VTqB=w+smV22 zyUgUEREDJGT5*OXgd!EFBDsJMN_T4P>($2-cr5RuSA^NJsM#$;lc3}tRv8=sByn+t zDT=WrH$&jjT%BtcI*+49Xt7Hy?>!$F5qxAxZZQ+ufIL3Nr<}6)k#cd~D1bloGYQj?2*8JO) zXU8{Wb|7tfc{MUjGdXsdvYURB-IHBu-S4wPS#5unqKGGju>|v*OQbQm17?+M%fI`x zOp$|1^YwT6C-jYzJ_6V34yRS=7s|!)AqyI%nRq@my5pnmd%5Zr`bp-w7pfc>fi18woyM#&!$uNYYPEVJ{>3Z+AJ*ePH6W61u#^_EF@1&B>u%tiI$_u*YU!U z%g}oZqnvbl!#_0fvHv3q5ul)#F~g!2dV53Ua+&h+{Wd)#wwtx9lMU1J?mR=A7f5f| zJz4zQba`*1HIeTo1F2TbC*9Fy<;7+Nv)bv+?mS4$NUATF3wlG;Gw?1;3(47KExhPc zAGO99eLS5teMEP;wj-qjxJ!4Zia!ss$4sU)k1Dr=0YPot=+y1!s-btsOt;`P9oOgE zt83SRYvR$m_wAj^!Tkhwj(!sMeXn|Gj`Gxulj?;=fJ0%{tdD$JeFwWU(>envh`0mI zwOvW>K6$V~LUI2Nw`sIzvDwgxNp~b5{b{=&Ky)**J?7y`Twbo);f`mAM_$!ro|LP5 zhNi3IboFoG?8>%D$<|P1#ecl&k2orUhicKxel8fU&3m4e(8P)Ae(Ri<2~w%U5+vh* z$0-7gU6Ik~qO~4oJy>Ev7s$6;TbBt#zhmo2E@CTYm1;zZT#tracn&sxJ1ABE8VMUJ zR97=obgi3ON|R`i?h@#!ti#NGp^FqK!_W~7wNBm{&YfU`aJK&2fR!oh`oyt0wF)w!wH)_X?~!s@Xa0PkQzU`je4wVk(flr$0ySr4Z18@a*$h#mG)l;phYQ?0XAdR z3JLYST#CKcA5YyIXpzQ{Td?NzaVb%$T}vv=cKGPo*wO5*3h%ArTW@oj%?gaB-Pg=3 zmx(yqZCiBlyGi2)hkK70*F9p@vK8YnZ?8D+v+_i`3^Y4~A&_Qqi$mZ1ch$ULj%deI zqy88+tg4tKuv|{v5z8J4W|uxmQnHxL=etg(%`;tIaKWSJH-?fSB}v6Sm#xcRy*x~fEKBYuRRd5=9g495nf=v{kYJPkN6W6&P+sx zRF)MQ*_`pqD^DQrbCG1oN+gG^zPCaxC>2>{%*9p{OG57@HcT5T7F0Se+PD|0L_IO3 z#AgIija77PLb0+pP$L|7cs%kuQ@BSezt30X8A|B6{r>fg`S|nUU&>{<9z%#oCR^$% z7c?T6X7Nc`9^6niCRK)#b6=yOerq%%Od3qjej0Lr926uvQvip$^~Re#r5Es*Xt>^| z0KwyzHE_H3rJi3uKFAa#@MMao(C|Lh_gH>5>#_~F*wI_<)gnGu?OvNZk$^va^0T(;n3y;#UI}P&&axLAHNi@dmd8vVL)L;aQI*Sal)U^%N zy`P2n3zOfm)SsDdDIcXgpPu;p zxQ(0-7278i7LSfE(VL5oA{=a%)qCvOI2tZgy8i@ID(1%)C-Dt!Y_?Z6msIF7K_XU9 z-x^fH2zMWU=ym&iFjMR!bdiq5E0O!K8yP%>=Yn{7Q^2G(T+Bznj<)-O5UL~u=8BUh za3Z~2ymh$YdppkKNTkxYf|trdV>>~oB0Vdf_nO^!DAfYzVu-(6jwgQo5Qk##tZsIm zw=eRdcI9kOc^;<9Inl*tWV~cIWW`fyqoGv`2woWHRN6j`xQxvMxc>&9_&XlDGVEV=^z zh(Ik)SkZW?3qz)p7-nF7?D#9*t;%1CG5v)SaJ;Gc{Vgy#xh>Il$IdfC4`-%8r(&+^ zt%AwMG1ew^e(mzjVAhkBpeLvuo|l7Am(@~ND_tcVqqst0lQB(uxU17pXu$TuM-&6a zqEf0zMgbe9bR10@f~wtT$Br5o8&17+^v~qOJTLkkr$_r%@Yt$mOXS4&gh?GS{5h)! zu6X=3#l5<)jIs;HL*oL*hSZUK4m)xX?T0JbPNjbIQB}j$^g&d!!BAL9rRBc5JS`fO z<&^v=3MB17!35humOojE z!OgcbkZvx(tWb)CB47+4E4I?p(=&l{CPVle`ebh&n9=8bfx#>Z)2B0KwVhLB>_pSD zopeiFlC?seo76;eaId0;Y$dOPSu|4BPV0tj+S3|z5(d`eQ+Zs!CXS|GNy)|ZkA&@% zW+&BnhRpuQhc$rll)l^saRpaAkAD12H^#sI zR3ef665?!2@BV|$&yckKPsZ>*J=EOWSFzdGk&7OA&48{Nq9&@J8!1@df&1$Sa8k}S z_05_bboN|RPyL{z5gtJ|6^Bpw9g>Uy(JNvV%n_Q$&d`~zlnM^K@fd$cIN#CHd+$x= zlEkx#sbZfdNf=vcp?;vh?e5Awpz0Y<-J4>0B``sGriD!>E09bZ8BiGU-4xn|or4;Ax+)d;1ieh~)V{KS*4^`AqDzd>$*>E!D z@nmXkb*!YVb!I~jjBEW>r=j=AatnHgYxRv=5!KdI&6YMw5OP6g84iljI@M@5iPI6} z8;;2wAGwyV=VF3UDiO_PGP#sEX=$xzXXKbA0QU3I4|z_6e#pZSTbY8inz`nJkKsOQ z{UC|KbAcu63>VA{86pVO#{Bkc}ga`iR)(o0-N}pl#mNc4w8nt`zcq|1QKwJeF0D(Q&aS%ymZ>`0<(E<-?q;KARD3xfT$Hl#S{7t=0jic}&gdI5fMV$tl?zP^9!uLsko<$;!i)SnO^5B~eg+83t5;{$;id;~ zx!XqmA9rsVR%O?<3*RCLq5>kJl8S(Uq#!7*0@BhYAfc3$bS**@ky4S4CDJ7z-AW2b zcT0D(Sk!`VUU+-I@AGUgo_Fsb-?5MF4-XFIT5Dc&jB$=S$DA$*$LCR|PKh=#t_a8X zmeg@NKLDl0SjjOq4Al0Y12><-a|r4nZ0M=)SJ1528Ju`tv`f};Du_?ZFYubs)QjYxM46Cjy{?B&l)HASg`TKCGTKs=^pG>!Uj zew+Hdc=0U7LF}-_*tIm#kQ@%yI zQOY4>icxg?NNalU=Xt&8-5tfLWN;OSb20VmZOZIL$4>dWsrll0Hz7dO>M!U@?VMe) z`5cX25T3Z5f9G1BOuXxirDfwOdP%7Z?{4>Fo;TUwRt29c>=D$; zlbLt@Tt@`qbkBZ!xQGioVsu1->0^3q1ZZ`np^D&cojZEprXO6&-}la;P#krOb$b{o z9C0&Y;Qq1fZ!jeTQD&YpDBTQCECP@(vXo5ufvTSEWml;(+f_dqQ!Ro2^cg3x?}Ny$ zd-K!S!1KpUT(knjPO_2meaV@!q?fal!+ZQ+4$}aZTZfPM3uCye1b@+Z{vKqIu{I-$ zd^1qju{86fkGwv^$IIU3@dTcU@>a#iOkvVL*Ia(9z7;$4LP-}@kR-Rg+I~`DP^Tl% z_Y!g_K8$8E&xGor@Ufo^r*i)}Is2PfBy!Z7s|!}N6-gmFbwdCUCo#jTcxfxiG|g<) zk>mtvca$`arCxe^gM+OZez1(BcZ)QR*dRhsxQG7e0g;lA;teiO4AXt?X9|CZsjB~7 zX`!`+0_JX#crcK_SYY!6{o_g=1-)U&)}# zgi0KVaBWIE3-dL~3)p7FX*{?tre@&r%YyZP2JgQeBGCYpD!n2;5qD*66b)oDg?Unc zpF;1_LmE;%0%OW60gWT%4^f}E{^&sep=wn?^=xoWA0ft;Z#~jq2l2?BvUGBc`7nl= z{GFI{x4X!jNvV}epY?0UZ>$FqR9XvYzgLx#lF|o~UN;G<*Z<6=VBfFCXrJcsuo|0w z5b#2VHCqq^^< zds!KaUYOWWn9h7y1K7o3365}%JD>lUJ^lW~yQ-k$()QO?TJCFQ`>IMo5V7kP^}bTU z{;tFO>bs=)s*cyw1ymNPnY1}>D)~D16F&O$Ir4w&e*y`l8a=c!<67}mRlmvW?SQ;~ zmqNqwH+fx>!rF18l1XxXi`i@7kzv;P*hN7zx<-CBlop59rXEjN%>neMcomN*eZ zlECuu^G6RNKtW5iV}*bYFaM5K^2KxS86L_Wud9C%K{SzOJ#Yty9Wx!C8@6=L?}hw{ zo-2ZX0c_5D0wZj_x3(nRa7Vcx?FZH)FhTN_|9c8G9k?0dKg_$mzX~w%BA6*nRZP@> z)1F}aD%k}L?@0RC9Jd@W{CmXOXf-%@_g>Ys+qC%cQHvlMDlFsCv9 zT9jfWv|=Uy-R)*Q?6@FLCb;vlj=t@|m7|T3rV+hJN=ll3OIbhVU{=ZhpfD>CjCdyy zX}Ux;;N5i*WIFb8tp};lA{#mUJ=%D)-h<8cekti&RqVDwd;D67@Jt}n1BYa+nT>OJ zJ*0@x`1F99M)=+MarO?55>SA8#Ld)~si|8aO}N%`-dk?~uWB6J%0S99_2;`L{ zkKYWrar|pDNkbEJ4V<+pK_fK4>R8`|0IS=Zl_WWSWR0j1Qjk~Yfv48nCJD{NxNj_WW%SCTxqGkC}AVO3Sa{jniY zMVsTbf|lq3w0U3(gSAi}cn8$yYP_mGbtZ*tK|!(ik987%Va45mtim6R`1x1@bkqwE ztsw*RkA(Sfv;4nw8BZHDhiT`uz*Ui&F%kOQ5AFn-9&3Fk7@+JTvJSxL{9|x#1zxEB zai;D`0u8!f1M>KvSOsW;csRUO5ELi6Oze8>?&4h&0|w0j<_AHN2QXc7?yNkT%j-W6 zwU-?+C}Qg`ADK0P9bz&Ddib_;w|ytQ&0JQ%;3<73TG2aaU#<``Ztqn z-T!F<6P#!Qc@za1nX@P2vPMXO`Gy>cCo6^N*xAir@Cvqho5%X1Xo{d8lepVixwT`} z^flz4nW=xB%8O+gr#!|Bx~!5##>l<_?t zX>giFJfp&~mhK^P{S4R>KpY4fB?g+FlcV^*;iY8R|DW_ycU!4YwO_IfYu{Kl`$4SC zOFZi*ZAP6Y+TwJfBIS>28h38Fjznpr;Q+ zYMjfc<%XSU?}s~frjiw9P8as@mf_<-T1g1vOy9I;XLEq_i4-jziz3c@Q(NUJb(?jC z=mt^R7a0%FTP7N}`%ng6;vaRZl+-M6uI&q{_91ux9kJ}DYx!&-um5ZyzS#W-&5_-1 zmJqf;>i~S)V+b+F^KgR)iG(p-3X-CNq zfYyHci2$vIoE29|_nNYN62&u1teHpRwbbS?Ph0uwyFCHhir;RT?Qn0`tlT2tvJm5i zx*%%}d4f|uUmI1@6MLiFrT|sQ`Q<`a8a*_H0*bhEqw_z%y%25+%^XCnsF?N4_JUh+ z#p+!7?#}n5X0B|I!#k=2D7EFD2Zvlg=-Xd{Vyw?lj1`3>DyvORy$jCdCQ3d5b-tIh z+~2OQuJX-bU7_7K-FaTF6M(b?ry?f%c+GtMR4ct{^)TS#4V{c|e=0#mLY#niUM;K& zy{XyRGZb!OB|nR(&$lt(T*9A1 z66GWGU6G(yG&R`}43V;K+b5~bR)6_+E zC#)9=mcF>4smm#66m05)kZm$sQXhK4pfNQc>Cg{VFV9HMccr4Zv@wYTxn{l8!(&;M z2OUjri|rDVu`c7hwkkSzgEXqY=?IocXT=V0er?qZjC59Zi|(aVwxhXe?{#!ZfziMR z`Ad?@KPY2cd<^31Ux28I3*-pwX#|X^PF=>49uD)#+Rbsz<-EFUI&I(cY zLu0+=X+O378J!Uvk%{I~7*i=2M+IevPk0hQ{Jr(QB|r)w8hK;vVe z@g$zLTsMJQ0kjfhIxWWenScxB&bGn5gL$FgN@NP3$zjoZh6DZ)pOPrFQA_K5qMw|u zz%W>#nq(iin$!3~LY5!M&&k86`5F7S8JECQwS&7X&L2WxSQM#ij7c!j>`RPup&M~ zC$~pnrYvsY=J*S}b^WI4X(r2o_@upk*Kd!q#0?{H7$3(=WQ}}rY*2L5Z0AW=1Z^>)_bH*IvH{u6%LJ63-2^aXHH`DUOd?gj9GV`cshN~#;Em! zsS1({-Tf-rBm2<9!Eh_Ocp3#J`FfJS?<+-BJ^vBD_7pD#E1!nf&FIv_9**WBXuwLb z^5Ew4iMn%6>FzWNoyM^Oc2W#7+v){NeQ~;~ajs5t9mOvyGL!0+=^BRIl4OzKgh$>Q zflRMlz@+bNq7t+(yPft>*1F_rH}9^Uskct~ZfjGPgoQj#8bLv461UrzyH5SN4oDf~ z?N06=F)U&BqxcPxJn4w6d|5yYMOEchrYWaglNV4>jk?S2L%n@li#J7A-n1u$Ejgdo zc4YtMbXQJ9o(C$~trV52sp1lA-glp{OSu^0fO%S?!oVD3lb(4=XB^F0UJ2&3RHZOh zZ9{>P0!tp}?EPSs&2?3`T!EJFP5SgS?cxMFp|xjbBypNG+`39$*u|QBw4}L{-K$43 z=6Mpg*+$EKKUTrJgxDQ613Wqur7cCbFV@Y+Kt(e}yf)>wM0|X_82?Ah^a)ZH0nOeaUJ-Pi^#Bcj9l~{dGPb8e&)WDc%@$4; z+(Kt*mok8EHTR43vFcafrP<3I=D#ksCM_r#T<-LVd}&M(bH^bpE0n#LkOxr_ zCv#D_&Us*I3(U8?vs>Vuo#`HUsf)1+~*=76BS!~>Ed07&*edMMOx*Cv?@r0S?Qc@x*cgwOuS>IvpY($xDYF8q(uf8GS zMeZr4lTV#w{mD~?IM{jHm8R7ykd+kVmL~N?Vd3H}3uhzlu|i#U`otS_w+X@6hG~ye zU76)@WQgKs+|}s{DhgBYpC7;7OO(sh8A8=}}PquV{4*2%lFwAXiB zZ}M?lsCHJSF`0L7=9!jD3BgD6Knft<1^1SNvW#{uURBX&K^N3JG3n4*M*lf6rDVCp zmGo8dmwa_I-`R1mx$SP&eI7jT?UH}q4h3cg$}DCIX!6gyIrR7`F*3;Pt1}?>QxO3? zL%FX^GpSSAyw@EspXqicQZhKX zk;r^N1*M>bJjgaFr>4}*GWkW94ML>Y)D!F)OXM4vD=A`C#l66lTu{?35K`P)>}NqC z%?zmr%MJX7+z&C+6$9!Ot}pl3W{|s_Jc?gHglD+^!Gz0kc5gRFU(B!Zu%Zk@FpGPz zxA2mTJz{CcYSKzUFEs|qAglE{lB=tG;KlsyG@7n@JpS&2%cAOgbGxQWj{=A(GXvzP zZd#P3FiD2p(D|rauY1gX-u=4evHGLmrci>&w360=$Lpc&$?uF~SRfD7oTmkOpq%5C zE4Dt7PrtO?qyIsc%Cj^=-8VWk3Rpe1i$l#r$o&S1U1en52?shn;V2%7Q)QUX))Du< z8c=dq(d1_(;c9;|u0S+iPO_Wp65T~F7z`25@aF&m$*@a;XmuUI)TJxRV&%;SKQ9u^d#W(9V|vqlYV(BKGGb>X%BOSfVSYYSPLmBja&PY%4w585s3mo9Nw&=-uETw-Ka4h2^e zzDa-Q@fjbSdY+zV(yIXOvAdpC*`ql^v1uRO)WsvKW6VVVhCHmBr83Ya_9XoF^&OF7 z5Hwi`c^Ytsf?6+M=mZEztJy$>VgiThnI*_261D4F2r}&n-eg>Px1s!Y8LJ9?iZGa| zGl~Dhf{8@HW3AHNElu~emnJ>Wi7ANExS&~Ux83q5a&e}tYL`;7%?BnWp_mb)+&s|w zJx&+VUih2*h9Yxw8QS*cGTl^n8@$|8+naRhrcuM-N=5Q$7>kiye=$blD2z}{P?vPm z!aR{7G^K94b&AkQmq9)ziAK?k$3oqmza~#-!gO$eT5TpK$nM#w+$naw zOiNp1XXkD{0Ec=k@I%|=^`4k0?fC%)9lAJ_FI`}|aAtX=T&>4)$e2|J8mhzHT53sWfWB2}CV- zUtSf^32qA>Ipxi~$#Sr2uzN^Z50aQj9vj>$< zTICu*of?B4Rs7Vo)NY+d(KVZUm9RFZWGnL4o94^W8oi%v$cYZt7xYpU(m(f?`VdmR zJ$q?6Lpw$85$M{3x(_{c_C%Hv1#t1*w$VL)nV_ahjf(f3$Ve8nkdTmQf<}VCC5`P^ zU%Q;QZNWXXmxRXeMerDXc3N3y50e5JNW+lRnFPQ$-?b*KSsJPk1D8S?>o{yo%cg2( z7Jwv1O1S6m6Aj!-H8#kf05Z*{Iq6>(qq{GvcVC{P;0dAPcZis|z~!~tTP=|Q&e7j3 zpU=vwP}dK1R^H>}j1G9*m|jp_>u!CmK_ugH3PUO&n;eOWKTIH?(gV-0iPJWx1KN9c5~?s#ze9rbEW z>{IM9s^V7iR3?g*jFm#e_Pb1n_xt(D_X`uTk~*F=MJZVeDHLS5=4{$vGD1pj1WJ$MD7xvzfc_7kPUQYp?o8TN_D%C2; zUw9kaa`$v{C7W*X)evNwMM1JwSW8E;1N%nWFUswl#d4N5Du9GVRT-iigCOKFwPpW4 z&QYDgeKLiU3o#yJcbdECqS42-}sNR~iSAAuI`wYx;++BX0E#Dm{BA z4_g?#V^c)1D2QzPN$pRhc91A!z4=~#>#L4&s2@%?%T*l;S!|LVTCdmFKk2OGpHM}F zJv}K}LW|e<{^4E82T{YLPAy!)5%j(qd`3o3(Sl{~Z=;-IorG}@RwvN6*JjeQe9O^J z8klX94mtKe)6x$p_sSMSb}TM;aw2z>6CUS$Z>`uXzr|KM{)TTn8(rfPr2bT> zB#3{x>?VQmJ|<`3b7{tAiS6i&oWO1Lkljqo#QsVQe+knbHXaeJh{s>xoZ^4^;UmY1 z6MA|drfwy8Sw&bHsA%E$9&DF3si`w<&9zce=wE)&n-0&JE2#yx?mK2 zNrI~UnMpfU!KhGd;S9AC-*zWbo9M7?;j^<#fQqHTX*_wMDL?C>(~LcX{fntl;tsby z=T@xcXRK!y>*pX83f6Xc%jgj{Yqh9*d*$;XIZFkt{#ushJS|P3cy6RM zJF*oqULDoB^!Lp?4ex~S4w@Hgj|aJzaaok@B<+`Ms%h$M`gc~oS7S)Rw8t4}judQ} zxYiroxz*#rF>JH!(Bg-7d)&Wwg17H6#_=Gw)i27Tgz0n1?tIxDF74iJ7sb~uv!x>! zgby0Nm(p~GZ#m74aJG%o6b?O?-ruh}vp~?49xk&a*xuSJCrlT3kR@L`>!8J!H9T-e zGtA-hTAwFwIV)Sy>?gk66uODGF9@FrJc;^Ww)oeklQ7fL-uRGPR;Nr9*f(O=`*^Wv z!@_QMs+>Fv!8KU4-k3h$Z8*1GFTB<6xNrqGlbcqi*XTtKWZ)1l&ZCLZr;wa^0;rm~nYEo!h(X55@)u zN1Y!G&#hs5r?x9qSea6~?9WhlB9=t(JuBU|G&nihboojA$XMTyr0~2-Odo$}`SY|z zLgQC$`$0O(sVXF<$mCgoY;vL;d(+5;2xc1Ie#ePACE3AV9T)AgS^B-ndbY~V@erLs z+jy#fQ^?;cy2^+B1|P4})YF7iT-skKP*Oca3vAu>7)+*Ry^qjy!x)HZRvf~482iE6!-LtqpwoDV&J+1L(~)L97EP6v*3-NbX1e_G2No3r z4d#XY3#?K zzOdIcxIH*(;ipu&ImL%Ao#Ba*^Bi6-T_%%oTcpeBe;RcwMw)+a*q70DIe~9x$R-M_ z&UU%k()C`&BNKC{R@;m%r`0&0@ta6NwiXL@Q*M<-D%YPUA{MmQ0`EzLs%vYAIF1z0 z)GU{-Xqv5ajvB?<*}JZFbF-UcUJH3}UOY$$U%9W$8k%w!51QQb@4!sn`|0 zlI4Mg&wiLKHq0IN!7^~~GfX8}as|27&x>ite7_uQsE z`Ov)??q2nVu(Y~^?&P&yTl1yZx`}*)_(7=#^iZ+QM8)26>x#6f3cgFI112qxw*+I` zdXiA!ZKc|3gFNDSM&XE(xbBxBjIC}OK4sb7auGV##UMW5mOpT}Q@w1K&jJE&=#=s4 zGN8WUQv2KWYV%e)pt{v`mo3wV!*->~QoU@d&lycgzwO!8XkXE3U(<|=uQcmHkGSam z2or@M4ezZa8+r@K8K_OvNND6h)l zyang#kA2s91F?`-67J){Yx!C3bVxrxwr$_}Z}t3Fmg)Y0r5=BIs*qi0rj^kU*&Ut0 z@lKZ!E;tj#jCXU}xkR9tqoOE>f2Ww-QC=RN9n&S$2iF?5CICI{gD->&<%V zPIp#H8AiFod6TvGl5=#*IDDaTIss(5FVk-96f|Do1wZ+7A|{&O?_^aj4CfZ?>(vpUZCvM=m>| z4)%B+=>@}J*9XRj$|5OV*=4sIwJg$Te+0@*(~CUO+_^i6vyg{F*37u|H+tB zVu|5+o1Are;5Z|Ea64M+bf?^I@CZvN)%U{AwTg-nMy?S_mn{4$Nq!W<(FboI6UaYa z(~2Y>s+)l&zSL~ogFW;;N@yIUz}ZH zOJ!^(hEpYMJQ+jJPgr-Q{X_RZaH|kKT_`5lY-TH#XufdFBlp20Re5E`YHDHlRbQyz z*Wp)f667LCy`LzUg~8|%5iLg?v>6xPI}n9#r$FC@QP15LwEufRuw^nBdIqPm%=N_ zfB)})-rm^e=w)k+6BTo6j5Rhgy|Qm;EWvLmLzu|B6MFA+Ym6mZ zs`}ULI*I|f)S4I1kllFm(l1QhZtme(qr>e#27u5@Pq4f#4`EA^S$e~hBpZ<5qbc|_ zqf}f*%e--$;I1VL3501>lUK%K4!37k+a8krFjTRalh4--SQM#<2}Gk%pRG9LGSok& zdU2HgAdYX!P~>n#3li47(BPZB2e+$v$zL~bT#L>>uocy#kO}h0KF6S^^6H~kwuXs` zUCNG!Alz$zWX1lnn^Wd-gLM1Gqar@)$VxttslE?lRUxzBHggtms?#**>}k038YqG? zWnXtU>4>O$M{t5y_9a!3zD2zt7$M}vi)flZmn0(*`eJPI!{xE4;PvPT6=I9k5BY=E z--GpK03`9;IH4!|>8B;Bck@TAs;7i5Fh>6|21hZm|5I&uk^9Jg#wRNUp{`|(o65um z#vO0Y;+4P1u8~w0Ejr5^a?Qa`C6p>#3~rZfy}#0(3G2F7l{YjyEkF2`|Fjl|4OSJ| zN8wmD3KqP0MPJX6<@(Ma-bFKkrOQD9Pg}x?DrOlpAIAUL z_W{igW#Ql!AR!bi<2yAY@D{n^HXx(o-4w#S@)(%#JxZ*1BGJIVe>rx#ni9Q0@hH z&_&e{>ARwo>}6&6>2s+G$E#d-0AKg0A5~w2fi4yz_4lkdofk$|0;IO&-A7-s6g7@< zVPktgNeOLB!mm2tWoB8zGD<`gy871S{h!x@=N~Up&}cklyD;n&bkdC?z@W8+nVB|2 zHJ$TGBMHEF)Pq!7%6Fthp(t+?IFRc9`dK)qtPbAYpq+Vn0DK7dr?|K)C-kh9I)@Yio=WNOsSw>d*$=CTcjQDw`XZTF zm}!+o)z|lVfHu2n=-a9`j7P$GS?oqKu(zyb`&)kacuEWZ5_u?Ps?zblwM?FHb1QG6 z@r`F8eJILTW#HQ$BE%pE;|4qf!7V)nPfLKgvvO*VQ>M{Ueghl7{njKBPI$Q<9#+xc zXqG;wc(f76)3+jl)E}SFYaooAKK*Cx{>q0BBz3+tE*)%AR|0<3slHFVDnx;=@$vi+ zU-IBQpC%(M?Xv(k;G=eNNLOfY7+w7bN}k^_-B88xHWw0o1^09vEiEnONmb7e;ISlI`#GfbXA zs6gtCVa99v)Wi1Y2SM~82NcWNi@IAD;E8DuKc2)GS@EEttB${ z^zg{Wnbin`!b;!v%{79m=(z1lkWhw$SJ*a^_Je_ok1F6Rd%lwc*DTI{WC@vGR@llS z>Z>mj;T&_@UFZ#0alA9FX>78X@g%+jO-9%b!B{@_fGVV|sZ7AjO8cualLABK2>_27 z;&?w1)Ej^cb|%TZ!W(+q}r5Cz`cjvoeBv`+|4>-=MzoQ0g{Er^KZyfro{;8625 z&~qO7yI_Sng5guDz?+#+gJ9Cf$Ka!-y`*5iLoQH?SQ=O#Zg3uaxmbFU89>3#*B}-c zDicUb1hJ)5Xw6nSt*igLlQ5RzA!2kI4^Qi+?k_nUNzOvrzvXgn9NcRE^4vNCNg)7| zo$T2lT;50l*3|>RPw+8pkk3j4HuNvS1x1m*%VFK zxD1qTsD3CNpeomiv2YqH-amutU!Db|5OkXYwNCFgm5Jc0C60e08xNk-IH9K@A8rq4 z5Zh+}8jlK~)B!$FlsgdhT*E)G$e{I?qCe$adS*H9X5e=0h$912-b`=%w}}Ws&ex z+}zyPXm31wD$dgdO!IvoNYLWF7h!zX39J|fpyJK`q+rsT_J-3zBuGOlZgAd7H?(es zHNGd{t((Yn5PJYyQ&9rf%GO(X&m}8XjY$wHr+0rRm}Kl+8+I7B-=VX%O~e8Q<9LI! zq@%t@5+;0TQ#dkDH2`ZBH6R3u_Ei?;8)v4yF|*Ub4D-^)FM-~Z&YuTMK+%ii;^Im` zI=_+j4LAoilBaufrOC<3Je7IXE-IpYT%I87bANjd2?J2vN0DWnbuIv?bGNf-Nr*yk zx`1^*!ttIG(A9*)<4i~|@0J(gR}IXvfE-jT2da-Lq}G7kFNezZffhJTO!l>c05L zZv+s;aR5ZM#w#xfLV+ZD@lFXGy_Sdjx?rzlAcv5fDE6nNf(P4}Ar~+SK*lVd{K!sJ zMftFRjJLmaz$vzI9Q20U{|)BBu@b^}EdDLU`3D5dSs-2R0$DX!8cWC3>DfKn+98pmeIpr^Q%Kr0>#(;fH?~`maAcRsn%;`X}Xm9v}^X z-w2CHO%NW*m=y#3eYTzhn^4=vLi=N*3N%0*>Er$4#f$2D_wMb;-FcX)8ef2{aB+6l z{J+kp)@jgcz2>qgK`oc{dR7F~6{8!5wSo7NRXB=Av~b4oX6<;-)md$|-gE};Xq7Gr zF6`p?j3phWIuSa!;`=LunixPEu4MdjysnWT*84f)xDfr|@EZv9x3l5n@E2dbB*^Ni z4$Fj-xe?v2JZ82d!eX;Tg|WM{LiO=m_wQmAkf|E0z_$ELGO6T$*% z{XIq#9KBt_y5_=WQleXeRQ52Dgvbou*>mS=?`@FR|BS|67e!kXh{{CW%C|v$#lD#8 zk$sN=O6DjwK;q3U+Z*#@L%;f5lEe2Oml)u{qV4+X_bPhMnOL9o`EQpxd;J4=T@@!x z1}A7A8j6I!G6PjC23edju0Cb4KAuFgen5PHTbl%4f)z)(%WB+?-&GeI?PnGJyJ@)p zRTu%uVLEFj0S`?!Tn>Ymmnp)drE0r2ZYoGAb8;z`hb~sYAhUpK<34pbc)6!)IC>lR zPvb=iE}s80zIfV`fcu~V>7~=wzJE&OT=B%&`SQsm`ev^d?=5x97<)kwws?I5=P(#G`Lcb+eW4xlxzB34gc(;$ zKm$NRK5~res7CmYB6_*Z4o!FPpI)gzk2$W!N!&q8T)YkQ72$tjg?yGeO9+lreJ<45 zt9|q76O|X8NwV548a{POkT$BkR{b&rvVo38NE@M`LwY{dCQADQ&Pzg}JdSC0^$&-Y z#EKz$&Kzzq@&O)&^g&#E7n3%pc|KFl7O1jp+V#Hf#(6Qpf0|`36MTyu=ZA7#MUf)n zMaJi80A4ZD-C16%zE&l0WeMPHOrY8^sDJO{{hiR~8Ct=ICD;rpP~~{M;a(NE2!2di zD%`#Uxk=nOfY@E0n~V7PVd~%psrS^bX$*{Efp65n{yTkFiucOC{|GK*2Yb!%4tyD? za7iPKm5D;nW;nq(KBz)}9x}^EphE8o{qFyynhz!EGjk@tL&>JO2kCIO0bb^UCXgc( zLI^Yb&dWF>a8T257C>l&_1U zcU;X1PeumnOe6&V+1)cF88%d=0hnHB0^0DxEEZnVML~*0#g)U1$8Y%QELlpqnon$|L*bD=xkP)SVzDXR^8zKVTuJdZ0 zuuASJd5xBK@omFD*%iOxbGmwvL zHh$In$2`Yan<0p{2_mfibGr)IL6rPMb4O-8GvW$(W5kYq4Ozl(7q#gtQ`fkxB3i^n zH3Nt2KuW4AkVt!w|4V)=Y6JV8KoLa2Q&x_Se14hC5X2Y*xHxKzKe0>HK1Kqkk0kK= z)vH&a6fVdTK=6P!1hx|-sf44~69#bkRIdPxOQaQf?1c}q1cq3F zYn0%RT%xxLBPLZf-N{66K&~Ey%?#I{6bnn^=oMFJ!j_NmWXL?Po`jmH+g&ODQF$S) zQX_ntRhJHacVyU#u$;eI#rTteWQHGGx?dh{ z*18_RmV!kU?hu^|m)enbbadi&Yv92F>%z=4m08>>Cegs*m>h~!sZ+gLp%y?nn5IZcoe)5RjT+~VT z{o%x%u~!s5=+3&^qX%-XKkw0`((FyW6!v2+a1?i|EU*UZCGH$FrtNU9jv?wt%ZP4~ z_OmZ*b@`n;XfTX8@kw_-4e@NF*=78aTaa7f7|p?Ia*<0*^nQt5j|P8iX}!%nIw1fY zI>t`Oez?}^UN>NtMa6IEhI+d4Ao`A-RHvQE%_l_EOC zMwA~!7xLq+oI97ZT+%t=1x^pml5}7CpndaZ=~jm{yWif7Ad#ipo_kMg`P#Xxpcw%d zwCiD_%L-{_`SxLVb+xixQBzB6ny%Jh(Ne+SM9DKsCI^|ZiCl+K!H$*nD==GzeMbx0 zvn1Nzb^c4Uoc%7il~(g6sFS^qQ1yHK#gJ&OJ1{g&jg!DNtWP)2PF^DajSenAoHLy^eR(G9fR6u8xh4fVJ z;7Xn};BJ@itc5LO-fuUW=w|3{6km`JY(=_Cxm8+E*woWdWR19w*4?vSv}WvbpODts z{M2W$XFhSWo>HxHy0@~7wuE;+P?=tRFWP1+!s+_Xy4o_i`}PEqI(OeRvuZVP@Zd7d zew)O^;OLG8!VDjCus*{=b3vKfzGe^5Lk~Y9n!D8wt|ZAusV=ne!l7ha+>!U;BlT`# znBNoLUU5Y0Npuk|hTs_2Q*X?vgHzDw>s1SKZ09=oX5SXpvm=T#n5{NZ0Gi}n(L*-l z5rh8NR!HHw4}4=a!Si9mR>R#n%Xb#K(1)%lo*c9gd-_VU$677BvtGqvtx_c=QqW$b zNUd_VJ%D70YOi0uenA%9c$XwyHRX8spyy=NHYBE~Bj2;FQtZ1nWMb6_ch$c(HhfdIxZ0hv~AihsHFTn5hppmMr~(5R)%Z3+$!)Z~7b# zdwbCCn=dvtp2#iW!ecHK3zWzcVFSRy2R=rlgydxTFg>d4QnQXTpn!c9_2GW$Z8FJZ zERAF>?7OOi9#E?{)6I3y$f>)$le5|TNz8A2u3W1J9V9ewVEz3YZWo&ThB}eU;*OrER=fUXal4CZF`S}Ej6eV@$MX)Q~pSIlp_f1{L^|n6MAa*Da57Z z;dNLRTnovHr8Yv^$O+<^?UORy7_yHtANS94r+?ZK9f2&FPZO?}KAa#$H62*hCUSz= z+Fir|-_)7pp3y^enp&BF-0hv3{e9k+{il>pUF|#eX~bn)^P3|Tsn&o;ZA2Q-+7y&r z*&@6GbT=$!gSlK5Iie*T8%pKYk}JWCZHCKUy7+9v`8lghwYYAR+TNzvNrTZc*1REjL{7!aS z?}20ozhals2}h3(4bBst06gSNYLC`Rw~>b!$w%(h=K_)qa+57CAFhn+imR!qMLYHM zueArcl_4rrt<{C!nkV=aZ}r=SX@B{;GPtum-0$*N;iyUPKVFkgz+BeVI~n-%Iz?2d>a`co)taW8Z)I7}B#q0AdxfCTejMTCAi2(_-?K|yg*_|UApt;;TOeS1eWUcfSToP&Q4$_XjRt2HEpvZn9 z-`0eu!esY75`G+JxmwceRXtgxjOJWy0df#anN}fU}RCLgxMDpK9t-<2bE1& zZFa;%(5b(a-BP24tH`){iy6wS(%0GlMb_+Y4FGp>p&{7>#37)W}8T?qqwDoKQ)=)mYW+ z+(H(O`@%Qe+WMf~S%c8lvbo(#0fs!S?8l-N_h;wRvveve3)b515!RBOY|U7z^R#rb ziMF2B*g=d=m#;18YCjYXOS@Efu`lTWQA2|`EOYFUfpmSK#%WbbP3IaN`-{|33(9Qwwh{JtKhpO-Vs)5UdZb#RXAD(8#Sw%6xme zE8yNLII(CM`_6I?mJSlm2a&y))IcL(^H~b*qf>i6!uw1*Ta9iDxQZzuLh|kdo%$_9 zx1DNrSSO|G0Z^0|?Z5XkXy)maDP2R|Z1Il|)G5fDB^b1%X&tl)5}@3lYh`=wK4Pba z(B55;5atuMAF|#9c)UHu?oMTS=jX>H1CyC`Y4$+bTO&nt+p{_LmU#+g>!j>Wq35+( z5qpDv6OF=$#R!a#@V2MpOq^va+7Ay8J$z_q-9Hu9J1Bo*Dpbqjd_4`ie>W%Z-QAkH zCyHi_@|dcIwe>KJi+L|>+{Pga`bkt=@1U#?M;~}(Dz5eCuXdJfDk)Y#7IgSz!ep+6 zCe~dCQ|^&ZA>`h{WL@RU*lmBa<4K53{tKV-J?*svG?mU^rh77)!lHbM0F`*J(>KOp zfC;gRS?S3#8QPX5!>#}kg^9ps_>v&Hk52Azzyw8<``ROW&CNG}YQ(-NAZNfdorn4j zZ~4^6?Wrupu1O%SY&3UBLR9+?{)?Blc-PO0V(^Q{co=%a@nGFOrVED;-8YjXyH=J+?= zH`32QabeWwz4pU>%ZeeBrJog9u42)y9W$n-u*DhA)7PLH!2_uB^r9WK?$d^k{A0i! zLEE|3A(v%ea7~-=t6cPMbL=oxXR>^2IuvkuH#Y`6rQIiSVUZ7qAAY+Z6v z(;Y}-wTUcH+mxJ;S>Xe7THp5Hqj=SYdvq7!M46o{JN9rC<7p}eHrFtm5pJ|Dp)3Qeuquk&#{~ACcvH$)EqAVE1E=v2`f6vQD zafGpD)`7!+^xHp)t(Ejv-iD^IT+33^)`BWa1zhCEKNL_`euFIy2FXJu*nqFZ@j;f< z2UDO(eMtZAOx^X>VC4RdkuBuPj}_C5`vNnIHw{{6mO1Ns9?8K~yKUcqHtD^mQaGSg ztc-hZW0g*bXeP!T(fN9GwWPN$r{1+>E*x$uN#wCUAi3}x3aNlsyl>aW0q(00s-+Km z6dB|=(VuIuQjUJ3wo<7p6IGyzi^0DK#}&Y?V}=koQ`MmH0~25?EnsKNqnPp3R2uEl zncIpoj7yRgQ#0l>Kf|$#uKPhGhx3{&u^~lcfNd&bN`kep3)f0Jq7b3I+8p#f)378q#1#QTNpYZfcJeh}3 zDZA$HQg-SGj->?QZc`q1R++&$M|t^H?8F;rBTxbBRa<}yDFGk;Do8;9AD$R_834w( z=`m$mU#LK}G5>9`+mRIXfdm9WRnoc}j$i_cgkPXJ3Xu1UVx=V|pp%4p%dJYJ z`USrVkbHvTbn+Zc1h!W{g$uju-OyAFnD2E( zBTB+%UCJ}$tPS-&A(`)s#(vDLO?fl5pCJvl7 za^F;m?K~_sYJMo$_el~ED`iet(Wm39fL_=AG!SQ$^0~}B?)~uj3GOpktfRUD^~q6h zv(r0B1*1NbgYN9o>m2z6Q8J)XyB=3Krsjvaozw6+Tqj+mZ%f)O37{1F1 zELp63)%Vugs{IVCT2Ah)jWpkSDAspP`9SVhLo_!1*JR4SGXCEqdEr{Oo?L%o{O9tU z2N6S!)8^@;9LmbM-EctCJ+0fcYpc@%;6uulK5bG@h%fr8p`>Qk++m1fBe%nV)H{8> zs|YS>$x9Y#e7lU~6;{-18!(geYrdtCC5U@RRDvFKmwX|vm;&#}9o|qjZD#NfGd&zI zHF{9)CcE*7(qVt%ARL@qConNbkul3vFAb%*GPRzdtqPj<^fHR3ih$6v2na3Ws!@RL zGK6A9&Vz|`E-Wo883j(%g_TlPK@K8PD^>Bd@HGR{hYKv z*}jJf96`iC_n)DFENCdeW+k|^6tpTpPkBuPnsZ zDCS4OBWh+CK>nlQ6K1Tc=L}y!$MTF0?(WmDPKBsKX4+>Pr?~`RiYOeX0A=!GHhx(i z2o%pX5Xa-l{g_QUYF`r%W_ek0zjK3mQsiSb2#hl$IDXMW&44SkFQgYY3g~ZxBa(WE zxExVjX+ZKYN3MJd7x##_{Jct8F}(7{YCFcZ>|bbxp%<4{ar;+cbVlR_2t-a8Q1aq< zPrSz5F9D+X#7OIURsNj|+!KF){r?FCF64f)OoYB~`+3~@v0JuClZ7F0SDc7srfV>zd@`&+#E^aC=R2p_}eDmFG?Lnkj^zD!I+cI#&RWm7% zTY#Q&7a$669dHMrzPt+v+D0OI(xVKFP9<^1aNP3iEq1KJwn?%6%JKkFDr#F6nWfffHVM4FV7 z3GrS}g0tl=@LID-uH#-hwft9%j`oD!K+hwFdrkePs984W$-(o%*Z(IRhG~BpbXe$oJ4O_w)#Jjz_Sw2@pJEhYl7kU+Yk?X z`V#o-_r~Ye%>K{l{-E9ms>_bJdcOt{Q~?X_WD*=dqzwxQ>`YrO+y4oR8AuBdp#HbU z!T@a;ns*rZJ&6}o9N=s9!o%w7k58j;fwwrcuTI3jGMyWRAP1A&=mT_m0m}c6z4ri$ zD($*PM-)Mkph%8JRI-wksEja*iisdmK*>=gXGtnaMFBxTML@}ju0d~^85d#moPTX&}Fo$0sAw!6>s?6mgUYk%YQ{fw1IC|m<08Tcy94_X7N zulMHYlH&{lQ56a{9aBQanigRk6FCRu%uf3&lj*?AYrQVSvNzcI9KSV})*!NvSp?zm0k%{qIC8{R- zV?9kxS;YGi1s6BK@k)g;YY!Xr?L~2mLkHH}VlvRUfg|;Ef$h)r{TY~!+^)7k?`VBE z8qr3M7qOP}AxO?12Na{9vd%QXr$k@f%6Q=fWc|RD%eF8r@FJ$85JrYv>O&e(sy(kv zm^G<-uF{nau?+6H<+-rVi)G!QmjKr&`)ytdTxEW%*k%?wJ6ffDvJ43TH zf#q3KxD|WF>oMh?c>7_cs$DdOn20#AHW))38EXxd&t*dXBo&IKH zzxxd#966)ZYl(@4!4+Y>h^2E^|;LAen$KDYv-tfG*L@d|3ic;FIu@q-EA<|BZBlX!8I1sz}*{8>tWRPaFZ z4BjDNoQn(@*}fJKj+DT&KVf3C6e{MvQj1FJ>Oc5DCkPrH?bFlGfEDZt@d9=R{i7tx z!i1arl2=iiKSPvWJ82ReXk|n^E?j=?A=Yc|*6ht6kq`dyGBpe&ugOL1P2x89)7IcH zmAQ(h7eQyN>1E2emtRLK)!*CXX?K=kd8koJbB;mGyN$?5Ez6eQuekYUn6z+dLxXb1 z58Uff&b@X@l=I3PkpbxpZe3N8Alm`w(aO!EsZDE0)S<)NvotU?$^u{+OpmUkJE6Vl z^%)@mMfgxh2p|Jiew$?9`#p_FA4#QOEtoxrEyTM?aJmlN-$K${kwdwQA77FEX=q^l zQ>>6)rS>;2WBwxu*v=wI8+*uYZP?V#GArQq)5%hQ#!4c2z%)tiMAG3ivAizu03%`CWT?aHIKuNda35(Hp$aK{jj^Pffr9u%PFf!sm| zAqVF;JPzgGe_Y%@j`Tk+?vE=7MDqWeTwIWgLUmZ7zaed_Oo+&RQx>e*tNe{vf;hzxY>m^e95Ca z_gb41*%3EmMWjhgnlrFNi^UkGd^buJ_u?e>_q%`?ar-nD} z4@II^?U{c$+Tn%WxHiZ0ro}jk`;5o*R;(hSX(}cr=G5txa`&9ASe}7F%RjF8-=CJ( zcBH%XkX-f9qn9wQ?DPiuCH+Y%sBd_CZH<_3PlkOE^Au8LzPmvnwDZ8Z5e=_J#y5UN z)Zf1QeIPv>evMp<$NzgvZf#nJAL@1!{#NMTte0PN3kf5K5PFVt2M~;p(w;_;4__E= zwP{(WdG#F79?5HdYi&#P5k0x5waDKMVhqp!8}RtyBmVZ{|Gxts&upU#2V-3Ni}2B0 zB!WkH<#;YhdO-HDY)+?r=NdgttUY#cV(N)h@kE!d@t40F;g1LD)rX{)KT6uXS0C;vIHesgddn}#q{jJ z0GDK%vqjw?2sl~2>iSlZs0_Ty-H5YWVn=Y~U~&(>Rxwaa{G1FER<3;*83x_uC2{?m zLU~fHwJ`-IPwWPUGZzP>t0l+POUZhV4R$SjR1B`s)HaIF?(;KCKbS{>KF1V-kcQ8b ztt)!m6?M?)HmuxmCluvPV{Letk4& z7DMP7tjw)jRdDN7a3VX%P5kUH8|jcycg(CRsC5fvbyLgXA5ZbGFPNj&TC}3HTR{H6 z|I$IdZ{0B+jffoZlbj4%22s!NC#%p2T+%Ff!wTYtW;M|+$}69*7(L+>bS#~jwzD*& zXquVtA-El=lOz#4p5+l60}Mig#MAJtViT~PJ#V(Lx}S?gG+KRP?_jkPwj!lO9JMtZ zf3V~*B1`@-TiLk^*I{O)azng}Ib7TQWKGvq?>0Arc0Z!o(|uT(6nd++cElTH$8UOfVd56|CRW?eTtJctsHf z=esCi9<8>`zETcyni=@I(hVA0eJ0ACe8AB12VLN~8M8)RTYNqbDH^jfUSU(0-}^o! z(Q~q#KDa)2c}Xm!r&hc^*UTe6v+L$e?V{DGDp&>1A!&}WsDX7H_IIq%V#rfD7zn~X zRJ**%hrI5b{3V#d(9?1rzMi?pMxTsvC_W-Hxw>a$If#vlQEErA=C)6?#rWH0eUoK9 z+Aw9nz1m>CqjZ8U48-5C_Sx}bQ4Zm~3P)sJnY zsP7wR4i97xUDY*Dh$i?n4{#Ar72N^Tdvwk?ynaLaJyzW@WPnB1>qG4tXvNDax?J zMhcT-mZ(BPj{0lUh;l;Le<8#2h;i{2fnLDj$hf9L3T4?i_s<1pX@M+93_lSRt+X6* zg1f2GX3C;0b4)HBARq$v5huJ~b7m5LSJr>XKYr?$_5s7E!6W!O2|I zjjVVj(utzwX#dKCx>9!eSey3-09o3glt$AUN4yNyT@J}86twW&_c6^rI7o@w!>2@LUY z{i5v`SC1+qx`{J(%|U2`V~X-tDNuq(mEcY5q(~gQMK!%q(<)nqNKC9wm(#Sbn$9gD zd6VjRXD_Nhk~z4^W!!8&1hnxh2pCdX-U7@Xxc4_Ga8-1jQVoP97B-m(&mX{D)p&l5wpY)FV#Hjq;UBpdk^^Kg6eY!1ck8%`Q`ny_g!*c&voWZGTs+D>=!ntA+j zn++Bmv|Gnzq*IL)n#doWY`J^eiBNp^H`kIiNkVl>TB?(5PP{!Q$tc>+Qg&0|E?l#2$)T=Mor;#1-W&Ar0|vpz4tGY* zhLkDRlN=Z3(yfM~X>6Vt#m*(r<(jU{JETzorQLV3WJczmM{jaiAO&f$gh0JKW*ckW zZP)5wMkD*$gG80ZEwof|x1LZ!DzY9n5*YPh@=zk4)jl>KT_UlYT!kWK8#s~uoUTX4O->xwIU zwl;2yKUZ_Fn24W^Zpp}9V&T^fdc=tUHFp^gv8&tIVwYx(<~oDKpSa9xmg!0@4i%M) z$#_o+`*Vin(vw)bIPIL|7SbGwq}grC>1USHN(RiX6xNURGF2_lnU!?mXBNU^b_(P} zFNz?$v^uAk7;V4Rq02@wua~&8JgDqZ+iQ4yaAv70x5wGc@yjm%`P@S*{zr7#VrHJ` zFSi^YbSyKg$)Ku@N4dXf5WjZe7EWuLKx1SBN&s$BS!Tof3nq;rYun_@m z`m>g2OGhkOfxecZNtKmcXhW@IH`E{7MmwQ@z2~S=asF(&7N&$_KEKzMRN_#y(q%n+ zL3xC$idM~SxElAkUM9yQE%(vzwCAMN>e44`qSD>Lk%|C{vP?(fipR1&qtv;okj2r4 zkj0tFGA_~q60yi2mJQ}&nr7;&H)6R+pL?;vk`oagOx3n{lBet_#SNa~F`vl~m(JB; z0zBL%X2SgkNzDOERlQ4J!VGLj5;$XI%I>ndgkXHwn;Bf zS`5ZK@{nbwtK?V4SYN-8-ClT9#$k#`B~U{f?(gZnxf&B7u0?#Wgx$hp$xiC$axivW zwW;#+m-^yK6eq0uHjY+H{x$3PV{+==Ic^m>=9vIPM3A%l)3#g@K$t??DlGDDO&+ax zbI+R&(5>;}T)Ic$udJ=*bGb}kQ)X$x0mH=U(1u$HNLwHpm6a^7nvf*i7b*nZx6?Qd zd9BzoU8^6Gl<17Kg)7!T>v44`-!Hag_6V+`3d4u7inkuSQZFOmI;Euh0n@7`mq&76 z_N|tgdF9?aRd$?$tFbp`7Exb!95@owLt2gMC8|n{yjLFPB9LKcWG0_Yj$l>_R~Pf_ z!!Y{Htu7o38UDVY@onrb0ncb$rC%)BJ3S=YOveti8l{3;iIoa84vi(6fuU|an(h%J)Yo>Cb#PefA}H*URZwN&?hXvDW&e^3J-WydIUhr1H1^5gM|6 z_d~|jdZfR;ukx|2b5~k;uk67}2sg*dN zE#VkZOV4)>O~QJVj5Xml&>E+TXra!tdll}w2h=V*e(teTd)?KJ=WU-I(6rpLEHKe3 zE1X5>!M#?JWtnd?2wBbV@edFvka)5%9rVbGDTZ|4`r~nmfznSUsVhtMvo3SV=cojo zrYemVDwbv4OHbmyChlaoF||Mh_d{!q{F07t}hi_??)`Jh~1X%_|;@3GetyvMbPaJoBLZM z^)wXuN}7XamEuoWXpT+l={Y+eBhKeysF8_KY*eqa zJ*QL!l>vFTT8ZoR)r=ZPB9W=+fa}C`b$=CIZdTFAhw8Gfu9fNj1!Ae<_4;>(i($Qk zRTNS~$8n8HGWR`VUKr6;zO{&5t{v zo=nHS=V7QRrcKVD>JyNp>F9ZMPjmt|Q;-9hQKt*wm$8!igqYb!p6&Qn24UDs{NrdeQ^NdvQjxB&Q-i5A7RM zCvm%Ik8+cWL^t4COIFiyf-y_sY#P*8j0=?{u97Hx!((0bdsacKUe@pc-?zZ(zZLW{9?$YB)3~8lzxz`Q6xIYXXC3}pa5l6T)?8pvo#7gYgfNQC(GQ@MH zaMwI<`xqJ9K9=42fl0i_(EuiH!~J>Fh*(CCY0JWHN!Otwg73<_%jf;AZga_M{)y84 z?NLv-q^4p#oQ)j&=*29Xj+#IYz;6^glQS8XRX0pZVfLf(OPmS_)v-U}`1K$UA-x^W zF{jg#N?D3HgoLk`y*k9nq^5J!g3_y)fnB3nF}CIlmEt5d((#`vR7(g zD6VWv6-J__&Qj+5$_KTyI&4;!KQ^iD<3_GYPxHcBdTgxhs*U`(04=59LYpGCl!B`b zvtRXX@xp?BR2}m<_hrqpe&-1r!6E<1ddc~GoL#!KG=>ml<*ek`DO+-c#Gczt{8Yg- z-xp6#OVULsSLj{&{f)$^U0CpTI${zPH;h5 zmX=vDWcc(6D(FK=*Y+IR+yPSkGx1xoQ;(_kpz_}-&%B05meDlqgJdunNfjVPe`r0G z{~UhA-ze8I#!25P#~-4k_cleNGSYEp1Z{n>jZ)e>1A7vL=ufOCC6%)aD6Kwc=C6|L z8H=1Sb2mJpV2Q3E1M2;I!D?!fDYCjgFvc;|P5@OSmra8zi(!3fn`}OKTcAP?bEd+jff>-lz|BWRQ<+Q>Jn8HZ00Gt>+`2OED;)!yW;)@-`C*=H)TUH{GVEg-S9 zeLb0=&ImZA6Z^Hu;fFueLt)`T=2@u)l$rthPi!$;u`xcVw;whmq;7}nlG8aZBr1A6 zpds~fAb)rAEo-G?C1`C}upjkQ>U)+6CE?JSG0YXz*s@G{Vl9;#3Q)t#Dh-1<-K|H zDJb4&ihI>0w-Xd{i0t+CwY5Q|^)W@eV3N|QPXE9{Cz};sZR#Npu2g)$l5W;>oY+QHJVO5)3tfl) z8?4ji<9P=sYB88SfFh1BG6^$xT^}6xy%lx%D4H;)DbsB|9Dln@as=8PJa!_R_!&Ou3vTDtqaIa(}OR3VR@fkBw%7`mHh4eGzBE%FjZ4J^<;k> z+V|7kdAoiuv6;HU1K(>#(7wFg@&fNlI26cv#l|U{RE|fu2TwCn@KUl}A;r%eIpc>nNIeRqpFgg_m<<^b~i*ds@trU)w$Qca3|flcU{16-akTGB~}uF zzD!pwjYe9k8!Rj*^mB16?g!6bNQ! za`gpgSm-KsLD|Nb6V58@fEf6-Ln9_d!14}Whvj`>4{WWAdoMajsp%?IK!``2#atnHCceGt~Vs_no)?uBeb%U-Td0~2qq z=1P*MP%tROhlhm~M4#i4z0K1xZa>6PMaa+3cXl84AsmC7vcd?FNO>WURR#^2fM0Dy zO-VUwBiIe%-?wTQbm?$;V8OaRp8E{yf}+(JNcPZr28F%!M@?q-dZW zT=*rUB!+gv!(Qb)|hB;!`b{e77MhbYB`Z`?u) zBoTb@R_Pf~LPowb{L1$39Oh^Niy^eHDhFz1$nh(F@C1Qr%|KO=kVBMI>hR(Xx;Vcn zu)bS7W9(p(4fz;&!{H5j!||v~0ECbw0L}>ta_S*2!$1ONyhes?R6^B|ziJLlqPa6? zAwTH0^ndm0AbcBSNdpz|J5_kmCuwHkQyQYGI4_a=6gP;&&~t}nVkdR?nDV^Z?e}_# zo$qBGj&)nN8PJYtj~4s9{hsqBly{pAUfh7|c&vo+L3yyqd2U32N(!+x!Pd3&G;Bj$ zyfZ7_5T=TxWff3(qnkd!b*MIuFBF3uMDf$`P2@yP{WMvDq;hKPIp7D}=%KG1h9O2D zzlNKRxn*r)@`^1K>$rRMeJ*pi^;$B}Jfp-@)Cz85EVoVnrZd*)6WR#WFUZX>L-HU+ zP^>1yn(n_yu7@)XXfSO-XX$dlBR%#>{`Z97C~SE7yp@ty>l$j}x(8XP!0amT@QUmN z9GjE}92@Lash~QGMyk2$1D~;7pZ4Y>h)QRH0P1;m1(M@kK(j4IU#l_#Ge<23u@Js7 zPjM_h&1b&+IL^r6$|4n5I-q^u#^6l4bl$X^Cvm5kvi=^F;WU%h5pfOt4N~geL)W`sY^5~F0(5el z_*Wc!a!UrkLgJ-$cB;HQ4&G}ctiu2=Z7IyE8*@D3$O{t4BP(KWj6Gf2RW3-RVIr5Xp~DyJ&-t5Qjk;<=s=u@Elds+BG81c9mU&tw~ST+nU4*&fWB3qi!h7&QusL z<+GUDWZV*(_*(Gtmi4=;4QSC#$O$fv^8nxPMx>;q)O{{7l&hQ`3iH|E{^hPNEosb6 ztP2N}%kBYirabG9&a`B022Yn4KA}Rccib?b;a@pLAw~+ZsM^mgJ`A;xV3EP#OamF} z#Kxa4PKLPi+Jgc%HyN-q9Q9Cn;GYga7^jfEE;j@pf{X!>smZD1cB7JznKPlHEdR__ zKu_1wuT#)kx)uA;*r$bfW>~BROHGE&77B1jL(~o+;U~ohjA?C&3Y5skoc@O>3%fsR zM5MkJ4;5+TJ~ALn=JxqsRX! zeE#a(&xFBSV_;@x?K>kb)Dp6raIu?cUzJ-(XYKg+z@_!@;#c@RSAiG*A}8Fr_A#(A zhy1Z;#%v?%Xc{vznkIb-^d^5Mv_j6;W|>>|>C;1gg=A#&f-j^{Nel_vC|RG7GZ?^7 ztwG3|X8@>#=l+5lSbzvC7+~nnC7W%(m*YY!I~n*P%~=bJNd>H<_T)YW($bsJ!s2Og zqe?dW3MIgrC?G6u@_BD)ctsu(E#W)bP;3|2)> zy>7BVBYkn>`@5{k^L%kkkr9b-NoB%UR!_pQ2hze!UgtqLMy}X&8eZ^?Y(y0wNjaWSl3Rgd8A98`Sq9K}h28cxg z9bDyXKLqDJ$=q6yKQN$c343ja8=<(VI3)_afqBaj%5YqQ6fASAVaK`Dll`Q5GUqZ|h_HftGlgk3M%aszq>8@WXb8evoOp4Hh@ENJ$j4nuU-*%wXHl^E#e6+m7A+HDAe`#}^s>vvot0tYtv zgbFJwpzT>yg-TQ_=;D$YRP*3cWyR(B0UoI=)?$L8<3$P7p4fh6xH$Ds;D+}P&D=xM z)z0CY)83BHA&u{`agB|NN*)Y9=eSmvSOJ$Co%G3OQ}6(KajL@(bvsG^*QQRfB& zk-5Q`SEt|t^u0CAVv+LBcnV1@QfeUYE_=r5pi|MygtVNRr{b}MC- zi#zJVl82xV71nUt!2+r7-!aAw4_progR8`LWk(^TI`n&Z{<)=rthpNv9<3E z#ecU&K*MmOALOUtkKB??c%Bq@0a zzd~-_8CNt9vi~&l4Q|G7qqGp#z=03vvnKap5I|qVw;kH)irY$_6@E(urCHMf-;acC zWa@da8SV=DCNMh$YdKH53rWe-Z)ISA#vY-Q+Xeyn?f{bfUyF|fbm!;Fd5HZtZC~pv z!4vMbH0G~KbL`fTJ;<#!dSs07&%x?#4lGkhG0H*GRhNcu-7gdN6mE0a$O)&KyxvAm zSmuel{gk>6AOFiQH271$9={xJ2L()Et@CEw;jV}N$T^q6IiFikmf^d>eJj{p0R^tR zhs=}uzT#JCN=H{^C_wV`cW@b9V2K#gvH@5XwlUseLKkHSLQ*7H&H25+`M0Lp&lne^ zP{tUig6si20rgGu)+V$)rOd?;>ShNrs~$qiP|p~5;n}TLNH!QN{Gj|GFAhW&b55{9iXr?D-LmLWvIjs+`vzOBp9!v6t(;fg;K?itv(C}pi zR6lv)qkshL;-WyQT0Gx=S;G*r;I^~7eal59qtCh1DZc?8l4LL*GSN9=;}*Q#%UeRr z{BWi*lPsdHD7mN{Vr#76t)cc>D=_#K{$0hc&faD&=52)JhY0AmLGRyqYUDu;v8idh zS>%RV@U!MJ)vg}OVIH@iu8%$^&OYC^(WTW{a2GUm-|nCbQx_4~_c!9qcQ0iifEdCp z=vyuxp(OvARJda+R*G9oLt~_JMWO#VvB@%59xqV>ow09V*!SwQ#)tI0P~3F`uG`j@ z38j)?R$NO~A0*upqD7jz2moCLPMXirg1<)fr&49C#BK(k|KjVVGj60D}MLW9m=g^TdZ6ok`^ES1~a^+mdi%o`gKEJ#Topae_(FPl)}GOPnclE(q1Z{HbyWmchu z@GI_;uGuvPT7F0xBjWlWY8a|#3rAA*WWJe zrnz+K($B&_NVZMbw#Eebp4FV`d7s8=)8L~3ylSCPhgZfC82vLA?v zT)O;5;s;yGGzB(W2d}f)`T!tfd=-zz`j8Cud}7mfy8Tg)uTxV441{pkaEEvKque*o zw=dQXBJ=VFNTlu#T0sgsTFn^O#ITN#(Mt`mx9rIe<4|B2jG{50Ql)knO%%GOX<#D&&i_oaKm5=dyHBwn3idUk5*5f8iH<6vt$=(=1NkGz$S$b! z`-h;|{uz4|fHp!D6u8h!{xkRJC&&Q+Zd#sGH8=TAH)2TF9OWVRF>~`)s4LK`FPaq3NpfsRJ!t-=qMAEBp4@*OuT7GOyHgYxzY@Tf^C! zwZYjQbjn7hNqsl6!%(8U=&}x>%^~*0L zlC;SUi|;acW8Ds}*HpSGJ|DT#K~~+U=di&ykIM31&(2r3UcO5vJA@^>yYt}I-P~kL z&u?AaR33cs#U8S|SC8x5ea?N0Tl@Gis%IY>!(N9L+3r6Ramw()nfb1oGt)JL^hTDF zgI$AKi{645TifX9=x)%HeO6dlxDZ}HC~qR^o|~IX5xYb_QGlPi8*nV)Tv{6^FI$MT zaI#}^wScrq zng+d+hi3L?q8{Y=w-}!z*E3tkF+BsvJ#t{AWB_-_GG|U1`y|C#=cfc=1y^x zLtCTa#R#ywm|-JAFPp8nmywl~RhXQdoVVTK%go~Wj^N($@$sPiCPsWVsJ2-PhA{&l zbzzUi0*a5~rle$>p@V<*>o@>?#pN9cKoa8m$xxp2$2Z7hMhGd;d(FwtCdYHv7;}Pn zCZNkloY8qQtPozze{p_>5S2`TsPy31J}U+v>8BvQzK)jVMu3-tN|jmYKyt#*7HpeB zO)df_kIq!W!nfPL*&|DGgPuXx=tOqWEuk+ro*9gMDd=RTW&&KTjIV$K?WY0MZ(b7$ z05hK|EhDlT+fXC~G}^nnpBZ#K40JY~0CG$1I^>oSK$u&Awbq3Miu-V=wMv+<54@DN zzI*KpKptJe{ztdv?=Mmz#+cW*Zuxc$@S(oR{KChcS;{m0{ZIZl#@cUhK_F%hb$yXI zfcV=sf^b$Krx4m^Td&P<>DWAVhAzsrF%t@a($FRkD^iymY|z^L#v0(%4S=rnbAKLs zuo3lxWqCIJe8Q2^X-*zoISQD}#Kgo2^GBC}B6`*O7|}reDQiKH=1k{|NzJ=SlBC#CSxA7RgeWMKpX zYmb$}5ii_B@A_uI5&M+(j{P3&BO0W_)ad+9yfUo%^qytzQy27aqSoo(d;;@KQRMB; zhgPgoAhcqW_*E{`**o|Ln_NnY5$|x=N~yTKtx)!#t(#d1@mn=Wv=BPGrN7 zuK)sZ-)UHwqQUN#Um{&N-ev&8zq9pWj40PhE)H^&p!>=ozYMK7B(|u6aYHwD9mPVC z^Pon@3}hw5@d1g37RVJ7idE!49L8T?JT*al>l)!yOJ5|s`ij0tvI)45J-pmCBU{ix zW+#Yy?+|9;W}i&Z{Z;VYCIfNH_kh^VGtp0u3)TW%DPwURy3%?0Sb3l;=>T0x1n5eq zc27B+B-eYUl?6`r=L{XW2C-3h8LN3I`!&*a#8GGiS2i%o zW9#bbCeqT<`V_kE|43%U2hM30wSwhhi`DgSo(NdKBU>r85pGmoD?;w&JggMhvNDv1 z#;*}UTC&0*cg-DG;bmcAQ7TD!1Y!3|XOLn4jlYI`QJodq^57LU;bakduQw`d?V}vx zvnSzzdVbO}LFyL?3;gNa+w!%BB97%xh$)nj1CvxJU;=IaaTWDHvdA%*!H42Zl>*Pb?EHv!={@Ca*WSVf?Jh?O$nd_UE}$kFM}gj$ z-2kp*^HZjRopfTA#S=i6S{jA4XZeLAe=d%J zQKoS66|o8?-V%y}{%l)6l*|K*sPrXcAMgU(e^khZNQLYI&0rRpbx6c~aKw6(;)TES zmmnpD2m9=NQoFF-4FzdYhb%JeKnzzL+IZOlnP>_96oN= zmn>B;(=oE8l8;Tk0|-K7hWxES#90Ft~H}S_8l=rD>ad9NVp#P z*MuDb_$~f5JL2qQj|rf~?6W{{F|R=wD+V@KH8nLSX|RC<0L{>*K%xFG{53F{el~qV zuw)0ipR-K*Z*CsULSBFgfk6s`8mzrMtwm~{yP*9vX(St!LdIhB=dSKT1o9PT)FChI z<~Wvc_WjGm0pR+!b*;no<%Fv)tub7;1}8)A)?GAf_6iCnwcqq1ogFi}Mu7$=f7DpZ zgu}lbg9hB9ybj1?N5pQT6@lCzwbtH(494_;KK}2i3j&a=qmTbR_a*lUvPmz5X1p|v zx)gA6pjEV93BW%PGXy@<&;5D0n~Sb8fo$S+JL0Y&Gzm^fI8>F<8HMi-IgVnU!?^(b_^6JI7)K1HBBZQ06>|?MlAkW${6=P?(BtZQ7=^$=egFsU+Lxa5 zHwXR0$bUdP2(%o|9K;3L;Nce}E-*V#$3IIlM06(pb8Nrr+7MElC`TVwWpMq`v zlYI(V2r*w8sJ|&OLgOfN_g?pzV1TOV`(#YH@#S})FX+G1> zrUFs-@~bz4!2VN;0}9UG9Uf|sfqW2^OgMY$=L{XL!BYM!<4ka|X>Y zuz$yPWtAIuhXhlh+yuEM7$jv|n?eFkQ%ehDkvE{)2sQ|X{lD?o{}<9HWITq$=jSZ* zo11S1v$$978~iGC)Y3pi3vylRtRYUcpB94T4$3E>SW%Z&f0qi_VuKW8wKiH2NGXCeZG>m;!(g7nRlsGxr7jRY{{>90^six@;Z1!mqx0QHv6I>9Tx#r{ zH$wM|bu%l@cy99HKU@d}2JlbYcGISyj(W3_N#j2ERK#WJM$?zLjh#9U({;RY-4K^t zWwf03gKQ8JkLo}7a_bSMZ|7SKamVOHeT%lbK_whutNage*BN4rYC%4 z1=g_UI;`PYh9NeZ5{I}T>{z>%52-FP^;U+UYGo%9Efe!kSnh+a1NPAOBK6ZHTbSc8 zpIB#(%3o`b z=ec$=6>Vrpj{BKhs&z(kvgA#fIsHsBiw~~@_Ksb+BZ6een(*xACBiq4Fs`uf;(Y_Bs|+1Vw(6zStYQj0@9pjIe>i zZ9IEmtol)A+4K@I9w}W%(r2=ndbThv*U~;2(via;_p}!7jgYcc*L65sXtYADY=L}$ znx5$jx4g|+RMqt|24+6?z$!wULAv#)+;f!lJbZNf`J_nJg*Uo|)`Su``;(_`fEb1_ zm3-c>km6s|(h(6j!YUiaGA=xYLHYhqA8+#8K{Ek5Rk&ug-UEJ0vao=8X-Rr4N>Yf){&>mhDPL z+w=Y?{V@X*IzWwv0630IOf0&UL6_(cZtKX%tHr43OD}i3P7jCQ#ph|gIB#F{dBG{l zf1H{@e3qrQ!oOeWM*>wzue#q#Lfl8}6 z6GL!Yu@9yK1dACBix*67Y>8wue!fhRm<)wXQq9vZ{ZW6(!2WQ@fj-FdS*R-&?-{zH zDxykLCyvh(vclCT3M1N;5`aO~SkoXEe5x%_>m$o|GULE?d<$rOC*a{E1zWamMrgdckl)EC{go_Qa`se$RwVlTKhU?G@^Ub1)FR}7uw3Sc!Vq@AF zHs0RVsOQhgd~UUewoIR%=|GX~M^jG*IqEjVhxrNoj>1bw7;oFE>ogZMxn#CDser@$ zdYfc-f>QeZ+-EgOZ96O|3P!L1=35{WB&6zJdG|a>YRPfc9Rz!c8(>8#Qy?yRl^_ajZ8@G9>DJH3 z!T+Av$p!`LxlgYR|2WhC(O|56fXESD1B&`zd)n(rk8h$3BnMS0Xk;I8*8pGo4URWl z2gUPzJG2uXtU~m+Qlz`NS*7c?S)dp&AM} zld=`d`1n8>8qBweQY0UFe19~pu`r&wDH(0c9;{VhrbkY`ZIjQwJLSFqi_Vsi0y&f# z2;MFaE=PVnovlhp@iR{D|D(If$dZp1oRQ!l5{ewhkA!FJPYKT{v{@p=@x)z01P+2A zNy3F*)Hxo(yEhYBB!(|j(PTd0Cl5ghp7X8?NpC96?=&zzmRRqBK9)lOfk*@pxC%8^ zsc9J6{pS{{glo=!7xhI0=g~_PU}5lueX1LCA5&*8AJ^ zSHeng%l!sV%^>i;=x6?0MEz5K_VzrYg&MyKh~nS+%f{#uYI0ZkC?Ti=JWiFV0;CRA z+Otp#ZYYFyRZ2l4+As##@!JE4Y>vY7wc=j2{`B|Ke!X7fFJ2e;p{WYwv?+s&0L=_l zLqEkEed`^!&}rl+-A`oRVGsXQUBu>XCxI1Zz7bjtUheGdR6KY+CpZ3n#7jy_qsa68*V4B zbN!fifVzePtgmO0mijZwjY?Vs-*r`l3ce9w_fwbPf&C7kRIGGPr=Egoj)!CRh{ZOtS`zyQ{;WWPSaqwaXmsB{IURn(P^TwP2tC1SQEQb3aktP zP;oW`o)x7ky3KkCBhQL26I3SFu^VLA8^E9vZser-$x{e;+)Awuozjqq6 z1R#@78G!zw>TA@F5;$^xMA`cx6qyS3f5NCgg&Lw@L5R&OWUYmaz5j)b|3b!pAp@M! ze<9;X?Q`|Nknvy0_@Cv*e<5S-nfzB}{8weHpf)exFmm z7F_6H#s;!%MAVoB5Ky zUg{%8mW6AI>!hufPkt}09YuA{k>ftbJYzDd-@??pa4m0aAaU^6&Xi_@ZB)5ejD|DD zot6Bz^bfwWi(EkgBawh-@EcNdzrRdJgDbsL`njGo>nK4e;BgM$#(}+VYxlvm|At;= zWoTKV_~RXnI#;{2vcKx1|6;EeMx-9v!r-$}igs{i?vC|`M=|lv>Yt3vRP+dB>sQ5Pt`!)LF!T zChE<{cXmlU>wKXEI>>J;zn<*u=m0uSbn`+?X`IKLTyOW;Ot*y`Uf=N+&4h!M`gH4j zl|6fqdN2+af6Lg~W53e++mfaoV~cCE^P60FdZ@vd8S$XiMqJiVWP zs@SQj4<{*^-nGL(^tW{$0}k5D{#I&pA5@dr$6PiZ+OmANZU3KehQGoffii35CPK0e z8sDd&oJV|ym86I@9!@Nm+@k5cv@PbZfBsK^oX|@IVzC!93}-H0z$^R32|8UpKdCB# zo3v2d+L@YrhId;t^EwxN1i{$C3OGVH;EF>q?Dh*iDo+T8A z7vf8JwCv`x?Sj%%`I+{gjohi~y4;D~%9{eJ1^QD3X%077yH1nc;e% zu%_)GyX!|Uy{QT&-rjKs2Cr6K59u!ZP7ak|A3FI~hZ5UINmZvMVN7)gt<1vt+l>zZPC~%7ufI7cSx0EA{WLRq=n&)o%)Th;j9HLd30lKu zW>f|v9I(f#Y!7bM%qXYk8m-aOLW^Vj#t2>vezqnFt=G2GRc=Us({~pa;*lp6$R#V& zG?ja!pA0LK#Y2~T^UH-{y?<_~T-gfHjFJKSkqvq&oL$o|qsq)|3r-Vc4q*mk6Zy5A z%8#8@I~f7i%wrSM?LejIK7RYc(b$8Qr{y9}2ygOIkKYq=npX24oyoh4+}GtK5_*n1 z4#XPzg?f;F3$tw!vA~l`8L(+lXG|ie%DuMjq^pcSA)7nM$-Tay{oj zbE;kYX{8`qCO4Cy1lCzOkb*NyV-YBv?Mv#9KElK0zA~y*CXSgK-{6h)#>`e&zg>+@ z@2FJoJqVw*>3{nya8N)E6A%}JpIGfJ=+mv7&s*&@Y~eD_z_f+yio24eF=`Qb?@cs zS(3*+&y^h;O;PC>BS@RIq*%AK56*RRIh={euwIx= zlT>35Xbuk7adxUGX{_8IWxi9@r8TLdcEO{_QLt5Oc~IZ?i$RDCW0?AlJ0FEtqV*oB zsM3t46x&A}zJ6o4QO~2-c~rJcchy;bYKcS~l*K()a#zVpm8U(L*Vm@1X|X-wY;;E< z{=32B?H5CG`7tN*R?}POC!B|#`In^&uAFro!YN|+_+l0w>*VD=Qj1|!9gpCIM%_Z` zT(ux(N7_RFBiKO2MHwPJukQHfDxvbp(Ua-#L-#W*FT7RN^t(gPGaqbZ>0Zv1T{$>E z*V=nP%ywirv31)k@!p$F89mNcEc^vS|7F2g!kDJhM-^u$=_ zV|)ce&!q{XP8Zu8;|Pr1H>SAumo1bpFLd@=j@d*V&H5~ML~6CX{~3QPW0+m;k0{Z(2!7@;NzB~q4(BZ zU}2OVmsicnI{9QLg@ZHx#bi7!M*8HXQymeVoBR)9aMHYkN>!8i%ey=ri*s?< z`g4-SBa6!uEBV%KYBaB$`6h~%CtR>$Tx|sdQ;$QW&fAO;Jo0vrl??VUET3l6U7X&f zhjEi-vc&Y)S$__v*+L+W>c-2fU_w?0TFV3^@jE*@I+8fKKGe}GA3OTw`~q9u;DP~$ z!=Ctw$3>9iwA?~pfS{i@##KFUY`S+|M{T@HF{gb$yW6MxdYKiB$@Y08CH`Dad!N`; zUJ)I9jX&Ax>NslsnF(`#`;B>NqXV7k#q%2KCL@ymH%u|MeXsLLCCkC?%Sw{^ZS$i_ zf>f}_Phr7GkEOd*W@@p=jZnWcCV)jR#;KCp-g#<)&8ld-z)I~`+!y=`x76fkZ}|el zrXX>e<*{P5HtRA6f~2fch^JL* z<&Qb!@rZUFjyp((#gjf);UfBj(@)*NOFQ`aGHPTH9FLIuB&W}(Wt5$Jq~c$^F?HIY zpHL$HWGO55a+mU>CesPl_nwo})$R=TPcqH&`s*akf)j~vANbyVf8^ZC@Vy=ur#aD9 zzhc6QtK)Edcy$zWBc483R%~qYeF*JvmnqM6hT9b`M?VKp`1b1BoXobQ=psx|i&4rF zS5N6Zz8=OhSwz9*dcd{lyv_2jMuVP%&ByY(t7KSV1=0cx7HH~SQ{~LAyej+8uyWD9 zdq7r69XKW_S!f`t#K#lp@q@9ntXZ7$rx_Jg$EXKHNf2fu<)Hvr0j7T+YIMoOzwvB54x-sBC`|6Moz^J+_C#()sCO zVgP=xlp9_J{u9X4 zcl}EL@}At~1)4cg_5QJ}dMRo1lJ*4A;Fg}~V{`$e(u#9OWH?rd;h0YoH*YJpXd8`N zH~2DLmcqv?IrJLf^;CGm(gaB|i!bQ0^PO%fdA!+*PO|R*+(LQ#7EHv;ZHZ$A#}MMz zl;)JuyZTU4;HY)bVB5ApMIZ)#rp zGjn7m^_E_l`MhUOI`B{MZiMoT?i{Z1ZrP8LlrGAw10f#WcA61FdM7HHr`-Iaimj55 zhgZ3_CM5&2K_@Tc%`k^(Y(SfJN?svO(1RvGP-d3ESvlG#N$lw&HvNCzT}5T2}2lD_AgD)MsxR(sPdZ&&_qLS?g_H(;_X;3WNK*7G#vS>IV(xB}|0Kqplb~Wp6e=RfJM8Qm8M(O5J zhVYi6(X_&{Ej-a$!UH92HV5lQNzN9%-F6net~2}<`*@PIcW!*uY58iLHJ!y&E)VC@ z!y%&qyf$t7$3ioik8!g{>R3g-EOmbk_giqUoUVW0yL?K9sb{PZ{%@YLod-8k_mYaifbqRtt?$ltF%p3Nzpg4HaL2&-ja^%6>fqU z;^~O_Iu$d{5U;*dJnXvLCQkD(gn7{TvpYNHjf|L`E<99XFzM7fp39g-IXK)+LIT6G zyF008yhK)Dd9GrmX9dFX%C51+e7B{Tj>2w-;hsjX{HX)kI23Y=-&*r+PHbc&mP5Hj znmBqVT#3+`fuEXr<2zcC?{4feBkF@GoEx?-7hDY;c2s@*=DdUzsrm4@!OI|8NHwj$ z6!N$yK7R%D8yN`y^t=3wnY5>q?;13V)k%ye;#M>*FW9_nhjjzEn)5wOgtu;lZ3}57 zccao?3&Rq<)?f*~vHU6{Dy5;4xtLm{;$WYw@GgrF*H*oLS-Q}D);qLJan&fQr(bxi zbFw(f*PqMng@f*5h^dNgd_S^{ylo(^^N|>Di9t4w%bgH)*KN+S<}A1Uu$2JS@|c=* zUcmq1?9BtA-249VyJRVf7EAVXT2%HVd#DqqgtBju93;!w2ZKqG%GPGdHc3eK?CWI2 z6j@5v!Ng==#yW#BGr!NJ&Ux5tWYaO19AL8TkIl7PbU5>~>H2429 zluuHdO?7VwYA3rnF4$#S2I11Ig>pslszFx~XdrMBui-de9%<2}FO?Wtpx8#Na=DYq3V$zCn+6`JrIMNnri zxVf^HEOpD`1M^_N`L*8l3nC|cI*Ev}&)9|lVlWI%^}zayP;RXbeS#TV?jzBQZ*wOV zrSRGWoKz)E*x3&6W)I&TW^W1NTFjlcg~ZB*2R)R3i|zj{U+}&I(g;OZFp&C1gsYD7 zKmZDq?n@=tzn~X)8w`KcuSqW-triJ7`FV9%s1|2VO+a5Q^=r5`?bsD~eRdy@)weD) z)UzC8&L+delcF`0Xv>BV4-ZlzYaG40bIonntGjc7+qW^wJbs^iD2^yNCpLX&{&T1P zUQ3s2U=H%^ZJt9TEaz9Ox+ut7}S z+UHH{0j{QFN5Ssz_aXDmop|FVsy}Zk(hWF-!|it6JBDv?M{5TY-f)eh`?FvgiXF*~ z)9IrcsCetQwaN1f*kF~J)olHJ-fMTT_XQs8CGEr3&z;=&wmWDgs4O*LD!Xt@S%AY& zWR#V#GM!uMYi1YK)qohP)igs=eZ+^H%!1wU_=c4UT(m3WQ}S6sIW5Gge}@1tn>E-3 z^JuW?cb~_7qD5-4gyvkaK9O8Xtj4GkhOV%(mm^l!$YFRq=?Y1~dcyV$Yp9M?{>E0TV@-&&10`b! za-y23;2Y)BLqiY6QUvu*Y#39P_X2Os88wf?)6@F1y=sXsnK;>ORKK$~xH(pY^s13J z#9fNI!tW0fZ}6V;rlT8bmfCQL{{1#;r;|O#%xiHg8xKN;!_}A5KtLrFkv6Q+v#-bQ zPIQka*v_$>6me%xvblDFTD`Nr; zciE*BMep4y3>TZ;gZbM7XsvuBCOR;71Q%xMR$J;|>E(5!T%X8aw-!Yg@;%tF_XB9{ zEU*nv@fG$(-@EQNt?tI5BVB4lbSidi-&Y;vTQEP4-pGLC-GgUEoF%@-aMuVE@&RvNFuHatWJRg@_G zj@OY?g;k6^@X>l*P_KL#v|8!Apo1OS9LNrgeKICF;=k2-Ju zx%$j7n5WKb1TIeMsV>@#1ZZ=+jq!$xRxttC?_0pAvOy5SlGs0(hxPE)Y6DAtv*f24 z!EcW1CaYTocJSMV_~ZAmB7-^&hXr`?sHz-I9KzA|VW$_mu>a+F)Q8r$sw5P%nvG%sZD zUrUlL7H>ByS6J1rR;EYim3a`(2Xc;|XE!d$3|jmo*1(P2K)a8|nug^R)RslZ;gwfv zyxKUDTxz~pstj$&Tg3$L-6}S`2kp|dbFBPA+lRT=Ua4T^GhTcTlc{K%y?ZIlh8t7Q zJTB6M8xVmZ&k6<%)z;#UtPz58r1nRnWL=L|=k}S_0$|=-g}2|KXg{=Vr#ZX!nX>F* zQ?KYU8UBW%R*d;cS4})Eq=&F}sY;0kMjW@-@LMjA-mAX;=_GXe=_DIEz7Xf=D~XeP zryefwpfyom{0g!sQJ$63+d$^z^X{oBY-1{VJC7$1@y=CE&$(k$;9&xE64{-IoD(RC z<%Chw+?!*N9mJa9fMw^8`?k3GD1>Oe|4qa`93IuOs0I8rc(oZ ziIIqaa_x1jqk^T>Om)}dkwMok+WmuE`%2C@%=~a|Dd3`c&&Jvg`H$!IkIQ0(Me67c zQ@?$GZ6CiEGU^yg*U%oX76GO2@9#B8OTW#%Eujw1tpj$7tS-s5?+Ztkt|N95PM3(K z_zhSJ#mpB}?*5ROTWHYw#Tq$SI_wIwOdDH};2a1ds|_c6wR1YA2I;lBW!Bo=t{9sV zUl&3N!~{oeMNB3rH~DiuD;<4zKBipV@XdFDn#&FvE1LWCP8e~}##)@i9)>+GT&^4q z;l<3U*XES;Jtl(CjUR+=Pxnq%!?GH7v)Kti3d6lI)|BL$52sja2OR;sDnOTLkS~9`AXR!87pskeKREnhZB91m)Md1m(09JJ`_&8I@s`ggoMFHs#)*Q4Un;G?!0Y- z!~Gd0!o+-2JBaXc{)Il#fY8iU=lA`$VN8V7+W^(NaUwztWwv5Au!dom4yD%3H$Fx^ zjkfF+UQqKx_m#3+eE_g0_9* z$tz2R^B8vHS;wrysC_-ar816@to(6cxXf}L&Fkr=`quX^F~<3YZ%{d+X3_8LP{zkUSW_zxt^2HPn)lvB6f+VET`^O*vqND{-;y%2nDm$zE{9) zbq=`CE*nc_xQ1?AV9D6Ackbta2zPq0I|+U+`ZnY#FeD)sBeHAkg>TaS)aX_hWn6+*29 zk=LxC9IWX+;I@Ko>pDr?`00Xbw~8MlHbyfM0$zkK1+prP^8TPXN#j>NDp-rE!bZ>y z-uQsSQ1xpMARQJfM+!?D3?jaXe6T;vRVH0~V|6qn$I%nr;N)Gg!**u+psz;tHuIWYq0APP9bt(A(fk_}Fj%YzR_mH^K6hPnx66JRS6#cy z6J1|3VV2tH7Zq1qY_gTOJ@WU_o6|r*VZx~+QS?W}cq$U8t6)Rh7oUrr+ia~2NKFFy z?1Uy`8-p?jM|;+2<@JPlf?=>lS=OTC%n_Ey#e2AsD#26FY@bCbOsW0G669P^u35xi z40l9{EYv2b5NqtroHDo@BBX2h+-O+IVKN&1`^K6ya?)$eG6^e4ZN**2cm#{Gvt>oG>W)3CzS^KO0n^PrUm#g_xPy^kq9Y)yuA{jB9-%X;5#hPHoh~ zJM$m#=LFj-=N7)IiP44KG*;&tU>pUb#o40CMPGinfA5Cr1Q#^8y1VVfypR*&oUWzR zGWiCz=qiNT$C+p1m6#+`dWk=jeM<~L8Ax1eKmWbDw7|J4YTU@_xC}3CWzk66ez9|v zb`=Ymv=)oFCLp_o@)9EQ!94L`_%?VmCiY)h0C0E@U4-p)ulS9(Elt{1i<(D%1drp$`CXj}0tN2?HX{Vj5s zLn%SDJM%d@tkz>C-o?Dh2;bgY>3emOj;!y)7-Yq$ot3L0QbpdF?>AG1oxppTe_d)< z%Ne7P?|;=&bu1ipYr&brQ%XTSV9kCle}q_oBuvbA8tPA%BNbKZ$b$zPd~qgHLIMq4 zYUF;$_hjGhPSZTPP2*Vuzi)Cp;|wQGZ%9B+9N|EErInkDge<>ELT>zF*Ua#DeQ~@| zXT1ct&XEU@4b|Od2#v8EI`L8AVo5f&ZLPaJhFyJQL4SQ#Ad0^iu^;%cn4$N>_LoCoiU@@BSmvUaN+?Vf)zNEZB>pwqKVwYDqm!Z44cuCEBi z2DJ+{li>QaU`1N!v-_U6{U&J0aR2d?@{s{3ClEOM5q7`WxrSIU@?*zYy>kmHwv^fO zynrU>f?A$))E8o&okMYE zrGl({(>1C%2PIW7-c^;HD+g7QxJ^=N`H9>vc|2TAl$v3f!u?tGh3vZW0T?1jvYaCe z<$IY((*om||I$}qy3z|rQyXjw&}neh3)F>&v*nyN>WdEwKmI;>%85{1f=#`U(@{MF zj}J38oqeM`5Fte~?xjcAybW-9nF6y64g)igLOkaRyC|&6k~NFX;=h@Qr+N*oF{PSL ze~;%pcVecPYz-;}EWwU;r(!G!g($|A_su2E$J%8Na@8$QB^Qofqq7yXei};{6Mp^xKd^u2$$3welYJ&uC4=Ls=}pcC921vunPD zm7cKCxFmzpD8$^F>4UCNBzi3~xZZ|?1&((uFPxTA2?I6tD!-sSGO5uE1gb*ZK^)6- zvzEO#2*9^alP&Gkk?2)RF>& zO3M-K$ir>)#xQC$d9a9VjkrH}uGl$2)!zVr;Zs>bB}so+1+R2+V~K>zDLagMBG;}c z=~74wFpj~d(5XaZ15AqInwB_U&zr)FxFqQU0WxG99+``JUnOupYm8F~>-6`*Oy{j;Pj zKPpMYdf}GhY$8@`<;Zd$R3t06ur&llqq3~5@cmBq(34>@GAwsNp#9e^@fm?^eJ52p$8*ACHL_w*S+H^3>FaCz2xmo3 zA3=T9FNa6u`phe``igD#dX3fWKQ4*$(qE`+iV;!&miac7mmWgL)6dz=93idzkybE= zkt+nXGhP}GyLkR8sc`09i%D%+YD%{1)MRHR!%74n`)0w=m}56P-Joio_^|qsfQPo# z>HHEQG&KDY<(&Kbz*vH1LoSAVYvYR54XcWoj?%>Z0QWN%)mwM^y#Ev6JB%Bw-!OIbR#wQ#PXxEuBnUrkF4!azO=2ZB%B-sBH#j7WwVTe-Y<4hPzs^760_eIyV>en43_)5&GbT4v6 z#>Zf(P9?wb;odEz7jD*K)6Wf_U{!1W_8_^$s1=Tc+kxJ@i#lC74NU z>yLON3GAJ#A^FKcB*k431n|>!`7fjrioU#tY&b0Qx5MlVd34k9(l`%YLs5WCyg(8_ zV?f0;NLNU2&`|BqiMLzQhlR zRLRX{4UOUUNJpy9*oohF?Xk=p(n-<(ufCZ7N&JpYAlHN%i1NwqYZoBq;SU*NA>OPd zUBqbOa`}deX71iY&2TUdVWG-P$hBiotC{%2=uK=GCN&{mEBxVL&(ZYZus$yqWyxZ@ zh)9%Jpj*o*2C(&Wa}j>Vj@>4SZ@EF#oRosFNv{zoqdkkm<2`L0H%4ld&UwvhBg(&$CDB;3~Q|N$@`p_w)WOE`un$cW_Y1Gpwh7H(R(ORT`bh$xEev za$jrJ52K(G)js4*sklNtjjrA1r(DpqcX#X8Cz5ryyf(g4NE@(ygNE7#+n#w&-;=)Y zJQr|wz@50{CB)No)xE)(hdoKpMk2RzmzJg{HwRru?rwav!PLvx6c z*!df2@8%cAt@pOqt!gdQh82t>;*O@ktSlc84_ZNOolr~4W?Sc3r~1PF#e@=aaSr4?y9$&dh=jiKPNKAS}O))X4B}Ru=ugl$C)YI&-z7V2G{~%oQ(ZKP#TPT)pMj z0fB`E&-oq#tdIuQ3(pmRE?F!Q|d;|3F>*Y0n%R}HkVdHxDyf1;PJnP?`&=K5}}mF#pt&luk* z1RJvSZ$L6DueTlo{#VEsXoOGV@Xwh%XtI4Y5#S;oozwK%<~r>(!F57&{HQoEb^Q}& zZ98-(Q5j{3a@T&1al(CkxD`gC|4Z?DUv>yIgc?nta7%$%Cu2tg8GYqITn9Jh-LjBVa$e*K7T=Od$@ zoxWT>O|SB+%6zz~X?I_-5dZzgjxUWn4gR=TwcK%c$Lqf)Bttbk#n4Lwe&GnQ z&eKYco-LF*Yrc_hzMsqFn5Q+^(|Oov>w6A4%l%PKba^jkrw!OQ#XNF5 zk+G%)O0Gi-u+;IxGhkoViP%@p>)(z~R=P0`hBp7lnWq}oJ8UxoWLomB$XawvbQ^O? znIE_=m&xrdwiUWI#QY`ipx>Q7-FPbmcG;EL_N2~DphzUHZZ{;_4dI@B>IJWG2s+z% z>lsJ#HebenMYR2nj9TJJEp~n|bj`fo&~=By=Ai*;bx&Wd+D4At!36z1&a0&efQ)h= zZX3Q9|NSvoe=QYn_c=i}lkfVIdcGM8?$CRaii~<)e-T5*Dx_v7W{Mhkukh59rFUg_ zWiDq-B%YHWSJ`gp`lZmW#XW5C66Q}EO^$JgUaURi131*TC(`NS;I^7fQUZKp!h?W- zfL-8M;-%c7Cm*JO4GSl3Z-p}j=w@LdS^(L)`hrVf2-sKJFp`I3YXs6e8WDSbaC03` zz5FMz-?c|$4`fCyn@YutXM{Pa%@Sv3T&^|W6agFQi|Q=4b}maIG*Wk@0XhacgK_)q z=AtzOc7xZMyk#xcC&Yqhmq-9NYTKb}--92;MX!LNYxY3I6o4O_w5WEtB0leyxSfMz z9HPpE`Rn!F!j}rb-e?vkZn^)0t>gt@bLiWHJ)nP&@XSi(Nk;@PrJ6Bo_|>j(f?T@jEiS=4jh-uv~x?o3^{uMI<6r=g9eW;FM`Ln9kV_Q z-A?Yi-GJGZq>Z`8INmC4=|yTJfrjm*zrH(*v!xq7%AYh$dOg04D2aCbxp~euzexE}zMK@Eg*bCZ|Smf3f#beJmL(?GDlh9zdAW$lz- zrQ1ygAR_z%82+DI3;MT>k;~y8hAD0*t^6dO*8pIa>*zrj(@9bvJ@X(ygs+T0Vq@ zhfl%HCF5;jNc9_M#@8cr9>~dgyr|%?D989a2+!|kzH?K_uvPchaW+=V06#S=CN{R+ z0ynt48DnNM14v$W4arxZz;`Uy7MiLj*Rh>q zvw5$J7req98VsWL==TY-LQhWZrZNr+UdjUq58czzVBhY(=Tr5^{cU}UM|T6|c^Q9+dDy@VFDx;^+$jN%lrKD7x|#u&z4p3ZXj>_ zsu?S_P*Rz_CGEyZ*oIp7G-6+nsyP@33u?2Ag6f!a)zIQeiT~;2$s| z_zJ71+@%=<0PeTrMi1}~$hUebGUE!bxx!vR1@WgaJ=XUHnYxghb4>F?}3A}VqF zGdMsjfZbfV?-c*O16*%fR5C-LyATKHcVG|G{4KDdevgEx#OlRP)eucU_)VTHN%TD}$zyKcLuyp_%TLu7UZ_O}AA&!onsyFzgz!t$z22%C$Wp$>$ zQPS8g5y3Tmt|J5N-?o;$P{SZSs__O*{_lu;rF}{ulsUR$ z4<@5xCvcp>8fw1t048(S;|7N6?$!b5a?XJ5RBJ4@noI?E-A@AS)R(*fE&o0MW))<4 zYRKqLMLTvuRO!+N60qsWTkQeZ=xdE%1w%%`$|Wh*I77zL4>cFF2q*XFcbRZ$WgoiU znbj@qmgKX;7ROP_>I!W(VA}Ucis3@(8=J3m`ek|r>w$WH3e+=4^-t~WIS%kr-2PO% zdJr&R(yc=8ScX)&$M)keS0;{)jkVtT$%Vh&NB4`0mSg=FBU(H_ZF)h2+#u_=vD&-Q zXlS?YAW-^w$^UT^*b#rW+izkoBeve9bqaEvYteT$ePF<3&bUMAN=yn)UOiSw%K~&&20ZqQ=k9w1P1}Q=sHh87(;)zfnSSndUY{^U|db)TH=+66mY~JW>39t zrsUKese99b)bl5ro$l?Uy0$FWFlF%e`JjKbxvXn*Rf97AQX`mpj8O1pygn)NK4hw= zoR1v^kjU0wJ39^g{!{^az^J_QU2(D3nbe%oIRJk<`FIc8w9RZW((95vR+Dw^ntA3) zZCEWKKS7RM*G&4*WfCP+-uHXXN6X@OweRST{-9gd2Hxx2;Lz(_&T^cBzq$408< z8Q!udkhZZ0=6@Li8#M)9gnw7J!**rjNQ~P49(e{!B=KSUB7$aRpw`2N!<^o*N zBI;6QgSP`pTduWXTC>KaAIyadM`ab;&%)Z{VIs~2l>xI-)8d+7hy8($!KVy4h8fcH znIrF`qoe0REXs0HKKrMX(3k~Ih~}pHv8`jntsqrV*LBK zJurEH;T@(o1FJLIU|SMM3xkC*j?y3Nbp2YR${QPERIgR~-R9wPlXG55Q1pmhZ_=`L z237s3W|(x1#9Kmb(^O95P(g1|c{s$Bs@3zA&Axj$70h6>Lv~%U%aTp#L3f0YoORU& zQxi?4vUk)#(qL0ger?8SZqNRO#gy{sGiuY+CE-W@oagvs)YrUom2J=GD*3M@lr|nO zY7Skfoh?}c1C}Y;_+m!01s#`n`m==AX_|dcx(58&AY9Skdf1dHy=iuMg5;vO!?sl0 zYivlMysc7<$9~xC?eS!#pl5#$_urz$iHX*1Bqk9;=Cn@@PEVX@oF*eL!c5!O22t-| zNmAJ^aMvOy7bR+AE%vSWl5pTrsp|EyDF?v{a?4GO`n@_V%1>FUZW&gKB_T$22%Pa& zu|aaBS#TSlV}porSH~tT!=9YE+qjH7g|6X4_ZXhw=BD$~0c?G<&e9Z#g}i-y`qS zvyo3(sE0{zRK)9GBc$|@`@P(!?k!UemQu{2u1Yh)jB+kuMIGdSoXO;qoM*nKS#hZ6 zQ2L5=-=9pFZ z>aj9hK*W^84VdAuPOL5MDlX zu7m(sTS$9Qjv-@!@Rud@6Bf{<_88Dk7)T8kS}|daO|y4-tiV6QbIyti>a_?72^B5* z7T=s=nf{iN3fHKZ*0aR2Oy^yP$CP+1xq1;uD)ei8c(TfamgE5zm0h8(G9QJXFgkP@ zwAt{J4DjRr>(Cj7kwYoEvH`G+FJDq`me4N64PG`3m|Iz#ozA4@rpAKzk;Q!@UtLuHQMLB+B!Hma{)8pe z$>NIujEa!(8|%A#PtMWhK=C{DDAAVs9~VW>61nvov)bQSJ%jQa#~FGf9;nj#O|J^J#BB?s74iizcBe&}r)*y!5L*}ljUntgjd zaT*rI`tFVl6^uQ#_B#BuuPb|bON<4$^3Qs7^}-9*VZyYPw*tj5np!u4oL5|z(cDyr z?!V*S&6Cvhc~W`~artxYcu2)urb+-=WK&;+F2v6Mm%;#G2nHNgzl#b!$7(VPh1kwS zLFREKB~xOn`fhK+H|g&Em$A-1u>i)P8jS4@x~1pn|cekbM$u9C4ijXDY?@KUq0va~hKzI_K$ zd&>FImvse~H+SaaROX^Bi_yzE#`kcrV*4vCz&x%9=xHQiYVjMx1%2qNaVGmSzZ37A zQM<*n%LEy3<$GOqz4uvHs~P#CbCZ;ci?M$bHr4MGEuc$xVPh#GASR0&Y^}vUIJ+zK z$W5>P4BZNTJz!{|?BInQVAu=q6w)pN!yfpfNNi-i58m`lzpcRPaUDUyt0_~~Z6;xz zMa$nA7^=tikl_p=D`VH*WB0n{{oN)H5O{G=-bDqqV5;WKV`Xm+Y1h%`nPhJI^ zscib{b_~CAx!h_bn^32a5PZ<|`Z3?YPjN+U{m$Js6;I26-p@puZmienh|}BN`1+vK z;L(?I@+<};)$NT#W4;;fCkcNvQC7aZ@HEfiFTcO6snlFxWG9hViSIKRd>#?vH)kt36r<4uw(ZqHQr zv@!V-qtSQ)1^aeGY94xS)3wNVGl`pA1#RR#Dnd^EmB5)O>hvu>)%BE@Itr^DM~`(? zYpq+`u4kNkU1x-(%&mMi%R|9V4=InQR*aIWPi6Uddw-ZF<7)#h{b$PsC0U9oo2k~g zZy*j~83d^WS%us4Cd{ELOx~?4<&A1&pPpGOc#6DTl6eyLhy3xbS=eY4bGuZeBkwtp z97pc^U0j8+D7d{^(j9mEPu0>4Ict#{vXiwy4-k95m6`yvt=rQtZ)Y*M;M)gwiN<&* z8Z6&W8vnq=dG4w4=cuSwg%qAn1>76jQOjMPAmA9=m3^3@zRCRyO6D$~02yHZfAdB* z6N)!PLFz!`G>MU&(s)@3bJ*o{EKLB8?UH5k~!!U1|H1p7ZS#vs4N0)>~$xscQM0NAQWAoVhTtn&=W5rNHQ z|5CcbIOnjLo&1&WJv0B)p0WqjpV7I&2OKb4QKc{jCUM73T~R0s3j%d~;4x=_5E!hV zO}3F}-ERhVR}XvMP+}Ys?gcxzT)x+5&szMGSDGCzGw0e;5wfq0-GkZBfpM402J0pZK|Wrf&0Z27eW!5r{x>H1Cjb=qA&ZVFL-Ch+07?b6 zRZ(}`xn{VPj4U-1p7xrbVlxfxECqQx4G_NXJQL4wFy-YAh%z}bV9V>}A2qq)&HbRn z)b<|i!@BV&IP>Ez5T`W^p`Zq&?!FJ30ROEQ_$}yLiDEJjK={Qa7|uB4{{ynglTXBd zDyh`e)Ml;zYf|y)RVi`*KN3OU&YyZfiKCYw?YM}Ua?pVQPd&}l4l%lD*Avh^KVni* zR>mlRB>F?Zf?*)%wS3np?!63PrUnhqJ&b<1D^UXqqt@U2lNBL-X*LOS@X2ni-(Kw=pe4OI#K%9~s*G*`>ew~U)NY|4$%i$Q16gUyIN8JPUTuQH;5ot>Ho77>8kXK6SADE|(W zhPJjgKzk`eUm!6|xZKfr2BlK|dJpbe=#3``$RsTWj;3cq-+yDEqDa=^iuSbXV=;!E z9~uJeAklJM3#6`=PiKQ{V^XtG=b7}MAD?J_MptfXiw=QS=t7^$8fmU#G=LBrU7FAa z$klAGo9m4>Ql1WdV1oWeqlckAqI1PP}IkFHY;S=3~+)RVX(yXmTFm247RMPJrJN8)I1 zSD~J$I+$P#j;0Gg z_q+A=^@0qmhZ^jVUOE2gpUSBTQ=(_3)k(=KI42}n5IwBz%uzGKy9s9>0jxf1K_6HYmlI2d|J>v76&EZulxy>P>rEV%m1~6n(>cF zn>{Ys20J-V5-2W^@xF_H2B{BgU@G{mwyDhVY`P4c5x(Bupoll620+4}|69`wxmxVQ z-&g?u&)H^=K}zx)s7SnrYct!1RBTfZE_ni}heWV&5j_4f9z6b^E5f$1x|`L=?7K3K zS6dnD3`jitp(d51Og5I9d>IDbn*n3bmODZLEwp~scjMj-Tn68fZV4PETR0HFj+8Sx zj0GMdY)A8E&Mp&K1}8wxhc59)vhg;N4f+Lcz(Q@$1ey1i_-Yh4VuI2--EZ7vRMv;r2Q z;BMh1mdgqZeTB`}@0I#*){q~xfc&RU5n#HF&;FB2{8>F~gLiBVKz>I<1xl5#v)G2f zu7LUl$rTHgxoeU#xBjkO7C#SQX{l+}e2NXU#<)Cv(z!M1HSGTGd>Cj3E=Uv%s*`X6 zld&t$d`sEYh+v0w%yID8>bw8kIsm0fZsdRu3HiFshunj#Ri-v@YBia_R$@`JZMuN^ zuWYh6c2^gjM9dCUL{%(pX4fx17DE_IZ-WNjb*)r-Gd04LLP@9}m;M(&J^=bG*8-61SGM?Mj%YGi4B%+HouFO-zLEPSlW%dJ;yY|LiMs!X z`_D{G72aWcjTo^9^XjkCt;O_~G*qep42o6&%9f>Dm7xiQblFQ?C^k80`m@IH^z!ab zt#$GEme{YDl2j7>e=S0wDh(Cg>MnxUZMW;-Wfb%}@veIrPeEJB%G^JZ;I;DY);?&k zZ?YfMkydvCG%fXc=WTj~3R|y70)1m|ArJrbDNtmSE!b=v+oVn*->tl@yVNYK0Ck|F zpf}%B3rajP+w}%YLPe;AOy1dviL07s-w>eB^Mv^}T~Yly;Qz~HgKr8UMuXLs9`aka z9^X0wIi-Ks38iE9VD8?K--hUN(Br3)HtS&q%(iVWx7^SRE+-pul3D8hk&;UR`AJ(^ z?j1XSW|01lx$bH~sjY^&MA^KTD%)+600sR;4z_2{KzXwqSZ|qQMNk>{=ysaM86vew z?5GTYXrS}^QB*z|%+a-fUTvIb=cj_2Do6+(<8!_5kOicrAnXTAf; z*sh!RWh*2ZIiQ>OS3Aw$A=AsP4duT=e4#=))UAn6__sjmKZwQV0m}aIrGAD-|Bqwy ziy1G#@0L- z!omwR1;pG2ejSU|e{`KT*&96epQr@P%6BHCS?hn&5G#+5ZK*AR-((B1XI#rDpPWPF zNv&{6bh6k#jjWQ6?0Q@KDqNymPe7`q6p-r|iGS6z+A!M*YB=z4hM(rXk=zVahLZj- z_phd=wiEMeh!=DRe!3mcFpArB>a3wi3NUCe3`i$OuK(74K+>DH2wf53cU7z3^a@u_hN2Z$M*kI zZItb>U>J7&)CLuRI-51&&*B znXYW2T;bSO`fSHeSE%6{iVkh~z^5^7vk0o=I3yPja}FHvg~O#MCnvLAUJHQ&*;||w zSoPd@ykzZM<{nI-TItrU+P6%>#odeAsKKHllIBuGyv8ISREq?kokM!ts;)0DEqudx zs-i2hzTy__RBlEYH+@icj*Q86E;x{sTYdWq>fLRZ+6Dfi`b2`j%tP-jS}`{rxhIhQ zF!ZTSUiQfTVKgru;$;)~N(u$wdQ8WW4KpG$96w|1L7$+X(q zpAhZ13mxB>A4D)WAB=9~{ysk=UQHNq7PcO~>i*I)qj3JcP)r#{^rM7OX zK7$4s(?i(#&*-)YL#YbsRI3WVL&SAV-f>6cP$CY|;5VIbXx-}+wWY$-c$xQaekh4d z8d!KQ=dl5si_RH8*M%RxhA+1xxng3_4xI_->_^_1Pp_qv}Aq2 z?i+ahr1}kP8^=s(4-`bX#a4k%LCB$L-feiv_MZrNte4v4I@Rge_HfC~@3n`DS!Xe| z?7CRAf6H>Bn#ne2f`Ur3_aB2dN^pdcz8LNfPY>O>u2SE?*$&A^)gf`){FEQ`z9lTl zoG~1|f!D^vlXx5Bhm_NaZn$kuA-P$gbT}X+=Wqp~FWb+77ZjWtT+zj(g$i!XRcFef zi#JYlMQi$z08pDOI@!|a*3_uuP@%bCN$0vQl|y-((XI|O`1$bDV>+*lPL(<`4-0Xs z9)wZ5qn;ct`5-NTU7ZXsdpK!${OrsU02|Kp_|h0JVM+%TJX!nZ=e^#~c(F|aFhS{n zFa0e(wa>JVL%_}-YWxSO)`1-Y*1U#UdXwaYl&ub}AqxptHXNAf*~47I!Ob%j0dt0p zksX(B`(RtUA3VeNls=t1x9o~0Q$?Dt*(e4~9As6p`}(@;W|oj|Bz^q{_GV5VKiQns zKQvRZWZataK?vS9SzeHpvfj|^+^)8+=CB2149HVQLKlZK;YwZW@%I?0g!9TNi&u8( z*El=_vJ+)K7z^seEtKdBL&B)lRc*W3(w?`2u4=3yPj-d2tJ4Dq+#7VMLu$;`a*l3& z#BG4%)1ctGGi9q9tJj3J{HMc9ym171thIo?l-0mdHS0PzJ!^ZHWJ>nz&9A5I;)s}Q ziYi1<9am!3S63m370s66A~6R`+9kCPa9Mk`mM-)w2o6YpRi3Un;Z{MK3XbX9ax8~K z612kh`Wi+CQ)-YOIHfC*w?>bjBV0#ZiQt^8_GnPk+z$l#W9!+$Gc@-h*v&3uQ`ZVd zx9)wQXyNl?c?*m3J9fpEDwQ8j6Yia~HSBZ{6SYW8k-Q_FxR@-pD`#na*cX!Q8ew zA1r{iElB9Ts(;!Hgr9q;WO8kJK04;yy4eO{2tl|n0E^TX_Ai+;Lkf)3re=EGfOdd? ztQ!S|H7T)#&Mm}OqaMocW#?r0uCWZ)V{JsdM1sq{4?9JUDOke06sPTQXSEz@eG&CL zF|*c-PZh~Mj$|CZF`{K5$A+q0x0)f%$$uz##EyPgWNjX~jJu!GA|@(obxzuGi!4gW zD~LgGz^8X62Amr5TAX=>&HPGpv&`i#=(8P6kaso6Rqzk=_I^GxEQX5dBZGnCq19W- z@zkg$A|)K&=$n);%U{mlt)|7#Wg>vySQH=mu7J*~s{Fk8o{1FmOn;qpnnJMF=}l#xDW_L~h+XCtE&`cGu)Ch?~vx$;!cRq2qTy*c{FKnv*aYq2dYdNC@# zGYKw_y5QOt`{P!!=Q zIHFirA&QfZWgRIk2-1xdP{_x7eLa+eb1An&W52fH9PnHK&~p6_7QZxfWQ079!V?$l z>Q>kM#%;s9g#_F*{4hP}{a8ojku@Q5tTYxkv_57prE5D;Kqo3JM8Sep#)f=)PGc#! zBJ5l>mJ*{_bD)d_A2_!~VAC-JDdSIRu63G<{GAcBkt4W2##s)AuLNHY8h_@oN`@Wo zt~{ca{U!5o>k-k7?sEM{cIbeCJ4fLI0baRf{qO7^TQ9>nh329k!z8?^*8X8*51H?jl~&MlZA6$7x}9F?Ji$7HX;C)^RqKKrd&{s zIDBt-MsZ@LAff=}ZUvH(?((DV{T}2GHgVag8j_QhP@XL|6L@ta+DG1M_cNnY)z^ zzg}Hnr2~Y?Y-E*s-2w%eUKz29pnUX=H#YG8G9Pz>s-n;vl^`&Q+OM#5i^HVQj!`6;dmiO+RV6*>+)cN z7p~REI5di%E12?u*c6Xm?5Z0S=_V=A$3qn-_7>zMCuQ;kt%;NPm#fY&PXm3)Kb>2w zXP+!pG5=~uI}w)5qT06w%l?{UfQAaPzpVpXJR%1Fa8W_MpX&l*a@>aP0E~v+@WaC0 zPLST3kR#%>lAB}huC}9M75J_h$>Xecu4$f6C zW~8IJ!fOeSs|LvfOEG>c!K@s7zQM#F&u-GGq{-UMYfPA-eGcM1R$i|5k2?B;x>gQ7 z9&(?3-&sJL>KQl549QEy!~N$9+N{R}9;2y){tI)pVx-P^V^6RbfGgZDZmDN335a5iBZm$e?!VkAoSNghIt43Mr zgd-kp1@r9-f;u|Tf z&-0rmq}lp%;@M*c@-`+#=4)!`3Nh;idVciyJ15DAjXV`6t7PPleHVvr_PTQNaP6yp zvP17mNC$U3HDiu)PYkt4UB!H{nIb=e*De7tAYK-l!;QqV-tDG!%A01Qhn#o68d!Tf zG!xC|#nM}q==Fgo3q$0ZoT|R+naRnAmTr~uV1<*>nq|<+C0Sd{SE3myb-6v(gp(~i zKmnkF+hEUHqUWh?Z`(VU#<4*FD)iau>Mrrdfc6%=+~$8)X{7A_o)E18cVAol zRQmYbNVvx3$t1_$y1x>ylFllAXue9Wa3$?6TA#|{>}^I2A^zB)Z7fmL zfeRST(L+NOB;)Y*4CE6^rwO@!8J-o8e zY&v%s=ThtgpkTFP*Pb}XSZ)=~B zr8^tFw#1qmN7CoD;YxzlEu$iq+dmIc1i8wGd@W~3@8xGX$(7@2$BGCf9Pk>@fgSGB z^53R*3}4_>J0#tgMxV5%+`D2|4q))GWjMEk(k+<}2mQx|;#It73$9bPNH}SvgBGiJ zufI&RG%5_of2@w7UCk#aS92c~5Q*8$5roIE!9!%Vx4Yt--kRK~9@er;dYFX$6vAw)Og@ z7zc2w&G&kFd^ata9hAD6VVuRM8`9y^V|DnTnBGkMY@Z`4sZf$x37s>)PD%!vFd}vtWHAcZRq}tbd%N9m_OlM~ss`aas@~g)u zhh}R`yHn0cwW)iC*CYvDLtN06UC+G7neR-NHotnNpt{=4X0}Pmk*@07UqpAbEV*^f za6-<&wa@#1bCp15Q*&--77JWELKgK3yX=w;1nAy_* zo9LDH7w0j=)Q#HJ{@wIvXSFv}AkPi9(Y`=6v5T&_#=AwOF(mc`6p8YH)^myiEYebh zZx!L0t`vv(Y$dLz=rZ{&eX>rBIy|&G)JW#;4trr}8quj<;$u52t>17DM3G5>R=5GrJmma6VoV<0 zJ}0=~(r-P?yw-gr7GJW3p< zIMM1c+GR=}yxwgt5;H|I{Hq@>7c&*&8L29Zr{C+JkDSA<+Gj+JacKGjGI10D^f;vc}+r)xB zwXM9D(p&ty=JoMltK34Eg6j~X{tG%;pl66`MD(3)J}XclOsCLz?`EKEhJ>)R_56NA z&+Euh6X{2UeJ`FkCmnHh@n^z#qw4SGdDoz`&vnw4ad0{^D#@+Re^LuYSEIwnn@lXZ z6_}k?d?R?{3HjB;N08!ot8((S0)~-^#iz^p>uKGz0NjiKqM;pJ5qlfO^>d#?b)Dy^23weCdswy>nddrOg~#xlY8Ue$t>jN z2+s_*Aw~xgZhRuWj_I4ce`GfsYtChz6u$*60W3bp!^DI=vQck{!A+&2#5tr z?mM@-mntkC;P^J0(r7;Z0do2@VX>#?3`Vc7XI%bhv|p)%xJsxy0P|lGpr{1`=z9SA zPV6HVl@xzr!qAjcclMx?gB_9s`t{dUtoq_JZ&L}4)EN35UZt%Xc=$K#A!^o^S7g`2 z1wT{Wpi3?L7C(3p8S{Mx03s9}mT zLg5l#8@a{7hec=;*!Vt_@9P^2BN1&8#wO+&c*3MeBpv>Zmr#a-#gukiI>1A40RW8OQ1G zXtg-5-ZKO61N^h(gN83}fwQ3TasjmMwkRDR)K?Xuh^V-0yi0CfL98Rl{W8&hEAL_h zMkSc9APG(0e|28$wpmZtnTvs?l4I6Wlwed4d0|V|~Mt}-9WZH0YmKV}!g($5JHu+r0D3kK>CHh86?zjL~q?%QQw zRL>${M5 zs9k;3`x#DqzOS&+(^wzoC zmwmfHQ$uHQ{NFkE^I=9x#{V>Bw>p$uLE`-|dl$&zJQBs~?Lq~3kJ{DG0?J3kPY5o@ zpJGJ^whbs&zt6sC$g13v#D`v;ID3y|e}*&qjOhPI+k1vJxiw*cp5QJ~kCHq}BI8Tg$zuewRaPLUpt>H{+CN8Ji68>HkG+A$6K8Qw9zMafeHR5}$>k zkt4?bB!FExWK!4WGpeuH4D?Cm6io}bM!ySflHNf-cUh^ug%165**MUC%pNfLESG0P z@Hh_5m9||c-+Qb`P2O?qU7iOn@}bh^!X~`^H=uJBXzFKFv<9D*nhUtSE+`ONcH-Qj z0axdM#fRLv`4S@nZJ6=r%ppL#QmvWFHb6JnDpMC*Z@tn#FP5OQ|qZR=v3_g<5` zqj2JeST)vu8poCHGZHp2=!e9S)J&q($HUzxGTlmDobzX8`_gKU@d4M9$@pOX;6b=< zgo}cpR33Gz9CoB%P$OZum7Ny5#m8b*ZCNxuOd$X)QyiVB^8ZCv$GIfdDRa%14Xc0*wMl?`;xl7LlSs(se%V2?+z5FZm1n zeva%%hr5q9NqVk4)toO=nob}}R0QSoBWo9P+*gj6F8Z6NGyS-JB&CXfd2%wZ5xCNu z55(2QE!Ta*YfivLTuS+!VXvXq`O^#&GLc)gP=6+I^kkFwc%IGWb$ykP+m*|$FC~7I zU4!y%bWVB*ta%!E32eF`s6DZ+UD+XZrDFMW&^lF)mf)bXFRgAZN7`f~b8;3@%D<^O zx8xR3QGf9hE}`M0)!NJ=ZkCUNk}u8ogO51v%@4%sIDy^ zOFSbO1W?az^&nf~9U9>anY1=sU)VJliN_kj$nlneKAYUl)<{Fwe1|(gpj5pbUI)8* zjMh84C!k~zRWG>y(|=u{9(eu02zFLkZQL75^$&QR7AsQBC8#v=>{?=P*9}eA`*;_F z6&%~ha_J`eS?*aF=HuRGhv>E0+y11g)pH>DI<+1h$p%}_c3+QF>?6&(jlS-$moiY= zD0H{ydJ%P5D{=J!u!v0`-TYjFumwYKG|nH`#1A~Mb-VX-eJ0jF&|4deke{ZoK}YK~ z=H`4h4?lIl#~za()=gzl-p`zT0q{c}w7e9)Ar?27_xQm^Lt`1WD=3`EMLpZ76j5Xm^?H1WrL$1Ua2+F*aMEiAv|06@O|Ek+nk+i3TAuG`NttD6?oxZlQeq8`;C(h5eVhJ zPAIPE48I5TaE`pl<_iLxF3+!?E^JbkwMqtSozPL!dKE?Tdrt#WX6tIFDw#)c?fc=f zW^qU~r}M@dw?ku4;7n{=Y69fgU(sTO4UN26>W=A9>?ak1*VfuGHvJZROYa3Nb~TTT z8Py4jrY~Zr?Aj!#Lk=0s8&m^x`=G(OuNT=GqYYJ0)=`565AarZ0(rr2Zg^O^+qpW) zJ`fZeI8+hK2ti1C2;O62a?&XSK*`TABA!okc@*YUWfwGy^Zc~@L4&VqW`VGD*HCTl z(l8XiKKDJCtr2*=ZnAM&WTb+qVN`)p-mMK0hiUOuf4_~jiB2&tBvC!lSgJF8_x|)q zh5kG8mX(yjvX8HpY!*5@uU)G@K{b2-zytjXxju&v(Jp z-xcW0FHoba-!qe{O1 zmQ&4SZpr~A{^=Kys{h=LiJ9f>R;BxpF?+78fpJKwG6mry2dW-%^Dt5HA?I0kKj;lE zb|fV+xRnAb%-$-iUj?zUR!pT9ozBZEkAb{uBV2O=*by|<3b8zs)wDlzm+z%j^p0S_9a zcg~V&sR9aI{FW<9{bZt62*Z_wi}fejrXmz)-dkccpYHulAJ*F@^d?YMmdcAMO|Vq*%`5P5t4FLQ2l$Oe!m+(;{pnHp7gDG(Ah5QNg17Cz$@&okV*gE=93AeUX@b4~oFEQ2SB+A&4bPn@ zG+=Q3r9V1it3Yz;b#eXqZ%`Hu*f_sC?qD;3D8WBva$x`yNE2T^>d~nWDi409%|`sv4_rF z*3Ef&=JH;D)y}o)S|!3`+DOZj@V%(N@%|Po^-V4h z|GiqGv_x~XRE?%Vy_cmL{Jq1oiUnmgAZ+-??VteV`5DsGZI*|9Cs4(~Mho+m(s}_s$9OKO_WW0eTMSoRU9q~~p zK}{DdkIpoY%nxPU{_#%c$z$aGGW!`>&*E?33I&tbrc#Ux?ART(=}{kBBL*eA+Rel1 zPPkEYqdv7Wn!YAjGrJ$_(NYmQ8G#zev*nitjuc)GFIT1k-D{Da% zHZ7hhZ0h4HJxi2rFYrcCj){XXgTj? ztQ=;RqJXVnRxHcEbO&WVdYxWdugJpp;GuUIV-X)}6$2HoCZALo!wx8m{IlC#{I$CC z-$rJ`n;Dlm(;EQ;djEX)d6!5KuNUKX@PKlD)5+^EX|zh|wfN4JJwR{IHvk**ra|Jh zeE{j=S1Y3m>bS}8l*P_t0bC0v{E&QqJ?|eB@hDCVC^aZp0~;_Xrl5~-*K4aFMK8;y zDMGe!V&YH+k})%gh5fe1)+$-hDTdYzt4iny+2RBPtyTZXxp{TB^{%M0f9|LCr)U#f zgtLQ`#YLb8h+Q%80ERL7!?#`qd`x#?iCze`J^bsdJf|be#ZH*@7{?kdyo@{l8Nn&q4Z!drXgn zWXs+o|K?=Xy1ScxsWwssp!t4h_K%wkGG<$f+D$)x=tSyA-He$nKpC(ay~yt`3jG2P zr|a(&eVX1mPG$+H;0Zq}{2K-b3;UNpi(8We>&_=)J~)Q{0~-GVs@CDHv{oIxKJ|B9 zfPsAuRH8o&&eHwO(Zl@wFVfyMVStIHaDDJMhEwRpQ~I8piav9Bf#=BYr+$TQ_XpL1 zTDpecaiz3&(R=g)if7tun((^~ySRmKlF5YzGMOw4l(@x3R%YLk((zW61iI1xUqBH! zVuc-l3&XE0e>b=94qrAF{GCJxw%Qc9S^`|kcZ^xEfP)BFaeBY3Zx44B{dOsw0jpPa zw2wh*ZwnEX^x|-!;q^OuO*vPPm72g`sQ7QdXvNYoEwaZb;@nExYuax>@LTTtUjX-` z({7BibN*7lN5id~K#%D^+XkQ%;lJ^b0sn3S$XV(14`F|&(V|;`O}<8edj7dnKLBma zHNK~w9#o3bgzDVTdh?v~yfQlzllBY>`Z4OrVQqHp-Hng$H9pSDFTJ<-5)*1epM=E8Fvi@M;esz1GL+mnHQ$BddP8Oku|MjEb z$Q2WwX(YY`>`}lu<==e(sU&?hx_~-r%os-w!h}vuo{O_~X%8PT=Xc zZ{0ejtgKuqr1wcB_sZX&^$#QY>}=V`;{Ei=lPAm}=@Rn)^6593TID-0n6u|v6K)Cw9L)!afz4_Ckhp-w`LH3$U!sef##M<~~s=ZChAaINA2WlR03&1Zbgj*4ylN z6MKC8Yd)mvF6Y^c*OJ;FzL2^sKbCcch!GOfBMxTXXDsn6L%_ob028ocW=xqFKN-=d zvMg<&!}p26-@j&XxG+{c`za>i5a=@j<|?Tm$zlY8n)#mH*-%sjF*wRd`9hz7p1ww) zk1YpSs>YNf&L2e%x({g^sHkjkok|W!%f6hu4uj9W5=H;axo@Ruulo zhcdLORQbz{Hp!QOt)+FB+jT)w*&-shdZV1|$Q)v3N=TJqB%Br(3p;ym^~=~J?1&r@ zAunN{B6i>@`>gHq7;28?*4rI`d$R(3ryJPk(~>UHK+vno7#()jY#Jl~&|{L>%q%SOZ&j3e8EfYleUg?e08lhS z(TKl#0W6I>vhHJN7h(A_&RFg@nOjl_15%tj)ZWo?q)AB6b=?lz#llii(yDCDXwXLz z?7)t66&4j2^3v7Unz;7!_Hh6PM;clU4l^`a`M2nT&Wp+m*&YBkoP~)1uzuo&X|asg z?*aWACNhAgP|D_yfe~1DmMb$_xg9&5PSX*5?W&2^M5RD`i#0IJ{{oXO3qXenxqO}u z6EjlkSw@3th3==r;`4vs30KSD2Lo^ZXEZ@6f6{`@0M<#nRB@Bh4V$CH?j|Bo;Ol&Vs^*VBV1 z_0D+6{B3;>4pvu%bDz_6>pbS`G;+zo40tg(Axl^KpuhTj%lk4LLcdj@JC~JLi}1|~ z6Un4GLV_G2QZdyIMYBBZJ>45Q96@AxllCz1xY~nRR5^rXKKZpQ`Z(sO6 zLcUE@+@Um;T*DuyS+^nV1%YmaP&;1ovOBj--aA{hl;EvXIP>tj+XdWN+h?N!zQJMkSoD*FTClO0AUNoLOd-gDjqQ*oe82 z{l-03gks-*@`cBvIB}CN^Mtmv(du?|!c=cz!s&~^5$uKKF+UbCVeK(V*ri((duad0 z2F-0{vRN=K>#TFnm8ss+j6;q4#4o;WO}x5vbWh?V!)yLQIaA436-fN>198!st6!C8 z`ap{DI?WN3=j_wS^0VS@d#|SRs!d#5u^;a*C&YV?6*y#EoyQGWWbMSmR6!-jls*h} z)yIo9DfG+150+m}*6Z?J7hDhbVk?jJoWXm!pBs6! z_g=a~{epiR@EXz?l)$V=PwGI0-PeaBDaJZUufMvwvrp&d!Hw+Muatez6A0N$_l@4XG{H`WQyvZJN!!h7md-^$tFMCI1S-}L|7 zjk791*x&T;8>!3}^ypCNw3dGVF5?(xNvV{jtWI3FVjl8hjCW_lsbCRbC?NM9JLopB zYyAQEd$G?ZZA|IjDYqzsXNjp(ER6LQL@Ph1-uZ)8k~$4Uyws#lO}%f8U=L3Xv|2)@ z!py1f8uto#y*u#UjvQ`@)5;^QTY;zZAEn~$>)+#@6II7Q@f8} zxoXTSUtnppo_=L%?%ICTP4A93tF=o$)BUB~>YGOuoJ06E`k;1BCf2G~M>qg^@0olk z0p1z6BhKQ)Hm~jKAHezf#~U>El(aMKEYNb6D2P@!NcdBK}Ug@0_;iRW|>RYBl4} z&&y4xBz2Csqcx>L_sgEh3r^=cj8W8dgp9XOW63hVX!I4A9{{z2qJ?P4@NIltj+yq5 zsT}b30FjGjBb4)6x818}c(-Ba$}PaE-1u1TYN&PloOEA_?ISPX<&fwU(Ovq`5EXCr z4QAz1^lsr>0=2V&zU#d$o%3l>vV^>S0HGbf`SoHs2{QDgcoSgNeYx6v1RB?8|Fqhp zA9b>}|D>B=U$LoSrRq&o&C)08{x_@kDatg}pWiRq7G4JV4mBpP1b*DPvK-82*D}&L z54^~@Eh^`FWC9RpFOD%Dj$;(TW3T9g1>zuddtm z=V1e2WW2|h!%zJ`+V}8ln=NA+1PP=wMyaMM&;e}~=;R-E#Ut8a<+ryAPVhsgZr!s$ z9bO|;I?eYkPL_MM-0X7a=Tmf&P_1+or3t(X>M5J|LX!i%H*7Ieo^6uwo`F9wZ^AeT za6)~O`ziQ2iy?2xIlrRal#Sk5w1SK6YEOY2DZId5qTa51Jwo1g7Dn|Bye>;_Da#st zN`Qlf_X-3&=&{&XZIhlKcPUjZ<@asx>aH}CcSAw(eju)mpTWGkrRk{;=3C003D;I2 zeG*2d6}DZa^(sp!Sjst%hXYVc%XD~JMpD0My;wN6q@1?~IpFp zv=crvSYD8nTMI7i?~w4st}PKWn##f|UGWB#0k``6%o>Z_rKYiUDlT@nCoyd0@bjJ` zGoxzPnl!ILC(+qN%4$wW(0cEIyhA1>ca9$&pMEoo;vcN`9tYir7a#Lm{?S>xu7lQ{ z1~(;Gsq`n-_1znC&FQ5S+m*s7XL@@SmIFUhvzGGvlPwMR@_Xdp$7I#OPM>k({R9~q zy=vFzl9k?NUl`!^qBVH;~^uaf--G5y|Zm@_(Z)pcFxbnOj|%2w=mD6y(h$+GJx;&(AdQ- zZ7y-Joj48|-|OiY3AV3m9@Y0dWVZq?obY>QY69!n=oCjo-&&xjvNt>4WYnzU)%Uk6 z03OUq{p548DC$suje)MXVP*fxT@Aa~lrM25&Q{Ob2Z=5aJL7^j*G-e%hAu#cY}l-t zzslah%i`AF`eqYx;QPW<91*dHnMJI)rB{J5Z8&3?^IEp2`zwO7V2V75;pwF?%)?SLxa(C zO-emnwbWtXyjp6fDn$gh77yx+!NF)siXo^cd=4{mYI4q%!#<>{a-X@ZFTHx-Zs?S} zUlp6IT<8?G46o91E20kgRMfd5DRMLENSk6Y=&;-^jg5Sz_L`OIA-lZJl&A{(rK2^g z^KGA+0xF?ziWQ$?VbL6718v?KYipO2u9saM6YKPZ8w|nmH^BA&K0V%9l9kjTIBV42 z6tKMlvsYdnw}pfbzkdWrEku3(io@K;rvf4gmKt9zglFc~q4>BT(BU5vd@A=7y=Hn^ z{IRGr#fn6l6(s4-y%M-%Y+Z@PfV0mC^r+Xs4r!ij9(WMkQ^;SmSfA>Ki^em=1Bx{M(=uTAl%tO89e&w*M z_Cbx(TF8TMx<_X$6=Wz1xw-4BRws&w?h96@(Z(YCEo1kRQT;IUk-6Iat5^CtqNwo+ zc@elKyTpPbKm8v0zB7Z08>Pq8Zi^&?Un|^tkoN_lUwO=~*i`bVMUzj)&?cYk1zZ4X zxp@bNs3D~R@r9Z|GrQ5}a`|GUYAEjL0<0KsMO9yWF)J9znfPw5GSF&4eJ^`Pi&eZ#piGqb@!7NRcQAeQq(EKMtkrN0DFV(*hqzauKWqrY_>5|R$ZLS ztG!HiC<~OvS=UKR$!A1Uh;{ys-iX!J<5`-+&(6gWk;Pfl$IX>Vc4boymO4ZmSl-}L z5$bIFnqO$GoYT#*@y-P-1pjGeHmai?8ek?u<5?Bx5j4~fTp{Emm8KGHa?slA_cmS- zs!T=NzHDLzy+IZ}t4R(mVR`8%jzWfFE%s<;7<&@iV~(3z@h_=#ug4RSU!~XV{*W4J3y>YRyDrj98tADyM7WaT(J}|XE`7-FP zCA7B}rw@kTH!d`hZ5#hG)f?n96zTIlsQb0`MPd!DSwoZ7R8B+Cc&6=hX?{gI{+s3y zxSF$BmASDpp%SA%U4e={%I<77Y~zaZw!!SQ0hu|Oa}1tuk+Oiq@rC04DEuZIAMHEB zYG!MWT3>A|wbUy5VcC(~X!FQheu<-acP8i%HnW=9>Av|PDGS7W<6STXnND)j_eYgxGw^C54p|Q%rUMV$QIg%%; zrWK*jB`)@lMkiGGQ5F$1J$$C{;RnG`j@cqlzvQolNThE;G-Q9OCwf-0Ipfwb=0U!x zgAS-41x8Se%0h%dW3ley_-q;+H(n*t<2T`8ejh>T7n)pQMSsEAWO!BgF7%r`hdgx3 z$bi`l418GS0Ki{$t;fuN6VY&3M?hG;bD9v>h5a%cd0$hK1q++f?+8Am-s7_J8ry)g zH9v%SG{paDHXfo{4K<}MIX*qXrZ)fVtXo&sX9%TcBHYhnlJ@3eUK9>tm3UXLln;7z z<(rLle@@^XTQkSoQqz}mxEG<1;-&;B6+KZ?%)o`uJDh~JR z7G!d~em6*C`Ajyvg4zD2acRrXUl_MAbX&HPd!Q92XmBd~7@^n{8Bz9lucW^jh<)mu z%1VvB#`=U!4Jke)Z7G^G*fH6JAx+%`y~cX>Tg*RV?e<1ARGJUYKHQbM(!CKd-=*V0 zlY}c3YP-nJ21X%hT~y4&^TES;oukEyxKkOh)i8NCxJ(4mKB#jrW0c}>qe$>X2>-C( z%V#m`X{E>+2ZXTZT7Gsddc-Pl!wf9of#Uvb88DeMzEJ#rcTGvg#MTaom&S1xN}Yha zmp=JovI0>@0!w{ zI2j)1NDmxb$=iQ+IsDi~zaLcm)2i;-3) z#QDSDi=$cXhgJBcNsYwx1W^J19y5XzrsHl3KjguXj&RCZ>Zep`<5vfODMppd4i;;O2=kpek5WCyRgk;SEbuNv1xo) zQ*ZWK@f~SepWKN&z^_U=1s`_z50m?**n0L2M&`@xo8o)}#ZkiKo8sMGsZC!dLqGPn zCSdZE!%0F{n}-pdUM_EA)A2C2$i)UDhs>4i^B+1yI8*GKqA-)kIQkr+R%F|)odLtb zBE01>F!jD{R1oP9X^_wD^pCuuRa6W~QAq8RS6ALB-(>@cg_VzcP?!6GHn}*_E6->C z=u^iTWjBA3&sE4bFtt+r>~wXq?%a=pJk(u9ljXco*|h%AW2MISwjao3@xJT~A#IS+ zK^)Ir$Nk?6OOTyNru(z~hZJV`KB@@V&!!WV3Z2<8w1zs0edij;ta*Q-J*%XT@mA@al=`VhhPbZ`U9T+3zT-7 z%wF1z?S3q??@;(l(HBxondx0CB8gwYpUx)wd9^zdPc210c=y2|nN9qHLgZHqfFiWNN_U{6g2wo1DtGWK?sP=`B;J4V!Db9flAI;v+MXTH2 zunj+iJYbqMx0i~8TKOBfZL}L1c!LcZ_pQ_($!`BPx_^-AbqQiMT63)3$k4m`d9C%@ z>zYUFtA(*?{h`V3D=Os5Sb4z}BGtIQZGaD?a5)}L_gg&1G+rKN$K5sWqhA?gkdSoR z7BnMfR{PZRVX+%N7|Dqmls{5>#ZSWJg>Aj&2{suba@O_*ulS9;^jy0cR=(hSFmiRL z7^$h0-{~ z-OQ*xw{qku;RNRS`p@ihRhHqEV$+^9+Jvus3214mH_f6}W5w9_=LRj+*HvMndY_;* z&Sp^moxx!1%9MD?gK^UAH4ApMWFPT89&Jo~csS?}Ssrwid7ftl3cpLpvf^cfoP2+A{i*Y982 zjF^NPW9&O>ZDjugW>*22U8y=^X(D>+z>^EweituiOlYE*Ml|}H*Vfj47W;&1@iKRx z5ek^ee2N0^#=18LZcK{SOg!G7Dqrpf9{iYD-N2_ZbI5_;|NaNZAFp89B;cZHhm4oZ zb{mmWz&Z5~&|a(V}7LZ8C#1O;>WOhuZbtfvhE+hiunnL4FOC*2;`(88!n zOABaysCB<;;ic+;3ZzhEZ4At*K5#yGa$GkrY@B-m@Q}U7v)9Y|VO?GXb2}T;63U8p z;V!6E8Q30C+U!?J-F(N}TzVCgd&6|e|KyOH*79?wI4;+2DOp1`%59?X!LqaMVr{qj zgwB|^-KP48n-slB=L0FH2RK9XQZB+ez-EP`SxZw!Yv~^F3PFb?u0XaF_YSY49+-~u zX)>jg&&BEPKZc{dLF#?{pp16u6OM0Uhe76wxq&oV7XY z9RBuFZog%c#K}$5;rZEIkeh;}4b&Z?*yDAk$Ob$W?EsdMEHEGs_L_k=m9&u4u+Bp z*hde5bTyGHs2sd1(`>10dKB3#?hF#`P^M9MbIF3Icv#}%_F#uDP)ATc#-S+% zG*J(o$>eRGhE;3Es-;;ug8C974h?lu&y|!u ziM(2coGW)LnQQ_VHhyuf;^g7SEeHpy{nALQb@?0`NrTh;P{~e0YCC6dwiZ{)qT46 z7e&#h`uqUNwcfz_ErFVz9}B*|8LZ#DZ*6fjTK7TqKxQ!pTNBs(`G~X_VB2B!!J*7+i$q0`sdtUV>&N1Z=7^c%M=dtODa5@nLhr_h6l?>vtj{ z24OrkOxCsNd+ND-ayolL$V*A|i%jpuv*P|{MTmF(T`RZo1rYV=Zxzk_cu5bV*&6J~ zSrgM9Tx(whEW#&)gK|3xqU9Kky-%v{89?fn?kyhTgnKL+h2Da3$D2Pe*kV`L5i=?_sX~$Ns#4W+Jvb4C z;w}squLlk}~5=HYsNbJ}@{)Z`acIZiF_N8tE3 zdA#*k^BisX1dwHXId_X? zbp)1V2X}^47#-r4D=J(TV85bpvk@C-P$!Y}f}} zrs1$2STfjLMr?C=<)}4bnGqHMpY*+TN&TBf*SJlu_86k9&WPj})sHZwIs}Fr1Z}D79Wp{}AEmclF8}l6y)m2C0fkSE&cZv4(FcpfD4^|)kyx1Hb4{Gl)P1}<~ zR-k@!k#@jsj8ZRyiXMI?``rSa?JF{F!@=5Diun{Nh)WBgJwsVVk}Oq15O$B3yq@o# zI-RG?hfB9)vqgHf^17~R!2Qo1w5=l8)=rKe?{Djo)_|InfljQdYf?jt)vIl5M{7z~ zu)zDo1|Z_SnV&+hHXEc3UkuAg=|1mx-f{1z!JDKWOgMw=YGKE;1IGuaSy*D7E-u=vwh`l@7t@vk&?Ggnyykaf^0WxedVY~<#E5YhROJT z3(@i;F;Y~c%Yc_EUz$V*$dVS=XBLyedU==)N}~t>p%J zbnAqu0JK%@gIAd^wF%rkETwbOuxVYPeh(9=T}dz2GIgTp-d+>Rxj?td#(gXqyov_t zN9CIa2o@(VUrDKUC1GjFR8Z(8Ah;6YU(tgK3mDMmR<=1XwIDm%eAUv@@^%t7JZ*W1bv45hmFg`fCe#xz*39o7;u&Fq zm=RMvfND&NUbBJnr4%q_LX`a*WSf=paJ~sr9@)I5MJGfw&p-FKn4YupBUSy;e>aPe ztOzY}^g#`VoK#T9zqawUIEr6&g)n0BJcj0> znSf00TNXDh;pC?pUMvH0gR_ww(*oj_*rwI=YxY_b@fQVkBo#&#n{_6X=v0mBxzdh z%qKxjI~JPM*-=5~+vt#Og*vtfpbA39JB!`(isyVugdOh2d##5Ox_tC{7= zNG+;8nTG>-V`k&Yyu8cNF%EiPb7GNoQB*G=OL)T6JG~h($y?(AltL_P zD)pPIAriux1-vPq0|e7A=bsAv;hiCFTJ(bM{~XHIb{G}Wul3%LmE5xgTmH1R+d~gt za1GQFF8Pn@Sn3 zF&SOt*14&3ebiTY^a&~t#pc9i@CVaDY9=i^@k`&QmSr%;v*4NEyU45#(bY8P{tEIY zP+`%aVT85WSn9XxjD9}6N$$Qr;I`j`#zSCb@z(2*?+8k8yk0aL4MayjPsd{=Gidn& zRnD%*U5DTc=4?|gfoF08ZxEr0`6s~3;I z05fIc0%igmSS{~n=Y6g9-A}Z?QHfchq_~Yui9c_)_OOap1RE5kq?4=3HgqH&vh1Pl zTp=W6q!|MVmT$amXly5}vUxN7*fiRW7r2IQR0q_g^5v+O(R4IU4OI_!N|sRHH-yNM z6O9o+DM<253crd^>2$;%#lgrl+!xH`_z*ZDO~nmy9Q^dspa%iBS0_0oCNj-?3EhL% zOi0n^en-9)!}LQRI4>}{Ks!P2wJ_0Z=mj(t;nzEl2SQi#y$Kj>))hPEklDJW>S53R zGy~q%WUh1|tyN?y0Br(b>BICRPY@;n$9hy$=<;LSp0WD4XnVyJD*lv~%`5@IsVVLf z3K2h5of00Q{L@E^>dg`e3kb@^ze_!M$j{K*)%_*GVve)8_xJk)R zgGvb|Yp4E*^*W4;IdVtjeVFUsS9=BSBiaSdei0AHC&i4T<_E`GtP6EQl`wUQVu}ZA zr0%DEc`*!=-Lic1G_x^*PcXfn(*Tv77w<~0IfWD$UT^Z#Tck(T)kCZE>j&?g>^&JM zldU<^^;18u+x)1CxLMGQ%_~u%9*deE)txT;Z?D%y%@96pxWBCHuY|Ke`Rna|E?B&` zox4|4X{AX-UJw3EyBu|ZCe=e-5YOeK_#K>TbH^fDxT@MHHb>QUM7H@uK$!**NPKBP zc3%uk*a*70vtbNT4rh7fwV$QR2jE--4Tf<3mRGTJ3$?rZBCr;(kUrnksaJqZ_Hdqc zbQ#z$wAeQNGfXw`0x#2eo0g43X%zc+pXWZ~1(S17? zTSPCnc-q&>rBw=dCaHJEljaxff+rt)QHtCM#ftb>*BuKM5Yvqz(}f)7eIy1v+BER6YFfYlo_|QPtNNRep6?pU({_=c@mQL zPLO2tN>QlCBxDfb)~6_+X5qqXsI+41bkF>S3$z9wD6cD`v6%|}R?#_mCNH%Q>raLs zK%7si+usqiIl+@oelz=MKU&p_OWJZwqFYcfZ3qdc$whB>)dpFCYFmrAWvLe%LDzR) zz~3plE%3Lx!^S^uh0QxuBw2G2^Tf0eWdt zE2U%Hp`-M(cRgh>o3(Y7N^+5Yt`daX&3(nxlcO_zE&jL}(+_%yph{=D>XK~zbIsNF zqr-VO1QkwuAWudj9-H~R;>DEAHj(e1><{ZJ$5;}@JY#9HnCCZwo1PzFSxVsQw}M4- z^9~D6teH$#-g0!F=P^7w8{$VjFq`oz5q4(!)h8l zSBVz6q!V0~0!A87DLS&d#3i;_1;2*2MY(B^?r-#V0COiE++>xZADKIMxcdK69KjCMU!v1E)WYChFFR2lJnj-OD%O}c@!R&QCIiK6T}i0$0Cai#XP=3mB+vv@6xNBE>rkDTx- zHkWnnGnEbmR333Jc&K#O|K1gA9F&nD()map(8u-)_=g%h9iRM)hmW zxx1}bNZ-m2?40CbzYGaqX=sy9;8E?=Xr$LGvEhO;Dz1PmEL;(B5ekiTKQGXUDYK+> zR5(h}CQW~RWZ_Fk+T4%<8K+(}tq&Cj?&LseYw@T-nfWWVrJ0J{rEl?I_j?+F%Nw-$ zcFgFt;e5<@?I2k^=?w>}1eHay1=2hzek&PHX)?|JHRsp&#prQGLH;?dW=;rl=>GyX~deAcY`t;+U6sr&A17|(xnVKC!xYBKSa5VH!tPqPw zZ4AyHUwsoM9nT_vmZe_{w5bizd^@z7h6|!v@Gg8CwbV7jmhK_N#8&?dz(pKf39d0Y zA<93xV2e?ya;mV8kyb`4Ecf|&BWd2I-8Pd4hj1|I2-VPgPIf)ho1H^oPe`F+R+YB7 zS>CO_!g$HUz`frjK7~xy_~VSui6we)Tu*8J`tAK*l19)4Gt7Fs{3T?lgqv1A?F=co z3v80Xr)i1vSy!-7zu#Trx1^d`^ilVlL3=F7t@f%0ZIQ#FteUx>>qks1zkL(!{FPcn zOxxP7Xb}~Jor2>%!WRZ3b9FJD7Km534VyV(_QO7yG_ts`C1#Gb z$~5d^w9=iKvNUh)dbDjQA9T8l zIJ(5~A?IzaA7)L@^P+y9I&~0KQWN-W_Tscd6eKQRamMSd>*L1a>cO{q#a3QwH3WN! zO9xZE?nseTYsy9VHE)0p_m)~|f+y~n5sYT-#?9r}x*rsV-pn~x9I+N}QLnpbkAlG8 z8sE$VTX?n?zw~Rzo32Yu_Z7$aHG$mTG$mJBR_Yp(rfxNRbi3TIP?|>4Zb*~-z&SOS ztBj+g?DCt%+Bgd%(8+u~Hq8+b$kO-h=+<)`DG7FP>*?Hb%cT#5r32z#f26;H9}DYq znX5vz*TgURJ!n3y+i*r1`OLfHJ*}VXa(xmt-ZG`i#qIb|rQ_kg?iLH_6hZN!E2U+Z z>W9P%H(FQIj*2T`1YfNISql_sW2m#W~>r)2#)P;(LrE#KtJ zs>MI?J2>qSj(uL1rTHw3>_-xGgNBQ!K%>7=)j8*1B%a)r@daW>?4xy>YEBPXT@dD>cN6q1XJyhzjk#6w zL147!DNb|era`s7h$`P3HPla`z&WO6M-MdQYBX=>3yA?tkazRbuWN;0@3!_OeCveo zV9IoR`=Mw#V=}i4&Z_9#Q^b2tV@P<{p4Q_}bp*Lj%ZTcnKYZoU2ZZX$D zwTP25_#&N|#R{-kMD)HK+D5&6qy_wS4`S8vJkQLO?q)X!du|YaTvZ6ZJ@riR-h=yw ziV9g(JEt+C_jwK3wS*8Kq%}7s*8QlECjEfb8X}L4mdn1;yWL9qjRtsGA*L&jT<1w7lm%SH3_|71xEf@q)oCstEia31^lz_`>W)KGD_as zR~d3_*nf&^X-$}lt@qg+Xjm+ajSvT3K+$A;Ms$0a;P1rOhk%+EPJETR?A^)=WD;)Uj~q*X-L|lWpRjG6vF31*vE7yiC@uEfv!b0TPNa zK2>rXeE(LXtpx#!zV;x~Sy_TDf}E`Ub~aR&p4hit$$ytpD+z#Rn_fH@pQBqAz@GJz zP3EeK`S?UgsQr5@p=aCD{~grjCV*Ut#%LZufA9}+fc_pBUw*_`f2VdeoaQ$hy;o9QUCvv%I+FGdz=e&$gp1RWfi`#Ey42lc?#VCFn4LUxEm1Y zX{|sfpX6Y-*EjaeVz@|Vr?j+q+FSdY9@W&;G|)9t|ACGu8Ds79j5YF85DRXQ)j<)IH4iiA}IdMDMKq)cy6EKsv)!*adAIkW6Uk+nDlhgXZ z+Mdl^gr#wXOo9}7jpTMVL=;>zVE#K`{;uI2Omr%8Hp}n^2pzCYyK#@P zT-P{&rPV);vO?4M62RKda?0EgKYf~=TB9*eB<4|huOlkCfZa4bmHB|N24B(hXm{>A zRRFtp@RqhcSI9#d`RTX{hG6&t2C#6qUz5rD-Ly1G*H9~wVtk3|H>#)r;FZ+UeQy`( z>RD-NL%{53smQ_EnM0C5@Kjz#;MJl4E42qn{@-^k=aC~<4Ga!R0^^*Mdk>7md#5J# zD8q63=;Oer_V3>h3+v zhsWPPv~3rg0gPsz;pfl`4cAZKdZW*}6!y23+Dkof#y1BzLQMRQT;IWX!C%rD*k0YQ zNC5BM=yDor?+KxH9D>-+Q1x=Abc5ur%boqd86=BGTp>;%g(M@Hoek{;x}CoPEil5@ zIF@l6fDpK+V&w5n=ba6&dpHT(>(c*@^AGjF`5RQ|WgZI{y#io;B>u0~r(&wgziW8- zPUdcFnQ3VmfO>sE&-li5#>xr+^tqaQ4S9q&ScdfW)WSZX-qF{)m6tKopKh>Rw2(Z# zVe|v=|4(xzBM2bP%dAkcjQ{bgmN0!gdF+1R!x$l3lHC*r zlu__=#`a~$(tsve|dzzWE#y`8UL#n zz~6nJKL`Bx-T+%)V*XY5qix2}1pQLb>AZ)OQ^^SlqRb%#W?T7xS(C>=wx8#-d3O81 zsPEN{*ZKKZJUl!c&)>eRXu^=A{cFjD-tFA#Z1SjBeILV)8ozc5P^<(za*u?G_J1h& zKYkVBU{2lM(&SHlCp{=h{kJtWlLnAy7R0(^`|kYD#dsmP6YxJJ6*_zwpGR9ycz5F= zSIC`x|DrS7BrU*H+nYstB7Xl#vomz@e%Iq*3o2UG`y8oRfRA@!f5Uy$P9?YBuI-~D zj=KjMiV8gp81^RFQn3(dcV+mEG{^Ia#il!mk^ix|xBvPg4sZeguLK1SX_pR9+`r6Q z__amEFM72}uZn2+_QC#RtzJ|CHuv|PJhs-;wql1xguN%pll(7(pQJ0@s5kZWzZir@ z1}0a-^7jL<2)zLMIECdVP1=queg5|K#ESq*?>2dcmSVE8aaq^BtbAiT^D5O+Si( z6eCU^yp19W)OJyKDEQ|ck~16aiFa~90NyE8?|J`!tNVXl8rck>i{VYh7vNP^qT9~@ zJT$gxu^5OAI7rZzlldS1^dx$NMuCx)+Iq*~<-5sy_y2Dn<6pNkfI(SJ?TPwc0RX3ANi6O8 zC+QvO7a{sY<)xU`!{=3hVgyT60q9o&MDG**a7@_xlm8PL;=k^d8EKvhy%NyT3FK?t z1grj}T|I!&Z)du3y~1so8>OQEgGrRpgIWXi9?YWsPQ$`QN%04^Bz%cdONRFb_S*a( zWf%&2*Y$BNFD(O6F@eh;cmA2${$jTQDl3ml8vlXHGV(xs3}Mn!MgSs%cK86 z`42Y!H97oWk#ux-yLu7j0D~7}<469ZT=WxTLg&R72S8NRjGXWMft*abG<R4Fe@X}_sQWa|*ne2` zkfa~rzvM7H!TQ5;pArs`1bUiNO4O1mpzwzLfx&_LIc`c878U>)XESu}IVnGz z-PV6t3WynE12X*o0LOCcbK^o~6$K89}8Y2TD< z_=CZR`T`XmKE5kV@-yqzok~vd5@%waS1gOpas0*UY?Dfdg-q}mZ*J@zf4%*Q*LBuB zRyx^nn_tX?*HoH5+BSIhnr;fhj$Y@4uDuyC^%nr1Wm0Fqk9p&^29172BCkGHGEkaI z-17g1Zmp5esY- zq9pE8P&%pU&wNKk?9V}8-?Qp;bM)#!CAIH8eMvlg>EypwmE!m8u4L7X#w!38Qb8$y z?{S@Y#(jTzTiFwF%a7q##%@n7`nmz*An&UH41v{1Z+xxi7`&*y2;a!wvUdkKut5BsDHN1trc>6I|#ta`dT8^%cP!TeAs(goHn*0{2WPfdI8U|(wD*s5o5T$ zIrybi?+dv7Yg)7$hmNIh!b{R@f0)FLwmlHv1`xjjbrvZDo(2T!)mHmrn)iZk^e<>qF*K0pkr!99 zqHB6DyMo1}a7JJn`c*|b!UHQuU0Fe#yDJ#}B3L+)VuToqwu3qmR%4v+B>A+BGSxZ@ z0i#)L>|xMNa=nAhf8flutkLMj_Y?xg05N5BUOv7K3OfzDCv6{e`h+Z$;&-mJ$Pal) zX#K?6Zda738nW5X=KRE0_V*s!#gYsKyy}4j{Tpp7<$|vz8&{uDhm%Q(P_aL2(6BQ~ zSLK&VS`#EKX6p-Xfd@4@0aj9+}6+ik$dT|Cvyk7lIc zwWXoCW4iU)@`l^s9}d8T^&}K5zC?XOxag;B((Eki$>q0xpWMHPkgW1}-v9aC7`xEF zYyvS2>YIsq9|+jV=gaody#uUY@gknCa4|1zbRL=>p6M`>#NyuD*ux8gS@&;}x7VS& zgdF02tK1+U1fZja79>U!g({AR4TPMd;wf&PkNbMQ!q^)y$Ml5l|ieafQESET-gwl=FXu zX@F3*0Mf-s0tngjf)LLMiNzn?XeMH30mLG-`M_xbyewc09eKRsFSd?>No02ScZ($WGf z{4pH&apd_ds^?+1+1^plQklo7*Qv=CBsxBs&ecXewvTnA{wpvz+2CIj=!leZW5b(loi^l5QPSfkG> zZR9r0CuO?7D|}YiCmAZ5*XR^};*DDb5b}*WoVP;HH(5YVgZ0Efqc@S!##jHbs}6|y z5Mj&is=FeW-6zy(epq%a;@RAU?8+h#`A9t5`*GGB%&1Zz_N^ix250ZaVA7yD^Ywev*#v4$2{z zPM1%GWJZ~49LKxINUhN}EinX9v-gTRWxqp!uy*9ZIS*juA!kM)BQe>Jm=_z#RbF ztEfG$@dR92mhL5sp2eg0`%AHFk7kp33Y%&!b}k~yM&m}o_A#u?fFCQT;GFgwd;JT< zE&@3Y?K@T62G5hvQfxMIJ+ktp>^`|KH~70DADhoZ^+&@oP=j zzEkn1N8V)T%_W?BAE;aVpmyb~izm|*VVM4q;#rzE4=5ZhencoDkTeqw`rZ?7_8;tZmqq!U4@;bSjLB

(^U+SZavKlhx&TL#jDgdkL&#O19RV&{^gum~-_Fj@4;$#S(`=?t6^$uXo%!X)ebVFn zdAe%Svwu2~XJf4EF#F+QEsfpy8(nfA4W(nVhvfh)@0ZP35Inrl03JbkG|*7ydFe&( zRxuPVd(zhy@7?Xr{pd{{m*QcX#h$$7*`3p01&?N)K12?o=j#+g_x0NCd=7gIk`0ng z4GOg{lHkuUxX=UN(_#Ikwh`9ZfOVce{`EtDPb^=P!8Q!sy=J$Rfsfa)@{tP>@M~m# z*mQy#$1e(_}Mr@@a)mj)fPoZ06nC01=lxTX$2PDgnSyst6<$JvIXFY+HFWrhcr0s zb{%r=hSBd`4prVeGiey6EgPwTj&59FpIwut|q!kSu>oSDWo6!yDth2$^ zJJ_HXlOXk0gVAJ0nu8|^zZnDmEY~uvelOhvTvoro#BCw1K*9aV7|a%0Gt39|1+{z| zPaWSFEoMDFrY3Wp4w573uJCkDdA57pbmgK$3sQ#{Z_ZRTR9GWeY?HNdAyJ8eIlS)(MW!AuY~9Tupr^D}q5N~#Y zp`&7*NjnhAGC=zl{UM?vu67dc)JI?9)%j@P0yD6$aTckp(U-`+5rXOT2R=~ghelLH4zK%jN*oQTNVxGeH@uJ_MTY&3Z^g7sq{z|YQH#z9h>%+ zge^8zE+Z_JAtslbG?hsjps*1@S|zEjxf7!pPsx>>wBcb$I?Y8z=41@qk+6hDuv&M& z0J|~ya`erTis;g|8eYxlnv6)M<|qv7%lHIj$KpsZF1LhugaF$)gyt;|I#~Jl14WGDMyt8=EQqV7!~3AjFi47OpSR7>ROa- zP5Sw`=_I8Gc^I!%_^#?XrN=sX*~E8>NyFZB#@2MI6&M5@X;5~_+uN|R9X_MK37K^$ zNmj+9I8U$6r;yOM-?K>XIB|zoCx7zT9D>49AU%#M-7cwJpQ-^JJ%P97=di(+YSuJq z&vLHF3o89N3A}DkVIKqWMba&cAX0qlU25m_b`I&&!Nx6c-TD=*X`!Cm;q@5X;Z~0h zyT}_B?TM_{DtfQZJJQDW*BaN+W74NN6Zx8+=paCc8xV z3>kSI1YyAd)bb@Ri_UYjl2Uh4-jewhfA{J2z#3zATc51yL0{g3z67)3tz#;|m+N-3 zf{10#or(9H`yXGwHXII8IG#{-qjD%!D{PSLab?;Ix;8a?4)v5}NV6M@KjcHz(=a`^ zUCOP5JseWT-fnv#Mpui{`3|jSX3)(*1IO(MQN9ON|dD#X`!q;XetfnnXh{yvF zB@_F#mnSItEfP}al-SsItjoV!BFTVRNmOEIC!F^vQ-}&-#wTz&*V;vSuUH=m*Avl3-%F@5}ri$FN*8yg8=t&Iy`eS!PT z<>$%)(WCkm>i#n+DMhbl?}s+#lQ4?z!_2J2y#KE!T^kj!G6|&uK-%JQ(@VMe~gfB0Sy!YC++$Y1b`Zo9@ zus)D6c#gJGJdxBF=)`^dfN(jf=slYh*x(Mkf<9BL0n0Y67@z0L_lDL0w)DE&x?2dx z+0MFh4UcYa8X>5h$(pGSW8Z~U<#5&x1+$*d=N-i?*4?R2{P8KX_MI6m{EdoO%O%Cc z8a?j%^4tz5$TROCy~`2z8Kz3!LfUwDyQK=Jl*6#opjJ{jv!fD-SIM#P8zBK3l~1=@zrz(hT6e z%zay~;fB>0Xjq8`O$Ge@7RYL45ZEgm?iCVu>hz!pn=`ns%{{ ze}BZi@xGMPVye84cDivASIK+Y*!Oq@{nYJXcxL?psx@vat3uTA_)yMXZ+3QrhHvdq zBYb8qB^mA?nK*Z($F%`ZES32VeKEm0opjl#=?JWz zZ365kM^oG`50uP^n3JP6v<7gYiC-cE9)e*sX&MZ&19ouI;+` zZfjw2{J=p;itE9lVuSaq7ev9JXVVPqse<~@Y1ec(I9XNE!N8rAC7DB2r241MaXvS~ z%)-iUJb@s z2*cZ1pSB0X$`q~~^M**ac-<}6bjWzNPAY%3`z+O5Ya*H37Dik zT#2rJ!8Q-df|9uJ6u2v5<=fXnia_6+aq+hg$fztF+Hy@K>)J;+ZuOrXHw&W3cS0)u495JNYR7!V|K=)j!Wasewy&-X??hM9nZzt za&U4g6n1XN4m&?~9dms&87|4c@86yhrOZrFiONwjOied~i(R(4&<{R)t=gM_bWrZk zMwv96y);T!5p3G8wRF$A8s3YPClvgC3@%*>R7xdehx<8P;g)m+=ZgyD);gS&GkDzT zdFG<`)wBPc>EfdB`QfSJs%NV6#c97Vz3U|0dCXP3wVTIK@sl+aRk^OfZs<^D$9I48 zqAqw8oSNqKxLqsgs)UvQ7Jjwz-Qt%v*H5FECm!>o7^B-qShU09I&ou);&>Lix)wwF z-_PZBB&}8lpLyfy9Fjgi^e`{}ye(vF*LZS(*mIfHP$WV+C@>?TA~Sdd=m8+NQv-*Q7v)(jp0%%=889;1k^>ZaRiurrjnK>Kz$S=D8-)3mVZy;Ll&Qm4ZM zET2Ec^CjjTNb_3PL8dQXSH40LqAqde1)w0PEu{$tU$sb>Fa^KPpLkuJMNX)3JT@#$IEuu37MH@+}U+t?yg% z?m0Smno*t3CtrMAd+`*7H8MwRam_6U?jdcTUyK8rKs(;<{7F4a|V@%&CBTlYom(= zF~jjM(;^S)W7oZ$@sJxEVBgV6i(kjaN^%$Ec9+TbPn{|Nhe28!`Iw$Q@!9k4b$hjQ z%EN>qgVYV)%U3cz0tujxg(T&;^&(~sK4;w|8^uhYTy@nQg1})9K3~SBuuTr(ca4qQ zNo?X(Ac4&;=q*oa@R({r3?+7`iz_+R{j=p=jq=t4+RX(PC}$?K6ZIws&nZZ#d#70F z8uu?VxHl5_dkLOr_7QqbwnNVc;Ejh=CC!G_Yn}ZXg0Z2;F7^yyLC>9LBo85B@UV%{ zZ@HaMENq8sm+d#upfjn1bEjg7-lk=6Ms8al{X#bZZtK-~OteK6? zWb;}ADju^k@PolQ_9FK^3 zJYiukdctPq7D^Z+pE&;hoM5MV7C7w?2w~ zhqd32qH{Bqx?U?4m@M6U4t3j1=;5pnBd($E@I=ly0behl`X{$r6a_duQ;tEE!a8Uz zo0}}}d8Tz$#mSn5=JzLbmqts*1z0VTpRrc!^iL#)1T9X7EOr~hyFSgvNVacw+jX(h z^swsHo!mC$%eq3D|0QeFTM5^W?@>};{MEU9A~3VyT(>g^9#7Xa+^(HE*Hu6dY!)oK zLCgTbcosx%@pJ&2c9X_3n=TUqFCRIw(jvDQs z&;rJ?{F*C1|D75tuz-co#2!`1iQuRP-WDu#M95pz3)O*tJ?EzxA$08e(AZTaH{&5| zo#3aCYkxkwtj247%i~p(NZ%TC+8ZMFYju-x%s8#z#s^4_2JpRQjE?tnQ^cE z<>E%EKj4UT5%f5T;vvAtc3l|Y$TqF4iBQyZd9HM(=knf`S3g?BvX-GTcOGr$B|P*7oiotW3^o8U? z9=Qu`G@JYo5Q<>4T$*jPn@sI(M#c^Yc+Y#6TK39Hch}622^? z@CI`!PAzo>+fsl`s{NjwOx3u;9@tJVO^@eT$9(UjA08`;X@d%B;^DR`=nx}(2gIC_ zGdm?=(v9qe-xaaPpKl(iUhN199DesTJ|=C8u*AdW0f!EPq}JXX{ICxb#)^;`M_#OR zZ+dD3OhMG0Erh39P=^&(-%_)}-E8LgdR$s3s$|s%dv>zg!TJ#Ew_ZG5-Sl?Wa+BXj zKg*_u-UBSoJEunZ7q!;$u0Fv8K}7{ z^Lq`J^_OB~@5$_bH$6WCnVg5Sj2HVqB{`k;Uan->PjS60Xjd-{*00@(+Xn^hZQAYY0xmejgo=g-$@Ry*k8PSTqe>EQIsIhkTIx=mDVEmd zwtS*AEdc=yA!8T>YnQl7TlPbQ5!m0@;w0qa)^JGS5a z>d~js(cJuJ!&bA)tb1FcnCK}yr3(`({XSM-@5gH?b*UAbeor_T%`JPTCbl};mcx@a z$C&+X-Rt13QjznxU}db{LDv_*jf;ypqG$lK+(x%$g6i|=;YanuIt8gAQmOm~v??cgu^eBUI+^sY&68c`iJF@hC zAKv}tcH_dnU~pzy>ntN2q*BQW)Cl}%wqIEVNI~HqnZk3S9ogCSnQzn9W^B$ntQ3-i zgxA(tlg(DKb_C9m-rZC~)t~@du9eecY{SZ7@c3bj8)KydVgk1xa`ur$8$PCkM@+cg z-w`!w=+y^eL)M(Sc{=}$FB9e&MYzeqnHO8!ECkP8#oOh4oLl1isv@a9hweBoZW;GX zbVz&z(%s7+Tw6!d7mxX*1+flKW-XaJoO#h%i!L_hwD)GnEH)}d6|3ULl}=~!*}`F3 z;5Eo{_!chPvv(U5+%j(7&mADXR$<4 zWwPIZ#+%=3?Q?)H0e6ELrfI*>DBRGzWL6buV0zOokwK1vi3a4yH7o`j8|ut@VkeX9v78rSN~tDWK}U@6Xz_*@?$qnlOzT=9 zi0u)EM$qL!>(uyn@H~2?SD%GF6%F13T4C{~4M*g4^plgZF1C??@x(PVe{ymfKqfm0 zC?)nm8JfXMUL@c;N|}z$t=gxiQ)Iro()247M6&fjjTyW9yL#2wVIsh((c6LnYF2fOkr=cT8=530L9cIHiPzX?zjD(5HKy=Pnnsea%dq2>8;CRgKjhh*5@cn2dT&n1UO+wYh`e%RU_Ex>u^m!mBC29~X0wNS2dN}bV@zjp zxjk+Wt?vzPyRZ3_kzy5!HpUK+Gg5A>1doh*w}*t)>{lgtt2J>&72vELhskBODD6m* z*ab1F9VO=KBYP#c+K37YE9@T=E=cWS9d#R3uRZ+J+S%SC&|vBEda84rn&P{7VFtY^ zUiU`TL~}kIQbKFZ&qUsb-Gd_h3FZCS`@#^rsC0R=RE^Au7)-5Lh03YzLkiRaLf59? zjNX#Q7EscL9y0@k%)wra0`ICI(eZ*)Ma2y8VbF?g_|&rHW+~a0K1hBZwl9f@a*fI5 z?;S1OPTX2FLV#qidWAF!8ukx#L7xD`Op}sV0Ad^O*=6P3GPmfRsageQ?#en92t{cAIv&aG}rjd1xeH7gU5ag4k} z7Wr&Spg@C#&VG7KD(M@R8+pbSBfESw5YoeU&$FESQLQMM8~Wu5szgI|us1k_{SG$s zi|LUU4LVw}(aq;>+rp+2-)EiBlbW|G$ufhV9M)4Cs zMxh@#H_Mfr0-ZAZ46W3sFonl|bzAVJqVrp?f$$ZH5 z-No#z!B7*bLws^Hcb+3(uSql{0md!z$XP053PRMMK(>0>(hNzW{2dF`vI*=RRO#uP6Q0t%0bA>Cr+#7@Hebwjqg@rvC`o@~7KwvkK;5i8=_JOF3og0hJ zPun4y-GKNs$AVj_RK_ISMah?oTxDD1r^Y%SeCa-0qgep3CIF4wj)Q$T)pTW{B^H|o zE<)C~@d)Q^Q8j)UpUHhR$4($>RT7-aSO3e$N_3UYhEf74^}tDy?@{sP#pUrS`#671 zO$u^KS=Il+$<>~{s#^Ep140#XEkF0beBW4YX4R$EwPe$YWw1csXZ=WnvEqo)%e>&g zdFZLl(Bk`kjjpra_|`CrZJRPUl4e!c=i zU52B2UTLv}4y^hU6l89n`m~jwk8)o5U^jxfI9j0l>XU3ar1Z#j+o5Xe2mQf`O(EPiE|X!i zJXdZqot{sBtk(cfNNKvRT@V@B#M&_^h1SI^n8rw&>InVB?7Ufw@0{X?Cjew|ougjX z8zaZIG&GYS;Nuq&Cfpgrb5eCdwpK0q4kpJ|L5Q+Q5xhLi;h44RK&tKY_?~(s$3$h< zB+TA{FZPNQ5C2@T_NSA;f+5;Pa-iNAjeQ%k5u5zW+9iB*xVx6LPPZ?L@pk#^_Icgi z2`w&?*#b)9Sg-9dzN47H@;>fSkFdoRENb_@p^JitL!ddkzOzyi&fepOQ4_|luF?f( z^ymltyc;t-i)6l6D8~C~%SrA__0R~qn)R^VGV>umfwYer&#O55t0rQnox;JEv-`!P zHm0L13ugO0+D8l)#rk!;TW*OpP2DMW&A4kqsy+MU6w35g@ImWN4JfTjohaTz>Oqj| z$hAPwm##}RyVHW&_#(oijQ;6X#^c0d@$PuXuwd^q*Ac#~a+~p-!rmW2A(5uC)9Xu; zb!*S>s@CR5OeG~fvRd}7C>&(ijs0dCcvlHwRuCLyg@qqBdF%1W6ZYNj5?J*>7s78b z6+&zP`hpCRlc9T@lN%OnrxUAL{z_doPkVS>g>`8EA%DUnT~(Du8FT2!v)HG%d2A%T zEHf*HIS3A~j?GLH`d8od*yPcwGi&m8H{xVS)F?j{y^UhrJbPmP>qdEl-p=RPQ40+F*MhznC z@eD6eD$`iOQ{wQC5Xm>7mEmH&;ojrnxA|7k7~hV*5S*Ac0M~9LDY~(0zxc!0`5a zJlwCXnZG&Te~lTH4AjPe1UFw~SONqH8;dWeQ;xn~Nw{krJ+n}l-=TmJPp*BQEbBaOHazh@-`DMvRE2m3x0Agr;q6C3&-1K<|&vFp(J`6e?QFp z-z{O1^uYC0D@!5uH@QBC$#*0GNSMy`r!fSt!cX7&Jv(qVWBxs^1bp8PFxao^XU=zl zMFSZnQ)4>dqR;?@G%lJ1%zsJJ?7Pq3B>iVV>aU5FU9MTuqC4>}f1DUMR^S=pO5ohP zlNb5yk>A%{;sKDLwCkpi54#S4MOhO8v~Rk(Rq@CeOT1~7sC}IKTLtD{wD33b?_E3p zW+D=10yK{;FwgN)|KiIda|cH0(5!QBzBA!`qWma5b{LqV6ulspm66@OdQPi1bjcTw zPoGWmgiY!;cG!~h0`#V{f0ZCM)a)X#+R0*E5*q(jY?m}|E+Y11k2nnZVc98!okjOa zw|nmLP=G!*jq-g|SmEfWa~(K)s4D5HKxNoqQ4tGj3vK-JjfBKrq|F!=wUW-e)fMzk zAN&4$F;`U-wMzLKT7lzT!L>!!4YP~yJnb7!dRQpv5jY3dBYL&t_77qoBHHo}l>k_U zs`iDuFtUn5%*%M7*M%vao$sbjd_sCu=^p!C``#ba^zPkuw8~EC_n6hGfuUWNNw@nx$KlJq7AmftAfqDcM0()e=g^KU%`0;_=DDFXi_vu-)P#IjTV zvXh6LTvlN|UqITs4dUB({O{2^49minc6JZ5&qlv}uchbWit&3^kWR~YQ(LWa7WxsT z$YU#M{MfwlZ(-sY{I}KEbrlQZo@%Cm@&~q;Hh`0y`py&NCZY&Hulb`kJm87nK1{|0 zg}3miqCZ9S=BbM(m|;qcm27U>?s_~Qn=(+8u_X&)kJP}jPJw>jzlW}EF`^m|Jxen~#g;tZya2O1AvAI5*;+!Np`8Jvvpa|#k3D|;o!G~} z&a&8cfTf_F@(w1>>YGnEEk+N=Zq!KpyQmNDLzAO^8O2HCC$mSMet-borXBiU?FztM zwNolHTXCV=oH-0+cQghw9E`2Yil)ltsy^?K!W8>{ddoZ`+Q8;V)p@Gb7Os?}Ra#b2 zr1JtX9?t%{Ipb0S=m~_MK9t4CWNo3mVQ>GoU;le7gs(9&HAu!_;~D5&zlLtQ?*I0? zCse?k`Kralf#`ub3%y?pZ)$n^TeUqoMb#k9hT@u|wnQU~JcDjz>A!{IyM$61lkRW# z2=@&B9^=NJGPtp;r`Z6m0G2Uty#L@v694@rzaV-hA59PMQlQ{WhOd=%-o{Z??csxr zo0_2hTN(Vd*4J!v*QU-=7dat99B9}2Bx($z{%4Y+i27^6b<4`ifffdNt$(JESjIP; zZwZ=}2kS8L0n34nAn5$>jokQCMjkph4L9=DI9n>9+i(*1Zxo4t(^;7k zy-a>7!;O92sqVAYLa+ev{-_ZI(7%ozrc-~y$-y9UNZf0_#5$6j7OVu4PEwtNf1t*F zdlY#7!=614({Jzi%sS@8f%#w$4iYeRBxNv+cYPD4sBX|U{|;zSb34HptM|uS^9ym! z@D#9yxAsLl^*ovc9v08zaHYKTNr8!wfB*ZN7SyIJwl9*Z$3Usq1WK(CxKv|C0^q17 z(pCDbmLUN;;+E`0ge0xGovb%nlYbMB(BE%zsZ7Tec#q@y8K*3hyTNy`c_=ID$bpQ% zSlmBgBmN~0?sK8QC+R1&C(0}bcbsNws9@{j9{{Agu!*-n-TuJEPxAb_)$q*|4PKO& zm47x1TP}0mAaP7y7f)@$3`A_@`Hto%YYl8TKmP+cu_+n-$?#AWA~PS{GnlN4mrpu~ zUF9wG8kl+uJXyAYw?1W1)lOeTWw^sX%@qGfj=jLKq{$P{VB}JrDlOexZ6)aXHP5as z>92U0Ay7j^7>om)(qo#Q9lAOGf4t5p=%EH*aWE9zvq134`)KCc`_j~&S_ZuH8Pag- zEgiL;mqLIrk(KSNsw1j=A(3ihg4&V=G1B`Jc)|s?WnAuj2IgC+((4UFr8w#0rs7EX zwCA1c0wZQe-qH^bg5P>kF#>N#kpROAaTlpGJ`z1{<+zE9{{45#=%`66ZAok|dHT6x z{l>K3CZYDF^C^K=$2EEFXKeL(s+|TJiBTnV18(Tu_?w}CpW0-^3egU`aZq8(11^t} z%$P8GKbh6u8wD4W#TV%|X>8Y=gAzGRYE6@Nv0w}G6m?Dmf?w^08CY3+1;ZFV2u_d= zphZ#&(2m-SR8_TA_h9sV4NjAfbT7V){cbG^ofz{K)`%Q*waZnFYkwREigz{f!m2H{ ztEhM#Mt+!rFo3VITEEY3Bm?5^3kMk0`k>;HrOcE4tA!@fM&z%{cqDT5808At-A{dS z(bk`SanU;~+A~Yw@^i=V;xeWQvVV$}-t_A<<_hW8KYMcVvET6YLi;U>2!s(r19_Ty zsw(9b98S)qBh@7ODvnjx9PDO^1P`9aL>Hej2(eUf3gTgJMJbB9jC6`V4lh)h#>w5} z)2x!+Ilxl3Gr{mqVqPbY4E9RWLD+H6g}Jut_-BCN5QovDBSX`J=UzpaMzuwDF6oL_ z^EBhfUXxY-TKT2ao5t6_E<<*jE|BeKIS>9?|4~2o-&;5!76}&F2h&6q$V3fz?Gctt7~|ccOWkI@2%hMaPxlFN vtgE2ZiPV=rrjj;zy@%4H8>kE0JCJBEvj>beh8<^b0smwr6<-!h7zO-45Daz& literal 0 HcmV?d00001 diff --git a/docs/management/connectors/index.asciidoc b/docs/management/connectors/index.asciidoc index ea4fa46d3e8082..033b1c3ac150ea 100644 --- a/docs/management/connectors/index.asciidoc +++ b/docs/management/connectors/index.asciidoc @@ -6,6 +6,7 @@ include::action-types/teams.asciidoc[] include::action-types/pagerduty.asciidoc[] include::action-types/server-log.asciidoc[] include::action-types/servicenow.asciidoc[] +include::action-types/swimlane.asciidoc[] include::action-types/slack.asciidoc[] include::action-types/webhook.asciidoc[] include::pre-configured-connectors.asciidoc[] diff --git a/x-pack/plugins/actions/README.md b/x-pack/plugins/actions/README.md index 5b4a197eea4620..b19e89a599840b 100644 --- a/x-pack/plugins/actions/README.md +++ b/x-pack/plugins/actions/README.md @@ -19,7 +19,7 @@ Table of Contents - [Usage](#usage) - [Kibana Actions Configuration](#kibana-actions-configuration) - [Configuration Options](#configuration-options) - - [Adding Built-in Action Types to allowedHosts](#adding-built-in-action-types-to-allowedhosts) + - [**allowedHosts** configuration](#allowedhosts-configuration) - [Configuration Utilities](#configuration-utilities) - [Action types](#action-types) - [Methods](#methods) @@ -54,6 +54,9 @@ Table of Contents - [`subActionParams (getFields)`](#subactionparams-getfields-2) - [`subActionParams (incidentTypes)`](#subactionparams-incidenttypes) - [`subActionParams (severity)`](#subactionparams-severity) + - [Swimlane](#swimlane) + - [`params`](#params-3) + - [| severity | The severity of the incident. | string _(optional)_ |](#-severity-----the-severity-of-the-incident-----string-optional-) - [Command Line Utility](#command-line-utility) - [Developing New Action Types](#developing-new-action-types) - [licensing](#licensing) @@ -102,8 +105,8 @@ This module provides utilities for interacting with the configuration. | ensureUriAllowed | _uri_: The URI you wish to validate is allowed | Validates whether the URI is allowed. This checks the configuration and validates that the hostname of the URI is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all URI's are allowed (using an "\*") then it will never throw. | No return value, throws if URI isn't allowed | | ensureHostnameAllowed | _hostname_: The Hostname you wish to validate is allowed | Validates whether the Hostname is allowed. This checks the configuration and validates that the hostname is in the list of allowed Hosts and throws an error if it is not allowed. If the configuration says that all Hostnames are allowed (using an "\*") then it will never throw | No return value, throws if Hostname isn't allowed . | | ensureActionTypeEnabled | _actionType_: The actionType to check to see if it's enabled | Throws an error if the actionType is not enabled | No return value, throws if actionType isn't enabled | -| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean | -| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings | +| isRejectUnauthorizedCertificatesEnabled | _none_ | Returns value of `rejectUnauthorized` from configuration. | Boolean | +| getProxySettings | _none_ | If `proxyUrl` is set in the configuration, returns the proxy settings `proxyUrl`, `proxyHeaders` and `proxyRejectUnauthorizedCertificates`. Otherwise returns _undefined_. | Undefined or ProxySettings | ## Action types @@ -113,17 +116,17 @@ This module provides utilities for interacting with the configuration. The following table describes the properties of the `options` object. -| Property | Description | Type | -| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | -| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string | -| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | -| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number | -| minimumLicenseRequired | The license required to use the action type. | string | +| Property | Description | Type | +| ------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------- | +| id | Unique identifier for the action type. For convention, ids starting with `.` are reserved for built in action types. We recommend using a convention like `.mySpecialAction` for your action types. | string | +| name | A user-friendly name for the action type. These will be displayed in dropdowns when chosing action types. | string | +| maxAttempts | The maximum number of times this action will attempt to execute when scheduled. | number | +| minimumLicenseRequired | The license required to use the action type. | string | | validate.params | When developing an action type, it needs to accept parameters to know what to do with the action. (Example `to`, `from`, `subject`, `body` of an email). See the current built-in email action type for an example of the state-of-the-art validation.

Technically, the value of this property should have a property named `validate()` which is a function that takes a params object to validate and returns a sanitized version of that object to pass to the execution function. Validation errors should be thrown from the `validate()` function and will be available as an error message | schema / validation function | -| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function | -| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function | -| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | -| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function | +| validate.config | Similar to params, a config may be required when creating an action (for example `host` and `port` for an email server). | schema / validation function | +| validate.secrets | Similar to params, a secrets object may be required when creating an action (for example `user` and `password` for an email server). | schema / validation function | +| executor | This is where the code of an action type lives. This is a function gets called for executing an action from either alerting or manually by using the exposed function (see firing actions). For full details, see executor section below. | Function | +| renderParameterTemplates | Optionally define a function to provide custom rendering for this action type. | Function | **Important** - The config object is persisted in ElasticSearch and updated via the ElasticSearch update document API. This API allows "partial updates" - and this can cause issues with the encryption used on specified properties. So, a `validate()` function should return values for all configuration properties, so that partial updates do not occur. Setting property values to `null` rather than `undefined`, or not including a property in the config object, is all you need to do to ensure partial updates won't occur. @@ -133,15 +136,15 @@ This is the primary function for an action type. Whenever the action needs to ex **executor(options)** -| Property | Description | -| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| actionId | The action saved object id that the action type is executing for. | -| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | -| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. | -| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | -| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead.| -| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | -| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) +| Property | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| actionId | The action saved object id that the action type is executing for. | +| config | The action configuration. If you would like to validate the config before being passed to the executor, define `validate.config` within the action type. | +| secrets | The decrypted secrets object given to an action. This comes from the action saved object that is partially or fully encrypted within the data store. If you would like to validate the secrets object before being passed to the executor, define `validate.secrets` within the action type. | +| params | Parameters for the execution. These will be given at execution time by either an alert or manually provided when calling the plugin provided execute function. | +| services.scopedClusterClient | Use this to do Elasticsearch queries on the cluster Kibana connects to. Serves the same purpose as the normal IClusterClient, but exposes an additional `asCurrentUser` method that doesn't use credentials of the Kibana internal user (as `asInternalUser` does) to request Elasticsearch API, but rather passes HTTP headers extracted from the current user request to the API instead. | +| services.savedObjectsClient | This is an instance of the saved objects client. This provides the ability to do CRUD on any saved objects within the same space the alert lives in.

The scope of the saved objects client is tied to the user in context calling the execute API or the API key provided to the execute plugin function (only when security isenabled). | +| services.log(tags, [data], [timestamp]) | Use this to create server logs. (This is the same function as server.log) | ### Example @@ -262,16 +265,16 @@ The [ServiceNow user documentation `params`](https://www.elastic.co/guide/en/kib The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------- | -| short_description | The title of the incident. | string | -| description | The description of the incident. | string _(optional)_ | +| Property | Description | Type | +| ----------------- | ---------------------------------------------------------------------------------------------------------------- | ------------------- | +| short_description | The title of the incident. | string | +| description | The description of the incident. | string _(optional)_ | | externalId | The ID of the incident in ServiceNow. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| severity | The severity in ServiceNow. | string _(optional)_ | -| urgency | The urgency in ServiceNow. | string _(optional)_ | -| impact | The impact in ServiceNow. | string _(optional)_ | -| category | The category in ServiceNow. | string _(optional)_ | -| subcategory | The subcategory in ServiceNow. | string _(optional)_ | +| severity | The severity in ServiceNow. | string _(optional)_ | +| urgency | The urgency in ServiceNow. | string _(optional)_ | +| impact | The impact in ServiceNow. | string _(optional)_ | +| category | The category in ServiceNow. | string _(optional)_ | +| subcategory | The subcategory in ServiceNow. | string _(optional)_ | #### `subActionParams (getFields)` @@ -311,20 +314,20 @@ The [Jira user documentation `params`](https://www.elastic.co/guide/en/kibana/ma The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ----------- | ---------------------------------------------------------------------------------------------------------------- | --------------------- | -| summary | The title of the issue. | string | -| description | The description of the issue. | string _(optional)_ | +| Property | Description | Type | +| ----------- | ------------------------------------------------------------------------------------------------------- | --------------------- | +| summary | The title of the issue. | string | +| description | The description of the issue. | string _(optional)_ | | externalId | The ID of the issue in Jira. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| issueType | The ID of the issue type in Jira. | string _(optional)_ | -| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | -| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | -| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ | +| issueType | The ID of the issue type in Jira. | string _(optional)_ | +| priority | The name of the priority in Jira. Example: `Medium`. | string _(optional)_ | +| labels | An array of labels. Labels cannot contain spaces. | string[] _(optional)_ | +| parent | The ID or key of the parent issue. Only for `Sub-task` issue types. | string _(optional)_ | #### `subActionParams (getIncident)` -| Property | Description | Type | -| ---------- | --------------------------- | ------ | +| Property | Description | Type | +| ---------- | ---------------------------- | ------ | | externalId | The ID of the issue in Jira. | string | #### `subActionParams (issueTypes)` @@ -333,20 +336,20 @@ No parameters for the `issueTypes` subaction. Provide an empty object `{}`. #### `subActionParams (fieldsByIssueType)` -| Property | Description | Type | -| -------- | -------------------------------- | ------ | +| Property | Description | Type | +| -------- | --------------------------------- | ------ | | id | The ID of the issue type in Jira. | string | #### `subActionParams (issues)` -| Property | Description | Type | -| -------- | ----------------------- | ------ | +| Property | Description | Type | +| -------- | ------------------------ | ------ | | title | The title to search for. | string | #### `subActionParams (issue)` -| Property | Description | Type | -| -------- | --------------------------- | ------ | +| Property | Description | Type | +| -------- | ---------------------------- | ------ | | id | The ID of the issue in Jira. | string | #### `subActionParams (getFields)` @@ -360,10 +363,10 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/ ### `params` -| Property | Description | Type | -| --------------- | -------------------------------------------------------------------------------------------------- | ------ | +| Property | Description | Type | +| --------------- | ------------------------------------------------------------------------------------------------- | ------ | | subAction | The subaction to perform. It can be `pushToService`, `getFields`, `incidentTypes`, and `severity. | string | -| subActionParams | The parameters of the subaction. | object | +| subActionParams | The parameters of the subaction. | object | #### `subActionParams (pushToService)` @@ -374,13 +377,13 @@ The [IBM Resilient user documentation `params`](https://www.elastic.co/guide/en/ The following table describes the properties of the `incident` object. -| Property | Description | Type | -| ------------- | ---------------------------------------------------------------------------------------------------------------------------- | --------------------- | -| name | The title of the incident. | string _(optional)_ | -| description | The description of the incident. | string _(optional)_ | +| Property | Description | Type | +| ------------- | ------------------------------------------------------------------------------------------------------------------- | --------------------- | +| name | The title of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | | externalId | The ID of the incident in IBM Resilient. If present, the incident is updated. Otherwise, a new incident is created. | string _(optional)_ | -| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ | -| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ | +| incidentTypes | An array with the IDs of IBM Resilient incident types. | number[] _(optional)_ | +| severityCode | IBM Resilient ID of the severity code. | number _(optional)_ | #### `subActionParams (getFields)` @@ -394,6 +397,36 @@ No parameters for the `incidentTypes` subaction. Provide an empty object `{}`. No parameters for the `severity` subaction. Provide an empty object `{}`. +--- +## Swimlane + + +### `params` + +| Property | Description | Type | +| --------------- | ---------------------------------------------------- | ------ | +| subAction | The subaction to perform. It can be `pushToService`. | string | +| subActionParams | The parameters of the subaction. | object | + + +`subActionParams (pushToService)` + +| Property | Description | Type | +| -------- | ------------------------------------------------------------------------------------------------------------- | --------------------- | +| incident | The Swimlane incident. | object | +| comments | The comments of the case. A comment is of the form `{ commentId: string, version: string, comment: string }`. | object[] _(optional)_ | + + +The following table describes the properties of the `incident` object. + +| Property | Description | Type | +| ----------- | -------------------------------- | ------------------- | +| alertId | The alert id. | string _(optional)_ | +| caseId | The case id of the incident. | string _(optional)_ | +| caseName | The case name of the incident. | string _(optional)_ | +| description | The description of the incident. | string _(optional)_ | +| ruleName | The rule name. | string _(optional)_ | +| severity | The severity of the incident. | string _(optional)_ | --- # Command Line Utility diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts index 10955af2f3b13d..5feb47ea6c962b 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.test.ts @@ -21,6 +21,7 @@ const ACTION_TYPE_IDS = [ '.pagerduty', '.server-log', '.slack', + '.swimlane', '.teams', '.webhook', ]; diff --git a/x-pack/plugins/actions/server/builtin_action_types/index.ts b/x-pack/plugins/actions/server/builtin_action_types/index.ts index 551d3d02ff05de..07859cba4c3719 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/index.ts @@ -12,6 +12,7 @@ import { Logger } from '../../../../../src/core/server'; import { getActionType as getEmailActionType } from './email'; import { getActionType as getIndexActionType } from './es_index'; import { getActionType as getPagerDutyActionType } from './pagerduty'; +import { getActionType as getSwimlaneActionType } from './swimlane'; import { getActionType as getServerLogActionType } from './server_log'; import { getActionType as getSlackActionType } from './slack'; import { getActionType as getWebhookActionType } from './webhook'; @@ -65,6 +66,7 @@ export function registerBuiltInActionTypes({ ); actionTypeRegistry.register(getIndexActionType({ logger })); actionTypeRegistry.register(getPagerDutyActionType({ logger, configurationUtilities })); + actionTypeRegistry.register(getSwimlaneActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getServerLogActionType({ logger })); actionTypeRegistry.register(getSlackActionType({ logger, configurationUtilities })); actionTypeRegistry.register(getWebhookActionType({ logger, configurationUtilities })); diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts index 3161e97583b72f..aa439787ad96fa 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/index.ts @@ -25,7 +25,7 @@ import { JiraSecretConfigurationType, JiraExecutorResultData, ExecutorSubActionGetFieldsByIssueTypeParams, - ExecutorSubActionGetIssueTypesParams, + ExecutorSubActionCommonFieldsParams, ExecutorSubActionGetIssuesParams, ExecutorSubActionGetIssueParams, ExecutorSubActionGetIncidentParams, @@ -137,7 +137,7 @@ async function executor( } if (subAction === 'issueTypes') { - const getIssueTypesParams = subActionParams as ExecutorSubActionGetIssueTypesParams; + const getIssueTypesParams = subActionParams as ExecutorSubActionCommonFieldsParams; data = await api.issueTypes({ externalService, params: getIssueTypesParams, diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts index a81dfaeef8175a..eb2f540deaa9ad 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/schema.ts @@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('issueTypes'), - schema.literal('fieldsByIssueType'), -]); - export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ summary: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts index f6462bac9d83e7..9430d734287d33 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/service.test.ts @@ -155,12 +155,12 @@ describe('Jira service', () => { ).toThrow(); }); - test('throws without username', () => { + test('throws without email/username', () => { expect(() => createExternalService( { - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: 'elastic@elastic.com' }, + config: { apiUrl: 'test.com', projectKey: 'CK' }, + secrets: { apiToken: 'token' }, }, logger, configurationUtilities @@ -168,12 +168,12 @@ describe('Jira service', () => { ).toThrow(); }); - test('throws without password', () => { + test('throws without apiToken/password', () => { expect(() => createExternalService( { - config: { apiUrl: 'test.com' }, - secrets: { apiToken: '', email: undefined }, + config: { apiUrl: 'test.com', projectKey: 'CK' }, + secrets: { email: 'elastic@elastic.com' }, }, logger, configurationUtilities diff --git a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts index 89a5551554c4a7..74d53901d55d91 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/jira/types.ts @@ -16,10 +16,10 @@ import { ExecutorSubActionGetIncidentParamsSchema, ExecutorSubActionHandshakeParamsSchema, ExecutorSubActionGetCapabilitiesParamsSchema, - ExecutorSubActionGetIssueTypesParamsSchema, ExecutorSubActionGetFieldsByIssueTypeParamsSchema, ExecutorSubActionGetIssuesParamsSchema, ExecutorSubActionGetIssueParamsSchema, + ExecutorSubActionCommonFieldsParamsSchema, } from './schema'; import { ActionsConfigurationUtilities } from '../../actions_config'; import { Logger } from '../../../../../../src/core/server'; @@ -124,8 +124,8 @@ export type ExecutorSubActionGetCapabilitiesParams = TypeOf< typeof ExecutorSubActionGetCapabilitiesParamsSchema >; -export type ExecutorSubActionGetIssueTypesParams = TypeOf< - typeof ExecutorSubActionGetIssueTypesParamsSchema +export type ExecutorSubActionCommonFieldsParams = TypeOf< + typeof ExecutorSubActionCommonFieldsParamsSchema >; export type ExecutorSubActionGetFieldsByIssueTypeParams = TypeOf< @@ -157,12 +157,12 @@ export interface HandshakeApiHandlerArgs extends ExternalServiceApiHandlerArgs { export interface GetIssueTypesHandlerArgs { externalService: ExternalService; - params: ExecutorSubActionGetIssueTypesParams; + params: ExecutorSubActionCommonFieldsParams; } export interface GetCommonFieldsHandlerArgs { externalService: ExternalService; - params: ExecutorSubActionGetIssueTypesParams; + params: ExecutorSubActionCommonFieldsParams; } export interface GetFieldsByIssueTypeHandlerArgs { diff --git a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts index 9095780fea17c2..9f76a236cacd5e 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/resilient/schema.ts @@ -25,14 +25,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('incidentTypes'), - schema.literal('severity'), -]); - export const ExecutorSubActionPushParamsSchema = schema.object({ incident: schema.object({ name: schema.string(), diff --git a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts index 59b0803d189cdd..6fec30803d6d79 100644 --- a/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts +++ b/x-pack/plugins/actions/server/builtin_action_types/servicenow/schema.ts @@ -24,14 +24,6 @@ export const ExternalIncidentServiceSecretConfigurationSchema = schema.object( ExternalIncidentServiceSecretConfiguration ); -export const ExecutorSubActionSchema = schema.oneOf([ - schema.literal('getFields'), - schema.literal('getIncident'), - schema.literal('pushToService'), - schema.literal('handshake'), - schema.literal('getChoices'), -]); - const CommentsSchema = schema.nullable( schema.arrayOf( schema.object({ diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts new file mode 100644 index 00000000000000..1e633e21758084 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.test.ts @@ -0,0 +1,142 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { api } from './api'; +import { ExternalService } from './types'; +import { + apiParams, + externalServiceMock, + recordResponseCreate, + recordResponseUpdate, +} from './mocks'; +import { Logger } from '@kbn/logging'; + +let mockedLogger: jest.Mocked; + +describe('api', () => { + let externalService: jest.Mocked; + + beforeEach(() => { + externalService = externalServiceMock.create(); + }); + + describe('pushToService', () => { + test('it pushes a new record', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params, + }); + + expect(externalService.createComment).toHaveBeenCalled(); + expect(externalService.createRecord).toHaveBeenCalled(); + expect(externalService.updateRecord).not.toHaveBeenCalled(); + + expect(res).toEqual({ + ...recordResponseCreate, + comments: [ + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + ], + }); + }); + + test('it pushes a new record without comment', async () => { + const params = { + ...apiParams, + incident: { ...apiParams.incident, externalId: null }, + comments: [], + }; + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params, + }); + + expect(externalService.createComment).not.toHaveBeenCalled(); + expect(externalService.createRecord).toHaveBeenCalled(); + expect(res).toEqual(recordResponseCreate); + }); + + test('updates existing record', async () => { + const res = await api.pushToService({ + externalService, + logger: mockedLogger, + params: apiParams, + }); + + expect(externalService.createComment).toHaveBeenCalled(); + expect(externalService.createRecord).not.toHaveBeenCalled(); + expect(externalService.updateRecord).toHaveBeenCalled(); + expect(res).toEqual({ + ...recordResponseUpdate, + comments: [ + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', + }, + ], + }); + }); + + test('it calls createRecord correctly', async () => { + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createRecord).toHaveBeenCalledWith({ + incident: { + alertId: '123456', + caseId: '123456', + caseName: 'case name', + description: 'case desc', + ruleName: 'rule name', + severity: 'critical', + }, + }); + }); + + test('it calls createComment correctly', async () => { + const mockedToISOString = jest + .spyOn(Date.prototype, 'toISOString') + .mockReturnValue('2021-06-15T18:02:29.404Z'); + + const params = { ...apiParams, incident: { ...apiParams.incident, externalId: null } }; + await api.pushToService({ externalService, params, logger: mockedLogger }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(1, { + createdDate: '2021-06-15T18:02:29.404Z', + incidentId: '123456', + comment: { + commentId: 'case-comment-1', + comment: 'A comment', + }, + }); + + expect(externalService.createComment).toHaveBeenNthCalledWith(2, { + createdDate: '2021-06-15T18:02:29.404Z', + incidentId: '123456', + comment: { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + }); + + mockedToISOString.mockRestore(); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts new file mode 100644 index 00000000000000..343a94e52711ff --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/api.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { + ExternalServiceIncidentResponse, + ExternalServiceApi, + Incident, + PushToServiceApiHandlerArgs, + PushToServiceResponse, +} from './types'; + +const pushToServiceHandler = async ({ + externalService, + params, +}: PushToServiceApiHandlerArgs): Promise => { + const { comments } = params; + let res: PushToServiceResponse; + const { externalId, ...rest } = params.incident; + const incident: Incident = rest; + + if (externalId != null) { + res = await externalService.updateRecord({ + incidentId: externalId, + incident, + }); + } else { + res = await externalService.createRecord({ incident }); + } + + const createdDate = new Date().toISOString(); + + if (comments && Array.isArray(comments) && comments.length > 0) { + res.comments = []; + for (const currentComment of comments) { + const comment = await externalService.createComment({ + incidentId: res.id, + comment: currentComment, + createdDate, + }); + + res.comments = [ + ...(res.comments ?? []), + { + commentId: comment.commentId, + pushedDate: comment.pushedDate, + }, + ]; + } + } + + return res; +}; + +export const api: ExternalServiceApi = { + pushToService: pushToServiceHandler, +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts new file mode 100644 index 00000000000000..c2974ec28486ce --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { getBodyForEventAction } from './helpers'; +import { mappings } from './mocks'; + +describe('Create Record Mapping', () => { + const appId = '45678'; + + test('it maps successfully', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + + const data = getBodyForEventAction(appId, mappings, params); + expect(data.applicationId).toEqual(appId); + expect(data.id).not.toBeDefined(); + expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toEqual(params.alertId); + expect(data.values?.[mappings.ruleNameConfig.id]).toEqual(params.ruleName); + expect(data.values?.[mappings.caseNameConfig?.id ?? 0]).toEqual(params.caseName); + expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toEqual(params.caseId); + expect(data.values?.[mappings?.severityConfig?.id ?? 0]).toEqual(params.severity); + expect(data.values?.[mappings?.descriptionConfig?.id ?? 0]).toEqual(params.description); + }); + + test('it contains the id if defined', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + const data = getBodyForEventAction(appId, mappings, params, '123'); + expect(data.id).toEqual('123'); + }); + + test('it does not includes null mappings', () => { + const params = { + alertId: 'al123', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'case desc', + externalId: null, + }; + + // @ts-expect-error + const data = getBodyForEventAction(appId, { ...mappings, test: null }, params); + expect(data.values?.test).not.toBeDefined(); + }); + + test('it converts a numeric values correctly', () => { + const params = { + alertId: 'thisIsNotANumber', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: '123', + description: 'case desc', + externalId: null, + }; + + const data = getBodyForEventAction( + appId, + { + ...mappings, + caseIdConfig: { ...mappings.caseIdConfig, fieldType: 'numeric' }, + alertIdConfig: { ...mappings.alertIdConfig, fieldType: 'numeric' }, + }, + params + ); + + expect(data.values?.[mappings.alertIdConfig?.id ?? 0]).toBe(0); + expect(data.values?.[mappings.caseIdConfig?.id ?? 0]).toBe(123); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts new file mode 100644 index 00000000000000..13b2df1c97f16c --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/helpers.ts @@ -0,0 +1,58 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { CreateRecordParams, Incident, SwimlaneRecordPayload, MappingConfigType } from './types'; + +type ConfigMapping = Omit; + +const mappingKeysToIncidentKeys: Record = { + ruleNameConfig: 'ruleName', + alertIdConfig: 'alertId', + caseIdConfig: 'caseId', + caseNameConfig: 'caseName', + severityConfig: 'severity', + descriptionConfig: 'description', +}; + +export const getBodyForEventAction = ( + applicationId: string, + mappingConfig: MappingConfigType, + params: CreateRecordParams['incident'], + incidentId?: string +): SwimlaneRecordPayload => { + const data: SwimlaneRecordPayload = { + applicationId, + ...(incidentId ? { id: incidentId } : {}), + values: {}, + }; + + return (Object.keys(mappingConfig) as Array).reduce((acc, key) => { + const fieldMap = mappingConfig[key]; + + if (!fieldMap) { + return acc; + } + + const { id, fieldType } = fieldMap; + const paramName = mappingKeysToIncidentKeys[key]; + const value = params[paramName]; + + if (value) { + switch (fieldType) { + case 'numeric': { + const number = Number(value); + return { ...acc, values: { ...acc.values, [id]: isNaN(number) ? 0 : number } }; + } + default: { + return { ...acc, values: { ...acc.values, [id]: value } }; + } + } + } + + return acc; + }, data); +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts new file mode 100644 index 00000000000000..de5010436b6b3d --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/index.ts @@ -0,0 +1,116 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { curry } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { schema } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { ActionType, ActionTypeExecutorOptions, ActionTypeExecutorResult } from '../../types'; +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { + SwimlaneExecutorResultData, + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams, + ExecutorSubActionPushParams, +} from './types'; +import { validate } from './validators'; +import { + ExecutorParamsSchema, + SwimlaneSecretsConfiguration, + SwimlaneServiceConfiguration, +} from './schema'; +import { createExternalService } from './service'; +import { api } from './api'; + +interface GetActionTypeParams { + logger: Logger; + configurationUtilities: ActionsConfigurationUtilities; +} + +const supportedSubActions: string[] = ['pushToService']; + +// action type definition +export function getActionType( + params: GetActionTypeParams +): ActionType< + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams, + SwimlaneExecutorResultData | {} +> { + const { logger, configurationUtilities } = params; + + return { + id: '.swimlane', + minimumLicenseRequired: 'gold', + name: i18n.translate('xpack.actions.builtin.swimlaneTitle', { + defaultMessage: 'Swimlane', + }), + validate: { + config: schema.object(SwimlaneServiceConfiguration, { + validate: curry(validate.config)(configurationUtilities), + }), + secrets: schema.object(SwimlaneSecretsConfiguration, { + validate: curry(validate.secrets)(configurationUtilities), + }), + params: ExecutorParamsSchema, + }, + executor: curry(executor)({ logger, configurationUtilities }), + }; +} + +async function executor( + { + logger, + configurationUtilities, + }: { logger: Logger; configurationUtilities: ActionsConfigurationUtilities }, + execOptions: ActionTypeExecutorOptions< + SwimlanePublicConfigurationType, + SwimlaneSecretConfigurationType, + ExecutorParams + > +): Promise> { + const { actionId, config, params, secrets } = execOptions; + const { subAction, subActionParams } = params as ExecutorParams; + let data: SwimlaneExecutorResultData | null = null; + + const externalService = createExternalService( + { + config, + secrets, + }, + logger, + configurationUtilities + ); + + if (!api[subAction]) { + const errorMessage = `[Action][ExternalService] -> [Swimlane] Unsupported subAction type ${subAction}.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (!supportedSubActions.includes(subAction)) { + const errorMessage = `[Action][ExternalService] -> [Swimlane] subAction ${subAction} not implemented.`; + logger.error(errorMessage); + throw new Error(errorMessage); + } + + if (subAction === 'pushToService') { + const pushToServiceParams = subActionParams as ExecutorSubActionPushParams; + + data = await api.pushToService({ + externalService, + params: pushToServiceParams, + logger, + }); + + logger.debug(`response push to service for incident id: ${data.id}`); + } + + return { status: 'ok', data: data ?? {}, actionId }; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts new file mode 100644 index 00000000000000..f9931049d81c2b --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/mocks.ts @@ -0,0 +1,124 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { ExecutorSubActionPushParams, ExternalService, PushToServiceApiParams } from './types'; + +export const applicationFields = [ + { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'notes', + }, + { + id: 'a6fde', + name: 'Description', + key: 'description', + fieldType: 'text', + }, + { + id: 'dfnkls', + name: 'Alert ID', + key: 'alert-id', + fieldType: 'text', + }, +]; + +export const mappings = { + severityConfig: applicationFields[0], + ruleNameConfig: applicationFields[1], + caseIdConfig: applicationFields[2], + caseNameConfig: applicationFields[3], + commentsConfig: applicationFields[4], + descriptionConfig: applicationFields[5], + alertIdConfig: applicationFields[6], +}; + +export const getApplicationResponse = { fields: applicationFields }; + +export const recordResponseCreate = { + id: '123456', + title: 'neato', + url: 'swimlane.com', + pushedDate: '2021-06-01T17:29:51.092Z', +}; + +export const recordResponseUpdate = { + id: '98765', + title: 'not neato', + url: 'laneswim.com', + pushedDate: '2021-06-01T17:29:51.092Z', +}; + +export const commentResponse = { + commentId: '123456', + pushedDate: '2021-06-01T17:29:51.092Z', +}; + +const createMock = (): jest.Mocked => { + return { + createComment: jest.fn().mockImplementation(() => Promise.resolve(commentResponse)), + createRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseCreate)), + updateRecord: jest.fn().mockImplementation(() => Promise.resolve(recordResponseUpdate)), + }; +}; + +const externalServiceMock = { + create: createMock, +}; + +const executorParams: ExecutorSubActionPushParams = { + incident: { + ruleName: 'rule name', + alertId: '123456', + caseName: 'case name', + severity: 'critical', + caseId: '123456', + description: 'case desc', + externalId: 'incident-3', + }, + comments: [ + { + commentId: 'case-comment-1', + comment: 'A comment', + }, + { + commentId: 'case-comment-2', + comment: 'Another comment', + }, + ], +}; + +const apiParams: PushToServiceApiParams = { + ...executorParams, +}; + +export { externalServiceMock, executorParams, apiParams }; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts new file mode 100644 index 00000000000000..7f4bdc8ca6c0d7 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/schema.ts @@ -0,0 +1,75 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { schema } from '@kbn/config-schema'; + +export const ConfigMap = { + id: schema.string(), + key: schema.string(), + name: schema.string(), + fieldType: schema.string(), +}; + +export const ConfigMapSchema = schema.object(ConfigMap); + +export const ConfigMapping = { + ruleNameConfig: schema.nullable(ConfigMapSchema), + alertIdConfig: schema.nullable(ConfigMapSchema), + caseIdConfig: schema.nullable(ConfigMapSchema), + caseNameConfig: schema.nullable(ConfigMapSchema), + commentsConfig: schema.nullable(ConfigMapSchema), + severityConfig: schema.nullable(ConfigMapSchema), + descriptionConfig: schema.nullable(ConfigMapSchema), +}; + +export const ConfigMappingSchema = schema.object(ConfigMapping); + +export const SwimlaneServiceConfiguration = { + apiUrl: schema.string(), + appId: schema.string(), + connectorType: schema.string(), + mappings: ConfigMappingSchema, +}; + +export const SwimlaneServiceConfigurationSchema = schema.object(SwimlaneServiceConfiguration); + +export const SwimlaneSecretsConfiguration = { + apiToken: schema.string(), +}; + +export const SwimlaneSecretsConfigurationSchema = schema.object(SwimlaneSecretsConfiguration); + +const SwimlaneFields = { + alertId: schema.nullable(schema.string()), + ruleName: schema.nullable(schema.string()), + caseId: schema.nullable(schema.string()), + caseName: schema.nullable(schema.string()), + severity: schema.nullable(schema.string()), + description: schema.nullable(schema.string()), +}; + +export const ExecutorSubActionPushParamsSchema = schema.object({ + incident: schema.object({ + ...SwimlaneFields, + externalId: schema.nullable(schema.string()), + }), + comments: schema.nullable( + schema.arrayOf( + schema.object({ + comment: schema.string(), + commentId: schema.string(), + }) + ) + ), +}); + +export const ExecutorParamsSchema = schema.oneOf([ + schema.object({ + subAction: schema.literal('pushToService'), + subActionParams: ExecutorSubActionPushParamsSchema, + }), +]); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts new file mode 100644 index 00000000000000..77f4686f8acd04 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.test.ts @@ -0,0 +1,434 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 axios from 'axios'; + +import { loggingSystemMock } from '../../../../../../src/core/server/mocks'; +import { Logger } from '../../../../../../src/core/server'; +import { actionsConfigMock } from '../../actions_config.mock'; +import * as utils from '../lib/axios_utils'; +import { createExternalService } from './service'; +import { mappings } from './mocks'; +import { ExternalService } from './types'; + +const logger = loggingSystemMock.create().get() as jest.Mocked; + +jest.mock('axios'); +jest.mock('../lib/axios_utils', () => { + const originalUtils = jest.requireActual('../lib/axios_utils'); + return { + ...originalUtils, + request: jest.fn(), + }; +}); + +axios.create = jest.fn(() => axios); +const requestMock = utils.request as jest.Mock; +const configurationUtilities = actionsConfigMock.create(); + +describe('Swimlane Service', () => { + let service: ExternalService; + const config = { + apiUrl: 'https://test.swimlane.com/', + appId: 'bcq16kdTbz5jlwM6h', + connectorType: 'all', + mappings, + }; + const apiToken = 'token'; + + const headers = { + 'Content-Type': 'application/json', + 'Private-Token': apiToken, + }; + + const incident = { + ruleName: 'Rule Name', + caseId: 'Case Id', + caseName: 'Case Name', + severity: 'Severity', + externalId: null, + description: 'Description', + alertId: 'Alert Id', + }; + + const url = config.apiUrl.slice(0, -1); + + beforeAll(() => { + service = createExternalService( + { + // The trailing slash at the end of the url is intended. + // All API calls need to have the trailing slash removed. + config, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ); + }); + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('createExternalService', () => { + test('throws without url', () => { + expect(() => + createExternalService( + { + config: { + // @ts-ignore + apiUrl: null, + appId: '99999', + mappings, + }, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without app id', () => { + expect(() => + createExternalService( + { + config: { + apiUrl: 'test.com', + // @ts-ignore + appId: null, + }, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without mappings', () => { + expect(() => + createExternalService( + { + config: { + apiUrl: 'test.com', + appId: '987987', + // @ts-ignore + mappings: null, + }, + secrets: { apiToken }, + }, + logger, + configurationUtilities + ) + ).toThrow(); + }); + + test('throws without api token', () => { + expect(() => { + return createExternalService( + { + config: { apiUrl: 'test.com', appId: '78978', mappings, connectorType: 'all' }, + secrets: { + // @ts-ignore + apiToken: null, + }, + }, + logger, + configurationUtilities + ); + }).toThrow(); + }); + }); + + describe('createRecord', () => { + const data = { + id: '123', + name: 'title', + createdDate: '2021-06-01T17:29:51.092Z', + }; + + test('it creates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.createRecord({ + incident, + }); + + expect(res).toEqual({ + id: '123', + title: 'title', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${url}/record/${config.appId}/123`, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + await service.createRecord({ + incident, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + applicationId: config.appId, + values: { + [mappings.ruleNameConfig.id]: 'Rule Name', + [mappings.caseNameConfig.id]: 'Case Name', + [mappings.caseIdConfig.id]: 'Case Id', + [mappings.severityConfig.id]: 'Severity', + [mappings.descriptionConfig.id]: 'Description', + [mappings.alertIdConfig.id]: 'Alert Id', + }, + }, + url: `${url}/api/app/${config.appId}/record`, + method: 'post', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.createRecord({ incident })).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + }); + + describe('updateRecord', () => { + const data = { + id: '123', + name: 'title', + modifiedDate: '2021-06-01T17:29:51.092Z', + }; + const incidentId = '123'; + + test('it updates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.updateRecord({ + incident, + incidentId, + }); + + expect(res).toEqual({ + id: '123', + title: 'title', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${url}/record/${config.appId}/123`, + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + await service.updateRecord({ + incident, + incidentId, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + applicationId: config.appId, + id: incidentId, + values: { + [mappings.ruleNameConfig.id]: 'Rule Name', + [mappings.caseNameConfig.id]: 'Case Name', + [mappings.caseIdConfig.id]: 'Case Id', + [mappings.severityConfig.id]: 'Severity', + [mappings.descriptionConfig.id]: 'Description', + [mappings.alertIdConfig.id]: 'Alert Id', + }, + }, + url: `${url}/api/app/${config.appId}/record/${incidentId}`, + method: 'patch', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.updateRecord({ incident, incidentId })).rejects.toThrow( + `[Action][Swimlane]: Unable to update record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + }); + + describe('createComment', () => { + const data = { + id: '123', + name: 'title', + modifiedDate: '2021-06-01T17:29:51.092Z', + }; + const incidentId = '123'; + const comment = { commentId: '456', comment: 'A comment' }; + const createdDate = '2021-06-01T17:29:51.092Z'; + + test('it updates a record correctly', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + const res = await service.createComment({ + comment, + incidentId, + createdDate, + }); + + expect(res).toEqual({ + commentId: '456', + pushedDate: '2021-06-01T17:29:51.092Z', + }); + }); + + test('it should call request with correct arguments', async () => { + requestMock.mockImplementation(() => ({ + data, + })); + + await service.createComment({ + comment, + incidentId, + createdDate, + }); + + expect(requestMock).toHaveBeenCalledWith({ + axios, + logger, + headers, + data: { + createdDate, + fieldId: mappings.commentsConfig.id, + isRichText: true, + message: comment.comment, + }, + url: `${url}/api/app/${config.appId}/record/${incidentId}/${mappings.commentsConfig.id}/comment`, + method: 'post', + configurationUtilities, + }); + }); + + test('it should throw an error', async () => { + requestMock.mockImplementation(() => { + throw new Error('An error has occurred'); + }); + + await expect(service.createComment({ comment, incidentId, createdDate })).rejects.toThrow( + `[Action][Swimlane]: Unable to create comment in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + }); + + describe('error messages', () => { + const errorResponse = { ErrorCode: '1', Argument: 'Invalid field' }; + + test('it contains the response error', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: errorResponse }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: Invalid field (1)` + ); + }); + + test('it shows an empty string for reason if the ErrorCode is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: { ErrorCode: '1' } }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + + test('it shows an empty string for reason if the Argument is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: { Argument: 'Invalid field' } }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + + test('it shows an empty string for reason if data is undefined', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = {}; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 500. Error: An error has occurred. Reason: unknown` + ); + }); + + test('it shows the status code', async () => { + requestMock.mockImplementation(() => { + const error = new Error('An error has occurred'); + // @ts-ignore + error.response = { data: errorResponse, status: 400 }; + throw error; + }); + + await expect( + service.createRecord({ + incident, + }) + ).rejects.toThrow( + `[Action][Swimlane]: Unable to create record in application with id ${config.appId}. Status: 400. Error: An error has occurred. Reason: Invalid field (1)` + ); + }); + }); +}); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts new file mode 100644 index 00000000000000..f68d22121dbcc9 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/service.ts @@ -0,0 +1,196 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { Logger } from '@kbn/logging'; +import axios from 'axios'; + +import { ActionsConfigurationUtilities } from '../../actions_config'; +import { getErrorMessage, request } from '../lib/axios_utils'; +import { getBodyForEventAction } from './helpers'; +import { + CreateCommentParams, + CreateRecordParams, + ExternalService, + ExternalServiceCredentials, + ExternalServiceIncidentResponse, + MappingConfigType, + ResponseError, + SwimlanePublicConfigurationType, + SwimlaneRecordPayload, + SwimlaneSecretConfigurationType, + UpdateRecordParams, +} from './types'; +import * as i18n from './translations'; + +const createErrorMessage = (errorResponse: ResponseError | null | undefined): string => { + if (errorResponse == null) { + return 'unknown'; + } + + const { ErrorCode, Argument } = errorResponse; + return Argument != null && ErrorCode != null ? `${Argument} (${ErrorCode})` : 'unknown'; +}; + +export const createExternalService = ( + { config, secrets }: ExternalServiceCredentials, + logger: Logger, + configurationUtilities: ActionsConfigurationUtilities +): ExternalService => { + const { apiUrl: url, appId, mappings } = config as SwimlanePublicConfigurationType; + const { apiToken } = secrets as SwimlaneSecretConfigurationType; + + const axiosInstance = axios.create(); + + if (!url || !appId || !apiToken || !mappings) { + throw Error(`[Action]${i18n.NAME}: Wrong configuration.`); + } + + const headers: Record = { + 'Content-Type': 'application/json', + 'Private-Token': `${secrets.apiToken}`, + }; + + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const apiUrl = urlWithoutTrailingSlash.endsWith('api') + ? urlWithoutTrailingSlash + : urlWithoutTrailingSlash + '/api'; + + const getPostRecordUrl = (id: string) => `${apiUrl}/app/${id}/record`; + + const getPostRecordIdUrl = (id: string, recordId: string) => + `${getPostRecordUrl(id)}/${recordId}`; + + const getRecordIdUrl = (id: string, recordId: string) => + `${urlWithoutTrailingSlash}/record/${id}/${recordId}`; + + const getPostCommentUrl = (id: string, recordId: string, commentFieldId: string) => + `${getPostRecordIdUrl(id, recordId)}/${commentFieldId}/comment`; + + const getCommentFieldId = (fieldMappings: MappingConfigType): string | null => + fieldMappings.commentsConfig?.id || null; + + const createRecord = async ( + params: CreateRecordParams + ): Promise => { + try { + const mappingConfig = mappings as MappingConfigType; + const data = getBodyForEventAction(appId, mappingConfig, params.incident); + + const res = await request({ + axios: axiosInstance, + configurationUtilities, + data, + headers, + logger, + method: 'post', + url: getPostRecordUrl(appId), + }); + return { + id: res.data.id, + title: res.data.name, + url: getRecordIdUrl(appId, res.data.id), + pushedDate: new Date(res.data.createdDate).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create record in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + } + }; + + const updateRecord = async ( + params: UpdateRecordParams + ): Promise => { + try { + const mappingConfig = mappings as MappingConfigType; + const data = getBodyForEventAction(appId, mappingConfig, params.incident, params.incidentId); + + const res = await request({ + axios: axiosInstance, + configurationUtilities, + data, + headers, + logger, + method: 'patch', + url: getPostRecordIdUrl(appId, params.incidentId), + }); + + return { + id: res.data.id, + title: res.data.name, + url: getRecordIdUrl(appId, params.incidentId), + pushedDate: new Date(res.data.modifiedDate).toISOString(), + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to update record in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + } + }; + + const createComment = async ({ incidentId, comment, createdDate }: CreateCommentParams) => { + try { + const mappingConfig = mappings as MappingConfigType; + const fieldId = getCommentFieldId(mappingConfig); + + if (fieldId == null) { + throw new Error(`No comment field mapped in ${i18n.NAME} connector`); + } + + const data = { + createdDate, + fieldId, + isRichText: true, + message: comment.comment, + }; + + await request({ + axios: axiosInstance, + configurationUtilities, + data, + headers, + logger, + method: 'post', + url: getPostCommentUrl(appId, incidentId, fieldId), + }); + + /** + * Swimlane response does not contain any data. + * We cannot get an externalCommentId + */ + return { + commentId: comment.commentId, + pushedDate: createdDate, + }; + } catch (error) { + throw new Error( + getErrorMessage( + i18n.NAME, + `Unable to create comment in application with id ${appId}. Status: ${ + error.response?.status ?? 500 + }. Error: ${error.message}. Reason: ${createErrorMessage(error.response?.data)}` + ) + ); + } + }; + + return { + createComment, + createRecord, + updateRecord, + }; +}; diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts new file mode 100644 index 00000000000000..671cf224448f66 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/translations.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { i18n } from '@kbn/i18n'; + +export const NAME = i18n.translate('xpack.actions.builtin.case.swimlaneTitle', { + defaultMessage: 'Swimlane', +}); + +export const ALLOWED_HOSTS_ERROR = (message: string) => + i18n.translate('xpack.actions.builtin.swimlane.configuration.apiAllowedHostsError', { + defaultMessage: 'error configuring connector action: {message}', + values: { + message, + }, + }); diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts new file mode 100644 index 00000000000000..5cb3b109896215 --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/types.ts @@ -0,0 +1,123 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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. + */ + +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { TypeOf } from '@kbn/config-schema'; +import { Logger } from '@kbn/logging'; +import { + ConfigMappingSchema, + ExecutorParamsSchema, + ExecutorSubActionPushParamsSchema, + SwimlaneSecretsConfigurationSchema, + SwimlaneServiceConfigurationSchema, +} from './schema'; +import { ActionsConfigurationUtilities } from '../../actions_config'; + +export type SwimlanePublicConfigurationType = TypeOf; +export type SwimlaneSecretConfigurationType = TypeOf; + +export type MappingConfigType = TypeOf; +export type ExecutorParams = TypeOf; +export type ExecutorSubActionPushParams = TypeOf; + +export interface ExternalServiceCredentials { + config: SwimlanePublicConfigurationType; + secrets: SwimlaneSecretConfigurationType; +} + +export interface ExternalServiceValidation { + config: (configurationUtilities: ActionsConfigurationUtilities, configObject: any) => void; + secrets: (configurationUtilities: ActionsConfigurationUtilities, secrets: any) => void; +} + +export interface CreateRecordParams { + incident: Incident; +} +export interface UpdateRecordParams extends CreateRecordParams { + incidentId: string; +} + +export type PushToServiceApiParams = ExecutorSubActionPushParams; +export interface PushToServiceApiHandlerArgs extends ExternalServiceApiHandlerArgs { + params: PushToServiceApiParams; + logger: Logger; +} + +export interface ExternalServiceIncidentResponse { + id: string; + title: string; + url: string; + pushedDate: string; +} +export interface ExternalServiceCommentResponse { + commentId: string; + pushedDate: string; + externalCommentId?: string; +} + +export interface FieldConfig { + id: string; + name: string; + key: string; + fieldType: string; +} + +export interface SwimlaneRecordPayload { + applicationId: string; + values: SwimlaneDataValues; + id?: string; +} + +export interface ExternalService { + createComment: (params: CreateCommentParams) => Promise; + createRecord: (params: CreateRecordParams) => Promise; + updateRecord: (params: UpdateRecordParams) => Promise; +} + +export type Incident = Omit; + +export interface ExternalServiceApiHandlerArgs { + externalService: ExternalService; +} + +export interface GetApplicationHandlerArgs { + externalService: ExternalService; +} + +export interface PushToServiceResponse extends ExternalServiceIncidentResponse { + comments?: ExternalServiceCommentResponse[]; +} + +export interface ExternalServiceApi { + pushToService: (args: PushToServiceApiHandlerArgs) => Promise; +} + +export type SwimlaneExecutorResultData = ExternalServiceIncidentResponse; +export type SwimlaneDataValues = Record; +export interface SwimlaneComment { + fieldId: string; + message: string | number; + createdDate: string; + isRichText: boolean; +} +export type SwimlaneDataComments = Record; + +export interface SimpleComment { + comment: SwimlaneComment['message']; + commentId: string; +} + +export interface CreateCommentParams { + incidentId: string; + comment: SimpleComment; + createdDate: string; +} + +export interface ResponseError { + ErrorCode: number; + Argument: string; +} diff --git a/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts new file mode 100644 index 00000000000000..1972cd7e6af0bd --- /dev/null +++ b/x-pack/plugins/actions/server/builtin_action_types/swimlane/validators.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { ActionsConfigurationUtilities } from '../../actions_config'; +import { ExternalServiceValidation, SwimlanePublicConfigurationType } from './types'; +import * as i18n from './translations'; + +export const validateCommonConfig = ( + configurationUtilities: ActionsConfigurationUtilities, + configObject: SwimlanePublicConfigurationType +) => { + try { + configurationUtilities.ensureUriAllowed(configObject.apiUrl); + } catch (allowedListError) { + return i18n.ALLOWED_HOSTS_ERROR(allowedListError.message); + } +}; + +export const validateCommonSecrets = () => {}; + +export const validate: ExternalServiceValidation = { + config: validateCommonConfig, + secrets: validateCommonSecrets, +}; diff --git a/x-pack/plugins/actions/server/index.ts b/x-pack/plugins/actions/server/index.ts index bcfc91d673bcc4..230ed826cb1083 100644 --- a/x-pack/plugins/actions/server/index.ts +++ b/x-pack/plugins/actions/server/index.ts @@ -47,7 +47,6 @@ export type { TeamsActionTypeId, TeamsActionParams, } from './builtin_action_types'; - export type { PluginSetupContract, PluginStartContract } from './plugin'; export { asSavedObjectExecutionSource, asHttpRequestExecutionSource } from './lib'; diff --git a/x-pack/plugins/actions/server/types.ts b/x-pack/plugins/actions/server/types.ts index a191728a204892..7c05d16923b9d2 100644 --- a/x-pack/plugins/actions/server/types.ts +++ b/x-pack/plugins/actions/server/types.ts @@ -22,7 +22,7 @@ export { ActionTypeExecutorResult } from '../common'; export { GetFieldsByIssueTypeResponse as JiraGetFieldsResponse } from './builtin_action_types/jira/types'; export { GetCommonFieldsResponse as ServiceNowGetFieldsResponse } from './builtin_action_types/servicenow/types'; export { GetCommonFieldsResponse as ResilientGetFieldsResponse } from './builtin_action_types/resilient/types'; - +export { SwimlanePublicConfigurationType } from './builtin_action_types/swimlane/types'; export type WithoutQueryAndParams = Pick>; export type GetServicesFunction = (request: KibanaRequest) => Services; export type ActionTypeRegistryContract = PublicMethodsOf; diff --git a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts index 06248e1fa95a88..80e0c19092c781 100644 --- a/x-pack/plugins/actions/server/usage/actions_usage_collector.ts +++ b/x-pack/plugins/actions/server/usage/actions_usage_collector.ts @@ -18,6 +18,7 @@ const byTypeSchema: MakeSchemaFrom['count_by_type'] = { __email: { type: 'long' }, __index: { type: 'long' }, __pagerduty: { type: 'long' }, + __swimlane: { type: 'long' }, '__server-log': { type: 'long' }, __slack: { type: 'long' }, __webhook: { type: 'long' }, diff --git a/x-pack/plugins/cases/README.md b/x-pack/plugins/cases/README.md index a1660911567da3..cfff8c79ee2d47 100644 --- a/x-pack/plugins/cases/README.md +++ b/x-pack/plugins/cases/README.md @@ -215,7 +215,7 @@ This action type has no `secrets` properties. | -------- | ------------------------------------------------------------------------------------------------- | ----------------- | | id | ID of the connector used for pushing case updates to external systems. | string | | name | The connector name. | string | -| type | The type of the connector. Must be one of these: `.servicenow`, `jira`, `.resilient`, and `.none` | string | +| type | The type of the connector. Must be one of these: `.servicenow`, `.servicenow-sir`, `.swimlane`, `jira`, `.resilient`, and `.none` | string | | fields | Object containing the connector’s fields. | [fields](#fields) | #### `fields` diff --git a/x-pack/plugins/cases/common/api/connectors/index.ts b/x-pack/plugins/cases/common/api/connectors/index.ts index 2a81396025d9af..cee432b17933b9 100644 --- a/x-pack/plugins/cases/common/api/connectors/index.ts +++ b/x-pack/plugins/cases/common/api/connectors/index.ts @@ -12,12 +12,14 @@ import { JiraFieldsRT } from './jira'; import { ResilientFieldsRT } from './resilient'; import { ServiceNowITSMFieldsRT } from './servicenow_itsm'; import { ServiceNowSIRFieldsRT } from './servicenow_sir'; +import { SwimlaneFieldsRT } from './swimlane'; export * from './jira'; export * from './servicenow_itsm'; export * from './servicenow_sir'; export * from './resilient'; export * from './mappings'; +export * from './swimlane'; export type ActionConnector = ActionResult; export type ActionTypeConnector = ActionType; @@ -32,10 +34,11 @@ export const ConnectorFieldsRt = rt.union([ export enum ConnectorTypes { jira = '.jira', + none = '.none', resilient = '.resilient', serviceNowITSM = '.servicenow', serviceNowSIR = '.servicenow-sir', - none = '.none', + swimlane = '.swimlane', } export const connectorTypes = Object.values(ConnectorTypes); @@ -55,6 +58,11 @@ const ConnectorServiceNowITSMTypeFieldsRt = rt.type({ fields: rt.union([ServiceNowITSMFieldsRT, rt.null]), }); +const ConnectorSwimlaneTypeFieldsRt = rt.type({ + type: rt.literal(ConnectorTypes.swimlane), + fields: rt.union([SwimlaneFieldsRT, rt.null]), +}); + const ConnectorServiceNowSIRTypeFieldsRt = rt.type({ type: rt.literal(ConnectorTypes.serviceNowSIR), fields: rt.union([ServiceNowSIRFieldsRT, rt.null]), @@ -67,10 +75,11 @@ const ConnectorNoneTypeFieldsRt = rt.type({ export const ConnectorTypeFieldsRt = rt.union([ ConnectorJiraTypeFieldsRt, + ConnectorNoneTypeFieldsRt, ConnectorResillientTypeFieldsRt, ConnectorServiceNowITSMTypeFieldsRt, ConnectorServiceNowSIRTypeFieldsRt, - ConnectorNoneTypeFieldsRt, + ConnectorSwimlaneTypeFieldsRt, ]); export const CaseConnectorRt = rt.intersection([ @@ -85,6 +94,7 @@ export type CaseConnector = rt.TypeOf; export type ConnectorTypeFields = rt.TypeOf; export type ConnectorJiraTypeFields = rt.TypeOf; export type ConnectorResillientTypeFields = rt.TypeOf; +export type ConnectorSwimlaneTypeFields = rt.TypeOf; export type ConnectorServiceNowITSMTypeFields = rt.TypeOf< typeof ConnectorServiceNowITSMTypeFieldsRt >; diff --git a/x-pack/plugins/cases/common/api/connectors/mappings.ts b/x-pack/plugins/cases/common/api/connectors/mappings.ts index e0fdd2d7e62dc4..8737a6c5a64628 100644 --- a/x-pack/plugins/cases/common/api/connectors/mappings.ts +++ b/x-pack/plugins/cases/common/api/connectors/mappings.ts @@ -48,9 +48,6 @@ const ConnectorFieldRt = rt.type({ export type ConnectorField = rt.TypeOf; -const GetFieldsResponseRt = rt.type({ - defaultMappings: rt.array(ConnectorMappingsAttributesRT), - fields: rt.array(ConnectorFieldRt), -}); +const GetDefaultMappingsResponseRt = rt.array(ConnectorMappingsAttributesRT); -export type GetFieldsResponse = rt.TypeOf; +export type GetDefaultMappingsResponse = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/api/connectors/swimlane.ts b/x-pack/plugins/cases/common/api/connectors/swimlane.ts new file mode 100644 index 00000000000000..bc4d9df9ae6a0f --- /dev/null +++ b/x-pack/plugins/cases/common/api/connectors/swimlane.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 * as rt from 'io-ts'; + +// New fields should also be added at: x-pack/plugins/cases/server/connectors/case/schema.ts +export const SwimlaneFieldsRT = rt.type({ + caseId: rt.union([rt.string, rt.null]), +}); + +export enum SwimlaneConnectorType { + All = 'all', + Alerts = 'alerts', + Cases = 'cases', +} + +export type SwimlaneFieldsType = rt.TypeOf; diff --git a/x-pack/plugins/cases/common/constants.ts b/x-pack/plugins/cases/common/constants.ts index 317fe1d8ed144a..5d7ee47bb8ea0e 100644 --- a/x-pack/plugins/cases/common/constants.ts +++ b/x-pack/plugins/cases/common/constants.ts @@ -4,6 +4,8 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import { ConnectorTypes } from './api'; + export const DEFAULT_DATE_FORMAT = 'dateFormat'; export const DEFAULT_DATE_FORMAT_TZ = 'dateFormat:tz'; @@ -59,16 +61,12 @@ export const CASE_DETAILS_ALERTS_URL = `${CASE_DETAILS_URL}/alerts`; export const ACTION_URL = '/api/actions'; export const ACTION_TYPES_URL = '/api/actions/list_action_types'; -export const SERVICENOW_ITSM_ACTION_TYPE_ID = '.servicenow'; -export const SERVICENOW_SIR_ACTION_TYPE_ID = '.servicenow-sir'; -export const JIRA_ACTION_TYPE_ID = '.jira'; -export const RESILIENT_ACTION_TYPE_ID = '.resilient'; - export const SUPPORTED_CONNECTORS = [ - SERVICENOW_ITSM_ACTION_TYPE_ID, - SERVICENOW_SIR_ACTION_TYPE_ID, - JIRA_ACTION_TYPE_ID, - RESILIENT_ACTION_TYPE_ID, + `${ConnectorTypes.serviceNowITSM}`, + `${ConnectorTypes.serviceNowSIR}`, + `${ConnectorTypes.jira}`, + `${ConnectorTypes.resilient}`, + `${ConnectorTypes.swimlane}`, ]; /** diff --git a/x-pack/plugins/cases/public/common/shared_imports.ts b/x-pack/plugins/cases/public/common/shared_imports.ts index 675204076b02a0..4641fcfa2167cb 100644 --- a/x-pack/plugins/cases/public/common/shared_imports.ts +++ b/x-pack/plugins/cases/public/common/shared_imports.ts @@ -24,6 +24,8 @@ export { ValidationError, ValidationFunc, VALIDATION_TYPES, + FieldConfig, + ValidationConfig, } from '../../../../../src/plugins/es_ui_shared/static/forms/hook_form_lib'; export { Field, diff --git a/x-pack/plugins/cases/public/components/case_view/index.test.tsx b/x-pack/plugins/cases/public/components/case_view/index.test.tsx index 55de4d07b13b92..1fafbac50c2b9a 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.test.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.test.tsx @@ -608,6 +608,7 @@ describe('CaseView ', () => { ).toBe(connectorName); }); }); + it('should update connector', async () => { const wrapper = mount( @@ -628,15 +629,19 @@ describe('CaseView ', () => { wrapper.find('[data-test-subj="connector-edit"] button').simulate('click'); wrapper.find('button[data-test-subj="dropdown-connectors"]').simulate('click'); - wrapper.find('button[data-test-subj="dropdown-connector-resilient-2"]').simulate('click'); - await waitFor(() => wrapper.update()); + await waitFor(() => { + wrapper.update(); + expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeTruthy(); + }); + wrapper.find(`button[data-test-subj="edit-connectors-submit"]`).first().simulate('click'); await waitFor(() => { - const updateObject = updateCaseProperty.mock.calls[0][0]; + wrapper.update(); expect(updateCaseProperty).toHaveBeenCalledTimes(1); + const updateObject = updateCaseProperty.mock.calls[0][0]; expect(updateObject.updateKey).toEqual('connector'); expect(updateObject.updateValue).toEqual({ id: 'resilient-2', diff --git a/x-pack/plugins/cases/public/components/case_view/index.tsx b/x-pack/plugins/cases/public/components/case_view/index.tsx index 05f1c6727b1680..9c6e9442c8f564 100644 --- a/x-pack/plugins/cases/public/components/case_view/index.tsx +++ b/x-pack/plugins/cases/public/components/case_view/index.tsx @@ -31,17 +31,14 @@ import { useGetCaseUserActions } from '../../containers/use_get_case_user_action import { usePushToService } from '../use_push_to_service'; import { EditConnector } from '../edit_connector'; import { useConnectors } from '../../containers/configure/use_connectors'; -import { - getConnectorById, - normalizeActionConnector, - getNoneConnector, -} from '../configure_cases/utils'; +import { normalizeActionConnector, getNoneConnector } from '../configure_cases/utils'; import { StatusActionButton } from '../status/button'; import * as i18n from './translations'; import { CasesTimelineIntegration, CasesTimelineIntegrationProvider } from '../timeline_context'; import { useTimelineContext } from '../timeline_context/use_timeline_context'; import { CasesNavigation } from '../links'; import { OwnerProvider } from '../owner_context'; +import { getConnectorById } from '../utils'; import { DoesNotExist } from './does_not_exist'; const gutterTimeline = '70px'; // seems to be a timeline reference from the original file diff --git a/x-pack/plugins/cases/public/components/configure_cases/index.tsx b/x-pack/plugins/cases/public/components/configure_cases/index.tsx index 3ee4bc77cd237c..ac43ec05319a0b 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/index.tsx +++ b/x-pack/plugins/cases/public/components/configure_cases/index.tsx @@ -24,15 +24,11 @@ import { ActionConnectorTableItem } from '../../../../triggers_actions_ui/public import { SectionWrapper } from '../wrappers'; import { Connectors } from './connectors'; import { ClosureOptions } from './closure_options'; -import { - getConnectorById, - getNoneConnector, - normalizeActionConnector, - normalizeCaseConnector, -} from './utils'; +import { getNoneConnector, normalizeActionConnector, normalizeCaseConnector } from './utils'; import * as i18n from './translations'; import { Owner } from '../../types'; import { OwnerProvider } from '../owner_context'; +import { getConnectorById } from '../utils'; const FormWrapper = styled.div` ${({ theme }) => css` diff --git a/x-pack/plugins/cases/public/components/configure_cases/utils.ts b/x-pack/plugins/cases/public/components/configure_cases/utils.ts index ade1a5e0c2bbab..6597417b5068ab 100644 --- a/x-pack/plugins/cases/public/components/configure_cases/utils.ts +++ b/x-pack/plugins/cases/public/components/configure_cases/utils.ts @@ -10,10 +10,10 @@ import { CaseField, ActionType, ThirdPartyField, - ActionConnector, CaseConnector, CaseConnectorMapping, } from '../../containers/configure/types'; +import { CaseActionConnector } from '../types'; export const setActionTypeToMapping = ( caseField: CaseField, @@ -54,13 +54,8 @@ export const getNoneConnector = (): CaseConnector => ({ fields: null, }); -export const getConnectorById = ( - id: string, - connectors: ActionConnector[] -): ActionConnector | null => connectors.find((c) => c.id === id) ?? null; - export const normalizeActionConnector = ( - actionConnector: ActionConnector, + actionConnector: CaseActionConnector, fields: CaseConnector['fields'] = null ): CaseConnector => { const caseConnectorFieldsType = { @@ -75,6 +70,6 @@ export const normalizeActionConnector = ( }; export const normalizeCaseConnector = ( - connectors: ActionConnector[], + connectors: CaseActionConnector[], caseConnector: CaseConnector -): ActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null; +): CaseActionConnector | null => connectors.find((c) => c.id === caseConnector.id) ?? null; diff --git a/x-pack/plugins/cases/public/components/connector_selector/form.tsx b/x-pack/plugins/cases/public/components/connector_selector/form.tsx index 210334e93adb8e..71a65ae030d9d8 100644 --- a/x-pack/plugins/cases/public/components/connector_selector/form.tsx +++ b/x-pack/plugins/cases/public/components/connector_selector/form.tsx @@ -8,6 +8,7 @@ import React, { useCallback } from 'react'; import { isEmpty } from 'lodash/fp'; import { EuiFormRow } from '@elastic/eui'; +import styled from 'styled-components'; import { FieldHook, getFieldValidityAndErrorMessage } from '../../common/shared_imports'; import { ConnectorsDropdown } from '../configure_cases/connectors_dropdown'; @@ -24,6 +25,13 @@ interface ConnectorSelectorProps { handleChange?: (newValue: string) => void; hideConnectorServiceNowSir?: boolean; } + +const EuiFormRowWrapper = styled(EuiFormRow)` + .euiFormErrorText { + display: none; + } +`; + export const ConnectorSelector = ({ connectors, dataTestSubj, @@ -47,7 +55,7 @@ export const ConnectorSelector = ({ ); return isEdit ? ( - - + ) : null; }; diff --git a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx index d71da6f87689d3..062695fa41cc28 100644 --- a/x-pack/plugins/cases/public/components/connectors/fields_form.tsx +++ b/x-pack/plugins/cases/public/components/connectors/fields_form.tsx @@ -8,7 +8,8 @@ import React, { memo, Suspense } from 'react'; import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner } from '@elastic/eui'; -import { CaseActionConnector, ConnectorFieldsProps } from './types'; +import { CaseActionConnector } from '../types'; +import { ConnectorFieldsProps } from './types'; import { getCaseConnectors } from '.'; import { ConnectorTypeFields } from '../../../common'; diff --git a/x-pack/plugins/cases/public/components/connectors/index.ts b/x-pack/plugins/cases/public/components/connectors/index.ts index ad202365ae9679..3aa10c56dd8e99 100644 --- a/x-pack/plugins/cases/public/components/connectors/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/index.ts @@ -8,6 +8,7 @@ import { CaseConnectorsRegistry } from './types'; import { createCaseConnectorsRegistry } from './connectors_registry'; import { getCaseConnector as getJiraCaseConnector } from './jira'; +import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; import { @@ -15,6 +16,7 @@ import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType, ResilientFieldsType, + SwimlaneFieldsType, } from '../../../common'; export { getActionType as getCaseConnectorUi } from './case'; @@ -40,6 +42,7 @@ class CaseConnectors { getServiceNowITSMCaseConnector() ); this.caseConnectorsRegistry.register(getServiceNowSIRCaseConnector()); + this.caseConnectorsRegistry.register(getSwimlaneCaseConnector()); } registry(): CaseConnectorsRegistry { diff --git a/x-pack/plugins/cases/public/components/connectors/jira/index.ts b/x-pack/plugins/cases/public/components/connectors/jira/index.ts index f987d9823af8e4..d59d20177c14d3 100644 --- a/x-pack/plugins/cases/public/components/connectors/jira/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/jira/index.ts @@ -8,13 +8,13 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { JiraFieldsType } from '../../../../common'; +import { ConnectorTypes, JiraFieldsType } from '../../../../common'; import * as i18n from './translations'; export * from './types'; export const getCaseConnector = (): CaseConnector => ({ - id: '.jira', + id: ConnectorTypes.jira, fieldsComponent: lazy(() => import('./case_fields')), }); export const fieldLabels = { diff --git a/x-pack/plugins/cases/public/components/connectors/mock.ts b/x-pack/plugins/cases/public/components/connectors/mock.ts index f5429fa2396aa6..663b397e6f4fec 100644 --- a/x-pack/plugins/cases/public/components/connectors/mock.ts +++ b/x-pack/plugins/cases/public/components/connectors/mock.ts @@ -5,6 +5,8 @@ * 2.0. */ +import { SwimlaneConnectorType } from '../../../common'; + export const connector = { id: '123', name: 'My connector', @@ -13,6 +15,22 @@ export const connector = { isPreconfigured: false, }; +export const swimlaneConnector = { + id: '123', + name: 'My connector', + actionTypeId: '.swimlane', + config: { + connectorType: SwimlaneConnectorType.Cases, + mappings: { + caseIdConfig: {}, + caseNameConfig: {}, + descriptionConfig: {}, + commentsConfig: {}, + }, + }, + isPreconfigured: false, +}; + export const issues = [ { id: 'personId', title: 'Person Task', key: 'personKey' }, { id: 'womanId', title: 'Woman Task', key: 'womanKey' }, diff --git a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts index 9bf96b16f358cb..8a429c0dea0914 100644 --- a/x-pack/plugins/cases/public/components/connectors/resilient/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/resilient/index.ts @@ -8,13 +8,13 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ResilientFieldsType } from '../../../../common'; +import { ConnectorTypes, ResilientFieldsType } from '../../../../common'; import * as i18n from './translations'; export * from './types'; export const getCaseConnector = (): CaseConnector => ({ - id: '.resilient', + id: ConnectorTypes.resilient, fieldsComponent: lazy(() => import('./case_fields')), }); diff --git a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts index 9df5f87b416e1c..88afd902ccf602 100644 --- a/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts +++ b/x-pack/plugins/cases/public/components/connectors/servicenow/index.ts @@ -8,16 +8,20 @@ import { lazy } from 'react'; import { CaseConnector } from '../types'; -import { ServiceNowITSMFieldsType, ServiceNowSIRFieldsType } from '../../../../common'; +import { + ConnectorTypes, + ServiceNowITSMFieldsType, + ServiceNowSIRFieldsType, +} from '../../../../common'; import * as i18n from './translations'; export const getServiceNowITSMCaseConnector = (): CaseConnector => ({ - id: '.servicenow', + id: ConnectorTypes.serviceNowITSM, fieldsComponent: lazy(() => import('./servicenow_itsm_case_fields')), }); export const getServiceNowSIRCaseConnector = (): CaseConnector => ({ - id: '.servicenow-sir', + id: ConnectorTypes.serviceNowSIR, fieldsComponent: lazy(() => import('./servicenow_sir_case_fields')), }); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx new file mode 100644 index 00000000000000..1a035d92611bdd --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.test.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React from 'react'; +import { render, screen } from '@testing-library/react'; + +import { SwimlaneConnectorType } from '../../../../common'; +import Fields from './case_fields'; +import * as i18n from './translations'; +import { swimlaneConnector as connector } from '../mock'; + +const fields = { + caseId: '123', +}; + +const onChange = jest.fn(); + +describe('Swimlane Cases Fields', () => { + test('it does not shows the mapping error callout', () => { + render(); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeFalsy(); + }); + + test('it shows the mapping error callout when mapping is invalid', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + mappings: {}, + }, + }; + + render(); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy(); + }); + + test('it shows the mapping error callout when the connector is of type alerts', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType: SwimlaneConnectorType.Alerts, + }, + }; + + render(); + expect(screen.queryByText(i18n.EMPTY_MAPPING_WARNING_TITLE)).toBeTruthy(); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx new file mode 100644 index 00000000000000..b6370504edbb61 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/case_fields.tsx @@ -0,0 +1,48 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React, { useMemo } from 'react'; +import { EuiCallOut } from '@elastic/eui'; +import * as i18n from './translations'; + +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; +import { ConnectorFieldsProps } from '../types'; +import { ConnectorCard } from '../card'; +import { connectorValidator } from './validator'; + +const SwimlaneComponent: React.FunctionComponent> = ({ + connector, + isEdit = true, +}) => { + const showMappingWarning = useMemo(() => connectorValidator(connector) != null, [connector]); + + return ( + <> + {!isEdit && ( + + )} + {showMappingWarning && ( + + {i18n.EMPTY_MAPPING_WARNING_DESC} + + )} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneComponent as default }; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts new file mode 100644 index 00000000000000..bd2eaae9e01741 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { lazy } from 'react'; + +import { CaseConnector } from '../types'; +import { ConnectorTypes, SwimlaneFieldsType } from '../../../../common'; +import * as i18n from './translations'; + +export const getCaseConnector = (): CaseConnector => { + return { + id: ConnectorTypes.swimlane, + fieldsComponent: lazy(() => import('./case_fields')), + }; +}; + +export const fieldLabels = { + caseId: i18n.CASE_ID_LABEL, + caseName: i18n.CASE_NAME_LABEL, + severity: i18n.SEVERITY_LABEL, +}; diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts new file mode 100644 index 00000000000000..eb6cd168fab991 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/translations.ts @@ -0,0 +1,42 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { i18n } from '@kbn/i18n'; + +export const ALERT_SOURCE_LABEL = i18n.translate( + 'xpack.cases.connectors.swimlane.alertSourceLabel', + { + defaultMessage: 'Alert Source', + } +); + +export const CASE_ID_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseIdLabel', { + defaultMessage: 'Case Id', +}); + +export const CASE_NAME_LABEL = i18n.translate('xpack.cases.connectors.swimlane.caseNameLabel', { + defaultMessage: 'Case Name', +}); + +export const SEVERITY_LABEL = i18n.translate('xpack.cases.connectors.swimlane.severityLabel', { + defaultMessage: 'Severity', +}); + +export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( + 'xpack.cases.connectors.swimlane.emptyMappingWarningTitle', + { + defaultMessage: 'This connector has missing field mappings', + } +); + +export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( + 'xpack.cases.connectors.swimlane.emptyMappingWarningDesc', + { + defaultMessage: + 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Cases.', + } +); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts new file mode 100644 index 00000000000000..552d988c26330d --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.test.ts @@ -0,0 +1,60 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { SwimlaneConnectorType } from '../../../../common'; +import { swimlaneConnector as connector } from '../mock'; +import { isAnyRequiredFieldNotSet, connectorValidator } from './validator'; + +describe('Swimlane validator', () => { + describe('isAnyRequiredFieldNotSet', () => { + test('it returns true if a required field is not set', () => { + expect(isAnyRequiredFieldNotSet({ notRequired: 'test' })).toBeTruthy(); + }); + + test('it returns false if all required fields are set', () => { + expect(isAnyRequiredFieldNotSet(connector.config.mappings)).toBeFalsy(); + }); + }); + + describe('connectorValidator', () => { + test('it returns an error message if the mapping is not correct', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + mappings: {}, + }, + }; + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' }); + }); + + test('it returns an error message if the connector is of type alerts', () => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType: SwimlaneConnectorType.Alerts, + }, + }; + expect(connectorValidator(invalidConnector)).toEqual({ message: 'Invalid connector' }); + }); + + test.each([SwimlaneConnectorType.Cases, SwimlaneConnectorType.All])( + 'it does not return an error message if the connector is of type %s', + (connectorType) => { + const invalidConnector = { + ...connector, + config: { + ...connector.config, + connectorType, + }, + }; + expect(connectorValidator(invalidConnector)).toBe(undefined); + } + ); + }); +}); diff --git a/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts new file mode 100644 index 00000000000000..4ead75e5854f96 --- /dev/null +++ b/x-pack/plugins/cases/public/components/connectors/swimlane/validator.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { SwimlaneConnectorType } from '../../../../common'; +import { ValidationConfig } from '../../../common/shared_imports'; +import { CaseActionConnector } from '../../types'; + +const casesRequiredFields = [ + 'caseIdConfig', + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', +]; + +export const isAnyRequiredFieldNotSet = (mapping: Record | undefined) => + casesRequiredFields.some((field) => mapping?.[field] == null); + +/** + * The user can use either a connector of type cases or all. + * If the connector is of type all we should check if all + * required field have been configured. + */ + +export const connectorValidator = ( + connector: CaseActionConnector +): ReturnType => { + const { + config: { mappings, connectorType }, + } = connector; + if (connectorType === SwimlaneConnectorType.Alerts || isAnyRequiredFieldNotSet(mappings)) { + return { + message: 'Invalid connector', + }; + } +}; diff --git a/x-pack/plugins/cases/public/components/connectors/types.ts b/x-pack/plugins/cases/public/components/connectors/types.ts index 4eb97513b9f58e..5bbd77c7909012 100644 --- a/x-pack/plugins/cases/public/components/connectors/types.ts +++ b/x-pack/plugins/cases/public/components/connectors/types.ts @@ -11,12 +11,11 @@ import React from 'react'; import { ActionType as ThirdPartySupportedActions, CaseField, - ActionConnector, ConnectorTypeFields, } from '../../../common'; +import { CaseActionConnector } from '../types'; export { ThirdPartyField as AllThirdPartyFields } from '../../../common'; -export type CaseActionConnector = ActionConnector; export interface ThirdPartyField { label: string; diff --git a/x-pack/plugins/cases/public/components/create/connector.test.tsx b/x-pack/plugins/cases/public/components/create/connector.test.tsx index c453838f6cd7a1..bc6d5c8717eced 100644 --- a/x-pack/plugins/cases/public/components/create/connector.test.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.test.tsx @@ -18,6 +18,9 @@ import { useGetSeverity } from '../connectors/resilient/use_get_severity'; import { useGetChoices } from '../connectors/servicenow/use_get_choices'; import { incidentTypes, severity, choices } from '../connectors/mock'; import { schema, FormProps } from './schema'; +import { TestProviders } from '../../common/mock'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { useCaseConfigureResponse } from '../configure_cases/__mock__'; jest.mock('../../common/lib/kibana', () => ({ useKibana: () => ({ @@ -39,10 +42,12 @@ jest.mock('../../common/lib/kibana', () => ({ jest.mock('../connectors/resilient/use_get_incident_types'); jest.mock('../connectors/resilient/use_get_severity'); jest.mock('../connectors/servicenow/use_get_choices'); +jest.mock('../../containers/configure/use_configure'); const useGetIncidentTypesMock = useGetIncidentTypes as jest.Mock; const useGetSeverityMock = useGetSeverity as jest.Mock; const useGetChoicesMock = useGetChoices as jest.Mock; +const useCaseConfigureMock = useCaseConfigure as jest.Mock; const useGetIncidentTypesResponse = { isLoading: false, @@ -87,35 +92,30 @@ describe('Connector', () => { useGetIncidentTypesMock.mockReturnValue(useGetIncidentTypesResponse); useGetSeverityMock.mockReturnValue(useGetSeverityResponse); useGetChoicesMock.mockReturnValue(useGetChoicesResponse); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); it('it renders', async () => { const wrapper = mount( - - - + + + + + ); expect(wrapper.find(`[data-test-subj="caseConnectors"]`).exists()).toBeTruthy(); - expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeTruthy(); - - await waitFor(() => { - expect(wrapper.find(`button[data-test-subj="dropdown-connectors"]`).first().text()).toBe( - 'My Connector' - ); - }); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find(`[data-test-subj="connector-fields-sn-itsm"]`).exists()).toBeTruthy(); - }); + // Selected connector is set to none so no fields should be displayed + expect(wrapper.find(`[data-test-subj="connector-fields"]`).exists()).toBeFalsy(); }); it('it is disabled and loading when isLoadingConnectors=true', async () => { const wrapper = mount( - - - + + + + + ); expect( @@ -129,9 +129,11 @@ describe('Connector', () => { it('it is disabled and loading when isLoading=true', async () => { const wrapper = mount( - - - + + + + + ); expect( @@ -144,9 +146,11 @@ describe('Connector', () => { it(`it should change connector`, async () => { const wrapper = mount( - - - + + + + + ); expect(wrapper.find(`[data-test-subj="connector-fields-resilient"]`).exists()).toBeFalsy(); diff --git a/x-pack/plugins/cases/public/components/create/connector.tsx b/x-pack/plugins/cases/public/components/create/connector.tsx index 2049f2a083a6ff..2ec6d1ffef23d4 100644 --- a/x-pack/plugins/cases/public/components/create/connector.tsx +++ b/x-pack/plugins/cases/public/components/create/connector.tsx @@ -5,15 +5,22 @@ * 2.0. */ -import React, { memo, useCallback } from 'react'; +import React, { memo, useCallback, useMemo, useEffect } from 'react'; import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; -import { ActionConnector, ConnectorTypes } from '../../../common'; -import { UseField, useFormData, FieldHook, useFormContext } from '../../common/shared_imports'; +import { ConnectorTypes, ActionConnector } from '../../../common'; +import { + UseField, + useFormData, + FieldHook, + useFormContext, + FieldConfig, +} from '../../common/shared_imports'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; -import { getConnectorById } from '../configure_cases/utils'; -import { FormProps } from './schema'; +import { FormProps, schema } from './schema'; +import { useCaseConfigure } from '../../containers/configure/use_configure'; +import { getConnectorById, getConnectorsFormValidators } from '../utils'; interface Props { connectors: ActionConnector[]; @@ -26,6 +33,7 @@ interface ConnectorsFieldProps { connectors: ActionConnector[]; field: FieldHook; isEdit: boolean; + setErrors: (errors: boolean) => void; hideConnectorServiceNowSir?: boolean; } @@ -33,11 +41,13 @@ const ConnectorFields = ({ connectors, isEdit, field, + setErrors, hideConnectorServiceNowSir = false, }: ConnectorsFieldProps) => { const [{ connectorId }] = useFormData({ watch: ['connectorId'] }); const { setValue } = field; let connector = getConnectorById(connectorId, connectors) ?? null; + if ( connector && hideConnectorServiceNowSir && @@ -61,18 +71,49 @@ const ConnectorComponent: React.FC = ({ isLoading, isLoadingConnectors, }) => { - const { getFields } = useFormContext(); + const { getFields, setFieldValue } = useFormContext(); + const { connector: configurationConnector } = useCaseConfigure(); + const handleConnectorChange = useCallback(() => { const { fields } = getFields(); fields.setValue(null); }, [getFields]); + const defaultConnectorId = useMemo(() => { + if ( + hideConnectorServiceNowSir && + configurationConnector.type === ConnectorTypes.serviceNowSIR + ) { + return 'none'; + } + return connectors.some((connector) => connector.id === configurationConnector.id) + ? configurationConnector.id + : 'none'; + }, [ + configurationConnector.id, + configurationConnector.type, + connectors, + hideConnectorServiceNowSir, + ]); + + useEffect(() => setFieldValue('connectorId', defaultConnectorId), [ + defaultConnectorId, + setFieldValue, + ]); + + const connectorIdConfig = getConnectorsFormValidators({ + config: schema.connectorId as FieldConfig, + connectors, + }); + return ( { jest.resetAllMocks(); useGetTagsMock.mockReturnValue({ tags: ['test'] }); useConnectorsMock.mockReturnValue({ loading: false, connectors: connectorsMock }); + useCaseConfigureMock.mockImplementation(() => useCaseConfigureResponse); }); it('it renders with steps', async () => { diff --git a/x-pack/plugins/cases/public/components/create/form_context.tsx b/x-pack/plugins/cases/public/components/create/form_context.tsx index 30a60fb5c1e47f..65c102583455a1 100644 --- a/x-pack/plugins/cases/public/components/create/form_context.tsx +++ b/x-pack/plugins/cases/public/components/create/form_context.tsx @@ -5,23 +5,19 @@ * 2.0. */ -import React, { useCallback, useEffect, useMemo } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { schema, FormProps } from './schema'; import { Form, useForm } from '../../common/shared_imports'; -import { - getConnectorById, - getNoneConnector, - normalizeActionConnector, -} from '../configure_cases/utils'; +import { getNoneConnector, normalizeActionConnector } from '../configure_cases/utils'; import { usePostCase } from '../../containers/use_post_case'; import { usePostPushToService } from '../../containers/use_post_push_to_service'; import { useConnectors } from '../../containers/configure/use_connectors'; -import { useCaseConfigure } from '../../containers/configure/use_configure'; import { Case } from '../../containers/types'; -import { CaseType, ConnectorTypes } from '../../../common'; +import { CaseType } from '../../../common'; import { UsePostComment, usePostComment } from '../../containers/use_post_comment'; import { useOwnerContext } from '../owner_context/use_owner_context'; +import { getConnectorById } from '../utils'; const initialCaseValue: FormProps = { description: '', @@ -49,28 +45,10 @@ export const FormContext: React.FC = ({ }) => { const { connectors, loading: isLoadingConnectors } = useConnectors(); const owner = useOwnerContext(); - const { connector: configurationConnector } = useCaseConfigure(); const { postCase } = usePostCase(); const { postComment } = usePostComment(); const { pushCaseToExternalService } = usePostPushToService(); - const connectorId = useMemo(() => { - if ( - hideConnectorServiceNowSir && - configurationConnector.type === ConnectorTypes.serviceNowSIR - ) { - return 'none'; - } - return connectors.some((connector) => connector.id === configurationConnector.id) - ? configurationConnector.id - : 'none'; - }, [ - configurationConnector.id, - configurationConnector.type, - connectors, - hideConnectorServiceNowSir, - ]); - const submitCase = useCallback( async ( { connectorId: dataConnectorId, fields, syncAlerts = true, ...dataWithoutConnectorId }, @@ -125,9 +103,6 @@ export const FormContext: React.FC = ({ schema, onSubmit: submitCase, }); - const { setFieldValue } = form; - // Set the selected connector to the configuration connector - useEffect(() => setFieldValue('connectorId', connectorId), [connectorId, setFieldValue]); const childrenWithExtraProp = useMemo( () => diff --git a/x-pack/plugins/cases/public/components/create/schema.tsx b/x-pack/plugins/cases/public/components/create/schema.tsx index 6e6d1a414280eb..bea1a46d93760c 100644 --- a/x-pack/plugins/cases/public/components/create/schema.tsx +++ b/x-pack/plugins/cases/public/components/create/schema.tsx @@ -49,7 +49,9 @@ export const schema: FormSchema = { label: i18n.CONNECTORS, defaultValue: 'none', }, - fields: {}, + fields: { + defaultValue: null, + }, syncAlerts: { helpText: i18n.SYNC_ALERTS_HELP, type: FIELD_TYPES.TOGGLE, diff --git a/x-pack/plugins/cases/public/components/edit_connector/index.tsx b/x-pack/plugins/cases/public/components/edit_connector/index.tsx index 570f6e34d25287..8057d188b8c047 100644 --- a/x-pack/plugins/cases/public/components/edit_connector/index.tsx +++ b/x-pack/plugins/cases/public/components/edit_connector/index.tsx @@ -20,15 +20,15 @@ import { import styled from 'styled-components'; import { noop } from 'lodash/fp'; -import { Form, UseField, useForm } from '../../common/shared_imports'; +import { FieldConfig, Form, UseField, useForm } from '../../common/shared_imports'; import { ActionConnector, ConnectorTypeFields } from '../../../common'; import { ConnectorSelector } from '../connector_selector/form'; import { ConnectorFieldsForm } from '../connectors/fields_form'; -import { getConnectorById } from '../configure_cases/utils'; import { CaseUserActions } from '../../containers/types'; import { schema } from './schema'; import { getConnectorFieldsFromUserActions } from './helpers'; import * as i18n from './translations'; +import { getConnectorById, getConnectorsFormValidators } from '../utils'; export interface EditConnectorProps { caseFields: ConnectorTypeFields['fields']; @@ -205,6 +205,11 @@ export const EditConnector = React.memo( }); }, [dispatch]); + const connectorIdConfig = getConnectorsFormValidators({ + config: schema.connectorId as FieldConfig, + connectors, + }); + /** * if this evaluates to true it means that the connector was likely deleted because the case connector was set to something * other than none but we don't find it in the list of connectors returned from the actions plugin @@ -243,6 +248,7 @@ export const EditConnector = React.memo( connectors.find((c) => c.id === id) ?? null; + +const validators: Record< + string, + (connector: CaseActionConnector) => ReturnType +> = { + [ConnectorTypes.swimlane]: swimlaneConnectorValidator, +}; + +export const getConnectorsFormValidators = ({ + connectors = [], + config = {}, +}: { + connectors: CaseActionConnector[]; + config: FieldConfig; +}): FieldConfig => ({ + ...config, + validations: [ + { + validator: ({ value: connectorId }) => { + const connector = getConnectorById(connectorId as string, connectors); + if (connector != null) { + return validators[connector.actionTypeId]?.(connector); + } + }, + }, + ], +}); diff --git a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx index 4f28d88c14b259..e4ea6d05011a78 100644 --- a/x-pack/plugins/cases/public/containers/use_get_action_license.tsx +++ b/x-pack/plugins/cases/public/containers/use_get_action_license.tsx @@ -11,6 +11,7 @@ import { useToasts } from '../common/lib/kibana'; import { getActionLicense } from './api'; import * as i18n from './translations'; import { ActionLicense } from './types'; +import { ConnectorTypes } from '../../common'; export interface ActionLicenseState { actionLicense: ActionLicense | null; @@ -24,7 +25,7 @@ export const initialData: ActionLicenseState = { isError: false, }; -const MINIMUM_LICENSE_REQUIRED_CONNECTOR = '.jira'; +const MINIMUM_LICENSE_REQUIRED_CONNECTOR = ConnectorTypes.jira; export const useGetActionLicense = (): ActionLicenseState => { const [actionLicenseState, setActionLicensesState] = useState(initialData); diff --git a/x-pack/plugins/cases/server/client/cases/get.ts b/x-pack/plugins/cases/server/client/cases/get.ts index 3df1891391c75e..4f8713704361b7 100644 --- a/x-pack/plugins/cases/server/client/cases/get.ts +++ b/x-pack/plugins/cases/server/client/cases/get.ts @@ -173,7 +173,6 @@ export const get = async ( let theCase: SavedObject; let subCaseIds: string[] = []; - if (ENABLE_CASE_CONNECTOR) { const [caseInfo, subCasesForCaseId] = await Promise.all([ caseService.getCase({ diff --git a/x-pack/plugins/cases/server/client/cases/utils.ts b/x-pack/plugins/cases/server/client/cases/utils.ts index d920c517a00044..f5a10d705e095d 100644 --- a/x-pack/plugins/cases/server/client/cases/utils.ts +++ b/x-pack/plugins/cases/server/client/cases/utils.ts @@ -252,6 +252,7 @@ export const prepareFieldsForTransformation = ({ mappings.reduce( (acc: PipedField[], mapping) => mapping != null && + mapping.target != null && mapping.target !== 'not_mapped' && mapping.action_type !== 'nothing' && mapping.source !== 'comments' diff --git a/x-pack/plugins/cases/server/connectors/case/index.test.ts b/x-pack/plugins/cases/server/connectors/case/index.test.ts index 7b8f57bf0d3bfb..51c45bd25444e9 100644 --- a/x-pack/plugins/cases/server/connectors/case/index.test.ts +++ b/x-pack/plugins/cases/server/connectors/case/index.test.ts @@ -60,7 +60,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -99,7 +99,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -293,7 +293,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { priority: 'High', parent: null, @@ -438,7 +438,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -640,7 +640,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { priority: 'High', parent: null, @@ -974,7 +974,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', @@ -1003,7 +1003,7 @@ describe('case connector', () => { connector: { id: 'jira', name: 'Jira', - type: '.jira', + type: ConnectorTypes.jira, fields: { issueType: '10006', priority: 'High', diff --git a/x-pack/plugins/cases/server/connectors/case/schema.ts b/x-pack/plugins/cases/server/connectors/case/schema.ts index 596a5a4aae45ed..79d3bf62e8a9e7 100644 --- a/x-pack/plugins/cases/server/connectors/case/schema.ts +++ b/x-pack/plugins/cases/server/connectors/case/schema.ts @@ -6,7 +6,7 @@ */ import { schema } from '@kbn/config-schema'; -import { CommentType } from '../../../common'; +import { CommentType, ConnectorTypes } from '../../../common'; import { validateConnector } from './validators'; // Reserved for future implementation @@ -77,23 +77,29 @@ const ServiceNowSIRFieldsSchema = schema.object({ subcategory: schema.nullable(schema.string()), }); +const SwimlaneFieldsSchema = schema.object({ + caseId: schema.nullable(schema.string()), +}); + const NoneFieldsSchema = schema.nullable(schema.object({})); const ReducedConnectorFieldsSchema: { [x: string]: any } = { - '.jira': JiraFieldsSchema, - '.resilient': ResilientFieldsSchema, - '.servicenow-sir': ServiceNowSIRFieldsSchema, + [ConnectorTypes.jira]: JiraFieldsSchema, + [ConnectorTypes.resilient]: ResilientFieldsSchema, + [ConnectorTypes.serviceNowSIR]: ServiceNowSIRFieldsSchema, + [ConnectorTypes.swimlane]: SwimlaneFieldsSchema, }; export const ConnectorProps = { id: schema.string(), name: schema.string(), type: schema.oneOf([ - schema.literal('.servicenow'), - schema.literal('.jira'), - schema.literal('.resilient'), - schema.literal('.servicenow-sir'), - schema.literal('.none'), + schema.literal(ConnectorTypes.jira), + schema.literal(ConnectorTypes.none), + schema.literal(ConnectorTypes.resilient), + schema.literal(ConnectorTypes.serviceNowITSM), + schema.literal(ConnectorTypes.serviceNowSIR), + schema.literal(ConnectorTypes.swimlane), ]), // Chain of conditional schemes fields: Object.keys(ReducedConnectorFieldsSchema).reduce( @@ -106,7 +112,7 @@ export const ConnectorProps = { ), schema.conditional( schema.siblingRef('type'), - '.servicenow', + ConnectorTypes.serviceNowITSM, ServiceNowITSMFieldsSchema, NoneFieldsSchema ) diff --git a/x-pack/plugins/cases/server/connectors/case/validators.ts b/x-pack/plugins/cases/server/connectors/case/validators.ts index 03110d15c9d3f5..6ab4f3a21a24ff 100644 --- a/x-pack/plugins/cases/server/connectors/case/validators.ts +++ b/x-pack/plugins/cases/server/connectors/case/validators.ts @@ -6,9 +6,10 @@ */ import { Connector } from './types'; +import { ConnectorTypes } from '../../../common'; export const validateConnector = (connector: Connector) => { - if (connector.type === '.none' && connector.fields !== null) { + if (connector.type === ConnectorTypes.none && connector.fields !== null) { return 'Fields must be set to null for connectors of type .none'; } }; diff --git a/x-pack/plugins/cases/server/connectors/factory.ts b/x-pack/plugins/cases/server/connectors/factory.ts index 5ed7eb4ade4caa..d0ae7154fe5d99 100644 --- a/x-pack/plugins/cases/server/connectors/factory.ts +++ b/x-pack/plugins/cases/server/connectors/factory.ts @@ -6,16 +6,18 @@ */ import { ConnectorTypes } from '../../common'; +import { ICasesConnector, CasesConnectorsMap } from './types'; import { getCaseConnector as getJiraCaseConnector } from './jira'; import { getCaseConnector as getResilientCaseConnector } from './resilient'; import { getServiceNowITSMCaseConnector, getServiceNowSIRCaseConnector } from './servicenow'; -import { ICasesConnector, CasesConnectorsMap } from './types'; +import { getCaseConnector as getSwimlaneCaseConnector } from './swimlane'; const mapping: Record = { [ConnectorTypes.jira]: getJiraCaseConnector(), [ConnectorTypes.serviceNowITSM]: getServiceNowITSMCaseConnector(), [ConnectorTypes.serviceNowSIR]: getServiceNowSIRCaseConnector(), [ConnectorTypes.resilient]: getResilientCaseConnector(), + [ConnectorTypes.swimlane]: getSwimlaneCaseConnector(), [ConnectorTypes.none]: null, }; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts new file mode 100644 index 00000000000000..55cbbdb68691e8 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.test.ts @@ -0,0 +1,21 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { CaseResponse } from '../../../common'; +import { format } from './format'; + +describe('Swimlane formatter', () => { + const theCase = { + id: 'case-id', + connector: { fields: null }, + } as CaseResponse; + + it('it formats correctly', async () => { + const res = await format(theCase, []); + expect(res).toEqual({ caseId: theCase.id }); + }); +}); diff --git a/x-pack/plugins/cases/server/connectors/swimlane/format.ts b/x-pack/plugins/cases/server/connectors/swimlane/format.ts new file mode 100644 index 00000000000000..9531e4099a4f4e --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/format.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { ConnectorSwimlaneTypeFields } from '../../../common'; +import { Format } from './types'; + +export const format: Format = (theCase) => { + const { caseId = theCase.id } = + (theCase.connector.fields as ConnectorSwimlaneTypeFields['fields']) ?? {}; + return { caseId }; +}; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/index.ts b/x-pack/plugins/cases/server/connectors/swimlane/index.ts new file mode 100644 index 00000000000000..2cad92391bdec0 --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/index.ts @@ -0,0 +1,15 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { getMapping } from './mapping'; +import { format } from './format'; +import { SwimlaneCaseConnector } from './types'; + +export const getCaseConnector = (): SwimlaneCaseConnector => ({ + getMapping, + format, +}); diff --git a/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts b/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts new file mode 100644 index 00000000000000..e1e34054463e5a --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/mapping.ts @@ -0,0 +1,28 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { GetMapping } from './types'; + +export const getMapping: GetMapping = () => { + return [ + { + source: 'title', + target: 'caseName', + action_type: 'overwrite', + }, + { + source: 'description', + target: 'description', + action_type: 'overwrite', + }, + { + source: 'comments', + target: 'comments', + action_type: 'append', + }, + ]; +}; diff --git a/x-pack/plugins/cases/server/connectors/swimlane/types.ts b/x-pack/plugins/cases/server/connectors/swimlane/types.ts new file mode 100644 index 00000000000000..22a1e9f6372d5c --- /dev/null +++ b/x-pack/plugins/cases/server/connectors/swimlane/types.ts @@ -0,0 +1,13 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { SwimlaneFieldsType } from '../../../common/api'; +import { ICasesConnector } from '../types'; + +export type SwimlaneCaseConnector = ICasesConnector; +export type Format = ICasesConnector['format']; +export type GetMapping = ICasesConnector['getMapping']; diff --git a/x-pack/plugins/security_solution/common/constants.ts b/x-pack/plugins/security_solution/common/constants.ts index e65ff1afcc9c33..fdbd535384270c 100644 --- a/x-pack/plugins/security_solution/common/constants.ts +++ b/x-pack/plugins/security_solution/common/constants.ts @@ -240,6 +240,7 @@ export const NOTIFICATION_SUPPORTED_ACTION_TYPES_IDS = [ '.email', '.slack', '.pagerduty', + '.swimlane', '.webhook', '.servicenow', '.jira', diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 9230b4d8298537..39852ebaeb46be 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -31,6 +31,9 @@ "__index": { "type": "long" }, + "__swimlane": { + "type": "long" + }, "__pagerduty": { "type": "long" }, @@ -68,6 +71,9 @@ "__index": { "type": "long" }, + "__swimlane": { + "type": "long" + }, "__pagerduty": { "type": "long" }, diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts index 2eda435d045a4a..4266822bda1fc1 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/index.ts @@ -10,6 +10,7 @@ import { getSlackActionType } from './slack'; import { getEmailActionType } from './email'; import { getIndexActionType } from './es_index'; import { getPagerDutyActionType } from './pagerduty'; +import { getSwimlaneActionType } from './swimlane'; import { getWebhookActionType } from './webhook'; import { TypeRegistry } from '../../type_registry'; import { ActionTypeModel } from '../../../types'; @@ -28,6 +29,7 @@ export function registerBuiltInActionTypes({ actionTypeRegistry.register(getEmailActionType()); actionTypeRegistry.register(getIndexActionType()); actionTypeRegistry.register(getPagerDutyActionType()); + actionTypeRegistry.register(getSwimlaneActionType()); actionTypeRegistry.register(getWebhookActionType()); actionTypeRegistry.register(getServiceNowITSMActionType()); actionTypeRegistry.register(getServiceNowSIRActionType()); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx index b89f71b0fc3548..be5250ccf8b293 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_connectors.test.tsx @@ -12,7 +12,7 @@ import { JiraActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('JiraActionConnectorFields renders', () => { - test('alerting Jira connector fields is rendered', () => { + test('alerting Jira connector fields are rendered', () => { const actionConnector = { secrets: { email: 'email', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx index 5897de46f94df7..99d7e9510454f4 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/jira/jira_params.tsx @@ -63,6 +63,7 @@ const JiraParamsFields: React.FunctionComponent { if (key === 'issueType') { @@ -75,9 +76,11 @@ const JiraParamsFields: React.FunctionComponent { if (incident.issueType != null && fields != null) { const priorities = fields.priority != null ? fields.priority.allowedValues : []; @@ -141,6 +145,7 @@ const JiraParamsFields: React.FunctionComponent { if (!hasPriority && incident.priority != null) { editSubActionProperty('priority', null); @@ -167,6 +172,7 @@ const JiraParamsFields: React.FunctionComponent { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx index b7b68b9485d8a7..bbd237a7cec897 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_connectors.test.tsx @@ -12,7 +12,7 @@ import { ResilientActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('ResilientActionConnectorFields renders', () => { - test('alerting Resilient connector fields is rendered', () => { + test('alerting Resilient connector fields are rendered', () => { const actionConnector = { secrets: { apiKeyId: 'key', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx index 54a138a2bc7cfc..b0f5198b6b5fde 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/resilient/resilient_params.tsx @@ -147,6 +147,7 @@ const ResilientParamsFields: React.FunctionComponent { if (!actionParams.subAction) { editAction('subAction', 'pushToService', index); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx index 330844b93b6b5a..4993c51f350ad6 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/servicenow/servicenow_connectors.test.tsx @@ -12,7 +12,7 @@ import { ServiceNowActionConnector } from './types'; jest.mock('../../../../common/lib/kibana'); describe('ServiceNowActionConnectorFields renders', () => { - test('alerting servicenow connector fields is rendered', () => { + test('alerting servicenow connector fields are rendered', () => { const actionConnector = { secrets: { username: 'user', diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts new file mode 100644 index 00000000000000..90bab65b83bfdb --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.test.ts @@ -0,0 +1,145 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { getApplication } from './api'; + +const getApplicationResponse = { + fields: [], +}; + +describe('Swimlane API', () => { + let fetchMock: jest.SpyInstance>; + + beforeAll(() => jest.spyOn(window, 'fetch')); + beforeEach(() => { + jest.resetAllMocks(); + fetchMock = jest.spyOn(window, 'fetch'); + }); + + describe('getApplication', () => { + it('should call getApplication API correctly', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => getApplicationResponse, + }); + const res = await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + + expect(res).toEqual(getApplicationResponse); + }); + + it('returns an error when the response fails', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: false, + status: 401, + json: async () => getApplicationResponse, + }); + + try { + await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + } catch (e) { + expect(e.message).toContain('Received status:'); + } + }); + + it('returns an error when parsing the json fails', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + status: 200, + json: async () => { + throw new Error('bad'); + }, + }); + + try { + await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + } catch (e) { + expect(e.message).toContain('bad'); + } + }); + + it('it removes unsafe fields', async () => { + const abortCtrl = new AbortController(); + + fetchMock.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + fields: [ + { + id: '__proto__', + name: 'Alert Id', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'a6ide', + name: '__proto__', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'a6ide', + name: 'Alert Id', + key: '__proto__', + fieldType: 'text', + }, + { + id: 'a6ide', + name: 'Alert Id', + key: 'alert-id', + fieldType: '__proto__', + }, + { + id: 'safe-id', + name: 'Safe', + key: 'safe-key', + fieldType: 'safe-text', + }, + ], + }), + }); + + const res = await getApplication({ + signal: abortCtrl.signal, + apiToken: '', + appId: '', + url: '', + }); + + expect(res).toEqual({ + fields: [ + { + id: 'safe-id', + name: 'Safe', + key: 'safe-key', + fieldType: 'safe-text', + }, + ], + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts new file mode 100644 index 00000000000000..c6f9d4bee3e138 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/api.ts @@ -0,0 +1,65 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { SwimlaneFieldMappingConfig } from './types'; + +const removeUnsafeFields = (fields: SwimlaneFieldMappingConfig[]): SwimlaneFieldMappingConfig[] => + fields.filter( + (filter) => + filter.id !== '__proto__' && + filter.key !== '__proto__' && + filter.name !== '__proto__' && + filter.fieldType !== '__proto__' + ); +export async function getApplication({ + signal, + url, + appId, + apiToken, +}: { + signal: AbortSignal; + url: string; + appId: string; + apiToken: string; +}): Promise> { + const headers: Record = { + 'Content-Type': 'application/json', + 'Private-Token': `${apiToken}`, + }; + + const urlWithoutTrailingSlash = url.endsWith('/') ? url.slice(0, -1) : url; + const apiUrl = urlWithoutTrailingSlash.endsWith('api') + ? urlWithoutTrailingSlash + : urlWithoutTrailingSlash + '/api'; + const applicationUrl = `${apiUrl}/app/{appId}`; + + const getApplicationUrl = (id: string) => applicationUrl.replace('{appId}', id); + + try { + const response = await fetch(getApplicationUrl(appId), { + method: 'GET', + headers, + signal, + }); + + /** + * Fetch do not throw when there is an HTTP error (status >= 400). + * We need to do it manually. + */ + + if (!response.ok) { + throw new Error( + `Received status: ${response.status} when attempting to get application with id: ${appId}` + ); + } + + const data = await response.json(); + return { ...data, fields: removeUnsafeFields(data?.fields ?? []) }; + } catch (error) { + throw new Error(`Unable to get application with id ${appId}. Error: ${error.message}`); + } +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts new file mode 100644 index 00000000000000..413b952675b8ce --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/helpers.ts @@ -0,0 +1,62 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { SwimlaneConnectorType, SwimlaneMappingConfig, MappingConfigurationKeys } from './types'; +import * as i18n from './translations'; + +const casesRequiredFields: MappingConfigurationKeys[] = [ + 'caseNameConfig', + 'descriptionConfig', + 'commentsConfig', + 'caseIdConfig', +]; +const casesFields = [...casesRequiredFields]; +const alertsRequiredFields: MappingConfigurationKeys[] = ['ruleNameConfig', 'alertIdConfig']; +const alertsFields = ['severityConfig', 'commentsConfig', ...alertsRequiredFields]; + +const translationMapping: Record = { + caseIdConfig: i18n.SW_REQUIRED_CASE_ID, + alertIdConfig: i18n.SW_REQUIRED_ALERT_ID, + caseNameConfig: i18n.SW_REQUIRED_CASE_NAME, + descriptionConfig: i18n.SW_REQUIRED_DESCRIPTION, + commentsConfig: i18n.SW_REQUIRED_COMMENTS, + ruleNameConfig: i18n.SW_REQUIRED_RULE_NAME, + severityConfig: i18n.SW_REQUIRED_SEVERITY, +}; + +export const isValidFieldForConnector = ( + connector: SwimlaneConnectorType, + field: MappingConfigurationKeys +): boolean => { + if (connector === SwimlaneConnectorType.All) { + return true; + } + + return connector === SwimlaneConnectorType.Alerts + ? alertsFields.includes(field) + : casesFields.includes(field); +}; + +export const validateMappingForConnector = ( + connectorType: SwimlaneConnectorType, + mapping: SwimlaneMappingConfig +): Record => { + if (connectorType === SwimlaneConnectorType.All || connectorType == null) { + return {}; + } + + const requiredFields = + connectorType === SwimlaneConnectorType.Alerts ? alertsRequiredFields : casesRequiredFields; + + return requiredFields.reduce((errors, field) => { + if (mapping?.[field] == null) { + errors = { ...errors, [field]: translationMapping[field] }; + } + + return errors; + }, {} as Record); +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts new file mode 100644 index 00000000000000..39a57e1bccb610 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/index.ts @@ -0,0 +1,8 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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. + */ + +export { getActionType as getSwimlaneActionType } from './swimlane'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx new file mode 100644 index 00000000000000..d22ff809fe74dd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/logo.tsx @@ -0,0 +1,53 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React from 'react'; + +const Logo = () => { + return ( + + + + + + + + + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { Logo as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts new file mode 100644 index 00000000000000..1574dfe2f5384d --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/mocks.ts @@ -0,0 +1,61 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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. + */ + +export const applicationFields = [ + { + id: 'a6ide', + name: 'Alert Id', + key: 'alert-id', + fieldType: 'text', + }, + { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + { + id: 'a6fdf', + name: 'Comments', + key: 'notes', + fieldType: 'comments', + }, + { + id: 'a6fde', + name: 'Description', + key: 'description', + fieldType: 'text', + }, +]; + +export const mappings = { + alertIdConfig: applicationFields[0], + severityConfig: applicationFields[1], + ruleNameConfig: applicationFields[2], + caseIdConfig: applicationFields[3], + caseNameConfig: applicationFields[4], + commentsConfig: applicationFields[5], + descriptionConfig: applicationFields[6], +}; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts new file mode 100644 index 00000000000000..ca7c39bf1378cc --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/index.ts @@ -0,0 +1,9 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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. + */ + +export { SwimlaneConnection } from './swimlane_connection'; +export { SwimlaneFields } from './swimlane_fields'; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx new file mode 100644 index 00000000000000..cd29037e3535fd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_connection.tsx @@ -0,0 +1,201 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { + EuiButton, + EuiCallOut, + EuiFieldText, + EuiFormRow, + EuiLink, + EuiSpacer, + EuiText, +} from '@elastic/eui'; +import React, { useCallback } from 'react'; +import { FormattedMessage } from 'react-intl'; +import * as i18n from '../translations'; +import { useKibana } from '../../../../../common/lib/kibana'; +import { useGetApplication } from '../use_get_application'; +import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from '../types'; +import { IErrorObject } from '../../../../../types'; + +interface Props { + action: SwimlaneActionConnector; + editActionConfig: (property: string, value: any) => void; + editActionSecrets: (property: string, value: any) => void; + errors: IErrorObject; + readOnly: boolean; + updateCurrentStep: (step: number) => void; + updateFields: (items: SwimlaneFieldMappingConfig[]) => void; +} + +const SwimlaneConnectionComponent: React.FunctionComponent = ({ + action, + editActionConfig, + editActionSecrets, + errors, + readOnly, + updateCurrentStep, + updateFields, +}) => { + const { + notifications: { toasts }, + } = useKibana().services; + const { apiUrl, appId } = action.config; + const { apiToken } = action.secrets; + const { docLinks } = useKibana().services; + const { getApplication } = useGetApplication({ + toastNotifications: toasts, + apiToken, + appId, + apiUrl, + }); + const isValid = apiUrl && apiToken && appId; + + const connectSwimlane = useCallback(async () => { + // fetch swimlane application configuration + const application = await getApplication(); + + if (application?.fields) { + const allFields = application.fields; + updateFields(allFields); + updateCurrentStep(2); + } + }, [getApplication, updateCurrentStep, updateFields]); + + const onChangeConfig = useCallback( + (e: React.ChangeEvent, key: 'apiUrl' | 'appId') => { + editActionConfig(key, e.target.value); + }, + [editActionConfig] + ); + + const onBlurConfig = useCallback( + (key: 'apiUrl' | 'appId') => { + if (!action.config[key]) { + editActionConfig(key, ''); + } + }, + [action.config, editActionConfig] + ); + + const onChangeSecrets = useCallback( + (e: React.ChangeEvent) => { + editActionSecrets('apiToken', e.target.value); + }, + [editActionSecrets] + ); + + const onBlurSecrets = useCallback(() => { + if (!apiToken) { + editActionSecrets('apiToken', ''); + } + }, [apiToken, editActionSecrets]); + + const isApiUrlInvalid = errors.apiUrl?.length > 0 && apiToken !== undefined; + const isAppIdInvalid = errors.appId?.length > 0 && apiToken !== undefined; + const isApiTokenInvalid = errors.apiToken?.length > 0 && apiToken !== undefined; + + return ( + <> + + onChangeConfig(e, 'apiUrl')} + onBlur={() => onBlurConfig('apiUrl')} + /> + + + onChangeConfig(e, 'appId')} + onBlur={() => onBlurConfig('appId')} + /> + + + + + } + error={errors.apiToken} + isInvalid={isApiTokenInvalid} + label={i18n.SW_API_TOKEN_TEXT_FIELD_LABEL} + > + <> + {!action.id ? ( + <> + + + {i18n.SW_REMEMBER_VALUE_LABEL} + + + + ) : ( + <> + + + + + )} + + + + + + {i18n.SW_RETRIEVE_CONFIGURATION_LABEL} + + + ); +}; + +export const SwimlaneConnection = React.memo(SwimlaneConnectionComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx new file mode 100644 index 00000000000000..87d0964322e140 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/steps/swimlane_fields.tsx @@ -0,0 +1,313 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React, { useMemo, useCallback, useEffect, useRef } from 'react'; +import { + EuiButton, + EuiFormRow, + EuiComboBox, + EuiComboBoxOptionOption, + EuiButtonGroup, +} from '@elastic/eui'; +import * as i18n from '../translations'; +import { + SwimlaneActionConnector, + SwimlaneConnectorType, + SwimlaneFieldMappingConfig, + SwimlaneMappingConfig, +} from '../types'; +import { IErrorObject } from '../../../../../types'; +import { isValidFieldForConnector } from '../helpers'; + +const SINGLE_SELECTION = { asPlainText: true }; +const EMPTY_COMBO_BOX_ARRAY: Array> | undefined = []; + +const formatOption = (field: SwimlaneFieldMappingConfig) => ({ + label: `${field.name} (${field.key})`, + value: field.id, +}); + +const createSelectedOption = (field: SwimlaneFieldMappingConfig | null | undefined) => + field != null ? [formatOption(field)] : EMPTY_COMBO_BOX_ARRAY; + +interface Props { + action: SwimlaneActionConnector; + editActionConfig: (property: string, value: any) => void; + updateCurrentStep: (step: number) => void; + fields: SwimlaneFieldMappingConfig[]; + errors: IErrorObject; +} + +const connectorTypeButtons = [ + { id: 'all', label: 'All' }, + { id: 'alerts', label: 'Alerts' }, + { id: 'cases', label: 'Cases' }, +]; + +const SwimlaneFieldsComponent: React.FC = ({ + action, + editActionConfig, + updateCurrentStep, + fields, + errors, +}) => { + const { mappings, connectorType = SwimlaneConnectorType.All } = action.config; + const prevConnectorType = useRef(connectorType); + const hasChangedConnectorType = connectorType !== prevConnectorType.current; + + const [fieldTypeMap, fieldIdMap] = useMemo( + () => + fields.reduce( + ([typeMap, idMap], field) => { + if (field != null) { + typeMap.set(field.fieldType, [ + ...(typeMap.get(field.fieldType) ?? []), + formatOption(field), + ]); + idMap.set(field.id, field); + } + + return [typeMap, idMap]; + }, + [ + new Map>>(), + new Map(), + ] + ), + [fields] + ); + + const textOptions = useMemo(() => fieldTypeMap.get('text') ?? [], [fieldTypeMap]); + const commentsOptions = useMemo(() => fieldTypeMap.get('comments') ?? [], [fieldTypeMap]); + + const state = useMemo( + () => ({ + alertIdConfig: createSelectedOption(mappings?.alertIdConfig), + severityConfig: createSelectedOption(mappings?.severityConfig), + ruleNameConfig: createSelectedOption(mappings?.ruleNameConfig), + caseIdConfig: createSelectedOption(mappings?.caseIdConfig), + caseNameConfig: createSelectedOption(mappings?.caseNameConfig), + commentsConfig: createSelectedOption(mappings?.commentsConfig), + descriptionConfig: createSelectedOption(mappings?.descriptionConfig), + }), + [mappings] + ); + + const mappingErrors: Record = useMemo( + () => (Array.isArray(errors?.mappings) ? errors?.mappings[0] : {}), + [errors] + ); + + const resetConnection = useCallback(() => { + updateCurrentStep(1); + }, [updateCurrentStep]); + + const editMappings = useCallback( + (key: keyof SwimlaneMappingConfig, e: Array>) => { + if (e.length === 0) { + const newProps = { + ...mappings, + [key]: null, + }; + editActionConfig('mappings', newProps); + return; + } + + const option = e[0]; + const item = fieldIdMap.get(option.value ?? ''); + if (!item) { + return; + } + + const newProps = { + ...mappings, + [key]: { id: item.id, name: item.name, key: item.key, fieldType: item.fieldType }, + }; + editActionConfig('mappings', newProps); + }, + [editActionConfig, fieldIdMap, mappings] + ); + + /** + * Connector type needs to be updated on mount to All. + * Otherwise it is undefined and this will cause an error + * if the user saves the connector without any mapping + */ + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => editActionConfig('connectorType', connectorType), []); + + useEffect(() => { + if (connectorType !== prevConnectorType.current) { + prevConnectorType.current = connectorType; + } + }, [connectorType]); + + return ( + <> + + editActionConfig('connectorType', type)} + buttonSize="compressed" + /> + + {isValidFieldForConnector(connectorType as SwimlaneConnectorType.All, 'alertIdConfig') && ( + <> + + editMappings('alertIdConfig', e)} + isInvalid={mappingErrors?.alertIdConfig != null && !hasChangedConnectorType} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'ruleNameConfig') && ( + <> + + editMappings('ruleNameConfig', e)} + isInvalid={mappingErrors?.ruleNameConfig != null && !hasChangedConnectorType} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'severityConfig') && ( + <> + + editMappings('severityConfig', e)} + isInvalid={mappingErrors?.severityConfig != null && !hasChangedConnectorType} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseIdConfig') && ( + <> + + editMappings('caseIdConfig', e)} + isInvalid={mappingErrors?.caseIdConfig != null && !hasChangedConnectorType} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'caseNameConfig') && ( + <> + + editMappings('caseNameConfig', e)} + isInvalid={mappingErrors?.caseNameConfig != null && !hasChangedConnectorType} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'commentsConfig') && ( + <> + + editMappings('commentsConfig', e)} + isInvalid={mappingErrors?.commentsConfig != null && !hasChangedConnectorType} + /> + + + )} + {isValidFieldForConnector(connectorType as SwimlaneConnectorType, 'descriptionConfig') && ( + <> + + editMappings('descriptionConfig', e)} + isInvalid={mappingErrors?.descriptionConfig != null && !hasChangedConnectorType} + /> + + + )} + {i18n.SW_CONFIGURE_API_LABEL} + + ); +}; + +export const SwimlaneFields = React.memo(SwimlaneFieldsComponent); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx new file mode 100644 index 00000000000000..07d78a8885c510 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.test.tsx @@ -0,0 +1,219 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { TypeRegistry } from '../../../type_registry'; +import { registerBuiltInActionTypes } from '.././index'; +import { ActionTypeModel } from '../../../../types'; +import { SwimlaneActionConnector } from './types'; + +const ACTION_TYPE_ID = '.swimlane'; +let actionTypeModel: ActionTypeModel; + +beforeAll(() => { + const actionTypeRegistry = new TypeRegistry(); + registerBuiltInActionTypes({ actionTypeRegistry }); + const getResult = actionTypeRegistry.get(ACTION_TYPE_ID); + if (getResult !== null) { + actionTypeModel = getResult; + } +}); + +describe('actionTypeRegistry.get() works', () => { + test('action type static data is as expected', () => { + expect(actionTypeModel.id).toEqual(ACTION_TYPE_ID); + }); +}); + +describe('swimlane connector validation', () => { + test('connector validation succeeds when connector is valid', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings: { + alertIdConfig: { id: '1234' }, + severityConfig: { id: '1234' }, + ruleNameConfig: { id: '1234' }, + caseIdConfig: { id: '1234' }, + caseNameConfig: { id: '1234' }, + descriptionConfig: { id: '1234' }, + commentsConfig: { id: '1234' }, + }, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=all', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings: {}, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { errors: { apiUrl: [], appId: [], mappings: [], connectorType: [] } }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=cases', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'cases', + mappings: {}, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: [], + appId: [], + mappings: [ + { + caseIdConfig: 'Case ID is required.', + caseNameConfig: 'Case name is required.', + commentsConfig: 'Comments are required.', + descriptionConfig: 'Description is required.', + }, + ], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly when connectorType=alerts', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'alerts', + mappings: {}, + }, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: [], + appId: [], + mappings: [ + { + alertIdConfig: 'Alert ID is required.', + ruleNameConfig: 'Rule name is required.', + }, + ], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: [] } }, + }); + }); + + test('it validates correctly required config/secrets fields', async () => { + const actionConnector = { + secrets: {}, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: {}, + } as SwimlaneActionConnector; + + expect(await actionTypeModel.validateConnector(actionConnector)).toEqual({ + config: { + errors: { + apiUrl: ['URL is required.'], + appId: ['An App ID is required.'], + mappings: [], + connectorType: [], + }, + }, + secrets: { errors: { apiToken: ['An API token is required.'] } }, + }); + }); +}); + +describe('swimlane action params validation', () => { + test('action params validation succeeds when action params is valid', async () => { + const actionParams = { + subActionParams: { + ruleName: 'Rule Name', + alertId: 'alert-id', + }, + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + }); + }); + + test('it validates correctly required fields', async () => { + const actionParams = { + subActionParams: { incident: {} }, + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': ['Rule name is required.'], + 'subActionParams.incident.alertId': ['Alert ID is required.'], + }, + }); + }); + + test('it succeeds when missing incident', async () => { + const actionParams = { + subActionParams: {}, + }; + + expect(await actionTypeModel.validateParams(actionParams)).toEqual({ + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx new file mode 100644 index 00000000000000..5e06e3935eebdd --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane.tsx @@ -0,0 +1,106 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { isEmpty } from 'lodash'; +import { lazy } from 'react'; +import { + ActionTypeModel, + ConnectorValidationResult, + GenericValidationResult, +} from '../../../../types'; +import { + SwimlaneActionConnector, + SwimlaneConfig, + SwimlaneSecrets, + SwimlaneActionParams, +} from './types'; +import * as i18n from './translations'; +import { isValidUrl } from '../../../lib/value_validators'; +import { validateMappingForConnector } from './helpers'; + +export function getActionType(): ActionTypeModel< + SwimlaneConfig, + SwimlaneSecrets, + SwimlaneActionParams +> { + return { + id: '.swimlane', + iconClass: lazy(() => import('./logo')), + selectMessage: i18n.SW_SELECT_MESSAGE_TEXT, + actionTypeTitle: i18n.SW_ACTION_TYPE_TITLE, + validateConnector: async ( + action: SwimlaneActionConnector + ): Promise> => { + const configErrors = { + apiUrl: new Array(), + appId: new Array(), + connectorType: new Array(), + mappings: new Array>(), + }; + const secretsErrors = { + apiToken: new Array(), + }; + + const validationResult = { + config: { errors: configErrors }, + secrets: { errors: secretsErrors }, + }; + + if (!action.config.apiUrl) { + configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_REQUIRED]; + } else if (action.config.apiUrl) { + if (!isValidUrl(action.config.apiUrl)) { + configErrors.apiUrl = [...configErrors.apiUrl, i18n.SW_API_URL_INVALID]; + } + } + + if (!action.secrets.apiToken) { + secretsErrors.apiToken = [...secretsErrors.apiToken, i18n.SW_REQUIRED_API_TOKEN_TEXT]; + } + + if (!action.config.appId) { + configErrors.appId = [...configErrors.appId, i18n.SW_REQUIRED_APP_ID_TEXT]; + } + + const mappingErrors = validateMappingForConnector( + action.config.connectorType, + action.config.mappings + ); + + if (!isEmpty(mappingErrors)) { + configErrors.mappings = [...configErrors.mappings, mappingErrors]; + } + + return validationResult; + }, + validateParams: async ( + actionParams: SwimlaneActionParams + ): Promise> => { + const errors = { + 'subActionParams.incident.ruleName': new Array(), + 'subActionParams.incident.alertId': new Array(), + }; + const validationResult = { + errors, + }; + + const hasIncident = actionParams.subActionParams && actionParams.subActionParams.incident; + + if (hasIncident && !actionParams.subActionParams.incident.ruleName?.length) { + errors['subActionParams.incident.ruleName'].push(i18n.SW_REQUIRED_RULE_NAME); + } + + if (hasIncident && !actionParams.subActionParams.incident.alertId?.length) { + errors['subActionParams.incident.alertId'].push(i18n.SW_REQUIRED_ALERT_ID); + } + + return validationResult; + }, + actionConnectorFields: lazy(() => import('./swimlane_connectors')), + actionParamsFields: lazy(() => import('./swimlane_params')), + }; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx new file mode 100644 index 00000000000000..6740179d786f27 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.test.tsx @@ -0,0 +1,319 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React from 'react'; +import { mountWithIntl, nextTick } from '@kbn/test/jest'; +import { act } from 'react-dom/test-utils'; +import { SwimlaneActionConnector } from './types'; +import SwimlaneActionConnectorFields from './swimlane_connectors'; +import { useGetApplication } from './use_get_application'; +import { applicationFields, mappings } from './mocks'; + +jest.mock('../../../../common/lib/kibana'); +jest.mock('./use_get_application'); + +const useGetApplicationMock = useGetApplication as jest.Mock; +const getApplication = jest.fn(); + +describe('SwimlaneActionConnectorFields renders', () => { + beforeAll(() => { + useGetApplicationMock.mockReturnValue({ + getApplication, + isLoading: false, + }); + }); + + test('all connector fields are rendered', async () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneApiUrlInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAppIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneApiTokenInput"]').exists()).toBeTruthy(); + }); + + test('should display a message on create to remember credentials', () => { + const actionConnector = { + actionTypeId: '.swimlane', + secrets: {}, + config: {}, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toEqual(0); + }); + + test('should display a message on edit to re-enter credentials', () => { + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + expect(wrapper.find('[data-test-subj="reenterValuesMessage"]').length).toBeGreaterThan(0); + expect(wrapper.find('[data-test-subj="rememberValuesMessage"]').length).toEqual(0); + }); + + test('renders the mappings correctly - connector type all', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + }); + + test('renders the mappings correctly - connector type cases', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'cases', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeTruthy(); + }); + + test('renders the mappings correctly - connector type alerts', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'alerts', + mappings, + }, + } as SwimlaneActionConnector; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneSeverityInput"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').exists()).toBeFalsy(); + expect(wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').exists()).toBeFalsy(); + }); + + test('renders the correct options per field', async () => { + getApplication.mockResolvedValue({ + fields: applicationFields, + }); + + const actionConnector = { + secrets: { + apiToken: 'test', + }, + id: 'test', + actionTypeId: '.swimlane', + name: 'swimlane', + config: { + apiUrl: 'http:\\test', + appId: '1234567asbd32', + connectorType: 'all', + mappings, + }, + } as SwimlaneActionConnector; + + const textOptions = [ + { label: 'Alert Id (alert-id)', value: 'a6ide' }, + { label: 'Severity (severity)', value: 'adnlas' }, + { label: 'Rule Name (rule-name)', value: 'adnfls' }, + { label: 'Case Id (case-id-name)', value: 'a6sst' }, + { label: 'Case Name (case-name)', value: 'a6fst' }, + { label: 'Description (description)', value: 'a6fde' }, + ]; + + const commentOptions = [{ label: 'Comments (notes)', value: 'a6fdf' }]; + + const wrapper = mountWithIntl( + {}} + editActionSecrets={() => {}} + readOnly={false} + /> + ); + + await act(async () => { + wrapper.find('[data-test-subj="swimlaneConfigureMapping"]').first().simulate('click'); + await nextTick(); + wrapper.update(); + }); + + expect(wrapper.find('[data-test-subj="swimlaneAlertIdInput"]').first().prop('options')).toEqual( + textOptions + ); + expect( + wrapper.find('[data-test-subj="swimlaneAlertNameInput"]').first().prop('options') + ).toEqual(textOptions); + expect( + wrapper.find('[data-test-subj="swimlaneSeverityInput"]').first().prop('options') + ).toEqual(textOptions); + expect(wrapper.find('[data-test-subj="swimlaneCaseIdConfig"]').first().prop('options')).toEqual( + textOptions + ); + expect( + wrapper.find('[data-test-subj="swimlaneCaseNameConfig"]').first().prop('options') + ).toEqual(textOptions); + expect( + wrapper.find('[data-test-subj="swimlaneCommentsConfig"]').first().prop('options') + ).toEqual(commentOptions); + expect( + wrapper.find('[data-test-subj="swimlaneDescriptionConfig"]').first().prop('options') + ).toEqual(textOptions); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx new file mode 100644 index 00000000000000..acf9f38e9ba48b --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_connectors.tsx @@ -0,0 +1,103 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React, { Fragment, useCallback, useMemo, useState } from 'react'; +import { EuiForm, EuiSpacer, EuiStepsHorizontal, EuiStepStatus } from '@elastic/eui'; +import * as i18n from './translations'; +import { ActionConnectorFieldsProps } from '../../../../types'; +import { SwimlaneActionConnector, SwimlaneFieldMappingConfig } from './types'; +import { SwimlaneConnection, SwimlaneFields } from './steps'; + +const SwimlaneActionConnectorFields: React.FunctionComponent< + ActionConnectorFieldsProps +> = ({ errors, action, editActionConfig, editActionSecrets, readOnly }) => { + const [currentStep, setCurrentStep] = useState(1); + const [stepsStatuses, setStepsStatuses] = useState<{ + connection: EuiStepStatus; + fields: EuiStepStatus; + }>({ connection: 'incomplete', fields: 'incomplete' }); + const [fields, setFields] = useState([]); + + const updateCurrentStep = useCallback( + (step: number) => { + setCurrentStep(step); + if (step === 2) { + setStepsStatuses((statuses) => ({ ...statuses, connection: 'complete' })); + } else if (step === 1) { + setStepsStatuses({ + fields: 'incomplete', + connection: 'incomplete', + }); + editActionConfig('mappings', action.config.mappings); + } + }, + [action.config.mappings, editActionConfig] + ); + + const setupSteps = useMemo( + () => [ + { + title: i18n.SW_CONFIGURE_CONNECTION_LABEL, + status: stepsStatuses.connection, + onClick: () => updateCurrentStep(1), + }, + { + title: i18n.SW_MAPPING_TITLE_TEXT_FIELD_LABEL, + disabled: stepsStatuses.connection !== 'complete', + status: stepsStatuses.fields, + onClick: () => updateCurrentStep(2), + }, + ], + [stepsStatuses.connection, stepsStatuses.fields, updateCurrentStep] + ); + + const editActionConfigCb = useCallback( + (k: string, v: string) => { + editActionConfig(k, v); + if ( + Object.values(errors?.mappings ?? {}).every((mappingError) => mappingError.length === 0) + ) { + setStepsStatuses((statuses) => ({ ...statuses, fields: 'complete' })); + } else { + setStepsStatuses((statuses) => ({ ...statuses, fields: 'incomplete' })); + } + }, + [editActionConfig, errors?.mappings] + ); + + return ( + + + + + {currentStep === 1 && ( + + )} + {currentStep === 2 && ( + + )} + + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneActionConnectorFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx new file mode 100644 index 00000000000000..32cf2c3c786d39 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.test.tsx @@ -0,0 +1,137 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React from 'react'; +import { mountWithIntl } from '@kbn/test/jest'; +import SwimlaneParamsFields from './swimlane_params'; +import { SwimlaneConnectorType } from './types'; +import { mappings } from './mocks'; + +describe('SwimlaneParamsFields renders', () => { + const editAction = jest.fn(); + const actionParams = { + subAction: 'pushToService', + subActionParams: { + incident: { + alertId: '3456789', + ruleName: 'rule name', + severity: 'critical', + caseId: null, + caseName: null, + description: null, + externalId: null, + }, + comments: [], + }, + }; + + const connector = { + secrets: {}, + config: { mappings, connectorType: SwimlaneConnectorType.All }, + id: 'test', + actionTypeId: '.test', + name: 'Test', + isPreconfigured: false, + }; + + const defaultProps = { + actionParams, + errors: { + 'subActionParams.incident.ruleName': [], + 'subActionParams.incident.alertId': [], + }, + editAction, + index: 0, + messageVariables: [], + actionConnector: connector, + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + test('all params fields are rendered', () => { + const wrapper = mountWithIntl(); + + expect(wrapper.find('[data-test-subj="severity"]').exists()).toBeTruthy(); + expect(wrapper.find('[data-test-subj="comments"]').exists()).toBeTruthy(); + }); + + test('it set the correct default params', () => { + mountWithIntl(); + expect(editAction).toHaveBeenCalledWith('subAction', 'pushToService', 0); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + test('it reset the fields when connector changes', () => { + const wrapper = mountWithIntl(); + expect(editAction).not.toHaveBeenCalled(); + + wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + test('it set the severity', () => { + const wrapper = mountWithIntl(); + expect(editAction).not.toHaveBeenCalled(); + + wrapper.setProps({ actionConnector: { ...connector, id: '1234' } }); + expect(editAction).toHaveBeenCalledWith( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + 0 + ); + }); + + describe('UI updates', () => { + const changeEvent = { target: { value: 'Bug' } } as React.ChangeEvent; + const simpleFields = [ + { dataTestSubj: 'input[data-test-subj="severityInput"]', key: 'severity' }, + ]; + + simpleFields.forEach((field) => + test(`${field.key} update triggers editAction`, () => { + const wrapper = mountWithIntl(); + const theField = wrapper.find(field.dataTestSubj).first(); + theField.prop('onChange')!(changeEvent); + expect(editAction.mock.calls[0][1].incident[field.key]).toEqual(changeEvent.target.value); + }) + ); + + test('A comment triggers editAction', () => { + const wrapper = mountWithIntl(); + const comments = wrapper.find('textarea[data-test-subj="commentsTextArea"]'); + expect(comments.simulate('change', changeEvent)); + expect(editAction.mock.calls[0][1].comments.length).toEqual(1); + }); + + test('An empty comment does not trigger editAction', () => { + const wrapper = mountWithIntl(); + const emptyComment = { target: { value: '' } }; + const comments = wrapper.find('[data-test-subj="commentsTextArea"] textarea'); + expect(comments.simulate('change', emptyComment)); + expect(editAction.mock.calls.length).toEqual(0); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx new file mode 100644 index 00000000000000..9bd14a06d657a3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/swimlane_params.tsx @@ -0,0 +1,159 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 React, { useCallback, useEffect, useRef, useMemo } from 'react'; +import { EuiCallOut, EuiFormRow, EuiSpacer } from '@elastic/eui'; +import * as i18n from './translations'; +import { ActionParamsProps } from '../../../../types'; +import { SwimlaneActionConnector, SwimlaneActionParams, SwimlaneConnectorType } from './types'; +import { TextFieldWithMessageVariables } from '../../text_field_with_message_variables'; +import { TextAreaWithMessageVariables } from '../../text_area_with_message_variables'; + +const SwimlaneParamsFields: React.FunctionComponent> = ({ + actionParams, + editAction, + index, + messageVariables, + actionConnector, +}) => { + const { incident, comments } = useMemo( + () => + actionParams.subActionParams ?? + (({ + incident: {}, + comments: [], + } as unknown) as SwimlaneActionParams['subActionParams']), + [actionParams.subActionParams] + ); + + const actionConnectorRef = useRef(actionConnector?.id ?? ''); + + const { + mappings, + connectorType, + } = ((actionConnector as unknown) as SwimlaneActionConnector).config; + const { hasAlertId, hasRuleName, hasComments, hasSeverity } = useMemo( + () => ({ + hasAlertId: mappings.alertIdConfig != null, + hasRuleName: mappings.ruleNameConfig != null, + hasComments: mappings.commentsConfig != null, + hasSeverity: mappings.severityConfig != null, + }), + [ + mappings.alertIdConfig, + mappings.ruleNameConfig, + mappings.commentsConfig, + mappings.severityConfig, + ] + ); + + /** + * The user can use either a connector of type alerts or all. + * If the connector is of type all we should check if all + * required field have been configured. + */ + const showMappingWarning = + connectorType === SwimlaneConnectorType.Cases || !hasRuleName || !hasAlertId; + + const editSubActionProperty = useCallback( + (key: string, value: any) => { + if (key === 'comments') { + return editAction('subActionParams', { incident, comments: value }, index); + } + + return editAction( + 'subActionParams', + { + incident: { ...incident, [key]: value }, + comments, + }, + index + ); + }, + [editAction, incident, comments, index] + ); + + const editComment = useCallback( + (key, value) => { + if (value.length > 0) { + editSubActionProperty(key, [{ commentId: '1', comment: value }]); + } + }, + [editSubActionProperty] + ); + + useEffect(() => { + if (actionConnector != null && actionConnectorRef.current !== actionConnector.id) { + actionConnectorRef.current = actionConnector.id; + editAction( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionConnector]); + + useEffect(() => { + if (!actionParams.subAction) { + editAction('subAction', 'pushToService', index); + } + + if (!actionParams.subActionParams) { + editAction( + 'subActionParams', + { + incident: { alertId: '{{alert.id}}', ruleName: '{{rule.name}}' }, + comments: [], + }, + index + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [actionParams]); + + return !showMappingWarning ? ( + <> + {hasSeverity && ( + <> + + + + + + )} + {hasComments && ( + 0 ? comments[0].comment : undefined} + label={i18n.SW_COMMENTS_FIELD_LABEL} + /> + )} + + ) : ( + + {i18n.EMPTY_MAPPING_WARNING_DESC} + + ); +}; + +// eslint-disable-next-line import/no-default-export +export { SwimlaneParamsFields as default }; diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts new file mode 100644 index 00000000000000..726997cb4456a3 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/translations.ts @@ -0,0 +1,282 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { i18n } from '@kbn/i18n'; + +export const SW_SELECT_MESSAGE_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.selectMessageText', + { + defaultMessage: 'Create record in Swimlane', + } +); + +export const SW_ACTION_TYPE_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.actionTypeTitle', + { + defaultMessage: 'Create Swimlane Record', + } +); + +export const SW_REQUIRED_RULE_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredRuleName', + { + defaultMessage: 'Rule name is required.', + } +); + +export const SW_REQUIRED_APP_ID_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAppIdText', + { + defaultMessage: 'An App ID is required.', + } +); + +export const SW_REQUIRED_FIELD_MAPPINGS_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredFieldMappingsText', + { + defaultMessage: 'Field mappings are required.', + } +); + +export const SW_REQUIRED_API_TOKEN_TEXT = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredApiTokenText', + { + defaultMessage: 'An API token is required.', + } +); + +export const SW_GET_APPLICATION_API_ERROR = (id: string | null) => + i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationMessage', + { + defaultMessage: 'Unable to get application with id {id}', + values: { id }, + } + ); + +export const SW_GET_APPLICATION_API_NO_FIELDS_ERROR = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlane.unableToGetApplicationFieldsMessage', + { + defaultMessage: 'Unable to get application fields', + } +); + +export const SW_API_URL_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiUrlTextFieldLabel', + { + defaultMessage: 'API Url', + } +); + +export const SW_API_URL_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.requiredApiUrlTextField', + { + defaultMessage: 'URL is required.', + } +); + +export const SW_API_URL_INVALID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.invalidApiUrlTextField', + { + defaultMessage: 'URL is invalid.', + } +); + +export const SW_APP_ID_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.appIdTextFieldLabel', + { + defaultMessage: 'Application ID', + } +); + +export const SW_API_TOKEN_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.apiTokenTextFieldLabel', + { + defaultMessage: 'API Token', + } +); + +export const SW_MAPPING_TITLE_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingTitleTextFieldLabel', + { + defaultMessage: 'Configure Field Mappings', + } +); + +export const SW_ALERT_SOURCE_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceFieldLabel', + { + defaultMessage: 'Alert source', + } +); + +export const SW_SEVERITY_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.severityFieldLabel', + { + defaultMessage: 'Severity', + } +); + +export const SW_MAPPING_DESCRIPTION_TEXT_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingDescriptionTextFieldLabel', + { + defaultMessage: 'Used to specify the field names in the Swimlane Application', + } +); + +export const SW_RULE_NAME_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.ruleNameFieldLabel', + { + defaultMessage: 'Rule name', + } +); + +export const SW_ALERT_ID_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertIdFieldLabel', + { + defaultMessage: 'Alert ID', + } +); + +export const SW_CASE_ID_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.caseIdFieldLabel', + { + defaultMessage: 'Case ID', + } +); + +export const SW_CASE_NAME_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.caseNameFieldLabel', + { + defaultMessage: 'Case name', + } +); + +export const SW_COMMENTS_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.commentsFieldLabel', + { + defaultMessage: 'Comments', + } +); + +export const SW_DESCRIPTION_FIELD_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.descriptionFieldLabel', + { + defaultMessage: 'Description', + } +); + +export const SW_REMEMBER_VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.rememberValueLabel', + { defaultMessage: 'Remember this value. You must reenter it each time you edit the connector.' } +); + +export const SW_REENTER_VALUE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.reenterValueLabel', + { defaultMessage: 'This key is encrypted. Please reenter a value for this field.' } +); + +export const SW_CONFIGURE_CONNECTION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureConnectionLabel', + { defaultMessage: 'Configure API Connection' } +); + +export const SW_RETRIEVE_CONFIGURATION_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.retrieveConfigurationLabel', + { defaultMessage: 'Configure Fields' } +); + +export const SW_CONFIGURE_API_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.configureAPILabel', + { defaultMessage: 'Configure API' } +); + +export const SW_CONNECTOR_TYPE_LABEL = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.connectorType', + { + defaultMessage: 'Connector Type', + } +); + +export const SW_FIELD_MAPPING_IS_REQUIRED = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.mappingFieldRequired', + { + defaultMessage: 'Field mapping is required.', + } +); + +export const EMPTY_MAPPING_WARNING_TITLE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningTitle', + { + defaultMessage: 'This connector has missing field mappings', + } +); + +export const EMPTY_MAPPING_WARNING_DESC = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.emptyMappingWarningDesc', + { + defaultMessage: + 'This connector cannot be selected because it is missing the required case field mappings. You can edit this connector to add required field mappings or select a connector of type Alerts.', + } +); + +export const SW_REQUIRED_ALERT_SOURCE = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertSource', + { + defaultMessage: 'Alert source is required.', + } +); + +export const SW_REQUIRED_SEVERITY = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredSeverity', + { + defaultMessage: 'Severity is required.', + } +); + +export const SW_REQUIRED_CASE_NAME = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseName', + { + defaultMessage: 'Case name is required.', + } +); + +export const SW_REQUIRED_CASE_ID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredCaseID', + { + defaultMessage: 'Case ID is required.', + } +); + +export const SW_REQUIRED_COMMENTS = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredComments', + { + defaultMessage: 'Comments are required.', + } +); + +export const SW_REQUIRED_DESCRIPTION = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredDescription', + { + defaultMessage: 'Description is required.', + } +); + +export const SW_REQUIRED_ALERT_ID = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.error.requiredAlertID', + { + defaultMessage: 'Alert ID is required.', + } +); + +export const SW_ALERT_SOURCE_TOOLTIP = i18n.translate( + 'xpack.triggersActionsUI.components.builtinActionTypes.swimlaneAction.alertSourceTooltip', + { + defaultMessage: 'The index of the alert. Use {index} in Detections.', + values: { index: '{{context.rule.output_index}}' }, + } +); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts new file mode 100644 index 00000000000000..f0a54e8b6c3bfe --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/types.ts @@ -0,0 +1,56 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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. + */ + +/* eslint-disable @kbn/eslint/no-restricted-paths */ + +import { UserConfiguredActionConnector } from '../../../../types'; +import { + ExecutorSubActionPushParams, + MappingConfigType, +} from '../../../../../../actions/server/builtin_action_types/swimlane/types'; + +export type SwimlaneActionConnector = UserConfiguredActionConnector< + SwimlaneConfig, + SwimlaneSecrets +>; + +export interface SwimlaneConfig { + apiUrl: string; + appId: string; + connectorType: SwimlaneConnectorType; + mappings: SwimlaneMappingConfig; +} + +export type MappingConfigurationKeys = keyof MappingConfigType; +export type SwimlaneMappingConfig = Record; + +export interface SwimlaneFieldMappingConfig { + id: string; + key: string; + name: string; + fieldType: string; +} + +export interface SwimlaneSecrets { + apiToken: string; +} + +export interface SwimlaneActionParams { + subAction: string; + subActionParams: ExecutorSubActionPushParams; +} + +export interface SwimlaneFieldMap { + key: string; + name: string; +} + +export enum SwimlaneConnectorType { + All = 'all', + Alerts = 'alerts', + Cases = 'cases', +} diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx new file mode 100644 index 00000000000000..4744c4d22fdc93 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.test.tsx @@ -0,0 +1,180 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useKibana } from '../../../../common/lib/kibana'; +import { getApplication } from './api'; +import { SwimlaneActionConnector } from './types'; +import { useGetApplication, UseGetApplication } from './use_get_application'; + +jest.mock('./api'); +jest.mock('../../../../common/lib/kibana'); + +const useKibanaMock = useKibana as jest.Mocked; +const getApplicationMock = getApplication as jest.Mock; + +const action = { + secrets: { apiToken: 'token' }, + id: 'test', + actionTypeId: '.swimlane', + name: 'Swimlane', + isPreconfigured: false, + config: { + apiUrl: 'https://test.swimlane.com/', + appId: 'bcq16kdTbz5jlwM6h', + mappings: {}, + }, +} as SwimlaneActionConnector; + +describe('useGetApplication', () => { + const { services } = useKibanaMock(); + getApplicationMock.mockResolvedValue({ + data: { fields: [] }, + }); + const abortCtrl = new AbortController(); + + beforeEach(() => { + jest.clearAllMocks(); + jest.restoreAllMocks(); + }); + + it('init', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + }); + }); + + it('calls getApplication with correct arguments', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + + result.current.getApplication(); + await waitForNextUpdate(); + expect(getApplicationMock).toBeCalledWith({ + signal: abortCtrl.signal, + appId: action.config.appId, + apiToken: action.secrets.apiToken, + url: action.config.apiUrl, + }); + }); + }); + + it('get application', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + result.current.getApplication(); + await waitForNextUpdate(); + + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + }); + }); + + it('set isLoading to true when getting the application', async () => { + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + + await waitForNextUpdate(); + result.current.getApplication(); + + expect(result.current.isLoading).toBe(true); + }); + }); + + it('it displays an error when http throws an error', async () => { + getApplicationMock.mockImplementation(() => { + throw new Error('Something went wrong'); + }); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + await waitForNextUpdate(); + result.current.getApplication(); + + expect(result.current).toEqual({ + isLoading: false, + getApplication: result.current.getApplication, + }); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Unable to get application with id bcq16kdTbz5jlwM6h', + text: 'Something went wrong', + }); + }); + }); + + it('it displays an error when the response does not contain the correct fields', async () => { + getApplicationMock.mockResolvedValue({}); + + await act(async () => { + const { result, waitForNextUpdate } = renderHook(() => + useGetApplication({ + appId: action.config.appId, + apiToken: action.secrets.apiToken, + apiUrl: action.config.apiUrl, + toastNotifications: services.notifications.toasts, + }) + ); + await waitForNextUpdate(); + result.current.getApplication(); + await waitForNextUpdate(); + + expect(services.notifications.toasts.addDanger).toHaveBeenCalledWith({ + title: 'Unable to get application with id bcq16kdTbz5jlwM6h', + text: 'Unable to get application fields', + }); + }); + }); +}); diff --git a/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx new file mode 100644 index 00000000000000..f18770067b8a86 --- /dev/null +++ b/x-pack/plugins/triggers_actions_ui/public/application/components/builtin_action_types/swimlane/use_get_application.tsx @@ -0,0 +1,82 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { useState, useCallback, useRef } from 'react'; +import { ToastsApi } from 'kibana/public'; +import { getApplication as getApplicationApi } from './api'; +import * as i18n from './translations'; +import { SwimlaneFieldMappingConfig } from './types'; + +interface Props { + toastNotifications: Pick< + ToastsApi, + 'get$' | 'add' | 'remove' | 'addSuccess' | 'addWarning' | 'addDanger' | 'addError' + >; + appId: string; + apiToken: string; + apiUrl: string; +} + +export interface UseGetApplication { + getApplication: () => Promise<{ fields?: SwimlaneFieldMappingConfig[] } | undefined>; + isLoading: boolean; +} + +export const useGetApplication = ({ + toastNotifications, + appId, + apiToken, + apiUrl, +}: Props): UseGetApplication => { + const [isLoading, setIsLoading] = useState(false); + const isCancelledRef = useRef(false); + const abortCtrlRef = useRef(new AbortController()); + + const getApplication = useCallback(async () => { + try { + isCancelledRef.current = false; + abortCtrlRef.current.abort(); + abortCtrlRef.current = new AbortController(); + setIsLoading(true); + + const data = await getApplicationApi({ + signal: abortCtrlRef.current.signal, + appId, + apiToken, + url: apiUrl, + }); + + if (!isCancelledRef.current) { + setIsLoading(false); + if (!data.fields) { + // If the response was malformed and fields doesn't exist, show an error toast + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), + text: i18n.SW_GET_APPLICATION_API_NO_FIELDS_ERROR, + }); + return; + } + return data; + } + } catch (error) { + if (!isCancelledRef.current) { + if (error.name !== 'AbortError') { + toastNotifications.addDanger({ + title: i18n.SW_GET_APPLICATION_API_ERROR(appId), + text: error.message, + }); + } + setIsLoading(false); + } + } + }, [apiToken, apiUrl, appId, toastNotifications]); + + return { + isLoading, + getApplication, + }; +}; diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts new file mode 100644 index 00000000000000..95e041bbeb03a7 --- /dev/null +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/builtin_action_types/swimlane.ts @@ -0,0 +1,91 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { + ExternalServiceSimulator, + getExternalServiceSimulatorPath, +} from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function swimlaneTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const kibanaServer = getService('kibanaServer'); + const mockSwimlane = { + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: 'http://swimlane.mynonexistent.co', + appId: '123456asdf', + connectorType: 'all', + mappings: { + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + ruleNameConfig: { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'text', + }, + }, + }, + secrets: { + apiToken: 'swimlane-api-key', + }, + }; + + describe('swimlane', () => { + let swimlaneSimulatorURL: string = ''; + + // need to wait for kibanaServer to settle ... + before(() => { + swimlaneSimulatorURL = kibanaServer.resolveUrl( + getExternalServiceSimulatorPath(ExternalServiceSimulator.SWIMLANE) + ); + }); + it('should return 403 when creating a swimlane action', async () => { + await supertest + .post('/api/actions/action') + .set('kbn-xsrf', 'foo') + .send({ + ...mockSwimlane, + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + }) + .expect(403, { + statusCode: 403, + error: 'Forbidden', + message: + 'Action type .swimlane is disabled because your basic license does not support it. Please upgrade your license.', + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts index 3f0524750d5f88..21cb0db3057bbe 100644 --- a/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/basic/tests/actions/index.ts @@ -14,6 +14,7 @@ export default function actionsTests({ loadTestFile }: FtrProviderContext) { loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/jira')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); loadTestFile(require.resolve('./builtin_action_types/slack')); diff --git a/x-pack/test/alerting_api_integration/common/config.ts b/x-pack/test/alerting_api_integration/common/config.ts index 61b452fc118358..3dcbde5f21149a 100644 --- a/x-pack/test/alerting_api_integration/common/config.ts +++ b/x-pack/test/alerting_api_integration/common/config.ts @@ -31,6 +31,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.server-log', '.servicenow', '.jira', diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts index 878507bcf4afc3..a479070c824f23 100644 --- a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/plugin.ts @@ -13,6 +13,7 @@ import { PluginSetupContract as FeaturesPluginSetup } from '../../../../../../.. import { PluginSetupContract as ActionsPluginSetupContract } from '../../../../../../../plugins/actions/server/plugin'; import { ActionType } from '../../../../../../../plugins/actions/server'; import { initPlugin as initPagerduty } from './pagerduty_simulation'; +import { initPlugin as initSwimlane } from './swimlane_simulation'; import { initPlugin as initServiceNow } from './servicenow_simulation'; import { initPlugin as initJira } from './jira_simulation'; import { initPlugin as initResilient } from './resilient_simulation'; @@ -23,6 +24,7 @@ export const NAME = 'actions-FTS-external-service-simulators'; export enum ExternalServiceSimulator { PAGERDUTY = 'pagerduty', + SWIMLANE = 'swimlane', SERVICENOW = 'servicenow', SLACK = 'slack', JIRA = 'jira', @@ -66,6 +68,10 @@ export async function getSlackServer(): Promise { return await initSlack(); } +export async function getSwimlaneServer(): Promise { + return await initSwimlane(); +} + interface FixtureSetupDeps { actions: ActionsPluginSetupContract; features: FeaturesPluginSetup; diff --git a/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts new file mode 100644 index 00000000000000..afba550908ddcd --- /dev/null +++ b/x-pack/test/alerting_api_integration/common/fixtures/plugins/actions_simulators/server/swimlane_simulation.ts @@ -0,0 +1,39 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 http from 'http'; + +export const initPlugin = async () => http.createServer(handler); + +const sendResponse = (response: http.ServerResponse, data: any) => { + response.statusCode = 200; + response.setHeader('Content-Type', 'application/json'); + response.end(JSON.stringify(data, null, 4)); +}; + +const handler = (request: http.IncomingMessage, response: http.ServerResponse) => { + if (request.method === 'POST') { + return sendResponse(response, { + id: 'wowzeronza', + name: 'ET-69', + createdDate: '2021-06-01T17:29:51.092Z', + }); + } + + if (request.method === 'PATCH') { + return sendResponse(response, { + id: 'wowzeronza', + name: 'ET-69', + modifiedDate: '2021-06-01T17:29:51.092Z', + }); + } + + // Return an 400 error if http method is not supported + response.statusCode = 400; + response.setHeader('Content-Type', 'application/json'); + response.end('Not supported http method to request slack simulator'); +}; diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts new file mode 100644 index 00000000000000..92e99a9d504f31 --- /dev/null +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/builtin_action_types/swimlane.ts @@ -0,0 +1,482 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. 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 httpProxy from 'http-proxy'; +import expect from '@kbn/expect'; +import getPort from 'get-port'; +import http from 'http'; + +import { getHttpProxyServer } from '../../../../common/lib/get_proxy_server'; +import { FtrProviderContext } from '../../../../common/ftr_provider_context'; +import { getSwimlaneServer } from '../../../../common/fixtures/plugins/actions_simulators/server/plugin'; + +// eslint-disable-next-line import/no-default-export +export default function swimlaneTest({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const configService = getService('config'); + + const mockSwimlane = { + name: 'A swimlane action', + actionTypeId: '.swimlane', + config: { + apiUrl: 'http://swimlane.mynonexistent.com', + appId: '123456asdf', + connectorType: 'all', + mappings: { + alertIdConfig: { + id: 'ednjls', + name: 'Alert id', + key: 'alert-id', + fieldType: 'text', + }, + severityConfig: { + id: 'adnlas', + name: 'Severity', + key: 'severity', + fieldType: 'text', + }, + ruleNameConfig: { + id: 'adnfls', + name: 'Rule Name', + key: 'rule-name', + fieldType: 'text', + }, + caseIdConfig: { + id: 'a6sst', + name: 'Case Id', + key: 'case-id-name', + fieldType: 'text', + }, + caseNameConfig: { + id: 'a6fst', + name: 'Case Name', + key: 'case-name', + fieldType: 'text', + }, + commentsConfig: { + id: 'a6fdf', + name: 'Comments', + key: 'comments', + fieldType: 'notes', + }, + descriptionConfig: { + id: 'a6fdf', + name: 'Description', + key: 'description', + fieldType: 'text', + }, + }, + }, + secrets: { + apiToken: 'swimlane-api-key', + }, + params: { + subAction: 'pushToService', + subActionParams: { + incident: { + alertId: 'fs345f78g', + ruleName: 'Rule Name', + severity: 'Critical', + caseName: 'Case Name', + caseId: 'es3456789', + description: 'This is a description', + externalId: null, + }, + comments: [ + { + comment: 'first comment', + commentId: '123', + }, + ], + }, + }, + }; + + describe('Swimlane', () => { + let simulatedActionId = ''; + let swimlaneSimulatorURL: string = ''; + let swimlaneServer: http.Server; + let proxyServer: httpProxy | undefined; + let proxyHaveBeenCalled = false; + + before(async () => { + swimlaneServer = await getSwimlaneServer(); + const availablePort = await getPort({ port: getPort.makeRange(9000, 9100) }); + if (!swimlaneServer.listening) { + swimlaneServer.listen(availablePort); + } + swimlaneSimulatorURL = `http://localhost:${availablePort}`; + proxyServer = await getHttpProxyServer( + swimlaneSimulatorURL, + configService.get('kbnTestServer.serverArgs'), + () => { + proxyHaveBeenCalled = true; + } + ); + }); + + after(() => { + swimlaneServer.close(); + if (proxyServer) { + proxyServer.close(); + } + }); + + describe('Swimlane - Action Creation', () => { + it('should return 200 when creating a swimlane action successfully', async () => { + const { body: createdAction } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }) + .expect(200); + + expect(createdAction).to.eql({ + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + connector_type_id: '.swimlane', + id: createdAction.id, + is_missing_secrets: false, + is_preconfigured: false, + name: 'A swimlane action', + }); + + expect(typeof createdAction.id).to.be('string'); + + const { body: fetchedAction } = await supertest + .get(`/api/actions/connector/${createdAction.id}`) + .expect(200); + + expect(fetchedAction).to.eql({ + id: fetchedAction.id, + is_preconfigured: false, + is_missing_secrets: false, + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + }); + }); + + it('should respond with a 400 Bad Request when creating a swimlane action with no apiUrl', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + appId: mockSwimlane.config.appId, + mappings: mockSwimlane.config.mappings, + }, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [apiUrl]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a swimlane action with no appId', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + mappings: mockSwimlane.config.mappings, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type config: [appId]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request when creating a swimlane action without secrets', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: {}, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: + 'error validating action type secrets: [apiToken]: expected value of type [string] but got [undefined]', + }); + }); + }); + + it('should respond with a 400 Bad Request default swimlane url is not present in allowedHosts', async () => { + await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane action', + connector_type_id: '.swimlane', + config: mockSwimlane.config, + secrets: mockSwimlane.secrets, + }) + .expect(400) + .then((resp: any) => { + expect(resp.body).to.eql({ + statusCode: 400, + error: 'Bad Request', + message: `error validating action type config: error configuring connector action: target url "${mockSwimlane.config.apiUrl}" is not added to the Kibana config xpack.actions.allowedHosts`, + }); + }); + }); + }); + + describe('Swimlane - Executor', () => { + before(async () => { + const { body } = await supertest + .post('/api/actions/connector') + .set('kbn-xsrf', 'foo') + .send({ + name: 'A swimlane simulator', + connector_type_id: '.swimlane', + config: { + ...mockSwimlane.config, + apiUrl: swimlaneSimulatorURL, + }, + secrets: mockSwimlane.secrets, + }); + simulatedActionId = body.id; + }); + + describe('Validation', () => { + it('should handle failing with a simulated success without action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: {}, + }) + .then((resp: any) => { + expect(Object.keys(resp.body)).to.eql(['status', 'message', 'retry', 'connector_id']); + expect(resp.body.connector_id).to.eql(simulatedActionId); + expect(resp.body.status).to.eql('error'); + expect(resp.body.retry).to.eql(false); + // Node.js 12 oddity: + // + // The first time after the server is booted, the error message will be: + // + // undefined is not iterable (cannot read property Symbol(Symbol.iterator)) + // + // After this, the error will be: + // + // Cannot destructure property 'value' of 'undefined' as it is undefined. + // + // The error seems to come from the exact same place in the code based on the + // exact same circomstances: + // + // https://github.com/elastic/kibana/blob/b0a223ebcbac7e404e8ae6da23b2cc6a4b509ff1/packages/kbn-config-schema/src/types/literal_type.ts#L28 + // + // What triggers the error is that the `handleError` function expects its 2nd + // argument to be an object containing a `valids` property of type array. + // + // In this test the object does not contain a `valids` property, so hence the + // error. + // + // Why the error message isn't the same in all scenarios is unknown to me and + // could be a bug in V8. + expect(resp.body.message).to.match( + /^error validating action params: (undefined is not iterable \(cannot read property Symbol\(Symbol.iterator\)\)|Cannot destructure property 'value' of 'undefined' as it is undefined\.)$/ + ); + }); + }); + + it('should handle failing with a simulated success without unsupported action', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'non-supported' }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subAction]: expected value to equal [pushToService]', + }); + }); + }); + + /** + * All subActionParams are optional. + * If subActionParams is not provided all + * the subActionParams attributes will be set to null + * and the validation will succeed. For that reason, + * the subActionParams need to be set to null. + */ + it('should handle failing with a simulated success without subActionParams', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { subAction: 'pushToService', subActionParams: null }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams]: expected a plain object value, but found [null] instead.', + }); + }); + }); + + it('should handle failing with a simulated success without commentId', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [{ comment: 'comment' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.comments]: types that failed validation:\n- [subActionParams.comments.0.0.commentId]: expected value of type [string] but got [undefined]\n- [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + + it('should handle failing with a simulated success without comment message', async () => { + await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [{ commentId: 'success' }], + }, + }, + }) + .then((resp: any) => { + expect(resp.body).to.eql({ + connector_id: simulatedActionId, + status: 'error', + retry: false, + message: + 'error validating action params: [subActionParams.comments]: types that failed validation:\n- [subActionParams.comments.0.0.comment]: expected value of type [string] but got [undefined]\n- [subActionParams.comments.1]: expected value to equal [null]', + }); + }); + }); + }); + + describe('Execution', () => { + it('should handle creating an incident', async () => { + const { body } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + ...mockSwimlane.params.subActionParams, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: 'wowzeronza', + title: 'ET-69', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, + }, + }); + }); + + it('should handle updating an incident', async () => { + const { body } = await supertest + .post(`/api/actions/connector/${simulatedActionId}/_execute`) + .set('kbn-xsrf', 'foo') + .send({ + params: { + ...mockSwimlane.params, + subActionParams: { + incident: { + ...mockSwimlane.params.subActionParams.incident, + externalId: 'wowzeronza', + }, + comments: [], + }, + }, + }) + .expect(200); + + expect(proxyHaveBeenCalled).to.equal(true); + expect(body).to.eql({ + status: 'ok', + connector_id: simulatedActionId, + data: { + id: 'wowzeronza', + title: 'ET-69', + pushedDate: '2021-06-01T17:29:51.092Z', + url: `${swimlaneSimulatorURL}/record/123456asdf/wowzeronza`, + }, + }); + }); + }); + }); + }); +} diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts index b5ff287ac58f6f..db57af0ba1a98d 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/tests/actions/index.ts @@ -23,6 +23,7 @@ export default function actionsTests({ loadTestFile, getService }: FtrProviderCo loadTestFile(require.resolve('./builtin_action_types/es_index')); loadTestFile(require.resolve('./builtin_action_types/es_index_preconfigured')); loadTestFile(require.resolve('./builtin_action_types/pagerduty')); + loadTestFile(require.resolve('./builtin_action_types/swimlane')); loadTestFile(require.resolve('./builtin_action_types/server_log')); loadTestFile(require.resolve('./builtin_action_types/servicenow')); loadTestFile(require.resolve('./builtin_action_types/jira')); diff --git a/x-pack/test/case_api_integration/common/config.ts b/x-pack/test/case_api_integration/common/config.ts index 6c81f1fcfa2640..887e6e7894f98e 100644 --- a/x-pack/test/case_api_integration/common/config.ts +++ b/x-pack/test/case_api_integration/common/config.ts @@ -26,6 +26,7 @@ const enabledActionTypes = [ '.index', '.jira', '.pagerduty', + '.swimlane', '.resilient', '.server-log', '.servicenow', diff --git a/x-pack/test/detection_engine_api_integration/common/config.ts b/x-pack/test/detection_engine_api_integration/common/config.ts index 5cbf9598dc4a14..ef822b0af2a290 100644 --- a/x-pack/test/detection_engine_api_integration/common/config.ts +++ b/x-pack/test/detection_engine_api_integration/common/config.ts @@ -20,6 +20,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.server-log', '.servicenow', '.slack', diff --git a/x-pack/test/functional_with_es_ssl/config.ts b/x-pack/test/functional_with_es_ssl/config.ts index 3ed382053f561f..b8010c089ad03f 100644 --- a/x-pack/test/functional_with_es_ssl/config.ts +++ b/x-pack/test/functional_with_es_ssl/config.ts @@ -16,6 +16,7 @@ const enabledActionTypes = [ '.email', '.index', '.pagerduty', + '.swimlane', '.servicenow', '.slack', '.webhook',