From e3072150d9e41204a9cfea7c4b976f82f0745833 Mon Sep 17 00:00:00 2001 From: Darryl Nixon Date: Wed, 6 Sep 2023 18:02:12 -0700 Subject: [PATCH] * feat(README.md): add detailed project information and installation instructions * feat(cli.py): add command line arguments and async main function * feat(influx.py): add InfluxDB class for handling InfluxDB operations * feat(logs.py): add logger configuration * feat(scan.py): add functions for running rustscan and parsing its output * feat(validation.py): add function for validating CIDR or IPv4 address * fix(pyproject.toml): update dependencies * fix(README.md): change 'Examples' to 'Example' * fix(bronzeburner.png): update image file * fix(cli.py): update main function * fix(scan.py): update run_rustscan function to use asyncio * --- README.md | 56 +++++++++++++++++++++++++-- bronzeburner.png | Bin 34396 -> 16964 bytes bronzeburner/cli.py | 52 ++++++++++++++++++++++--- bronzeburner/influx.py | 36 ++++++++++++++++++ bronzeburner/logs.py | 34 +++++++++++++++++ bronzeburner/scan.py | 76 +++++++++++++++++++++++++++++++++++++ bronzeburner/validation.py | 23 +++++++++++ pyproject.toml | 2 +- 8 files changed, 270 insertions(+), 9 deletions(-) create mode 100644 bronzeburner/influx.py create mode 100644 bronzeburner/logs.py create mode 100644 bronzeburner/scan.py create mode 100644 bronzeburner/validation.py diff --git a/README.md b/README.md index 7565dc8..8594f84 100644 --- a/README.md +++ b/README.md @@ -7,17 +7,67 @@ use it to monitor your enterprise's ports over time
+[About](#about) • [Installation](#installation) • -[Examples](#examples) • +[Example](#example) • [Contributing](#contributing) • [License](#license) +## About + +*bronzeburner* words + ## Installation -TBD +This project was authored for use with Pypy3.10 for performance reasons, but will likely run fine with any full Python implementation. +Unfortunately, this means several useful libraries are yet incompatible (e.g., uvloop). -## Examples +### Requirements & Recommendations + +- [RustScan](https://github.com/RustScan/RustScan) (required, in $PATH) +- [InfluxDB](https://github.com/influxdata/influxdb) (required) +- [Grafana](https://github.com/grafana/grafana) (optional, recommended) +- Docker (recommended) + +### Instructions + +These instructions assume you're running a Linux or macOS system. If you aren't, the instructions can easily be adapted. +If you don't already use [pyenv](https://github.com/pyenv/pyenv), look into using it to manage your Python versions. Use it to install +Pypy3.10 or install it manually. For macOS users, Pypy3.10 can be installed with `brew install pypy3.10`. + +Clone this repository with `git clone ...`. Browse to the newly created project directory with `cd bronzeburner`. Create a new virtual +Python environment with `pypy3.10 -m venv venv` and activate it with `source venv/bin/activate`. Install bronzeburner and its dependencies +with `pip install .`. + +Install Docker if you don't already use it. Create a persistent directory to store your data (i.e., `/opt/influxdb`). To run an InfluxDB instance, +run `docker run -v /opt/influxdb:/var/lib/influxdb2 -p 8086:8086 influxdb:2.7.1-alpine`. Browse to [http://127.0.0.1:8086/](http://127.0.0.1:8086/) and +set up your instance. Create a new API key with write access to your new org's new bucket and note it down. + +You're ready to run bronzeburner. + +```bash +❯ bronzeburner -h +usage: bronzeburner [-h] -a ADDRESS -u URL -o ORG -b BUCKET -t TOKEN + +A humble network scanner + +options: + -h, --help show this help message and exit + -a ADDRESS, --address ADDRESS + IP address or CIDR range to scan + -u URL, --url URL InfluxDB server URL + -o ORG, --org ORG InfluxDB organization + -b BUCKET, --bucket BUCKET + InfluxDB bucket + -t TOKEN, --token TOKEN + InfluxDB token +``` + +Decide on a target. bronzeburner accepts IPv4 addresses and CIDR ranges as address targets but can be extended to include additional options. See the +example execution below. + +## Example TBD diff --git a/bronzeburner.png b/bronzeburner.png index 3de026ec74628ec02721d34b8f5db3e23c6dd514..ca1c62e1350c1ce754441dab2f62d28d9f0753e9 100644 GIT binary patch literal 16964 zcmV)BK*PU@P)LRYX?&^;fk>B05i)~R>SzVFcUslBqNbl*r_mG)n`tA3g z_P<{{=iEuaM%kl7X6`-Z``-Gz<@=6_|K-Q}ZQG~EV%CyGGC9q0++~(&F7)Ad(pT-8 zrlsh|px-rlx=)8oKNb3YSc5ib+xAw|GS^osl_S+kW#d^(mK=WRX7h&+^Q9g@^!VNH zf3HuPJY``#nK;KVj4O0qKZ}0So?U@EwilXPLTzjxk-Y(@(hsMw54uO(@I7c6vB{@j zp__VcX3j`==X9%OrkS0-(o3{mn@V*dlj*|VqesiG<2>cK-d)v7`H@2h4{p8Ul1u!T zdhY6Y$r*CbgAW;#r%XP}ipBog(Dkc`5iL%60N&s(*h3rWXCt)oQ9yUVs77QbbzlnN zL8qTkHMHQKZecTAI!||X*z~cllM<04Y1vuOJC>!*_Pz_OmNPC$ws#_%%a85YzyHp9 zz45~%hYmk=$we2sF9jXc@scrQ-PY~XQ|aV8Ow)Xip<}8&ybFzE55nnx0H_VX*F0f} z7@;-B0$s2{V+`^6cRK$~Xa`Z9@rbhv8e^6Eo&Q3I1Ff(N7~hThf&UWUrFvj?uWqp> zuYPZ??ezDaF@Nqe+xG6ewOB0PzG~@`BQFJAUUY`6+p_iaOfGvPG2$}6UP#yX{u<8F zUje1VL?qGzMsQ6qfe}sD1w|RnK}^y$Kkyhj|F8IiGsWOe1Rg&{Fa|zp2I-6L8~i?u zZ|u%GG1{vffwS*Nu=oErD(UGkr(QHQIrE~MIyyT(vSa^&yT?YyZhYC9OZU8(^m)-3 zvUbbXzFaQ%&xUEd#w{E&tGm8WX6aEPV3`{pjR-GjZ3sg0PX%){aR%22!?s|$jJRZu zl4ba&O~2a2DDF()k-^c+7M+(x!!qINf(guo8SMVtv>99j`S0vH)S;F3z~8wS<^A7- z(S1QCKI_eI?dtA*)6Rnj?-?5zyJ6L`C0kw$I=$cwA(YgaN+myU#jLB{;xVhV=@wx0 zaiUrSf-nXVN|+X(7{MGgGgF!$gH^L&jiq4>=`aX084VMrM{G)r1M^3aTwa6GTY?^E zTp+5Ghi6*ll@lmF?4$Z%3`SuJym%6B&V|!ag_o6zL4~K!5H$y`i)jLm-)LyBJo+jHj*np z`W5J-k8no#I+J~q7@2TEIFA`XqOyB`m#Fw zT%AVGkW~z5#9? z95}fW*D{U9;&b1`n95YD4s-AU)E|5tUe|e6dhvDdjCZtOzIoTKPZbM=Z=Jnjh5bU% z?FC`T#_c=iCDW8v^bYG;Yi}Q>=0p){!h?2Z$j=REr5b z85vDyF581a|JtV?LgL*+&=*$8EogA+aSZR)(Z5SasaQv*)k7vvOd_r8Ka@a%G_Pl_ zj?~2sBv*~1@bzuTJUkN#(mJ+2sY{C4U&o}L!SB5=d)sxS6<@H2cEro;{~3CH zKRra_lhcR^Ob%JVppzt9qZwjC3dzGM#A-T^DFm!VaIT&xqH)0iq_3&K%hZU1CWiMK zXjC<{O-Ufz8N>0zCXVcOv3sYD@;II2mXVG7*t^C+$D|k*U#=m4LlM=5o1uU2G?=A4 zC%8dY(;768Am-S~G-}gqRC_C^x0dMnHaBZbfVe;N$KY-K3dRro4(a6|UX{ z;ydZ*Xabf^D9F<|N@5x$%$UzL$ZZ*h`L4aloJ-n8YU9+%JRyl>tBDi+4Qzaz?7dyY z)ZPSU&oQug6=|0hL$&6kRQ6CFa#1Om*!^e&(-vrGxwL_5?l7EhPK7nn&M+OBIN?WP zWzy8$bnk<7{p18{b4tXRDhC7{zA+ZE@t3!v^2qhD=3X05&bsUtbJC=<*KFGIz7>o6 zhMx~QJ}(SeyLt1hd_Mn6IMumhkA03L@NpunFCnVN3C_qJujUZjkwW4iX+*z)>6fx7jc? zi-yp^;BXPsPEVt2dIG)E4Y+O+{X58CxHYt&UxAGiaK2)|Bva}5IyZ~pVWz{eM&@t= z@#Ar>EwwXhXe8;G3~hVuozo zv3+SKll{F@Jla}#;xkA%2gDbIdVHF!?z=ENn&AFC;@jxo3ORF^^Ma!(A5z8ECim5(6^Kf`VgWGEbf+Vj9Mn z^zIz2{xs@mR8gBjCd**D#C$n1hftX@^Z>^0JBsYtpPiR)nesIGl?%wPZ2U{;`4=%{ zW08a}pezYqtW1n}Du>MR6qtQrO5-sK@OVH#O~V(k zlTpJ>m0^AK5cG4109EjyE}l-}z(xn9;Rd>=0}Ec3Mz!K$*VZaFJYGZ3j0UF9OQSI2 zVg3C@{i1M45G(ztXaat2mjPTKlrvEZWdTFv}iW=qWIYLNG-p)HQ%xDi4EI#Uc7Yv+y`HD zhHTij^%dEC%RRM$wPtzkCqN3$9WwI~Y*IQAH{RcZ#F`uu6|(&-V&gDN<}!FXLl*%Z zM8M_}$}v8$AI4e36id*(j3ka9B#2rf0-upYqi$p1fQNy{$I#i6z@jr-F@AcHK68oS z!3)x57{rL;W@mY}PUR%m(6bBS*h!fP6;} z$%4)q!oe>O^?mNsUESwkBi$c|aUTVxOXDa_9|5hUUu*L zZQCwfwqU^nFB(HOY}lq6348W8fNt7P}l0@@TUN%VC*T<7INRkMdR=(rr{b-6Y?Yn%nluyJv6tfj^fgBIH|hGixKlIvJ&p5 zn^3p@#mY@u`rCC|wyZp}uW$Pc#*j6eH&4&za}PB}_NHrVJ^@RsNOE!khgDA_@pKDP z$5QmS%h8kgUi~JRAu}a3M~_G9jZ6W__YQ*W9?AnLn1+FLmx~jJeH`8Ap}i}Hd5hB+ z87-kO>|@uq5@}8x)23xnXenWE064tAglyKp6w-ivYXip#77vrpIdNhPQ>LdeYio`HN&!`k54TJBH$l zA=qib6c+7!WG3wmpM-Yizov5?^B;O@MxPTUvhURfjrCYd%S| z8|9;82|uP8@mh+Ucb;r*nvY)i6A=UgQqq(JN=EQ#oeZ?`rXiSw``tziwIM=TULA>~ zi;fNpZF!*K)-f_%0Rb#z5+I*qVyxtmnHohV>to6^6O9_#_M(Sl&lHetaWG}Ffl2K; z#>RcrYZdY<6fEX+OrK7a?V$+7Ca3P|II$b(TG&A9tws1>$w9Mo;Bf~-S|Yg*k&a|j zaXGV~3(-mrS-`qBl$MW>P&GvQlW|Tb6u16H=r8|jM{C>UU#!`<>AV$77LPt}40-s8 zC-qz|cZ=>jeWQjCnidayMc$l8 z<4$iCiPus9>BX_;_vA19R#fXX(lAXRHIoQu`=~Zt`WI+x&!D~ChLy14*If)8Co&j0 zm>CKjNh|HDi(`X?f;1DUY=WLj(RZ1&qDE215mI9}M)xMd>2<^sbqB6{z6VMtp?`@1dExO({UFT$#A=P7MNs^d`>zDd40DPjw*l_K@-d0G0En3fvHHSeh5E!Wbe?;3@> zk^F$>a-hMBVHOD;#UK9O1gy24aL7lxz8Z_Ly__j)^6*xT!Hv7zL^D7TukS_S$uA)P z@{hf(wXN+jy5QEom?4CyW+jrzua)-xMvINy!;^1@L`)_TIKC;1^wBgYgeQrk$wH8) zIWR6ej`&qaxrDjCFahU}u_gti<3$C;+E zXOmAe8*vmq@4|dyrW7iTn_X_g*vL;L2{8C5FCUlHPWq0RVvatH((a{N`i$3qb^VrY z_buyNuk!aOq*DM97>OOlip6OqyE8 z;zbQCn6IO&o1%ZyLZ#&5$Pot{*X!7^&BAyg#WAZN;3Z$wT*MP3i#0NJDg4jR;<)=) zMNFO)N1lR5i_l&=9U~KI;x(_XVad`0^j@0(Q#>UHp|cQwNVJ$bl7W|VQJPyIhI@iB zx&v?T?QkY7O{BBk-+O$`x>vn?#hK5Vx_?fFp)pzAltV;nM-E<|G_R*9;Q*OceH6uY-$d&x zZdy$!_5JkWm;YpjtlP4+JCRJ?R6p_{%+eYzS6Z&w4tCg_^F2ugPMN7_>T+*V9$JLeFEjG13)Wc3A;`dyR)we1NCNbtwl>CWPOB z<;Dn+rE9qA?H10xpbek>%m}t@ZKcN#%#90lMN- zlju=EWKCX9X4LVK+L(o#b5J8l&eJUPd9uD1#l80+JLk1Gt>3csj%9rd2mU06B-5!c zYfdFy-F7Ph9Z4pZex^&3Wbf0X31QR47YcjG;7^zrVTMFqn@JaEfUcB63*0nM`TBa3 zjaOR3`J1TWnrq9r@^X&?(J_b~2qEP`yeXR^lx^pN;@2^?rypPbS_l5|ABM4Eb0;aR z!8LEm?O~sfW6NfH7R+mF{zALFdLCT=>dSy!kSN=3Y{TMugn*5h|sUTESG#7n1a3 zY6$=O#YwpK+F|q`>Iy<|k2gAmTJC9HIDQ`ixKMhbNz=rc=85r%@k;dPYJtYa2I7eC z%cFYw2q_68vkS{Ofx`N4q4n&Kzj@83&0k)zc+t8)iXr4X{?)4tn>hBXfT-L;J250S zX2@^&Vh&ytZjm-slFOH2t>^Q^p_Dlur)W}|g_8{gk{`MjY7LunzU>6A`{LmZYliqnAA(bOrmdsI;6MuPe<1BT%VsrOlYp^FqaCjdpvNb)gK2o8a+ZIQKA7M@OAd}a~no|DGI54jL0 zoiu?arU-!I%SJpu$Zx5cs(Gm2{ph!Ja;!qAavS-RWmO4KnCY&fxcOGJo^#VHNK5)i zOPbn?PGU%|CHD!xGGu6j_eX?`I9o~~y`zN_S?Fc6WQoD)2;v9L4~(!Tm0w-qNpNry z5t{xYi>{Y&-fA*c^smW%kS3&&WtBN64D8t1iErQLV)q^m(|Qd2&AVe*v3yLzP8qw% zM=_K4#w%Lz=wl>MZknrRkU*SpXwq&;>nBZ-A2DEx5o+AZGN}KE>*I%FXv|E*>nQPN zJ&$~fG5$CjV~34we)6a3q&Ge*L!REWX@+H57nio)O%|gdB4H5#AV&5uUWpSi90{is zBa{J;!WKw7qy;{~^L(ixIDYm^^8d@?*Y2H@aNYHB z)EaG!%_xj&_@|GQ@Wn5-Vfl&yY0f+W8pL2Rd!CC_&PQbo5o3HRW+<@`zE6{QgX0kP=wH#wu!{9EE;W0OB~*B>=|{`cKsN+l^?ly?UpUQ%lrC*4s`HbEQYJR(eCgzW6n zDL^amgq^QNAT~EuCjRqI8*XC?Ptk~)w+qKk;lKZT9Lttlf{}`>d<-7a*&0FOILc#Q zct1_S)^HqpL@)y3@qoYzz?Z)86f@C;-(;HX8>A)u38V-N6{ie~v9rMpJp`|DT`HAI zUqe^icoIYId*}gj9oBowN1lS_4DjNrsnqUR4)Ic)+vh+m_*W;Jt_kA`W~gaV?P$I# zu*F))4)%a_!VsV7O>j;a4f4u`q9tLf91`;jbZ%ixna=PUAwQxbXvIYQBid9$G0-(- zd?KDK5>{zyjJ_WTS2KE$s4wbYSrwAiiT0B~caM|u*bv>41ym3I7RlL{zvrF@9=iF$ zSDfcGF{Gn&(kjz5xEn!fHPV`@tWfGunOg-}6#`cR zqiG6Nh2BBoKrb>*fw|ShCNM)jKb(aNS!(3ZjSrcK6_XS!Hbj>s#U}32d!Wy`qPu%? z_bR&J(I$q(5{b7sr9pDu8vS>%suHa2c>V3%`TSa!x+ni%q%`|i^W!?>z;WGzhQ%ptP5>?d_F4bwr; zyW~glxE4Ve64vU0pAa+WUF}dN3qlfQ^6(^Mr>>Qg(*w@=f#&%L36aQ*;u~LY#ZT@Y z#P%I7<}cQ9#p`YI5n~9GIbHy*ed!B4sqOr3OnTE5{PX80Vb-i6ig$DzK0FnF|DFNZ z^^9zO03Wlb0zoN!r}Q7;WP(hv8cOV#1!Eqo6sXS|X!JjVRLf~s{^s8MuD|%zuktxV zI=Z?RSeDgRJoX?g`8-x>(Cs+P{v?+Q-s~0xKth_8QYg*QCJZQKj+7QUj^oROqQ+|<^A`X! zXAa4dvWuCgjbQ3j;J`li2vIRxPakgM3`r!D=X;G3yz$Lpl8*ixE5s11noyP4FjAHOdVV0mr(y}9ghNHU zf^0!F?yL15DyRx2FGxDF_8`|3y}pJEMHOGR;RO9%yoRAm^C>4A6#$=ii}Bo}?~3P( zma95?&2Nr6w+^O~{^{z>AXV6d{H zVsPBClO6VWHdd1rIdMO}GRjld9zkp%Otz1w9?Bw-&0^{DK{A1kaIUfrAr|)t=?_w! zL0qGP2NRDy(SgN%4k0W$k7a#1VI6+XkwYf_{o5s6c6lDLn2sO*sDSaY9DfEEP+hus zmF;Z;IMOr+&Q%W>!lQTPYgm9xLS!C~L0h2n?m(U#HElR!J79F3eFa_k4bBiUS1T$b z+j)1ZSTrMKH1`0x}i6@nl^gy(1Rt9v`} z#V?ic(>oF{V^J-Z6*=}1Jf6B3{JQJHIEUlY>w!+8r5DTosyE+v@U2(xTyLnqXl;tXNkova7i8(zz- zM`qpTZPU&6j*i(}HOICDzJp1xMuDL27zpdzeq=a^M6qhLYG@J13c^%ysrpzTm zzWI$Y^mHHLwx3z8*7h>4d;d&)>cd4k$qb`?#$m&hg{L@v`>PH-@<0I>UYx}nE{~yi z21%8{u3>&8j!G?!O*?eld1nLbpP=WuZJa@d#}?z5!J77vkbW>9QL_hk*qX1ZmE6EW zEktdG{eH-pV9FDaj*#WB@<|rJ9Et%`Sc|I8QJhvjL_VxW&^tAQf^%!GQyZZHmjYPI z|0(L>-8?TCXp_W5%Wg6})gskDWj@%C>x<}_*-gi*Vbw}YGFvo0u(V8Xug~*6@M5x% zC6)kiz|CqS%-7IowQ3GO{~sSe`=18PcnvMBc}8p0DlW#yiQ!HhmX_g~!_1DjgU5?n zK1=ZNl#tLs1vYrrD?oT&6IO9OMfi|~0{kdI;)P}GllUU&!Bcx2h>&273A60$f&w}d z%5p!<%=Cq(JKS}=GWz za=_eAYqlORS)fRM6N6$w!X%z#gd&Bf%#-$qxgI{JnIzJL@RUJ-MgkpEvk>qva*M!1 zM}0`(h_pxzTV69^Y~nBXBqj?*6I~+eI&Qf92IifX;wx}9UnVeNWAT|e{P+$jJf6UB z@D&>(rWt5*@XyAKuqr9mk!ZFdXRDmOV6CZC-Q##&G9==#MP=>eiAM>diQ-37bxqX= zgSt6C*9Z!rkn7e@W%T&ecB`)uaY55bYI+^Acu znE(?$!vy>xwL;gz1YB78)h1dOkxNt3CWV6$s;NWuS;iCix>KCsYf7@ka&5>dvRLNx zHxJ^YAM3)rxx@^L57uvw;g>%jf^>p`$5B2oX!wab(@2pmcwm!7fIw6bpgh`DAnwJH zgU=X3s55ScafQcoO}x9J$&YA}39EA z7JHrq%5pNTPjN)WBK(M@DtKK-2B_k>K1PRI@sWQlBAF)W>03ORsk5aKs-;xwdjKdV z*3C~kn&F8ckVJl+~qA@lS3*gOVV5d&5u;R62U7Chb3rY@#hjvit#|D=KfL%NZuo zC?>d6>wd&#`@U@I)+7i)paqfQVY6oN2R3;+bvGpj@L45LQ%t5{F*k>uSY}Rr*i3ci z!b@e2#fUbMc}=#P6TzDx9|0%AtAf5)6>xE&B71)I24>EwV`_H{8`qan8qe}NMKpaE z#10Bv1YR^$l~XZ8)1m_8hoC^#gjrzdNye&Ljez(e;Q|4PPMtqit^N$@9+o5mTZ?89 zQzbVE6WNcY1t|kQzjQY{Z^-mqZ%{LHGob~=PtmPzO1texYUoi-&=9fJ;b27hSBzAQ zONXjuD+04!8(x0Chfm#*fuRp$bgT>SeeVcH`g4-!qMOQ zsXT|T?yqQoP>EG`LWM#+8D-h<;*G#O^Wt)DE~}1iGh^}CT1{)aM!{Pl!9JXp2JlQc z5%H{$453^hGD)T0LQGsHDZ0pjdY)Wb=8OK^YKdh!<8(ot*UW2`NKT43LVL0&R}T7l6!m~m=JJ^5%}hZ5c-($UKRzgZ%` zQWup1$%uL_4Y7_`ni*fM6_MBYn%u`ISXdFxS!Rg?zpnfSgqAoNhqstQ(T|;?IzvN33DW4LwT}mwtv?q$Tl;*gy;4d7hLH9p) zq!n*}^C&W{E=EtVfGi`rzEq&a&zEMz(;}pe_^XqgNi(BFZ&##A1F`_7dhwaIk`R98 z#uQ$*ian?ye3oP3Lm#SR+qNv%b}!;XgWohQA|Pl+i%~`>jpEAa@%5HoYptn;N>5EE zDNVAm?l;d~vHb9!BS%U`>!OTb*dZoFlG;d-wj^xco5i)pM;I$cN#OfU2rpc7$2+@3 zJfnuCdBN&e_Fb)>ME#h~;~us|S@2Nicmk}i)_^Evr55R&N<*530umkzrI6|PKv#Y7 zJzGz26`>nL0s=ETZ&=9YGXexLp>U=Dlkrq@F-y^2q8v?&&?7{pB1#v+;kMg+v8>1U zBOscztz@T_W=`%t%)3rp$60H%&O6^eBn5}@((+*EYogI%BG!*+#B&-BrKyp0cR(oF z*6GV-)*OaL93Yn`2?D$TS@=!&IQ7#fo+m%mWT5RsBu-V-&AMNBn#KzHm)UA#)?MhU z_)F#EC*E^U*suN7Gu^g`KT)@FHeMP``BnyT=-KnqY|M-4$y+ z<9wU#kIvT)iXGG3PC{G#bb_Nb5g@ub%Sk-YR1MD!{EqlsX_6$mkD!nf-fe=zloV9v zGIYtR@$L^nU?@*31ft2>3RkVh#J!d-e3!1pP%q2ck96vCTW3V~m{;RzHk}jAhm0r+1|*QgA*YucaY8!L8e%jy~M6-N33^9R1wVSfq+cpO*Bf^q_gAy z^Hn{Z&CA5tXhRT4GM&}+$AJq5(}WT96LA?2z^Ny1NohMI}0XdZR|3dgD1FC*MI z1^L1RhU!Rk-HCHlP1@pjPk{&JXnkOUl0aIhzvXuz5)&D^o_2 zpuUuJ)o`PbUoe8^g0mV5U*8{$gDlR>GH-1XiAo=4svCO#40f*{tFr=z3?3VJvgfqk z0d4ZDy0ParK^Ee^ovp#?B(!xn6Iw(~1wbcDxEheM-Wm@qy+Eq{pb2e4Stkgbq_k|y!;IQh!l0=C}JfFI*z_sudjA1f?1Gak!`(81eS4Me7$b?guIaXgK8pSms zAdp5g>O;d{l{i3I43*1ezJqOW*66H=AeYUJq5UbZWZjelzU@DG5<|{keYUgv(4kvn z(_ZuG+JQSLI4Z+%MCV|=w*YG(0j(bHUJy94CbZ)>*>A4ut}s)2;DFs{0jq)$dPFKM z%it}fi3FRKEU)TL2ok)gG=|Po(M0X4t%WoKlMuFZkccJJ618Y-q6gy(n4dq@gh(BXc$VQg;2VvwEjECozN_wQBYI>1^vip>1Uf z#~LXV96d$-SZNO1{wnJ%=xsU^Aa5{GLxtZNN&neDAH^nFbCY z@Zc~qL^Lx}7>8h|FfE2DYB;{*N*07-)Li$_f*)SMRwym$pxQeoz%@UgG_!pL5N{(C zp?yC}zb1w(nmhN*@*Y_BKo-o@PAk9|j6rjR3VRV7s|KTXKM)~{ z&(khm#pm0O$CDy$p^_keQ!+$IwR*dbXEyMPsr_*fjD^+)l@{aJ#vrqoOgUgSn$ewRW<9+G%`t)R+GoDyROz(N5N z(HK{oHHvz=A!{%Wr;#!19qehpnK`xl(6cgx9hFk)=4|Wfe^qbkGqlkSqQRL)SD#u! zygdW!cq+u>LE=vQ9{AM&nX=Aqi~pI;O39I_k!WE}1Zcv%c%%_?=MLfn*XLkpL!4Q< zCmQ8^#b`9AYofScWn%@@RmNT{)HG!5kcf}q6Ca;~_w|oq_l{1r$H#=^y{O4BBBv94 z*%kQ9_#!?cdISrUq)^GW9ysY5X~ziEn}XPojFvuf=JQyV@aBp1KZzlW=g-@?=kVbN z6Mfgevi9h^1?(iLa4Z|Ox#KWKV$fL&iz^I`4<)J18798(J4uH#W*|?mkNbaLK)KusnXL*;Amfo?Ra_A_!CWQ!ZI4jTRKVWPL>QgGTbs?jgM4O_TAT-&d-w?ju@V zPoGvndq)+{V>$ANX%0}-=E_lDB3z&@Haj3AP@O-7TCU1+`Qnxa*@>A~K}&Tr>HhGu z(fwH&GJp2$fn5g=ekyVL|I5E7r#=MCM0`tVk~6dOujAt+Yj+ zDB&^R_c1U@>N^AVdK(H8A}v$x zDWfudgfr9=MHT4jURblQX1D+3%&AjOx!>Y*GK3xVTK(&0CUaS0=?$xEk9~+tSDiaY zW=hI)M`09_e7gvhXF{OvGV?3j`WyEqk-6Bwt1qeH*4rx>9qUAsgJyGga=7u+HQaSq z3U7Z`4lB>9!P3b7q8>0=9YM&T1(%1vXfj2v1>BJg6i86jAga|a-2F=vKe}DRAi;TE zGlB?G@JmM`IfB<+N+c>};5?NJP?|3p8V$n?xn)zM)kd-JI9$^YOqFjYU@f`4&S_~`o^=s68|=im0=yt9W8w+cK+ z<%?bXh)WOLh%}oLtwq~?!qrPYM@$v+6~DPBi`)Obfsr95OcqB_qA?Hwmp#M9x#!l9 z$&SN*u#3O5Ds1M6hPE=rqqCHlgYucj_$!i_Ht`jCL=Wqxv zeYA?i1>?B+rY?NogH_ZUDd_>^63wPIBbO)ThK{u1OE-J?&cD_1*0*%w^{=a;o$VFE zUQo%;5G7vj%C&Ge2jQ4v7%%4WTVlj*x7JY{O_FJ`AogaIn{h_E^Tp~a`YK+>-~X+K zj<#_$9-abyZwK2>8No)(@&bdqD2h)ga4esBf_!U%=5LB^KbYT0bRl-;&BV0$4@t9c z`(tSRM=@mn>2thY2M+$d8Ott?FTc63_V|b4*=7EUJKwb_J}JUq__rlH#iFE^+FAsI%-@fkS_S;Ro_H_wd z@dg_+daGp0>N1E!z*x2ie6f(n-M@(8jvoUmoH3K-+|mxlmkpvkd7PUm4%1mC*GM6@{BzK(Jlo{* zgEOX1eU2qie4aHK z29-7;sba6*7}?>%Ee&KpUB{c>?%>cNANSmA!qJTIT$LjALdjLg$|MTdg&cl%hmW8B zq=*$OHN4}Uc`RK-Ore;95xP=p#m|2M-1#HM2x0 z&Br!OM(I08kbQ3*AO1Ko17+NQAH^!ZrFal5g9v*Wh4gaS*5&t& z4Awr9!PAe_F@3g%OD@Y{l!CXrf9j$#o)^)wCUyxDrb%=_N|zmBir^6IvY9q(0AIO@ z;5@}dW4E^K(I;Vattu0^SxwA1 z{HM_PPksm0+?g|XY~H!+qICCJ_d5$eU};;wB1t6{_%}#u+_R3s-OAN$Xw2j$l68Ufo_-9s+!#k|qJi?y@~|H2CE%sjHfkc$ zXDJZ!Go=}0DD(}%NwA%X48AuEUv5GE!6#Zk! zUR}cbKNQ1^83w-jjXD}`5`HAo4vmU3!SY|OmePU3mkYL4Z9B|D^I#7mw-yGH;6!$5 zoLZ8g_$5WW{~8ZQql&_hyI|bg%imJ1UM8S6qOcRhjJ^RBdqvO(-4fsPNSv6l_>+iD zJ>O~6>#rff{gdC&2K)u@F+8(x;r;8kZo4qwbM~(_^Ro$O)l*~=&eGK1NI*y4}D>Rwf=RVo+HpFR7*KbNk5(VIS(EnM)x zhHcwkmg!n{zrONcT5B6V4ZYeg3>X_MX+k|whqHVD^&W~N2>Qmx(=8#3)wwAbSsC~*L7W(At&vje8vz;(}!WloB&Dj&CdwX zXjTubGd~Thea>jTR=a?h@zT9|l^shLEZDea-!(WOaLGK?rj4O^`f=3L6=C`e8OZXUT!QPlRY)wq0a`4(qf{tdvUuLSKN$G^ zq2qaZTl9)0i}yagapSqUeEyrcRiC}GwD&G}`|g107DYZs_*6KAWXAf25uZW6+WX+QWHgH+b^0zPl5p9}`61Fa@bP3uxuu@}$w#Y2&E&4xyH)Syoy+?0gQBF z{HGb<{vM>+W;ZO5i+OMfzSepQ6yy#GjnJhL9vHI+D*ucKNVcXVwQ@I)6Z$1B(Rh5Ikfw$}XgyjpoPQab3C$dbn zTn+VH1;%V*#83vwLm4DSQ%E&PTkcI`@abU~uN+0oB_5WXQ^0pt#_;r`CH(LQb?n@e zhUX@s1?dc5uIwfHSw$@9E8rb(HL!eH4Te>RM#L}vVk(?pPDA`eD~H#OH0wDlqdI*M zjn*+jT^@JfzU-Y+jqJL~2Hg(Kz6PlouO+``JW(!|t|hd&^ZBG7JDyjDsAK7Zg*zX8 z`svkV!mdl5cF9fN)YZB2rd#S7*la6v*7|QxDxxxp{78jN*%)ceP!h4R z9Q^-j#lXYE$h|3v&Bl#)q_f%2=g#`*)lTW1R(boa z(1sr;EopEI#_P@8lxh4uv~H%33Tci%V-%)i!l)*Y8c(2le=EkHwqULH(Awu=J_Tl9 z{yKZ@o{jZutB9v{EM01&y^U>jNyup|h0)C|s6X6^cqvaYk&V$gyU<9D^Vfv)MrY5E zIWw^biptpV46dGp+ww9b7F-8CJ=Lz)>i=0C8U5&)i~0s%D4Mb31!IUhRxDYApPQx?oH!NS=x8>R9_yTcC27q|)ZGf+y1*MkL5H`0G{)kFjn_az@o>bkt5* zh<9sfU+BUm%QL*)ggu^Q8&vQC2m<&0%ba)(Dq|&+CBd?PxhJRmHwHa19a{IR5$nB_ zsFE!?wsZH$iQyYpEM2tc#i9{AUNnZNgBZgmcr}@_o>V4tO=9My?@6EjhN<KepS+wH+lgT}iXfvXThkTL$Ox?Estui)x`-&xfM_+8(u;WE%h&srWu?c_n7r*-D=Xz$$ zJS(2+d0S%MU%$obySBxx48kkyhBLGQ_Q)1!#eF=o5Bi`a>SY;RmjBc2q*^Nz+WTO@`f4KlzAp_~ScBGivZE z{csC=$pKqpZ+2g-hT)-T_Zqi0dcmZ}F8tmB0}^jO$aaNKa1FnG4- zyYT$dTV;8i$19evs4X8}KJxs*gL^nncC}jV`x^E7w(+s?L$|(hQ}k-|$4~0`4+*S0 z?!G(h9~itPUo2ivz}ZNrHoC66%zQ6E6xBx18QTgLPlI)4(4Kf+$R_%(fh4XWX%MiY zmOzL^kp!3cATh?oX>czOC*Kc7zac)e68!4v;Q2nV!l3I`)>pj3V8wO))9J2Tf*}0W zd@g^ow{O|jZ7;m=^yI|E=8=;p?|J*1-~1mAWdC7+_0VHa3|6YuoBY6gx99tt2uOiA zFF=%ZYG|F<0X}yC?UPRcbB72_Qv{@la6l*VLLiaa4pIYZC1|fuNTaW( z!E2N9DkpYiIqj4830X$ibJe~1~1=eEANQmuTHfOHY5I3bl7kK1S-e+HeycOjYC z4YWq3WDtCF>UtLvSOTa_ z(lT*x1lBkVG>#%U{vcZP%tqKlU>gSOIS1kDO$d9>$OnP{4&?Fna-}@7bKm|a#z#m0 z;Of_2{^+aIqp!^I?-E##Jn`h&)xMrz2z>u7M3mRlDVB8Vs2_X?{Ny7@Cbj~d6EwIa zc?1A7Kw^0}ljj6{Tnzvto4Au`iKP!KwbDbLVhT{YI0+_kaHh(Z)LvewpDm%MpWYq? z#Lx7zXde6~nmLka%PxR7d@ailT{slxazC|v)yg*!FWc2_wZ1iSa^$aWxaR7kKc=qy zu^s@5li%- zOIfAJIEfxez;FYhL@{#4?DLquL&8iTg=q*nS~GGbxugu_TEcPH0!e)DXtl|?xhc^? zP^n4|qE^krNDdz(Ree9&`4wP;S0TUhTF0-hImas&&tAQD%?F-)e$RL7wc6hvJ9_ll zx4rq+ZlwPkJ^oz)>ygK|Z0sBC{~gcsZXtD7;z>la{a(aJ?g6Hrq=B2ZYD|Gjb0HdN zhY6{XXfPRd$F-`8l{})~1{;hiHK%BlQh0__nR|_an8aIrD*-CGo6wKq z8^+YNOaYl|JJZqSaq%IZ1)JLmPO5wE;P>GUT!L`TCYBqzcx9h!6e~-1adEm_$kwSJ7+% zC>nj2D=CvWVn7pxiv$WJf>_fzS4tWoQ(U-l2)EnTuiEit8J zqu+i+7U{@8lOb>$+U2w1uls4_R$kNR?=ljM-M*;)#H3p89Ek#IrBE&b zs7$3r35c;PCy6t=6Unw0FuU&?;45xL^~|?8MDlN>3HLYw?3>4q9R8JCHf^fCdZV@Y z@ox%P_dN7SwSS=hWBEe<{~{}|lCUM4`nm@fn+g45a<*bri1QvtO{Mlp5g6!Hg_qtqTCsYL(_VswfH z@G60qlBLVm-h|?5H@*2k?T0{#K0wnxQpuv zhHee*;o}J2K8|JAfK*@|5tT<=PN^oIqu)71(;iyWJ|<5Rt(&3}IzFny4wkKEC=U=2 z*VIv5)j{bE920lFfaV<&=sj`<;zk~E%}0Dv@w86ZL@YqoPsZUe86Q1$!~q!`t}-4J zu}_p^Rx&uPIwc8g!D*1iyK4t#j%-Hm+z%og+}PjK*ZbLAA^&raKl99QTz=t&_q@6z zsg742Sob~naAn!h;2#%?g`cO6^`f~Gn0fS%;Lbh=-)TvQfsjxNM%w8px}2m`BKlvb z3sOm5l7}~0hI5Fd(g_05TtHk!oTu;QKqZ1%BcTBls!xC_E{&se(P32IO`q2{(Q%c! za~Od$$)P^xVeb>bfjx7m&vsDm&7sogAeUD}{KTOEM-KSNQSFA;HnI9*0?<$$J@59Y z+y=(}VmtbGttY8jB4J0;mg?^{NL4p|@KG1h{uZJNakEtwv{#V|E;f|wLJ~$z|Lvb9 zuq6J9;ZgszpZFa#`YuQ1l8+;ot6V}{^tNYq?)=R7=-8)kxb~XHt2wT(8nC|i{oC29 zwQJr{s+2xU*4~QF>eHHA)9mplK5-`#oV4RJDcL*M$39nEaK0^3&IRX-a>Tr%! zorj=Fppp|lR^D?0l(h7i3d49p@%Rli`j4Xc-dPkbZz9S!;drV$qR9{kpHEO5Zy-P( zgQVupcx?`aVg)?%FgL?6F&1HJtc}JD@vLTy76JI!_7I1jY+~KT6$vn$)xhBIF*N^W zFXC_Yp)fH(fDX_h13*YZ7ANrQG-+BnB;zE#cDrZ~l+amDQf#0_lC33;1_df@1ZZ{4 zr88H6ZU)Zi-I(6I6aMhKQsFMM5Q$F2G z7G1)vijxFL`w-;JXWHG6o5bA411P<%1NR(~7+!}!MN))F{P+pcy|q>$2w@#s4S{ZK==58g91sfY85CIsa_@E&~tN`CU9qAwKXm1j!f?WJuqWcJN3OT?Kv>w{qx$pj`$#kcYo7hmg37 z7cr>{4JVNuE+Q;ksXOBnl?Hur68xHd=y@j*{Qib?4YK5_VNf6dwea}n z2)mw|#@tv3{mTLjuL^L+)gCH?E=qkKN+pt(IZyVWpx>=p2kjXLvy%?mHHMw{Phveu zuHLmk;kphXaR%dG>Y(q?da73-9k{|(qMY>7fhPH1SSP@?97JO-T1Py1t4Xb{CN-UF zO3%I;GVQL=3{3sh;Z8q{xknEnclHO6Uw=a{(Tm^mJooI)_dW2zO>ek%`X@QgKT%*k zy?xuQ#d75JF>21bs}VSI#R{8$_HxkO5o(04Jk zf>h^)C7gC%2u{H793?Ard4$4MMC|+XnEs<~#_tUaNg6Xkp3rVYXmMu73iYW2oK zlEyBYhkZmF0yKr13_H?7b+q{+q!wp-_K-w(I|tF(`bTIUe*)Eueu;(o-nXq@xAvMX z&uzW+((^Yy@sk?spBS*7*|FmjrAp;@$l5B;9eoz9r#_2(vY!T@RD$bB9-;iw%Ij@h zi4O6SaI6YvCs}sm0fHzXKyc-J6lOnH)|Se>BLHW<;pYSsdEkAPcgNpZchW!ZL>FoEd~aU%=cK zkD`2RIn~)iTi0F1DIGPJ5)%L?P!sU;<0V97j5x>Jh|h@8=xHDcI&xA8-Wh9&t|l0@ zIBvlmxdW3A97OfCpMc+U+CWcl&%ZvkeaFWyKKGn|`bmuUj~!UIZ@$Z0wPxLa&lmC^ zr#^6Jc6}d>oqvatJFQ5#wDd%OdfNLeyxhXc9U>qrkG(+BinvrDI8@9f03~(Gda=0< z(r^V(#x&0&j5T`3;l1@J@^6a}`4NFC4@NwyLs)9mYVe60opx~^Zji$qNu%9QB{;gj zPJ=UvN)M^PH6(#9?nSXTBo)`fft_{CG{(?B?BT}O^@4c;#*R1e%wsJa+1JLz;W<vfEisOrDLLWwGAB^4?MR)@(3OoCG1EU zohjmk!vWf-HPEiqv{Dq;N6L~EQqUDXuoi;)Hq1QmN#xG`WfV4C+h3_v{^7Y@&#yjq zbpTxv&G(N`TRF~MO+&em83kEL6SR7iP}KsAEgJ87l|M1W}! zJ2P#d6CeuL ztRU`i?0lk5s&f);0(?KI!1Ffx7`m|!l`?fJnVGeE2NNecc=`eQJr?8ocNURb-az46 zAIV)Y)zX<*cV2K$kEiL*ktOK`@q-M_h`!#u43IL80S0;~sq@JQG{H1ti9R5$UcffEn?9&$5}(ts*) zEohJ<^_8Q-Dq?mLk@iQH!P)AgG*MDO;juEQ6YUg*5>n8=9cJdV02EW)j7WCmHxb=H z*58L}^tp~`njlLp&G%F=J(A$SwmNd2gR?I!lhmT?+XVc>F^)V^!`$2~mJRth_q;w_ zalKE2O{#AK*t1IjW*qr|fOc*XmtWrt*Kc9sWQ4~ajId`%9bxF<%yV+M>ZTs_4iY!4 zFv*iTO^(J)M3!S8CJ#2yx0VQac?PBT^k8QHaTNEgA=J2XQpCD_CZR|2FCFJ$B8x(Y ze8{{(NR!1S3%Nnk%RM)T+VHgEtV~I4t^?gvDnd4k_~2JC*_cA_WxtG4x%8U^yn*{3 ze)yN(aMe|>xa4}pfhE-6z|heD3Um1z_mj= z7aGVHe4KSrm4s-Fnehfj4-s@DG7PE?&bl<9?+d8cIVMJDuzOpKRwF{O*XQw;_vKI?2nnPSpwBh#f?c!Jn;qM1docq8knn7c!l-Hm7y&D&$WBtJYPUSz{=5}P%kJl z%XQwc&9gEeOyZtNo>OX3N-rwAHgpudA0R0ePQw@6gsi{mk&cWb4zDU=ZnjBanjvln z8r(dVEf0wfg{TwvI<}v{a(D_+GeK{Eh;RtJWJi!8 zQ+h`pGkZvC?Jtt?7!#;;3J`O27o=^sbU?S+U@oCP7S7_*{sO!P$-Q$(686@lzuA+P zr^@$oq>F<1*!UfoecD0!(vOkkD*l(1YgZln{_S^u;ntgP?C$6v@_5C7wPMYh-^&#W z9~0d$`q*C~oD!gFN#5yky*^JQ-w~)h0#$CZ0{^)X;aDDybjfryag}LwdU6^LH81Zx z%V?QmU$NH&fBi`m-Y|za?-JL_;pom9ShJ1Fkc+-`72;VX96Qj!gLgI2obBK>XZ2zA zT85s>iUgJbEwc2Udc29F`FEZZeq;=XUTD!ANw9K76~ik%Ty#ZB-FVM8vV19u(^pc z4#cG9u8?9x@<(sS#N!E;z3vkz6bpZ{V$CW+!Y_{Bo8_?t)~Z|v!R zKyKnTYJXE%ernOM%^?>vrZCY@mLFMw6L}rIiFMB{hH}uFXg${AN0052ICgjzhser&?%7#%+LI{f82Xl#vF40C856>Ip>_rda@V#| ze{vGZ10`yG4$}E6A_<(?zO!_IrLF~GDXLT(9fS~rO5n;(P#N1ixaTp{`{xKu$^k2l z9-c^dgdqAkvec)Z>_zp$_vgyx^5>p-ZtKWp=WqPZkBW&O8CYAkZoRHlsr)fndEVsC z?}DB9E{WQPmNBtZeQL3bh{pQ}CyQWDxhPEKWjs$0$*8$5>7||0(s{@f zU+a{nQG6%ym6gP;XeI_>6|*OR7xvDQrPe{7RNK1sIZ}^4S!gtXvvpEaT=JMw#lab8 zd&q|gaVz06aBy%>9dk5y!$N{;#luFDXoWJ+tb3RwzIOa+l6XzB>Uv@VNr<)UO7MdS zam&NVNDX^PQZ`$O3^3=541~T!al*g>Z{C^!y)X z)SewfFuPo)F+)0y6RZCu#F*(BMdBs7*XcttBSffADC&`$B=Br6fp4T~zHCl9PSx$c zOkPxAY{EP7J=FKDMDg^S!=9e%Uv7E!xx<%UaQ>4&3NC&mVBK-gy}jj1<$pPj(=&JQ zVetK5hmUC~aa7RQm3dR50V3ENj*~ii!bf?!43Tau#Ifoky_`yBQ!?3TvtUNje5QRo zQY|%irM5x^k3RRL?%SpB6#lY6lnQlspa%7YwOu2;tBIPSi^CdvyHhtfq^5-1P zHUsH?7$DNWay7ME^eh1@BEA_l8;Hr$oS7ym)^P}IWd2t=SXOZ{ur>tCaVdGcbE%$Z zO(XYYKdCNHrPnwrOyihU*e1RRr2-v6&Yd{@PSXUYgYbE37u!lLzc4|guOVF;azZm< z4b!!rXy*)?yZ??lrys>tmk*Pm_|6@7-*@&+HxRG*NdxP_M;|Nn4-R~pq(Faj{16(? zd>%PADi<&a@or>{v}~v&@*}-)wgf1U#F9Q{&b2saO7cMct(IY_*{4*1L&VutR+79t zR^eAiS%Ix`4b^uR5PAFHwhJU}O8CbAOQf|z>ao>C6bYcZ1UD`5+y=;KBej&!Aamhs z1etkQHURX{Kv#<{NxX>aK-?$tsq=}ixH(dNAzc@t6LknsjDRX~-hHAhRCgMjVljsx zpmM4323da%QoD_gYC%jEq3}~l&D}BlQAo3n)Sw=__D-If{kkpEIHQ?7u=m1(u zslbhpLP~zS+U;O+OqulKK`utptAP@gC?dKo94jDsmOyx(gJyqC0+H!;k+EI{=d&@? zpZyH{%4ZN%R;*gNX62te^w{J7=hd&h{D*Qw{LsMa?O*l>K^VTNGdG5*$Nv<0c2dm( zF?^0%HYH}3%r%-S!zECaCddkuA(=Q$+SAm*kq%EqRvYV2z&U-6W<8U=Bkoa$_C$*r zDl$k#IjiY25n#7o#qD=6{Ka24F+Q`L$_-?BYWbH|t1=5S(q0VXLR7p(g;7`+Us3Xs z**3EzRDoVycf}2rw_K?3B#C77AeQ>NBJYuLPjRnaNhDR4)NUS}kR!N#4i}s`iI4wA zm3UVj{)G$^*BzpXIE9XUMkF1hx&frcs6V_!X4PUeK)9ETpn3^W;>fHA2|k_ACRL=P z+E^IRqxl?tx_~J{uh%t-dXYsXvoi`fj@c*v6xAz!8~H--FHlj>(^LQKhr!1W1*|8x zZM!uWh949YX5#U`N3pX@`buQ@KxTTna@Q8oprB6X?=vBBs)D>Xv4k5b3D?mr;lyk7 zpTx3{&VsEMLF!lzKlIXduFWEqva6SNk%iOZMcjFti_ibH@SG3PHKD#Q*3wZ9u}rBk ztDsZ3sFgBO;W#>w4V_KTOokV;89H|PyfiFaIeA1|5>JGhJmmu5OiSEY4ls0ia?%N( zlB19-42x!5#8X>=|Ni?k`0Y;*VD;)620lm=ra3A^z3x+%a!*I6*|?<2CtNgcCrS6u z!(@P0jd^6j&x})5^RRF-M6!b@$VCy_rH)#4l&G~#a@~ON=GV4j^2xu)z%{?>hWYTf zw>-D?2bZ3|@$r|7k(UjuJMXzWUn-Zs;J9vOX3u7XQ@2wo9p(9!u~bKLB!`pL*DeyM z0_2XDBoMe+6|8+TYNc?jW||CC-SGAg>d2?)DB*a;=0QRilyRT-EsH5SKM^N%k#{?Y+wy7U-@a`RqNW5 z2hn`~%gA|6S&TGi)$~%!mBjkJogu=*B}qgR<9Fgl^_Y~G)Y3`37IG^)bY5g4_;_a9 zGCa100MsFii`0zBC}7?Vm9ljBICI3a#<5`?$7{~2VEKvwJ-wt_NlLU^5vC^EICiXsT|4G*n1CI{%SfAq zR92+iBYH)nJ(y0$<953dElCvc(^-o>iH`&`=xweKGQ4hoq1p7dj}F zbLd}I!1Cc3SG+DpE-!p`P2!Uk@RP5VXG2Url=DEK?81JVx zxXJ-%;&s9%6glbp$_RFslnu=bPSPYYR+^=>lsYt#YlZ)g2OPMCZsn((!#~2O+GF>Zy743 zIcnM%mDSc+nt)16B~tJBG}%iSA1Pqxwl?nkehrU4k&p#iRfHJOYSghU70*eyiUl$g zuF{Jd9mOC2L5OQ!KSk#|D)5|*_t;2S8VfS+ z8u8>HCP}-yNPSIq@7ftjIZPGwrUEFkJ(zv=tLVMr!>IODfB(tnwmo>s`RBdl4Ahql ztowiPNI!w%0^?n@$8%z3_I6*k2&UI5*^5d)93lm5Jet zG-9UxDvq{ngjx}cN)f0mW9gqs_2 zY`ky|?|N4W7j0}K=Z_Kyns7}7KwGG4z{EM3f2}O&b-h&ANDG!}k)~WMjwb*50s%g^gnGk((-8Y?TvbmlD<7ea z4ON6Ndc@+BWFL)DDMEc{RvJn&nkc!eD}{kK^$2G6oQ0lqZ^;)*#Vyav2%H%W5J%ZgT!%cVL)i|JSTi}5&1vl}-9%#zni!y& zXl7Rqq#Y%f&P&p*$3ot#$hPwD=zXf}y(y@So7HU1VWpV%?u%4lLC2i%INo#-0wXCByf zHN`1O0y_G+t}}_bgWpE9{&ffoJs0%!_P&R1|I3$xmzM;rdmnswFki_3im3S1Ghadw z9aOWzQT#yqp@f=>^YHeRP&`tStB#=;m6`$P$jmCED45>dhe)t=s1jK>`&l_mTPq^W zlhk7gx~z+AkID8!{CFzg)P-q zkz7eth@1`rz7Hc${XLdn`7z>jxj(x5{`H$=;YbwO5E76A05WXDWrv#_ zC5=njIdb+gvzQ>QvjLLrRr5(oDTnyv5!S2Xo^h8G#pJ8O{)SeATsfKbc7`I0H$0dJABr)zhE%*xTe7N@tn=K=189!F z3HgCDtNla6e@Hj}%8OyhJID&G z3m;NjcX#Squu_?6>(Q(%QBzrp=|qPbl&VTqs4?l)@TqyL3$wq|Zw=q#8PTA2c99Pj z?ycMm-g0w?Koydts?+0IGGEN7Em^+Z4l?CO=IPvKUzou(TSPLgji^7I3B3Df2XXQQ z$2Yz;Ma5O=Sst_qPh)%BvDu=`tg>Ml>aBCKmGfCs4z6XDrV}dMqxMmis**dFS)}UF zq;V_Pg6d~T3(_US-q{l%s1i4<%*Z+j;7M&j+{@+Tn0fjy5xn6KMT*V)AARzvzr5m- zi?{zsz$#bDe?}b^*7n~Ir}m6WebF;c>UZI{At@6O>O-=SB;n--@y6VR&1mdC3^z#A zW0>i93ygFY8q&|Re98)?ZUvLdL-mwIJ7dYB!0D%t;R8Rnnn>_T8tk@~5@x3tHGnA@ zPBe_%j1)AMsc9S1iK8o&$c;rt@sSU2z|QR>*tMgejSgir7)i%$Ef*d}otvhaG-Xp) zmU*5Mw3%`=sW;E*GHiI3=c|4RMnc3dkPf(@Nh+|dwp8@bY#V`e*e zCXdRrDdP%YIs^fo{VA7UY7s4zOk!px%*GAoscaVZ85sgZSj9CJ?pyRjxmj zN>ZB)rpk1&!-9(&z6kBsf$C?jC`Xp3r-n+VtFoZ77NhnqN;>SqJ4{k*Wf94MaKtK= zZ8Cj9QoXhRJ1C!aok-ul)1Sr}!Rpa*U7F}_r1IVzrI{5N{$>tqkf$D|irPb<0V*J`zNcIz>L zO+qrAn(0f{^+~9~HmHUtxw!2%Qn@Uz)RD881Yi6}Cu5=|d6K$NtnVhnuk?9& zg|>t(J?sLRYgO^=_B;+9ie$3iX{R%+U0Ww3rlu`P$ErMYy2%>?heTX7Yh+36|}HbV^y%g0p$0WS-0QU(^@=7+K}V@@Q?SYc6Zz zjgI7_odf@ZWZkv!gWP+DSFHRi`sTUCYP%$`$h7>R<2bqI;irh)?a{HOG)-2V@Z&sO zvI28;5%rJf4R4aIx^@R-yU-E`*&xFi1TZxxRa`B@UwHSZWqTzpb%FDd!358W`8k|( zuE@}wAeGcsq}NCSW8NlO1#*?{vgN5>eEBOaYoo%l zRjqET5@Sk2Yr)`)^_P2nge!9-wHnfJN@QYzPK9di#@vxDsBCzB*$@1m#>)ZLgO5FS zo*x98#6#=5zY9O9>G84gbGiSfJ61scL|M-kp00T?GXT)sK2l2{w3D`Foz zb`9aTJ~fBA+Dh3~VnL_?-1HRio$oaPYD0=wGC(u=v-nMbHE>+ECmFFeHn z%s;d$ARe@CZ4p7PAwN4jUC^0UM#nn^Uv;YZv;SuT&Dv?yNuHEQasmmbuk@#dc;LZ2 z-n`w%#TS!NAX8iv7c;a+fy-6 zow^pH@Ju;|#B-Qfnlzq7y~Sp~dx^;R7ZF#c)pcB1hjNRk_8v4xb|62zaZR~geLEHP z+4;S`D6oPsc#}YXr|(+wi`f17BAE7 zu~7ywgj{Wv9h2EKJJV5Nn8e6+o@p!BU7;-Bd_gRXZ>mSyBu9>{Bxm{_%cT|=I!J@! zwolj+lhy>!Sr=7MxkE-`rdx69K%}gj@W?wS0(h(QXjj`Zmrbfe@jdlVbm9R-wYR}9 zE#LU~Q_sBa@{2G0LH5ZDfYsZ(?8CyvP&@G~(AqDrPzH~xpvVBt)j|X#1$eQ^PDr|v z=D4<6U_v_CQO05}!K{VHtb^5@6RO9}T)m{^o8PTAMMr)1T+Pk0G|j{m6GP-x?ed|6 zVy=x^GgjOQDj@DK9T0ZZ?C$9mrhi9gj%J9owK%jVT^KvQgT=U@0XWm?KrGuO*=x0h zqXO@-?m3pBoY{Wt?%9QhX=}G3#8rS)g2)#;+t4^S$}@l_?{Zj0WFbC@_Q|K=mshYt zx%44Ay)>}yedwVf*YnO7y0&rbAq3tW()Ylq*9^%muaD%An+jfI3exBe7>h&gx@%GCwOiH1pbaOd! zL;eB{yUHCJ9?x1;O=R<*Zxlj}Hr&53<*YTKz3L@t%5 zxsO^Ez&d$&qy{~??MbY8Rr(e!E5f_u97<5|RQQ_;8d0r0`LR@4YBO6$&k_Z|6euap zu-&7-H|Vr9#CC#GZz9LrWRfcGN-zq88eu%48_gd~AO?uO|jod6Q6|#go zYOx15F-T_JCkAxVicSmVl3}B?K~rQZ~yMO=qc}& z>e?X*_S(w=oO#-X_{9%CgXzg$SXtWdlH|_318~xZUEy?C&y`;lTII#zpAn|qLS^1Vs6p~wdYX0li4xlr096`^j;?U6W zg>+Ib&9}g+R(swceoybek7m$}cJV7(tnz@!5}3}x6}$H)i5dH@igeXG6;!&hNoK~( z8-UEXr2bWI1eFcIYMEtK0YjEQq;A|Up=O=|v*u3xWk8iC7}~(G1DzLT&MP9G@U{es ztNmX}da4v5%b6KVqYt8B*<`cugQ4B(z^p}-b<^1&ay7M3W3kw~v}2vj@;XvG?RGZK zYdc6=humUY0fCtnvbOJ1-q%g)y9OZp1P<%+kvU#%2f34ZwJnl)`QU z^Y8q_s801cP3OqoDeWz7b!DZMi8E#m15=MQ>r@9(^tO}Dh9|5U$SgN09duTxb)D#u zV(EVBQli=VXr*TT1KJwQ4kGuVGwx;CQ7e(l+7v(46_@c>9}hh^hMR9)i@bM~Bw{1}54Q$_;SZlX>Gn&9?4x3kx`JggD)!)U+2Z)mdq76PG zl3I?QTyi@F5s4UolDg*Y@B;tJyY7APuitpxH6018N~N-fE1TRRU+G!_9NefG9_!863q6waOv@ z*BdMF!mb+5y&zI85}R=7ym`3@)s#;UczzehSN{Gu-t&Qel=C&&5B0_%KL5Gr(WqCY zpH04)O1D?6Hdih=X3b?2qRM*VQ{eEly1K4BrLUGFn{U-gv!steHS5D~_-Gci1+2q& zVjl>nc~I^xPjc!Feam_W=)*AytYWEneh>t`?db#X+DBEvT>DDIV37Fp>K@z)Uz@z~ zJOF7EdPz0}>uQjn**qty<_P9x6e$2xGue3eGcH&ymPJBZN3U1$)V=zhGYQM=y}*>Z z6f<2c%Q|+rgx~zF8b1B$+ttrzX&YquiF6_d zGI0Ihet!{UW-9SdbaLW0Q0h5>%Py;8?b@>3-}B)Ij^f185_r2J9l4IK zpHw5_FVPN7c3scI{=)O-6kB)p;=OMp%aPTo4!(NruV|i~L3*M|^(fXi7BM+h#19_S zyUI8fh!x%@h|id@w!2W2pRgv;tI`tnf;EZ1?)7Qb8LJK^hvlE%v{_QUk4;m(8yKlXH9mPyk{ zdFovPSj6` zyhx3Gq9+!Q&^rDD1k0~P9LIvdf64pj+ioX|)mripM$;4?jDq!pApR9)amPqpeFO{&FzUW`?%XYtFwv>wIq1!`PN zY&IaJTe&iW5BRcvpBx5fWYfl&*AF5JyHSC zv)oMV(&qcm8)6mx(NZzdF%2uG)IRX(E1GO!mP#y5M!KiH6?Pf_;X=?e7<)wW!dZH1 zjX)o2lSE8>S7{SAy^a2kK$L2uxYG-Axk1NaeTd0AsvS^frjh)55ds%4zHS5=oo8Zq zJgw=xBdhMR@jrE6>gzJ+yVeG3k7AN!XOOxkK^oOg2Q4Sn)=AnU3B@;2y;r zZ(fTpeqMOQLn|%ij=|azOPDf1j3#iU->8V0**yOIa}9j$YvXvs^&YOdb~VmAyMSD7 zhOB-vwFTi$0+MJY)apg-K9FGZ<}o~U{}jef^iV1N@I9e`RZv=NK<9a$Opp`2EU^I5 zYII&7DL00GSre3eHenp2-W0sF)Jo0ah5c`YwK0#3L1O)gi|{H7E~ei~rp8M8GR1qi zBbtnLs@pOJfmG`dJ_tMFWfAa-%O8C7iL%$<-#<*>^t7fAQ}b)7&}2#l$k&lHh!#gC zoz|=fNw)-As&(1bI9+H1VAkxV;bWG$#FfvM0!uoUlowb4z1^FFu7S_P6qz3`Y!1=M|Gg2Up^#EOQ5>&(uCrOsM(gG)^?(=TR)qi$P@ z6xpEI8abKFU8M6;6=xJ(l1?OppzN4sK4ybLDs05GQ9{Y3?S{ssFy8BjnRi50tnbS5TR0?4IQ^1VADPMj1fEHGoBeOba=>|2#0OLml0)=^H| z7&Rz@{8vbjS1?AxeC#B*QpV8|AW-YKG%(E=nYO6!%xW6J$6Qxi)Y{@v?ul$aaohi< zhh+0b%CU~cs@?t6>LJ^{u7NNTDoGuuhUiIg?R^2A zcCNM?NQCt#{`Nba&I+&HYF#PsZjH+Q7g7n$vFZZz4QQAnZ z#$2cJD!I%|Th%a-gel*#tUcU>JRE6Z#DJ?Vxo}6aglq>OD^lxbcNK^Cq8pXU@EndgRUI%36j= zH;glC&UW&=0!^Kf&2ZB$iG{R9w}qK@(Oq@%dOzZNPON)XteTG1D+0y4`qV=%@p6U2 zIkJk=yX7n1c*e^jyOj!YX7v747Rf1T*Yg8uW+u8^3QHYb!D-nC24pYS#F~J zC%lC3eETS_zy1uYUNcIDg~$_<3(?4e?Zs{1kMQhM!cE|&^)*3k^GL2UMbic>R>KrD zTzOw)zB^B4n_T)YZ_ljW?rx+ECPOce_k7_CYipczrve!Xq3ojWG?#Q^kaP;D0p~KN zXzFX$t8^3dd3|mrnvtZ&QQA#gx&a256lLDNXC2`KT0CaDm)MWTAGtPLn<;A1Bn&s1vBz-*RYWbW^}hgtQc{s3D* zq`ym)|A;HU)a*<@KJjaN@zGyji_0!u4&P~^5f|{SZ%^UtUmBx!EK@4dzIIN7OTT?p zfY-@bsNBxhbbwXorWUW+auN1hQD$uPccvdwKB;X5J%19JKxo_@spBQRPQRW>6`chT zwAuu>94K7@&0%se6B|>@jK`uQkRFNUHr*1-8Wczq4rO>t2%5;XgKhS;5cP6BU%qCB zL`;tt$MK+aBzEc=<;E(}g?5POH$0f`$Ra440lFZ-Z&;)=Puv^bZwEp*%A}pxa?*S) zS#`TZ3;h-O7&$qB-}#LZ^epS3P;oIm!BHct&P7!LNXOEcmcr^S7CPQ$^1GPHy37#U zwDOsbRzGSjZYJH(hC$MsyS><{8dq)({cM){mZTuG9miCnq0Km*S0$~^#gD^GIff)C z7euN(uSod?n=Mm7G$QLTE$9t!2D$zw!h|eJL~@b6sWT5 z1UejM2HdC)tsd>ZVM3CTg&SiAuHQgTyh?}K)vSq)?Mh??i`7MPn-nR16USto7-^9b z6SXPa(ye&8hgHCu7pko)(&uKUa6GdgNpl=der3PsI8K;|$h08AW-yE?3`Y&T-i9uX zF`-M`%im@a&gZWz^V<;1d3w_dGR~}Iv*g|uXgX;r5VJ0clBS6mAnAY2--c5l75T3xd386y;QAGG65N)E zC3@?pRN}x?(@3Iuq{K=+Zj_IR;nG;&*@d46GSCN#|-XZhJ7C=G~LvMGwOz1wR%OqGJ*!wTcI_*cR1sC9t@jStQYWsK1R)i*3)mK4?Ci@t z3}{R~chWsTstr7Y0G@FS8fV8M#b#UzMp7CMvarTW&+er`)g32Yv_!L^m`wx8P69RZ z8_({+@POdBO+8uE3oB)Pof%y9`qlWSuS{WfR!b?fAC{ej*~;p6%paD_-!BDB-9D|% z_zUABa$&L_?2L4HRtY&m6*9w5Y_6rDDq;Ezg)&B<2k@W4hjEk*B)CaD!}J=Qm|3P}$_a`OmVC)Jcai^s9Ka zGuJ%CO3MU=fWC{Hh+SdrspWQwmI$J*xdM|fOE*ANd1@x>BHgyr7z&~frRyj=PK&hd zRLA_x3A-t;2+Hb&MPyeE$v#Op-ta7M655{W_rqk=jUw(!MVhQb01E)N*pYR%nRmP_ zNb@jGS#Fk+u}r@!StReaUQ>0BSs#-D#5`4K&d7h1QIZ%)=~$Vd8^)@-97&SVvh0G2c{j3>l^N+BK@Ka|Np zFzL5?Kwm^!nTIdfa3Q@?{c8tV1DbK`T$If+4Y1NryHGP!qq|>BtZlHvv76LSQ z3Rv>#+IwOKn46{K3DP96jI=QWyre2LkTBfHS%AVU$a-w>Oqh9>|7bkJ( zKoK{;WdK8c0i9zwa6G{`zjYj2w+8gg9JMA=QAUD&lkBR+k2$ceoV|`nCVjwg5 zc_tY?y~X)uR2A< zbn%j;(SQ%BXm+?={ha0>rFS^C(dw`H<#)#`#iv+XFJh*~-HzQwRmDp=<_*Fk-^s@% zYETlT0%X59dU?Nu97&(KxjuaR+fDrQ_Ya_0jp{_g9erFbjs6Oo85yNDPi2cwQH!d4ftZ~)KBNCqX7IOoSJ2{9qE2t$- zOgDn5w_%yrqS@+ptBs{VWgD;rpt=dRM!ut@iHxmjZ>Ui__NiH?sF^Wc{EubI*XNm; zRcfL*)<)K$5573fTC{DaIJzdBnJZwKCGbXqCHA=vS2<@6D*~MoYNIM9$n;i2R`tui z)EmHMk^s0N?(#4|NPTSfyQTbp_LoVwVHx*}V^fPn9fZ#+@nYB&LiT35(zog0bs@24 zys%tk`CcTm6G*#Py=YRTp32;tloF<@!_3Ng)}P7K>8?Mw;OJ5%83L_bPCRwQ8L|z% z^_u8pN6jo72TM$cWnt0nPOz#rBlcW3CAsEqCEGJ8(?8;&-p8m3w)p5;-??mqll3aw zh(N4YqD4@aI5I%u@|?1mwN;pj#inU9>10^cuQ-2w0?QI$Cq0k!EoEw$l$7MYcJ5jd ztQVA&*AXogyB6Z4=P+lIB-V1~6*3aZLdXJ(+rdWW!6r9ozRzO`3DqSMjHEFsEqa(8 zU8o>bTBy?Bkn9CZRaWQasw@HpOo!vX0OpIlxKiGe+*6` zt>#a#vlY@vXWGs5_iAD|TJMRsRdAilpHd8MmWZXRZNk>yvS1BA44 zRRe@o>zLS=1;+f&UP9t>L(a46G;3u5W~)d;Mq|4LrERSQxEs#l9q%mTUGG?j(63{1 z+QXN>bQGKKYUrh3rvG#&ngOt?FEf)fxJYA-FZIaI?o1!pCnvTa<(x4)=D^sr$n-(_ zA{Ay^ovNws@JuSfj0P}SvWvNX?O}^F)^SF~E_!JHm|h5>`i=Ns5LsZVbrC8u;`$Y8 zq%TU`=QZl}$x^vQl`C6yVF;p73kiWL@jEaUnDJkAS0&R4rlE^ewp#`286?6*j+&9m z5vw-oSNYoM422QBZiEf5Y2&>=TS3m-Bj1CQA9V4tkFCOy<0IJllt|k3)5(i3+1Wc*%LPp!k{l5UR* zDn@qwD$mqy(ayA0Sy<7b=ZtH66OP6X2SY}HjW*aRvx|Z%n zM!6Z^(!z;p?l;z;QcJ0%XG^LJ>(A`c*qJ}Nnv>YCyVitq{hY9kIB08$>sW0mbNYKP z6u}&!X?A&qd|qvqMWjo5jF_p3sAMGH^&FRDnX2I=-M$dh0rT6E74+F_yBcKsnA>xD zEs17cZg=PEEXE{mW5uv9LBSaP}MQf18rOwu))fU1&rzC zT7}s|J(8t-B@?zVd`N4_titblZua@=U*)byjONH1FJ1g~kv<1t7KC(Ri5qRrwIt2f z$iQAbH>N)3URDCD!xNUaP=)dt$rE0jBqx$2iJampPrb6WlhQ!p-4z_G8p+KcSfX}H zNgr-HfrFodn{e&yOqYCFPH0n-wV+j|t#rn@a<@CnjdcGoC%_YrPvOkd^Hka_XrOIUT_H@=|}p z%s#AU&MS8Xv>U*1;unK|nl_}0Hd0A9)cT{$j5D!EWk9KOaiLAX(rG(T^^o@oJ{O`KEJi(77G)v2wo_GQUkpVeD1gw{$*Txl=8gw8mG zq}QOZQv7walkf@e=1td+?KymS!u5*-ys!#ry^v`;lUoMG0o@)_;Z5^qP8FU>4ufVg(ecbG~WviFEJWQ=mo=%YD^nyW%xW4< z`q0xqi=TPtF!K2tTqjYly3=bvnhoIF-)`X0LGivKU#%39i#WVAwMIX}EHE~Elgf4* z2etK->nH#+0cMN-Y~rG!WlBg08Wel<+;T^AS1%nOqD)Tt3_4X5Wgb6k_3}BHjFH6g zKG${6WYyEbPVR+$*&mm9~;Z_J^mXPm&Kcl2X=clo4@ojZISIV4Q{0LHJC z7-3~!1fY(jOC=f(w=m>p3>lLNnhoZgu2HU%#OAxlE`h4lfsHjnzt2$gTlqDB%8c0} z%8;`gHm6q)6+Gn50*C8bG?uab#*v|GStvxXO=SX~X_3%@gY>#75o zmH*aC)PdU6-vVvX>9xmB2qxZ_DE%lcIk562&;*c`!6A?!9gcE~qejX_1r?DmDQKfm4lS2anZAh>g{rr7A1Q~^ zDJR8zM|Rd>ojOlb-()s4CRJsD_-sK*T&CSbB4@`D(sTF*eUFoF6&ZCp_mLd_HLtoB ztwcYNsYi(ANK*6l2!B?r-!y;fH`*8=7!iHS>#X-0+RdT=wBa8E22B#7J5hd2G)LjG zDX|8klOdqcWr#hpgzqX*#Koq6F=Pi`la!YreU~EfU9nMSG3eQlk!B-8DPD~Wus zjw@1t95YST`?^`U%(Q<8pn)|vH@72~&y9MO)dQ_?kdQW_1%^g3wKuAE5Cu&Ht)eEt zTK&z}DM?1+EH)fGquVpYtjpPFRIzDXqTVQK9k)`oDN7~Mv!6+ar`cB041w`PGh2>% zTCBN`U9^!sEO1|p#4qCY%DQ2+=h678#I87soNFM<*k>$(t`xR<-JE%AhAJaKM;bLM z|4O}nR9Vc1D{`|Ft)(RMC2IFrqIdzb112YT-@6Iv6S#}Z+0*A%g4S{({^3CRvUHq2_5RNAo2&r*$8T;gV_ zh|(51Z0<&gdOEU6CfC78d{*8-_!2Md!aZ>mkBJWwSZ}%M=FX12`yTUs|9aMQCfMxL zO2k5EEK-NWUWB+OhBumtTFtMwQQDbv*-4-7Lz;BmqkXsR+7zwmjT8>$&YItVA zxLHZ9tse6{Zn7$jdqLh3$=&nRMcI89ba(0dl)J9`#Y#&Z*<5_R76-}*Nx{q$v(=IM zUa1%dolYzlT^PcjrckRPnwyDXmbuF}%R3^w*ZATRb-m?BjuFtv%$~(7YX>Opi>_sT zS`CW-qGAhCxuYbfl`Txx%k8^JOH@31+UgbEFA$iXp1MDmAO2*x;u=H;zX7p%SfWi< zvC~W<(l~m>?s*03gb*yV!L7{|TL7s^i*xa)DzI@$Z^Q`&q7N%51F8p8ys8rYGBv`^0 zpxr54``=`0&)^mEUe|FSH>Fu%jg5}n-M4I6E$BPF*etGqTR&h;(Zq4#5{GH`G~pN9 za619411QUrj2XIE09ul7Piu-aY>>7ubIz!Tc{9uI*}h2J(P~jO918|o6y-W@+P_Jn zh5RY{T{cE0*k&Zp!eha+GGQuMNocH2XrZ1ilb@L?W7qS*ij~5`ZE82JsGp0G(Gm_F zJ|-QKv5uR{)e~B{i{C1RJZ-;e$O3QtqH)9}gaJ3nkW`Z}MrV0Lf=%*9M`yu`YbvM1 zDXkWLFcC+|HgmfL)|+p-Ioi2@-}k`$pLP4r0~-6G;%$c8cxaa*ghON%%xHBrU-j{Y zpt7_wea(cP6=v5Iz%I7b+(h|t%z^@QQMNSG_RQyb{ldDw(Wx>SZk4X1WLT_cZN#6c zNnt8;%Cwa1{VbbC2zxeX3xBZ}&2~?Q zi<(Jf%I12X5`Bf#Q)ziQC z6D;%QvXnxkX+}K5_D;o5y4Ei&&^}D=UKOn-eii7~wLnWl;j;1aU;~%x9$~j-%k2Wz z+|=Ad#X@n^>)$xoDJ+LOyHAQ^%?9Lin>{szJw@PTfK-#G`Z+xu3!3>WYP$~oy_G1| zYnQ0Eb#a}i#Zr)FJLScWGglM~42BDC6M_lcxMl4fQNCD*G)vlBsy4pT8>J7+3v zL1u)$0!G(}NDS(J$mACY$rR<<;DasI4lyBJRd=DN{qlM^)zihL2WX=1%JzE~u&%uP z;)&e{4?XSqg`3cG5t7*#Ugku`ZVK*1!nQPOXKeGd`d7+-AE>b$EHa!sa6X`%E@i#L_x(((THqZY5 z>+U?+H=|EJu3r}s?BWD?RzLJ}f@Kmwsj6%Y{l7Ij7afo;{buKxU1wxR4Uy7q5( z-E~D!*QF~68bL~=h9V&$z0D+Fo;Ow=Lu+!m6 zYo3m%*%!ow=o+Rx?U9Ns!UQ$;gU*kOb=XN`K}=<+>%~((4!v<4PNH?m-;`=Jbd#$e z))XXZuq;zUZU#UdS*z;yA`Ur4LxlGec|CaWOGpY$Mf}BUj3Dk1b=i8v2zBf;l(Y_? zDmnl>PLqVgAqa~p)LRZa?ESZl2y&6?7hu)Her3!PU}tPty;X?HK0Q0>LhFM zcy=MiFus21XB-Bsr3>e;+p>GlX0xGxj@_~t+QA3q>BB-L1@tL)wu(f1ok+YPI_L(P zJcL{AWLcbg7yu&maipS8K0&^WM+R)Uq+Hfy{!yf9e#^{FwRAKoauSAY7)BOhDy>>d ztL}Kde4n&zRcHi}qhLzOlNN=>qeWHnNQn3a;zSx# z)U5=nCJP%P{vJtMp(eulM(kfv<&Ly2`8y0r;1x{}pUbqa*J8u&C`;gLP&qwpu8i(e zpryOmFV@fO@A=K4OK=#l*zi2()LZUR+@o9+;m304sm}`l5*dSs zBv@)orhzy@?jU?O$%{5TrIx~~q3G&LGO6su(8uIQ+)9B?cpAj-JdJjHBFpE7@l?|! ziA?D7JjoNJI#fP1UpZ^lb)mkijiO-g%aj~JBOnhXjgqZnVYPUODV!4-wFtv!>LFEy zGfR;P;PhAFBx_s^YBJWU1cKgm3QI#~AMhVXT9G3Ht5hg{qouw5oyndR-S);F=+<6Q zg~1U-H)tBLn(9b&6R?UYu@t@@Gs_j{(x>bmm7M@Yxm=2<2U4B-mME8JJS&kD6B0?o z(DsSmtEtIU`C5o@%RY-{-cNey=^nxCIc>Zk76}U%U)G3IPHjTH9>6AKvoo045i3(N zeFFuSKnY;}2bE(Lguj%3XLIAJ>`He3Sh`en_ftfx=+erF`E~a z7cT)E8zC69=sVjyh9Nf{^X7YMQ!9yi7=sE)do-r7T17v_Is~-7oki3;%RCM#-sZ`C zGWy(HFSHhRwz#&HUU2J?FmyyU>$f=Us2#Z~0%J@I5-jqxkNCNf!7@yt$}5B`|ypCi?XZ#ZaaJ%EpXpauPXY?3T- z57G|c-CH945Us^wJ6pbDYKT*+tiuxnLn1DZ$TOw*V^u|MZNvD$2m4X2P2$PN7>msC z*G}Pt;oZc36S`r9%E}}(J~tebP9&q^^#^gC5Z<$z{xAhxDR&X-VktRpO9l0|BCWB` zpV5#7%j|i~rU4k8tJpio>FW->%y?N~jSLSz);~D?XNgqP#UOne?6?K0MUKEWbM@PB z21q6DOCuIh#Qg|p&qo+5g$ zZregn=P*8a%^*IqWgjL+Q>-_ORKL;lOr1Glb@JSQZr`}bq(#?7KIQdrb_GWat?5Gy zf*6>TWK@n&6@2%SbvQFiaA|Ed&ijM}=T{_RXY^e}R3guQ{!h&hw%}#P%L424HLIN` zo_XePT01%}%Fe$^tL$Hgpgt@n(2qL2*)2tA0|{g{8@$7EBv2w#R!QAhOEB?hfm#?j z{RAw#0Azep6J{Nq9c=R0tYr}G+W3fy7gQkwx0@JQPdHe!%Evk9HWSe;2+1PFy*RB@ znharRL{5KPWyk)n#eNDa#-k;R`H?h4eeyjSi;G!+O!etC%$&I!*Id(&FaCWIj?)6BQ9=b2S;v9wLB`XLD_DK9g&+UO#l{zf;o%*UfF~U4L1?wH%W!A4?x3mF zw7wL@ik8y5O0*Lz^xj+v)2HWMLHN)R3;sbyme#*s2`zeTw@e{H0W5O3B2OqXa{g;i7GlW|gvi_(sseejWjz z6Ly~ti>S;hq1sdl?_Ge<;|)Nx%ybw%tJ$9>$@2XSe#`i+z*;hY!G@Q1?YzrKzXwz5;#oz;BdbcP_Y$}!6EI&g#d&r|jN@^A7^y&IlyKoEc5|}c0C#i}=rqvS zWMOD1z~)UJTy?ssG}GdeJ>1!B#z-o)w@u(J7xdAsvD2t*eoztzm97YEaEJzsc6Au0 z3F4NYy3fO+m9h7PA{bLrfFKsm_xJvZ7n8_Sy`2v zbm2^|k~&Nvbuh=ZFwZFpVpftQ=uD#NF>x&VaeCY-nsBplc1^;VM*vNfvEsNEY~4)P z4n)VnLoGoqPI~Fuo>s(VZ*4+5^BC)OdcDUvRp<(dgGx@Y>P&|q#-wyDv_(SSTj*Yh zUm&KIV~jqkdQq)5l?0JeiK~86G3Eih^@>4k+*CoeoR)Y{)J5?|Sz5+YCA=a|TtPOt zQHN2>piUAi6QmFYlVnf(K;Da{42r4usjH{}ebpMOum&edYFXfin`;{4Jz%w#CD5Ds zvEc_uTK(NC!OSZH*0N)l6kgo6?S^DJeO-3h$5C7NURui{_r|j`4n;w;j(Sra=6vEu zPY^e%7HDr47>`X!q5g($TP*#9+q8nv6V&*^%mtE3L3$~jBPS;e~6f*>r1J@5Fmy$Gry0N%44LMgJ@eudXA?5*?3NrP7 z(JNF`t+KPBRUa(A#dy*qF($*ti6>RCY^jG|J>WuRkH(V~IlMHXlA4lZSh$FtiDDxd zX+|)dk~7GX|5T8B>h9xF3r!ZOGR@!%Krt<0ll5oMoZ{Md~CzUnK%%qs#G8+o;FVKXLbemaP4vG5dHO!b*!Gif7e*KgqEIt+L28byDNuwA)o+XQ)Lv&xbP zq*cj|w-Z(>Yx!ssPU<}+i0FsG=StU9dU6VhW8Ojhs)K#{&*&0Aex-PM#lTv&cyVdt z)@>g(4D+YyV=hhC#~+6`zFE$_AbD&ayo8JTj3Uf}f#lw1N#cbiB$Je4izobH2p2&t zl{t|o?ofu-SB8-u!=+bD!}<*c(rC?LV#<^u0nWP5JhtA&Uww80S6$tS<%^gFRRnj8 zN=YHayeXwsJd6?nDU=r46v$2aJau5@RQaIZ69uw0b$^;BH<#?CuchF)?RfOzEdK2~ z<9KN!sX>-|#V-@TqTH1Hf|EGs!Ys_h1av!#%F_l+CoAI-QKyP{^dOXax{jzZ8+(Px zvqBwOD?33v6h@f2%Qq>bCG9I<^qtM#UZ!__WJd3_+AGD&D+U%D^JWhHa?7q=*XfD; z-zFD)2-ahNg}|PK2pS{I<*~CCta+rbojOt@Sz*2DvZc0k8z^53_wlHm#fvt47-C@>ml8 z_OBz@zKN-&E(i{WLjV!TOR-GO0KGj0tb9|0n8zKo`JQh=f)Qyx-g?qzI`n?s)V)o+())vsIhRJn0_Ng?z+kFornnxMMkg;RWL%3mY`|Az+7A*D_rpFx@RVs0WpvTJ$V_ru#}FvEks z+@Kpj`AGsl_`x`KY$Fxsb<_POL|Xu*r^BaG{}QedB#Lm zdn5>UxLQL47%XZP#4^Uf-%Ja(pXpNy>vRjf-Sa}ShAFW_oRm;K2CQdI-} z6FBc|k{+xTW3dzdqs=hL(D77}Ys^p;hN%LS&zD(~{L5iv{n#Q(T@&2RK!k-Kb;Aq; zU6y+!y4S$yV(~z2H@)lqg9E+ptHsHy1{NE$2L~T{Y3I%_8;Qi{lZ!rSSdV`aL3tm4 zFs|g7=bH;25B1(6S^adwFSJ0bnGlC#OMS11u`-F}0tqQXlSQK5B_QcbZDjHVyzSB! z+_1fbYPB`YX$q9|lT)b$itGlGztvFT?n>k4?^yWxPpepYs)oN|Am@;FufEQyJ6S(4%EEy(6o*Q3TN8D<-O-4~eI~6LcRGla*CPa2=WPzX* z@!wfxRC~wZ^B^{bF6)4uy@+V*MgDP1S~cj+qzyijQM)D z>ut%!pMkahqx=LbamW)}R$)5383NZa6U4=6Z@koigk^@(I&R*xrxI=p86c)zl&LnO z^y9r~o|%E)R>JBvz&5gkzIQWmNR4%1QSCE)!9!w!U#NMNRucctfKLmve@_y(-sIxm zpX|d~XB)WeE&Z52{UG6~ETn}l84+6)$^QiLhYYb8LKSQB)0m%(`zKk!X#y2{8XkSr zz}LUNAG>xo(W@JIsg%yTB{J~FEm~#9Xwt^ie0A5NaDj}WH79vEV-;C}zJt_Q53HXz zAZ@1zJdSYr#H6VPg;!w(`YIJB=@C&cpf-CPZo=o~RMbg0*p$Jw)z=}f2gtpQ&p$o=& zS3qrJE0J@PFO<$|i!?vRjFm(WLV%{8Z$tU^5oG_2UYi)hJKo)k=bsdc($a7U22>^aGw&X7n288lc?|jz~7R{M}5tJcXWX;!5^ zAy0ZRaG@ zse4lWZ|=5gWFc<-KJNt|xYFuli|GWgleD{zndHPiwK0!GEhQ51RGKVDl=3dTbBU~XpTg#Bi|+;J z%o^yg|1Q}0oq)y0yqQDKY}&T{nnWsjOLEqwDbJb&cHJpGy=)0Z-Vo!dEbFvBG$}F* zUL=K5&p;W-f;C6h7td0a7FV*`g2MNQ(cIMlx37rqfpPrhr+e{#KRbqUnaI8JzK0Vm z(`iEgk4j}Zq!~#dRGR_Or5W``Y~0}Cxo0QwgP9e)`3xNwoZpLf;x>j~<_qDgG#KVK zsJ7w0`z_q|KV@v(n57NX$rVPe2!p__a z#*9m$_npZ;zv{c*JC7OY-}<}a;&%oX8*_$c-1gG0T^~2hb2D*TU?66G9>LUgws))mA0Ix(4p(2wVMdG(&&t?tZ+Em34 zZW+UAXJ&BTd3~5Yvr1~z#yCl+pWW}_e{LNmi?fw3(85c-gm#nC4XUh4gE$lc2%FbJ zSb}~$wTRO#x_6TW^+#~oMGmgGn54B^Lh?W>#{Vmc?6!W9JH=FR>|jvoH4#A8g-?-X zoYWRfqS8|!>yN4I#32WNYFv8h)6PU<_IntpXwibJ2viRr4NM#rV6n03g%`d$YxbNB zsm1HFOa6igY!%wr6P(1e3`w7ZOXDBOa;7T%W5_lVk9m$r`hh$@2Zvi`(hb3jGh(?! z2`2^Zk#^cGxq-9ETh4^NwCDubsSb4g@`alUx&Rp_5W5CT$D zVnY`eEi;-Jpvz;uxcz1a_uO8`;4BxtgDE`wWCf!mX?op&kc7IAuzWreLek5RkWNKj zZ&6UI$qm5!H|V0A-Aj(*>Ob@G_6yTQAqZ5ZJPzD!5`E|=j>XFDT`n;N+fw5yPpN%T=SZL1y#~qo*YbvGp5uJDQYigs#nPomaM{1Manyjt#u=xc z;@-dRfxqkSoz|DhwY)2T+=uki1_O!FUy1x9=}O@BgHn>z@?|*l=x=!o!L|k@3K>?f zAzTB2st6W4oT-4clSB2sHcX}qXugEGGs;-7$i?4X--mzt;z4ZM!cumU(qXGbJ6TGm z05SE0_&kO^_Jm53HT{9KT19D@E+hZ4Xft$XDqaVKvPmVK#Xd*1!d8i4>>5l>1q7wKwFE`5E;qJ=M;X8c;lQ3Vzor=Pmo{>6h2UDMgq z)sW4#Ue>t$W0+WPBUxCFfQxa_;lKzzHAD;kpjo@WoL){Y(xnfs0#=P`)yiT_6_ch+S$p-aO@3 zgaTloE1ITCYav_?p%ojKddjCHtOKiUl@%{WwSRBC+xcj>4jHwAEQ;P zf1*+<-ne|plA{hPHeMHCv2oVvr(1X5d+#R(1_wuobNzMGl53M?vh1+qCYqF@XjQI@ zKrrtW>?PSpv@W|t&ZtXm;c4os57h3>8sJsACN-<(den#XIgCgYd#GS|?%YRZ< zo^;kKvp_|;x!=enW|b^p?))M?@aJhPT2O)S(q{4+P-GgX130_oU&@(r{nEhU)ht6iNt68<$her1w z{Ew4QJmEDvK!4kK-GIf$xo4ea-}#ffzchVlW|hG8j}7xKHOt9HxLdx7L{L<1L!sO; z%5rg8l)bG2H%lOEao}&yAya4&k^Df}aKh)KxjIr?I#K+Vhy0~F(kE1j)K@ULz{1Bq z(}qIr9e0;dt+k=<3P~jZqsX@eO(2td4=G;*RBn8v`r`RoJoQ=Aq?+gC8gz3UD_1%A*ax$iM$!g8J-0`yexwKce@K$$k#@9Jnt4tek0NN|kN}=eMAvdN z1gNeuYD1H#H57OkYo4&E3uA*zTtnniX&UqUXK8~Si`3!|BH20!&-HdyE9Lhro;UAj zulJg>@wx*`jpd6LeedySpV`{b(0FU6eQuX={O3`6?wdsFA0ZxPgUb$0G|y*eC$or! z=82Q0YK=8`L)Jflo-qwKet8$3WnOZ>g~u^H8B7keunYkLM4>Fjp;BtqmzUgYa<}f0u0n)pk(c(ZFuc< z*aKx$TP9$$I!RUr7^tRqqH3HYni(0)f9Iv)wX8;V$+a*tEsUh^tW_#+Ub0}pYsyr3 zE#vnFEH;i`wD{3`?)%xC{=peHrt>}TXg%R`T50ElsBQdDnt#1C1ujnC7Ld3ee6MTcS{ta|Np#mT~V2~_s)ZGq+Zy#tGllUJU| zBInmX@ys)i<{KNom2MrJYp(nh%G-VlYxnKQSo;u!&cB#K3PdRtQ+*a|qts=?YONqu zNg;7C17n0#WHG@*9~xOsN&3$i>4tr886#T{Ab+}t+%gkRBT1@E0V__^uwZEp58UVB z`#0~!@cum9ph5IWRp~g0DMPL$wXmFumv`}2Nj^!a3CtKQ;_}P0IC+JRTz*_;!4UZ# zZ$P~+NPeJ`lHyD{ zv+PeM9SK;D*$;E(l}Pojfo7&j^*N7L%cX0NText;8?nA@ydl6+WBKC65B}nTUoLHF z@A!B_Q`28H&$}$?&NvI>Pka^W!mmksl_jl~3+RbzUsbfq0B5BLTqfTTB)sJbkM~D#^ELSGr0zNypco;yWMPTS|I*d@Inv?Do9*zvLL&VI zt#Dh{BD?tABq?*u_^Ff&#ZOI+kAM4(kW%sSh60O?(^juxB}HF&;+bdf$>ww4NN1Xs zcC7patjX=DKJyKv>zim}({5&oa;#p%H-t}1CiAhDMZ-Lgp86@jUOi?peucJnue$e8w=WtYz)rPP3Brp!ppx2>4on@rg=J- zV)a_>4y#uG^l=LpysB!N*K54tz*1xR;zf_$_seyw+B!Ne&1Q2qq}yh9Bu}{>wOzl0 zz2|PEYR}QaF%243{PHlfCUez1kuJ>0vqxOTX{eJmNt?D`BP+?I#ZDqsA~U>@MrI<7 z)Shyoz@{!5$iMviaWz`!hrQ+ic z23TyIvF21Jy}oPxf?mRJ;-|Dc*> zC`QD?9~w8_x%b-PF*ys>M*3TifrLdMb4)T8i1gcyC_hrg!7XEGo1aG0Y`RnfYkE?^ z5P|3;bFz5I2; zpoc}~9vInw!{Y%X_kq>J!-vywuDi?&V}r*{fk}Gg*|RYMCEO*Hdu% z%CNNwBIQ0;aV*i%;GOpp#P`JE+&nQz*Q0tyU53b27)Y08B1Qsk_E;nauSB-%I8Mxo zM|rh!<-bTueeQ&1$L)FJR_T8qe~`dZ z)W=?ewfC3s_CJ7xJuEAOgrhT1!DCfz0~H1I5oMCHMK!n{^?0K-pA%)>AqliEz@A|j zsdk{1Bv_`0)NjHT!SYfXjq&qfv{x7xrDAfT|?Ag z@*uHnuM~uhOSqv(f^Yy@_ga{pry$)jhf6ZD^!B>d+P6nX4t#6%s+IeHzbo23|G>i!f48Z%^{vTd@*Sys&vA){ zX?k|>bks*T!`}ZC+>wW1*7uM+asb(Rg=0t;>A=3^H-RcFU_YxgWid$| zb&w<$Ni&TAWytsQ*YfMgc1%Gm^FdSXo1!2#1E0b2%yp6a!rCzg*%>g}S0mB>CYVh_ zJWqt&72O;@M0 znR7Co3;MHN3$?Ok@d++)`e^l*?mlU zvFzi(%ZLM?MC?RxCFZD<{7$GlaYCv9RS?D?<<;nS1f)zC83!|Awl0OG(_ zAn;soyHzdSF)=>=^;1@^eCdsv-=i`9D1j9pt4?^+ll1M$`+xb1#+LSuCAzL(lul>f zmul|M&=)P(TL9OBSKbZZ8b^JYz&7y=P}xb+L*1kKE?oWPhhVfVL$d8y=$Rf~WvrWNs3>E#Tq(J(bED_^caD!9+<3;Rr#ZKh>TAA4Q0dnI>dvgA6q zK$6TQ!fj8&AAJ&rNuaO`@a-b;sO?0KsysDC)0MN50nm^=M2X5L8+hd~=`fZ*j=;!3 zYn%n7TA`)dI8V~dOn8q>)z+%jt-4cR=eq8r<71;wjvpL*{^AQScwMqM-^lSN1FZNs z|GaZqX5{1S>woUJ>)UPZ?H!qXb}*aC&UGE%-yd|Z6Nc?@L3^6OFh`VRa3f8TaT None: + """ + Invoked after parsing args. Checks InfluxDB state and initiates network scan. + + Args: + args (argparse.Namespace): All the arguments passed to the command line + + Returns: + int: Exit code from invoking rustscan + + Raises: + SystemExit: When there is an error connecting to InfluxDB + """ + if not await args.db.check_settings(): + logger.error("There was an error connecting to InfluxDB. Check your settings!") + exit(1) + return await run_rustscan(args) -def run(): +def run() -> None: + """ + This function creates and configures an ArgumentParser object for command line input, + instantiates the InfluxDB class with the provided arguments, and asynchronously runs the main function. + + Command Line Arguments: + -a, --address: IP address or CIDR range to scan. Required. + -u, --url: InfluxDB server URL. Required. + -o, --org: InfluxDB organization. Required. + -b, --bucket: InfluxDB bucket. Required. + -t, --token: InfluxDB token. Required. + + Raises: + argparse.ArgumentError: If any of the required arguments are not provided + """ parser = argparse.ArgumentParser(description="A humble network scanner") - + parser.add_argument( + "-a", "--address", help="IP address or CIDR range to scan", type=validate_cidr_or_ipv4, required=True + ) + parser.add_argument("-u", "--url", help="InfluxDB server URL", type=str, required=True) + parser.add_argument("-o", "--org", help="InfluxDB organization", required=True) + parser.add_argument("-b", "--bucket", help="InfluxDB bucket", required=True) + parser.add_argument("-t", "--token", help="InfluxDB token", required=True) args = parser.parse_args() - main(args) + args.db = InfluxDB(args.url, args.org, args.bucket, args.token) + asyncio.run(main(args)) if __name__ == "__main__": diff --git a/bronzeburner/influx.py b/bronzeburner/influx.py new file mode 100644 index 0000000..841675d --- /dev/null +++ b/bronzeburner/influx.py @@ -0,0 +1,36 @@ +from collections import namedtuple + +import aiohttp + +from .logs import logger + + +class InfluxDB: + def __init__(self, url: str, org: str, bucket: str, token: str) -> "InfluxDB": + self.url = url + self.org = org + self.bucket = bucket + self.headers = {"Authorization": f"Token {token}", "Content-Type": "text/plain"} + + async def check_settings(self) -> bool: + async with aiohttp.ClientSession() as session: + async with session.get(f"{self.url}/api/v2/orgs", headers=self.headers) as response: + return response.status == 200 + + async def insert(self, host: str, ports: namedtuple) -> None: + payload = "\n".join( + f'port_scan,host={host},port={p.port} state="{p.state}",protocol="{p.protocol}",service="{p.service}"' + for p in ports + ) + + async with aiohttp.ClientSession() as session: + async with session.post( + f"{self.url}/api/v2/write?org={self.org}&bucket={self.bucket}&precision=s", + headers=self.headers, + data=payload, + ) as response: + if response.status != 204: + logger.error(f"Failed write to InfluxDB (HTTP {response.status}): {await response.text()}") + return False + logger.info(f"Received HTTP {response.status} from InfluxDB") + return True diff --git a/bronzeburner/logs.py b/bronzeburner/logs.py new file mode 100644 index 0000000..088de0d --- /dev/null +++ b/bronzeburner/logs.py @@ -0,0 +1,34 @@ +from loguru import logger + +logger.add( + "app.log", + format="{time:YYYY-MM-DD HH:mm:ss} | {message}", + level="INFO", + rotation="1 day", + retention="30 days", +) + +logger.add( + "errors.log", + format="ℹ️ {time:YYYY-MM-DD HH:mm:ss} | {message}", + level="WARNING", + rotation="1 day", + retention="30 days", +) + +logger.add( + "error.log", + format="⛔️ {time:YYYY-MM-DD HH:mm:ss} | {message}", + level="ERROR", + rotation="1 day", + retention="30 days", +) + + +logger.add( + "critical.log", + format="🚨 {time:YYYY-MM-DD HH:mm:ss} | {message}", + level="CRITICAL", + rotation="1 day", + retention="30 days", +) diff --git a/bronzeburner/scan.py b/bronzeburner/scan.py new file mode 100644 index 0000000..e35c3cc --- /dev/null +++ b/bronzeburner/scan.py @@ -0,0 +1,76 @@ +import argparse +import asyncio +import re +from collections import namedtuple + +from .influx import InfluxDB +from .logs import logger + +PORTS_LINE_RE = re.compile(r"Host: (\d+\.\d+\.\d+\.\d+).*Ports: (.+)$") +PORT_ENTRY_RE = re.compile(r"(\d+)/([^/]*)/([^/]*)/([^/]*)/([^/]*)/([^/]*)/([^/]*)/") +PortEntry = namedtuple("PortEntry", "port state protocol owner service rpc_info version") + + +async def parse_output_line(db: InfluxDB, line: str) -> None: + """ + This function parses a line of output from RustScan and stores port information in an InfluxDB database. + + Args: + db (InfluxDB): The InfluxDB instance where the data will be stored. + line (str): A single line of output from RustScan. + + Returns: + None + """ + if match := PORTS_LINE_RE.match(line): + host_ip, port_findings = match.groups() + port_entries = PORT_ENTRY_RE.findall(port_findings) + parsed_ports = [PortEntry(*p) for p in port_entries] + + logger.info(f"Found {len(port_entries)} ports for {host_ip}: {','.join(str(p.port) for p in parsed_ports)}") + + if await db.insert(host_ip, parsed_ports): + logger.info(f"Successfully wrote {len(parsed_ports)} ports to InfluxDB") + else: + logger.error(f"Failed to write {len(parsed_ports)} ports to InfluxDB") + + +async def run_rustscan(args: argparse.Namespace) -> int: + """ + Runs rustscan with specified arguments and processes its output line by line using parse_output_line(). + + Args: + args (argparse.Namespace): Parsed command line arguments containing the address to scan + + Returns: + int: The exit code of the rustscan process + + Raises: + May raise various exceptions depending on the rustscan output and parsing process + """ + rustscan_args = [ + "rustscan", + "-t", + "500", + "-b", + "1500", + "--ulimit", + "5500", + "-a", + str(args.address), + "--", + "-oG", + "-", + ] + logger.info(f"Invoking rustscan: {' '.join(rustscan_args)}") + + process = await asyncio.create_subprocess_exec(*rustscan_args, stdout=asyncio.subprocess.PIPE) + + async for line in process.stdout: + await parse_output_line(args.db, line.decode().strip()) + + returncode = await process.wait() + if returncode != 0: + logger.critical(f"rustscan exited with code {returncode}") + + return returncode diff --git a/bronzeburner/validation.py b/bronzeburner/validation.py new file mode 100644 index 0000000..1b527a7 --- /dev/null +++ b/bronzeburner/validation.py @@ -0,0 +1,23 @@ +import argparse +from ipaddress import AddressValueError +from ipaddress import IPv4Network + + +def validate_cidr_or_ipv4(value: str) -> IPv4Network: + """ + Validates whether a given value is a valid CIDR or IPv4 address + by attempting return a newly constructed IPv4Network object. + + Args: + value (str): The string value to be validated as CIDR or IPv4 address + + Returns: + IPv4Network: An IPv4Network instance constructed with the input value + + Raises: + argparse.ArgumentTypeError: If the input value is not a valid CIDR or IPv4 address + """ + try: + return IPv4Network(value) + except AddressValueError as err: + raise argparse.ArgumentTypeError(str(err)) diff --git a/pyproject.toml b/pyproject.toml index a10883e..f50c81d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ authors = [{ name = "Darryl Nixon", email = "git@nixon.mozmail.com" }] description = "A humble network scanner" requires-python = ">=3.9" license = { text = "AGPL 3.0" } -dependencies = ["sh>=2.0.6", "rocketry>=2.5.1"] +dependencies = ["aiohttp>=3.8.5", "loguru>=0.7.1"] [project.scripts] bronzeburner = "bronzeburner.cli:run"