From c8fadae515561af81259ad13a570b978108a867f Mon Sep 17 00:00:00 2001 From: Matteo Lodi <30625432+mlodic@users.noreply.github.com> Date: Tue, 21 May 2024 18:09:48 +0200 Subject: [PATCH 01/17] fix guide --- frontend/src/components/GuideWrapper.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/GuideWrapper.jsx b/frontend/src/components/GuideWrapper.jsx index 32ccd1ce27..db9a540814 100644 --- a/frontend/src/components/GuideWrapper.jsx +++ b/frontend/src/components/GuideWrapper.jsx @@ -19,7 +19,7 @@ export default function GuideWrapper() { questions you could either check out our{" "} docs or reach us out on{" "} - + the official IntelOwl slack channel

From 1ba910bf320fee7af3f58708ad79a944242f1283 Mon Sep 17 00:00:00 2001 From: Martina Carella Date: Wed, 22 May 2024 12:15:13 +0200 Subject: [PATCH 02/17] Job reports improvements (#2337) * adjusted job reports * fix plugin icon id * fix visualizer description --- .../components/jobs/result/JobInfoIcon.jsx | 1 - .../components/jobs/result/JobOverview.jsx | 16 ++++++----- .../src/components/jobs/result/JobResult.jsx | 27 ++++++++++--------- .../jobs/result/pluginReportTables.jsx | 13 ++++++--- frontend/src/stores/useJobOverviewStore.jsx | 20 -------------- 5 files changed, 34 insertions(+), 43 deletions(-) delete mode 100644 frontend/src/stores/useJobOverviewStore.jsx diff --git a/frontend/src/components/jobs/result/JobInfoIcon.jsx b/frontend/src/components/jobs/result/JobInfoIcon.jsx index 4bd719fd86..ff0fb80ce8 100644 --- a/frontend/src/components/jobs/result/JobInfoIcon.jsx +++ b/frontend/src/components/jobs/result/JobInfoIcon.jsx @@ -38,5 +38,4 @@ export function JobInfoIcon({ job }) { JobInfoIcon.propTypes = { job: PropTypes.object.isRequired, - countryInfo: PropTypes.object.isRequired, }; diff --git a/frontend/src/components/jobs/result/JobOverview.jsx b/frontend/src/components/jobs/result/JobOverview.jsx index a77e24cae7..bc90d127eb 100644 --- a/frontend/src/components/jobs/result/JobOverview.jsx +++ b/frontend/src/components/jobs/result/JobOverview.jsx @@ -221,9 +221,11 @@ export function JobOverview({ const location = useLocation(); const [UIElements, setUIElements] = useState([]); console.debug( - `location pathname: ${location.pathname}, state: ${JSON.stringify( - location?.state, - )}`, + `location pathname: ${ + location.pathname + }, state - userChanged: ${JSON.stringify( + location?.state?.userChanged, + )}, state - jobReport: #${location?.state?.jobReport.id}`, ); useEffect(() => { @@ -395,7 +397,7 @@ export function JobOverview({ `/jobs/${job.id}/${ JobResultSections.VISUALIZER }/${encodeURIComponent(UIElements[0].name)}`, - { state: { userChanged: true } }, + { state: { userChanged: true, jobReport: job } }, ) } > @@ -408,7 +410,7 @@ export function JobOverview({ onClick={() => navigate( `/jobs/${job.id}/${JobResultSections.RAW}/${rawElements[0].name}`, - { state: { userChanged: true } }, + { state: { userChanged: true, jobReport: job } }, ) } > @@ -440,7 +442,9 @@ export function JobOverview({ }/${section}/${encodeURIComponent( componentsObject.name, )}`, - { state: { userChanged: true } }, + { + state: { userChanged: true, jobReport: job }, + }, ) } > diff --git a/frontend/src/components/jobs/result/JobResult.jsx b/frontend/src/components/jobs/result/JobResult.jsx index 48efd1b086..c57bf4fbe4 100644 --- a/frontend/src/components/jobs/result/JobResult.jsx +++ b/frontend/src/components/jobs/result/JobResult.jsx @@ -1,9 +1,9 @@ import React, { useEffect } from "react"; import useTitle from "react-use/lib/useTitle"; -import { useParams } from "react-router-dom"; +import { useParams, useLocation } from "react-router-dom"; import { Loader } from "@certego/certego-ui"; -import axios from "axios"; +import useAxios from "axios-hooks"; import { WEBSOCKET_JOBS_URI, JOB_BASE_URI } from "../../../constants/apiURLs"; import { JobOverview } from "./JobOverview"; @@ -17,9 +17,10 @@ import { JobFinalStatuses } from "../../../constants/jobConst"; export default function JobResult() { console.debug("JobResult rendered!"); + // state + const location = useLocation(); const [initialLoading, setInitialLoading] = React.useState(true); - const [initialError, setInitialError] = React.useState(""); - const [job, setJob] = React.useState(undefined); + const [job, setJob] = React.useState(location.state?.jobReport || undefined); // this state var is used to check if we notified the user, in this way we avoid to notify more than once const [notified, setNotified] = React.useState(false); // this state var is used to check if the user changed page, in case he waited the result on the page we avoid the notification @@ -53,7 +54,11 @@ export default function JobResult() { `notified: ${notified}, toNotify: ${toNotify}`, ); - const getJob = () => axios.get(`${JOB_BASE_URI}/${jobId}`); + // useAxios caches the request by default + const [{ data: respData, loading, error }, refetchJob] = useAxios({ + url: `${JOB_BASE_URI}/${jobId}`, + }); + useEffect(() => { /* INITIAL SETUP: - add a focus listener: @@ -66,12 +71,10 @@ export default function JobResult() { setToNotify(false); }); window.addEventListener("blur", () => setToNotify(true)); - getJob() - .then((response) => setJob(response.data)) - .catch((err) => setInitialError(err)) - .finally((_) => setInitialLoading(false)); + if (!job && respData && !loading && error == null) setJob(respData); + if (!loading) setInitialLoading(false); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [loading]); // page title useTitle( @@ -133,12 +136,12 @@ export default function JobResult() { return ( ( diff --git a/frontend/src/components/jobs/result/pluginReportTables.jsx b/frontend/src/components/jobs/result/pluginReportTables.jsx index 2a4248b6d6..79be3188f7 100644 --- a/frontend/src/components/jobs/result/pluginReportTables.jsx +++ b/frontend/src/components/jobs/result/pluginReportTables.jsx @@ -19,7 +19,7 @@ import { import { StatusTag } from "../../common/StatusTag"; import { killPlugin, retryPlugin } from "./jobApi"; -import { PluginStatuses } from "../../../constants/pluginConst"; +import { PluginStatuses, PluginsTypes } from "../../../constants/pluginConst"; import { markdownToHtml } from "../../common/markdownToHtml"; const tableProps = { @@ -83,12 +83,12 @@ const tableProps = {
{value}
{ pluginsStored.forEach((plugin) => { - if (plugin.name === report.name) { + if ( + (report.type !== PluginsTypes.VISUALIZER && + plugin.name === report.name) || + (report.type === PluginsTypes.VISUALIZER && + plugin.name === report.config) + ) { reports[index].description = plugin.description; } }); diff --git a/frontend/src/stores/useJobOverviewStore.jsx b/frontend/src/stores/useJobOverviewStore.jsx deleted file mode 100644 index c9f2061e3a..0000000000 --- a/frontend/src/stores/useJobOverviewStore.jsx +++ /dev/null @@ -1,20 +0,0 @@ -/* This store is used to save the data about the section selected by the user in the JobOverview (raw or UI). - -This store is required because we have a problem with the rendering logic for the jobs: -JobResult correctly handle the polling and the rendering of the job data (when some data are available). -JobOverview need to wrap the job's metadata, the UI and the raw format. -Unfortunately in case the jobs require some time to be completed the JobResult perform other requests and this lead to a re-render of JobOverview. -If the section(UI/raw) selection is stored in JobOverview's state, for each request the preference is resetted: -we need to move the user selection preference outside the component (here). -*/ - -import { create } from "zustand"; - -export const useJobOverviewStore = create((set) => ({ - isSelectedUI: true, - activeElement: undefined, - setIsSelectedUI: (isSelectedUI) => set(() => ({ isSelectedUI })), - setActiveElement: (activeElement) => set(() => ({ activeElement })), - resetJobOverview: () => - set(() => ({ isSelectedUI: true, activeElement: undefined })), -})); From 30ddcbadcc08973554eaa76cdd70b06e60a4204a Mon Sep 17 00:00:00 2001 From: Martina Carella Date: Wed, 22 May 2024 14:44:34 +0200 Subject: [PATCH 03/17] Visualizer Framework - Table element (#2319) * added VisualizableTable * added size field * adjusted VerticalList * added filters * adjusted VisualizableTable * backend tests * frontend tests * prettier * changes * changes --- api_app/visualizers_manager/classes.py | 59 ++++++++- docs/source/Contribute.md | 5 + docs/static/visualizableTable_example.png | Bin 0 -> 49559 bytes .../jobs/result/visualizer/elements/const.js | 1 + .../jobs/result/visualizer/elements/table.jsx | 79 ++++++++++++ .../visualizer/elements/verticalList.jsx | 79 +++++++----- .../jobs/result/visualizer/validators.js | 26 +++- .../jobs/result/visualizer/visualizer.jsx | 33 ++++- frontend/src/styles/App.scss | 4 + .../result/visualizer/elements/table.test.jsx | 118 ++++++++++++++++++ .../jobs/result/visualizer/validators.test.js | 67 ++++++++++ .../result/visualizer/visualizer.test.jsx | 33 ++++- .../visualizers_manager/test_classes.py | 73 +++++++++++ 13 files changed, 542 insertions(+), 35 deletions(-) create mode 100644 docs/static/visualizableTable_example.png create mode 100644 frontend/src/components/jobs/result/visualizer/elements/table.jsx create mode 100644 frontend/tests/components/jobs/result/visualizer/elements/table.test.jsx diff --git a/api_app/visualizers_manager/classes.py b/api_app/visualizers_manager/classes.py index 3e33948832..f86b0002b0 100644 --- a/api_app/visualizers_manager/classes.py +++ b/api_app/visualizers_manager/classes.py @@ -184,8 +184,8 @@ def to_dict(self) -> Dict: class VisualizableVerticalList(VisualizableListMixin, VisualizableObject): def __init__( self, - name: VisualizableBase, value: List[VisualizableObject], + name: VisualizableBase = None, start_open: bool = False, # noqa add_count_in_title: bool = True, fill_empty: bool = True, @@ -200,7 +200,7 @@ def __init__( alignment=alignment, disable=disable, ) - if add_count_in_title: + if name and add_count_in_title: name.value += f" ({len(value)})" for v in value: if isinstance(v, str): @@ -209,6 +209,8 @@ def __init__( ) if fill_empty and not value: value = [VisualizableBase(value="no data available", disable=False)] + if not name: + start_open = True self.value = value self.name = name self.add_count_in_title = add_count_in_title @@ -259,6 +261,58 @@ def type(self) -> str: return "vertical_list" +class VisualizableTable(VisualizableObject): + def __init__( + self, + columns: List[str], + data: List[Dict[str, VisualizableObject]], + size: VisualizableSize = VisualizableSize.S_AUTO, + alignment: VisualizableAlignment = VisualizableAlignment.AROUND, + page_size: int = 5, + disable_filters: bool = False, + disable_sort_by: bool = False, + ): + super().__init__(size=size, alignment=alignment, disable=False) + self.data = data + self.columns = columns + self.page_size = page_size + self.disable_filters = disable_filters + self.disable_sort_by = disable_sort_by + + @property + def attributes(self) -> List[str]: + return super().attributes + [ + "data", + "columns", + "page_size", + "disable_filters", + "disable_sort_by", + ] + + @property + def type(self) -> str: + return "table" + + def to_dict(self) -> Dict: + result = super().to_dict() + data: List[Dict[str, VisualizableObject]] = result.pop("data", []) + if any(x for x in data): + new_data = [] + for element in data: + new_data.append( + { + key: value.to_dict() + for [key, value] in element.items() + if value is not None + } + ) + result["data"] = new_data + else: + result["data"] = [] + result.pop("disable") + return result + + class VisualizableHorizontalList(VisualizableListMixin, VisualizableObject): def __init__( self, @@ -328,6 +382,7 @@ class Visualizer(Plugin, metaclass=abc.ABCMeta): Bool = VisualizableBool VList = VisualizableVerticalList HList = VisualizableHorizontalList + Table = VisualizableTable LevelSize = VisualizableLevelSize Page = VisualizablePage diff --git a/docs/source/Contribute.md b/docs/source/Contribute.md index 317e398fa5..dc199d2295 100644 --- a/docs/source/Contribute.md +++ b/docs/source/Contribute.md @@ -350,6 +350,11 @@ To do so, some utility classes have been made: A vertical list made of a name, a title, and the list of elements. Visualizable Vertical List Example + + VisualizableTable + A table of visualizable elements. In the example there is a table of base and vertical lists. + Visualizable Table Example + VisualizableBool The representation of a boolean value. It can be enabled or disabled with colors. diff --git a/docs/static/visualizableTable_example.png b/docs/static/visualizableTable_example.png new file mode 100644 index 0000000000000000000000000000000000000000..a139d694b3ee0cf357eaa2aaba3654e617255d55 GIT binary patch literal 49559 zcmdSBWn7!x(=JL&fl{E9V#Ny-DDG}8?i6>I;1*mfxVua7;_eQ`1Hm=H3GVJYJn#E| zf6v)p_P2e`Szqp7k~?$H%(`aQtZP;h@=ZY!;|<{(1Ox;OX(=&f1O((61caA@RE?!!NIZBGRg_;lum2X)ydXfwQ=#vx>dBvzw8V83Mr0-qwr}XyRmMW(TyecRoVw z5JEush#)QYRnuCS@?7#1BT1V#vX<(ymU{FTS!^VwJ z@5kTuBv}6*vsHIxbdR8s%zHN_eN?u^*K2l4Oj)Zl*)L{&}fe+zZ|{>J*> z5-8q&Gm`(egugH7N<;oFf#*vA=f5Rjq5R)aBJot9NwSV`)5G)M&yG>fr|HwH16ua| zZ&nVu^eyo26vjS<4nON2Pmj4=9N??o2mf+R2ihhv{CdMrm+DxKJo5iffBb)9QvQ!h zXwo&mpg6rUL)`_PftSI*kBjFNZMKrYOIuJgnt_(?v*w_yt)0v|Rz$<~bC~7*a;%AF zMeE>y0GSXUVrBa@Vy%e&TQoXkLr?a4`v81>@Xa-#ZeV{m!Vi}j0&2>mrix3?Un0UP zx`$&s=G3UTj}eT%waf@*LHMcT2u6#lcDwsfkXdWc6@sB_-LQ4}6^jSakZQF3XH*h# z3t<`FekVtRkFe6xQzt8#B-bJLT$6y$gKE#UGD&dBR47 zFlVdv>2K+#UzQJOD^0uJ}JescSo9LZs#W@lIrX$d1bQ!pimDOpg^=EjQ>y zomIT<%vMgn>&SZYsTesVI35iS_ZX+x!Qv;PLCTW`CtywS-1^hT)|31?7!qoi^xuI# zu>5UFU&!Z?N&z|^HSiHFgdhKSM3G49p`;)vV+kCG$^&SDBLaD)?o#hWcsn@6-L@-_XWJAfh&>178PYFsBhYwvbl$=>69jnzwdFGhLw;nnpvP zDV*}3r+P23Fo^#s!XxI^MFimw>9+_#Ut9JUxHJlnUvHAJYnOz)ddhbnC0a5t4Y^5dxU5Ylh0+I^p z6Iv@Xtk=tFZO`P^Kppcd zQj=Uq+1o{I+Fhe=eR~y_qf;WmQL2U^%#Ah!V*f-OwFze2OXlCg#5+*jl&LpX8hu}s z9uE|Id>guf>7;qzcObyVebC)*O=T9}Hacaw7^mYU{(e4>sqUf8ku&2UE@Zwc1qlc^ z-CHop!#|L&=U5GpK4Pg2od0pDHUDtHg6!v2DK*2QlJARxe7P%@l!$>^riE(&e|)KG z>{?_c@Xj2MF&&&EY`B|`E8 zWXr5z)MB>&xP;on9;cVj`qXptuzBUj@G%{g7e5iG^ zGLMYAa+?Cr^q&8uNoSeO;|@vYtuYBrXc2Krt7y~aBZ^0@fl4tyejGO_^BQHkU-*xZ zZSL*aF*GdYcvtfNL)_Q-TwIKFD`(@WW17a?Y>m}clM|}Ouk=5OYIB~VyFk|~ocft0 zVNG8RB--4C5%)Zd>0+bqe-XXMg+w#?ps+jNq6nOfvqi=$>#;-j*tTN|2^>{Ki*erS z@Oa!}z2B9TvXHfF-Bd7!`Spa6jTqmV*$m`7N0;(Va!K9lpk(I85A{bL+)z~W;#V`d zdWM1vE(hSYJ?hi_5|uf3*zU+A%N^%>W?`nGW|Fpha;Paz6y2-Y3K53~^K(LX6tGQ3 z*R@-RiTZwi(rmnv7(AIJ_yh+_d9WQ@-+f&6JO3mq5{5S&O1djIGH`cu9m91~GtH!D z`E@rP_+x0Y=Yg%_IkWpw#o2RI$Twz@iicT=e=s-Dh6;s(kF2|U6+M2@+1hOuy%-5) zF2)76dL7uc-XDIptJv&(Z3|=!j3gQC=x&u6;SaQa{gCCAfQl(RG)pGb^_wUZ{#auB z)4Y>TEN`JwswFC7z;*3fQ`FM>#SbHV-04t$gPx_Rvz{Q#@xiMJWOiBrJbkK)z}^M6 zpRz^OGHI*QcRLhFv}Ai+xI#}D|#~L$cIr8TFv0COM zGvX~yn;SeZT|i(>p_~o+>f^Du`IWiX40r5eqx#8ls` z6WkZXsLERwwXtxQQ5ie+LUB74^mnmlN+F%~xr2E`LQ`+;WskV(PJ}eRvFW1T^|J&r zdH+@`!n7BqL?y1^rC*iNpI8Sh0Wop%l2_M`NaS4sW!+8+zjy zn>Aq&SGnVk=Ec%snZd+`=G1V9<$?TuhNWEEOVjOLd5P!6RHe zk?5N&K3T}_GeY_{qkc_4A31`e*}=L)f#a0Z#BfFb$$ZIYhecFk6!r&kPlWCthpQti zC@3Jz0afPO>@Z3$`yAO8QJ)s%ckrge-*Kz>k`Md4owc~H_TfT=8CUmD7d-B+*LF~n zjagaln^_;*_$?Ec*KNR>BaMDF-RoezCsE_{k4AiLp}?t7S7PaNjS#o>#<&@Xu5tL%tw?+Nka&S+(p%dE@q zKkl)vI36AH@Aj&azxi<`f^yuqhdE9;<|IuSw=(f_iua8w9KZ>|f=P1$PpegKFswMG z%I*tBvf8F3ut7(FLqiQ}w?lFNZy}7Ewe^gEE9x3jvr_tImfA4c9^SWFFqu_=Xs6T#w)LPvIhBw_~}TnD(26YbL7iDklwTJsX#i z0rFw@@IA)|@h9qSb07ImrZ^r&nmUx-zf4|LXd!A*2EcIMYQCtJ7V?cZ?HrI|szpf? zqzw60CCTG3?lfIqOK|(4VY>C3YEAyZMw(;A+2g|B?i-sS%+|#f34K4?4{cWI2bgRg z^&3U~URB38ynhp_J<8cagX8fTk!UG!`;Pbi+Gcn&-;0{^58hw(v9wz#VIc4Wj0;%3 zr=s>jP3Lv3S!9?fdzfoaI3QAn0oslSBADArE7j#dGNE z>FCpt+_yODDF#?YHpo_(^)@WO7jJrW$oS~U;IyY5O;0FL1u>Psk~)^rR?t4;uH8Y2 z@hQEg!8O!3YPic#* zz(XJH^_!itt5BS2rb_CvtuhTiLs{2HX?eBn`3_38;Sb|^|77E)dG`9#rQs}HRaL}+ z2!qj*BQRC@7jM>CYZ2hpuz}LR@os0w9kQM#>0%-%9d+Hk&_e0JW={&rABUBTc8NmZ zUPs!w!k5q7UltB8wZ!42RuN12Dx<}!6M3)}=IC0S_+UKSTySY}k#`eY*_25rkSaHv zLj+!gZV&>%M$q$=eTH`ZSDN!R#*9@+Fa5FJeOcLjY-zswfDUUOA z0dn)PT2}ODZ^2thj7>MQ91gYe|4!p<1@)^@5k~Yeo+g^OKv?cxAu<;NT&{UZc=}r|-SUR`7Ra{)Pl4vAk9bS}Cle zkLFgdW_dc2Ue8G_)JHs+F-F-c0@h(Z8!omXXH>ceWs2>FOB0(((-VyRu6i^7AcWbN zzv~i8h5P#yQ(ETZ5;FG&rVW>_t|Q~66t;fdBHJGW96fE0-@ehpY67|rtZA$=rRf>a zw7=h#7YsMQfW9)3qWG>}p@s}v-S+tP<$9}e&YWpi88Ai(aSl&$2_50N6K(s?M^j6xGlwI;J zcbsiZnWc6{KdnLKxyiZnE%MWxZpR^EMX7_2puY(`*V@miE9^OS89^YrZ^HNMfv{Ri z_VN9^Oj@6;sMyaq@P+MOetwcyW#;!91kx>wMxZOu15ynb@G{sq;7?n%Z@=G+2nEo< zOYkZ5xFns*0N2tTf`CifCMi4pqN70v;N}~3OFp;v0+1mgxqo!7u8vDg)t3;KW#SbV zksL&f9D=PLYUc@mhW>c`NK{mWbQggRfkK8fS-(817xa&MY;4tUUU4Imkul4C+4&VO zw-xnFJn#-jJ~&B0ag1|jzHeP08;qO?;dAUafI}vebe!|S7_6%hZ zZ#Ls7SC5>}lU`zc`^bVByaI+R)vp;M?Cj5>L+LG#KwZpbuYh=l=bEUl#olTG z&9Q@fjy~uu34QK|!sj3s{U?aE7V9f{`BO)4h{{q#Z%;JAS^``&*?$c;W;XCh_qUZ` zs!utYWS1Y?a{2Yt`L`Igx^%L<9knWO;k3q3*Kw)j`foHdHtRLz_Dn5ZC5%r0w31<} zt(cBfK^JyeH`qjeK0C5u@%vuNhR4y}qpPIqSt7$K!$JCLkSXmyJEjfg4I6|0;&amT zGd}*DLERSSDCex+IKNK`ji2&#*1I5!husrVm#x1TbPP^Uem`q2SyB`t6;M@JYB+Qn zk48SQAv#;wcdD%F!nA}q>#w*@-YR^EN2tlvMl=jB0jn21sQydcZhZ?{@}4z+AgUi@ z%FCQ4#_IPB^0u<3Fy>(?KKy!gjZ{pM{-7rtk?HpZ3r*AErhg?3&JHp#91N{FzdXZI zj449U1!Eb^7kZ(4&2}@szEc&x|KafMFK14 zH0QZwbKEatxZUMXtn&%A$g0s(3Dog9^xl;9+}9m}aJKM7CkM>aL|3Y3Y)=;8Ey0F~ zh#qHX42~b?k{YvJ%Va&Fk8_lS+3@YF6G>%ZZzbo9OwuiYUV*y9f|RT|9%^7uzT=%0 z!Tw{!^KAbZW&6-;BxxFfzfi-sCrwu8)YVxHg!~~9p;_pj$?tr*n{1ftbSqm-VT*TF zEuYV5WMP7mEgpde(JPlVT#;~;{cLSm*I4?$ES7((<9nSf9aC|76n&p^L7EM@xYam^ zRE0KKy8=J+BDQ!T7?n}%EIuONzd^6A&sgeg$zq_c3dbe=CvQG8b+|)>+)-Qo$Y&$g z4P$>PJAMs0kXK4`xN(Ot!EjxxYk-cfj_rAyNfQ(7?w+eh+jDV$$-1W)T^8zrxn40ROWP16aBea@2eRtr)QH@N8}i2^ zA?am8z@0ms4f!hH;%GlSRV(L=;&4KA0oWXeFk448+r{+Oa~`_A(!uno}%wS z1*;`~;)~ChW4a19_dkp(M##(e@10|SKdk)rW7wk9qT%{CEzL>kp1vp6qhrzIJR=lD&jbm*{diHCIV^HuSsT(Wf+D zk2t?}Rad10;{R5=Npg$*J_g_2>%?fn*31;giwRZ7cQYkO@^m#(bx45&0N*c2d5nc~ zVl^SpNr$EMgkdsnEnW(KfRqUy5YA@1a*{#hJn{4Q_WeR1&|Dj)-lARKzHuKUcDg502 zpF4Hgub~4jMUD9gIC=2c7AsHxFLlEH*=Vw^ukzQoDje>wB7L!&*_4~W#iBLfZE9lB zuM3P${YuPB+P~c({4M%@j52RaGybg>fuvZTJG`Jxh;oVy*2RPiDHq;iQ?Qd*P~$I5 z{D}j{nqzdL)H9kp=>MA!o=}c{-k<)@z3WNb(_{3evlC7hBF#sbkR_|e)Ex@)HGRr1uoYXl1dHd8m1Vw$mf}t5uNFWw zWkh{^1Q`5}Bpk{ANCNg1sXj~Un;#(w>qTvWO4yn6&4jDk%oEI&HQ_=9;O;H@5;%_< z7shYy#>~J}&ePIp#HKSa2=_<5tfwGU&-uT z)=a#G@2kuU)SaK{z70a72AJ^(kI$u_oj%WsTC|Tzl;rVt{lfBJbEopR%5vc{D2dl7Fr%F;054NKE(s zrK6K874sd0x^ecc*>n5BF&GX^c&SFB)*X)cjn*}|4nA4QavPhbdYWvVEZ^S2tEHGQ zf*an|eT%}@bO!V|)UX;^q=r4?As)CxoGkxi%Bd;wAGj7ZaoBTRsroFz&%AK_n(jwg zMI>0hni%k%ksp!YRSsrtk7VA;GIJH58D4v3}3xLwMGA*(rIY2oJd*9SmMPb$}_wj+*V+7-t}3|DM#1 zLlmseps?z=zi@j+bv)u35S<1Ig6IZzo}2i1@=uWfZOx$*5oIBeKlB0h3?R1|r(;!| zt10&P>+A(KctbC0RImjb+RTda6Y=EAsn=$h;^165l5}A>@*qvC$(wNzyQWIR*#tfZ zV#H=Axzbyt=b<*z?oh**CGIQ6LsaYs9H^3*)n2n)GiCaNyKS@^>drd<+-<&s1xa0M zxUywh8oo1c4(GPR6zSLxJ$d-6SRq+|V@lkuVl`yf9(JR*5@ggHjxWz!HtpUwfMLI`I!s)0_%ffPmk>}av*;&0 z9Q-xngq>)vEG1orrz?BCDH+oGXV)N$t1DYf4ElHM?rn|fT2L#uwJ-rL)DG_8o5iNL zXx7L}Wn?R5057FqDmH0CA@&8#fW?r~mo*a%?&|hm#QH7l_7@Y54ed*3-q=*bl?+A< zw;B&8zIvVE0}NR{#x{`8>=|mK85AOOLQikAp5V!$Z1cLx)@dVjhpC!|{o}z^k#&B_ zlc8u&9r0cSf#`z+D5= zDw%EW^uj_yfnOQ)Udkh{_sIC9qD52S41ZT$o<;$&={crSVRC0(Zevw%(sgfG73DQW zewJ;sx;l*#n+?9vmTjG;R7QXG_D`6~p)YlWM~MBUsh)QJ;?qPyC@DH^&;Wx@MmZ%Lo%wV&I}PDr4- z5R28QLA?H-%NjT;aLzPyaC{Ebk!K12ltQDMy%kqp_oLf7LqXs3rxeVaEJ05{UEvvN z;9>_ZJ4K-Fgssz+Iv}2{#46H(`nM&w-Qi@2YsB9BjlIAaV4#1G*LIdGcU&3TX=DXakXK7ID}VE1e~nmB_U5JJqv0=?>J(w z*ZgU*d_{dLUjJK((cseW?la|9{ek)bt-d8CXAAwT<`-%X>|3$EY>__v+*KP< z^Y>MZMd6$8(IM7#xli)YZ3sjB!i%Lw!ufO|$7GVUM0S%tZ?3-pgFe6xkL<-LI@#ST z!C%#L?s~t#|2YeA+fx#LOBEO{Vt+ny7`93<_y;Wc?w~g1DzL4)G_P9ns6|p>)KAi_ zzuJl7%o^Gi-O1Q`H&y}?(i*bX(>a6S=kBZ%^x#KDm=2vNdkQjU3tVTN-1nHUG7DXk z&EVZHb~zNfw&1W z+xa&n+9Dr5K-w4?316+Z-5>_Gw!m_7YGf~9=R31;f7Fd2-`d_B7U(L+MOJ$aTfso- zJ)u3?P5NSaA_S!FUa6g~sd*|ICK%R;4ZqbP?kbqgwdlEa-!aQgV`HCMvv`3~v&i1=Q3fcH_=fJHtSD7j~dk^tv!sucuC$d~!7=XI51oz~}% ztkKgy{gH+-r0349(tJ>$fH{ghKRkPn`p8r4swGC>HtikZepFIT21w;(f4M0Wi5-%DfU>G;R3`)R@u)=;DOWnyY*;fiHQN0PQt>i z<5_eiG%ODHGW*l1m)2x+M|}v!>KI4tUw1F}h}e0) zm%OwEY<6qO={SC8r+REU4ENLY@`UeNN~p7^FiL+mq@}cms}Kv%1x=RwPAJV8wyKGF6DdqGc+(gG~^0OMhsjsOWazX3_n`}x~- z4v?rFrGP8N6~D&7AO>O zTN%rL)b7kgwP$FLH7Z=xTzx#lIjLf`(QZp7IGe>zQ|k6B&8mnPtv}i`0x(z>v9^`@reJ`jisCCYEYUi3-r)8T=FhJsn^D^ zp&*Vp>+kAcVg~&mua?JRmo9WBa@}wYT7`aOT_rwlwW>sX&h$G`@unl07V_^mV+M8i&R6qUG2ibb4fWag zvBItp1NiNj*4yVMxsDfecyf@rwqThq$^17T^IfG0ADzj@u!I*KeKWa!@yaiEt0?qMlS90u*#5#W9)BC^9)$*SkM zJdjgCeLUK#`DWG!VmWsAB9n-41cQ{iaw|5``RDZ8<1Q;?xlJ8*;=Qr>U z@#+@jsX8^0XlR<6+L{ zi?}#hW`dvkSQ$1#rM4+Y?i^G~^HSjr=Z zLs0q@HnA{X|6n>rf9*XDvk<30p09c3R*d86tDvN&W|*cV;965cp1FGEg;%(?9?TmXGGB=7qU}mkYo=j$ z$e+MXEoW>ZvkEB4*S0?Nyf?D5#`<1k)Su`Pdi{5OKZ^VK95csm^0S@ALW(ccZ#7}| zu^*<4_=^%uhgcE#K=26a*v8rTY19zwNcY18WwML?`|m)&9giLHG0k$v1sAqk9$wrN zI!JBtbh}28t+AbEj!jBhYGd0H;QdhIC>mxrWl!2GKfZjjcD)ln{^0BpPFLGzkHd`F z_~IT^RHw!TnNSzkxBgO6#4OOl#oowsq&WP@KWXERJX7E?RaP6iJ3LY&Y_zXI zA2RI6060qS0X?)e9R=#6qi2mu`XvetEFRkoujq5KW@v*dCShx{4j)HS-3qJ+?C-C> zNgL1sQk@UP8J}IIbFy;GAL4OpU7v_!$KcZBBj%Rs zaRk`)GGMj55mLbQku9)t_Jg@dxSj@0wp{y6nEL8hM>Lk+BkQyl%@%OGEdz-%ngzS| zQfSfd))XhusMxU8_yvddIE&*NZ0RcegyS|@Y0e}Ci5k`HN55xdIx3oy+zLP71ZDZA z6y=W$G@Bx48i{`3-c`_c&oDknT6Z^tWp`5sGvBpuR01!FUC&#RsN3xFF6^KBo0Yrs zHUpQ?t#)NbN44Y@jYgvvbDF7USvCPfz7J-i=A6hVuQs}Ct{yJPKOMu?v^4MkAo zCgW@sdRA-N8j3*E#M&VihvloZg4V&p$r<41G7A%3)RqG?YRLS%1_*X($|>S9SbO9U z*zW*_Peq1%S}%6wNz1d zVTWd;+$t&UHtb$;HDI^X4~KQzPKzv=gplNI$xPTvdyJfbsxlsMLu*qkBTav$4Mk@y zBdIgcfcoU2RBc~s$j_5-RMmDY^6L}nknPAF`)xi)CLwP?G6fQa$~Q8JHqE$BcI%tN z^+CC1hv3PZ5jL;DF{70>0Y_`JR0vl(;H~gTqZtizhl3V+u8b@x;RA8Qj2rBF0@MAm z$BdZyyh``cr;4V+aQD=2q5zs4@kOIJZXn!}UTXCFO^M3(QVAk7 z&9@&G(d(wEwJSN^b~%q@=w4p{@aG;*N~gW#lQ8@QCE|~(GeaF7EqibH=J!@j1uI20 zUHZZ(c=<7jmj2S|RJS8R1-F~;e7(HIu0U_rs8y#orO+T{F#G*&o!qq7$^GQ<5K9 zW_vQI^`!?(2d;ZVYgB_IREngK!8qxuTSIunWF&j)zDo_6lv8H{aGU{|27XwFaz`7} z?TBK-H;IWiTZCMBF28}$ui@S+uT47idQXNmKldL;nf|rz*&&VY{UL5}cB6PU?=Hap zDKXksB zG{0>RL%HQL+5WSd?rTmwxgMG)41C?t!I94A>j-+fC7~W&9vTn7H(?Os*!M<@5**9w zD+^?UE%gZ8?79oT}=tGG6n$ zv77OHn+f(FRkfaoyCc2f7Q7Sec~wYE`qXg&0Q{VfWCQd`*!eXx3f{V{+}X;$0l%aF z`&;7b(fP94@wk7dR8g_Z6)$3me9A683$iTVP4DQwTn zLyv-uUGT&aH;ob}MehV{t>vjNi|A?-JP9%YsqZ}xc zW>Pe}9_o~1X3WbGggvuq?+2Uzpp5*M$l7wZl@d&kRcA5UNRwN7FR9%%IbSW!+Z>lq z|8`Ala`cv0b}ljA=kcO_we70ITkYZJTfTz0hzn7oyce8m)`@5BeECjZ)D~ z)py2OY>5d$^^#myzna>o^xyt9ZWe%2e7Lnx-2DWC zT(a6bx0hXkY+Gww{6WyJG)0&0Wl#Sd`}Tn}potXSIZ{n*Vr7<#tOm9QdnkE~ivwpT zUPEM766t3li_lwiW)-JSrkJ9tKDgRLT z3G->+KJ=3*PHRB|XrsqQyFJmpgD_o-Gl=R=F(Zr0jmgWs$!j&`8pTAyw{zd8aDZv- zqt~eATYH^B32UGyCuV_y!`qTA+uYjeEaup(#og4%YMb3RZ}A3<5<8_bJvMofXIO#G z<97mA<8Ri&ylpgG4Nijtvp%+^~Oaqxu^Zq-=sNYcrjLpdUnHqHVN^xGFu+in3K`g&?$ zbTa!=Ge_R1=l8QQ4dn((T9cA_kzwg;?WeOv0)!3v9#wb)_$r-L*5~T%X!j&GW)+TDPhr-0pQhcIMa9e*X-lu}`1v;pRJ)EEvZDyIQQ&(b9*kJ-fysxZ zoG;2;e#(ng+4eA`q;zC1sn@oI*6@^-0tYjgwey3%I; z6WK!d1oY5(7f-^0#4k39!Q4%{W;E)(7Zkr6}g}xGOw4 zO}5uG$U8M_Y*-s-#T=WVj7+g_@4UXblcseREi?UeXtr4`*jTqKa;Gj|9hOr}d$$?Xyl{6nr4*0CAKdGlaYF7IIhVc}C7 zG@gxj6u7&ABk^Wwv!JiW3#vR*FipdkLoT0o<77Cl{T5#vx;QTMg`FHxlC8jAUhbyJ zYG?*K4?na&FK9^%QYwiR6Y5yU{@i86ZP-cQbAp&1 z#_)#wx)K{O&i7ju?$+*m@Pi|LlLq5Dw(d^AXH&v=W3;t&OjMgfNe*^RO<3i;{3+Kb za&Hqg1qasW?!nhqK76AX_G^hSCL~_~jpdX6d)YLGB;20X{GdvLL;pZHmD)BobkelU zX6A>A;b_+#O+Io`OT&6J3D$gPa4LRK$aQ>TO?J9?xF-2wk-gz~xgs$$%heBS5%uxf z4AqefTTQ3F;886F+mf8c>BCWEb4p-2D#Kkzk_TM(urMwSPlNrU($uQtbyw~M$kZ=B zD!DecmpKIQcft;iy?vq49XJVsDVt#pv6jV>SEF@dDRpMwa!oi!(%I)59fW{*r4%6u zIXOh@%Al$xET!73>T0(^wzse#G8HNN$LXp!d1YH?n1H0y$Njv6$B+iUCDC%)U|&(b zRgHbOd4(hvsW{&Bqd6UZY8(24VwY{d$9J80?#(5z!_mm>0hrMVP^Ja_L5zo#OuuLyG1IIu1z_vSpL3CMLkiSsYWXH zc1Q6~T*TZC*~!jrd@NuauE?XG{(4V9P-xbmt?Y;)3Sv z8u8Uc3Uh5lZ7?OP3EzbWk2BWic+>>I2l?Bihm4ug^ZN zePw+-%F*B{lB4WU-53?D+rZt>aMtbW&Ud6ZFJBe)8sSb_-S>j?EDpo!qBUhy2x-(p zar29uZqYBPvq&cMrBMNgV#!?3eCN>klhQZao@ftN$piBjl4X#IDOYMKZKjRfD`dI)>pKHhroHz=vdn({ zeUsukhVRB~Sp*S38g(Ukw`conX@3{AC0h37i=(_Ob|#sMAlxEMY0FsNI_$Ty?38~9 z-813!BFS=n*wI6bO_T>XIz6hfaa9=}(1w4PYUXBwB?MxUG}rg>+cTfxGngJ$Q_i(6 zj8S%%B!dS6%*SJ1y$7%L@}G53A|08`r>w7lYEpXOCs_{!f5~juGUj5AQKge3b~^w+ zY2@3iOh%bIZdloX87Q1GnwZe65j;Y4lJ5^4>^iOpBz#UO+0~CWGz*A0)5O?TA|>57 zGb*k(UeHHO|^gnsQ>#Kt!tfk4Y%rd>oaJqw{Prc_Jj*ICGVZD!3uhNS!ud&FLR54cxmRN zCPz2k##`sjy`}QpSQAa`7@(|`>j^7oGW&vrpdQJT-d|vY-F2{_eD!|zWj!@b zv>{^8;#E$m%U1$0-IfDi-KEpDmKvegE;XJ#6D5sVZ{>p_X>zg3Stz`nc!+^~vb)!_ zO&s0-V;;p+#_a zeo$3YL!Q@S7<-Xq*k?ZVDo2MMWn-uJTFvf6S_($z+p6n1!dwg(IXmjZA1$Zfb8TYs zwN90$wgE_im)fk}+LKi=Dpmb+?Df^gvJzDtJ%2>f>a%%bx|+4nc(m04v4SDuz&D3+ z@a7zDPhSzV#^Wd;sPUkxs#VaDI;?>N7OsL|Wb5U$^^5X)CG05q{I&=E%@zH`lQ9KP z4hFe7kry7Lxj_+8>rX)+cJ`!RqcaPMLUXE!wa+`fMV~&I{#*@>ijocS;msE=@ea0F z{(vu;rIugSTjL}yx5GQ=o0`ENWyB6r#eCDaHY(CnNN^QFH2jOvTWa}+k~uPCAaRZE zS0FQ%Ezz1y6kw;@HvZT$J&A-pi`UDRC!jt*D8Ml;FoTiafQ^End?n|t~< z`1|YDQj}y?6P+If7xOmyt)Z`qCur%2XLCl*tQEm{ho(oGptWn=OJ=P;p%N6VaoZC{ zPl5*X^X+fAcQKm`JhNKPMOP=fu+EUN;F(u=iwP>~WQMN}?wJHNLV^eKn+`8E>FH=T z5lUdhhSTYThK(x*6|7}C;$uxfnuQt59$UrQkHsT20#~CVk+2sZ+^64kflX<6X>n(Z zA64ns3Hyq8BDF9GXUau6*A>^^Fj5_eeECJ~KU*TrY0s@Y60jboa*&$*BCaX^L}Z4? zcwNTxHKF@-vA9}X#(A69XGdoBpgCzF&(;kky{(OWCu&4=dwg9z44Ju=5}QDxY(&Z9 zK0(G4c7i#Tx7Dt9A#0MQ24#1~VJ1tU3kVjTbVC~7>2g`v8^$wJcL|m}fPDA{(tR^W z(YG&Eg|^0B3zK4Q2Vz%&ar15KA|Te;upBQvY#Mw9bG>Ggx#6E?k{|A)UVGM_nvM*i z8TB9AD}rI=rnWjm)&9VzX%VRdL%EGAW^-Zj`T*KQK^Nhcs6`8d7HXGYa=NuCX@Wr} zk_z#fui25NauQ2^JcyyUkZAAGMIqjq3{py21H*!K5%;yE_ceKck)x#W zMs7m$<(^X48%@Rl#U&hmb@6Yjv3Gdc0NC0)gK@BBK24-H+Z!FKGY_X=cJzI=R*l*D^78VGjaOEc+}Xwm zW}?l}{Z3dtCEN`SX&e{%T=DAR2Qm`}oR~gbnh?ug(B#c68gGe-5UPg#Rz{)+41GzF z9!{>^R!?<_Dp2fa`>PMLDi-8yS~HggwY8hr)|=r8U6ISW7mxZlFIMoQNDbody@ z+wx>OTCx0Y>DEpHb5CLu5A%2fW6|Bl>acTvXD8%GK;Nxw)%pkI1l-x+ zsVIR?B(~^4#vm@i*o)p~P7Obx-_bWYjD{F@UQ3VO$1*rHf}m4;`pw%Td1-!yrZVRhvvRfz!H=7yH)lJt0N!%#er-6dx_4iZ<4N=y zv!ypc@$yaYT0oO&F|TGa=M6b%nJBQ87h0|+x9=WE&_2+?wj(xi0u{9WUK%}|<8i%g z$tXA=QnAvKTYAxgY^GC}TW?KBZ=AT5W(e#Z9u75vMW^n91Q9^0$`kUH?rSdN%>`O{ z2Abh_3_X+Bik)(@R3n477WK6pY;wnRkNNNuW>M8~zBV+&Tl=Or=^@^_MQBk%9%Lll zEyo+bbmnh88E8g;9d~7F$fu7S`5=J}ENKgfydTucp42rDZQO6P)d}?gj~sv`2)^ zH#o9(_~6UMeT_Pkd(!4;Jaa_0C+OPZxA{)5pQ(=C6EPFzD`UN-(+_6$Lp8&sb8Swd zIXaY56Sigrl8U3y^hT&HWhQZ{<338voANro&gd~Re%WV+!i#LT$$t#$5w##Qcc5GS z)K30G?9wAWdoj}(QXH1xao#yRi7<M+X?$`92q%2n$4f>S8MVGNEXz3IL%mr z>aYu_z-UggJVc{}DMLGe)%G+7(vKmW+F&27IO+;x7K%R_HMsQ=AOgC(uXZLOw!W{&gBxKo7NPUZII`Y?C8J zg(B2r^+ip9LmnCI+tRj`zl+(q?A64;`lmhLyYM4C8$*q5OExEBBxIqh8AVnX)iYyl zCeJ^hgNh#w4H_Zy(i_Uw9~EcW96vn_(!l?cpaQjyfcKK1d6p2g&F{v3A104KmETTy zh4g*jR_?)srtPp;1Wa0Y{SYVLgv!=H2${;|E8nqB;c$BrUj#3_m)x%^Ket{=L^iN9 z6sKO%4^vK8GFgSPR)WWmag{-LG@}5(psR_=^M=O5pqFj zq-EpU-g@L$g_9SttG}|~%M1DhGoOm4Gn%;sGQs!NgRrQ8wRUWTm9>MO8?|2eF)>e* z*k-S{Cd+RqtRP=<2x}=8_#>N&sLeh3cD}D)8w=fXMD~nQmk`tL^8>k3WN{hq655RTP(VDkBP#1^4a8@_{F zO7Oi(Ax;;_FvHhTv!ky5?23IgT`t+5XqL>k!xGt=0J(rnYBo6_ZT&Cy-U2AjrfU-> zA%PI=Aq2N1Sa5esAc5cs?#|#kxJ?o)xVwd5!CeM-8!R{s5@ZI~!3O<@JkR^>`|V$~ zRlD`?)^6?J#Z*n*!`*%Q+^5gE`gHeo7X?~W@ZWo;#<5mAzSgD6K+5)}x4JqPBC}$q z1Wky?DjO#14vg~38J?xG(q7Wp^(}{4nax;C91cD$xaoyQmLA?4y(o4DXJ3e{+0?lM zclN@aN13_ye9gDjVk(i!ETmFI1Y29nurf4M_)uITZa1LCbNLSUp^o?vMc4da@s0k4 zG}rAXsDmN#@k(`KcUr5GIl&SZw(c~HIvffNR$B}no-1qXTCT%nd7Gl^#xK%tfJ-Y| zQ8O#RHSA`0cdABwn%+9byQmbB-5OzFhIjJyl5z=C8S%Qdo60iv>+iTYHo#m6B?fwXt0b{9ilXn?#=5{ z7Z<-T>R4j)($;o8h zpG!86qV_yOXNX}OJsy1f5HdWx;jEF+TUx8dR>o4$($Mw9$0BWx#`rG*+7sWHN;Kl^ zFcBnFxd)#aT&y>HLd~~4_$-xo-iR~-dWn+Xbt=%Y187l(B4419m%?>Qniy)g`Ips7 z_w}sL7mCjjH&=MSEp-E#`HEKgLHd;&{iJzCA9G3`^IW~~PS5-TIcK8lCd@Kc0lV;7 zKccW@IzxBOc20Pr*WftSfZBr$az1kasE(sCUtSUJI=m?gp*5DcnounAXo{`z#D!o> z=(l{dcl91?s-mdNkl?&5HJsbnZJsh0gf2%HO+MkC^(vuL5GITEjh0yQOmR1c! z1J!QNodLlE14NZ2;HH;s^L4LG2H%~jWS;F6t)(`90~g0)DJa{-Nln zlSS|dC&9c~e$ir$f>ruV+SjjM&3sE4>W%QOtS^x=`uJvP1kSTu$g*N1(OabM&3@xu z{3$iEMdz6rUFC0o&6JAfi=(xwIsB>(BRs$ss=n<7E!RCV(%eelL9NUiYq0`_56Q>u z2Wr$1L}`xa`oQh3-}@$R!C+qBxUCV=undd?oWrt-ff zEY$KE$1t@vte0bh1I{k&vJ`3^rwTFEO4yf+s4K;)Z}^yF$I=<`Xw_r{gpV1lQD&Be z@>>gU`l46&e_+SxH;%XCjti6-EyCg>`{#$MaIK6=K}* zq>W=U!VzN!9cXSq7krrZ3;bmR92-MYf?7r`PP7|U{MFM}M9tSY-tvQp#|k&Bp2(eZ zsfV2|gs-?RE&?;c+a+7liRLGPLLar_5ewGans5DW31JZWa1ime9MZ%nHr)puwj-)n=^a!3tIS2FGRXL+}q7hi~K#4(Xpn|B?c#i5mxMoI7#5+IS zSj|8M7Y*^qiKe-$+gGSTle!&oF0+<(=P%@)@t|gBD%GSwI5Y-qMa*+P?^L=JVKAk~ z^-$Ld2>ok_7W=~Pz4OZXkirXgdu(lczOC6Gyu|0J;Dlg2R_2eb!kW_iTrZ@BglkGe z!}2Kipc*Iug^L)`wRG9PWaomv2;O6vQF*7O@fun-RU+)U<&=POVX1culnuJ4P|`cN zzf0sHt9XB9&^TDx<}0oU=5(2eCOfAEMcZ9#q?){^JvCPV|I7EdZ`k9lCR4rgj@!~Q zCMOlzGrqUK`pOnXesZ9FHCRssKNaYC@&h7;KOt0dNF345faAOW$xE}P4omQA1jgF< zJ3s?dhFIZuL<(l5_Y8cA(V?JKv(>=h$ajDvY?$ZnH%A)wRFfNRV+Za@*|ywv(bf>N zZOBB50kvND*FFAbikfWo9=MCurXswTbd~)XK}w-bO4BOuG|IJW)!t)76KaE@uX`j) z&xG(sV8QPhIep2U?hXyqSY6J>+6&+>q!;zbNUq-I-_`AQlCO?%s%i^c@n7u{cY5Q& zRu~=x-YGN$j3ZaPw|*N=Do1o-$x7Mg|>tvF5H$PD2%WVVE{G1B! z-dyA%%Pk%#zm1srguJ&)@gX+8z5d{s>nsq*!Ej|hk*)5)VlOC;B%WhvNg$AmhJB@B zOBsSmZSnETm)pSuc|Z7(AOnZ|!#ntim1YLJK;4UWg3%JIolqee)(pN%79n#c<*Pcr z!ih2>g5FOi#P;C+AG9Lgrlxbo=Q*BDy=wNqg1q+0N+Jdfxw%K@wD_V&@WmAhewa*s z?iJ;&5iw^=vCZI4s7+Eo>!5oHc!r2dYf#@ARjr@QJw&RzGoMnAw48>q7uq{H!AvAZ z0>#*0y-Mno45Tnz?_z9J?JB>pV(pShz0G)4A*@sd7~ZeO)~3YVIKKHo_}64)Uyysg zWcHV*Mp)nd(l7$xcl7r8aj|eT8?=rGf#+R(KQINR-Puk&HkL-9}7Kp1MQWY`J!tb++xsO8EA(L#FN+m_9lEpi|P zR#~Fxo(D+NP_vrNoPLYv)}5*8`C558QOhJ!QmeR*w`4vjl87x`$H@RE zG>HrqfcH|uLwc?S4?1}qGGc2}mJ4iYyFwZUpUox*2fG#6e(DHa`HHe9VLR8JQwf1$ zR6c(Mw5$0&(7D#;nRcFta@1WiepW*S1-a&{*|p{eosI=Ar*1f&TdvaYu2w&MTS07j z3nsz2g!s>CmqO&*UqfSltr?hG&gcXXS@wl0&v~8rS>n)yQx6nJxxKj9oe;7$XpDU~ z5wYp#J}$i8`F3R6FibbgGnS&OO!|{poGC(lb+O^m(05`{ zcs@;eyugaWe*YuZ!4B7iaef7Srygl-BV0!0bu=`Vh}7WK-|}VZck*}{r&(Ye4~`Kq zX#Bz%$?1OiG&WRTC8h6M#G+iCjIX`#4-Q;0742H1_b&%Zai)7%*BS^$aHE!Ff?ve7 zRE9Mey?y>UWoVpZ$zt%9@_Ku*tLaW4UK|%f{6v_|z}#y5+=-!iGRm5I%NFS=x_sk` z%zIwtemyvwRUfynl9{R8GLYO+T1cIIfcCRA zMKoC(Xmmr0JX7~be|8N8?+5f~ZB!YX`RheRzBt@Dy!IEF%=vMLtY#r@?%W>V>Chw- zXb`3rI#Rf_rVXL3qXTRHxWjfhy4EU}KDl^XG>3JU~@SC#;WkUH;*lA2E2uBRxepZqF%Yh`Ahe z@s@toA1)~JIVtLUugL8;+eq*q&@Ms3)MX-kLxP(oUrmaEO19;Di2X?s)33b}t7B`< zSe8=Vepf-1)2u+pa{*+#!$;q}?nsgk%_MZBFX3sM)xT9+Ukcz;bcvi#zO+I5XfPP= zVopnX)c?9ZhQ4EH;fqe)@A=~%hMweV?7e2L_5F;M)!N5$zVN1aJKH0J#;oB|H4lbn z;0dJDm!qI7MaIy~Y+Q-kGF0y(&QqEtg1-asxmJcz1Qr#P;>ftCL73Tm!pFZMrk;op z3(`fAYdu!eWk@WMOocA<#QDC`JH5lE#r6UfflOcfzoyEizp?Tb3YszDnH&9cRK%K_|+kI{o!zv zL^b_z@TGfm@168%fqJgtm-p}>?-x@#xT^{E2Bs$y%0P>3nu>uC`iT%BzSo=NGrzs0_-bYZ&PP?(6%(R!jCBf?~j%U=i4pQHb+zgX1e z*Qxl%av!z{9(vSxE%+#gtJ&K@-v@Zc<9V%F(y%+ejE6ULO+!7z^y(JGtDu zSuz{Ev)fzlF6G7Eu@3#8P&YJM=viNld92GZT|w+bCD?<2^qE|AJ$8^)Y@<8-K?{-$ zG9qC#sX%9cU2T@-ksc#3m3uJ8b>jIVA|MBg&Rq8h>&Z*jcI;x~*j9-k{8k-FlU9cD z8Q;EHDQGtp}TCTbGO^aGj zN$qsRx_O{$p4Qer`<^c}Jq(|6Dp#9Hv-YK)(hZr!w%GBj)|rd^dK=emURl!A#6Ap3 z>JY7s2HU7$?PyuBtlq;6vpL`Z(jp6+*34PLOg*P{_>*KSLxr&ByS)|%(k#Ex_aM#p^;mBM1ng$0aj$%k+C zUs2mA-z7=B@LGJTu`f8ZWlQ2C`NMLIHIGgV5D$AQnbHzFU$#b-=-uAPS1x&s8;h=*MGeXjVk)UG9hrgu7c zy4;TY)3_*s?Fmash`F;N<88QssLm*1m($n431Z+v$}ERB1tN)Zr8fENIeq^Nd?+VDo2p<{* zo!GvEh)Y>cZTRh`Fr|xnhUV7T;wXb8wZ<%(J!9dkQ?K@4%9TcTKL zoQ*T-43BkKq-e5VY*(LBjFrbKjY_JKgC-77S{F+z%wOU-3)|Wh+I{4s?#p}5>zf=U zZaZ2Dd*;DP%`(LYW4c{-sapf$*(0*j9@NEMBO#2}I%=Fr;1DLzZVuFr z$cbyWdo*odFLqGD)OCnisdoEApfdwmvFSG!m(Ko?r3OJaoCbe#81616$YRMb@SMrC zuUr%%iJ=_coH30wjf}JUO(h2&TewP&Yshq1@{Ky+H&{vWs*_Rx0eS0!GmR{-gik0m zL^#Ui=pH+KmHK)JtT$qC*hmuSyI4;8bR{iqiasa#zACoTme!QkwS4RHi z*4j&<%Mxu5I@Q&#c6m^pxW`afanN4(E)jG?>CSE`8D(#0rCm<`$(ZO#-Fmdw6&~Yc=s*PY9yzBCSCCs)q+hdpY7(OoxDKF~MtZ@g+-OfWL^A$kE+}9o#xn4fGAO zY`PX2S-lIfn5+3tCWc}PHNRngmx#yMZ_T*3{$RRXM`kxYWoRzG|5SL{B?y&zU82?u zdCGB`zeJ_%c(7l9Gy{6=zwx6#hly(InuZ81Watj&RhrF9rbm1lURNK|O_QtESfXRq z)pALB1!af#P<>x6C77-6edb-WDZmxSe>{vWr9Iog(;~vl*y#1Rvy--RJvWZDErw_( zk=sUtbtlTOBfIVK5@$lGs=-Dk(dpJw4xt^z^mmn%s&|iJ(XkC#>hhY$eJ69Q^GTt3 z&L=(4{<+RwZK&0lSb%S}2@;|)bdRveDCuta;wqy?H+E2lOSbqqsJ57Zi1>}82Pbt< z#K}70?ovZ4ZZQdu_pS9Akg~TJmTU?^UO8i?QIoOFgVy%p`;L5C9b^7*Zo%IV7|ZlWAJ$j zU8z?8pf|;@ksZM>6MmwW-+hVgv7=dlwNCiS?sR72DKjAf`P;wda_RPsPhU;6vU?SR%pf{7=pbln?yiobM0n&8id`Q1Au<691H77o=GyxSqZNaH8M;Q zRBAEMiDaEk{a~DMH#CukTuCtQox2R!~Qt#JpAO-5pCJ8|YD(?w>qRASHZ z|9n*oQ?tb!! ze3$0fcPV_Y8tV@kq2@pTLhaG%Wx{iq>z{{VCks1JVe*uX`wlIP?z+=hoTj^->`$lW z^RcM3Dm;2y$E!q%%08=CDx_3?vy*k}95igkg6<3E+uM%({uwuvUN3a8?C&;7_UYcb4TYtP5^=)NE}3Sqvg_PG*r1=`#VF08mL^J#a;Wenmd?W2Q>Z6 zXtQsER)xa_A>L>;?O@P{v%<3#pdcgNo__pqWi z7ew7Hp(wXDc)o7AKuU2+c9gQ-9_@<8huVC2lpy6>k!!m;;qGS~#*mI`GWU^lxhvuM zS=*N}zZStN3mPnb31+pYjAMMk{)pQ?Z|qu+5BHK$cNJjOFD@mWwF+<_j!~a6ymbte zi$_$)f^D8T_<$3iKSptv91B#g?%jSJ@w&g9( zFWUt7P`tTXf^7vo80OI|ZihY$A7|%3b zehoE$Six{$I+cFlVcN-Cm&bx)XkgkZa|@4)%k=8;74G`((!I<-?hzdSz~y%-ih4p% zUh}RUiZU70_x{>E!9Vmo5oLbiacAj?70jqy__5~UEL3`38tm-<>4EX-Nl(u()x=7BVuovJ z6Y#;`w}SuUR_Z^ursT^ixiJJ8ca*Ae5(`AxEYhPI1dnJhh&r+mg&S-24~#Uk?>Mvk zd4m8X`=3W4Cy82-rmMtYsM}?=t)pbs&|E-=Sk}g&T7Evby%pwdM25O%TbJHUq%>p7 zng2&G>=FOyg;|ITuR(7IPY~iF*gx_CQ`ynUsYmQPFaH6G`!UYREYH-SAb9qkJwu|a zqS46dymK2C9F``6k`e%65TQkNs$qJOCEny-%}kO1sYQ`9>W^nO z(N`;QGk^(Xa8L|wW{N0m>iw5wlt`dtV=Yca!X`0HZzX&1=LO+~8H<6>VW0lcHIe*# z9jni5z(WgCj@8`#1A`6I8K}kyz;b^Zbz8#UQoE0hzqr5tABFMt|M#Z-(d0kB*#b-M zzk0L9Ta;V;_wv6)2==?~G>c6#t|ao`JES`$oO^Y2 zIhYAoeJh%T!CSP8QJBZ_+M)vG({jXpt5f4Bb~6!5>rQ~3XdxVM#e@4B(r zXByE_x&Q7`wzZ8G1%Z0W%zBzNsBt|PTp21c&xRTwF_PL}!KXJ_o|T`B4cm5Yk?RJR zpcecqiFvNSM-N!-A9eQgna~$4zOnzg8~G)p3&O}{CC?16-JhoCyxtjWQ|8UqK&AP4 z_BPDBSw#MhWXI)ssE`%U3gGtUw*zF@wD#dh{IFJk@utTP6*S~Xu|Mr$@YEEOVDL}V zjhZ`S$(Lx$O&Pa~xvTU&rew6J@LPTV%m#qh=k9>I`L^7U{gGyg^kbT}tlct{Q$`iN zm#7$-Yc_z4JCX|K7QZtsi|}+E)WUdVZE{bLS0)9wg13l#^?Xcyk~@>8>e-}dd|G-R zukLr$4Bd^wemkWzu@U4}Q)rkV zXa4Z$+@&KiMf$ifXLqh8Z8&ZE8A((4RPZAVEskvCFq)tansH?3pK za_mBvs603jcOPa8kfv&!DAuPX*NFJ;6U{-LNU5SkX+9bm!D=8Fyi8)hqUH8$n3USw zYPy!9MMp-pO-|7KVY3s^%=xnWrJMcYn}tq^ z_DFAsdYh63-fh?|LadcqcX)GPs2CIP`7PqmS7- zw9INY??&I&R~sK+iQaTbo&of8siHnyM*759c)wenO^dG-_vqWPs^2r6ad`O~Ij^dZY_b__30M6U6U0S?%m>!9tJ*w9W6 z+%M#||IV#Bi!16p`_uQ+X1C@i%({W!tJE#0j{rgYQKp>~Ye+y>p-F5a8U{6izQ;`h z!Aaq|dWBi2JQ@LC>a$NdSOU1sDF+7LF_mG2oao!mqZ7(JHLlvC*h_kHfzsoGys^D| z-oW$mQI8Xt;aqaj?%xpY8u$-3ivdN;c4kdnbU$ftm+Nu!-z2wspb#rCx>CT`H%f0a zWLwvhmDI@f&{E8V^5DGuTT%?vU}Vc<+F`b%m5@xU`D;j#dQv0I$FcHK^$z~)nv`rv zOj*fH7jWzLaO-$7t$CX7c`*tes6_3Xbw&D9D%YYgQl1O=b&)1cPJHo~x56O1d3lD< zb~M1&tH?!-ibRJ2Dk_#$W3;oIA*2(5_px2GGo39%A!<1a#?@R1>LV3zY@_}%-Ha1& zwRV!NZwm#*;*c12zGD8wH;Kw0|JH8B&FKwZ6S(wxgYn z5k}f{PD9&slIs%qn)w23LW-f$M!tX?v@sLEV>7@P8{JC~?QdaANf#s?y(LW1x{)lT z)F!7Xeh|cBt}E6p!;#PZdrO=0vdR5X=pVnZ{^=idaQhZ7vOKf)IuZVQMIymi6Mqu2(@1vht7~Z;@%HhGsOXG3?A-AItUx z?Le$5&F`Ruo}MGktaKqQI|CSJ4H$Wl-z=W0xv|^yz2^H=J*A@XIV_UA>B7<6+Mau` zE|@IqV8B=Wl|Oth=p)LZ6$o%0Rdc+~b%v<}Ue+L9i<7Eky}>pT{-Ae9;ShNjqYaI# z$nZ=-Es}Xd#ai17k?!hN)NAoThkLE+%FDpd(62lY<@%ny8CA-}y4rA~mT453u#F@}XiTL!udfhs)@a(4 zI!JV>;MSgncbKFaue|V$9Vb&%7jdp_Ch5+j7|0;4E2k3d3Plf4=N~G4#vf6Wqq41_ znUQLU(FMP0cLjgII{g&hC@AAglwW_%wJaYRPaN$|0b;dFnbUZfo!IY|)i+ZtPOa&K z2*SW(>|0cd_KQM{=;2?dsvi3teV!Ym3&0(mbLIt}ANWMr)QRp7cwA`@M1?~IHr7#l zTLe|+#5csKJ!pdh9%hyLcLDFBI&we&+16b0H)ltuKFH4M@a6dPhMp`XG2M?vMtgF9 z_q;9QpShOGL#-z}lWBN)IPG+va~sY4=UOr5PxfTsh|3L|4?G=?rIK0uQ=^HyoFyCF zT*S&8qH>c0wEod}zMItX-neI`)t*PCk11JA3er<N99jv+4^ey)-dx``J_UZH+`YV!YMpbfl-fsAc(%sCEo3;L%;SqobL+s> zhmB(1-i0M)B|6W?c;iy@S$~qM)%x?8)@f9Jw2J>3CH-ozmTNt4Rj zJ4#C3=a5E9!itY&e)@y3Jr()++B8}>H1w#=mp<1nhB}(8fji6I1ezfNim5XLVl%jy zO#memt3lXv$>wIa-C#J16{VSHzQnl3^4Z($wCUX=)^Bs~W2U(5ytUiJ#2hSAWk>Sg zcheEKh;iCMiQU#)x>t2mKSr4}vHb}i&gz!>_mr!s*nU@H{*8Gf=z-D2*xG5zNkcaS zt2yN3(!;AgBPe$WKC$a+gxgy!Hu1@AzVuOVJX)Tu7f|v)=v1pN=SQ8JMp0YD-~%*s+U8U~OCN;BWX)7VyvWcfR3SIBM+Fmkml|+_zwU zBOJ-Km$zcJfOryhZYLdI{BW*5#k=i$htW=z8pk>*%?QEd-IxBERktqu@#H{Zog5j6v zbL_0mP6o$G*bWKphK;{|||(X-r4I zunx&(k|r84C-Mn_SLxnob;Brmc)loxSyWmbl{Cw+dSGaL*J!b{Cv{{&-+sJma+JNI zkN?YkWbNAV#JYKhsDj4a{W@i|GfB9Xwt{`xbqn@lPi8KmJfqR|RVDkfwvR{L3j`_W~x0k{|L-uDBib2D9jYDJvq`%-js7j$QK?C8bvPCvAp45(VQCSwc?7 zLf+2;GE68k>0!5}&4csloRx_8I28&=k1&m5o>V_cZU*lRNP#9U-(bf*yhg zZM3pxKh|CUTw!bZjk&T#?oKc`#>|mQt1OE}^CH1D$i> zr^^J(_2rWPNg2Vn9Zxo0-@ysDLrcF~BSCyIB=cz9lh@HTz(Uc_cQ+TbJbk!pk<;o} zrw#p@v|;#D4wRk0Q^CI6K({Jmiw?TjwROL<^pT7Vt_5<=pAJN#h&E*-E6EXwi7qB$ z$@vJA!BLvS9+3lXM6l+T=Iw%oX@1#y%lVohK%fKV>mb1E>wWZvk3CTjJ-NCvD!jxz zk)SowrG{sF`(#}Q<>xPUhhX`ofa`!+pFL4X)Y^WY&HJpcd%xvAWMnpn#s<&)5ETzKV&pceHu>atrGy`Tnmd+1;Z5aae|)yuj+d zMPKy``^SuH=g~k%7MVmvz<9FvCi#lz$`#sgC9||9kBuTSZku+!62v!MdAyurRAt?`%EpJg&`oDJzQIt8Pom1aH?lzq3q z6Wnu5RnaI3zTk#Z!#U^MSs+b_H4j zM{JoFmTUtgW_?v!S3O$dg?vGnHdjVmqOX^~i=mUJ-#`P-w%%PVgmfDY`b4Mn6y_=@ z=m9k?(uC;f^FDpY;>n0u7VBxhX!3+r`tf4B_HgQ9Jt==1c8E<*=L^DFGw=$gxg8Wd zC`9cxKs@)yTN+L7yBd2v7HZr;K&c(35wjs+8e1n3iRIATCqNN9_8j%b$`4qm1$M|H z`5`wo&V>nK+le!y%}88)n>z*>32LLt$(q8|t^v8ntWec1i-F&U2t(;S1S^@5iuyQ~QUFp(=;Cb0=#U!|_xs@$fY0fs`2Pk$lY)KkcI zQ_s21Ij%GNF-kWlOQ=y4t2x%l7Y}`WkDM#xQoLfNj=X0Z>>pQKj96@asgaEPfZCeS zi%-?rn1<=BThKchhLas%oDsi+8=Oq`n2hSTpHR*nMNSuJT#lRF zS}<^gc*T_x~!(#SPc-29q?3YL`4w@BK@NHTBU#R$u?DI%s z*Ze*D_lm-@q*G)+Xc)X`oH9YfaQLdJOK)3zAV?zU)5JpOW@L-U9tZTjF84!wv(-VY z6T}>3HZ#M+I>Z0l709?RIlm#Fbt8r1zW-~x5 z|5V?V?$zBh3zPJkHJLJour6;} z%Z!cshAo%n$LtMW8;vxT-n^BFo$9{*ymasBPjlWaMC0xV{re9#%yFu zb;tZo)%14DM7eubN!tjyHn@)gg;-Cktdd%)Q75S^##g<_(8AvK#Ae>tiH3$E7Lq&@ zWwAif-H?6CQmdhg&Y0HS=bWyibgcuR0_uc~oy-uLKO}9t2lX*^F2gp6! zUp1TQXswh1eD~FSE{}K4SIo$Cjt$kY8wZFlKbygA;{$mByap38oX3P1&C}cx5LAVe z&ZWDRc2T@3=lI3*xxlM!B#l=u6nyn%B_)wg(RyMxCz#Dg3C$4W_WaR(y@v@ZN!g?L zqv6iumMu5Vmys+5+V7dB>@Lmvz7&r{nw^%zVhK40b7&GLJf>QJ&_&QtPq1wquI7I9 z^l)mPUBS}aF;rsccT*XpwhjDBV)@7LuGpMPOk`7L)&}*R%>EtH9oQ}~tyE5=lV8{P zDYb4hx;1St38FOnsy5FVfaCj0S-~Jc=z$Mb{CL@piLSBkgj*opKs?t0Ync=pnvb#d zyzBMs=!g^AXY)V=<;6BrEqOBJ;L;9nTO&bb(CVkd2WIBD5D;XEf;x4lHz6*59B|VG z9EL4Jrm2cI0;-jnEw)+eVf{Bs{Rc8bALQ(+p!DwR^A4rS+enF*vO=yoG=oRpl<8qX zO1j-bzHaI^>CrDiBHS%3OOU)K`*>RF%u|U7OdsU1SYHaa+vEm6U}}T^*}d}61yYDcUz zRb#9O`vB_|jLtJuph4J-MEnf=tn#>F!Kb@j)N`+H&Zh4RS%rPu$=*1BsZH?&r=8@I zm$@Z%>v^4?7wosFP?>!UTy4(Ns8z%=f|uj+)sD=1zTr}gu`4#{^_x!`B@fV^uglq^ z8EL~rF}29m9DFnkn#{9YSQ|Tjg^N@{2ZLdWDC2%zVOwQ_d*74K<+&;NS+Pn~O4{QS zXsxTEb-wqP(a%{yXy4I{|NJW&+S9-OFSztmXPod}0Y-Bw6J=|$nGew_aFV&< z8T4X-cP2gL_dlVbJvaLMTUIg0+7t{8Een(7|5Vk8sFNZ7wHM&NYyYz!^1t+p_IcDW zr=((f8hI9}3rJI(Q79*Z0+8^j!Zcon^41Hr+uNia=cCgoSZRv9S7!Tocj(_9(cBqmg7TMd>j7$>vdWZNM{x48|plCt2wLFIX(S-$~a)KExc!-VNl-NlA2+iR)tU?je>e~fi6Mn#Er zjmC)g^=W)xcz=WluTm`U+{(BD6#_75x}@Ma!~(Whu0yU5|3y)&E3z!KMFKwP=;OH^ zp5flvGbTITEJY&bt~Mz2CHC?oA|~u6Zk$&V1S~dzqZWfA<>qh-4tNXW{?JXEh)4n6 z{0sdwpEDu0(Bl9`M4T@MtU_737EHc0N+qz6^b1$EunMTLwO;J}o%A2ePE?0!ryO@+ zk_lvUN7(4()-}c-4>6^1ngamKNJk9@r7ZV64Rh2n6K8wmlzWvy@nC#xJOr01$!6R* zp|%>qY-3R%c0S0*NEt6>@5MK4dMBgSRpvyGrcL)R8c$C)9zf$*kC zPpC_g;$dha{6(B=vHF_evCsJfbY!q@+Fyp+3; z##5@*`xw{LWjeh;ku-sTP};m3I||+_pES0U8Z}=V+5gzvRo2|oEAx6$9Spkc%tDX; zSHrPI;v(h;ei5pve4=gFN4SW0wVv@3{pcju-2RLV5nlVrZ;LhQnJg|($e0)lEhhxh zPD*aF&SZ2KtS@~S1#VZql}y#7V?||Vp>t4WYGZ@Q-#q)r96m6WY8Z;KXwnzjWIhPK zqg&jTfMpY9jJGk!suz^Z6}*=l4TtEuFQ;Bb=?du9&vR}poR03E_WOf3LSA~hZH9TC zA&tqzt)0c~8aR}g*rvrrL9J&OwoCc(vZFV`O;nEWGqWmlHy*Pp{tF2;tEARj@_XlL z|Br$`gI9s2uj~YbUiyRFE%_~D6g*aw=By5|5n};8O>qM4=Sf@1D^t;*9{WiL&sg$1CXjTukp*q<1z!rrKGjVi2&L2XBA=Kn#cD2^Xd zHO{`Val4FS_DU=sg}P@XmLK?ne`5FmI{$>LcJA_YE7gL6^xPlnX*DUM0$r|M4#PB= zDVfgZ1(Yr$vUBoz#^$Zg-x$RnR#Hg2BxYSS1y zYvL|n_Hq~KM13*EzNIb3QoiJ+he7S3Xpns7NrQR8T&SQ3nIM_@J;gU1OIMxmQ;F^U z0F5|!I~7iIH7A6J@^Oxt=!TDBL%X~$g7J3aw}~3_dy4Tlz7WvRm0p=O;p%Qg%=G1z ziM||d{g^fEzVv!gq%3C9O^1VOP?dYde;`FiQ_ikQz3SH)=K7DY(INaZ%H%q!+f)(5 zti;?`RPJqbQuc!Ey6(k_>f79Q+cgSV+F671ZdVdo(w+YpMM52*BNqVRdTJP^#!nH4XlSN~(7x zkInY_sNImtysN&gje*U4Su|Y=3Lx~jK0*7;p`h3l`R_f&&C@^!BK6yb-!2xZ$O++| z;>x}^=RR_n^X`%YE;cyFs?3*wJj%e=-gAwnu5u_SpI%tLkVtzj^0JMFidq z#-^>-q-f(VfDSep;rF`lz{!!F{4$H)vNI;yWe6dPdm^i|WXTRT`g(gwAL=^pBFwF2J4~;l| zhH!dSZoz{?f-vJh!|DGv!1{j>D1V)&KXbfF-kiVpG|a~G-uI!QY5im}MV;6*)&4tJ zq72|4G==u@^WPJ6)Fm}v0!$S6Xy91oL_-TxSA_8Y+ZaP*4HNf_M0i&GJ*D}d4`Kfg z()fQ%)XzoqVh=vQS51NIPY-FNf!HX@!PZ``+AsHX*^_$nE)}vw#SdDMI+?z3Lonh1 zfBne4prARo02?hj_Y)+cYT&bC6;jx@`cf`q!U+i|v{D-^v7!O&{;cP=-Q)qEXL=u> zXCGL=V>4M%&pE3o-c6qbdOpHur}-gP0co_bD*Wh>Xgqf!Na*4az4~BAIIr$UO?zMB zPA@Ek0+dN}eHId4boZ6v2`j={rw)0a3^4kYpmQ>8FLJ7U6`Cu01H)<-stN8 z;-@&MQoj@^{xC?0ST}SSU0Wj;KV8X7wVyL?lL8J2TostcJ{g~BG;%@(B$NsLhpU_jL33;E=+0x8R4V$+c~(*S zz-@bcK)pnBW}X!n4N4l#w>_OS4=Vzu+TwPU#Q6&r5P&7`{rTPc<@4h%8=9eNgmf)k zVOlal)Iz#xlsIF3WF{hQ@N5oHEM92v41s~x+6ge2FwJTQ>3LmIMkbyhHvKtIrtnh{i-kF0 zv6EUW#o)>?RpMYqEI}3^2GVYIIw|$+)1Ys^>tCWhB;zBCHeB>msbhA`lV7Z0sahlu zuvqs=vgVU5sKo@#l=2Ts`)VrH*A1(xswWK)N^jH>NFl}~_6OL`@gd3D*y7TKTcOB# zjpcqfLtR>%vT-Jsx)NDefcfBh2-(dDh36J5>3EVb3YH0Ki9O$mRjYxBw_W%u8RY!x zG;A+nbY4{|fRDz({f3G@xaOz;MszObtG8>T2E|Brqw$DjJ>fY*eti>&nNb&!{z2Y@ zR{K`)X+GIlo2HkWj(BOR#&GLnmy7nkbM717H|^+u8BCl?vPH1l( zy;`b1)yIdq)@mlty>4?{1l@gBNZG*KJ`#CTO}1gejGFIqJ)8b){_(wV$(Wm)n!M0N zU-;2uY&2F`S6bibh4*_mBsDZeG(@4MDW|;UT@(o?-%y|?7ctx^L~By zUXN-G6RR)N_Us=BKnBERhj*>CiI?|a78;Et&#Ejd-9P7V#kxC;ojMrE4lGcMI?Io+ znfz_@^T#F4_}~#6xnUNu-F?(2Br;EDfcVsX%jS4>Qhs*nzC_qqS!_%7u#H}0U=9@q z(=a(ckc_-Ybza`&hW5kH>!wl}tf052Jjur3S<&nR0Dl264Rnx=NyPmFkOhN&$oph@vjDuKmm)FTNDKFWo2zn=MJA|NcYh&EcsQ(-4+XdvqiucNXU0 zT3NpkqRo5cmxubiznWH`c6f#D7}zm-pM1TXF~fe5o<3(Hku@*RwjOY1zI1JgoJ%)% z(uFzbm`yZOfwp-W1j;|NV(|@8qh)!r?sPw8mxdJNPU+V#_o}|$?5p@_J1fXV# zwecTY=Q}XE@^GwQH8fQRvyfCS@=0}B1rOc#r&0A>POXOW^MkjZ#@oec0Di3KWA3iX zaytkfe7c{cnR@M^+w8!*LPU~<^kwpMFAV!ASyV)WowLT+!gHi|AVh~Q73NJg2c=Q9cr7X9tzvCs@}fwuevlEz z@6>P#uy54q3TK6JX|+sSS?!BVt<^NUX%3f^Z!^%QZP;-a0kINfZ{7}ZafG|EB{gbq z3zmAifm_pj(4mp(Bos5F1pam=il5k`P>~uY);nXO9^^pBuZz;Cge9szP>}6E-Th^D zEoo1aR$16^<+N5!xfGe59fwUVrymD_VWcI)2|^n5rPlsS#MmbHpccDGR@;O9 zEei9#J}KH=v$ymTU!OwqpKtcc)H})B5QmQ$ZauXxK0Slkp+o|&&PpirEdw0q$}PGN z_T8$gF?vU`A(US4?w@&-TMC7xm>87*6=OBq{2)8L8O;iq9d3(BwBH`rM)q{#3?XtWW_Rfw+@5KBlaRR|64(1xYI-gR!Cwr&jpUcmA?8^Khv3|Kp zdwRdN%Egj0*x(r;)F0!1N*(>Wt>S?VWPox_3v(;w)rs%_ELQD8F;CQ= za4ZcIRy2DRu<(9zw?`E7;5E!yxKwd8b@K9rzB}!SQ`VIIb65~|J~Q6XS}V7q>LPU& zBY)?JS_$><|8?V|wrf=bq+>LMu~6rn-`g0~A@$eWIK;IWTK*Fm#pktX=@j+N&fUyA zMcVrO=rNR6#V^*s^XDTYL!QJ4mFz?MK^|jz;;==Er|Q}#8lk<>SH~5}E>47gY18Y; zW>;t2gK|Kvq*Q;+76{vokrXuGi;Y&1PyT*-J@}~72ULT7)+?%(| zGSJ(om>i~<9XEsoUaUTJP{*}8JHyf?fH55*m%WeTP>=4x#`3_bwtj4mAwhpf_P)Els#2P!IzHPieR_t(WhKVYg<*MYnLQvq9l=P#ZUP_gJz!n)AYdx@Txr)=X5k0t+KxLn_ATRGr`*urz2U4ZZ%8*u7jqFT5~y?tBebC=)jN7xs~ncgnbPKb`T5G`!|`@ zbu!?7#Fb`9o*DX+^{b*|2Oe#m)Yimdaic|{=6+GD6}3UhD}HER1)GcvDtFK+N~f|R ztaWdDglwO5>U{xY!1u<{LwA0=;F?k8t&yvTvpC~>l+JNRPWc|5>J(}8ddI(nA)AsZ z&>}Y5)bOO{>2M$Elm-1svU3?o+|@j7Mr`Ir8OP^HepFNVXj#<%*R^~hCi+`BWD zSt>`ePiXiGnst;i&%QoV6H(@6G)B7PhFeaA1o}8o>p6Y?_-F@63qQ}|VG+SIm(=K{vhEB<$juwgrK_MTnP`Aivpj zIlE@}X;+8F_V7g@e|k7^tfVa6^pZxVzkE>{C`zf_Gcl~8ofCh1rv7Ob7Z<6Z(GcJ(mWmUEG$IyIM_vmwP&_N|O|4XltarwII3B2EQ@c zASAn)d$(dUN#YuG7plwOkGi6Wx~5e$3YUNtQ2Nd_)$&Vm^duU|K*UGVCWC7WBgr8ySo5^jrKCxXv%)b4&CDG>Xx0W+rqP}F~ zk-#x5jkqp~UyhfFX%btuj?EFeXbu*C8@)0hYMwKsVx+M)bLgr(VeC@*SXElV+L%S9 zZffDNN&k%~PL6msZ{rGvM`ww^7esr<;k1a{sAsdh`s?MGQH*O7&8;Ut*Kzl%L;aIN zAF8CS`*ZL)d)Y z2?pg1f1YitXD+#WsQmV*nYTb%;1k8poEbyp5^+lpfmQyK$=#}$F*x~C5oqpnVN)#D^G?y6U%P&f-^$hQpYABt+@wbvJrHVU z_(siVQx)b$mVZNz&Fc|;4o-uyTD(WSOT6uTBCaOA41O)RYZiMKho*kx`SJDSG95Bf z`odSoN&^=eX>sSeUB_}X=byIvf$ZbzmQm2;CR$NA17JXp4k0>O?qkqU3#&tS8$)@$ zbg!Z?*SQ$Q5g5yK;V^=|-YgQP6e!|I&c^k_T9xc5FiAtN_`-=97RsVqOa4+w|o z(!qSr?=cQZ)31xUaN(Vk67Anu0I#a3UqU*93ar-!R)q1pk`+&EWdrt8|849-8UZ$9 z#I@!TiS3v;(;SoP<&}+y_tRL3%={ZpdCMQiSQX6TiW-BiBEl7Od%JX#Dcpk(E5q9LO$<@6QYH}@XX+mV>a81w&YXJ zSy7tn=c{mG`RY>vZA2Q>#x~Eu8e|m}LVUu`5LF8ENO>wp((sf`J5H_-saxOuVs*an zVDS=hB!>6s(m%t|Ngz2%bdk$JC{9dCe=V^z)FyooKITfuU1=I1*MQ#@SKV9sz@71s zE66M^QpB6;A|pT7t>HC{uC~U=m{9ngQvb;DDcdXhVBH|AfW#52L)h{FTz2w;si*u} zw_=T+ncIG($|pa4NfWpEakW5BNZYTOd(B=wO&G$mq3b!6-e#M(kKW4lBgGs|%u3Y6?AW*d-n2IR=W|Xoiv`iKw|EAP=w~K9!&#>mt$T zQQd1trxQ&37AQDlKnT;qWC5g;r$ROvniehQG$u1X(AFH~r}n9?u8#mG4>S1#(4_vC z8@xgCKsRgdL&0*Fx?3N|R*F8AJfUw7vM}OljRb3Ea@9r+zbO8)AvN)sR5YA>VCZsd zjAhhp=9a=?c+dedLvU9jkIZuX3V}OL2X4!&iw||7(Z?tj)4yBSi>g?t4{XnZx#hrj zFOH>g@K)zZ7%jlWT?A+2&kKEu(=d3mCds`>W>v0wGut9}RhW^VH&#RsPlT1_W`1{r zOLy#wn|h_Z(1%P;?2jWQT$s!saJ2H$=S+%m8gK8lcX0QHd~v1qX|YpMkx$HuXivm$kC;ZK5 z;JcT{O-h{lUjY32>Y+B7WAaG61~w}Cw(CwM+B2$a_^_%JK`-Vdw99)`?{u!J8U-&H zYV#8LTbcFAcTJNM+YRtz_zblOYKLV01f{ z*SO>=#ACSN#Q^fsUlfG`WUbs->!Z4YXLAsp$7~_ZlBmYQ=QlQ(TBTk_!BwNiMTj0= zC03cgC9KY?^sXYI7G4@+Z8k11V3r?ROFu1IOJ6VU#fLh8kQVn0t-g|a_b};$ZJ2r% zcETM^J=7|6`wXP#j1;Da1>O}E3zp_#3>#UEpgkmRW#V?pc46(;W5PGf<E^QJ1{D*@LE5b74%>T#XUY@R!iKUPdOGR2zO5M4Qsqzn1I;SzJB*`VHHel_2 zbjH6CvaMeTSu3*i)ixoU7(7^UWl2G!s;+at(0`ro4!(~%7GVMDX?Al9c|*;@{D6Z+ zF3q0$Rw|!?R;eS&{U=NOQth=fhFv~kWljsy~5}%=tUT0B0CPX$+4e)ez-*c6A{!}hT{%AeHB+6_2#pW<*lnp zcOz2FlXhjELk)x#C#bKI6Fn8>3tQX?`@{Od@iF^ufD^L5;NBSY<&SPp)2#2U?w zRW%nC*u%#UP#BTBgp5e6fFNty@-p|RbY|otaXQ^4x#EwGI`XAcMp!TFNY6)m%@*LNI^vxa@5sVB`$#J;rIFdd`jm3M=8 zeN0Z8(R?mm8BYJ@%HRIHmdmro|MUc!4cNVRZ#5H*@zt;p$KnTUeJH^kkricUnOF4J z7V;ok3Zlb`{hH3R58e&MOjoLM zcfi80`0GRkL^gOK;FB;jLua%-EMwnA&_MBdd2H1`2Kw_E4X}q+z zyUj9;#a0k=o?r-)wK>cdYe`l7)`q;$&E>xXIFdq&-#nXd@mJunc)-|*Is7%6OLfy8 zC+U%H8^pnj{ zv+P%LMQlq2+bXwGQIN8t1c5&zx}e8k`BMD((z&NTcX+j`?2{PjAE>$>@be=zp9P9Z z@~7TV#fVp#fmW&bR(J5muq)y22H$zL8!h?7hxWDa0lHTV!!H_8WKUvlR8sq6dHaF# zalK_xD~aJ~lEyZZ!rybay*kSK!NOUfL?rR2Jr(6s1zIx}rtY$jgE8#}1mgv&nxx>U zapYz&run(En|Ewt1s8vDZ6fOGDWZiXA|ceHkZ>q|79Bn{LZM>3(U3_;K!Dhglx@Xv z8F|)N|Cbq^EJQY0Y)XMO?(iIK8PqPx;uxTP=~ZS!n{4?8dAgr6wNQd1hGos?+%HI` zodv|&9HtBs&F7Gruk_rt5~0&=2Ml^w5Agi~`qgb`6lF1@n=@`q($#@yOmw7~>}D1p z2j`A74?JX6)`FFB`~Vt=D$lT;t^nM_!+R9V2E|n>AYU{0Bp+8G3b$uBF;^0_o4Gnrn?=klX|}bd#1y>Q{kO~$U%5~v9kd`F z0#2o_Xl(M-JU-;i=41>S^bxx${C?Sf#&{Z}x3b5{Cyn>@)z!nQ4+EwOtEKN&j0|2Src=%o7hHrrJYjNIFZio;ixM1M_`h1R0H z{)?&I-vQAjAAy+7=kVfUfF>OnhX)>xsYdKpQG0s-=&F;vV-B9?WX|XguE_FL0h)M+ zvh}@%OKH}%d)0!?r3CIY;D8F_-oEm>t&Mi)gV*-0-(-As1%Gs^w$A9`20?V0j4G=& za4r->>W=cGpHR35Gy{-coLcnN-AIrH$p&|Oi{;`_vIC14K1J)rCBvk{4TcI3nlX+E zd=(qf8UD-HJKkEQ63l`jRIr1YZ=FNy!!|gFB;o=n*^gX`zzeHPm>!j>13x zer#qn1vF|f(aJQ~)ElJMFORoRF8iQTVQ~?HGxDl$0ybP6W~`^O6M$hQE53n$kbVTi zqgBRirE6uEsP&@vBof`Y=CO*0ic4|vTKY+}Xf`Jaocah@pEZ0!NhvdDHc}n5%s!&5 zbW6Vib@k2L%S(h9xbW=EXk=oAs~PBqX6#9!3!27nmfmJ}nCc8soiOvEhJ~U?E4auk z`eyHsI_V;Fdjt*Y?2vW-is~^FONPOAyB#l zvsV|cD-IB!h}N%FuQEeVq-m`Vl^B%TE8u7|vqoRr|MkDWx$@4&U)twH-f%>hUWA2& zIG}W4o;VxZ385o!>b|GKoJX1%s#$r20%f$*$T>hv#*j5C#=v&gTe9X`U1XFoBWff~ zeH?0bn0Re?S6GIro_;jBHqe{vC%F^l@rrN=T$Pg)Tslmyi&lgj z0~Cjtj?mD|-NWcO4xm`@yTq=e0i6&meC78)SOJrunUSK{9E1~XDyxToU%EgwtZLWC z$HczU_d7VVNzkts|E^xw#aF(WhHfeG=I1m7mJOtX4~W)ZV#8PQ{#oAnxsqxGL6%Dh zL9Y8Gjo15I9+bs68BRTN6!x!o7e1f#Vny=vjAP2_brKRVqOHdxxf!Axb|7!}E5XE-!1g02=zqM=GvdS^Sk86)@0YKV6J* zj5vK2SgZA#eQqH2i@$#_(_Hvbv#=+}_STG%66kV+=<6`r`MuNEUZiwy=U0YbN$IDb z|3|&`Z{C(yRt~#L-)5)M+wT>=414gz<{qnmPZ<09%A+Fl4c9&fdQM$JW~a z*;w4ZEy{?ST2>~3MTH2_hG8UBNX^Xu)`Rnxn_cNMvfF;DFH5^{b6H*TYB$Z!$!pfc zb*=XWiqWq)AFXJKcNEoG+dNfNd{XZ54ER?3)s{0yZD$1Fr zZ}X2DwZBr1Z@qDt*?2gKnrK!oyn*pruw2dFTzt&h{s>XG$h-PvCNFrlMH@U@0`*nw z%%J2E6cp5on`$&uQF`m;!T&kf)gjBKx_3&{3?56y*bmuO)8FbtX}c2l__?L6HjP=? z3+it)abH5CBTFGeio~kL;R5n}aWJY63FTKljCpwd0+K6el=lk6Z|HS>>~c6SvX4k` z7E$95EU(|#&&$lkr;W+CAeON?gsoi}!8jmbBGN9>yNH+-Vr`wPiHhRXch~nm@L>=W|?S~RP13f-G8FWx!ZhU`Y zeUyL@StMR~H|_&LXPFsa?frf4`ygflhhHZ^_nyRjnBPC=lFMo3w-}WKh&7NfuMapq z^i2M~-H`*kcQrFQT2~lJV??5@;ipUTl*psg>+z8O#THnAEedXzuT{1~uDabHzF*EA z|2hm+`foR>A;s!6iT)e0WoVITjl1T}XyFIDkEfS+xMV%qI2_&uKg=7?m1k4h&8=L0 zu?U*Fm?2Ku3VIZw2WLMhLByv}`ca8s-4@=x@&)QZQzdw^P#R7JALg;c4wzsdt}Rtf z(b3gbKX$FXx0$n9$rG&7z=3}0gslTb?A!Q_YLpb$?re6$C+SGoWrJv3?MbVvz^i{b za*jXiDE8pdMa+9-J$b}Oy}K`+ei0XNbE_%=6Wab%F5}H$K74#kUL8#n73nAU23pAt zuoG*MRM(@tJr9Cl4(Ao}TC3#U`#_;T01(=?5yQE=D%qULKbp17FYWhgm7j4>6y%xH z(@?VSdZtq)S1N~rSYm7}W4z7S8HYO81mYXUhsLSBopo(k*=|rJrG^?P|zy=Z!dG2}dzw`@E#b&Fs?T9J8G3Vnl2uu)_N8PDxi%U+;$MkcKtRZsWN zZ}M%)n&uHmnpvS+kyK?XUh0zwuSNuDD41R`(l}w?(en2XX{UF||NN42@%Fi{^ybh$ zHsvs3R3j5LaHQ^SO{P{xaO6dpL+h$EKt|2>s6++ zlZvzsdKPh2fX}$v{)26>o(SEOeHG*Ku3gJK0+;5gd^@ef2GF-~LQ%k};W-FsA4%9@ zWDDX`$Bo0*QaAqr<{eSRtR;)+AS5gwWKfb9Y0nY(WQ`n(ftH##A|1hs0CHePg`0a8 z224#{C0HWej${db>kNl{9iTnlWgr4J`ojl{j*0R3TaWx!H+K5xdHmdED;h)Xr-vHO zrmg}w7P-iVv%3{ss%#^aNM{N&>5;(GfRNfNDlB*eZjE|;0+ZqrioV5Q{jtz)@-zrA z0F1KEMs3lht$S^?<^SB`$N`|Jz}@qAuH|1Iy@&5o{TgwH{cPmL&Cm@w@Up-z0)g)OQ3e2H|f)VH+( zb?ZpY-~RH?D}69)f__Nq6NqOK?Hum3PkxG3HAOEi$Dx$hjzl_1aMpv%ft5;qP9O~whl>POzb>KxwW(0W?lsMy@jb$3m45KE}qc?NqqiQ)f zG=eV<68o4*4r5|@8~t+Y$UW$(zakCcPSr9S=p`gXw*_9^wZH#Ei#hm0+zKb=*6!*J zPV!?e=EJXQ7+RredP_i$bN3W9OEX2*>}A;QHj@p>o2y&soJaMum^`Shl@-9h#oxRF zC!aPf+kvAn9#mI5t=k~^67sW z;5Ie~2M&JiaM8&WS7Ij+k50~TgdkH9}>R) lOJURhEK}^i`NdB84!01se`MRCWZS<0d;#)H)w!#`{wpCHi?sj% literal 0 HcmV?d00001 diff --git a/frontend/src/components/jobs/result/visualizer/elements/const.js b/frontend/src/components/jobs/result/visualizer/elements/const.js index 3ba12f827b..7e0d1a211d 100644 --- a/frontend/src/components/jobs/result/visualizer/elements/const.js +++ b/frontend/src/components/jobs/result/visualizer/elements/const.js @@ -4,4 +4,5 @@ export const VisualizerComponentType = Object.freeze({ VLIST: "vertical_list", HLIST: "horizontal_list", TITLE: "title", + TABLE: "table", }); diff --git a/frontend/src/components/jobs/result/visualizer/elements/table.jsx b/frontend/src/components/jobs/result/visualizer/elements/table.jsx new file mode 100644 index 0000000000..f684a74e8e --- /dev/null +++ b/frontend/src/components/jobs/result/visualizer/elements/table.jsx @@ -0,0 +1,79 @@ +import React from "react"; +import PropTypes from "prop-types"; +import { DataTable, DefaultColumnFilter } from "@certego/certego-ui"; + +import { VerticalListVisualizer } from "./verticalList"; +import { HorizontalListVisualizer } from "./horizontalList"; + +function getAccessor(element) { + if ([VerticalListVisualizer, HorizontalListVisualizer].includes(element.type)) + // recursive call + return element.props.values.map((val) => getAccessor(val)).flat(); + return element.props.value; +} + +export function TableVisualizer({ + id, + size, + columns, + data, + pageSize, + disableFilters, + disableSortBy, +}) { + const tableColumns = []; + + columns.forEach((column) => { + const columnHeader = column.replaceAll("_", " "); + tableColumns.push({ + Header: columnHeader, + id: column, + accessor: (row) => getAccessor(row[column]), + Cell: ({ + cell: { + row: { original }, + }, + }) => original[column], + disableFilters, + disableSortBy, + Filter: DefaultColumnFilter, + }); + }); + + const tableConfig = {}; + const tableInitialState = { + pageSize, + }; + + return ( +
+ +
+ ); +} + +TableVisualizer.propTypes = { + id: PropTypes.string.isRequired, + size: PropTypes.string.isRequired, + columns: PropTypes.arrayOf(PropTypes.string).isRequired, + data: PropTypes.array.isRequired, + pageSize: PropTypes.number, + disableFilters: PropTypes.bool, + disableSortBy: PropTypes.bool, +}; + +TableVisualizer.defaultProps = { + pageSize: 5, + disableFilters: false, + disableSortBy: false, +}; diff --git a/frontend/src/components/jobs/result/visualizer/elements/verticalList.jsx b/frontend/src/components/jobs/result/visualizer/elements/verticalList.jsx index 3c91e99eee..ca04c1a2f1 100644 --- a/frontend/src/components/jobs/result/visualizer/elements/verticalList.jsx +++ b/frontend/src/components/jobs/result/visualizer/elements/verticalList.jsx @@ -25,47 +25,65 @@ export function VerticalListVisualizer({ }) { const [isListOpen, setIsListOpen] = useState(startOpen); const toggleList = () => setIsListOpen(!isListOpen); - const color = name.props.color.replace("bg-", ""); + let color = ""; + if (name) color = name.props.color.replace("bg-", ""); return (
- - -
- - - - - {values.map((listElement, index) => ( - - {listElement} - - ))} - - - +
{name}
+ {isListOpen ? ( + + ) : ( + + )} +
+ + + + + {values.map((listElement, index) => ( + + {listElement} + + ))} + + + + ) : ( + + {values.map((listElement) => ( + + {listElement} + + ))} + + )} ); } VerticalListVisualizer.propTypes = { size: PropTypes.string.isRequired, - name: PropTypes.element.isRequired, + name: PropTypes.element, values: PropTypes.arrayOf(PropTypes.element).isRequired, alignment: PropTypes.string, startOpen: PropTypes.bool, @@ -77,4 +95,5 @@ VerticalListVisualizer.defaultProps = { alignment: "", startOpen: false, disable: false, + name: null, }; diff --git a/frontend/src/components/jobs/result/visualizer/validators.js b/frontend/src/components/jobs/result/visualizer/validators.js index 5107b6816e..7ceafe06c1 100644 --- a/frontend/src/components/jobs/result/visualizer/validators.js +++ b/frontend/src/components/jobs/result/visualizer/validators.js @@ -49,6 +49,7 @@ function parseComponentType(value) { VisualizerComponentType.BOOL, VisualizerComponentType.VLIST, VisualizerComponentType.HLIST, + VisualizerComponentType.TABLE, ].includes(value) ) { return value; @@ -100,6 +101,17 @@ function parseElementList(rawElementList) { ); } +// parse list of dict with this format {key: Element} +function parseElementListOfDict(rawElementList) { + return rawElementList?.map((additionalElementrawData) => { + const obj = {}; + Object.entries(additionalElementrawData).forEach(([key, value]) => { + obj[key] = parseElementFields(value); + }); + return obj; + }); +} + // parse a single element function parseElementFields(rawElement) { // HList and Title don't have disable field, they will not be used @@ -129,7 +141,9 @@ function parseElementFields(rawElement) { break; } case VisualizerComponentType.VLIST: { - validatedFields.name = parseElementFields(rawElement.name); + if (rawElement.name !== null) + validatedFields.name = parseElementFields(rawElement.name); + else validatedFields.name = rawElement.name; validatedFields.values = parseElementList(rawElement.values || []); validatedFields.startOpen = parseBool(rawElement.start_open); break; @@ -139,6 +153,16 @@ function parseElementFields(rawElement) { validatedFields.value = parseElementFields(rawElement.value); break; } + case VisualizerComponentType.TABLE: { + validatedFields.data = parseElementListOfDict(rawElement.data || []); + validatedFields.columns = rawElement.columns.map((column) => + parseString(column), + ); + validatedFields.pageSize = rawElement.page_size; + validatedFields.disableFilters = parseBool(rawElement.disable_filters); + validatedFields.disableSortBy = parseBool(rawElement.disable_sort_by); + break; + } // base case default: { validatedFields.value = parseString(rawElement.value); diff --git a/frontend/src/components/jobs/result/visualizer/visualizer.jsx b/frontend/src/components/jobs/result/visualizer/visualizer.jsx index d21d72cb68..49350d5607 100644 --- a/frontend/src/components/jobs/result/visualizer/visualizer.jsx +++ b/frontend/src/components/jobs/result/visualizer/visualizer.jsx @@ -13,6 +13,7 @@ import { VisualizerComponentType } from "./elements/const"; import { getIcon } from "./icons"; import { HorizontalListVisualizer } from "./elements/horizontalList"; +import { TableVisualizer } from "./elements/table"; /** * Convert the validated data into a VisualizerElement. @@ -70,7 +71,11 @@ function convertToElement(element, idElement, isChild = false) { key={idElement} id={idElement} size={element.size} - name={convertToElement(element.name, `${idElement}-vlist`)} + name={ + element.name + ? convertToElement(element.name, `${idElement}-vlist`) + : null + } values={element.values.map((additionalElement, index) => convertToElement( additionalElement, @@ -98,6 +103,32 @@ function convertToElement(element, idElement, isChild = false) { ); break; } + case VisualizerComponentType.TABLE: { + visualizerElement = ( + { + const obj = {}; + Object.entries(additionalElement).forEach( + ([key, value], valueIndex) => { + obj[key] = convertToElement( + value, + `${idElement}-table-item${index}-value${valueIndex}`, + ); + }, + ); + return obj; + })} + pageSize={element.pageSize} + disableFilters={element.disableFilters} + disableSortBy={element.disableSortBy} + /> + ); + break; + } default: { visualizerElement = ( ({ + ...jest.requireActual("react-router-dom"), + useLocation: () => ({ + pathname: "localhost/jobs/123/visualizer", + }), +})); + +describe("TableVisualizer component", () => { + test("required-only params", async () => { + const { container } = render( + + ), + }, + ]} + />, + ); + + // check id + const idElement = container.querySelector("#test-id"); + expect(idElement).toBeInTheDocument(); + // check size + expect(idElement.className).toBe("col-6"); + // check table component + const tableComponent = screen.getByRole("table"); + expect(tableComponent).toBeInTheDocument(); + // check column header + const columnHeader = screen.getByText("column name"); + expect(columnHeader).toBeInTheDocument(); + // check toggle sort by + const toggleSortByButton = screen.getByTitle("Toggle SortBy"); + expect(toggleSortByButton).toBeInTheDocument(); + // check filter + const filterComponent = container.querySelector( + "#datatable-select-column_name", + ); + expect(filterComponent).toBeInTheDocument(); + // check cell text (base visualizer) + const cellComponent = screen.getByText("base visualizer test"); + expect(cellComponent).toBeInTheDocument(); + // check color, bold and italic + expect(cellComponent.className).toBe(" "); + // check tooltip + const user = userEvent.setup(); + await user.hover(cellComponent); + await waitFor(() => { + const tooltipElement = screen.getByRole("tooltip"); + expect(tooltipElement).toBeInTheDocument(); + }); + }); + + test("all params", async () => { + const { container } = render( + + ), + }, + ]} + pageSize={3} + disableFilters + disableSortBy + />, + ); + + // check id + const idElement = container.querySelector("#test-id"); + expect(idElement).toBeInTheDocument(); + // check size + expect(idElement.className).toBe("col-6"); + // check table component + const tableComponent = screen.getByRole("table"); + expect(tableComponent).toBeInTheDocument(); + // check column header + const columnHeader = screen.getByText("column name"); + expect(columnHeader).toBeInTheDocument(); + // check cell text (base visualizer) + const cellComponent = screen.getByText("base visualizer test"); + expect(cellComponent).toBeInTheDocument(); + // check color, bold and italic + expect(cellComponent.className).toBe(" "); + // check tooltip + const user = userEvent.setup(); + await user.hover(cellComponent); + await waitFor(() => { + const tooltipElement = screen.getByRole("tooltip"); + expect(tooltipElement).toBeInTheDocument(); + }); + }); +}); diff --git a/frontend/tests/components/jobs/result/visualizer/validators.test.js b/frontend/tests/components/jobs/result/visualizer/validators.test.js index ec9036254e..21e6e11d5a 100644 --- a/frontend/tests/components/jobs/result/visualizer/validators.test.js +++ b/frontend/tests/components/jobs/result/visualizer/validators.test.js @@ -16,6 +16,7 @@ describe("visualizer data validation", () => { { type: "title", title: { type: "base" }, value: { type: "base" } }, { type: "horizontal_list", value: [] }, { type: "vertical_list", name: { type: "base" }, value: [] }, + { type: "table", columns: [], data: [] }, ], }, }); @@ -132,6 +133,17 @@ describe("visualizer data validation", () => { type: "vertical_list", values: [], }, + { + alignment: "around", + size: "col-auto", + type: "table", + columns: [], + pageSize: undefined, + disableFilters: false, + disableSortBy: false, + data: [], + disable: false, + }, ], }, }); @@ -286,6 +298,33 @@ describe("visualizer data validation", () => { }, ], }, + { + type: "table", + size: "6", + alignment: "start", + columns: ["column_name"], + data: [ + { + column_name: { + type: "base", + value: "placeholder", + icon: "it", + color: "success", + link: "https://google.com", + bold: true, + italic: true, + disable: false, + size: "1", + alignment: "start", + copy_text: "placeholder", + description: "description", + }, + }, + ], + page_size: 7, + disable_filters: true, + disable_sort_by: true, + }, ], }, }); @@ -458,6 +497,34 @@ describe("visualizer data validation", () => { }, ], }, + { + type: "table", + size: "col-6", + alignment: "start", + columns: ["column_name"], + data: [ + { + column_name: { + type: "base", + value: "placeholder", + icon: "it", + color: "bg-success", + link: "https://google.com", + bold: true, + italic: true, + disable: false, + size: "col-1", + alignment: "start", + copyText: "placeholder", + description: "description", + }, + }, + ], + disable: false, + pageSize: 7, + disableFilters: true, + disableSortBy: true, + }, ], }, levelSize: "h5", diff --git a/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx b/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx index 60b39da95c..595b125d82 100644 --- a/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx +++ b/frontend/tests/components/jobs/result/visualizer/visualizer.test.jsx @@ -160,6 +160,33 @@ describe("test VisualizerReport (conversion from backend data to frontend compon disable: false, alignment: "center", }, + { + type: "table", + size: "auto", + alignment: "start", + columns: ["column_name"], + data: [ + { + column_name: { + type: "base", + value: "placeholder", + icon: "it", + color: "success", + link: "https://google.com", + bold: true, + italic: true, + disable: false, + size: "1", + alignment: "start", + copy_text: "placeholder", + description: "description", + }, + }, + ], + page_size: 5, + disable_filters: true, + disable_sort_by: true, + }, ], alignment: "around", }, @@ -180,7 +207,7 @@ describe("test VisualizerReport (conversion from backend data to frontend compon expect(firstLevelId).toBeInTheDocument(); const secondLevelId = container.querySelector("#page105-level2"); expect(secondLevelId).toBeInTheDocument(); - // check the first line has vlist and title and NOT base and bool + // check the first line has vlist, title and table and NOT base and bool const vListComponent = within(container.firstChild.firstChild).getByText( "vlist title", ); @@ -189,6 +216,10 @@ describe("test VisualizerReport (conversion from backend data to frontend compon "title title", ); expect(titleComponent).toBeInTheDocument(); + const tableComponent = within(container.firstChild.firstChild).getByText( + "column name", + ); + expect(tableComponent).toBeInTheDocument(); expect( within(container.firstChild.firstChild).queryByText("base component"), ).toBeNull(); diff --git a/tests/api_app/visualizers_manager/test_classes.py b/tests/api_app/visualizers_manager/test_classes.py index e0018b7f44..ecfe166f0c 100644 --- a/tests/api_app/visualizers_manager/test_classes.py +++ b/tests/api_app/visualizers_manager/test_classes.py @@ -16,6 +16,7 @@ VisualizableLevelSize, VisualizableObject, VisualizablePage, + VisualizableTable, VisualizableTitle, VisualizableVerticalList, Visualizer, @@ -214,6 +215,78 @@ def test_to_dict_values_empty(self): } self.assertEqual(vvl.to_dict(), expected_result) + def test_to_dict_name_null(self): + value = VisualizableBase( + value="", color=VisualizableColor.DANGER, link="http://test_value" + ) + vvl = VisualizableVerticalList(value=[value]) + expected_result = { + "alignment": "center", + "type": "vertical_list", + "name": None, + "disable": True, + "start_open": True, + "size": "auto", + "values": [], + } + self.assertEqual(vvl.to_dict(), expected_result) + + +class VisualizableTableTestCase(CustomTestCase): + def test_to_dict(self): + data = [ + { + "column_name": VisualizableBase( + value="test_value", color=VisualizableColor.DANGER + ) + } + ] + columns = ["column_name"] + vvl = VisualizableTable(columns=columns, data=data) + expected_result = { + "size": "auto", + "alignment": "around", + "columns": ["column_name"], + "page_size": 5, + "disable_filters": False, + "disable_sort_by": False, + "type": "table", + "data": [ + { + "column_name": { + "size": "auto", + "alignment": "center", + "disable": True, + "value": "test_value", + "color": "danger", + "link": "", + "icon": "", + "bold": False, + "italic": False, + "copy_text": "test_value", + "description": "", + "type": "base", + } + } + ], + } + self.assertEqual(vvl.to_dict(), expected_result) + + def test_to_dict_data_null(self): + columns = ["column_name"] + vvl = VisualizableTable(columns=columns, data=[]) + expected_result = { + "size": "auto", + "alignment": "around", + "columns": ["column_name"], + "page_size": 5, + "disable_filters": False, + "disable_sort_by": False, + "type": "table", + "data": [], + } + self.assertCountEqual(vvl.to_dict(), expected_result) + class VisualizableHorizontalListTestCase(CustomTestCase): def test_to_dict(self): From 7a162cbf8a7ecc2468838ab192fd169a881ef710 Mon Sep 17 00:00:00 2001 From: Matteo Lodi <30625432+mlodic@users.noreply.github.com> Date: Wed, 22 May 2024 16:35:22 +0200 Subject: [PATCH 04/17] added publication --- docs/source/Introduction.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/source/Introduction.md b/docs/source/Introduction.md index 184139718c..3a44ed502b 100644 --- a/docs/source/Introduction.md +++ b/docs/source/Introduction.md @@ -18,10 +18,11 @@ Main features: ## Publications and media To know more about the project and its growth over time, you may be interested in reading the following official blog posts and/or videos: +- [The Honeynet Workshop: Denmark 2024](https://github.com/intelowlproject/thp_workshop_2024) - [Certego Blog: v6 Announcement (in Italian)](https://www.certego.net/blog/intelowl-six-release/) - [HackinBo 2023 Presentation (in Italian)](https://www.youtube.com/watch?v=55GKEZoDBgU) - [Certego Blog: v.5.0.0 Announcement](https://www.certego.net/blog/intelowl-v5-released) -- [Youtube demo](https://youtu.be/pHnh3qTzSeM) +- [Youtube demo: IntelOwl v4](https://youtu.be/pHnh3qTzSeM) - [Certego Blog: v.4.0.0 Announcement](https://www.certego.net/en/news/intel-owl-release-v4-0-0/) - [Honeynet Blog: v3.0.0 Announcement](https://www.honeynet.org/2021/09/13/intel-owl-release-v3-0-0/) - [Intel Owl on Daily Swig](https://portswigger.net/daily-swig/intel-owl-osint-tool-automates-the-intel-gathering-process-using-a-single-api) From 08b2fc0966eb8499637fc1f7d5abb47e889b86d5 Mon Sep 17 00:00:00 2001 From: Martina Carella Date: Wed, 22 May 2024 16:38:32 +0200 Subject: [PATCH 05/17] fix icon id (#2339) --- frontend/src/components/jobs/result/pluginReportTables.jsx | 4 ++-- .../tests/components/jobs/result/pluginReportTable.test.jsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/components/jobs/result/pluginReportTables.jsx b/frontend/src/components/jobs/result/pluginReportTables.jsx index 79be3188f7..f6d88fd6c5 100644 --- a/frontend/src/components/jobs/result/pluginReportTables.jsx +++ b/frontend/src/components/jobs/result/pluginReportTables.jsx @@ -83,12 +83,12 @@ const tableProps = {
{value}
{ expect(screen.getAllByText("SUCCESS")[1]).toBeInTheDocument(); // status expect(screen.getByText("TEST_ANALYZER")).toBeInTheDocument(); // name const infoIcon = container.querySelector( - `#pluginReport-infoicon__TEST_ANALYZER`, + `#pluginReport-infoicon__analyzer_174`, ); expect(infoIcon).toBeInTheDocument(); expect(screen.getByText("0.07")).toBeInTheDocument(); // process time From c45c84ad764e7e22add11c4e51761135fb12dda7 Mon Sep 17 00:00:00 2001 From: Daniele Rosetti Date: Wed, 22 May 2024 17:37:55 +0200 Subject: [PATCH 06/17] updated unittest --- tests/api_app/pivots_manager/test_models.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/api_app/pivots_manager/test_models.py b/tests/api_app/pivots_manager/test_models.py index 3a9ab1f1f8..30d48372a5 100644 --- a/tests/api_app/pivots_manager/test_models.py +++ b/tests/api_app/pivots_manager/test_models.py @@ -1,6 +1,7 @@ from django.core.exceptions import ValidationError from django.db import transaction +from api_app.analyzers_manager.constants import AllTypes from api_app.analyzers_manager.models import AnalyzerConfig from api_app.connectors_manager.models import ConnectorConfig from api_app.models import Job, PythonModule @@ -95,9 +96,9 @@ def test_create_job_multiple_file(self): python_module=PythonModule.objects.filter( base_path="api_app.pivots_manager.pivots" ).first(), - playbook_to_execute=PlaybookConfig.objects.get( - name="Sample_Static_Analysis" - ), + playbook_to_execute=PlaybookConfig.objects.filter( + disabled=False, type__icontains=AllTypes.FILE.value + ).first(), ) with open("test_files/file.exe", "rb") as f: content = f.read() From f4e716aaa168a29f9ddc5fb425989c64e029e232 Mon Sep 17 00:00:00 2001 From: Nilay Gupta <102874321+g4ze@users.noreply.github.com> Date: Mon, 27 May 2024 02:15:50 +0530 Subject: [PATCH 07/17] Vulners#1257 (#2340) * vulners * vulners wrapper * docs * lesser variables * migrations * code quality * migration * code --------- Co-authored-by: g4ze --- .../0091_analyzer_config_vulners.py | 235 ++++++++++++++++++ .../observable_analyzers/vulners.py | 65 +++++ docs/source/Usage.md | 3 +- 3 files changed, 302 insertions(+), 1 deletion(-) create mode 100644 api_app/analyzers_manager/migrations/0091_analyzer_config_vulners.py create mode 100644 api_app/analyzers_manager/observable_analyzers/vulners.py diff --git a/api_app/analyzers_manager/migrations/0091_analyzer_config_vulners.py b/api_app/analyzers_manager/migrations/0091_analyzer_config_vulners.py new file mode 100644 index 0000000000..98efca1de6 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0091_analyzer_config_vulners.py @@ -0,0 +1,235 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": { + "minute": "0", + "hour": "0", + "day_of_week": "*", + "day_of_month": "*", + "month_of_year": "*", + }, + "update_schedule": None, + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "Vulners", + "description": "[Vulners](vulners.com) is the most complete and the only fully correlated security intelligence database, which goes through constant updates and links 200+ data sources in a unified machine-readable format. It contains 8 mln+ entries, including CVEs, advisories, exploits, and IoCs — everything you need to stay abreast on the latest security threats.", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": False, + "maximum_tlp": "AMBER", + "observable_supported": ["generic"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "score_AI", + "type": "bool", + "description": "Score any vulnerability with Vulners AI.\r\nDefault: False", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "api_key_name", + "type": "str", + "description": "api key for vulners", + "is_secret": True, + "required": True, + }, + { + "python_module": { + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "skip", + "type": "int", + "description": "skip parameter for vulners analyzer", + "is_secret": False, + "required": False, + }, + { + "python_module": { + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "size", + "type": "int", + "description": "size parameter for vulners analyzer", + "is_secret": False, + "required": False, + }, +] + +values = [ + { + "parameter": { + "python_module": { + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "score_AI", + "type": "bool", + "description": "Score any vulnerability with Vulners AI.\r\nDefault: False", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Vulners", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": False, + "updated_at": "2024-05-22T18:49:52.056060Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "skip", + "type": "int", + "description": "skip parameter for vulners analyzer", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Vulners", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": 0, + "updated_at": "2024-05-23T06:45:24.105426Z", + "owner": None, + }, + { + "parameter": { + "python_module": { + "module": "vulners.Vulners", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "size", + "type": "int", + "description": "size parameter for vulners analyzer", + "is_secret": False, + "required": False, + }, + "analyzer_config": "Vulners", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": 5, + "updated_at": "2024-05-23T06:45:24.109831Z", + "owner": None, + }, +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("analyzers_manager", "0090_analyzer_config_cycat"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/observable_analyzers/vulners.py b/api_app/analyzers_manager/observable_analyzers/vulners.py new file mode 100644 index 0000000000..2efc53f476 --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/vulners.py @@ -0,0 +1,65 @@ +import logging + +import requests + +from api_app.analyzers_manager import classes +from tests.mock_utils import MockUpResponse, if_mock_connections, patch + +logger = logging.getLogger(__name__) + + +class Vulners(classes.ObservableAnalyzer): + """ + This analyzer is a wrapper for the vulners project. + """ + + score_AI: bool = False + skip: int = 0 + size: int = 5 + _api_key_name: str + url = "https://vulners.com/api/v3" + + def search_ai(self): + return requests.post( + url=self.url + "/ai/scoretext/", + headers={"Content-Type": "application/json"}, + json={"text": self.observable_name, "apiKey": self._api_key_name}, + ) + + def search_databse(self): + return requests.post( + url=self.url + "/search/lucene", + headers={"Content-Type": "application/json"}, + json={ + "query": self.observable_name, + "skip": self.size, + "size": self.skip, + "apiKey": self._api_key_name, + }, + ) + + def run(self): + response = None + if self.score_AI: + response = self.search_ai() + else: + response = self.search_databse() + response.raise_for_status() + return response.json() + + # this is a framework implication + def update(self) -> bool: + pass + + @classmethod + def _monkeypatch(cls): + response = {"result": "OK", "data": {"score": [6.5, "NONE"]}} + patches = [ + if_mock_connections( + patch( + "requests.post", + return_value=MockUpResponse(response, 200), + ), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/docs/source/Usage.md b/docs/source/Usage.md index b605ee4209..a13076d12d 100644 --- a/docs/source/Usage.md +++ b/docs/source/Usage.md @@ -255,7 +255,8 @@ The following is the list of the available analyzers you can run out-of-the-box. * `TweetFeed`: [TweetFeed](https://tweetfeed.live/) collects Indicators of Compromise (IOCs) shared by the infosec community at Twitter.\r\nHere you will find malicious URLs, domains, IPs, and SHA256/MD5 hashes. * `HudsonRock`: [Hudson Rock](https://cavalier.hudsonrock.com/docs) provides its clients the ability to query a database of over 27,541,128 computers which were compromised through global info-stealer campaigns performed by threat actors. * `CyCat`: [CyCat](https://cycat.org/) or the CYbersecurity Resource CATalogue aims at mapping and documenting, in a single formalism and catalogue available cybersecurity tools, rules, playbooks, processes and controls. - +* `Vulners`: [Vulners](vulners.com) is the most complete and the only fully correlated security intelligence database, which goes through constant updates and links 200+ data sources in a unified machine-readable format. It contains 8 mln+ entries, including CVEs, advisories, exploits, and IoCs — everything you need to stay abreast on the latest security threats. + ##### Generic analyzers (email, phone number, etc.; anything really) Some analyzers require details other than just IP, URL, Domain, etc. We classified them as `generic` Analyzers. Since the type of field is not known, there is a format for strings to be followed. From fd2ac9fef611f1f67078d0d4fad8c025a95205ec Mon Sep 17 00:00:00 2001 From: Matteo Lodi <30625432+mlodic@users.noreply.github.com> Date: Mon, 27 May 2024 23:11:17 +0200 Subject: [PATCH 08/17] bump 6.0.3 --- docker/.env | 2 +- docs/source/schema.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/.env b/docker/.env index bc984be7da..2306fca63a 100644 --- a/docker/.env +++ b/docker/.env @@ -1,6 +1,6 @@ ### DO NOT CHANGE THIS VALUE !! ### It should be updated only when you pull latest changes off from the 'master' branch of IntelOwl. # this variable must start with "REACT_APP_" to be used in the frontend too -REACT_APP_INTELOWL_VERSION="v6.0.2" +REACT_APP_INTELOWL_VERSION="v6.0.3" # if you want to use a nfs volume for shared files # NFS_ADDRESS= diff --git a/docs/source/schema.yml b/docs/source/schema.yml index 62bc131470..2e361f4fe2 100644 --- a/docs/source/schema.yml +++ b/docs/source/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: IntelOwl API specification - version: 6.0.2 + version: 6.0.3 paths: /api/analyze_file: post: From a3b230bba83d89a7cd1df5b467a262120b48f223 Mon Sep 17 00:00:00 2001 From: Daniele Rosetti Date: Tue, 28 May 2024 15:25:21 +0200 Subject: [PATCH 09/17] updated docs --- docs/source/Contribute.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/source/Contribute.md b/docs/source/Contribute.md index dc199d2295..3dfe00dd34 100644 --- a/docs/source/Contribute.md +++ b/docs/source/Contribute.md @@ -337,7 +337,11 @@ To do so, some utility classes have been made: VisualizableLevel - Each level corresponds to a line in the final frontend visualizations. Every level is made of a VisualizableHorizontalList. + + Each level corresponds to a line in the final frontend visualizations. Every level is made of a + VisualizableHorizontalList. + The dimension of the level can be customized with the size parameter (1 is the biggest, 6 is the smallest). + Visualizable Level example From 0da8a36705740e0310404cdf43a92236e596eadb Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 14:30:27 +0200 Subject: [PATCH 10/17] Bump django-ses from 4.0.0 to 4.1.0 in /requirements (#2342) Bumps [django-ses](https://github.com/django-ses/django-ses) from 4.0.0 to 4.1.0. - [Release notes](https://github.com/django-ses/django-ses/releases) - [Changelog](https://github.com/django-ses/django-ses/blob/main/CHANGES.md) - [Commits](https://github.com/django-ses/django-ses/compare/v4.0.0...v4.1.0) --- updated-dependencies: - dependency-name: django-ses dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements/project-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index e34fdee9e7..398ccfb1e3 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -7,7 +7,7 @@ django-filter==24.2 django-storages==1.14 django-celery-beat==2.6.0 django-celery-results==2.5.0 -django-ses == 4.0.0 +django-ses == 4.1.0 django-iam-dbauth==0.1.4 django-prettyjson==0.4.1 django-silk==5.1.0 From 94f55b28910c8d6363dc2bdb8a791ab8262bc75f Mon Sep 17 00:00:00 2001 From: Nilay Gupta <102874321+g4ze@users.noreply.github.com> Date: Thu, 30 May 2024 15:23:30 +0530 Subject: [PATCH 11/17] migrate (#2353) Co-authored-by: g4ze --- .../migrations/0092_alter_validin_desc.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 api_app/analyzers_manager/migrations/0092_alter_validin_desc.py diff --git a/api_app/analyzers_manager/migrations/0092_alter_validin_desc.py b/api_app/analyzers_manager/migrations/0092_alter_validin_desc.py new file mode 100644 index 0000000000..a7476a1a84 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0092_alter_validin_desc.py @@ -0,0 +1,36 @@ +from django.db import migrations + + +def migrate(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + plugin_name = "Validin" + correct_description = "[Validin's](https://app.validin.com) API for threat researchers, teams, and companies to investigate historic and current data describing the structure and composition of the internet." + + try: + plugin = AnalyzerConfig.objects.get(name=plugin_name) + plugin.description = correct_description + plugin.save() + except AnalyzerConfig.DoesNotExist: + pass + + +def reverse_migrate(apps, schema_editor): + AnalyzerConfig = apps.get_model("analyzers_manager", "AnalyzerConfig") + plugin_name = "Validin" + original_description = "(Validin's)[https://app.validin.com/docs] API for threat researchers, teams, and companies to investigate historic and current data describing the structure and composition of the internet." + + try: + plugin = AnalyzerConfig.objects.get(name=plugin_name) + plugin.description = original_description + plugin.save() + except AnalyzerConfig.DoesNotExist: + pass + + +class Migration(migrations.Migration): + atomic = False + + dependencies = [ + ("analyzers_manager", "0091_analyzer_config_vulners"), + ] + operations = [migrations.RunPython(migrate, reverse_migrate)] From c5a1d323ccd7b06042fc0232d6f07f20bdff3969 Mon Sep 17 00:00:00 2001 From: Matteo Lodi <30625432+mlodic@users.noreply.github.com> Date: Fri, 31 May 2024 10:06:20 +0200 Subject: [PATCH 12/17] incrementing uwsgi start-up period to due to migration time --- docker/default.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker/default.yml b/docker/default.yml index 49d17d5ec2..5dc13dee13 100644 --- a/docker/default.yml +++ b/docker/default.yml @@ -26,8 +26,8 @@ services: test: [ "CMD-SHELL", "nc -z localhost 8001 || exit 1" ] interval: 5s timeout: 2s - start_period: 3s - retries: 15 + start_period: 300s + retries: 2 daphne: image: intelowlproject/intelowl:${REACT_APP_INTELOWL_VERSION} @@ -52,7 +52,6 @@ services: uwsgi: condition: service_healthy - nginx: image: intelowlproject/intelowl_nginx:${REACT_APP_INTELOWL_VERSION} container_name: intelowl_nginx From 6c926b1fb88bb19f62c5724a52ecb150cb291774 Mon Sep 17 00:00:00 2001 From: Matteo Lodi <30625432+mlodic@users.noreply.github.com> Date: Fri, 31 May 2024 10:50:12 +0200 Subject: [PATCH 13/17] adjusting doc + https nginx file --- configuration/nginx/https.conf | 1 - docs/source/Advanced-Usage.md | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/configuration/nginx/https.conf b/configuration/nginx/https.conf index 8cc801b3da..5abfbad906 100644 --- a/configuration/nginx/https.conf +++ b/configuration/nginx/https.conf @@ -14,7 +14,6 @@ limit_req_zone $binary_remote_addr zone=adminlimit:10m rate=1r/s; server { listen 443 ssl; - ssl on; ssl_protocols TLSv1.2 TLSv1.3; ssl_certificate /usr/local/share/ca-certificates/intelowl.crt; ssl_certificate_key /etc/ssl/private/intelowl.key; diff --git a/docs/source/Advanced-Usage.md b/docs/source/Advanced-Usage.md index f0200bb9eb..310cbb1605 100644 --- a/docs/source/Advanced-Usage.md +++ b/docs/source/Advanced-Usage.md @@ -60,7 +60,7 @@ After a user registration has been made, an email is sent to the user to verify Once the user has verified their email, they would be manually vetted before being allowed to use the IntelOwl platform. The registration requests would be handled in the Django Admin page by admins. If you have IntelOwl deployed on an AWS instance with an IAM role you can use the [SES](/Advanced-Usage.md#ses) service. -To have the "Registration" page to work correctly, you must configure some variables before starting IntelOwl. See [Optional Environment Configuration](/Installation.md#other-optional-configuration-to-enable-specific-services-features) +To have the "Registration" page to work correctly, you must configure some variables before starting IntelOwl. See [Optional Environment Configuration](https://intelowl.readthedocs.io/en/latest/Installation.html#other-optional-configuration-to-enable-specific-services-features) In a development environment the emails that would be sent are written to the standard output. From 0bac4e6dcfd86c1227e55febd486f185f44bf3e7 Mon Sep 17 00:00:00 2001 From: Nilay Gupta <102874321+g4ze@users.noreply.github.com> Date: Fri, 31 May 2024 15:24:48 +0530 Subject: [PATCH 14/17] ailtyposquatting (#2341) * ailtyposquatting * restore a file that was deleted * fix * fix * changes * tests * no files * logs * files * variables * test * test * enum * tests * tests * dns_resolve * migration * a log :p --------- Co-authored-by: g4ze --- .../0093_analyzer_config_ailtyposquatting.py | 151 ++++++++++++++++++ .../observable_analyzers/ailtyposquatting.py | 67 ++++++++ docs/source/Usage.md | 1 + requirements/project-requirements.txt | 2 +- 4 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 api_app/analyzers_manager/migrations/0093_analyzer_config_ailtyposquatting.py create mode 100644 api_app/analyzers_manager/observable_analyzers/ailtyposquatting.py diff --git a/api_app/analyzers_manager/migrations/0093_analyzer_config_ailtyposquatting.py b/api_app/analyzers_manager/migrations/0093_analyzer_config_ailtyposquatting.py new file mode 100644 index 0000000000..fc7ad9dff1 --- /dev/null +++ b/api_app/analyzers_manager/migrations/0093_analyzer_config_ailtyposquatting.py @@ -0,0 +1,151 @@ +from django.db import migrations +from django.db.models.fields.related_descriptors import ( + ForwardManyToOneDescriptor, + ForwardOneToOneDescriptor, + ManyToManyDescriptor, +) + +plugin = { + "python_module": { + "health_check_schedule": None, + "update_schedule": None, + "module": "ailtyposquatting.AilTypoSquatting", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "AILTypoSquatting", + "description": "[AILTypoSquatting](https://github.com/typosquatter/ail-typo-squatting) is a Python library to generate list of potential typo squatting domains with domain name permutation engine to feed AIL and other systems.", + "disabled": False, + "soft_time_limit": 60, + "routing_key": "default", + "health_check_status": True, + "type": "observable", + "docker_based": False, + "maximum_tlp": "RED", + "observable_supported": ["domain"], + "supported_filetypes": [], + "run_hash": False, + "run_hash_type": "", + "not_supported_filetypes": [], + "model": "analyzers_manager.AnalyzerConfig", +} + +params = [ + { + "python_module": { + "module": "ailtyposquatting.AilTypoSquatting", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "dns_resolving", + "type": "bool", + "description": "dns_resolving for AilTypoSquatting; only works for TLP CLEAR", + "is_secret": False, + "required": False, + }, +] +values = [ + { + "parameter": { + "python_module": { + "module": "ailtyposquatting.AilTypoSquatting", + "base_path": "api_app.analyzers_manager.observable_analyzers", + }, + "name": "dns_resolving", + "type": "bool", + "description": "dns_resolving for AilTypoSquatting; only works for TLP CLEAR", + "is_secret": False, + "required": False, + }, + "analyzer_config": "AILTypoSquatting", + "connector_config": None, + "visualizer_config": None, + "ingestor_config": None, + "pivot_config": None, + "for_organization": False, + "value": False, + "updated_at": "2024-05-26T00:10:15.236358Z", + "owner": None, + }, +] + + +def _get_real_obj(Model, field, value): + def _get_obj(Model, other_model, value): + if isinstance(value, dict): + real_vals = {} + for key, real_val in value.items(): + real_vals[key] = _get_real_obj(other_model, key, real_val) + value = other_model.objects.get_or_create(**real_vals)[0] + # it is just the primary key serialized + else: + if isinstance(value, int): + if Model.__name__ == "PluginConfig": + value = other_model.objects.get(name=plugin["name"]) + else: + value = other_model.objects.get(pk=value) + else: + value = other_model.objects.get(name=value) + return value + + if ( + type(getattr(Model, field)) + in [ForwardManyToOneDescriptor, ForwardOneToOneDescriptor] + and value + ): + other_model = getattr(Model, field).get_queryset().model + value = _get_obj(Model, other_model, value) + elif type(getattr(Model, field)) in [ManyToManyDescriptor] and value: + other_model = getattr(Model, field).rel.model + value = [_get_obj(Model, other_model, val) for val in value] + return value + + +def _create_object(Model, data): + mtm, no_mtm = {}, {} + for field, value in data.items(): + value = _get_real_obj(Model, field, value) + if type(getattr(Model, field)) is ManyToManyDescriptor: + mtm[field] = value + else: + no_mtm[field] = value + try: + o = Model.objects.get(**no_mtm) + except Model.DoesNotExist: + o = Model(**no_mtm) + o.full_clean() + o.save() + for field, value in mtm.items(): + attribute = getattr(o, field) + if value is not None: + attribute.set(value) + return False + return True + + +def migrate(apps, schema_editor): + Parameter = apps.get_model("api_app", "Parameter") + PluginConfig = apps.get_model("api_app", "PluginConfig") + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + if not Model.objects.filter(name=plugin["name"]).exists(): + exists = _create_object(Model, plugin) + if not exists: + for param in params: + _create_object(Parameter, param) + for value in values: + _create_object(PluginConfig, value) + + +def reverse_migrate(apps, schema_editor): + python_path = plugin.pop("model") + Model = apps.get_model(*python_path.split(".")) + Model.objects.get(name=plugin["name"]).delete() + + +class Migration(migrations.Migration): + atomic = False + dependencies = [ + ("api_app", "0062_alter_parameter_python_module"), + ("analyzers_manager", "0092_alter_validin_desc"), + ] + + operations = [migrations.RunPython(migrate, reverse_migrate)] diff --git a/api_app/analyzers_manager/observable_analyzers/ailtyposquatting.py b/api_app/analyzers_manager/observable_analyzers/ailtyposquatting.py new file mode 100644 index 0000000000..0b79815813 --- /dev/null +++ b/api_app/analyzers_manager/observable_analyzers/ailtyposquatting.py @@ -0,0 +1,67 @@ +import logging +import math + +from ail_typo_squatting import typo +from ail_typo_squatting.dns_local import resolving + +from api_app.analyzers_manager import classes +from tests.mock_utils import if_mock_connections, patch + +logger = logging.getLogger(__name__) + + +class AilTypoSquatting(classes.ObservableAnalyzer): + """ + wrapper for https://github.com/typosquatter/ail-typo-squatting + """ + + dns_resolving: bool = False + + def update(self) -> bool: + pass + + def run(self): + response = {} + logger.info( + f"""running AilTypoSquatting on {self.observable_name} + with tlp {self._job.tlp} + and dns resolving {self.dns_resolving}""" + ) + + response["algorithms"] = typo.runAll( + domain=self.observable_name, + limit=math.inf, + formatoutput="text", + pathOutput=None, + ) + if self._job.tlp == self._job.TLP.CLEAR.value and self.dns_resolving: + # for "x.com", response["algorithms"][0]=".com" + # which is not valid for look up + if len(self.observable_name.split(".")[0]) == 1: + logger.info( + f"""running dns resolving on {self.observable_name} + excluding {response['algorithms'][0]}""" + ) + response["dnsResolving"] = resolving.dnsResolving( + resultList=response["algorithms"][1:], + domain=self.observable_name, + pathOutput=None, + ) + else: + response["dnsResolving"] = resolving.dnsResolving( + resultList=response["algorithms"], + domain=self.observable_name, + pathOutput=None, + ) + + return response + + @classmethod + def _monkeypatch(cls): + patches = [ + if_mock_connections( + patch.object(typo, "runAll", return_value=None), + patch.object(resolving, "dnsResolving", return_value=None), + ) + ] + return super()._monkeypatch(patches=patches) diff --git a/docs/source/Usage.md b/docs/source/Usage.md index a13076d12d..1508af89a0 100644 --- a/docs/source/Usage.md +++ b/docs/source/Usage.md @@ -256,6 +256,7 @@ The following is the list of the available analyzers you can run out-of-the-box. * `HudsonRock`: [Hudson Rock](https://cavalier.hudsonrock.com/docs) provides its clients the ability to query a database of over 27,541,128 computers which were compromised through global info-stealer campaigns performed by threat actors. * `CyCat`: [CyCat](https://cycat.org/) or the CYbersecurity Resource CATalogue aims at mapping and documenting, in a single formalism and catalogue available cybersecurity tools, rules, playbooks, processes and controls. * `Vulners`: [Vulners](vulners.com) is the most complete and the only fully correlated security intelligence database, which goes through constant updates and links 200+ data sources in a unified machine-readable format. It contains 8 mln+ entries, including CVEs, advisories, exploits, and IoCs — everything you need to stay abreast on the latest security threats. +* `AILTypoSquatting`:[AILTypoSquatting](https://github.com/typosquatter/ail-typo-squatting) is a Python library to generate list of potential typo squatting domains with domain name permutation engine to feed AIL and other systems. ##### Generic analyzers (email, phone number, etc.; anything really) diff --git a/requirements/project-requirements.txt b/requirements/project-requirements.txt index 398ccfb1e3..e2c63b402b 100644 --- a/requirements/project-requirements.txt +++ b/requirements/project-requirements.txt @@ -78,7 +78,7 @@ querycontacts==2.0.0 blint==2.1.5 hfinger==0.2.2 permhash==0.1.4 - +ail_typo_squatting==2.7.4 # this is required because XLMMacroDeobfuscator does not pin the following packages pyxlsb2==0.0.8 xlrd2==1.3.4 From 5023a77a06c0e1713edb5163f2f9d68e91cb87bb Mon Sep 17 00:00:00 2001 From: Daniele Rosetti Date: Mon, 3 Jun 2024 11:54:40 +0200 Subject: [PATCH 15/17] supported sh tld --- frontend/src/constants/jobConst.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/constants/jobConst.js b/frontend/src/constants/jobConst.js index 6c0e530201..ddd9865854 100644 --- a/frontend/src/constants/jobConst.js +++ b/frontend/src/constants/jobConst.js @@ -180,9 +180,12 @@ export const FileExtensions = Object.freeze({ OK: "ok", PUBLICVM: "publicvm", ISO: "iso", - SH: "sh", CRX: "crx", CONFIG: "config", + /* This is a list of valid tld that are also file extnesions. + This could generate some false positives in the auto-extraction, if they are too much filter them. + sh + */ }); export const InvalidTLD = Object.freeze({ From c116e2816b0ece04c0c6abcea8c696e0c5ce77ff Mon Sep 17 00:00:00 2001 From: Matteo Lodi <30625432+mlodic@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:28:16 +0200 Subject: [PATCH 16/17] bump --- .github/CHANGELOG.md | 5 ++++- docker/.env | 2 +- docs/source/schema.yml | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 64a0697ece..344ad10e0f 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -2,7 +2,10 @@ [**Upgrade Guide**](https://intelowl.readthedocs.io/en/latest/Installation.md#update-to-the-most-recent-version) -## [v6.0.2](https://github.com/intelowlproject/IntelOwl/releases/tag/v6.0.1) +## [v6.0.4](https://github.com/intelowlproject/IntelOwl/releases/tag/v6.0.4) +Mostly adjusts and fixes with few new analyzers: Vulners and AILTypoSquatting Library. + +## [v6.0.2](https://github.com/intelowlproject/IntelOwl/releases/tag/v6.0.2) Major fixes and adjustments. We improved the documentation to help the transition to the new major version. We added **Pivot** buttons to enable manual Pivoting from an Observable/File analysis to another. See [Doc](https://intelowl.readthedocs.io/en/latest/Usage.html#pivots) for more info diff --git a/docker/.env b/docker/.env index 2306fca63a..b9f079e8eb 100644 --- a/docker/.env +++ b/docker/.env @@ -1,6 +1,6 @@ ### DO NOT CHANGE THIS VALUE !! ### It should be updated only when you pull latest changes off from the 'master' branch of IntelOwl. # this variable must start with "REACT_APP_" to be used in the frontend too -REACT_APP_INTELOWL_VERSION="v6.0.3" +REACT_APP_INTELOWL_VERSION="v6.0.4" # if you want to use a nfs volume for shared files # NFS_ADDRESS= diff --git a/docs/source/schema.yml b/docs/source/schema.yml index 2e361f4fe2..e21cbf65a2 100644 --- a/docs/source/schema.yml +++ b/docs/source/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: IntelOwl API specification - version: 6.0.3 + version: 6.0.4 paths: /api/analyze_file: post: From 46e2d0615910b1ad8cdda086ea420bb34ceab9a2 Mon Sep 17 00:00:00 2001 From: Matteo Lodi <30625432+mlodic@users.noreply.github.com> Date: Wed, 5 Jun 2024 10:07:48 +0200 Subject: [PATCH 17/17] removed initialize.sh from start script --- docs/source/Installation.md | 6 +++++- start | 6 +----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/source/Installation.md b/docs/source/Installation.md index cad75d7f49..9b2809fe0d 100644 --- a/docs/source/Installation.md +++ b/docs/source/Installation.md @@ -13,6 +13,7 @@ In some systems you could find pre-installed older versions. Please check this a
  • The project uses public docker images that are available on Docker Hub
  • IntelOwl is tested and supported to work in a Debian distro. More precisely we suggest using Ubuntu. Other Linux-based OS should work but that has not been tested much. It may also run on Windows, but that is not officially supported.
  • +
  • IntelOwl does not support ARM at the moment. We'll fix this with the next v6.0.5 release
  • Before installing remember that you must comply with the LICENSE and the Legal Terms
@@ -35,7 +36,10 @@ However, if you feel lazy, you could just install and test IntelOwl with the fol git clone https://github.com/intelowlproject/IntelOwl cd IntelOwl/ -# verify installed dependencies and start the app +# run helper script to verify installed dependencies and configure basic stuff +./initialize.sh + +# start the app ./start prod up # now the application is running on http://localhost:80 diff --git a/start b/start index e7dd338601..646cfc57b2 100755 --- a/start +++ b/start @@ -91,11 +91,7 @@ load_env () { } if ! docker compose version > /dev/null 2>&1; then - ./initialize.sh - if ! [ $? ]; then - echo "Failed to install dependencies." >&2 - exit 127 - fi + echo "Run ./initialize.sh to install Docker Compose 2" fi check_parameters "$@" && shift 2