From 555c261a8266831b63400b51d16eeaee2bcb5f33 Mon Sep 17 00:00:00 2001 From: ammar92 Date: Mon, 8 Apr 2024 12:47:22 +0200 Subject: [PATCH] Replace Wappalyzer (#2727) Co-authored-by: Jan Klopper Co-authored-by: Jeroen Dekkers --- .github/workflows/boefjes_tests.yml | 2 +- .github/workflows/build-rdo-package.yml | 2 +- boefjes/Dockerfile | 10 ++- .../__init__.py | 0 .../plugins/kat_wappalyzer/normalize.py | 33 +++++++++ .../normalizer.json | 4 +- .../plugins/kat_webpage_analysis/boefje.json | 3 +- .../plugins/kat_webpage_analysis/main.py | 24 ++++++- .../plugins/kat_website_software/boefje.json | 9 --- .../plugins/kat_website_software/cover.jpg | Bin 45081 -> 0 bytes .../kat_website_software/description.md | 3 - .../plugins/kat_website_software/main.py | 31 -------- .../plugins/kat_website_software/normalize.py | 45 ------------ boefjes/debian/rules | 5 +- boefjes/poetry.lock | 20 ++++-- boefjes/pyproject.toml | 3 +- boefjes/requirements-dev.txt | 4 +- boefjes/requirements.txt | 4 +- .../body-page-analysis-normalize.json | 65 +++++++++++++++++ .../tests/examples/download_page_analysis.raw | 68 ++++++++++++++++++ boefjes/tests/loading.py | 2 +- boefjes/tests/test_bodyimage.py | 12 +++- boefjes/tests/test_wappalizer.py | 52 -------------- boefjes/tests/test_wappalyzer_normalizer.py | 23 ++++++ 24 files changed, 257 insertions(+), 167 deletions(-) rename boefjes/boefjes/plugins/{kat_website_software => kat_wappalyzer}/__init__.py (100%) create mode 100644 boefjes/boefjes/plugins/kat_wappalyzer/normalize.py rename boefjes/boefjes/plugins/{kat_website_software => kat_wappalyzer}/normalizer.json (53%) delete mode 100644 boefjes/boefjes/plugins/kat_website_software/boefje.json delete mode 100644 boefjes/boefjes/plugins/kat_website_software/cover.jpg delete mode 100644 boefjes/boefjes/plugins/kat_website_software/description.md delete mode 100644 boefjes/boefjes/plugins/kat_website_software/main.py delete mode 100644 boefjes/boefjes/plugins/kat_website_software/normalize.py create mode 100644 boefjes/tests/examples/body-page-analysis-normalize.json create mode 100644 boefjes/tests/examples/download_page_analysis.raw delete mode 100644 boefjes/tests/test_wappalizer.py create mode 100644 boefjes/tests/test_wappalyzer_normalizer.py diff --git a/.github/workflows/boefjes_tests.yml b/.github/workflows/boefjes_tests.yml index cefd0afb47a..377142a6479 100644 --- a/.github/workflows/boefjes_tests.yml +++ b/.github/workflows/boefjes_tests.yml @@ -39,7 +39,7 @@ jobs: run: python3 -m pip install --upgrade pip - name: Install dev requirements - run: pip install -r requirements-dev.txt + run: grep -v git+https:// requirements-dev.txt | pip install -r /dev/stdin && grep git+https:// requirements-dev.txt | pip install -r /dev/stdin working-directory: ./boefjes - name: Install requirements diff --git a/.github/workflows/build-rdo-package.yml b/.github/workflows/build-rdo-package.yml index 949df39399e..9a807192751 100644 --- a/.github/workflows/build-rdo-package.yml +++ b/.github/workflows/build-rdo-package.yml @@ -187,7 +187,7 @@ jobs: run: python3.10 -m venv /var/www/html/.venv - name: Install requirements - run: source .venv/bin/activate; pip install --upgrade pip; find . -name requirements.txt | xargs -L 1 pip install -r; pip install ${{ github.workspace }}/octopoes/dist/octopoes*.whl + run: source .venv/bin/activate; pip install --upgrade pip; grep -v git+https:// requirements.txt | pip install -r /dev/stdin ; grep git+https:// requirements.txt | pip install -r /dev/stdin; pip install ${{ github.workspace }}/octopoes/dist/octopoes*.whl working-directory: /var/www/html - name: Create archive diff --git a/boefjes/Dockerfile b/boefjes/Dockerfile index acc5ef3211e..c0a75319776 100644 --- a/boefjes/Dockerfile +++ b/boefjes/Dockerfile @@ -18,8 +18,14 @@ COPY boefjes/requirements-dev.txt boefjes/requirements.txt . RUN --mount=type=cache,target=/root/.cache \ pip install --upgrade pip \ - && pip install -r requirements.txt \ - && if [ "$ENVIRONMENT" = "dev" ]; then pip install -r requirements-dev.txt; fi + && if [ "$ENVIRONMENT" = "dev" ]; \ + then \ + grep -v git+https:// requirements-dev.txt | pip install -r /dev/stdin ; \ + grep git+https:// requirements-dev.txt | pip install -r /dev/stdin ; \ + else \ + grep -v git+https:// requirements.txt | pip install -r /dev/stdin ;\ + grep git+https:// requirements.txt | pip install -r /dev/stdin ; \ + fi FROM dev diff --git a/boefjes/boefjes/plugins/kat_website_software/__init__.py b/boefjes/boefjes/plugins/kat_wappalyzer/__init__.py similarity index 100% rename from boefjes/boefjes/plugins/kat_website_software/__init__.py rename to boefjes/boefjes/plugins/kat_wappalyzer/__init__.py diff --git a/boefjes/boefjes/plugins/kat_wappalyzer/normalize.py b/boefjes/boefjes/plugins/kat_wappalyzer/normalize.py new file mode 100644 index 00000000000..532160a84c7 --- /dev/null +++ b/boefjes/boefjes/plugins/kat_wappalyzer/normalize.py @@ -0,0 +1,33 @@ +import json +from collections.abc import Iterable + +from Wappalyzer import Wappalyzer, WebPage + +from boefjes.job_models import NormalizerMeta +from octopoes.models import OOI, Reference +from octopoes.models.ooi.dns.zone import Hostname +from octopoes.models.ooi.network import Network +from octopoes.models.ooi.software import Software, SoftwareInstance + + +def run(normalizer_meta: NormalizerMeta, raw: bytes | str) -> Iterable[OOI]: + pk = normalizer_meta.raw_data.boefje_meta.input_ooi + tokenized_hostname = Reference.from_str(pk).tokenized["website"]["hostname"] + hostname = Hostname( + network=Network(name=tokenized_hostname["network"]["name"]).reference, name=tokenized_hostname["name"] + ) + raw_respsone, body = raw.split(b"\n\n", 1) + response_object = json.loads(raw_respsone) + url = response_object["response"]["url"] + + headers = response_object["response"]["headers"] + body = body.decode(response_object.get("encoding") or "utf-8", "replace") + + wappalyzer = Wappalyzer.latest() + web_page = WebPage(url, body, headers) + results = wappalyzer.analyze_with_versions_and_categories(web_page) + + for name, data in results.items(): + software = Software(name=name, version=data["versions"].pop(0)) + software_instance = SoftwareInstance(ooi=hostname.reference, software=software.reference) + yield from [software, software_instance] diff --git a/boefjes/boefjes/plugins/kat_website_software/normalizer.json b/boefjes/boefjes/plugins/kat_wappalyzer/normalizer.json similarity index 53% rename from boefjes/boefjes/plugins/kat_website_software/normalizer.json rename to boefjes/boefjes/plugins/kat_wappalyzer/normalizer.json index 2934d591920..d53581bf6cb 100644 --- a/boefjes/boefjes/plugins/kat_website_software/normalizer.json +++ b/boefjes/boefjes/plugins/kat_wappalyzer/normalizer.json @@ -1,7 +1,7 @@ { - "id": "kat_website_software_normalize", + "id": "kat_wappalyzer_normalize", "consumes": [ - "boefje/website-software" + "openkat-http/response" ], "produces": [ "Software", diff --git a/boefjes/boefjes/plugins/kat_webpage_analysis/boefje.json b/boefjes/boefjes/plugins/kat_webpage_analysis/boefje.json index 46e8d33476f..c6c12b6e73c 100644 --- a/boefjes/boefjes/plugins/kat_webpage_analysis/boefje.json +++ b/boefjes/boefjes/plugins/kat_webpage_analysis/boefje.json @@ -6,8 +6,9 @@ "HTTPResource" ], "produces": [ - "openkat-http/full", + "openkat-http/response", "openkat-http/headers", + "openkat-http/body", "application/javascript", "application/javascript", "application/json", diff --git a/boefjes/boefjes/plugins/kat_webpage_analysis/main.py b/boefjes/boefjes/plugins/kat_webpage_analysis/main.py index a422a25d3ed..e8accd74d7b 100644 --- a/boefjes/boefjes/plugins/kat_webpage_analysis/main.py +++ b/boefjes/boefjes/plugins/kat_webpage_analysis/main.py @@ -66,13 +66,35 @@ def run(boefje_meta: BoefjeMeta) -> list[tuple[set, bytes | str]]: if content_type[0] in ALLOWED_CONTENT_TYPES: body_mimetypes.add(content_type[0]) + # in case of a full response object, we hexdump to avoid issues with binary data or different encoding + response_dump = json.dumps(create_response_object(response)) + return [ - ({"openkat-http/full"}, f"{response.headers}\n\n{response.content}"), + ({"openkat-http/response"}, response_dump.encode() + b"\n\n" + response.content), ({"openkat-http/headers"}, json.dumps(dict(response.headers))), (body_mimetypes, response.content), ] +# todo: perhaps also implement response.history? +def create_response_object(response: requests.Response) -> dict: + return { + "response": { + "url": response.url, + "status_code": response.status_code, + "headers": dict(response.headers), + "cookies": dict(response.cookies), + "is_redirect": response.is_redirect, + "encoding": response.encoding, + }, + "request": { + "url": response.request.url, + "method": response.request.method, + "headers": dict(response.request.headers), + }, + } + + def do_request(hostname: str, session: Session, uri: str, useragent: str): response = session.get( uri, diff --git a/boefjes/boefjes/plugins/kat_website_software/boefje.json b/boefjes/boefjes/plugins/kat_website_software/boefje.json deleted file mode 100644 index 3623e91edb3..00000000000 --- a/boefjes/boefjes/plugins/kat_website_software/boefje.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "id": "website-software", - "name": "Wappalyzer - Software Scan", - "description": "Scan for software on websites using Wappalyzer", - "consumes": [ - "HostnameHTTPURL" - ], - "scan_level": 2 -} diff --git a/boefjes/boefjes/plugins/kat_website_software/cover.jpg b/boefjes/boefjes/plugins/kat_website_software/cover.jpg deleted file mode 100644 index 66c29af187c72ac8d631ef1ccefdc0a0ce4dd79f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45081 zcmb4p^;4Wpu=V0@!QCaeI}3#1?yzWpKyV1b9Rk7K3GS}T;u73_Np^7p$-=uK1ozAR zs_q}~_48v-ou_MRrl!y7^ZfmJ7eK6`qOJl!K>+|zUOvDx9H0ci!NkJG!o=OUq@jMJ5!A7}2xj;i90icqgppl?Fj{)ca08})TmjL`< zprQd#&@o;re#yQi2B4y#p`oIqqhp|=W1yp8prDcf(3sI*31KLbvgp6V6!wlJOD}7~ zVtr#UB4S6*7PU+vs`T-%VaDE>54)Is`N|6l_5b3(#Q*)_f42q|4ISep#YOV}5~!#z zMF3vVUzVVdprSFq5<-8$7uNU2AY&DI7x@eW;J++DB|#$r$OFDZ)p$J=^yKG#g7MZ| zn)qfvnDtPiME_PrC{)1)+>#}=qJ6X6@1fwV4zL>t=nfU*kcV@`dcwu4hbh1_TG_oc zD3{g3@W0tr5Pus^0(afoh9=j#^X>3(B?h&=KX*E_1Krx|pd8m2CKR@~;XPd$X<(cQc&u#`bl_a^YYv`NCZ3({bY+9Mo zmkDc07Zy>_6-Bg9KJ%{BJ8sDFZz{WtqzRF33dWH@?~##8eF}Z&C=U~OI;rk-9wu{y z2P|V91GFpHT{h65}D}CSKR;MC4o146{hE z&g%-y4ebe(kTFZ=Voa{j;4YJqKxc|GMvytjLHlO(!e60D^0yXJ6rZtWbUlNXw8yqK z(ZcIXA=O+lsO^6u6u4A@mqshiPp|nYOm<009>ua=s-AP?%RnL75v_KO>DQV%*QMLf zHw0GiQN*E?mi`)aSwl6d6%y#F4rQrib1-PYipdniw}-Y{>VKef+VVZKls9m>hTme9Y^taU0|U`I8ohZE_GGY*-k-$rLysEsQ%{lUvtJ%I}YeTsFE zhSm95fy4wAlzNfv$B8B^eMplo;WRgjzjD%ajbJ}p}_!fOzP zR_y5dKksDQCD7b-2fUl`!#bz%5eo;Pv2Zuj@QG)DTVPm`xc?mKc&P5`!f@Avwn#&Y zqib??X!^%zz@BtKJHJv2N(wN0aqypvg1G{q@5hP=tJQ&>EeP2a?fW{5t!;JF6Bvm9kt{r&*Ehl(5wB zm+Gp{JUxuD=$0;SPA>zjJgQ=jAn*Nw!Cu&P^QoF;R_9(Gwu-``-BpE(9@8D3T60T= zMvkn25@@W_zE~km4->{9cJW$$Si>k^JxuUwlv+rvcJaTRm_M&W6#)RZx{-#KsMapsUb#;mF>-k@}5a#=g^3KgroqmX$;MR)$#~Ls6 zIlg?1#pDmg+rAObBx5GFjsTY>svFI(PNI1l4N=qyCBJaSlr>R;b=<3KnchjDk*5SU zNi}8q)g^rkqhr)ryL(p*`wXtaP@s*w9r$fTXZFczylE=}C7--Htd`&E-N$`EpVB)8 zIYh#WKq)R&qoCVl&?*hileygz3m}PZ$$|!67$FQWt0g(rd%YIM^v=Th-63%8tEz2? zJTXCo2MSu!5-|vOgF&*pO-hLTZJ)=eYYKcg`efckpo%u>USlj#1DhPhLidPOH><6r z(TK}q8%VK+bd9`hAY=ZX`va_UKb$GlN2g~V$z7_LTtjlI)j*SVsF`m=Y#Yk8rW^k= z&;-DvNZ+CMC`QFK)>+pJ;F%=jn(yW7(sADh_8Z=5Iy)w1G=CgSS4nLb`0T3Xh&dij zD)snUa_+lZ8>$6Zjomg?;QjRJWt~gLJba2j;GMAp8eIX;0t#;#h2+Q+Z ztIbS9-erQ=YSU#)%&8F+VXNy^ZeT;Mgrg+Usb;z!tst8b#+4zIY1wHA$oXz-v z0kp)>-R;At*#jHq8ZU++uv7ENQ0i!|YwAYOmk0X;ZTb;*ipNfces0WR^2NHZpi}AX zc6nucVkh#wNfWD2NVDcJ_22(yVLgvs0vc6D3k3=r)K9RBFoAoYe~;1|^a_%B)GgKL zZ7z*GppN8`A&tXLVaY9*1Gv4jA6HUQx$btqwNA(CaTueB*dCB?sIIl(J&W-vQL1n?CEhGGxnaYjW|rBMr{xsQmJbakcL^N#t^IgW@j0c zs6$#mN>G*+p~@;2_G2(6eALRKX!mqAnpiP!KL-RP4_M#sl>8gAx+vB>n47ZIe7IG< zJ0ij~a@DwDU_4`KM$OxOS$iM{^-9o46W1ZSuN6G9gP^A|&!vL#s1si``t*H}#W_lk z?g)e-!Fqj(tp~8YY%e6Fdoo*<(MAKTAdBB)Pr2a?$iG`IhZ?f%=o4DtGu=BD*)2K> zb;AZ^Gp0l*v=sL9;#Og=J+(;Gc)T)8!kq8zQs3c@L~xGD_!X=rL4ym)3;hn+JH$#& z&bpbXSrnz}(j_5#ZPb}hzT$wlX{g@DikhT3a4v4*BDz-l}v zG@v%weH^M!&~@34eUZYsQfc*NbM#}3)dnjS@afVP$I+NQ5LSuGA);YD!A3mk?Z+vJ z%1uFrSLmP?`2kzxM5i)a<&Df%nq9D7{=(?{{JH24yGe8Faom2}1QBRB7kLdN33E_0 zR^7Cpq0VIff83cYs3=i5VcZxEEm7MB;xIOex~=bPI{*42(32;Knt3Gi>GO6!)^b9E zv>c^;dO?~_DeH>18_u7$CUd;ik|@OfR38=PeG`Jyk%$%i3zRanl<@c^9ISW90NE=- zS+nrtoHJ2iWmtnu%2+Wb_8N4>j^AtGaT7fKf_~BEloC5n_NZ9+ltUSMFc*(^YCQP$ z<{)&V@u>I{`3&Iy{`E@qz;Mfj;z4uU*01Ng&$w@i zmO1h~tYfLsm%pXKqq3PCJ`AokiD;H{{bG7g#AxL$adEo?LV$|Xn?w2QKGxt%zWbiQ z3P1Q0C^rzD7)}_1SEDIG`NXA!qG;lsv zENdA9i)6!3;Mr^AqqQ~!rRb>g(xc1cQ)i%}`C1>j1*}GsuLf}C!pLvYZR(r@S{*LL z0}Us=L~*pA-iPkzCoNg6ep$c7=w`S6&M8?iK-6n|)ur>bYag`8dR1%q^hzcuW-aN) zQCAKzq&j(ZAu+KhOZ{+W?jbBFGYu%}!$y*e8~-vW(?U{A-Or<;`_WF>(@s6UzR0J} z>nNY8Ri3+FD-iuTFzi4_YPU;CDo&a1{6&Pf5=!w&%npL~9_wwYi-o<5} zfG}?G`W{==INPqOvfDk{0wVzdw#Z@^HPZJ>oL{W!iNs!5!%snTep>@Or7)`K?I|`d zoe!W_UnbZ~Q>leS;`TqI9`eBfQAF`*Fz1Lam3Ls&2oiv&hC#E41y|#bvph5ix-zIj zx=kF0|3*Osa1;e$Yt{ZdK0@q3XA&8#`-Lv~i+m7?=?^?VQ$nDdUmc$AQ(=VSlcC?Q zd95;mR}eGgEB?ki?*i_~ZJH(YVVGAKSUDPw;YXJXeC%6Ei0bG?hZ`{6FI505X1#|0 zEOC#~3gEX$W>X`L%qG{2$^Vmn7iYVCB5Eoi0F16I%=KHOUkqZMcLc`&olOSDG&9WD z-Wwne+7&}Apq()O4BD{sy+35Xsy2LvP8ekN_;YWXSbh24WNd5|9`j^3{X+aFD!u|E z@A9ipTJT0{C5GCos>9}ke(D@B%^UtVSG4+K8*U~~_pJfTUn zCFmVEVO{AD(|iU@T?Pac1)Lp9JFh&vb5gH@mfRL<)i+7;Y??q;Q|WvcGg&Feh+J*X z46E9t-I*v}VKRGdMeRrd###-;TBqbyKGwz=Yjx^s(6Yk z;$ShExzdbQ=j|hpRzGCipJK;w81n!{IH0BE*U*nEdED@`eoj;Hn)ImHJTMA5E3H{f zYX!*{k@c}2`gY_a_VeLS(+Sq!Z_SYXl*r083)`*-C+>el5Ln-BXn%+&aK~c39Au6^j0o}f>6lu;%cr1JWpq*WlPopjL; z;5)Y@e^StpW-6rW-Qm=H2cQdn zojsIQ8PN*e&4kM?B`;f`B$5&wyh0DwOX@+L!T`NdTpZB6Sje4WAI9`=849wk28L60 zupv36=%9IJ?khvpe4Q_Myw(p8G zzFF5LAQDQmCU~KHI^~kXg56gmZuYl5o3cus;oDznKJuWWr5WSQ;}jz{cL9Ad6Y@3W zc1}F)%X%VvXHrsiF8LuRNrk1P1U za-2yGIQBQKqVVafx%Eq|jgd_6r%sLSrv|^Gr!AKl7R*ksZX%{3OGO*Aanwg(!e0W# zzDTR5n);3kKX&gSxfuGLRcpeqli#7zODmtOfJKHy~WO0w4eIeVw-dD5Y7Np7WCSdzJb@QP5om2iX^s&nq#rZi-~d#hwOg$R=rR^dR85yB3Mi&Qun9L@2dbe@O3mb2hZU~!_|1>6!k+xY8d>m zcp0NTTzXP+T^0rF!Bp35BuLr2iMJ9^Zu{32vcHH_%|!HkV_*FQ(8G{}Ys<$fyP(DQ5c zr9^AU&c2wYGi^C*Iw{c=cq*Hm?7otgSaKBug&OCs+IYoe?33G}T>RkyKLajzcXXOd z+oB<#Oh9LwP6TL?mRma_J-ZF3X8^VsK(RxRN$EZFX1WX}6mm}Z3~f5R00AwVtWeW0hFgj+il|Ze|)d;jA);PerTdPX4F#z zV6Jf-VSP2J5RbI1ujK*YKQoQl}>a)#-9^CW?f{2)9eRb2slIgo&j_E(HvM{<)o6M4C65U{OdBhN=WXkOo76k zLHPsIG}Dn0iHKT?sXn}iT$pXw#5MPml`Lh?#h5;W&i3GW_U~YS#bTL)HPeTFk5nCu zdsx#OMczSbxiKjFMnj6vm!(gp;bd;+dgfm}*oZVHs6*)+OhP{@*z)D2YJ{c^Ir(zB zLv;nQ0as0Gi>9lNIWGdlMV|v6EOW2yK<*j9Y?|A+*TN_R6%BE6bAF=oV&SW$ZV^cF z)IS~Uxwy3%C%UnT`VZmmCLz`vHR<@?r1$Y^k)iwQFa)JRWxz+V9;iyI1}v^XfHqsO ze)<$2z(1u#bUnRB2(CUf`8eEHo3RC#o1f;9Q^`r4wAvG6eYP#>E=Ce zrG~n~Q~D_<k_8c-EKb-`$r*@_^RULA}qFv{xbN@Z=b$zt6X=w-WnyUeC>BNO8&Oe zirhag=B22>i|;=m90c8~vR(}yoKLnNXprbJ*LR65zaSk#q#gvRwUefE1k!m7{6_bjynOZ==J&?z=a zTE(xyCKY5P5=hbPMB=x0b`9o_WHHw;oP0YGEyqmv<_22v?2iy2|;=vfOgAfGqMv%_Wx7Z*xQ<%tn!nYOc8=X0ygis!2uXZxm|XTNEpH9rGnH~#ruIHd4}_!nm2PaAfMdThWWvuvUn<%F^|eJ`a6pRc5-tYY z>%S8du!)&AW7)v&CV}OIs;@tz4^#s8OP%&SZsY}K-BLn-f19th7<}#eS>S7+`c*Yw zjL3gocsP7+*Xbf^{Fl>qMp2q^j4o+>9T@Jd^10~u+PIyXmcAzMXY<$&63CpG)OP>q zF$iyk4^E|RV&!=-dFr*0IYw|qs#us&89if_`jjJ8Tr4}t&IOMJf36?(FM7EzttT_ zVXXfxij6}fM-%%D*!=OL_;zgwh}}#x7#gLn1c8&S%D@shDOgROwTFR4|L$`~ZCy%K z>s3q*D*U!Nq*~`| zF&J{Hml0YBuJZJ80*w|NrsQ(D*E!?0?z|5b{~CTR-5SB;NXcG!o4WXd^OJSXpldxk zZH$scUM7P4qS0O(8a-s;z$`zd6uB7G-abUkslTdKLaL6&@GWot-nIjbM3`+c;r&#n z`UU1)5pz?X?HUzAJ)Q9(U(J=dItdYPge5K*<*OCQc#enmQKB%=;;@@hmAL#oy_)U2 z{V;m3_f|D1QH|5*mDCyLkE1M3y`xQ1=B$#L^b2h`5n(KK2-{>BGrJ>_KU~`L^%9fRFX~L75(hQ{4eQ9AU*^Oxmjvu&`j}gU8UzyvX0uA{{uAG=T8L0oR@{r_8@7JB=%_Ayr^o~mnDo|n>P743 z_?aqzmTp9KU}?5yNgZ(p>@+PL@?BbPHFmFz7jsS3x*O0u>5Ul>-rTBON&8c}yP0~X z3SZwXp>DpO=z<$pb<`O@11vN@v~*&NEvA}G*0h9eD^>a~j)IJwSdNQggA=UfB~t!O zR)E)Anud-xVa3&8V6jCIgf;n$a)Q?lm|DHb)|!~x{ZI=Dl`Cc>cpOmu+ZJ7;=*O*L zGvvuBs|)Kd*1fosI$_kifoSGsr&MEI&^znYmHXf{u67@hIlYriN^d8>3 zkBWMBEIC36>z!7_Dhe!Dhr;UlFWmxCYf${}W@09=y7!z448I8)R`R)@n##u#D~A7l zgxF9)@PY5-`b@e0OgmS-`AkekA`fcN_V~P&#vB=NJ+7eWLFFbWBY9_#rB@YUsb&-? z92#t*p3}^;>H0NXhzVs|sw$&p(~+>{huNQ$4|;{S23qfp_G>eCod>DZaG3m$a?H1X zxWZhcy;zp1fNLFh+$*&Zx7Xh`rC47YItiqyI(n_h!vbrutCj&@7Zi6U%x~@-X@nd6 zx;2}4T!QFBldw{9*)0~G(Q;Q|2)5naL&>q|2T=l(tC7p;39eJ3tH$!H;V6{|g+J-H!$DqWOqjdWnyw&vdkYx-rCK{k1%j#7^xR}7AF zyVDHvJqnW+&Lel3$@H1N(=)(0uB1vZXSVCx?OapIxh9VUQJhk&=Mj-0-Tr4QxMy3+ zih>zq5sQ%Jn}x=G({MC|xvQr8Hf4b=+vIi@Q==KCJ_EBb5h^z;wy~K&?o6B9xgm3p zcN{T!`|4>zl5ycj-3TaI{nX)bzKwrREYZ$e($94BtwH z64t_K!_oKafKlOz;=8-tJxGnj-ps-uJ)>>h`&NUlnc_hu-E;JnMv2zFX~}eWbDz)Tx4TfO7tK;^J=)A}@2EQv z2tQ2oL0T4APZO(Reg(vFuC9WeXka=ON>yv52hMJrB}U{ePQNS%R7dGqZqGk_Bb}IF ziFY-eHWOpSK(G~>Hd|1Q09J#Z0cUkTp8<^$Kw_vxDw;tOC6hX4vPU56>)%B#6XXJ+ z27Awd0fq{nL1)KP(5mkFW!J$v{9*O?VzQ`jN;6<Nc&`#3ghu!=#YV1$?JIO@MYl>ZJg?exU9PT=uJk?ArZFg^`Wbih$+P8 z-(uDaoRvUaH%-t2X`oM>2ETJa7^Q~`5~#t1t_-R+Sn!HEYW%S@jj?-e@4Fyo#rKN5 zMr7&njAAy9Vq5v>yp8?&SC6liEBf&jXo00hu`ye3{NP@1{R%SpzOkb)CB_L|l(DaE zq6b2FqI82Y#m?Or(aKi1~@F`(-{{-JKb|ZN;g+ zd@@vR))n35IQnZ@BE+93mlUKo^q&8a*-q#F>(y0384eqWxZ+Sj)x)_|@Z?hlp{HP~ z8kvjudJA{mzf1?MzdoR)?U}eEvX2;=RF}LM5IENCMeU>{rYSEe?E>z-UaJJ(AC-SG zPoSTSL3%5zXiRaux(I!)xYT1>)96zb)Ll*Y6btk<8i|OhQUHn#*l034_=*1Hr0!p$ zo7P<_=+N4e6Zk0NymdF&WNbV{;5Kb1mEZ)ip-c#P+-Q2)15Z@*&)z9KH9}3$HK4@X z&cxUin+zG`yJtYqVF|;Qv(e=4<+4;Uydi2o}|$qn68p+!;s9*};0ob6Oh1(8 zPs8YIfPvCR|7JF~!gl#Q;R_=rCeVkwSDo|GH^f_wuJK4WL zy7JLOCu~U~WVC&PSee>2^3(BBpN`8u_`)&bF+2nxxsBR|Xj2OvoF0ryck2wlA;VS+ zO4q<19z6incn=Y*ga(_6sK6+y;lOD5Cj1T?1C!r#d{qaTYEHdW*8LiPvPfjlP3lpO zteBl*0cmn?X~Nw+t2K2d;BR6X*t|33Z?|(if1DobdC7h!ubsv|_G{bC`D)iWOz_PR zV7GaGs>f-jmHi>J3s8%wZ#ww z%zxpYDZ_WW#w6@klc+|}RbO9%(w>D8~(I&_TH zqsiOAO1j=Ca%fDSnf)noX+^w`>+C~MB~X!}uAz<^^t>s|LYmkU2P@O8(sabg{O!YH z1U2dV3?g7^vVzrf@b@^^m6AP*I)cALpVh4L$#!*2(y#)z-0tKd26DpvfoI~5rFJ` z>RQgE%hzrMM)WaYzc-YZKDWCPy$A18qH-W9RkO9O^;SsOqa4xbGk_4B4X)2|G~p$q zB0W8^6abz4omxC|>~}sKBBYDN@|xUkyR_nz-_XAC7G3DN4AKQem4UXpusqlNnyq|Y zhp1DkQ>%XbehfKo692hIE~7B)zBMG>`_CeyL-H#!s3e&v@$XZek@&&;pWJi%kRcuU z%iDXcegBGy#LRv&pq~=+9E6RvcW`-X!wq#s`t7n z-!>K=%F(gzyC3X-i5nKamaLfm+z_CWUT~+;qI3~aJ_AeLBeAu#daG?$Xcng)IQR^;BbzfX! z+H*z@e>tv57%#qx*Nj}N?sDFFuDwrz7iIHtXx;^vHfMYthLd)4Rh45AKI@g4JU^A+ zq~Zd1#U@6xV6nB!QS=3Xy~l3FRY~Xcyo1+{QM4F|7Y}tx;qJAKb?l9zYG{KWt^^P5 zARZJ7tl!Y-&Q0EpVI-kCJ-s_GwyPtLiPBE1CAy=Q52{bQPhtek9kzKhg|Di=@7Gxu z#^k4x`;GqPjEqO2C~`=C1&q=tpg(@$p|KlJtIlZqe!{^%>+XF!RD zke~}eYm8A#5mw%06Hd-%vJzmpev^rC)8K8Lze9#sy^4&sdX>VCfJ}UVyvMp?B`VV^ z^@u2KQ9v`$?KUY|?)T@0s`c`B5`@64js#5?=wAF5Sb-GXu>yS<<1qqhL0M_x~Wy81mDX3*k z+9H_9xD|4-(VxroQ!z`fVTZE7SGfNfV1S?*%$RPzu`%|&2C!Da;HnMJhp@ctq5gB~Ez6LM^VK1G&C%t{#UvoZi|EZSu({TQVb1%S zmFarF_D@`DH3C}Kot+!gItoozV=Y=UoUJ}Ak#JLiYF zm6^JV&~z$@1*+|rWvOlJ)wuspR<_|{_4l6n*&99VsQ=7q17$w z9CWX+=AXEDx8~r}ZV!VWOiF^k3iUrKsCsN3i_bTobHm|4GS4 z4~-a??s##TXz+JL@+#!Z)t05d{miTtpAO4W>v54)?uUnC zv9d$69HtS-F-|SDTZdrDaraHF?)xgRtJW9)J0^gZf8D!rYJoNdA1|UXnUz+;n8rGX zSA5-*jZ68kF^|bBg?K8s$#C|9LPz{wKAote%$) zwM^&L2gqJOaV z3gfa~X{`sP&|eQ}OKWm%U=&{c0dX?8sqj$r%tFE6e_t7SF7r?sy28 zFImc&$2PB*^OteJc)1ug0#cy4%a=KKf@>GssS$1!y6Fbu9$HVHiq4ehDqq3{%xIrqaa%=9-dcYxZ6R2Pr(;= zoPP_oJNDTJ+#wdO1SFF#|885VU*4RZ?2Yeh8fmV%^17~=6@;o%3BNX0^}xd{@m013 z3MOn(F7Z^3j?Uv~=&Lb+l#j}K1oqIZ_>6F*0ZrV#bA)}0tKuQD{1hF9R;|T^)D1*X z7doQA#Y6;JOG-7au(&Eyi?z|n#CSxr>hV^}a_y3Bb;SM;0|#VXoYPyKX;L+UqW`6v zWL(gxx;eMnf?ml?A2*h5a;IPkI%zwc0hqik>*7;cN+PPTbreDBb8jsPW&UCWW1#5s zbuZ@r7SY1$+5Gd(FY1=en1qbnK*^)f=~T@fZLS4Z96UX*){s)4HsdskS$7fNzgLts z=T?y4xc?nDM(0IpOL|lZRoEO1Q~c@QB$Bkt5}>R?FwMGJpp0!m=1xa(T3}C>tnk^e z@AKujD>}hm@79Sne$3J^wLB<}FG|3n4D|%S!Af6GrQH{Il{*8VP-7zWc_X^X>k)bQ z(LD{N+V~=;ImVm4D<_bg%N-)4M;+~8f4be$mW)%2dca$9`6fx#Mc@Bj8Z{#;H94;K z4~%?^sJ=d@P9rZaLc)jSTY0cX{cbP&mO6%egvjk$9JBIHUQ5y6K&rY>3U+rX$$zzv zAK3clCmd}~JYhM$9ugc>*(g#-b5539y@gH9Ila6z1CtW&*G zTk8`+RHgiqDl;k&YJ}oi!qs#~ z^Y7PwowbV*G{X?Njt<0kMB3Rt=2Nl+QFf{hJSzgsELt6J% zHIo$E5qvzE7LdptDhBq3CYr#*@YZ1VPZ`pgbIM-7R;|t5Le48J8OcH<>JbGFM74-( z<$IxwDKCZljo?Su*}ssa0FRFP?%W;i_Ctt)@in&Cij~jVO^dwe%W=n$4y&Hs?yD8= zA}iGYBouv&sT?TJibNbtp4wR2Jkg8|7Fj;(SPg<_8<2Gu?B)-Yh22LtS%Yu6bll-;p)RvYPFqjuZK% zpe-rT1_~Wv*PZwA5OzuYZSsfyOPG94nbtittX{08^`qaJACA;qu;E3VyXwD*@pD>j z$a&d2x7|L{?xNo~m9AD0Qx=8k53+XIs&&0_tst*HMOpn9HMH5qg`_|YpqW7VJ+XPk zeHxt;D^s1zATCipE1o7<#lqfunh*t@ea$GF`6+vsc#tG6XpycH5@4%3!V z3Uq5pt_k#6wG58&mkFJ1n^hCD)LMNvO z3D#T&sc$MY17W9Jag$P`udnp4TP`_=oiQp>Lz-INHo|4YCt;Dv$mBw@lxA~~23jf{i| zPew(e43Oe{|Gs+Zd)LB9XJSY96xk8`yNf?fj4TSptNQDBT(S}3J5(e0{-we<;j<=5 z=z;;!qGPu0M~Gsj2-ZRFS15U$j+-bf%9?5egD2z*=4$TiWSS;x9m4UkN%IDk-7~dz zWL$_W*@83glU30@2_gVv z+4ov&1Q>s01QlfpWwixK>+oq^0Djr%sb+PiStkVsa50S@o09Wp6KT&zwwCL{$niSY`?fOJhxW(Fl)mGPu2XYjoq8tBsbNK03-KCZ9jXU^Vi`hE&D;$`z z?~B_yCKEmdK3@(RZ2z`(W`gYKA46dwf^`Q5J}sX-=v^Xb#_0!)w@bV4MJ^@GH8)*Tf{`uOqs9cFW+)-Th=8(nY?}(pQe?o{>b88+9n>*b< z_jdVZaI&~E=7w3$$dayya!h_UY8pj9vwRRKlnI8Z2Il>f)I>LD+LHMvwkyZ-)YLz*~>N9|PO%z_RqrC>Ae6cp5rv}j1PWiQN9*3&cS3kZf&(}ksha$d? zfPaUfY}l+_T4`ANvAa!eGJRcMpLj4CC~UP%pNdQIG4recNWbg;RF+`Gt|cp2lEYHo zP|YW4gB+0uk>p@I+S$?{>B3?{`NMOZI7h7s30>ddTcYXrDqs{2fl;WUo$=8qo&l9> zWt54qAwSh@cTCjKW1|K_bG;5!DqZ<1eh1pFNQ_WhoT@^tH;sD8{eXE7q0qL?7yy?& zbBbh>h#5?~^Of0+-uJJI`J{2Q^vJ>?+FnDKC%D~iSSA+66#YW zZH%vyoF4pK)FVO#Dz(=3?&M!yxTZZ=x;2b3Jt~t~zv5wKT63RXi$dT58ae4Am}8bK z$b)uk-S7BWBrxa9fRMl0?xk_#T^MmkRcEW9bL{-qtOyYz#R{(?5J^m!Q=&o*6nV?j z7qs~qi&*ZMgseT#-;Smn4}vF%-#VzTMnBMz#)6$bDY{$US7XB0^Crg2pD6bKPpuGbd{c4m=?`{xybW55YAHnW}E$ z<$rOXSk>+s&&*F}B@0sq<*`EV5^oY~#Kwl!o4FFzYNybLWed(OU!2_=HA{b=#Ll!4 zv&(oip#-x>`%Q=F6-n(Fr`D;t3-3u&KW`*=!V;xNn)Uw84;1wIEXY3kBCnnS5xq^# zZ4dWLci?ui&8>{g_!i2ak9{}Qs{I}Btf$;3S%pAnH$68Et8(*vUEj98hi`rkTi+<5 zF9FNE%vEwbt*lTUDD@P7H#&rYhbsQ<6F16;qbRtcu&-CdJq@v$~ zS$Vx=wm?3y@B--D*8Bb@n`>Xn(0mY02C%xLCDwJ9ZeGk#VI$ZW!&V2lmiv8UHVBH| zIG=Z%$;H~ZsO^t!&~Kjx%!YSww3ZiYqI`O5h84)UeU3;8T~$&RoNS3wY4J?c?wFQzhOh7vKa5 zV5;}jB*08N(a%BiXS+{=bfL}9cfZEilL$_fdb(myd6 z=IgS+#9{qrFuEa1;H*G#G{MG5=07Erphr+%N-V)h@UvvQymcmFrZOHIlUot%{qhfG z=rt!yco1l#62*Fxmp{57Ym~NuNcn?6JU-z!wmFCW*Z80dii-(N5h~+? zd6T>Ms~pQx|58EYfi3W=715pe^#&k>waLb6wJ6z=CDuYM6 z{ZY_U<*W7Shmm=Dzr+2r?P5dY#({cX>Nx9fJ)k?FJmmyYDO@558iN7@-QjT3ZOC)p zw5X;$1ciM$4&@Ww!ZK-83do6LMOrh+hSRl}T0N97=#K3ml3m>ge!6ud7M5J$b65W^ z{mc-b28#Hzh8V<-_C3w5?{~Mtik|^vy+|V6_BwmzDkC9cn$U)$ z!1o>2$P)bvWh*{7cZz#kiUdxJO-i_FNfXq*bd~MD2dfg_?&Z7rPme@)G*${Cu60r= z8%;dAhnOWw`lfYkZx90fVJ5Cl{}xYGt7^rh*9;GS`%*mv5bc}f4%X8HKGnx6TVcug zQEF?><>4AY!56b^_?|%YUA)YlsV;AL^Xz|~OY3LjRW-v!GFqcgTc{u?&)U$(M-Sla_{~h2KxmerB)AC zwCG4!>D&{Kh|numzRB&9?@m1)hr`Ael5sCa?D@KtY?J6AA^FYFu&Aizzuo$4o)|@g zWZcDsAGI+y`>9RH0~vKLtE#q8GT1(DUe|@DtTg24VJDZOofw#W&jh|9f{SsvxrN5` z!&_%$l(Dg?DbVJae#spxsc;;HYCRc^3C=*TqR>HX)JQRQ%5&MD=yYf&n&{bJX+Ptc zX>@yXEFf|WyOq_|u5+4!V^`-$j|K&;V9lpeY?#F8Rp8f$wf0;rF&alH zTitULe1mBP(b^0xsYPgXZC^6Pkg2JYR)tfHbylU+BUSF~>+D(u9?B>>7rRvGP#i}S%+yzl@2ta4hjdN9#4pNN z+zoweD%14;&nm6es%Nn=$RrScJYaHnar|jsLi@NNS;9y&jG=!1#I+Xu!B+HP`M0b1 z&vvlf#`oSIJIl?#vZR_BYdmX4#8jWi*GDO31f1N?3NC#7q-=DC*R5Av__-l>&f+^z zSO0-ubioTA1D=0FI;@YI-D?Fad!+2;PNr`q#wnGv=`*8H0i8H6#faBWa!3pQQmd5}=k7w~{^S15iVdOpV6_xTwL~ zJ1t}a3Lepkpi+x6UcKew+QxaUF-RZ!Dpzn8{dnf3`eMF=U>y1RBk77{XK;;>z!gv! z$*spL_M(WQ+bn`o3=Z_9ziJm?A)jdGpp9d~YhGNN;B&?)h*s?3Oh7i>j_&!PEGn(H zKhhT10y9z?gj@S_V zChFH5Aih>lF-D6rH`w_+OSvZ`kyFjp_UY*qfnwiW@Rp+2hns@brFko+@tA6nP)euwevwOO1n_JM@ z;n{u47*UfyO3zcO!il0ySG#__u5WD9@UQh^If>U-)-vO{{U#b ztCO(y_OB<@dUM<1-xlavfGwm<%iiLz>V93S&tCW{7jIz&IryK>2%8CW0Ap@yaFcTe)QR@{3!pWdO1@EqXVLZgEb zQpJME*qDw3Vx{aXOY%dke6FFUtR?t5QVBL}1MMk4l^C@az=%iGBxX|f@6gRykc;JI zbudPMonRb&qO4wxTiT^u4K=|rhA25JE9IU8kP;sE*~hxMTahIK@jzY}*?Q9itz5lGHG| zH*#3O^~ESRhw`U!ELV=;Rf3pRChKnV8(=}^rZANVgHI&oVm{R`fQvAe+vXzKq$$vqXJSJrNcz$jd<&&9<{FGoGT5&*J(!TgxoVhXwIHIrFEujh^Ioq)Qrie`%0M|Eogu@Tc zwPQMqrM4}*lEOiO_o5cNHf?SZF+I;1q#Ft#h{6C2bDnBNYV%_D;6j9UV>yVaPq)cN zU5uW4)-}=`gz+^Y9B@d@NF==TG3%Pi`!zq9WQ7Dr0+%lRIty?Rs;Bj%nEE1uWu{Y`i`fpKjCb+T|J{P#y2%{liT0o+;S7AYOOG3 zNjSh^O>Z~t^i?_7++D%oH+jWvh=Ce+tE$TGR2u)h4J-2N? zk*h{lgz(&E>Kx)&8np?urLziX8u`7L+Die)Z>?h6=FNctwsoPcH1Gxn%U%g)XhgKSgX$|$uetDF6$S# zvGXnFkFfQq{SdpA10ORB3=Vzys6`0eLdMp6d>(2h0#{INh{eb4j0&PI+mWS~iSTB4 z%=V-;5Aa1nsA^@57nv6EvdCy^(KW^1fEW(hbVHYfY>WfI~1~mGI4TZIE z{*=`DQo$t0HO;WD41wX6+#_hur9h%mH7Cp%D$EW~wKx@Qp=)4_i*u9hQv^VG zAcF+vYf=uV0RU!rCTmWAqBej8EC8QsWMmEsJGmf_Cw?d>Yc+}Ktf}#ssSwX>3F9C9=Cg-Hg;l3`ea`G;$7-=?06r9g zB-ve}0mRb-Uzy$s5-tgkrAAOI`2gCjhCi(_kh#Q$gS0@J4574189&?z`%@8nj>5Qr zquyvkAwwOrS9te<=erR7Y+NQEG?QrdBzvLa{UH zP<@B^PLFfpZlI_)Gh8a8*UMfHHP0i};`JVTs@#5;sJGL#x@ETda7#76KJD2PUd}J4 zUaZ#v?%HSb9jtN#2qLOBJs~yleFIy^(`)K zYV~Lrx?V7A2m04zP5edc7GN(>Hz$Fpup9QGGkE5^cK-m08vQ++7d1K^E%dwz4w93f z(yyWXGPPU&DrmO(`c>`S_TCXjvHT$`Do7AP7@umP3W8TC(j$sdim$|CNTDIZFmLKr z5)DaL7a%ACZvtphnTQM^2>e70)qad_wzBX{5JgIyPTN_Jm%rAQM&>7iH<>?bnuL{< z?d3%{$9j-U0A393+;iHc8A68h2{Rb1v}p4K<%W_fNY>n=4bowAJW>m|%%fCJ@96~g zA4*wJBYNPG;dk$fV+9{F+f$!Da}}m5J*M)5IWkV*Oj6AujH<95#{y!CW0!P|UY$W=HFihx{{a60BKjY3Tjq|Vr_pZ>U9gV%1Y@+; zs}x!1v(y4N1gITqy%a3c~i%qIG8rj>rdH(0rDNlb40kcw@42MuKlY&lF;GaQDmcwZX~Bc~Rr*h~{{RPoUx#lJFm)%Nps%0# zAMQOoPftJAb=qs4L#TzaF177O<+@H*FhKUNUmeej(a(;!TH9B(7oO*ZxB>?gir2~u z6^IbQz&{ukkY<@K7&KHfIr{{Z^~R=nxp zbsK-_Yi(T~mu!EKYQ^bai0Zb$52MrGBmV%UdujZbRfA&Jsk#Zj)Ag2zbFeu4)YX7D zb-gE6z80+5X&){7fByi`si|?~{wMewplY-lonK3{YfY+4W!J#DGBoT zn|gM)cWHgtXM-NK%I>?#uCG$Q_OyB%Pj1^0Yj)V%v>&Bf)RsBJRbhm_o#)xXsC#`* zd5{%&T ztVVIoNN^tcEx;faJWN!Wdi1A8)-cA)e2;TQQoe1a0Jqk%-|4kuZa}3#HfA#v5CIu) z)`S7`7@?s6SAug&Q66KH`9(n>m@!d80OpGj(?3dLBI?6qgH=Hul|-VmiKr(cKU$Ht zkVp8a$Jze?g=g^p014l?2JN>Upk$nJUpvk3$n|mb@fwRabuDvKXrfv-$O#`S9M`jn zG1B#ZoXtI(--}Q#7~hdGIjww0Jy+QN2|7D}4%+-qlJhub-*><)37*7eD^r|#{M}n! z>sLB!f1$2xB;q_mzoDheb*sYEKsL3);Qs)9u-~YPy`fIEdADNc(TbneYFQQdtEznZ zOUdq#-?5r?oQQS0R~v6g2l{TIAGoODZKLUWkbjY;xDWQI&*e<3nRW0#iY!{O*LAk+ z+3@XKyj4&f5nS!|^-gXJsOiHb{Px{O5MW5b1g;G!j{5k4_L2-3BLrrKr32e-;xchW zLbqpapsYWpKDnqE_6!0J-#D6JD^wXY`i2EbEei*5$Fv`LWy{teJ z+a2psk{FT(;y@BSw2I6@aJ+w~nD3g@VvgLKpco)9rJVF#!dU=Q$~=GCsp);W+WwmN zZG&l4w$KXx^I3G9k#D8myQHyeTAm%NK>2rMlin$nSnG7Fc62&#<*KE%Cv&j*s?={uloMFW~Er$Vd%wwyz{b zeE$G1za!Jd$ne^oVq1G^wx-c7byIjHa~ZE^JjoipYn7(dS$%c6xWE?NL>aAnzTD@2 z-*Wg4^_BQ)!xy-v*_iGJ*Ricf4*K4ngqv5yZ?kvU!KKejIvqA=PK!O$%701@gy{n2 z-Th6t?Z0sSXc$?`tAoDFk@^~oF1-H$%3fihe-mNa0)k18 zQA1o$Z_m2F#Wtt@VGC}!L$d=TJ?o8oX5(*G)Tq2P+ex)yc3W-S5A#!2`ehgFpPQep zLPykFD%oxu+O+eTC&gHAEdKzCP+s8+w5)l5b5H?581Li79lZrakf4iYV3}r;J?IA8 zk>CM4V4dEyz}VJ-xFDG)x6-DD%ND>8L@1~fNA(Wnsv&R#p7|72jCXvXv>4kF_oxdY zpqcY=RclPP;jO`0^7i`HY7;;=og0F_Dgn$(VZ z!dN=n3m~wNA~Tvp8M(O>1Lcz>#W9+tw=V)`Xacp2+K#T;fU7bew-^;cewlBpHOnXq z4aiJ`5N54va(1I$wk(y6#0V~ZXs)r$(qFr%zV6I+o)5J+#lGDhxvjK%c+%U+#uRh+ ztlesRaC&x!PN-j8%x#0%PHB{5(e(b0q3U;Rrk?P&n1x{(_Nk|)>pF{C1h;5ha@GKV zj{*IUtrsz<`KQKr?sXoN+S{NhTb>rcu&du4>yf#S3uLm~R$LHkbJK--n^z6%Ht#5h z117x^>~gO`(e)pQvQ>!$0uJx3LDI-!wPs(}v{032aJD2LD&QVDpqPjGjT0r9f!m5h zm+>(t;q6a2sIEwz z)>wOt&$)%J48>u`507i$IcOt4HTT zg)!Xiw-7)-l{PgiBZx>R`bU3y%v3=?m>HCqB;vFnmG^-jQv(rMg{@dB%#WN3MziV` zRBjVDPc*<&g4cBRR)CW2xWOXw#=#LE-qD+0QK838NXkIhuVFE#w!M2fcLXmmaIr z4KiENm)Z!BK9$->Jtt3f?B6UJ6p#)%qM2Tj-Q;ysXvva2h>mM0`$iNK#?=SEdTw~AV>rs`_h+fvIKs#5sdNA>p_SKCnBP9#>oc02R<(L)H2cTPK40cD7H_4HuceZ1cT-pKksi-|_wu>F%fzO}rr_ z#(itNo}7C5PQAyW+`!J?xHQPYcLlqmFIx;f9YcQftz&+Z(=HhrWJVqBl85v(V$@!i zZGd#$M_7Pxrr>q_h^1=U*HpJ24zo{idS0?YKX7U?x&HtVTfZ;;)NB;M^692%O@%SnE3W+tqr;y8fZf*GINols7DZNc)Mb z<#hQn4xwy;yGvxBP)Mg1zR>!1ZQBVE?Z}E*)!ReQ8O;4E#T2T-b2GL+lT3o4Uc`u& z&!tFLRH|G83GkFgI5j3#g}wj=WK^S5@>)hGj@YRRJix{zVu*nM03aiVToc+&N-dNE zW*Bcgdc``JTG(+UmGY0?hK4MZWjuqpPpu&>BQKN)Sj5(1X||972?uu-27AnA*xI># zgWEL0OK+P{pk#rX)U4RYa^kD3f=kzIM4b9%218VvcRzl(z3~63sI^p#D_I&3CljKF*^;Br3hUtp2svnB&zmTcWXJ zr)(F{IP($eYfL=;oH1q(Ej*DJqP1E&cBERq+IJ=dkWXq8(CNv6F@U0dXc}O`?K%03 zed$`{{9gm1Uc?U(W8Se>H`~Hkh>4umW2-;};2*UxCC~Q=GnuNQXD8OORsf9BR6zBh zg|R|FaY{+{=Zb*SoZ}Rc7M>`jYl)Mer!_;OnFf#*oSq<1mZLEc38HFwEgSdCH;)AGkaIn5tu#u`&Enr<$(O? zzBzq~-+aA~z#kdf>H3D#^{rLwdRI^o$pYJY{S9tG-**0=SkUV9eL)4zm)31xU-r7a z4paXC*xIS6RURuVH*yvMtxCsJ{vqAn0s2#*1Y10z2fZO650%(dy|`Z+brI8k7wR|e z9mB#X0l_=5Rg)U$@7weCj@6sJM#Z}?a5FfK=^wRm_Ul@@s2Yu9C982!LAy=9~`oMWYvp5L+^EDMVcxg_k37-GS-v zQ#PvLaF?0~XpvH`eemM=IgOmwv5L&gxm>vONX2R~U>w^Q3V{d29pG{6T9K9~4R94! z&ekBdMOGR>UXFRV<<*W) z#riuIb((duN}+JKo3IDH9p76AGPZxibe$bFJ~i7KzI-73b+#~N= zfiF6fgH=!xBi6E32c~H#MokF5+=z-&aALAh5!*4^lrf$$N?gNz=qplo<1_(oAxHV7 z#PbnS5(3|D)foGK!I~@D-+*jsH1`!QU$V-s!ayXP*N2;)S9@|e_dWybT1)8v0EN-( z7ShJccea-IGhX@BbDg;w?}x5w%<8%aQPP2u*5=}m%4zvVr&772rS$8TotIou!P>U( zAyR0V)=E{rkn42a(tZ}=lL4(h-^wZY!#QXgQCpk=u}CNDH8XsDIbCb`kw{+6zJT{C`~b^R0J3);JgI>x_8afmx_>BFce+*Rz!UgxFsZ7cNq z14*yZ-?WGjIou{as>s$?KHpC0_t*JX-pLs}uYYRBriAI*>n;w5Tw2%|1-A-DHCxte zwCaBoXFny}ijrO$`?%UqOWS$STAlBOGRbb*OiJk19<`4!-v6D?4vRn(g2+neT zv^pUhE=-dwOns^tgMuctl^Xo{&U|!w$nB4%MO4Me#_vi5j34w3v-ll6mhq)!7sb~hZ z7_gQL;ssBb-H0MFNM&~d*3!e}ZOSv=BAa!c%C4DZpHH!2t}m8B{HqdsdM8`Sy4G%+ zq44bd%sBM-u9)L?*9P_5!r8fS?+he{VrH?^q}d$QR=fhUmACz*VzDDlx@!u+t%Yll zGC>eNq}HSlN7FR6G`e;1PnolmiKki3Ghe8;ZqZ_K-x&K*X>c#x(&}zayKww-%`sMe zdJ9&7-Gh$CwH)h-t+-cWe&;mSaJx4`F)(1_W~pky_@V^x59L~vW4F*Xp&VOwF)|MC zYP|~Yt?Mgf7j;|=PrY7-I6oHZtX=7WEifl|Fb*p{81dN&70u8JPpwfblQK-wkx2v- z6Ip~_DJ*31=}I+Q5s$4g0#9m^@frKjkYI5_U93PTRruNOT30PW%0i#dM*bIvrZX4;tB4 zihgE!BahTR2J4z-+&Xo;z;bkVGxr9slXJdnhj-!onteUPtg9nH)^|RlKi{{B6_gx|!;hwHmb=qEsP!_Lkt=Kfk?Z^YU4X>W!!B z9Z9ZPfwhPN*Ozys)j6?kWAS>WuPvQWRQ}O8t0>F=0AEOL8Byin_8+BjN;-R}3^5(I z4o9i0QkPq;w#-`Zh_U5Y10Io9tyA?|iHo{$?IC=IGv2i;F{4e^EIER=5axTt)1@xo z(&`@F!PGYS#wTcaF%>A&etbei9DkaK277}WjxkcyB3KFeb_oR45RCX(oT>fk6xLY4 zP6eO#^shLxrz0uQtlQ_J;OY| zrcG`#%c>+^x_!bs{jL>r8gVYJD;LVz?14v#S39 zLF9LuV>j)cwO91oJGZoI8^U4ug$jJ2@MgK&j@-{c(&{xGUA$po#gs9<$b;-FbFWP2 zw^rv$wR6gdSxSw;5_4GjI!PS++6^*ZTVnqJ;oe9dJ-7$yG-b)@K`$aHXfs>Igg zo{H7$H!T}(@Cb8n(}@=3L4)3y9L-j(n3uS_2c;}m zII~@O88m_3iy6Fg915?c7;kEsn;cg=PsDb$7l#CoLOWABtK!+?UU3&L5(lZ0YT9QZ zbX$XJ?q)uws%IfOHb5lf=|RG-S}4&Jj4I&CsgOaStWuU3j2b0E3=KMjDMA3ymIxfp zB_@8fB=5&+lK4HdK^mC*(veJHcc{qy7yLeJfB0vn02s}H1d8$Tf4y(H{a%vj01c}- z;25uDaf((Af{wrYg;=q1A_4kUicp(K#!3QxVyrrX*k6#c{S8>PN5vdm4IPwqogWxyT}LBR@DIbffK9DE z%{e0tV(A~HU-)bJdT_NL!hI^8{Wq)XY&gg+p|ka)*`81OrT7=(8#dC_zNe(Ph_I)_ zgC6**+SkvP@SMnnoc#r5Xm6p~R}et=qZYH9K$DCCCPzM%VrM#Ka{GjZ0eJ3tsT6dZ zV1b;;K7)Z&IKRT$@a$W7B^)p(){yF5LT*Or5HU(;nT!#Jnxe2l;lh;=Sbo)EKFkr@ zD}(o>HV~y|0BI!tR3>IY;gO$|aX}<*R`W}Fk7`mV-LOvGpv;PaZ~?a%@}IpSk5WOj zvpKuZ9qDGX-iwcL?}B0}j8Z`gLm9#3VwOl32|KsXnXy`o)^-q&GFxC6?^>djL0N%4^x5rrP^vz9#&|6Wb@a+RCA|iWMT_KLc@gbJ&z^nxAxdtmO zZaH0hU8A-Z#nyIUK}KgTRQhMNGBa+tcP_rGQgtqyEyP<0W=(X*EihaGC2aoy)<$bJ z(~?-)^KJ}I2?B_1j$W@B^G4%8|3{U7iRqif;5 znxMh_I-L90kHS`L=h7Iy>y{$I6NzDTOW<+$VOX0Otf zn^ki1-H#Ape{Fd@omtjLZhko6YzzqY&*xprHM#Qx#Pd=m(2d#j20bWDa6u8n62_!r zC-V$~L;+O$LA`8vfWR3sN{tRY>N9{T)r`!ic_aG?fzLH-R95BOpF)1vr_gA$XTM?l z)S50F=6N}yg6@#26cha;iit!+!v$%>I|0ouIzG6yt@#`cRXxH18l zJ*kYaovf`A2LAP-V>?LZNCfj!ogqqXTxJ-^;e#K7F$9_5 zQe>SQb`lgIw+r;*mUq_b|dy+Oq0y^+Lr@VF6(8RDR` zncUlf?7$k1gTuEU>8Sqz+vTWgz=4Kw_nMLjWSLS=atNr3Tl)|FCTdF?g5=5dA}R|E zYC=-Rg_^wjQZpIv-&A&#iVP@{kFL@9Q-ZZ+U?QqbI#W z5YA7q%`jVOE+mW6*kcDrzDK}RGHXh0s$l%7NZTjkEOAYD?q1U&OkB(W=9nY&vmx7 zzcT?6BfT+}01T)Z+{BvHW`&Vn1V8|t#8Sm%YjB~L2g4`LOktC0Hr+`1SV8Ydbb2Mm zQ*@kXAa<;}^X-?cUsCdi%?Bo}YCNvC+B-2LlQ^qRGCh@GNb>>6?^)Gj6~wZQ487x- zs4Z+;Q2Pv$AbL-wEQnb?3aD+jBP$cT=}Hpl&6W*z2|cM;-nZ?cRY0nLsYm&&wHYt# zBoq^l2tBGA!@K}UBu{CXp+eb%ZtsD|0*HWjv4QyyN>W>}*F3rBy+GE}l*wRVdeoqH z18>YDG@{YOP716Fingf$ z`KNg~;-FEHpQ$lSCNUdkAc4h4S;CovjI5caBvZL3&^O;T15sy@Cvf|StdmO3vJ8B< zJol(031Ut>-Gxb%sXk?HJvru>mhKizl1U35l!D8G*YoZ4H7Z7>41#6?^IDA77-s;o z=41m)7JrxG9x29*E;ud60 zYZFRZR0M-{?62)>Hwy(IKPh#Yih~u`*25lf^Blx|TnrMntRt+w>tZGllDMMzS>W=Gn9HDG_L zq%l5~C4EInMMEhW$c}3sNwW;DQ*^tG#D&NH`uQDx4%=S7e9}iafa}h9p4IhSfpr88 zzN5WHGq`~S=V=tg5YS}Hh9bd!1%!j=<0 z!i_0qX4nM(08RvYnv@xGs%H|-81$eVOrBH?=av_J9efsS+6iC;m8~e3e@z)r6Z^c z!*R(n4N5lXVePRhGP6FlH5j^z4>%=O2Ox~rE2MdCLj>QmB>VpWijFysE*pfMt0RL` zCL>A^Nme8%0FF<8r4+^ny@CNb^rJvQ zXCF!sTXzr-)|3ci#xuvI2}XGB-lP_x1QQvlC?0)kOBkseZsZ7@)}S5PXyBObNGu#w zgb%GsEgLg5#fHh^W|Sp?$*Bs|?@{W*kF95~s@F5y^%qIIYR^xwuW=LkY4aGw3iJ4D zr*Wl~oR&*AZZG>zW9?o2FpXtOsV3qPm=9N>t1H-CSm(?m_N5}LfB;ZH{?`8hy-@+K z6vfT__e1_EShc%tgd-_F$3K+-V2^Q9LlgZhJq<~4pW4gxsVSdm5pd+jNdEveVi@r| zZEs2yl|-4%qdvxI$i2MCmQt)EnIgG6p(FSrPTM=L{KqvJ7<#4VvtX$s&}W)8r+*PQn zmjd~i0A^|h9Z1}KvpuRsUA$YBG$mS6dx12>ARrOJT9TZPP(i5$V2m1)kIj+@s22Ue z@5KbOfl&y-s4jDZK@g9ZZI4d;|OWeVH)*3xfhNo62UqBjh81D+QhM=_jq-`c61d3L~Bg+?UmXB$pOu6)5;k3j`gKYYv z6`;0H3u~$iw3Yk|5y0j2zYaIMHd8u;-+ZYPBH;r)Kds6w;_~?!HNizd4?na7IEwAP^Q^++>4T8X0;fl zYmV$7ndWOzn&v&$A~#OodRVc~y{Ou--z<|nn$tJZ3&Xw8i7*;F z3e*x3uD0cx>yKfI&A#0^nCI)!?>6D6ksLNn6}@rw9X{>C?9yX0Ffir6Ys?!m%Pjf$I_r#a!&#(&>&cfh#83B3HGW18RkJenv@!@;zKDC zF^Y;CQ;*t^OTsV%nu36kO$4@rDkYyvlD@PhueC@(5M*;o7-2c4GG`(yK_dbUNkbD- z5XNdvojO}xrvts!j{V1K&wu8%#P?T5a%l8) z4`$uZn$5SzBL<5ils|_ELAP@KzhLq|BU$+W0K>3BHA5+=w*J>G$L&s2>WhA%aN2O* z_S}BPm@I&8b)+C&OV-K$`$j)8R%&OP1MB8RnT*3|dCc%2qtRjZWw#PSflE z0BYwhyI6W%UBp59`_-!fcf(^lsSNg!ORxY&MDg^f>qf~QNcp5p*wv@58nFW#FV`J8P)d=+$(a@VJ7?_jjYA1BdH;D}64gJeZX4k6e z;^Ma+QGy3*t6ZDb?lNf!LxX{gR%>q$scSaWmvOxIJetV3PKrwa44^+oHOtWdr~sYv%2h>Vz_s zZ{=LOY?1vAN9jfKcRyz(THV(mk*OB|99*^!>MK+7JA(M&*Qjb&7c`dGf3o2f)1S(} zxXtlH)2Z46`Ihh7KI^;ouDt$N7wwDUr&`x(?qPK{iS%rK_1by3zqT(GplxA=Qof?M zJLa>LAgCBO5Kn*q05z#X)S2?SlioyCpj)m+Q~v<8%`IXr&`0$z)K+nQDXrPN{-v~N z;#Z1re2e@YkLe7f(OiL2*O*JHxj;cY&T(FN^;mM=kN`OyyH%qKx=qd7lt&`9qYGP4 z{{W`=Bfn~8SY^Ant@&;c!g&-*J}cB-3cKBR03ZS0w>lOxBcV_fG?B#?>r!AZ4Xq%3 zg(Vd6B1Gr%tih>mf0eZS+j%3<)|QFw7>pTc2kS*rvotKO-<3v12SMT6CnWMmy;zv_ z8oL%#`L3XVc{w!W8sO`GDz{?mcuH8Rz!O@KTEe}1SE*TYi3T`|)Qq^DcKzjY5y=O+ zrDKV1O70}Y%uNA)VjyI(?@%i0J-dTRjKx=5T!|$16%@@X#IEH7K+!bYG9u6+}5H-E;HEh zO#ubN#v-Jfwk{Z%^yZ?1^D>Y~9>7$zV)PXpdcmjw;^ATPnu|heSQrIQzqM*DZ1Gy- z!vIGfT2lt3DN_d{fkh5+7{RQisFk1ugT+KLcM(w%oB~ZTlkAw1YowyA(Av++ZV%Ow)&Qs}Nmxc*eX8>{&jw!(PlSbp}q=m+y`!(SsMB<)h^T_o(1Q zTwFnbKRQvULoYENyww11kl;ZP+!~aG0H#5CBa!J;I3>2jX%2BSJ?nbT7vejySR|h7 zR*V_ARfMVhqO~hIPHV`hB!M2^k*0eVc51UtN8fBs9vZK9O3`&@@YM7FSmE6PnF1dR;T= z?G8FY*u1Gp{DybMxRby*^~h=PH3t=b@3La;k@KLqY_}|j8?c-*i84Kh1>zrkAA|W zRi=XyMM&bMkhFk%3W;jw2ss0_2}A%XL6Ax2ptJ21r4mT?6ci1S+MqjScX^D`o2=YY zu)O~C=?=EyD#AgLpK7fRX5o=oNThKUBw#sQkJ_Y6%g7QcLi7~G(4bcFomms!q<|Vy zFM72)e-0@V?EdV4uuZ@5cR| zm7V@A_=Bg%{8OPV#0Cw`GfaN;{AK#4a}Uyg5&AvS2CBxdPRArNZUgeG;nKXjdOwPK zCbTc%`ktO!g}8Pf){l*2;_F(yZt~5WwyuvDsMwT4(U?YynmZeQUnF`0%B! zhc^wj8Rjb{ff?P&whWB%P4*(M*}S88LJuNqIZm``&G716Z5(IPkBd^rTx!|0ucp%p zxLQE~lRnh^RqHF2ujpM;`>o50Qh8gB0b0f#Y}5P~e)W-Z-%!%;{=&o>a5N8uy5^Pt z0F_}cDvX#`$*oc)q(v;7|;cL?rdSQMaahOA>H1+LZ7*`>f2$*l(p} zn;f!}^7_RC6dAS^D-r8dRIp&l6eBIPNEwp}@I2&omj&6X zoGxS0cKV9fKWuez>R;i7@Y$^rO8nU3yW@=Mh58NFEwBmgRztOt_ea%P%iA$o#-(O$ zDMpQ_13vXgble;6ed|USRxxGaBgmD$mtL?a5 zxXyDF^Y#IOy0f0@h@`}s$FCGb0fXC`lODIFbp3Me)#+_q9_ydXR{5)I&Fj7c(A^Al zx{HQbH;+~BQb6cxj zy|0bHes#IWkD;!fkNBt7tsT0(K9^7ja)n9!s=tjd)o0~DZtjctxuGyM-7`^Zu{NAA ztbgtMpH+OLvt#&`@U`1}-&u0mKhpM$)<1*E{{R-x`q#19VVVclzK#tktucMMYQ82^ zYiy`+qciIjn=sCj)$}SP>EmOKi5yo3oHRed{=Mn}mll&_R(S zKb;B~x5^N136a~qFki8*shpTr@{D4wIA7wNv`^TIwv1iXSGL6$3^U$qeD#dpbik3wn#}>!{Ia!Z%@WB>g*8J7)OV*QZ|7TK@o+(`lAu zk#ToZ{E?sIrCPJBx=N;s$}*WHml0b2NsQkeXTQ-eU%S2SiDMDz_M`bC{wnEcx^4Xv zwf#1kZUhI!9DY?^oiC3gFIDL}UZ>JE8tZz{Zz$ii5eKwZCA!~S^YM61Esmhf^Wv0G znU{fD*>BsFGx|sU_=$kIf|WLIh4D^`tHUK3d7sCKyIB zT7hy81_A9-3_-`|MkOgA5_9eAQVBlZ*sBm!5t1=dM*x{xKz8zb(3J;mM;5>QH6(wR zADsx6;M8PQn@@3}0b}hZHgFPQ=AW4 z=jOlCx8nPKPjDRZXx+|x z)_yqG*v6$r!nr0UMr!#i8r}?8Ngkf{ZmbQr@)G6u42c+_2YNS|L7DGbiioKZ={$QHOT>O5Y-(ag_JniqHNHi@y*##urQgzZ-AcUQ z(Kr_yXqDc34$_-%t5OThw(tM+i(wI#UopE))M8i=K84bns7P2SqLgEf>YZyn;MRLTVQ~MZ_12B zt-F^90UCakV<)DEP(+JF4&?DxQv|v;jF2h#g*A|X%a6{Zg#vOrcA+e(GXzI^RscYm zVqozUAuZKVe7mRGtpIkDF%%N_9@J`7pQR~NQW8r~(xQxD0BA{XPWp?(I8jpE zdtZn86|Ehw7gQU00)6JYa~yj1?XH(jI0M)kbbM;gjn{=NOyF@;t$a+grMYoHLZPQJ zMg1qz#r1SHRbDbby6=3NbYMt8?6@X%1jLVb``IMOE(liBm-LhKC3$lH$ngb%+)_1 zp{)M^h`i4VvLh#muDt#|8fD{mbe0ftU8jOG@@BXoJ--oyIp%Am@bd=N8`gr6Y_Z6k zo@=DyMmCY*Yy$vCnEF<>Me($|cE!T1(F|h)iqz;{QudV?lFc9tA9{63+jNp-A3;bxx2t@tJ>vs> z;BIM{s3pR$a8DmOQ*6)M40~o zbvU&{STw^H@!t|EO?LXNokhK+7Dsrj=_~Pq+>jJ~s!nbFYso0Y8O&B~Ku)1|(0De?NGLgg_)PmzD*V2?(BaZY`VyNyUnt^PESdQNH z6jc%HQd58jy+vx_QxQJ31eG`$J)^xz?!F1=t8GTRQjOxwqQshGFI{^`w{3Zl1Kh=F zVM9PXQ;)YRnlvaCrf zxE>%@X~R~)1AUc(I3V|{4j*Sxst1H@8YFfRip23owt>n8 z7VVd3k1>krJaBCQw9X5X0p#MT%nN5C*c*HHr{d5GTv-Y?_a>?D>a!cFniT%Wxfsdg9-?M2PTHE(;3sUG9nh*GGNxRg^=qC1Z0v} zWYh+(0)WWG_wQ22R>*4`m4@Khh?>K;jqXF0O~N|?LDj2VYk)V(MoAo0wC1uHu!ub7 zlp|^2lz<{?8d6wmo689ufuTwk)19OcdHU6cR6=bqwk9J2jY+!O5L#6CBv(#sWLFhM zpuhruyV{G1R&A0=oUp+eipCH609SA`z&MIV2JO}a5a19vnW*B$ z$Z~FyU}HQ}tgptm1X>*6k}A|M2%Jd~pS@CI*9Wg})oAbO^}22LfGuw7k1`+gT{-I* z>zAL=d}y}x_OxARrJL52Z7jKvJLif&Z112q#hnwTBfn148`_5i1$Bi0`uf)XPI2UK zb%CqC&w9`sj|+e)l~P*zfXt>RfGO5-H2TK0!I87xX`Krln>}TT*zy`M!!^0k#w}|Q z#D-Jc9y3MhIrlFU^GGn&+_h3}J&C@>3p2Nf(A;tXI4kQ(c;fTo14?-<0=wCyI3SFXA( zX{|fEU;*Bs>Dc&RsaTv7_!jD6~AN3^wJtqW?aAaDjE zjXwhomS-y@H@JaTwKyN(z?ME}!B~nk{7$i^wt6nTLW z=@iTW+*!l{u=kwOR8u5l?^Z;c1AOFd?0eA@HQP)8QgPS=+N%zV-J&;b5xO|52AvAC zxMng*$)JMegkdH@;!QzTw9Db2FdnnbSVOTDwv*+K4<@C84QUvS!yIC*Sg~*(<8K^Q zD8#>R+oWw%BgzPhbz{JMPtkggsmZj8H0poTM;!W--k+X1-X2d=@Xu9A@bq__a0;Z> zrDNeqUxs?d58&=+ne9%-el+`k3w3MQ{65GzF2H~Lr(nwHC&0Z{*+Su19>{pBWM3NH zbKyRrY#q97-jZu`xV{XU{{V%$!$Dm(WmEO$uj`Y6{*tr)}{DYsToDG3wq8e`N8m_9|ddOOWxR=00^wx4C02zz*^QYU3YpK zHXnndZ}@k10lTWSM}45ktK_~FfA|95i8nP26MzEEUnB5tU*YFW7NuJC$M3!1eSo7^ zz_j=qLN9KEv}0`IogV|2;7*o7-)-7?JD?A0Csh0^Nca<_HKIHO3^-{s_o`L=bjA2H zq^-8McEV)glymP*r{Hyugt|tUvi=Pv`79^*;*CEAH^Mz7E=|2XnT|b8IzA0rUWuo* zx57kL6D$@el>8f)r{u6j805D#YMcz|imJJsXP#)*=ONhc-c{OLykf0i7LN`PF_OS$ zFeoxE(N}LZvH;9adWx=FXJ&D8YL4Eg+Nz67ZGw399q2~ekio!V29(myaxpAMVx0(V zjnZQSk4ln>Rq}vmnG_Ke3`BmFEm*R#69A0U=qOqncKpC`nh9-aI+9pz0xLBUV^47< z%K{I*QCg=)RhMOwFccZ9NFK)U5EDGrrV{p$fXt(C^#1@gA%xn7Kda^Tq%#>1pl7(? znj$LVhJCPCIQKOiE!_cz-VAm$EP5d3Jfk2%HKs|r0<7fE7^4g%TWhbp5U0{Pq7xNX z;B_$3>rNmYF$*BZWC|=^hix3~+#)h5z&nrfoF7neR-qz>6LXM#c%%i|+PKP*-`b5x zx2^6huyMeu(}5bhOvc{Rx;Ud&1=nC>1du1`QHZR~5t!}kOn?NLJ7fTJ1GQBbaf7H= z1bR<3mY_ws64B<90-8>??BFKE1$%*$RAnyO4EDn?=0G%JE5ZixVn9N2dmm#@#WWje z0)$yLtn>L(68-up?3OfkIQHD{al`vm)@=q0i^U05kbVb*3^@Uq0A!wN(6HWe14$f+qe+V8IYM(2 zQcyv1LHS~&AjElpqzIvEmAo=Y1QCp$)k3qigo7$`k6LhPx0iL0zz|2JK$}FA+~5d3 z&T44X0E6cW2$`CKEyRzRL~>@L4;($L3QLluxgjK_L$ zX-%hPV+L2S@A=h>O1WrnIbcsXtd%aGe%59Ha3oVAFzBr@lB!AXP9s*>l0&o~dd&L* zDyq9l1bUA3q$OK<$pm_FM8X$Ec2KGV0W(!e;xeAX4t=XD2);MLMkLN)O(_+8g-$b6 z!USe#+DjoFz?rL7DTTie&jxuFU_h3MRzn*{98-lhrt-t{tl=CovdhBvnz05#uG4S^ zaVvr%m8GJmJ~rihK-W;1334oD3&Cd2XKN9KWcCk07q@0jAnaNs|Rl3BeYC&M5_zEb#4Cu zsImdz8nodzd=eFbI6hf5ELN=ex_7pCVHq`Q#ah&EQEuQ8pbYv^rw&uZOp(Z|*j2Dp zGO`c{e_Bx1;t{%XDi{Ly7{=Hhvr@x1+y-D0M0?N^#8n1Wq~wXGOocV}xVvqv5@Xte zY9j^nm0^!B6$QI)1Qpq9*wjE;!WdK!p5l-oLAfLa?*fH`Vvt3%$6@PQ3v0nL%B$`> z)Rx+WfFuxej@3%Ac9fB_Nepo{VkYEUxXR5riK(dth%=CVK@n3UQi^~g0T=|;s|qGT zQqoDl6hnz{N%Ix}9uEeTB89Te1JH3&3R&B_50s1#S`nTY3xOH!P+mwTOfPDTNevz) z!h1jz5io%Y(H_)e(R0w^#-Fkq5-fleb4f${(+(9;?%bP%-^@`rQgF4dwNk2K?Gz;?t4-Jxfa}9 zf#^HbR+km+43Kz^^=dW3?UBI26jcfenApM(siGZQDjP%)81a!wgdN@+C}P=wBA`XE zsH4OzPd-uq0LY?LeXCY(uI;;sK2m#DwNqQfLFfU*)DVk~HoHzaBNZz^EtCMsG1@A` zwP!F?i27Bl3>Q$jQU|3hr<8&`qB}PxgjfJ&SJ;|i%4(@A{-7#(%?kqAfTxUPP*f5v zvld)%X#%za%RJ|RD#WiDixnV%PxkRrs8yJl5(aWHRZ($mZNVabnV}LLQGA7(av+mb zs}jOLh;9P|y=b3u@qN%`h9FU(Hm%zV!QV5CarLPjs;VUp<2mhAN;fkMKGR59xY4I>;p-kd8rz!xb87A_MpLsP)S|0z!d1k7aO+)0QacCc*SjP-?u%DLc<-l zF9$ps=A@-`Xghf3eQHY%yxDZV9+d?J4FoV& z^c5IjLzf`uk?T^Z%Mwuc!K+baNS)UK6Tz!cz_u;zQMcUCRF=vEEd=vKNE!Bk?+@l_9^g$0LeN3G`>-aGZrheYkP4g}Nu~-t>k7MM z_Z0{WmejPd0L~9;6F-dJBLxcp@@-gx3Kk1c6nn3y@);f$2QZ76L)I zt1U5s6zWDVl14rI)Ug}PMB@-()r7WYwnBykdt#&$d4$XcBL-@N3`=PC;_gT^vN+ld zi1dR}M6##8?cDA>RE)|pft>OxC2CbkFvJpmw53_FY^mBoEMs?SF)7ici((tQ4%H1? zTG%(cl1?%?tI^#>@XYx^EMSvPqZbOr4oJp$tMmd)Y;%}6njszEFpc{`>@sRWRba&K zCO9ILwKo-%l{;sco3fe3LoxpG>6{8MUKuBfD zQQL8;IRST1Ec5+nu`%+-oF!|g-9D7g~Z~%do#D27> zyF-1GxO3}FAGb4Bmn~mMLn|svL$N(SWtt&*33wq>=SrZm*(K}~~wGPR-Zdq_S=BZXA-F9vv zd$1Uyltxg~xrBBUAiKEAv0x9OhPU znWCVzvQj}MN3BT%Z8FxI(nUcS~0ki17SLGRzKS;+zG(Oo)-%XvUyG zY2fF$;-yY;v=U^2-`0fzvvH8)ieg2!@ggJ{$>yM&fypHHgW97J3XR+Y%zDz6R^@Flktx8gH2s&M{O#xU8U0e1dqW7^IjiWW%l!_r+SOd0TMXj^4(#D5(Qs+O3h8s{*`6 z3VvhhG@y%&+=(Nx`qLFug|^VZNyM6jgiBk#Hi=R* z^{OVM7Q!)s5i$jD?kx|F0+tqlBfN823R}#;+;$Lo#ROaAU;`(SO43YQbMpdyMggFn z9f*^0VDbQ(QHgPLBW!-PU`DophPGF1W@!R8m4qk@?m4C)3t>zQ{b;C2`+*Fg9@I1& zvI4LxJ;hRm2VTH#oP$n;f?M0MiV}#pzt!cV2$@+x*#VFB13`;#VUipo7uk$|R@!fe~!3A7IM zQxR1_`uZAT8m{r&ZT6Z{ija0-j@YRcb$r;LsqIEBHs0b$e>C^>rvmPi;o=IBn$095 zs!0I(u^p`R}q`?BZ1r+5|-zP zzyqII#UMA8kd1_A6hd{q*xv+F2-PZ#+MM(6LDkwu3OrCrJ^d-gqg!gkc|=ADs}`;9 zI*@=#?~GPVQM8XciO=@-sOUT*hzVWWOiwi6)s@=2NE;$X)5Tad75p{Ys#p^fJXEmh z3<21sYAokX60A@$Oa-u0+CF&2oH(n-QJYU>O8_!f_R>2 zK(}p`j1p9y37T+f?p#}Kj>1p5sz72Az>{)hNM6+~3ARZICPz4+=&o841DS)xG9fYG zCf+?M+yxx47eFBP^c57ks;lJ#93N`53eynVN!kFI0*zR!h#1HmXCkAe+c#mDTQAs9 zjkInRo54mhIHyp-WmvZn#wwL1w}0vl`DD~lollj4QZs=nDnV4OUnpo#6?M9 z0fWzAX-F9E3(h&IB)Ye#CyA&p#4T_t+xH|KRDOLJ^R*b6}B)jilJ!TG70TDq%zz zk%7qo$YIzi%~FH6+}c1vvz&LUM1c$%F$g_sk$nW)RF?$pG_4vuwlh3?RD`JVV8926 zngL6^5;rhCjVXQzXxvGTYDHW|4jKduLCC3Ts}r~;*$@wS{An#f(oBP zd8J}peS>z{Z$rfeoL)Q2Tr52x(1mc_$ChXGsR5D1sVD2+qP8c(Jg$4yfmT_!WK0P6 zszP&IB!H_V_MRzN9}(5aBL}!O1+fb*P{}bQ(1m2Ekf(pwno+BF$puCaGexK_>i7fl z%~5Gjcz_l}&lAl$hOG0s1bTs1p$D1YZXn@_=AH{%T@7lD^ z0hp;ET;rMasiBOZ-6V5Fi$irxhEfN&tsz9d& z!){+nbr>y}1pZ>Ol<1@7B2P79F5;cOU!Z|hics5CMU_~bnH8)SiT5-alpV)v28>$_ zjkMt8`%@|mW&sQU=4u98lff{31u+|JDGUwHI3BeEE|mlYZ#Nk{)lATaR)u&I@6Bfj zZERve20dwj!0NEFC!ErYy=fV@$=wlyStVRXSZi{yJJf}33T-z75_?jWb#1d9p+W6O z?HZP)MoB*PR0C{@W}n_^Ah=Xu49-PbEK7F`62=JPnNi*gWKMPg()WC3Z6;KcdJnDTk#iSGA=VdwPuYSn|>G|wtkeruK@uLR$IJNNv?pb zp&^DjssP&F7%@^vsG9yG86$zu2BZ}aAf4IbXbniAgc+Fp>Ke22V~*riheo8Tw(ur! zDj#Q~3zp0(Fr1Gr2?9^!%z7)$^RcABEZ8ig~M^@`HOOR};^p<|zA(`hdsY9(~U zxa!m4kOvqQRe{`9fb2OYh>t^&%zbM>rw~|jD%7w7s4552vV`MwU<%eCD&3h_2hbV{ z?td{zZPV!-(jjPyK5o^VA_YMp0B7@|0BtWlyJP&*g+X?{j??xS3TMD6DKIgSCXp~zw3_$>Q?@~J2LdXP90%{An#6$_HB9K^H89mKw0^ZVs zWCKEhwgg5o>MACl1Q0REs{t$;NG=FIltHU&L?9sN)@jA6x5n3E392mZ;dmLS0Q?L$GhVaqCpUOts#m1zF}%kv}y$uuJwgCJ%;!YW0n^W}`jW|WrI z0WJrctP+74_ZD29#BoDM+YHgXf%K^q_QSW9BX)gi3KmpdB<4FBQK2m`m7k%ZErI|M zlnmlcDl95(17np@BOa!seTM1t4Xnm}DvAeuq`ZR>x+;lbf`Pew!@UG(sS~^j%}ecq@hTxiA6hF) zKnjfcjY!o-5M2o;81$w^rfgSTk_2||Oa)z=L5fKe#b*7L?>7XLh8X7*qb=OtHoHOe z98)M*UAWvxKcz`Tl|EB}%+P{nVThkmN($P6D$;Y_vxO{4mDvOyX~nCTU_yWga79a0 zZQW9KfgS3_sITSo$nVEA4Kfzu8Nf8+&JDW+f~pDDqf)nbCUQ9w|# zYSW8>5=KQp7YSm!Mh7RFh`dm{OanNGt5KU3+C-07r|c{6s>|~T2LREFS?)g8Hqd9e znzV&&m47rD;Da@Nro{}P5ID^#oy!J$hygrLr4>eTahS5IMB+%HQ|;Q@0tB4qw9bz1 zoRtD#Mrvz9Z2*Bw!Sv#Yv?-^CQeJ+Pqt4OgRTKLfDlJve9tl5cBs!qXAlwt}IjAn! zEK7X8gVvF&GD%_q^b`{AHUg^g#aeYj-sQ<9{rgl5PZHh%k?&HC78#vHfPS>B1<*I1 z$J&HtyemTPGu%)U!%|CcJ9~jz2=0q)sb!N@C0KL-fPB5p5)@`Cr(i59!2(-$A}5*} z)tgI_%w{7r$j+Qm0;?zxJJJTF3c&!FsvxbE1dsp|k7|@p4GS4z9+eWv03C!t$P-cw zGno&caW!DovXF8_{V2wzMAC*b$8e7IVbZQ!g8%}c;(4hYj{ppcciO-;yw2i1;-RYv z3>caAHB+VDA(eta&L@hnN(*~~!vQ}^3qe-Sy9F|f8DfNL) zfZMmYlgHMKI=hOgi9cGfe%YS!MD31oQPFAlIDs2Mny?L3gBdo@&J^E88f9%o(TYpR%q4aimO&&)Lxg z^5+MGG=xL|osA&(_pMK)(fCm7kqzrs0*$stg%W-1Eop3VfyGKpF)h3Ux+)rmcY!A# ztwdsH59Ibs(dNqc95xxhK-1bEv_ReW^fuW9R)ipumH$LXr=7p)8Yec7YS?Oa(QB7dIFrRF}QH zQo&$-YQ?69+!%L>sNraFmIuBcCg2KmY6LnoQV@swO1JfdJj)pge<6(mAd*wc**(CiX=LtU z*n5h}Q>Y3E1QY90f+>Ijicptosgg)JsU@-GIM1l6C9v|50$@@Lw`h|j00He&T6fBX z08ICyg4qZllaA3uC~X2kfYo3rAxQ-w6B0#7MiB&=VHo11qb^i%J!t|K!m(gxGtV^` zM~ceCn3^=4ra&a0Nv1NlmcU^YQkN3Rx4B6zREV);xRzr~q|mB>5ttN;FcZvo2m*0c z1tB*9Ygo)dtfWD;SS;Y?q_kNCkVYz`R_#oa^8IQGHrD`W9mPp|Triap9966IZIs@8 z#tkD)@K$78nC(*0Dw%a;}0tQK-gxn+LFi&Ap!P+eRu`$gdE;^aoAkC<0+2sCK{~SpM|u%Yag;)uq-h!0sL833+Ga4ti6qc)>%{I+*x=M~D#vtD z3ZCNv!f9p{7|A~MAzLNbs}lyI(Qni3Q!D0VaYIp str: - client = docker.from_env() - - return client.containers.run(WAPPALYZER_IMAGE, ["wappalyzer", url], remove=True).decode() - - -def run(boefje_meta: BoefjeMeta) -> list[tuple[set, bytes | str]]: - input_ = boefje_meta.arguments["input"] - - hostname = input_["netloc"]["name"] - path = input_["path"] - scheme = input_["scheme"] - - url = f"{scheme}://{hostname}{path}" - - results = run_wappalyzer(url) - - return [(set(), results)] diff --git a/boefjes/boefjes/plugins/kat_website_software/normalize.py b/boefjes/boefjes/plugins/kat_website_software/normalize.py deleted file mode 100644 index e85b01b6357..00000000000 --- a/boefjes/boefjes/plugins/kat_website_software/normalize.py +++ /dev/null @@ -1,45 +0,0 @@ -import json -from collections.abc import Iterable - -from boefjes.job_models import NormalizerMeta -from octopoes.models import OOI, Reference -from octopoes.models.ooi.network import Network -from octopoes.models.ooi.software import Software, SoftwareInstance -from octopoes.models.ooi.web import URL - - -def run(normalizer_meta: NormalizerMeta, raw: bytes | str) -> Iterable[OOI]: - results = json.loads(raw) - boefje_meta = normalizer_meta.raw_data.boefje_meta - - input_ = boefje_meta.arguments["input"] - hostname = input_["netloc"]["name"] - path = input_["path"] - scheme = input_["scheme"] - url = f"{scheme}://{hostname}{path}" - - pk = boefje_meta.input_ooi - hostname_reference = Reference.from_str(pk) - - original_url_status = results["urls"][url]["status"] - - if 300 <= original_url_status < 400: - # The requested url was redirected, so only return the new url instance. If needed we rescan the new url. - results["urls"].pop(url) - - for redirected_url in results["urls"]: - yield URL( - network=Network(name=hostname_reference.tokenized.netloc.network.name).reference, raw=redirected_url - ) - - return - - for technology in results["technologies"]: - s = Software( - name=technology["name"], - version=technology["version"], - cpe=technology["cpe"], - ) - si = SoftwareInstance(ooi=hostname_reference, software=s.reference) - yield s - yield si diff --git a/boefjes/debian/rules b/boefjes/debian/rules index a9df3889bc0..ce5a950e9cf 100755 --- a/boefjes/debian/rules +++ b/boefjes/debian/rules @@ -22,7 +22,10 @@ override_dh_fixperms: chmod 755 $(DESTDIR)/usr/bin/update-katalogus-db override_dh_virtualenv: - dh_virtualenv $(DH_VENV_ARGS) + grep -v git+https:// requirements.txt > /tmp/requirements-nogit.txt + grep git+https:// requirements.txt > /tmp/requirements-git.txt + dh_virtualenv --requirements=/tmp/requirements-nogit.txt $(DH_VENV_ARGS) + $(DH_VENV_DIR)/bin/python -m pip install -r /tmp/requirements-git.txt $(DH_VENV_DIR)/bin/python -m pip install gunicorn==20.1.0 cd /octopoes && /usr/bin/python3 setup.py bdist_wheel diff --git a/boefjes/poetry.lock b/boefjes/poetry.lock index fcafec90123..422000db648 100644 --- a/boefjes/poetry.lock +++ b/boefjes/poetry.lock @@ -2144,15 +2144,13 @@ files = [ defusedxml = ["defusedxml (>=0.6.0)"] [[package]] -name = "python-wappalyzer" -version = "0.3.1" +name = "python-Wappalyzer" +version = "0.4.0" description = "Python implementation of the Wappalyzer web application detection utility" optional = false python-versions = "*" -files = [ - {file = "python-Wappalyzer-0.3.1.tar.gz", hash = "sha256:28fc8d5b8ace221aad7c5729b923976af53c5b7116fd0ddc452a0dcaeaf4b831"}, - {file = "python_Wappalyzer-0.3.1-py3-none-any.whl", hash = "sha256:0c76e4bbc1e782795f2ccda627add6366153cd53d8f8eb5a5b62431c7c4ecdfe"}, -] +files = [] +develop = false [package.dependencies] aiohttp = "*" @@ -2162,6 +2160,16 @@ httpretty = "*" lxml = "*" requests = "*" +[package.extras] +dev = ["mypy (>=0.812)", "pytest", "pytest-asyncio", "tox"] +docs = ["docutils", "pydoctor"] + +[package.source] +type = "git" +url = "https://github.com/chorsley/python-Wappalyzer.git" +reference = "0.4.0" +resolved_reference = "ac651718af77804e52b826944933be831d491387" + [[package]] name = "pywin32" version = "306" diff --git a/boefjes/pyproject.toml b/boefjes/pyproject.toml index ca3d0a70b6b..251bbce8963 100644 --- a/boefjes/pyproject.toml +++ b/boefjes/pyproject.toml @@ -49,10 +49,9 @@ shodan = "1.25.0" cryptography = "^42.0.1" # required by kat_webpage_analysis forcediphttpsadapter = "1.1.0" +python-wappalyzer = {git = "https://github.com/chorsley/python-Wappalyzer.git", rev = "0.4.0"} # required by kat_webpage_analysis (forcediphttpsadapter) urllib3 = "^2.1.0" -# required by kat_website_software -python-Wappalyzer = "0.3.1" # required by kat_wpscan wpscan-out-parse = "1.9.3" # required by kat_sec_txt diff --git a/boefjes/requirements-dev.txt b/boefjes/requirements-dev.txt index 2fd7a1878d3..e880f82a2e6 100644 --- a/boefjes/requirements-dev.txt +++ b/boefjes/requirements-dev.txt @@ -1068,9 +1068,7 @@ python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a python-libnmap==0.7.3 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:d03629256c2ee9ab37390c28d4c4c2ae9637cd0861dd8ab9e0f32779545936c0 -python-wappalyzer==0.3.1 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:0c76e4bbc1e782795f2ccda627add6366153cd53d8f8eb5a5b62431c7c4ecdfe \ - --hash=sha256:28fc8d5b8ace221aad7c5729b923976af53c5b7116fd0ddc452a0dcaeaf4b831 +python-wappalyzer @ git+https://github.com/chorsley/python-Wappalyzer.git@ac651718af77804e52b826944933be831d491387 ; python_version >= "3.10" and python_version < "4.0" pywin32==306 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" \ --hash=sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d \ --hash=sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65 \ diff --git a/boefjes/requirements.txt b/boefjes/requirements.txt index ed319cc0bdc..c9e438a9e50 100644 --- a/boefjes/requirements.txt +++ b/boefjes/requirements.txt @@ -1056,9 +1056,7 @@ python-dotenv==1.0.1 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a python-libnmap==0.7.3 ; python_version >= "3.10" and python_version < "4.0" \ --hash=sha256:d03629256c2ee9ab37390c28d4c4c2ae9637cd0861dd8ab9e0f32779545936c0 -python-wappalyzer==0.3.1 ; python_version >= "3.10" and python_version < "4.0" \ - --hash=sha256:0c76e4bbc1e782795f2ccda627add6366153cd53d8f8eb5a5b62431c7c4ecdfe \ - --hash=sha256:28fc8d5b8ace221aad7c5729b923976af53c5b7116fd0ddc452a0dcaeaf4b831 +python-wappalyzer @ git+https://github.com/chorsley/python-Wappalyzer.git@ac651718af77804e52b826944933be831d491387 ; python_version >= "3.10" and python_version < "4.0" pywin32==306 ; python_version >= "3.10" and python_version < "4.0" and sys_platform == "win32" \ --hash=sha256:06d3420a5155ba65f0b72f2699b5bacf3109f36acbe8923765c22938a69dfc8d \ --hash=sha256:1c73ea9a0d2283d889001998059f5eaaba3b6238f767c9cf2833b13e6a685f65 \ diff --git a/boefjes/tests/examples/body-page-analysis-normalize.json b/boefjes/tests/examples/body-page-analysis-normalize.json new file mode 100644 index 00000000000..f85c7cfd126 --- /dev/null +++ b/boefjes/tests/examples/body-page-analysis-normalize.json @@ -0,0 +1,65 @@ +{ + "id": "312b968d-0453-48fd-8e7b-ecfcb757dc7e", + "raw_data": { + "id": "e20e3de6-4305-4344-bfcf-a8b9ecc76ccd", + "boefje_meta": { + "id": "a8d1830b-3e2e-4dab-928e-4493a9710ff1", + "boefje": { + "id": "webpage-analysis" + }, + "organization": "_dev", + "input_ooi": "HTTPResource|internet|134.209.85.72|tcp|443|https|internet|mispo.es|https|internet|mispo.es|443|/", + "arguments": { + "input": { + "object_type": "HTTPResource", + "scan_profile": "reference=Reference('HTTPResource|internet|134.209.85.72|tcp|443|https|internet|mispo.es|https|internet|mispo.es|443|/') level=4 scan_profile_type='inherited'", + "primary_key": "HTTPResource|internet|134.209.85.72|tcp|443|https|internet|mispo.es|https|internet|mispo.es|443|/", + "website": { + "ip_service": { + "ip_port": { + "address": { + "network": { + "name": "internet" + }, + "address": "134.209.85.72" + }, + "protocol": "tcp", + "port": "443" + }, + "service": { + "name": "https" + } + }, + "hostname": { + "network": { + "name": "internet" + }, + "name": "mispo.es" + } + }, + "web_url": { + "scheme": "https", + "netloc": { + "network": { + "name": "internet" + }, + "name": "mispo.es" + }, + "port": "443", + "path": "/" + }, + "redirects_to": "None" + } + } + }, + "mime_types": [ + { + "value": "openkat-http/response" + } + ] + }, + "normalizer": { + "id": "kat_wappalyzer_normalize", + "version": null + } +} diff --git a/boefjes/tests/examples/download_page_analysis.raw b/boefjes/tests/examples/download_page_analysis.raw new file mode 100644 index 00000000000..52b5fd1fa9b --- /dev/null +++ b/boefjes/tests/examples/download_page_analysis.raw @@ -0,0 +1,68 @@ +{ + "response": { + "url": "https://mispo.es/", + "status_code": 200, + "headers": { + "Server": "nginx/1.18.0", + "Date": "Tue, 26 Mar 2024 13:59:01 GMT", + "Content-Type": "text/html", + "Last-Modified": "Fri, 18 Feb 2022 09:21:01 GMT", + "Transfer-Encoding": "chunked", + "Connection": "keep-alive", + "ETag": "W/\"620f64fd-72f\"", + "Content-Security-Policy": "default-src * 'unsafe-inline' 'unsafe-eval'; script-src * 'unsafe-inline' 'unsafe-eval'; connect-src * 'unsafe-inline'; img-src * data: blob: 'unsafe-inline'; frame-src *; style-src * 'unsafe-inline';", + "Content-Encoding": "gzip" + }, + "cookies": {}, + "is_redirect": false + }, + "request": { + "url": "https://mispo.es/", + "method": "GET", + "headers": { + "User-Agent": "OpenKAT", + "Accept-Encoding": "gzip, deflate", + "Accept": "*/*", + "Connection": "keep-alive" + } + } +} + + + + + + + + Mispoes! + + + + + + + +
+

Mispoes!

+
+

+ Miauw miauw miauw +

+ + + + +
+
+ + + + + + + + diff --git a/boefjes/tests/loading.py b/boefjes/tests/loading.py index 3f8bb1c7bb6..3eae8bc6b5a 100644 --- a/boefjes/tests/loading.py +++ b/boefjes/tests/loading.py @@ -7,7 +7,7 @@ def get_dummy_data(filename: str) -> bytes: - path = BASE_DIR / ".." / "tests" / "examples" / filename + path = BASE_DIR.parent / "tests" / "examples" / filename return path.read_bytes() diff --git a/boefjes/tests/test_bodyimage.py b/boefjes/tests/test_bodyimage.py index 48d19700924..3750ea7a8ed 100644 --- a/boefjes/tests/test_bodyimage.py +++ b/boefjes/tests/test_bodyimage.py @@ -3,7 +3,7 @@ from unittest import TestCase, mock from unittest.mock import MagicMock -from requests.models import CaseInsensitiveDict, Response +from requests.models import CaseInsensitiveDict, PreparedRequest, Response from boefjes.job_models import BoefjeMeta, NormalizerMeta from boefjes.katalogus.local_repository import LocalPluginRepository @@ -14,7 +14,7 @@ class WebsiteAnalysisTest(TestCase): maxDiff = None - @mock.patch("boefjes.plugins.kat_webpage_analysis.main.do_request") + @mock.patch("boefjes.plugins.kat_webpage_analysis.main.do_request", spec=Response) def test_website_analysis(self, do_request_mock: MagicMock): meta = BoefjeMeta.model_validate_json(get_dummy_data("webpage-analysis.json")) local_repository = LocalPluginRepository(Path(__file__).parent.parent / "boefjes" / "plugins") @@ -23,13 +23,16 @@ def test_website_analysis(self, do_request_mock: MagicMock): mock_response = Response() mock_response._content = bytes(get_dummy_data("download_body")) + mock_response.request = MagicMock(spec=PreparedRequest()) + mock_response.request.url = "" + mock_response.request.method = "GET" mock_response.headers = CaseInsensitiveDict(json.loads(get_dummy_data("download_headers.json"))) do_request_mock.return_value = mock_response output = runner.run(meta, {}) - self.assertIn("openkat-http/full", output[0][0]) + self.assertIn("openkat-http/response", output[0][0]) self.assertIn("openkat-http/headers", output[1][0]) self.assertIn("openkat-http/body", output[2][0]) @@ -42,6 +45,9 @@ def test_website_analysis_for_image(self, do_request_mock: MagicMock): mock_response = Response() mock_response._content = bytes(get_dummy_data("cat_image")) + mock_response.request = MagicMock(spec=PreparedRequest()) + mock_response.request.url = "" + mock_response.request.method = "GET" mock_response.headers = CaseInsensitiveDict(json.loads(get_dummy_data("download_image_headers.json"))) do_request_mock.return_value = mock_response diff --git a/boefjes/tests/test_wappalizer.py b/boefjes/tests/test_wappalizer.py deleted file mode 100644 index bf69678b79e..00000000000 --- a/boefjes/tests/test_wappalizer.py +++ /dev/null @@ -1,52 +0,0 @@ -from unittest import TestCase - -from pydantic import parse_obj_as - -from boefjes.job_handler import serialize_ooi -from boefjes.plugins.kat_website_software.normalize import run -from octopoes.models.types import OOIType -from tests.loading import get_boefje_meta, get_dummy_data, get_normalizer_meta - - -class WappalizerNormalizerTest(TestCase): - def test_only_yield_redirected_url_when_redirected(self): - input_ooi = parse_obj_as( - OOIType, - { - "object_type": "HostnameHTTPURL", - "network": "Network|internet", - "scheme": "https", - "port": 443, - "path": "/", - "netloc": "Hostname|internet|web.site", - }, - ) - boefje_meta = get_boefje_meta(input_ooi=input_ooi.reference) - boefje_meta.arguments["input"] = serialize_ooi(input_ooi) - - output = [x for x in run(get_normalizer_meta(boefje_meta), get_dummy_data("raw/wappalizer_redirected.json"))] - - self.assertEqual(2, len(output)) - self.assertEqual("URL|internet|https://mid.url/", str(output[0])) - self.assertEqual("URL|internet|https://redirected.url/", str(output[1])) - - def test_yield_software_when_not_redirected(self): - input_ooi = parse_obj_as( - OOIType, - { - "object_type": "HostnameHTTPURL", - "network": "Network|internet", - "scheme": "https", - "port": 443, - "path": "/", - "netloc": "Hostname|internet|redirected.url", - }, - ) - boefje_meta = get_boefje_meta(input_ooi=input_ooi.reference) - boefje_meta.arguments["input"] = serialize_ooi(input_ooi) - output = [x for x in run(get_normalizer_meta(boefje_meta), get_dummy_data("raw/wappalizer.json"))] - - self.assertEqual(4, len(output)) - self.assertEqual("Software|Hugo|0.104.0|", str(output[0])) - self.assertEqual("HostnameHTTPURL|https|internet|redirected.url|443|/", str(output[1].ooi)) - self.assertEqual("Software|Hugo|0.104.0|", str(output[1].software)) diff --git a/boefjes/tests/test_wappalyzer_normalizer.py b/boefjes/tests/test_wappalyzer_normalizer.py new file mode 100644 index 00000000000..421616f2b7c --- /dev/null +++ b/boefjes/tests/test_wappalyzer_normalizer.py @@ -0,0 +1,23 @@ +from pathlib import Path +from unittest import TestCase + +from boefjes.job_models import NormalizerMeta +from boefjes.katalogus.local_repository import LocalPluginRepository +from boefjes.local import LocalNormalizerJobRunner +from tests.loading import get_dummy_data + + +class WappalyzerNormalizerTest(TestCase): + def test_page_analyzer_normalizer(self): + meta = NormalizerMeta.model_validate_json(get_dummy_data("body-page-analysis-normalize.json")) + local_repository = LocalPluginRepository(Path(__file__).parent.parent / "boefjes" / "plugins") + + runner = LocalNormalizerJobRunner(local_repository) + output = runner.run(meta, get_dummy_data("download_page_analysis.raw")) + + results = output.observations[0].results + self.assertEqual(6, len(results)) + self.assertCountEqual( + ["Software|jQuery Migrate|1.0.0|", "Software|jQuery|3.6.0|", "Software|Bootstrap|3.3.7|"], + [o.primary_key for o in results if o.object_type == "Software"], + )