From 52cadcf47c914660816e032b43a09f482fc4c62c Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 15:32:41 -0400 Subject: [PATCH 01/16] Add mkdocs files --- docs/images/logo.png | Bin 0 -> 22921 bytes docs/images/nemesis_white.png | Bin 0 -> 24228 bytes docs/index.md | 59 ++++++++++++++ docs/stylesheets/colors.css | 147 ++++++++++++++++++++++++++++++++++ mkdocs.yml | 30 +++++++ 5 files changed, 236 insertions(+) create mode 100644 docs/images/logo.png create mode 100644 docs/images/nemesis_white.png create mode 100644 docs/index.md create mode 100644 docs/stylesheets/colors.css create mode 100644 mkdocs.yml diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0871b276025ae316d35a7128f27fbb7832213d82 GIT binary patch literal 22921 zcmeFYWmH_vwl>-X_uvp3cXw#qArM>w!QG*8*Wke+xQE~l!QCB#6Fj)P>(_bTz4zIB z+;5CK&basdJKdwZSFf7&RL!Sm&6;zqTH#6x(x^!9kN^Mxs?2){70B3;c?O2Z7AU|OtN4{a5Vl{Fk3l_bsuPU0cMm`MS=*4r! z?Bg2MX~}e|+dqz2>5+h^kuu&n;#$Uo=0ToG0bPqR9Hz2^BpJA4{hYdAIL@{3FO{xB z?r7wMvHj5>u#7v}&qvY#0H`2Kad9OXaq)ku2T>}+CyxJpj|i~8qu>iVJz6>GMr)m z;zH|NYp%?LOn0u5P!!^A}8 zvX{)LdZEh^zBWD=QxPi^M?KG*?69WWBcfKTxOz_T9vRxhmTJr;EicR%5s{KiObXRh z&45g-RO(AO_VIwg6GgPJ40EcEDXSx64iZlMCj4vQ+Dck*u?!F0Tj*wen@3}mywa(*Kk7Rc0Xsr6l?`=g7_5z`Iywzp9_WqBP$AjQL$SaiMO9jC|67iS z0^r+L*oXP$Rjo&m4}g7U_5#)39w8Es&TD0Jr7g4V*{fi74IP`@arfJZ8ZH2Urfvx_ zH%)mtK4Y*gv!Myt$duXL)*fPq0Dyq7yS<^YwW%|yk*T?*ogn#XO9wfrrHLT92A4dm zyuG-oh2?usM^jZ#1vO(&Yhzv$a$zAP0e3zKfUT*sA*s8qjhz#pyCC^rxO|Y~Kc`v9 zN&gaYwiYDUlvg4Z2RoXQax!x=vocA#Te`B73n7sTIGUL8sYpou0|N3-kle!A*`AMu z#m&u)*^Pr4>}bxy#>>mg!phFV&dvmpU~=-Xb2fBmvU8&N1MxS8gsGFUqouvGCD@Mi z52m3J*u`0poE%b4`p^E^+RMxT3*OG@A1pxlU~xCJXJKPzWwEto`S%)5&XTSWkbiXO z|60RI4Kiz4R7{<~E{?{glCGw9&J_O+VPgER`t~l4Hh-yOV$5P{V`>W#b%M0Y_8(13 z%g8JJtHvJ+%q?y0|EdKc`#&U|EzSOmtpDiSpOU}S`S*@Ms{af3KcxSu`(MHkDS3H5 z39zxtpYF*>2$KJ4pU(trY-z&x*C7Y5DHj(Tn-LS6A)6T!CzmNFlOYcmh>6G8loiAZ zVly@2=KeP*89OIuLpx*BKTr^GW=jZ;87qk0kd2#*$=HyU9RdMjW8&pvTLLjPd08|E)I5X9&TR6bT*VQ zbT);6va)mVvGVehC$FYU=R!zrX+5SpGGcNJ;;i7JP=rf2-hR=xS>6S3e!snat6`< z{U;{?01xxe2g*Eq>d$o$cxM@TN%&nv9Beo^L^b(O001dKMnY80eeqb=P2}tKtDj$% z!^H7sG?lm{3|<%#!kcf6l6}geX_kvsqajJ3(l%#C?hZSt35s^&)$03x4wRW4Et_Gn*aJbg5SapFs`N%@{bP) z1$PDyqoIiP-UAx6=YQB=248JziFEQ(ZBa^>itA^6Mi#^3j~CY={0xPM0)@}^ z9U5j<3(DcDUhc~Dbrg$0!IF1qlAhI83;%xA;kkvG@4=QVP-hef&uHH{=K+NeID7j> zFd9XRxZZ4_3T>EY?u3gs|JFT#U(!Ihm6$}a&F$OKBe#?*^@zRqw4lmNT_y-%0M+>h zfIkw)o_2WXr9I8dN@sDJg*88>K*u*UdC>{=a4o)p#R7dBpJn_ykSek)SQkZ1iG&Jp zL?+zw7}?XUVYZw9wN&3hs%I)cxU^Z+rwuGsrExv4g1|6F8S9dY?PSNkRVn zm|14E$?o0pGZ`x`LhaH7cxf4Gh7Yyni(4*hLsbC(nO@g@?TV;3{?H1=d|c8IfHyOl zMxshQvf?8DJ`JHhI<(u-Kq?5&emu4%0muZ4!cg9ljxi9BC7njP zSXt99U4mmf>KRnMm%E1}?cYhKM8m6vGTI9NWN(1+rfIzQR-xlSd5z;Z;!CT}ryZ!i zfVY5&&Y}!cDg{SQKl|{zVB)R>R)0#YCt+cHm~}fNSVSOOTf6k8{6bm17V?n6Yb75( zO29AJmfzTA3Ba*;_ifF$Ei3u<9U4*hH0{Bb(!$Pj)11h;i3J*d`-Hhc;pbBwuN3vk zkD+mb8ZndjG>)UMG{|g#pKmnj?>+}A>wi%0!z5 zX46Q?Qhae{8L?&phcnp_QVc*@M|AQ6h~IQVL6HJNHakb5>|Iu2&;Xp_sm%2lXp!TL zcZ%xGlA@wta=wu#UPkc_{RVW1h2w-g3(cvEvwTfmAm$u-vl6@F3Eo^fP&NjLkfWR+ zobzTfXK3GplT!)tcIIyC%|bd+CO)waszgjmznC*;xVDSX@E(0WO40khsvaU~B*^i_ z1WFFo#sdXF8ch5&g!QJpwkFpnAXU$Fv1)UO6(ORdwqX+KC>XsL;d9{ z=@teB>4!Zq7NIjNpkE>vT-g{QwD0w_MYOx*9G|Bg%g#DX)A-%BUIOePGw4W8F~C)X zc;_$jfxJRzc*!#->S;RK(?`>y=|#RNU_Y`N$co_KqM`K}uoCrU9f4E8IbWw+8#`YA zZbW$A!hhTqo%qPEig8_gK{Fb*YUUFa9KbtBBviCo;e8n&=IYJTQ2Nbb26$d3+AsK1 zEha3>_bk6vUE@>=E{@}}dmI74IsyuPu*4ievHj5A=yJ!&F70Y2w(g0K5^; zp_m+cC)89no!V>b|F!JWmVe0bk}d;_h;OuIvFg2tauS?Sv=cx82%QM%a2N~L z;-%rU7vDKB5pP4MO(KBCmcjN51!1H zPMK)et}XG^{OV0QXe zI>)YPWd=QF(^JTb2*8gPh0O-JsE+GT2|Hc6N-rGohlIMFIE#ku5&4pW+6VLlXtX!FaxYbzl{rJb?-*HYX@E?)1KqzKA@o~>6uvQ zPrPeWu`+@uCsZ&L3K2yOPF!^$(|E<5GF%xTbhSEa8--karrTh_?qHrzGaqKmh?vcR zxa>1~+E|r&SH!c4kC`pamfjM((Gx{ur?*N3(k+Apqk|6Ultd)?I830=R%A`35(>=W zL$sjZl+p}c62ydT$S)4Cpc?E9Rpnn;#Ro(i#*}{dmhUVnh*wI+cP8VMngpEy@}kXT_BD1&&@4#pM(%s6*@&3D> zdOxqNi!4j z0MTGThi2Kj-&-f|6EhJRSC*GYV+rTS9PK)%ZLLeS@Ze2?F0C32kq;R5L&1FZnsQ9K z?vw*&7yUH-%^gmvk6HuoHc{N5H?52o;Hh(FcC8Z;!=H?Je#iStOXvS^_WRzKKvC{C zv3kr|An6Fr5pM#j!cD^=g_#v7E0_uHaMcewJ!m>~)mZY1N-$HzZ1dQfajvwQ=10QX zHQZW&<9as)1qkn9qN}7`ldrN-5jJF}o!;}iT=@_>;7qJn;IpP-oJPvQ9>gW4;@>Gk zUW9pBpY4RhNSy5Y4KgpxLcS_13uS_>b_ThU@Fa0RF8ur;eXGwGkV4M}9+Bi3stNir zdKvxGLi*tZxKuyf__Igk)*vN>>)kk%5(qBe``ghshU;G~nM;1URSsVA#*C)Z>p~7~ z^jF9}R`SsAk&S0bFi2snkfy)N6oD_Gu}cf63lkyn$jmd+HuekF(uJP$eA=2zwS7A^ zp;)cIaSrAN0go=shX>QGQ#T3igJ|F1ZG0L8xJ`$9Y}jL;nTDirf>R@?W)$h#nMr(O z9j~$LB^T9uuJwv3;MD7_NW+Xn5ntC@ag&KQy`ZgoH*E zfkyaC&r~Oebqb@bE*>4QZiugTc}_@$>DYI-`v7iy>ep&P2kQwk&RHv2j@h7>d!)^b zekrFRgpv~g%Jy!xC)j9bz z)D7+8>_any=dwiaWyL{tgz#@4vbs&hr*~{zB$RTPeevJTQhk$nY;Tzp(37YQ3a_-8Nd3eRS>~c77fY6p4y_EAm25rklPSQxifHj%oQ&o=7D=0H>qQ9wAj z6jq{(DcHclS*lw>I*6=;gm{|wBHApgaQrFqXYX5T22m&>(x2z7ADY5$KWM?9*AVb? zu<+od!qZ`S9Ny}r9TjqS6tey#Do^^hj2?;{?xdkplpkmYYZGiXhrD()7`G~H;}33f zh!X?vgX^|7SLq6Nep8uZt{!mZ`s#!2MTQ2-dJVtGXq=9osc5gprfTwFE!m?5zynaC zMe|eS+78Hc1URpI;|l0sT@tF#cmf8<3!^=h=RgO_IN3IBb5FFOqn&pGl;7Ru2h2 zp{5S2$4zS^!Ol7Rz_WZuBB{9FhN$<_KLuPJIO$Sef&O5HNB1Mi;Y~*WrP+_kls_7{StGI&lNunv%#vSpas?ZlcN#Yh5LJ(t zL-$aK(WA(sGZ9VHvMJ_}Ri|OH-EU)%+^6x}5SLoG-Y~)Mz0W?oL*r_ebfmyCET=;W zY{6|qG*2Cz+muh+U< zEbg=TYffyn?Q>5Un1Ht-qWND3JBr^jpZJ-~R1|1_nW7x3ZUJkW;3Y!F(mcH3Zy?r8}Gv&7w3KX)J#vgqvWs3>c|3_%f( z1(1ZWNta8@=q(Ff(;!)Rlkf(GPs_?=Ti#Diy{{>R6|(-1PzadNi9t`Bku4TOs-ZQC z{6osG<2`DNOARj&<_h`p(ku%57 zIFTg-S%{bRK5|V}tMcw~s$Q#f5ve&}x&y6|@eCJ!xVlPE$7C<<95D5m(jQ;}Z3q${ zt+lwd(D#t@lzULdnAy@tX{a{V%k^q^tmr>gJ8DZcB>-<(EpL=2kA7SvFVFPA>$30Y!lh{(mg%?EMnFjsY712mfw^#Tgp%(3fM0V-fF<(d;gId zl+)pTb(u$XskoZ5lgBoulJbcJc?2r_?TnJ9deheiS2wR)mL1#L4l`JVa8oH;ui?Vs zADy$16)(~@pLW~($g?=Iqjh4iGb+csHJ>&n8JK~|kK5v9(qi>27CeVJ{!8T|;7@om zKcYLhJtF1$E9D3PMPC@{LZI%x&BRi-%@rITw}jo^%T|ON-vVxV`{9Ats0O~U7ftFE zIH_!_wNX)f<=N=Hr_q>og`yoRsz zO3WCF$4;i7{p!wrc(7Lbl5zkJ8hM%JvGE_o6$Pqim7{ll5sRMI&$5zWn89744#vYH zSO5cL<%k<|-ZB#Z&aqgn511qh9I@VQ+8a#>#vLyRDoP5S+*UbARz3M_iiK{XZ*{(< z$!k|M5Js3^i(@tIoGKseh9C!elNi?U+)U^<6r0zC*5nEDFi>;0Z!)VcoMTV<@t0wf zR$C=zyzW=9-qxM^I4K>!Z_-0TJjWrF17D0DmA=6Zk{W;5nLcB63J$w9F3C2yKcwTG zKk{1u@U3=elqu$aqMQE&XHf6Vy8)S$n(D9-B&*|AQ(JzviphOP{vqY z>IBCjR++NA1;K5LecMaQc*t2hXG5DD9K>nd6ee6n(uMTaIb_8NkxXGm5dsYJQa z>KKCJ(8VgCLfFk}Jy&UYHv$Q2HjP!whzCTdI@!N$e-54`ph4$JWw%ub#|eIZiEK^M z+cPqP9?OW@4C$5V;=+5G3fPY(;JGq1D)Z{5x@L5XvJ4p=m9?byc>bZ4W_bTx-Y^x=hm~J(G_x3>_?T3b@N+0yX&NHn;H|j z-LW}>F3vqmH)Wpr!bRa_w@|cf5kws}hUj2t*q{a;;wm68;$WzgL_Uy#Q8#iNpmH0E z59;}JAFa7t(Rj9AVcH+863DCkereAyzOh|G>?PKAKn1%c*7*8sHFLEj&+3^9;i?k8 zrp6%^i5v3ijM$S%HFAFBIPIV_WO}ds+*{vcAA7@7N2e^|G?dE#bMxVZ+x4mh>1i2*zKY%%!Pc>%ZiwabR^=nz=uI{zh%ojVz zsMTeF9&Ym-FIFo8!EyUGQLVQ`UGI=sn(43@!-ol;y_`s-FP|VjkV8=~{dWOd2NTb) zBsWO5JPa!_m(T5O`eZn8wE!lVuC$#fRd>>F+CzRze{#m>gLu=TXq;IbA=$OD6+0-dv@)GTQyJ|6MbTS*HF{7M-hP05DAXQI@A(q*Q=d-_5^yT$b^FfO z_EAq|;!=&EK$Zz;Iub@q5j}~6c`=XNUW48bDk9!LPto#6LqRxQtDeenp4EIX=2p-s z!(A^?p#vYkC`@rV@f2z-`DMy7a#)!g@h2Jv42rkBC=fQj$nB^zAQAPv2egS4j1b;4b0Vqm;n7@5rYr{aO>f){=)3 zg?J&tkNR0#iBCclEqtCyOkpzBv%u_mOhM~or7uzX5$JCplc=PE&IF& z*H{>k2pV6+9N=HlQVRou+g*y74<#P+>uWDW2}P3(j=Inw#BCx-VU4aQzRqDqDWP>P zRg+~RdCiyz837-0i8A!oAO;EyWH4oK7jSgYb5vtmdxUnFBSs8DOIC>GQ)n4Pkb40y z80OG9`TT*J#w@EaN~efXAhd?U^?G!)LtmlqkCkNki$uil_(qojY;OSwfPgsiD-E1h zuHqFz@}5hDN-v$>_ww>oplGj}`j598?LLnZF~+E;U|uy`?~=2&AJH`n=_&_`NuB;y z>rb4}0O{KGJZ|)Tru%u^cgOxv z&{HELBMB!&U(g2be0U4_vgy1Jel*zIhGB=clXS$+QoE~t!Yl$U&DKcpV|(k>JI=2MF+^f z$JVvS6ChGb0ePBeDV%I#`g(YH=tuO`Xwzcb^*9JTfL}u?y_#EEUJGWeZkHP!D0a@3 za2=pIkX*e5I`dvF-F^e3!WeN35UMvC+N(xHfk25P;dn=8L<6O2pDgOvWuF(<>1 z(jt_6EahoxZZ5!t!O&mZl^~x~0)tOyhKEZ~rK5p}-azZ;gHldgXS=RpqJRpv-4)n1 zLf9Zlf2FqmBv`;GCo-{>lV+vS^|+H@^XtoDRT!~?BLHA(UVE^Vi$SE|cG@mlz0W-O+F>zm^(#~UI1@-R|bR%RpFV7J1q^L@&k4TYji zQ2Z^-{y|B~I}D6Cm(oV}--X$}CGJ3LKNq)$*`LePET%t_QCq0$YkMs`A3 z8s_@c?T5c4&kkI8UEf*!+7rzxPXhwke+2_8+tX;r!;W~pw(1r)1FSYVr@EC;B~{(+ zZ{Jlp60lG-lsmi6f96P_eolPUbi_?Co50m=^SrM6UH!~1C?K#COG)=!=odJ#%i!JC zhSzbo@nL{Pr;(N5L^7Mm+6wA+EB9z7XFd;cvC>0MT?_MN+8@R}<)KT95&Zn_ft(2_+^+iwVZ914LIWybqr z$HQ&1R29n${hbHpZS^)II|m1rP&z+Pop>mkb+A~MU-Z^{$_?gBK9`5;Z#9l!Jw3f6 z<6m4Xe#YSwqx!FsDc0FK=v}*9Z@MW(5R?Ql&3@|o^-3x}!YA%`thAk&4L@`RnwCcc z;rQ=>w)C?^0zYd1&5(^BD3Xe|12Od+SAJ`hDv6sEIYoY<^w!BaaN7b?)WdaldlUiBon);L6w!a zmx1u;L(R_H!^@V_1%gS_cXi^tsc~_wcfw0&`puNPYwK~Kx664A0JmF84h{qd2m?S6 z_k<`ZjJjCO-j4v z^z(QmCCA`--^yw$udN9N9fF7Na4~1a(EvH=qG9vQVr6R&Hd}-EA?x1@S$$lb3)`Wi zcVBNB#cD&*hzQcsligS;DYqC><;)WFbEo$kr-Ors)_iVlZRF%;o@|%up9KePt`BB4 z^1eODJ)ZAohhpR664h(kq%)Prjs$wbz`ts#Xr(#s{mcy;Ad5I^`_)irmhElY9t2GP zODEb(!+B82MqB@)7?|-_p9SEfNlwZiK#B0hGo$L`d0iA|?2`~etyQv)J&Dc1Z+92=FPC0%3 zHFMZE?edO{3=9mbZtI<+jg7tz3Dj~6wwv?Sih^7xyYu>fO!QpWgYg$%kkVy)gT(@> zd>(Hq(Zi$0nCba$7r)Y}-bE*2A&wE=)W{PR$Pt-#uOw?ID6ICvBIBAlAI_OpOOPim zZD|zELIUC=mSh%vB0=xHpImMy%l8B287*$76~rJ`yUE)%AH2+Lzk}VyMXhw|!InW2 zp_OKrPc4qy!+F1cHBk^XH7P+W@bndGk??|oK+^AC21O<0&#>Mb*`8CqxawiiV`H6?A ztx}JSAH3TByaXZkGs$gBb(Q=Y8O^(FJN!`15L-H1v84tum~g6gw(DOX+D}s1xtB6c_xy>Twyt%gPxhJ92)K8@h45c>g^AhB7A4EhtTg|gP zxG8vUx13ygDa);*KkmgIio%brlnsxL%H185-o^yOz*ok}QmxhtsA_AEY-KXuIrOjy zOQRAC*bj9DMyDsHXS7yx^6?#%XZx*UYinx$5HGB?^-d@)6(D6S+wG5^ntBe6AwE8( z%?{iIaD{HrcPnToIemqs_-OYhD=jUzdy~fPEiKaIG1809HEbx*5Ke@%va*`o4`yWG ziFZdbxU4@40V0>4mcWVo~WFDhKMK(5K-Od-_7!P*cZlzf`a$e zz8xD8?b2Plv6}9;Dh9<2P(ViU_FH=T;oinZjk%Avz$?e(;rv0-C(u?DNqKjb*EU7; z{c+T`)7|6aZG9p>4o>1jo^(uwr}bQ!kTE7^yPa6?9bw5xvUMm5L8Ie-Jk{hc8U^pX zZ4BsN|J)1kvDCJCOPUhX!`;ivxu%v5#o1c!&FA_6TFL2<*>|YrAK_sOk=>pyM!?n2 zkGFm_yu3|6ieHOd4(D`_u`tdr`lE!=>FDYCJSYqPhcz5@8$lf}qAS>Oq=EKf)Y@dclnA@PRC2eVolb`gr}!SrkI%QNP}87h*7=hh+LAs?w0O!- z@HWiNS**9s4Tr6ToW$ReT^R0=xxU{c5jODX>Zj z8elWB^qwgi7)>g$@m4kFQ-FKx$0A-6{C+aPxN^z#ek_k}D3HtEkZH_OA-gAh4jIKk zu2!!@&>O8H*Z~r1@aL(&nmKXh@V=G)F_&u>ot_*RtNMY9XRHf~G{hp<3`aZA3Bbf4 ze&Rg%dTa*RQEsQPqOR5{Yw9qO9xZo_&pY=)>#ZLS4~3bZIyt45nIb5E;92XMRYWVO zLyHgD*^g@g6+Tu6ijQd(M4dVrM2{gFpdk?QkCph*HFyq7gJ2MTk1x{GXw~glz(ogB z4E+vI(D^_i{o#ris3z3S=M&9G8-OYO3I~^A5ifGKHwOzdw(0pfbklsU2dCi zi^T!aN;L!v>SKK3C!D2e&Ek6ClOmTi_4(6?K2cKq!cVg%WAydDV*w8ChZLb}D?y*~ z5(35cFe0}74IuZWYPLkiQSPA|z4ck6iizGk7s$OY`-Z4PQ=~dQ1p|%9E6s_pijHZn10SJZN zXXsio93?+7awFUVdih*vFIyMZO4-R4i$6-e%WU&4Ew8;q2gTnq%BggG@tITBh};Ru zcNu5rorn6k_IC_Y>F-SN1!wjE%eUAtDJNJujV{H;WqFV(3^{}{Qk9Y3yk@(z4p%Hj2M0YO( zB=DRK*Wl;Pm`O`W#81R-@zdN^K(Gy1)3hLc8nmO*zq+@D0F9`zmcxacTc9IqtxmBH zLJJ6hdQQmlcGTC?TfLRdXU{3_Sf3DjWZc>sQXR^Zidg9` zLW5l4?V4vL_IvIN+q;pMdHW|s-fUR&jc9hfKA5LYu>Ebw!=rHvi6Tgj@NhMbwUFAoT;me#)y*0rz&DF6(rlRqwjV<@52MbI3IzY_a-+3Paw;E zY;AR8V|k_G*FU0`F>HpRG-;SGP4=p!4}gU!Zs>s`lye@oZO;xg0eXHeNF2 z>-BtY%6KO9n!XTNPtv$NRAgMCQ<_(&_2Gpz5O4)V#-uo2DV{nT8nBdros zLT(O85(G{*H#r3O`5%8lGP=upfu3E}pXJ)~7!#D#M7&w4_p@6=2}`_IZB9F*ry1$# zx0ln3!q*xG1_q2FHcM{X+ljov!RFF;51uqsR8dSe&X;NB(oUoz2;fX1c&`!pMh$5r zYN!&w`b=1W918MDUD8it9?m7^tEcB%Tk&sR++@F4m6eq{yzjU1s)HpZKh*JM4kv|f zIpWbbD`xML_%;eSO=NYLz40H^Lqr%qr#?_v~$j|?6fZ+aSMrk%W6>e1;r=~(bJw54Vvu)mMN5`gi8GM#DPliv|^duZgRJlH{s8jCzPx*5Z3-Z?nfbpc`yV1c{ng z2nm;XDsplRq+DF;b%up~$$7HSFD|Xxlgk$ZzWz~3;iDlZvKhnY zEeaSef!1a*xpH~*i|3=+v6j;G__wzwD@!fb^A!=TZEe!=+zMz0_;AcRjVT^y>j6~Y z_WK?E7*8JImsV37HHo|K&Q3r&G>%zqDl-$)N=9mOgUu6`qKRou`Cg-z{hOyeoE^ie+8_oAiC_1` z1Pd7rjk&j^WFJAgfE6&*ZlzgDK1;ykw%T}rr{nSDbI#*ygUQfERqNB!@n(PI!)5l{ zyl*uZnyz}QXE(=7!ncj#hR5&w9V{-~k4kK}zvmK*k0*^@C~2EDrr?K%Z@!y(6#y96 zqtM^?{$|Mf*4GoA1zP+Hla`V~o6h6+8^y1~6)cmlqM@DL;OG)cV zM2FhD*j~gHkA7U>BQNoKU+o&uI`@^f-rm3_hYAJ?K=O9aJ*8Mq)^7MZ-j~x9+@Dw= z5wGv}TxV=yapz5r6ChhX*!q zg$p)%j6h1qY4*$5bev#}GvnN~R|*g}h~BNf=Sd&rD?05xR^l1^gQm9XgXr(+d)s`9 zk?*ZQ;^Jcb^C=jU}%627t2k6T2`g}P|?{wV5_s93*rz}W=`uj6<2388LH?i^Y zQ=AnTyUUZDV_%b!Y{y1NqmCDg@oLSGoY zZdV zcij5LzyE2gdy&7sMZ^z(eXN5rJ)Awz; z4YVzPZ2Q4;?5g@8sNG<9OpJli)L9#4PQE6~9#gO{TmJFURJ1FR1r7zF(nbNM@#Ofm z6_P}~(vCS)=b5ODcD1gc3hHCyxu>4ivZTHIQ9kT6ShK>>b;m3}>^b`{Y5 zkuPZ(9m;m1X{XTnt9Np@RWdx$=z=r&)44PV%^gl^M%2{TsW-nS66?31-UrA%0vq-h zbrqG|Z2iaYqmf5(+lh(4VjfUNYP9ku?_4Y`Ex+n8YjE=zY4XR}hAX`vDa9K}d8LAb zMX<+&&I^J`ih(8}EAo1#fd+&@QC8HI4M(9~T1{>>GAL6KCOBN&Dt12^O|&5I9KcAV zu9M}?zy_X_dUS>tfMDuqTuj?gPDpIvSw2{OXp zDO_A!Y=0AA;0d`l!NQ|~$vJb>tLZR!DJI^6#CW78z9Iq#-`T?kq9`1H-6!*=nay7F zOUQ7i9*gSGpdkfRUVcKhCpGTU)-!Z(l1x{R>8?D7+%W!Pz3XYglt77_WWM#Ty*mjV z;Rhp(Pd1oTK{s`Sjz)FV(wCDjQJdgrmc z%zZB}cwRvPbsY6>ujBO3;Z`?B^!bD z-Blzv*^Spl=B}ZxYj_%#K*v`C6u>B`fC0e)l_V+N<0ZRdjm&KJetrF{#(&(;wKad} zhP}r6*%}3Wr=)22SM9J9!Cvq>j;PP+*8dPElS+SHe*ZJ}3=enJ;L&~PcY9F*2 ze~66Q7`AoTUPs21|v!CRL$t)=4qdkCr4 zE6r__1#=nnY_-OSX`6?iaIfrnpzQ!?&Wy-mhHS-g(PyRXPp-LMx8Qa^p`C21OC?OK z^wh`cY&K@)T>+R6&Tne8!VEut*fjP0AX3QQ6Ji}Pal-!=k9hv;Q``VtwD@j|8{zbB zQbEHW)gzt({HvpXoz$z5vW2-S1w84ti969xu&G?9h_6fD% zY)~C-X)WVUbH+n(y~;Z<93aW8OVu{|&_UH{U1#Ei&=((*U_r|x4ug=`q23-wKohr@c&dFJ{^=o$f*}Q=I{cphM+87b zk@Q4?U)eT+e4uZAokqt>{)1)`^h53|MPp0yWGwm^-S3mky{vZ+#k+dN< zuWxs7tqcn`T$)qZ06CwE+R1S7xCtDA_(1A}J;o74+^S|%5)iydEC@ToeKGyV_Oc7q zCur|CgP{mwwTxkrNloTGFFuj)-r7TRT;9UA3fPOp6P~wBy1cfuIykss^=U@%#{FG>bY4cceAnqyOLWhQ0 zgb{3EZ%vP9v=6tgORv7Eo@6RMiI2G1sc{On%ACSgVVXqWmbjUH&LX9Ie)ridSskNW zZNpms3fYQVhnIRfj&cQw?DWwZ9~u-OkV0w5eT{UxqqMdbnfE)~LpZzw5?W^`jX`>3 z%B%BSF|4&8zc(zv+0Rz7MTl#2t8gf5E655ex6(@j@>q&!ts>Y~6M88!l^;DZID^sq z(S%=ejgZ}nP~4~!+^NO7av9KSJ=W8*HkF@+j;MG00V{p#4qV00O#UQme-Yg&`^=r1 zbz*9=@XF?On04kI1R&$r@wcM7W3$ng@`wu3jNV*;CgFtkS5p+vcGUfe1-f7Ev@&jO zSp3dfdjxGt`9>tCt2z)*%H?Hqjk$Vqi0yD%0_x44k{c~#FQIv_+HdU&TVcN$j?==d zMK@(xh*!`(?t-#^+lg|#yOMH9n3|;gv+k_8aJ&v9njty%zW!_fxe~1-S7<}sniv-4u}c3OKr7;AonvD1B_w9z$!(H;Mr5ej z#n#zOuc7jT`vNYRXDlh>8&+}v(Lew($i0zJ*Z*zN{X$*DanN+a*P$J1@#p~91{}t4 zfmA*Rki>=o;L>%WrC~L#ORLtfA^7ffy9x*(k6TVQKUvu0ho$@Fn+m(Mi*mN-*QI(^ zul0VpdbMQa^TK;CSNi#KEr-3)^SrpWEE`$XcZcHLKVS^?+$RXck!0EVEaW)okSS~i zD@l0)UpjNdx*0(G6u#d+{7z+KAsHD4+Y|=&v6gPVmz6b(?sfHS(tJ0q*yt(M_AZ(+rAL# z8P6y%KeIic9UtZ7S{z}hW}3L0mjS+U08M^k4KB)7#i2e`{r0>&Bs1;%%jfXqm7*p% zAwA|^*7dxWYE#UUq%b9ffVX4@Y~M@s&se}e@X*oZS@^E?lNa~47UssKgFiK5wnm5d ze!>^QXT#mtI2hAyRO+UYX|7lqgklAFLNBb0`>{|xo877w7MeTThqut}zV^l8E1^D6 z0K&|=G;9?RI&u~GafoXkmS{G-(~(%}(Y_qHR(qZ8tYaP%qeoFqjaC_0c2>Rxyum`} z(RI$7BRSb1#>rHzx+sM_x^NSzQEkwYkbNtkMMYV8mNzQmv4z8X{gH}9IEVUNHBszB zi%T+Na)Y)`OpZ}$Xy|>+$gV!J;z+4YhWa8@19Z#LSHjewW-exGq|of;pokrlY@G|? zbPj6gjUPxo=t!BtWlN)<9RSo{ekQjl5jye@3HiRv=iOw_4A}Oe_xWaEYZm*hsVl-y zq9CKnYj#8VhZ-sk4Mc$H?ihq3knWg0TrP~8UhaDw0(GQynX>mQxY~Xj?R=La3{n3H z1Gi0naYn@x=QtCWVI0*&oCxl$eG_EsnN$A-y7LYu;~GyG%ZKz<|5oennKf8=?}vgW za{{62*R{Q4aYOzbR?0ir*LRn0Qb|6fU$vi@({`;4-jvtjK(^Ix;HxR! z1*bc{)GU-Wr9)A1J(O#s##4y!v3cr`n^|;MA@g!m_BW#oe#)ZZV6{E8bI)iw%+SD8 z>UlqeRK(`h2u&;PlpV3H%NB>xuy>J*dcH2|r8_-r+y5qISI1y%% z#QI4lUfEwn(MXAmsv+|xi7~T^=kmMY^B12$&px=XX}Hb%d!qQpJ|e^}U=h70543%s1<3P5RS}CnlyYmY2ACLAlzYUsDPXLGD%zNG+gEm( zo=jdUMmMCck2mu8U7Pb#cPrQwF|{>oa(!`~V}3w(O_fs4Bt_`WOtJZG>s2+3mUzF3 z55m7$>e=eNhsn51mOF4-@12lO1-c?j-UzYT8K=U$W{|%$m;{Ej)Uk64ypMcjsm={Z zlUn!B`DCGCEO(?@Cg@vYK3LNfUVU>^A|m~r4x~S) zH*#cq%EFVa*i|XS%_YWUv7hE$zaug_5{P|(?ZTiH8;LHvdXVug-99CdQyJg6&jA1W^ zj?=a|-4EQarROWm@3jS=jcmvN6Iu#BQ(;3ge-{aCPEMZ=rKf_`NXjj8M!uWggoWlJA`OLUd0=3 zYU5k3Cf3E4s0%}2P$lLay{zCZ9(E)@wV}HI#=@^dql3Gb*3S3PniGX)4zo6;@t$eJ z-4gS6rMiok&OVfJ!lqL=ws-$tD*d81I?Djk#_20HJX~KP<9)TSjpCCzd(ukn{*pOB zd66Ll+SFK-5+f4}?4!fShyx%51Qhv8r$p_~`LWfL`3+!8}e$T7g z^sI}C;TgetFu+J4s1fqE7kEpSJay2H!GJo*hzkwyEj7*)8R=*FG)Fe{o-htTC?Fxn zU;0Aq{_G7-a{R~5cW^-OL4FkmrH9rW0}Uj!?hBufA*kl#m1Q#CXIn!2KL2>bPK~{n zIYl{z|I+@#=Y6XT@3|Xjt5X_r4uB*uC>9VU`R>BiOS^YiSM*h~762e&LY&)-8?`e# zR~x5$>MRNq)3Dh_z}<55)g8(=scB=QENgmb4`7#RxS*-X!1P4Ii7?cZ$ zbHxhY2YoZf{@TRGwKi{Z+{fxCV!ei5QNQh~F})(Sup*3|WToDw@2v+IctRk{V9Mka(| z8a`n$xEluMtOXx%3{*n$zy(kKlK8Z|bVtZLrEgffF4e|4E_%!JBAOG}l2_4^h^dnC ze>v^VUp9VE&c)`o$C~%F4Ni$2irqQJUu9hAuA_UhG~1vVazFH|;^Vi9U3ed_is|&d zBkG_MUriLx$+g z7^k^v=y#-M*4viIA%|m7Ata7o^A;!vAIwX0MC8y(j(RY0V!U+brIJ^C)TUW32BwXP zVH$)p{@qlCb`yBBCw+!#&7gVB~>!C4Ek(ybn(Wk%t3WPvS;i@|# z@{7Ox1)tB`mQ&?`KnRu>5i-)N+PqL|X2V&w;Xug$=bVVE#d6+?XOChq#sB~aE=fc| zR401iFv=C^%O6A;$YS}N38Ey--sS0c4BG zs^($Z3Ni1il=dEA)slF1nT-7MU>Dal zv$v$#f1-J+k~sF>jtOnmrg`oLx(iMJe`!1k06<_+B_cDgDX7H*kLK@pgl79;MDI3H zCHk4JHJ0fj`fka)wZp|gAW(^Uf8@IhmyS=(In|Q(QOlk1w{V$5b3prq}+%oh@eDPFl&@_L&Z(@ZxL=K=^oMd3GZy9qv2 zPKepK_l>o7H%;PSkEcF7etUA8TFZMfGwW~lCN1u+Sz09|^R6lcO9rN&NO4$szLOk{ z=ujraxsASE{bn1D@in;(r6?5KZ2+p2kcZAIM2j1{0jNHFKRTv!)OGbdjD_kGo#ZPf z{dug!0e%HK8qsT@U#)SG+{nao))K66H#Dmf;CG5$_@De3F8jv59fMACI0U~B>mAg! z#IV3aV@?#>Sc+21!z?N(`D1Qd#fOtqj$Ug?^4L2dymO~A{d}p8o*-m6+-OTEoWP6~ zBJzjKcG49u?#Z~(()Ud38l21lI?yj{M0=HSQ5&tLH&_Y%VjKV;$Do>rm6yt>Pe1A6 zz3!J^_SQCM5qz2$r5JWj^SYbP^175}Z7%sxiOK!v6vD-$j-JS9E!(A4P8E++PT{ZE zdGbXst?4~CBkN;JTPcoXP$|Gp-;@cK_RDH=x7FHkFl@#1BR@#;tg$SR8=0kXIL;ZS3`&a1~x#%oI3EE1@$GyVM8Vau}k&&P8>nvMq&sJI|Ihwlt-M@G5 zz$*PBh1T}lYSlcO=Ol)pXSRj~B= z!;L&&)(TSkM??iy8Wwu!O~ZM%R4D_3S_;#uMC7{Mj^3*#Z`<#@IMhjwf4J%ThR1cS zHYnY-mj2e}LZN%kY!!v;ugV4AZTFOayn5H+VtcM{`ICM9hg{db=UroX&c()hG6R92 zMnwL3*+aBscnd4}aFWB(P$u%S_FjC}r`j^t)kp`J85S5D{E^#2;?8+j;i}O+I(!TF zT3{OseV-XAUv#zPRsR~pOfNm-W^GL*0AM%$7!SV1j`IX*U}wS{J#FHn!4M z^0Cq?3AOT*0iNqOHIcx9lN>D&KaYs&9(Gf+$W3Q{LTKTTEhBlk&xsNMS|_y^0< z^0WTU_pa<0F)&VToZmrX8o)Q(*h)(vlUl;tklNnmqnGQMa^i%O9F2>6r6~R?<022O zr8j58R1^wBa9c>2^QGiZn?=Nmoi<*sGG%a7_ovkQdBHWNBo70F89A=O5vmj*u;2h{ zKDqar8-K}wRV%Zv4hScvF2CYFS}P$*qL${>Vuv+p^C4R&1lMplN_xCd*VX-1eTi?x#}##d5o2O19#Uis1gvc zUw}^jz?xUfIFLMp$eDwTT%g!MgBcp0LeEDCbQq#F65dZ2+Dbnh;fN`ZPI5T$@BI-a zmfm+~V%sXi0#^eQWUUkhyBGSsC?tU50Q~e8us)m`0fzyoMZ}@2?))YFkDfg06ugrh z%?8Se@zOEpN?!BS7-z{%jGK|Q=8tf|0ctph0suS=K`n)!SBa?)yNwyQ1(f!0=XH|9 z3AwL)_3iEh#_l9Dr3PU$+y(E>~@K&i3e z{o7aRl&*TlYAH(H7j&R3(x3_V#BzHOBW9CN`j< zj=cMRg`S5UE|>6@gq$oq;uN5h94#K-B@R}!oZGKTWt!8kaFU~yDUzXKUE19-&6jD- z%Kee7$eu&)bm@Aeykn0dWN?Sz?i$=BNN{%v?k)-LZoxgceUsgN-`&l< z_vbg9nd#}%Ro&Hgs-CLmi;{vQ@*BK200014T1s36003n{(sl4a$fqQd$Op(5$V^0D z1OTXsL3}cRf&3;hl2VZe06eGw0KXsr-~p25w+{d~GXnsJU;uzO6#&5boYA7p54jO! zqA6`EFAt!Dq~QTDQ0M??ND2z_2Y|u@{NV;k0c4=?|2M4yMf0yRkS2s$06_mLqYe4~ z^Gb%i|IqyN8#){6-yE}n|5F;2l@0wrX+Yf{GXEf0NP7VYpQW@MA?-u|^M*3}HU*Iw z_`^a?(@9faj@QV}hS9*-&d`L>&F1qTDgeJ5FC=MW;$%SLW@ByZ$m=FR_Ky<0ko2Ey zCNh$L6mhZ=Ak&mrA`!E5Fd^Y&WM*V06MRELLc;H0Y|5)5{^4KbkY561=1xwZd6}48 zU0oSn*%<8{%$QhsczBqYS(#W_86YJX9Nlf54BQxO9m)U6ZRdSbTP}u(Kuk z!`Hyj&e=(TjOW#%*4&&|3UWW&Ogci zA=f{vuKFO7q|M z|D&G&%>A1}(ZRw5(v|;g*}p3J&%FOA|3^MvB?~tbYfW(r8xvc{e@Mf^4Qbo|_152{ zVs_Sc4yvCGj7$Vs{zdX1S$~)QM@3ryQ4trCDb9VhB7c#vB z-|#d2?^G3h)5fK$1po*Gq{T(l+@MY};ZiYXmj>CSsfJB)Pz%D^H~Xz5`a^aD7K9Jl z$w}I2U6>XszhP;5Fir6-z@T9*H|SRqR^@66V_LK>%4eQ^BaEt(=1d=xi%;A67HXC@ zmhr&HmUflZNFWkVfha^F4E*adr2=y}ZZEo^lR*9V<<5xl^vlgW2#qEDoBbc=aR0>_Li_(W^W(xZe;>kF83F8X<6Ee~ zJ)+MtCenX(2t}+55c7mSCqBOGis@c|3G?Xz|6qbtRypn@AA|5m75eguafJ@`33=C> zznd@21_kNmAHk_jfLEO}H)6NGn_Z1;)L44b2;&BLmu{e#`s>9Y&l%oPQ%S*>*1^IA z07C44Tsv_7ZP+JX>5XoZE2h_OZ5KN_%Bb|5>30*{8soP2_H5NDe>K*WGPW~U?|eD4 z;G_lq@$KFF9o}w3dw5`?v(oxK1+5xOHfID0_c4qtc36tO^&FaMtMq>l(WRg8%ZDNQ zbmsnxB3-jUakIK$#!mW~I-P0>w&7iLhS+O*kqF~kxREUS?;xQ2XSV8y|Fa2225JGV zKIfg6mf518;F=YR)yvGcjH0b7lS3z8UqW~D29|v9Xu)ydGTOFRk&V391*VpX^1v~d zzvbuk=bv2?QFCL+w)*BX=PDplj{m@3_c7c`&UD# zsA7w9^~`UHJcEZRAI==5CembHfrUQJv@m;``Hmt6LmgqPYJUsXtUP!h+#0Q=9PucZAn|b0SR48>{_8e0Sm;>Kwq^nU8&2~G^nb5UD0F5#fBvO;#`EE#wJnQoc^#6U zhI_obQeeqerFM@Sg_C2mCx)gY$@S8P?`It;)8OsP4Qc8*51)3WJgKzc!N0nX0!#pO zCtV8mS=149jP_lmt0_ksX^~M`{U!$0$t&shzV#f=lGptc$eq~Herr1##b#Ib%PXwl zYmdrd4OcUXJ*z(D-+O{L!p>cXOZ@ziPV)el_xHOi+aagjq)id>&=;|VUPkzZ%(n8{ zF}PiQe=zdz!$b4+LWd6S^?dJ6&ppO-25qt=&wdmeJ@QcP7$HUl-Yl}e`1`Y?4w+PB z`W_v1Y+3Y3z9Kx81V;e+Dq(ZM7vDU;yAAiR?Uw0=qVztYPi1BRL^D-UpK{HKCmJ_y zSg<`sp}61uu2|O8=a*l(4lGA^BDKrRBY=M3@pLZ2H4EbTd+VE$B;k^1-I#y9!dAQ$ zxFWb=4-1lwBNeqqjL^S61sgc!iral@cV;O4FK`D{&znFd^TNp8i**u1dT z+UhnL{AKcS-Fy5KKf~`iKGZkQZ)6p%D*&(U!g-y|ACNJ0^u9Hr{-6CA1#*zDM?864 z<$ASv_aQtKB}F!8gends4dhWSON>LF=mXYy}NV)&Ti0b=XQSP9G&4Uu5lGT@1 zZBsP5yFb~_WA1mo|4{OA@b}-7O^Lyu0PW=I)n}c{q7>8U-Itfq?{P1xij40#bgfrM z41=W$A7Wk$e(YitEp*=1_Hb3H@Wc}3vWMQ-eJw8GrZ2?5djH&6ndq`_uGFnUCoP%zi2>%=0CdT+f;#kDlmxe|}&FKZ5S& zUN?&_=@9sub#}L{n$B%Cv|T%NP397zR~>}+4e>|L!V?`sl!5-= zD^dxaxyGEDknw_1|JS$Hu9sy&cxM?lso$uku~22|)}9HDeW=SNMKq5YhxX?w?HxQ< z&$1uGh!oW%u%_}hn4BU6TKVh_%qnznf@QN<*K4sl(yop@eXnImo0e&X3?Ah`TFoT| zjyufD>v(ib7pedKX!ZlmpfQ)3@A?TFW-L(M$WIjst!e3*41~)G1_6i7%3c) zEg_Qi>GZw!IGe%UiV+eijzFJ6rKF^hHXJ?5%nMn3S)(aEpHLJ^QA|Jh)S&SDK32p1 z`mM8J6j_a(;@%JMa!JCHwh^PhMVCtQyBBZmx_Rx{V!0dnIAmADXyg0t0}4KcizQRo z!lUr>VCM;g-G$m_`&+a)N+m7MgvNfvEN`lO$w6lZ^qI}q4RUU%kTo|9i^Nv)jrSRV zobIbkr(CN(RLNFOY(9l^vA6gi`FZ}!B2My za`k~r0)w|5Z@|?fHW#+dJZ%j6Iju|D;P?aI8yKlCH?Q9KTz790-Fi$?g1Drbc5*Fc zjevhk7Dfq1nN*--x~G8a@&%o&J2>hix@|fB(Ps|-y0sZ zpT43%$WQNb#rig{?}>~x>}!v2x8BiS>QU8l7_)!-dN(K(3(Li9YjodKN`Js@XFkc; zv~6{^bI@Uy6*{h)NCJ{<{fspBa7_>h%iZFvFu=?T)zLM$TvH=*K08nHN#g$Oc^8e# z^4*rr8%{$0t*7O+8-*;Yr$_H%Vk5%yo=GjdOro2u){793&Cdearn67J&(2r^rRrta zM4bGPp*N*M{5oH*#pdiBMJ)94@_43^GC$NjQ36Ng)-}X1Q)a`%8K7|gk|3p)~~?L$Qzdf&nba^FEXU6%scOxyO! ziQB_DoOuFBifx753?tRd#y2BlSLjaH&ymY*_3>~}k{NbdahL=Ngx4a{<*E`~+TTny z5**wu@YqHOQ(Rp{~}kw2q^&J-AJ#`>4wefY>YjK0)fr_vO#>kG$ST>DmA+~1ci@0!|w6NBnZawfWj9yYc{RCXy@IXfQV=>O*JC) zh4Z~4x~Pl~RH??2zZNy8`k&r?`);!=T2xeHY)TD<>gS-)5lr5g)}dKF=083Vg}>{3 z;y5m#(GhyVML4a|`^80tdH+*mjn}2q;B70(OpsM(B;M{AmWjJLy4c zd2~H>@9sXVZ(Cc26n?Pk9ih&;MScc({(}Cl>sP<`_gYUFHy`zgqyyf(zw{?G_9u}l z$~???O7xs0h;q;a9~~8BdcwZmXaByk1#u`3m~;=oD$PmnWvMoSzk4p_I}JI2{j_cV-(ap@vzp})l{W2;t+36i&-dS9$Jl9b0@eMH_7jlGzlZr zJN+#}kj0z1Yux0c+@-*!8Y%nBVd`Ebj58c|dOH8`st;%<0qbXUt%l)| zEbtNRi-fDSW6EvXZPX?ouu!<$v}U>Z?mF%Gdj}JLDalK zUb=)3H5jm~UP9kw)738gG*{!cvfK>Nn&L4N@d~!ar3JAT!ELhpt`QR4q8cl|CeM?l zxMfpxtvgX76D9avAZ&P>@}|qNzmu&PFdM~=7k+>r5+%$pIaUbEr-p z*F6vk>*kuDQi_DSV}lsL$cW#>ocefjM&2q3UA$rT;9J-2N(3cgYbcSo%7YDYPbf_R zZ_{irGA6`)dj73@+c#)K{Vl)a0X)=bD~6CiI6R#nxbU%3MIubfA{6GAP^D9qw#xYg z_c9Bzng#_Sci5%68O{pK&-6!3TV2ZQQxqPIsb8NSTEpMvY>@Lm1{8@kUJg=AJ^E zsshEvL2Qoqh5Mo=bafQxiw3*=#JwdG%-Umi{rr!PNmqEj&WN+l`E#|sM1<4ex|68! zitqJw7(t>P4frVQGdcM-IO_v@^hrU3bZq_YV1b*|fY8BK4SoiW);Fxw6ys8?VJ=`S zUbi7u=UCeM^utL ziE0_kd3R_*`@`eFivK=SvR%h@bW=n9E#MW7xZtDA?#vA$9u8v$a+)gdGdr{LX(5~e z3TtWg5!~xS>m=VR-)m(CM^$GxdSLX7yEk0*RUw(*mlRq|S91z=m%^611x;^wVLiY& zG272CNzW-T{P4vkmO_ca=L`8DW@_;ubk$aANf1BHcmi&4WVlP%#l9Y9^eqj^nvs@P zq5;JiQ}07b@_g$&JyX*jiq&e%uoHv%7oj8QM1 zLT5KCE*cgo`aRzpjStYEz~_n(TS#kuEz5HN&i9Gd7BPVrAxSSCpVVF3jyO>78ddfe z)3aW`{@!OCOgh7GkAXF;0JUL77^t?;yQ?3b{-80@F3crnRrV=*@}DkWg8^V6=( z1N6uWD+=(%6F#Yi47B!=ZXE8w*rU&H)0H;~mL*foMcuO^_mqE$^Q|Y(zbx2+Z*6HZ zb8qnd4C$PHY6V}JVUJ~4zk22^FjD#sG#(^PDXfaU6`*^*v~rRVbu)sNO+eh})L=e9 zSRSUP{AB%<=nr^0q8IF(l0~KMeZ+QapQ9W_BO2hdBNhK~x0t|fRiQP_V<@#bFdIf0 z7{)q+jQ&zQbk zyqo(Ea~HRm724qpZR8ai_NY%=vg6sCSubMr-+WM#z+(fj0iTaqzQ20X9Cy0z?$V9K z{4f;Qyz6mis~*pqxzmM@VtK@tO8(@7EO0Jmu}AbGU)5)-_!0@hy3k|?+KoGQx2R%I zcPf^5k7Mb2(U0vO^Qoy4o%VluauU3<-N!q!Qf8hyIA|`G=u(2uy=hBoM`!V4o@Imq zzan_3=5y-2k})FPqV-0kn=3z7XuC0&5tjT8cZuaf9YsSUWEk&w*RT%I#$L0_?xMUt zj!upJ`QnX6T)Xt^V-m^lBfH(#TM!+({tBpuD*9F5z`$HsXul&vpamX9Qhm3WkBReD za&x5rCUj6I+QJXOEK`WeL~5)pcwb)siaMT+);->-rV*SWYwc0uxhQ(^MKT{FI;IpJ z#jRNC?$GM2n#JT97Ut#Z69d++us-kVZWl={qPu9<(4&&FNeVR=X=@1u0FCCBeKr+) zI#-fsgvSdhlWl;P>zFyI7u1aM)oeGdX;6lR!WNiBP3ha!=%MG<>0iPo7K)N&ux=wE zt;DcKE~!#rdK-GRv)8wEd?zawG6<_7b$)-COC=ZV#w9IkL0OzeTlk@3d6#+ zAf`Ztcb+4+LNt=Zsnq*r?wwZC+l6YibZTG;v?t1lhOGB+O`t$rk5637$ zMWte?_%r(AxWJ%*U7^*0amnQRgvo9P3_KAu+RSBd%uDO(mDAD5`b~1OrHvW8gV^Ux zAywYnW4A0HQiEHf@lK-o+K86qU$~@9nWvG=Z>*sa)MUF-!}*4so6V+FPc)fD1Tp+t zpv!WbB>|U~tjX5R^B4Tb$_kb8V1f&4DxR-56w?LCW2S*k1!KSll`hkSr3ROncZPiT z6uSj#F=C-_AF&y(#&y8!x1R7zB_sEwn;>!Jo&i|$kR=7~j?4Z?pFk;#&dbZLDXh80 zD}v4w=vXNwC9N&zcAPcj@yW^t$2%|tr6{(|*#279KbroKQuXF%hEDY!P8DXj8YbsY z5c1td3FGP$?xs`dMB~D2-;>^O91(c&ptP)VBJ?)iG{iAg-w>uu}GNVk}L3mf1S z_Db&^K&t%2zG7UYbt z=OZLXNiN@AgNpRVip%4B+pqog!O5b~*dRw^iq=bEn$JA8r#7uGcxS&C=QxfErlvYe zl?zAbjoKv6XLc9(7TN;8U^VFKsj;19zsuQmBi^pC&hQMRTY*ENeQ>3yjfxI5cqQzns!$-I~9}FrZW4t0^wAvcDtQY(`@rs5X6mH9Y4Zhc+ z@@ti-!h%}lVH*xhjI%XD5OaUKJ=YIcF;yHF(!+eID}E^RIa?)MO3Cn;5`yjIdf>7) z?WNShz3d*$s4hHND#dSETPxLtu>Vf(L4Kc^$+u54-+uoq-ShS|>Lj{_?It1}tifyF zlZWn7Tz2e7eWnAx|E04&v@GNK_T2Y8u7z$#SJeMOfTDiJ>b$M0^M%)U1zGTeH4(y4 zI3M|Esc-5rXdZ){vBLCFV;b z6dDgF%lXS0X#$&coGg}`_p%T(@u{Nh8-5IHK)E9YXBTJkxhiCEqxDmL(@0~TJ+Pt@ z=R-`iPP(=p!KUU;Zqr}i+J(QO8S~`bxe&Lph5*6UFs5ZkXf(AOl{Is`l9V)b1+wo# zq6zu*-FK5TbnW}M`Xd5GyvgL=aHi)OJ?0{p<;RHyYd5^9W(v&JQrZ=<#9DK!E4Q*6 zU)>fJddY7}9m%1aDNzH@)rb9x)JW0N8Fxt#_Cb}pCQPQHgSGhZ)nm|5nk3neaoSMo zliT=!w)Vislqp=yuvu)afxzNqm9<3rMHRZVFHs+aQ#{`R-<=vnAz~;SE8(5?hDE~7 zPZxs_I>_v0==d;`&?Xs063RL--L`*BRklChMmIjSE$6`_2N1YK01)L4GU`?kO-aS` zD<;=V8L1lHd)^#3Ki@2q&Q$2NVoyrC94}0^x}GI7=!gcUXp5*9eTbX@_QC6Q_DmHf zfg9*<8HB00H-g*r4GzMmT=d!;f5{ePmC9}4> z8L_Y8W@$k*_K(-L+-u)&<6IEQMj2a=hpE5ud)-ynHp!=R>Um$!2Ex4^0Mm?H@qFjt zuKB)c4*dRkn4)41#W2&|pXoKs2t29Yci zUJePzU-Z_b9qp=<#RfoNS_!!@#~!}YiD zTyhUPye3!KVZC@;FVBxPQgK~V?{W*n%|GuE6CqJ-WI%A1K91cf4q z|8H|ZJ^IBPrqNcxr1qTCe1e1?hIUdin)L3+SB|48S*9qgCo&5)g0LTzblS}eFXzud zvTX3QkOUf31SU%G<-wi_bZ9aP0rt}|V!s4YDYbAB-}P~70*F-<@V11J$-+5GVs>e- z>9Ew45q$_7DolE*>}T5nd7_&exJ<mr%XElsJ_n zswVP`)Mt1+EUN-Lxc4G+Q5ZSUrdt_)fk%|^;ON?N;`MZ8T4~b8o0vaz33ZMS&)xKf zqs+SA{ZT;Jn)G~BGhx?B%Xq0o3B|iw0vfcYh@}M{u9>yeGe)Q%P5EGp0_ot*zhUR8 zz-!Z$&Sgn_^aEQ6j`T#0*SdVCq7!+%6vxH^W`7lX%meY8U(E=p{sVdO{={ope%@)9 zQ>p^X&@BiX>yodk;DRZ1;NwxbumfyAty}kfeeQ-MwqB~I1r`LoEbX>FYze*GZQqM!ZIx>&Y_1TEj@k(fL!`=ySD8PCS(|ILDbrw5|xK&%e5i5F2d!e_-Qg)SKkY+KZGV~ihTR^Z=<@T1C-!mSN3 zwxrJLM@jExx@4V?f~D9Z$(FQGgQqN`tnWf@MBK5WN!|O@z>bpP^;#QGKw%%l^sWOkF6&OF>l!Yb zshqNMTn20n3zyv*67E@|s=}9a#C|fI_;!da_87(778S}nc&ydnNdnm@vR!y-!EfPH zsReYv&n?rGD~{^BSNssq89bOrXatT*njTl*OSy=8<`Sq751kMP!RCrJu~~eI7DVe% zCv24?vkq0l(86e8;K=LusyH=|=3kE@zLOWvXL%gb_BDzp%2v(eH&!22w%U#+GfWpL z#zZM^f93|KA;g{iyv}08 zVyz!uu~8$$3ZsK&5G*3F8AejKP_S}ZC|b}n#WNLHTQGg7p{_871b%$E9}E(Z^7mux z4pJ#*vXPE@zZBel_WQ0}rx67^$!IWowcXQoG zx6|@>8$v#p$aJI{%~I#hi1eC~1d{B9>8cUqwrqbu<|%uZ(MNQtWB>sesj^9x+ACKT z)}BSig(5W?H1)yn)W$+=G@y($TNadfvfOB9%~JFA#k|!TJgJ~F82gwPBPdEUibBY% zhBZ>FkjcwD?0Hnt*y4I-+dvh(mErhPtKH+8ZJgh6GZ<&UhUA7=d_!L93w+xbHwOFZ z^ZXq64TE1TtvMfpqK2h`hXhlsc}=PnwW`#pzjp3wRVe_IjAn96++Tl@Wk;BYGVTp1 zdZ=7RHp9NF2o6B8YysgvHqC9Cx|6 z&wuh2sQ0qRIM3Gy&6EIhEtwb@3#aukF?=YYyb(xB{SCxSO0Q~rV`B8L5WJP^*wG<= zK1LBqFif5;4E5|Bs{lg&7XUVfBbYPQDwk0Az;rwdxd1ogXu}Z4^|-2O>rD`U_ZloR zK4)Y)Nm@d1aB$v-sDRJhPWux90Rc@~f3zBG25lq)Ze}j_Hv7la>x9&VgW61#%+p%I zL~G%n7)p&KUv1R^zmX1+hKoc#eD#Y+j;&6U)1XCRMXr0%!qIPnsz7>6BMpL-Cd3*~!J=oXYnZ4m^&y`AJh>)yQ> zBo-PYp|Quc;6sqaksGZ)doNMMXS3b4LZKu8We#h&t)`xiJYL9Kq1`|^ilD?@D#dC~ zfy*Adm+7j%^Zu<=*9^#Ma%mW}vDc&iR&^bT%BD@96Vr`w!=AapDTO?j(1oPyqchja zXJ~@Gt+hlB%bBL_XufRYKon#&rtLyy@5`+SCZE&hy=10OKEKk;1{jRTvjoB_znC*h zVRJz#T9W!9Br~!J@vd6n!u+E2H-<0PnH+@9q9P{3EIXYS)02!B>0_sebq79Fs&)Ox|AH?hRI{GZWznpBP>HWuj*c({H{8Bn5^*nx^Wv$^&rPc`3MrT zEk2S1tE4!e)JJ=NvS>=5PpH!>>+9uB&YKiI1ZVBH`ungf-Zjf7HN)b{kpjbPEvQ|X z@+ry#3uzb9bHSvDr2a^#N=0Wf)4%gSuz$< zLj__;P3s{m!oj=M!p2a2h3$kJTjOujotlIR`jH63Uw3SgRhLWHQvf!=vZtSB85>Sq zY;$+C4y;VkF=ORmjfz%qweT3WkAy^$RlmtoI4V$BO#w8u0B3h)CXn>WpvY0X5Y=!- zQ<}WH@xtjhHdntwckgcB#8KBN2(PA74x5T~SnqD)LvNYLf{0w)Y`xu-+u)32# zsvmJmsuN!&Af&q0kP|UYm}Ri44!Y+dJLW*>L+?V=>{nzr4doR~I#hEp=bZO8giADY z2o%;f8xRJq@~u^zEJ0$JP)yoZn#_=95$P3+mjyWP^*1;`#QZcD#(DuPCT$s5C#7#n z;-Bs;6lvAH>Qj2rRmiearU~^O3Zs3}(b=h}{N>QQ`V=yEBct)yoFxi8Pt%8$!Fm*^ z5`e9b516haExE@0<#iJuRfj_dOA(464K$Pq>7z_=-+J)3;UJlmrPbwThT9t^uaIQt zO%tv;dadsa=ymJAi}Gv1sn!qYOAX2?R!b8$k(^{yHG zcn!5O!~UybRN+`eJ-NpC5?O^n4Fs1*zly^02+kf}7>U-M&=^{IF@PPs9#Uh|v>tB= zful!I{g3vT(~I3HsC8}=!vfV+M9maQoz*8SD*-om{LmZ&o%pixrj~2?UKG`pkWE+X znPO#orh>=(a#)L%CKexp^YTebrqwRT2Ct{KGUmH^1#-(wpb*ZYc2Tx@kvF?m02)Hq zifqa)1_T1)Zn0KXKvm*W{=jY-Rme0HEviP6KcWD`ZpuOk#h;qSGFl#vG(%D=OY{+N zLB|z|ItWP(?b4>bdN{o7|8h^VzMC4yYt(gi%{s zX?p3E*lcS0+YR%Cm8Y-#{WPfBA@BJaM9G(V$|~Sc&Ljk1oKRa&bE;N$Jj1~u6cX1y z#sLh8KQ(Sb!;Sg=wb`Yyph&1m5s7Sd_&MPRhlv2u8kGfdh4dt&z%8aM2P{;1?a@}6 zSP`F5B(+YQ6{!0ASA+h{7Ge;4JYB19>;1yFAkao_v~w(mB0q~kDZOr@4aWt44|Ayf z&st`{uaYVAsZ2Q(5B>#9!$%NJ?nPJuG_^#llafgqEE&YsfJTN*7X`9_&Tpc4G@cP4 z?JV~oL;Mvyn?jTjK-c{uU-O3#c# z{wV75btEQyY|?<+kTy{Rxobit;V!HBO2RqRNre+*nk5UfA#^!mz}xSPbavdUC3VDX zP~@C(pjvUjcw!5etqiMy%olQsY~{XOyFbq{5+Q`4gr8q9s7pQ7*Z|0Q{Hf--nY0ym z#@PntJtUy>H+~!WwnDRGPEgUXVRgBu>IK%M5_}L3?j9>td&Vi42pMr)m#V)bPIv&J zcsi1@swuE0nb#5L#BMRN&N!GNmJ#TtK+4@MI9&}Sq8j{+He*7`rH86zdjmgI_M{|I zhPa-T79s`{<*7OP2C^{6A~CI4Kz~?QvlmB9t)MC`z@sw*pyY68<*^fV%|uw~4~v$R zi;s&76tVE8_c)>4)(zUBuN2JBXWWMi)8A$}aWh>E%X67sCBL0z6jeQ4X`Zge<9`=6 zED|{*QfOWjjqjKSQD+of_xYXtoR&C`@W3s?sWPa)jZ7RF> zHIrNAN-b>j?TJZqAFyc!A=aL9-UnQHH00;HHwV$VdLK_IKYo=5PV^t4kEAKME)q!0 zWjcbYk8^+W!^Z0v_Ukp&e$q{jJvyI@@%%y!iS0`S?cB3GVS;yH3B&9O(r8v`L%$3n zz;^-A?cUJjMp12xw8+jvLm-X-|6c_elmHDVy)^;I3}4gQOjq&*6WC5f3zS1rs9-dElV>yt5P!>%E%;Ty4w|`)Iuz5g6z~C~D*|pxug>%b zJ2nH~Y7R}*F0*Gm=zRGN6v=R3nq0`dFjd*;Y|A-aaEW7SdvE&Ngdt5kfs8(kua*{4 zRSG0(%4l_c=zd6Gx#p$RPW~D|lnZQU)ZIo}Kq_1?k$%cu*=e`hDu;V?B`+V_FE*$r z0+HGtI8-qHmsT8NtTS2$q?$6x6<6IAR)~QaESfA#VvFG(|7QuiwDe+=;f}(HYQ3&1;sP4Cqzu_ev`p#w39E!Xz?MvbCc!{e)hmYMRcuWX|f;i=tHI zQ0T?s0AqfB?^I&)deg6It?>dho_7=x^-j~YKdN(Xu(-tU78n&)7N~`e7n5IEO1IPrj#7;~M>4x2NTp}Sm zH5k3b))uEmL;02>SEcmW?J-W+1!3btF~JXR@Gdc6$Seigii_5NhzzEj-T6$EwGH+Im*exHhE{Fwph~4TY$tE+6R>rhc!NVUqiqzB7QgKnUp12*%DCIJfG|7${&>S3w0I^6O$q>6NXtFfaskb z=KjQxqaJg__E4!~Ab`r{FiVntTHMK%Hpg!SC8;nm_zPrHnUWOFA~2F=aTbEH#}9G- z*0lF$&WiwS-I{h$owcfZHXj2iWBLkA#Ov#{8Bip*T+G|>!W!*?;(tH@J&%I|3@SMq zTG~lXiTK4v>qUnj9P;hRG?eL{$-wG5nmt@O}c?4(2Z!8_7sd$ zVLM3b%$ZRbGB)rhb_m>nS!LRu&zI_f*Ewo!z=+n>ilmUmE8{x*Q*D}YDR{&B`yS4E z-FT6621#e8{Q$0g*+RSLtxe;q3j|T8%Ik~N5sjzrgcu2H?Vg`pSDhiKKty^$ad9yu z2E?A}B3}d^>}>+E7nblL+R6!%77(Er<7CFZw-1d?%Go_F0s_odfX*%F-UGv1_GK(i z2ocBOfKC3(^%i-uie-9PQk~{zHBzCnBa$m0mt4Uc+fFue?!%NRU3I4rXpjG}IBzQ; zu;G1QDFoEpCb0!FP(wgJS2bkBvHl1gxWMivi5!=w>=p0ZRr{*Te0z8>XUXWcWD(zX zB(0923Q+TS#=d}!1;l?>%xI7ICw3eHr5kcmG(Jj!Ae7(InJnXgWrB5fhs^6UM$ZwCa9PklqeZn8oLu0g&gOmqvgN9PhL zr54MXfFOEIolg#Z8c!#U>szLFD@_+I;p6vv--sbbDqa+TNXU8eK9^UtsFjA~Nd{Pg zJ`@>8q2HJ`gIf5;{{S*&goUd<1zMU7Z+aIr;HD|DI-H^1bNCA?@jBAXaew+#U$Ecf44~;n5)yX+2x2 z4l$QI@3tbEoH@)VHcE=JJhr2_%tLWgVhj|wE~fjva1>l4N5BiproD0^0OfqP?518i zBuH>f6Y;`OJ$mun#QTZ{4IOTxDWoSnwPhqKW_qZ7Bjn2XS&90x8y@)xbB)i-H1?-X zqdaC<@ByorDSmL*dTE&PE@udWcYmTLI)3O(Ir-L`Pa0nR`T+q4dG2=-wm(8K2ErnX z!v9G5^KxlI`jfT&->*M9GY#LcavQq0kv~FcEJ&PE9|r1>KEz5gnJb^ z;rDl);0_q^f0$)b#PWfh+;OM|a#A=+>>1i*fFG#*+|df^T#YvO9~ z%#ADPld0>^>eOv^UhAb$0%i}JzDq#d5_6AdSa<{{1FrpkaP;WxZpn&*S1bGrj-%%r zdl%~Z;RKc_geZf4P|(?$t&rjWV^221H|;f8FQQvELPng`Heew^>49_%HqnR8>}R>9 zQJ_^-JGMP5T83(#^0(O&YyuP!QhXiE@QH=*7m*zx=A4=S1BLaEHSFj^TtH2;$rnaB z0I~;Q3>KYYgA#oR4p^%M$7CO-7^3cduKXiZM_G^9ei{A|~_J#*rlSGlISKcbXB=+3Q`IH|lVVp_C`-ucEZwaHk~OWM6wajjvyV3$uhNikYMTc-=bd@l07L*kR0EXb%0^5OUtX|_l+IlCC!iP| z)nOLIjTI6B9N^p2RU77fxp8WFuc_$;T^cM!@QzIi;LqeE`;lpUogE?eWFXGTo19pIoaVG z_F3aOy|2rdrk0LU2!+m4`Rxy#1qWP<5rR3xJv*2heOHm5(9WQ(IRzk}ErB#`S|s-pK57FXnRq zJP!U6z$`nSG>1vK)GrzBd6qG=o!~k z0uB5`#kBX{gDvdj&_5Lp$h+dV(WR=TsLRfBojmLjhsTDI%9CjN%jBF!wDUr&hPrwn zzT3)7P!TkW+lY4i0tqQ5dX{@fy=<)^`@oC#84Gv z9W{|JjR7)TY<4tTVTY#(W`|D+$fmW&Fh0p|m`V-N!;@gKiW*J|KX6KUPd@CEnkpQS z(%7PJZ-?%@lKMUcUag>3^#(1$C``-^(D0rhvA>jiuD_#FyC5_?ghdt`0UVOdZB8-c zdfven@CMHcW4Y08eyDT_Ygoq6Z(t;T+?H?C?RNW>PPapgA1sTC_uEXJ7h)ks7Q7eFVt=a-vmybm7K5^FY6Xo?QcS(?_pF5*I>|gv)QicmU zx>dHKa8i>BOsVvPZ6NgG{ttT zey^iIXD0ABhRmi9VqHh`RiD0t`1Oc2lp6*%qByNeftDv^ z*)Zwq*6W1>AxS+IPfL3S5y9hl#NJrofp_UkkjlVSQ1jAhd6t)6I#E+7~sL!b1x7di9_`KiG4?dr_AJ*x^8r z+#Eji=u&`rfvNI`h7u*Rq=k8dunB(g!liV4Ci(=UR9_pc<_D$|PrH&P#=11#1`CV1f zUeP1>^MO~%a4@QZA>p}b=UGZthT2qRnO$#P@&vJ=oEj-!C#T)oQ<8>$8lNj;Zhkh6 z_#^>DM_)NSPY>AtiifDEdb6Ry2ejf8{uFviiV>JTliO_MQR8_4-2Ayre|lVrasH2ODocV!E|YkROF`UYPq34PmIFJTdUxi_BA@c$Wg1MhXN zobmKk@k{PPr?0Pd?D>9PjO|jr1>^`ui{#4O%76^ao&L(sZx0h`oDp7~s#6KaB0k$T zk+w^R`Dzgf*^%a$bLTb}-c6&dc22Q%V5c#4kR`v0x-X4iN8)0obIXTqa|W zsz2l@T4}yPNKN+ZU}AOmi8Qgl$12f_fy+U`aOFQkCA>Dw7^~N;YSC0^K`>Wa?B{%o z-Kn&-c>=(F8*DYdZBPK+NTB<8?$r0OXbWj4&bZ!1y(PJ%nAH}0SoPO>8)fxw!v@zn z>|4TGLi?bTdeuIw%I}TlRBCAYDyR~Y^9qI$zjhsQv~if6i5O|$hMvD}L&NbCG(!S% zey{p2^L=M}$2sKjWbdm#bMpFdeYEbrm)U})(M)o1Zg94>YZ$TdfxQs`z$^IkzW_|X zPvd{=$o-|WA+)^9u)&h-pLaC>j&H>9S=O+T;~{;aGv@cu_IY>Uo!P1{3FLH@ReQeS zK(U3q&rWw;xXC-hb*_8=sCEh{Z+LJ|(eAcr5}d&5g(}*@Mp^IG zGzpU@xJ8=d`22PoZCX|pr-X8tu}Gs{%vkoUH%43NS+}?vt6m0MWP#H9waWDb_EQ34 z#_gJg&}*AiL>@{IuO!xrb9&xM)Y|ZL@Sc7j6|@qs#FX$torm{#llQWmcJ?TTXZvgF zG<1==9GCuQ%{c7|g-b4840;Z6C?`P^p69V+{fzM4Y%iu2dVHmt%P>*0ba&*qGagfC z7K;qbrBfNN;nxcM?JYu$Jmd17a2FA2J5}j7El#xEx)rkpqX&$jDot_b(Db12@azlO z7vzduFVjf{fGQdPucgspO$1Y~0k(->Lzr4b@MG~gZ{tk(3BZ-~-$G5Hi3yA*?6XsR z#29t&xuEqvejMDcNAIc8b&8K-Z) zF6vi>eg1}~W1ZGBQ7(qAQndY9zH{8ng~+z~`d}U})O5XAK5CfDeEp;950Dbo13e7M z+fm<1Nw{`Qi9H$55d?b*yObn)Kd6zD?y0WCN!e2EqF$Qfk%B`&qVJmJtnA7=E1FLC z)+)an$obN82C8gureA777N1E+-W!`01|H;GU!~0}&4m%^@}$XJvQqmtE>vfa8-r)f1PJwz96h=g>y)M@2dcL#< zXgDb!P{{E(9P74T*g{A(=M7(|s-j#xcs};&@pAzawU>@8fVh-v)!Q-{Ft6-TVb!7G z!iUc{FpNwXLBHuLlGq6%Sfc$~lEw8(_Hv6|nb4E_rt@t9^?&w_f5~gHT)F3RAB3|C zm`cRO9J!HG9l|%v5!!l`AExoNz3ZHof5wZZhlax7l@H-#S*8RCGw-?*+%B{1ej|@d z@i-LW;mJhtC)`I@u*yIJnA$9NzJUfZ#XWFO9dY$X?l*SxQe@CX`e!Ef5n za|p-bCY+t17|76nGY`$7PIy&j_WbcVH-Ne@elW_^t^BLucLKUaso9YDjp=DBeAJy^ z6U{9!E|S;*Xe4*9AOsAIN552gj*v#^#T7q)ve#5uldBWc`Zb)d;k4C5m(Y^CRt{Uq z2f4WPHLvbAm&}Gd0DEL{_vsvbGJ8;51vBRHRFS{)f!mH9XJ3F&r`yRdH0CF~k6dfC z8(9&l@x@J6EsCub=)|3T@V&0#1L;ZIsoe?-wpb-JgXF8Tm6*fjwNQ!FK}*pD&tRqP zV7G87Fd3)%qnDRD_M+<_xl7e@6{>wt#q%YZU}U1jKU_8(qiM^oSsgwPtlLe_Jn{M3 zI8BpF|IFLFguvpb#O#nnpSZ10TiWL6BCWK)Es@j4$#(^h)~c&U&a)vb74Yei)AYqg zV&qLs(|T`M)X7mpSN%0vM0rpIjM-9Ge}_%rQGsA*Cs=DzE?b`9)WnLtSdXEvB5!=7 zs_8NV8Jb+W=uhOI_mo)3`q*T6YPu$&-Y}(@2kp$i%^r6`w@A75@Q);jC3ZBDAKv-W zUI@mHxmh3>rE7^yt_(e-I% z1x8|e%Lz&7gAp}4v6`okUqWkgb@i<=Ja=_nV2{gMd+M5VI>9$RRwqF6^K)TY0KnlC(FCmWSr z+p3@rId`dw;4jCU^DbUPG zx^HW_wi|@SAY?dOU5n*vqMVoF8ugL{nJXnli9~nsKsl657B2D)ZU97lxa@GP4V`N! z0a04*QEhGG0@y#P!alukBSevvw@{NjdIaAgP4i-g_9SE8_}=C7?K~Grv7?!mn~w)S zH=|7&Wa4%8>x3(lwCz3g{&-TqUCjQbz_07_oI@;p=Op*Ly=R{!7N&6ONSSS1fo@pq zy68#O|D0D0y6DClc*+uB%tGSo&YO$@kV4qlq)d!uo;E6?{UB>C!b(zfb#UeDbLaku z(9?i&M=GOu3g%gyS@WX50<{(-Y!Am*-?yIG8jpiCWI;$zZfLGt6?8N2Wd^*Kbk@|b zTrRIZ73U8ZY;v*FE5-LRHkZpdrJw6RPM_v6KJ@rryG2A_5iJd>cpcz8B$18O`J@1v?Waf zPE~r)r7GT+LE7Pb=Yi37ZZfXPuISWe+lrHqL;-jgGFC7D8ph?B0( zn&MBcO>a8omWQSohD$N@OOK@lW={0kqM-LKLpK_z20hf$xkR245PA_8urMmd5ZCfa zUfutC*z62(Y6L{N*Gy;E0z;0T(q&`|z^;l53GVHZX%1!2b+&fwLt5FF7%kh);~J_W zML4+H3<2kK?I1)rpS;ve@6F0qyPk^i$}Y(#;}j-5!A(UJ^(MFau{ciHU%*KBeN@%x zeahK-_hM)|Q zjwQs6Oc6W*_)Aiv7T9^O%A2$#ZZ~DqeX$vx!c`1%Ni4S8P9?aIKUy66>9d_Y^R4{TnQM= zL-I|!oMqQZBraLJhQ3ys-pjpY{1R{-rCpZr8#^~HSh{Z}3hvnOK_iui{&CiYI*I*+ z8v$b^{a&s*5~E>W@3~eORHiMfV)vsnWJ=qqr3jjp?t5}RdNL2#$XH(EI!GjH)6Z=& zO)62D{4#yGD8Bpv@Y@yc;8|DoMm>8@SY;E7q95rBUNXRZUdi(>GDFY~aRj!P5PqLJ zo^}aEFR1^$g?_4~1jmpX8u4!zVe>a{9G2}U$<(@Y>bcU!SZ0<*UEGm$Tf<+7*CQxS z@%zcw#8H?qdN^Q@iLzA)1y~mKW21X>HSKN#Ube4xI5wP3NPf225Sx9^`8Ct0+HP8A zvr?sE!^i<*v5D}SS^kpl|4K}1M&)LL(66zndNJ$3!s;Ep*=KNXS{9C8w%gVI&xBj7 zMM=aayoZYR6Dh;{32hBUgo}&=O~wNzKt4?EtU1N@_eykeq*_C`asBp$HXrR=-6!Zc zqVEOKSmc44&tn-7dRc8Dlr!A{xpuS}5C2{b`^9 zw;V>*b&RdNxNZ2YXQcIOV?G0lJb3raEW|rs`!pgzx}a);s}i5upxOfd`3)8qeldSV z5qh@STmH+*6=%E;C%=jPxyd&=sxQ#LQKpBsC(@crc4*aEAT)kuK)E$6 zq`IhsBp=(LK;_dmx5V7XdF>De^};HB+^Jqopz+a^HC}XtkbsYi%gf8JE$2zZnNy+G zy>_C0@_i~(+tC{SEDc`1fXZ)MHc4?7lGVYI$G)!Lv!**FEoP12CceJA7q-8?XX<(y zv~p~wgUB5XY^a}%`*e~ZD5Pb`7JTwc6wHR`Ps2Q}iECL3OXeDuHi6SuDJ)5OZEaYJ zhhJzdK&iA$(6}~dX?M2NeY=yNtuP^=#p#?!m#sgQ7UXA8NQ!Suq&2JpwVWVZCV`Kc zOeh2q`~{i~$DuqW6vCQsc`I3MJ)O9(30_sjbq5i?mLLzUqn?{GKT2_@CMqU%UXT_R zdiHLrP|)u<70x!xEZTs3Dh^sr0@|o?!bur@`%9>8y{nb4{tdL>)6xnHxGHyoXf1wcw8CcbJrKKbEwwH8w3T8M9Wu8 z_nvkBLDXxyrUpC_`>SbO*zd$=f8L*NF1b}%#S;fum+@_>_}bnKdc-iS+l@R0BpD$ONrQ?=v!%b=!&{7X>@&M+CRI8iN<>fj)d z!a3Fv@2?T|AMqb0VCYM3$p|V_%7&hzzbW(}LCQKF>P6wTD9y-n`{PwoBzE{-R)>SZ z%_^t$3CJIvX+@7u=Lkaa)t8l#zE&dqF;!i-@CqT1t|-4MV^gm3AIDFxNb_b@SEi@z z2WX|lyBS>HEZC&k#{DPb0TYV}JP4@V)YrKd`*hC>zRMWNP^%aE>S0hOBf}v{!LtE% zDv!qn{n(=LJ%*zv>Ct8BX|kAxF)uZrc-k)#4|Fv+zkdtx-)>Rm;tJU$^mSLMAP9fj z$!_L}l^BguyTjaXRqtwp>rZ}en3{a+UzKSY2D`g2Q(-Z>dq<_=aXdP^Ul)4uWnih+ z?jT*yUwFkPX;x*Y$Z0dJPAq4a<%d*G?}NTcV|oSttF=tLbM=ThA}?AF;&Y*UNoL*; zHLeI+(BAB`7hpKB3CjKIb}7O-lOBwWjI-~+Pc&_IvKc%uoBQ#;X6zs=_Mz$d?m+MM z5a2+0T99pa;$u6wFKEP`l-Na4eD%6^s_N5W(x=knE&Dnm8Q)W%4Zb76=Nw5smb}4v zglMKE<5g>C8(80Z1*uIz<%- zAh7vg(hb1bi9JM*j5m@Q7^X|f>)U{#F3Ime?xVxI#!m4);%f8LBmScc(Sps@c_x<7EEWUv z3k}f76W55TGJ^J2If4EEMD78b=qIUAnH{Itg|;M56Z7f%@x&Kx-V`=tLGHbIj-xe!`uNoq1Vm3-sC$9 z5Rsn6>Qh1Ml-6qDxBu4^mSBG;jn0t~LT0|1_#^7qe|X+jfc#&EaF~ zz!i{ou5Rji;#zKow=!8%X2bJ%`h+$5VK>Gpp`+AWU9Sov1(Fe`noW z$P=ZzwGWV5GR4j2E?-?mOPNisbI3U(0E1JJj9128LX{;>id1z1E&avv_XBR>t5;$X zH)p+Ntt2%&{ysp5&#|;rX`157TPzp62>mg5;Cj}gL?z}DMQzct6K~+boxbnngV82h zni;@QO!4l5BuH6yBNIzU!+|_b8=&tk?p_aJd2QGsdKCWCV z9{83S@@9D|WUe}hs;-vD>EHD#nP_se)M~F(H<#CwHjFbg?{yM#G z4u$z2W>ON-hnX3}?bzawOQ0H00$OYFo3H&b#G=0r%cqypMRl(B^9^c2c2vvVwk>&e z_rP3f{`%{mzSI2o0q}p~&^0pfIRxx|KiKST$l1pm+l(p|?eS&|pgOBr8*Qce);8vK zl8om!`N7+7pDb$Yi6n3h_@<~eo|iX<$m~VRPf=<#E*q|~Gu**9#s=D>ty3abUtgLn zQc}VtuD#9uK}8 z5kpbC)VP4hA0*ymUd){Oqqxw22WY~K8I=jdB!TrR^Q=ej@%Lk(Ky};rGCh%y-zcuU*1TrO6N*(AyDxynXH~Ni! zXzeT4MN9`SNuJ!_sF0p=X#MGIN7vYT$>_zYmhh#C!c!O%NDk@}Jt9DI_)?SQD2y3} zw7$b$4VvSK4FU3^c{7z*8|u(aSF%J8+AGyHu3n>eDpVOnYN=j7D0)?T7{iG#T(?wj zQ{8jQq5R{oe&9Q0FSNvb;~!6OBGrliPg>Fz>cE9CBS~(5_vm76CKb*CnnqEAxEA*ReqB!>I=)Ck+^IZRKo5qypp1h zXd=ix9C{N?ooz^6z!&=u7-cM5&>R)gni#0=y;J09B3a}nWPWt*3=Q#r@rkzB6^iF~ z@e!~+FRcye!vdYTm75P3e42rFdl(T3khs&-*Yy4ool^BUt1vsPXl<|3Vm`{{gFd>?;qR$Lv`q1@D<{FW&2M|4=s{v#3?>y8=@~&{i9iwlZiuq6 z#VX{a_e+DEEBqE@Zrj-{DQJf==yIC;jV2?QrvlXv&aa9mee(y2-(FBRaCgoN3x@5a(}#AqUuql4^068H$ax!?nQ-a$ zoF&SP(pyYTT%^owhjwMOEY`nAirfjBKdTD>XO>uRiqP4B;{mu-l@` z0gBTAC}7*=yS&NTvwywN{2QLLn=;4`SifE79ZfM`(>W<_SF|VH(Bs~`XyBPmWg1&L zV%c;j-LVlTqd*#3D$pga=V%ue%oEfQO+gMHHzn6r_a?AQjr3|n+Rfm55qBRc57h&6 zOdh#9D4R-nry|FO?rA^PhqmYw?7Z0hHOu>vVBmA#PtSkM4;ugwy9k9Vi{HTeLSRXT zG?^Q8;{Cws(uH)N{O&s64+Cb#6D|Yt(gfJHy?u@UK=p+&ON|T)$+m^vNjI^9hG-F>U=JIPrQ1pqAXi~fU`Joe}N7tGfGfF=j#(Et9oJOB5GLgK*n t + Nemesis +

+
+ +

+version 0.1.0a + + Slack + + + @tifkin_ on Twitter + + @harmj0y on Twitter + + @0xdab0 on Twitter + + Sponsored by SpecterOps + +

+
+ + +# Overview + +Nemesis is an offensive data enrichment pipeline and operator support system. + +Built on Kubernetes with scale in mind, our goal with Nemesis was to create a centralized data processing platform that ingests data produced during offensive security assessments. + +Nemesis aims to automate a number of repetitive tasks operators encounter on engagements, empower operators’ analytic capabilities and collective knowledge, and create structured and unstructured data stores of as much operational data as possible to help guide future research and facilitate offensive data analysis. + +# Setup / Installation +See the [setup instructions](docs/setup.md). + +# Usage +See the [Nemesis Usage Guide](docs/usage_guide.md). + +# Contributing / Development Environment Setup +See [development.md](./docs/development.md) + +## Further Reading + +| Post Name | Publication Date | Link | +|---------------------------------------------|------------------|------------------------------------------------------------------------------------| +| *Shadow Wizard Registry Gang: Structured Registry Querying* | Sep 5, 2023 | https://posts.specterops.io/shadow-wizard-registry-gang-structured-registry-querying-9a2fab62a26f | +| *Hacking With Your Nemesis* | Aug 9, 2023 | https://posts.specterops.io/hacking-with-your-nemesis-7861f75fcab4 | +| *Challenges In Post-Exploitation Workflows* | Aug 2, 2023 | https://posts.specterops.io/challenges-in-post-exploitation-workflows-2b3469810fe9 | +| *On (Structured) Data* | Jul 26, 2023 | https://posts.specterops.io/on-structured-data-707b7d9876c6 | + + +# Acknowledgments + +Nemesis is built on large chunk of other people's work. Throughout the codebase we've provided citations, references, and applicable licenses for anything used or adapted from public sources. If we're forgotten proper credit anywhere, please let us know or submit a pull request! + +We also want to acknowledge Evan McBroom, Hope Walker, and Carlo Alcantara from SpecterOps for their help with the initial Nemesis concept and amazing feedback throughout the development process. diff --git a/docs/stylesheets/colors.css b/docs/stylesheets/colors.css new file mode 100644 index 0000000..f5a2f43 --- /dev/null +++ b/docs/stylesheets/colors.css @@ -0,0 +1,147 @@ +[data-md-color-scheme="nemesis-dark"] { + color-scheme: dark; + + --md-primary-fg-color: #262730; + + --mod-hue: 210; + + --md-default-fg-color: hsla(var(--md-hue), 15%, 90%, 0.82); + --md-default-fg-color--light: hsla(var(--md-hue), 15%, 90%, 0.56); + --md-default-fg-color--lighter: hsla(var(--md-hue), 15%, 90%, 0.32); + --md-default-fg-color--lightest: hsla(var(--md-hue), 15%, 90%, 0.12); + --md-default-bg-color: #0E1116; + --md-default-bg-color--light: hsla(var(--md-hue), 15%, 14%, 0.54); + --md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 14%, 0.26); + --md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 14%, 0.07); + + --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 0.82); + --md-code-bg-color: hsla(var(--md-hue), 15%, 18%, 1); + + --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a400)}, 1); + --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a400)}, 0.1); + + --md-code-hl-number-color: hsla(6, 74%, 63%, 1); + --md-code-hl-special-color: hsla(340, 83%, 66%, 1); + --md-code-hl-function-color: hsla(291, 57%, 65%, 1); + --md-code-hl-constant-color: hsla(250, 62%, 70%, 1); + --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1); + --md-code-hl-string-color: hsla(150, 58%, 44%, 1); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: var(--md-default-fg-color--light); + + --md-typeset-color: var(--md-default-fg-color); + + --md-typeset-a-color: #498AFB; + + --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 90%, 0.12); + --md-typeset-kbd-accent-color: hsla(var(--md-hue), 15%, 90%, 0.2); + --md-typeset-kbd-border-color: hsla(var(--md-hue), 15%, 14%, 1); + + --md-typeset-mark-color: hsla(#{hex2hsl($clr-blue-a200)}, 0.3); + + --md-typeset-table-color: hsla(var(--md-hue), 15%, 95%, 0.12); + --md-typeset-table-color--light: hsla(var(--md-hue), 15%, 95%, 0.035); + + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + + --md-footer-bg-color: hsla(var(--md-hue), 15%, 10%, 0.87); + --md-footer-bg-color--dark: hsla(var(--md-hue), 15%, 8%, 1); + + --md-shadow-z1: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); + + --md-shadow-z2: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.25), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); + + --md-shadow-z3: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.4), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); + + img[src$="#only-light"], + img[src$="#gh-light-mode-only"] { + display: none; + } +} + +[data-md-color-scheme="nemesis-light"] { + color-scheme: light; + + --md-primary-fg-color: #262730; + + --md-hue: 225deg; + + --md-default-fg-color: hsla(0, 0%, 0%, 0.87); + --md-default-fg-color--light: hsla(0, 0%, 0%, 0.54); + --md-default-fg-color--lighter: hsla(0, 0%, 0%, 0.32); + --md-default-fg-color--lightest: hsla(0, 0%, 0%, 0.07); + --md-default-bg-color: hsla(0, 0%, 100%, 1); + --md-default-bg-color--light: hsla(0, 0%, 100%, 0.7); + --md-default-bg-color--lighter: hsla(0, 0%, 100%, 0.3); + --md-default-bg-color--lightest: hsla(0, 0%, 100%, 0.12); + + --md-code-fg-color: hsla(200, 18%, 26%, 1); + --md-code-bg-color: hsla(200, 0%, 96%, 1); + + --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a200)}, 1); + --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a200)}, 0.1); + + --md-code-hl-number-color: hsla(0, 67%, 50%, 1); + --md-code-hl-special-color: hsla(340, 83%, 47%, 1); + --md-code-hl-function-color: hsla(291, 45%, 50%, 1); + --md-code-hl-constant-color: hsla(250, 63%, 60%, 1); + --md-code-hl-keyword-color: hsla(219, 54%, 51%, 1); + --md-code-hl-string-color: hsla(150, 63%, 30%, 1); + --md-code-hl-name-color: var(--md-code-fg-color); + --md-code-hl-operator-color: var(--md-default-fg-color--light); + --md-code-hl-punctuation-color: var(--md-default-fg-color--light); + --md-code-hl-comment-color: var(--md-default-fg-color--light); + --md-code-hl-generic-color: var(--md-default-fg-color--light); + --md-code-hl-variable-color: var(--md-default-fg-color--light); + + --md-typeset-color: var(--md-default-fg-color); + + --md-typeset-a-color: #498AFB; + + --md-typeset-del-color: hsla(6, 90%, 60%, 0.15); + --md-typeset-ins-color: hsla(150, 90%, 44%, 0.15); + + --md-typeset-kbd-color: hsla(0, 0%, 98%, 1); + --md-typeset-kbd-accent-color: hsla(0, 100%, 100%, 1); + --md-typeset-kbd-border-color: hsla(0, 0%, 72%, 1); + + --md-typeset-mark-color: hsla(#{hex2hsl($clr-yellow-a200)}, 0.5); + + --md-typeset-table-color: hsla(0, 0%, 0%, 0.12); + --md-typeset-table-color--light: hsla(0, 0%, 0%, 0.035); + + --md-admonition-fg-color: var(--md-default-fg-color); + --md-admonition-bg-color: var(--md-default-bg-color); + + --md-warning-fg-color: hsla(0, 0%, 0%, 0.87); + --md-warning-bg-color: hsla(60, 100%, 80%, 1); + + --md-footer-fg-color: hsla(0, 0%, 100%, 1); + --md-footer-fg-color--light: hsla(0, 0%, 100%, 0.7); + --md-footer-fg-color--lighter: hsla(0, 0%, 100%, 0.45); + --md-footer-bg-color: hsla(0, 0%, 0%, 0.87); + --md-footer-bg-color--dark: hsla(0, 0%, 0%, 0.32); + + --md-shadow-z1: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.05), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.1); + + --md-shadow-z2: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.1), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.25); + + --md-shadow-z3: + 0 #{px2rem(4px)} #{px2rem(10px)} hsla(0, 0%, 0%, 0.2), + 0 0 #{px2rem(1px)} hsla(0, 0%, 0%, 0.35); +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c4436cb --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,30 @@ +site_name: Nemesis Documentation +site_url: https://specterops.github.io/Nemesis/ +repo_url: https://github.com/SpecterOps/Nemesis +repo_name: SpecterOps/Nemesis +edit_uri: edit/main/docs/ + +theme: + name: material + locale: en + logo: images/logo.png + favicon: images/logo.png + + icon: + repo: fontawesome/brands/github + + palette: + - media: "(prefers-color-scheme: light)" + scheme: nemesis-light + toggle: + icon: material/brightness-7 + name: Switch to dark mode + + - media: "(prefers-color-scheme: dark)" + scheme: nemesis-dark + toggle: + icon: material/brightness-4 + name: Switch to light mode + +extra_css: + - stylesheets/colors.css From bdd359ce396e8175180a69479bcc41ebe52ad931 Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 15:33:08 -0400 Subject: [PATCH 02/16] Update README.md banner path Moves the banner image to `docs/images/nemesis_white.png` and updates the README.md reference to it --- README.md | 2 +- img/nemesis_white.png | Bin 24228 -> 0 bytes 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 img/nemesis_white.png diff --git a/README.md b/README.md index 3237817..59cd3d0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@

- Nemesis + Nemesis


diff --git a/img/nemesis_white.png b/img/nemesis_white.png deleted file mode 100644 index 3d3c873c74dd301393bb7f81d593a661bfef5727..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 24228 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 t Date: Fri, 12 Apr 2024 16:10:05 -0400 Subject: [PATCH 03/16] Update rel links Links outside of the docs/ directory will not get included in the documentation site. This updates relative links outside the docs/ directory to point to the Github repository --- docs/access_nemesis.md | 8 +++--- docs/development.md | 4 +-- docs/elasticsearch-kibana.md | 4 +-- docs/hasura.md | 2 +- docs/index.md | 6 ++--- docs/kubernetes.md | 2 +- docs/nemesis_chart.md | 4 +-- docs/new_connector.md | 10 ++++---- docs/odr/references/host_data/named_pipe.md | 2 +- docs/postgres.md | 8 +++--- docs/quickstart_chart.md | 4 +-- docs/requirements.md | 2 +- docs/setup.md | 28 ++++++++++----------- docs/submit_to_nemesis.md | 6 ++--- docs/troubleshooting.md | 4 +-- docs/usage_guide.md | 14 +++++------ 16 files changed, 54 insertions(+), 54 deletions(-) diff --git a/docs/access_nemesis.md b/docs/access_nemesis.md index 816a219..d855c4a 100644 --- a/docs/access_nemesis.md +++ b/docs/access_nemesis.md @@ -7,9 +7,9 @@ If you use Minikube, by default, services are not exposed anywhere outside of th In the examples below, the following assumptions are made: - Minikube server IP: `192.168.230.42`. -- Nemesis's [`nemesisHttpServer` option](../helm/nemesis/values.yaml) is configured to be `https://192.168.230.42:7443/` +- Nemesis's [`nemesisHttpServer` option](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) is configured to be `https://192.168.230.42:7443/` -To quickly setup an SSH port forward, you can use the [minikube_port_forward.sh](../scripts/minikube_port_forward.sh) script: +To quickly setup an SSH port forward, you can use the [minikube_port_forward.sh](https://github.com/SpecterOps/Nemesis/blob/main/scripts/minikube_port_forward.sh) script: ```bash cd Nemesis/scripts/ ./minikube_port_forward.sh 7443 @@ -65,7 +65,7 @@ There's many ways you can do this (kubectl, SSH local port forward, Socat, IP ta **SSH** Using an SSH local port forward is our preferred method right now as it's simple to setup and proven reliable. -Let's say you configure the [`nemesisHttpServer` option](../helm/nemesis/values.yaml#L8) to listen on port `:7443`. Running the following command on the k8s host will expose the Minikube's endpoint externally (output in Step 1) using an SSH local port forward: +Let's say you configure the [`nemesisHttpServer` option](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml#L8) to listen on port `:7443`. Running the following command on the k8s host will expose the Minikube's endpoint externally (output in Step 1) using an SSH local port forward: ```bash ssh -N -L :7443:192.168.49.2:30123 ``` @@ -84,4 +84,4 @@ sudo setcap CAP_NET_BIND_SERVICE=+eip $(which kubectl) ``` ## Accessing Nemesis via Docker Desktop -Nemesis can run locally Docker Desktop. In that case, once Nemesis is deployed, you can access the nginx endpoint at `https://localhost/`. \ No newline at end of file +Nemesis can run locally Docker Desktop. In that case, once Nemesis is deployed, you can access the nginx endpoint at `https://localhost/`. diff --git a/docs/development.md b/docs/development.md index cd72204..700657c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -22,9 +22,9 @@ sudo unzip protoc-21.5-linux-x86_64.zip -d /usr/local/ # Running Nemesis during Dev -If you're doing general development, if you set the **operation.environment** variable in [values.yaml](../helm/nemesis/values.yaml) to *test* which will deploy everything without persistent storage. Then running `skaffold dev -m nemesis` will build the images and kick everything off. +If you're doing general development, if you set the **operation.environment** variable in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) to *test* which will deploy everything without persistent storage. Then running `skaffold dev -m nemesis` will build the images and kick everything off. -If you want to perform remote debugging for the `enrichment` container (see [remote_debugging.md](remote_debugging.md)) set the **operation.environment** variable in [values.yaml](../helm/nemesis/values.yaml) to *development* for the Helm chart which will deploy everything but the `enrichment` container without persistent storage. Run `skaffold dev -m nemesis` (or `helm install nemesis ./helm/nemesis --timeout '45m'` to use the public images) and then launching `skaffold dev -m enrichment` via VS Code will kick off the separate chart for just the enrichment container. +If you want to perform remote debugging for the `enrichment` container (see [remote_debugging.md](remote_debugging.md)) set the **operation.environment** variable in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) to *development* for the Helm chart which will deploy everything but the `enrichment` container without persistent storage. Run `skaffold dev -m nemesis` (or `helm install nemesis ./helm/nemesis --timeout '45m'` to use the public images) and then launching `skaffold dev -m enrichment` via VS Code will kick off the separate chart for just the enrichment container. # Service Development diff --git a/docs/elasticsearch-kibana.md b/docs/elasticsearch-kibana.md index e2507f9..c0699c1 100644 --- a/docs/elasticsearch-kibana.md +++ b/docs/elasticsearch-kibana.md @@ -8,9 +8,9 @@ The custom resource definitions that the helm chart installs can all be found [h # Storage -By default this Elasticsearch instance uses a persistent data store. The size of the datastore can be adjusted in the [values.yaml](../helm/nemesis/values.yaml) file by modifying the `storage: 20Gi` in the `elasticsearch` config section. +By default this Elasticsearch instance uses a persistent data store. The size of the datastore can be adjusted in the [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) file by modifying the `storage: 20Gi` in the `elasticsearch` config section. -To use temporary storage that is wiped on every run, set "environment" to "test" at the top of [values.yaml](../helm/nemesis/values.yaml). +To use temporary storage that is wiped on every run, set "environment" to "test" at the top of [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml). # Accessing Elastic/Kibana To login to Elastic or Kibana, run `./scripts/get_service_credentials.sh` to get the basic auth credentials. diff --git a/docs/hasura.md b/docs/hasura.md index 73a83c5..b7e6af4 100644 --- a/docs/hasura.md +++ b/docs/hasura.md @@ -14,7 +14,7 @@ There is a [quickstart to Hasura queries here](https://hasura.io/docs/latest/que ## Scripting -Hasura allows for _external_ queries and subscriptions to the backend schema, very similar to Mythic (in fact, this is what we do for the [Mythic Connector](../cmd/connectors/mythic-connector/README.md)!) +Hasura allows for _external_ queries and subscriptions to the backend schema, very similar to Mythic (in fact, this is what we do for the [Mythic Connector](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/mythic-connector#readme)!) ### Queries diff --git a/docs/index.md b/docs/index.md index 81536f6..88d794f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -34,13 +34,13 @@ Built on Kubernetes with scale in mind, our goal with Nemesis was to create a ce Nemesis aims to automate a number of repetitive tasks operators encounter on engagements, empower operators’ analytic capabilities and collective knowledge, and create structured and unstructured data stores of as much operational data as possible to help guide future research and facilitate offensive data analysis. # Setup / Installation -See the [setup instructions](docs/setup.md). +See the [setup instructions](setup.md). # Usage -See the [Nemesis Usage Guide](docs/usage_guide.md). +See the [Nemesis Usage Guide](usage_guide.md). # Contributing / Development Environment Setup -See [development.md](./docs/development.md) +See [development.md](development.md) ## Further Reading diff --git a/docs/kubernetes.md b/docs/kubernetes.md index b7f0e1a..4a7c6b5 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -17,7 +17,7 @@ May consider aliasing `kubectl` to `k` just so you don't have to type it out eac Reasons why it might happen include: * Pod doesn't have enough memory. Solution: Look at the pod's k8s code and check the `resources` section and increase the memory in [the limits/request section](https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/) -* The kubernetes node doesn't have enough memory to deploy the pods. Solution: If you're using Minikube, make sure the VM has enough memory and that minikube is configured to start with more memory (see the [preq instructions](prerequisites.md) for Minikube for how to configure the memory). +* The kubernetes node doesn't have enough memory to deploy the pods. Solution: If you're using Minikube, make sure the VM has enough memory and that minikube is configured to start with more memory (see the preq instructions for Minikube for how to configure the memory). * The application has a memory leak, and over time, consumes all the available memory dedicated to it. Solution: Fix the memory leak. # CrashLoopBackOff Error diff --git a/docs/nemesis_chart.md b/docs/nemesis_chart.md index 4149a22..c373bac 100644 --- a/docs/nemesis_chart.md +++ b/docs/nemesis_chart.md @@ -1,11 +1,11 @@ # Nemesis Helm Chart -The [`nemesis` Helm chart](../helm/nemesis/) deploys Nemesis's services. You can run the chart with its default configuration using the following command: +The [`nemesis` Helm chart](https://github.com/SpecterOps/Nemesis/tree/main/helm/nemesis) deploys Nemesis's services. You can run the chart with its default configuration using the following command: ```bash helm install --repo https://specterops.github.io/Nemesis/ nemesis nemesis --timeout '45m' ``` -If you want customize the deployment (e.g., HTTP server URI, pod CPU/memory resources, Minio disk size), you need to download the `nemesis` chart's [values.yaml](../helm/nemesis/values.yaml) file, edit it, and then run the `nemesis` chart using the customize values. You can do so with the following commands: +If you want customize the deployment (e.g., HTTP server URI, pod CPU/memory resources, Minio disk size), you need to download the `nemesis` chart's [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) file, edit it, and then run the `nemesis` chart using the customize values. You can do so with the following commands: 1. Download the quickstart chart's `values.yaml`: ```bash diff --git a/docs/new_connector.md b/docs/new_connector.md index 6bfdb16..382afc7 100644 --- a/docs/new_connector.md +++ b/docs/new_connector.md @@ -17,7 +17,7 @@ File processing is the one flow that differs from other structured data ingestio ## Step 1 - File Upload -For a file to be processed, the raw file bytes first need to be posted to the correct API route for storage in the data lake. This is accomplished by POSTing the file bytes to the `https:///api/file` which returns a simple JSON response with an `object_id` field containing a UUID that references the uploaded file. For example, to do this in Python (as shown in [mythic-connector](../cmd/connectors/mythic-connector/sync.py)), you would run something like this: +For a file to be processed, the raw file bytes first need to be posted to the correct API route for storage in the data lake. This is accomplished by POSTing the file bytes to the `https:///api/file` which returns a simple JSON response with an `object_id` field containing a UUID that references the uploaded file. For example, to do this in Python (as shown in [mythic-connector](https://github.com/SpecterOps/Nemesis/blob/main/cmd/connectors/mythic-connector/sync.py)), you would run something like this: ```python basic = HTTPBasicAuth(NEMESIS_USERNAME, NEMESIS_PASSWORD) @@ -34,7 +34,7 @@ The `nemesis_file_id` is used in the `file_data` message in Step 2 below. This U ## Step 2 - File Data Message -After the file is uploaded to Nemesis, a [file_data](./odr/references/file_data.md) ODR message needs to be posted with file metadata information. The example from the [mythic-connector](../cmd/connectors/mythic-connector/sync.py) is: +After the file is uploaded to Nemesis, a [file_data](odr/references/file_data.md) ODR message needs to be posted with file metadata information. The example from the [mythic-connector](https://github.com/SpecterOps/Nemesis/blob/main/cmd/connectors/mythic-connector/sync.py) is: ```python metadata = {} @@ -64,13 +64,13 @@ r = requests.request("POST", f"{NEMESIS_URL}/data", auth=basic, data=data, heade # Other Structured Data -For other types of structured data, only a single message needs to be posted to the `http:///api/data` API route, e.g. Step 2 in the downloading processing example. The `metadata["data_type"]` field should be one of the types defined in the [ODR](./odr/references/). The appropriate ODR document will also define the fields and structure needed for the datatype. +For other types of structured data, only a single message needs to be posted to the `http:///api/data` API route, e.g. Step 2 in the downloading processing example. The `metadata["data_type"]` field should be one of the types defined in the [ODR](odr/references/). The appropriate ODR document will also define the fields and structure needed for the datatype. Note that the "data" section of the message is an array of dictionaries, i.e., multiple instances of a datatype can be posted in a single message. For example, multiple process messages can exist in the single post. -As an example, see the `handle_process()` function in the [mythic-connector](../cmd/connectors/mythic-connector/sync.py). +As an example, see the `handle_process()` function in the [mythic-connector](https://github.com/SpecterOps/Nemesis/blob/main/cmd/connectors/mythic-connector/sync.py). Example of many of the structured datatypes can be found in the `./sample_files/structured/` folder. Example of using these to submit process data: ```bash curl -H "Content-Type: application/octet-stream" -k -v --user 'nemesis:Qwerty12345' --data-binary @./sample_files/structured/process_data.json https://192.168.230.42:8080/api/data -``` \ No newline at end of file +``` diff --git a/docs/odr/references/host_data/named_pipe.md b/docs/odr/references/host_data/named_pipe.md index d8eaa16..a8fd240 100644 --- a/docs/odr/references/host_data/named_pipe.md +++ b/docs/odr/references/host_data/named_pipe.md @@ -18,4 +18,4 @@ Information about a Windows named pipe. ## Examples -[named_pipes.json](../../../../sample_files/structured/named_pipes.json) \ No newline at end of file +[named_pipes.json](https://github.com/SpecterOps/Nemesis/blob/main/sample_files/structured/named_pipes.json) diff --git a/docs/postgres.md b/docs/postgres.md index a31c1f6..a8cd9e5 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -4,14 +4,14 @@ In addition to Elasticsearch for an unstructed/NoSQL approach, we are using Post The database schema for Postgres is at `./helm/nemesis/files/postgres/nemesis.sql`. It mimics the Protobufs defined in ./packages/nemesis.proto, **but are not guaranteed to match!** -We do not recommend interacting with Postgres directly- instead, use the [`/hasura/`](#hasura.md) endpoint +We do not recommend interacting with Postgres directly- instead, use the [`/hasura/`](hasura.md) endpoint # Storage -By default this PostgreSQL instance uses a persistent data store. The size of the datastore can be adjusted in [values.yaml](./helm/nemesis/values.yaml) by modifying the `storage: 15Gi` in the postgres section. +By default this PostgreSQL instance uses a persistent data store. The size of the datastore can be adjusted in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) by modifying the `storage: 15Gi` in the postgres section. -To use temporary storage that is wiped on every run, set the `operation.environment` value in [values.yaml](./helm/nemesis/values.yaml) to "test". +To use temporary storage that is wiped on every run, set the `operation.environment` value in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) to "test". ## pgAdmin -A pgAdmin interface is exposed at `NEMESIS_URL/pgadmin` with the credentials from [values.yaml](./helm/nemesis/values.yaml) +A pgAdmin interface is exposed at `NEMESIS_URL/pgadmin` with the credentials from [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) diff --git a/docs/quickstart_chart.md b/docs/quickstart_chart.md index 8ce1344..70b9876 100644 --- a/docs/quickstart_chart.md +++ b/docs/quickstart_chart.md @@ -1,5 +1,5 @@ # Quickstart Helm Chart -The purpose of the [`quickstart` Helm chart](../helm/quickstart/) is to configure and set secrets for each Nemesis service (e.g., usernames and passwords and ingress TLS certificates). You can run the quickstart chart with the following command: +The purpose of the [`quickstart` Helm chart](https://github.com/SpecterOps/Nemesis/tree/main/helm/quickstart) is to configure and set secrets for each Nemesis service (e.g., usernames and passwords and ingress TLS certificates). You can run the quickstart chart with the following command: ```bash helm install --repo https://specterops.github.io/Nemesis/ nemesis-quickstart quickstart @@ -13,7 +13,7 @@ echo "Basic Auth Password: ${BASIC_AUTH_PASSWORD}" ``` # Customizing the Configuration -If you want customize any of the services' secrets, you need to download the `quickstart` chart's [values.yaml](../helm/quickstart/values.yaml) file, edit it, and then run the `quickstart` chart using the customized values. You can do so with the following commands: +If you want customize any of the services' secrets, you need to download the `quickstart` chart's [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/quickstart/values.yaml) file, edit it, and then run the `quickstart` chart using the customized values. You can do so with the following commands: 1. Download the quickstart chart's `values.yaml`: ```bash diff --git a/docs/requirements.md b/docs/requirements.md index 110f2ac..ad95e76 100644 --- a/docs/requirements.md +++ b/docs/requirements.md @@ -82,7 +82,7 @@ helm repo add bitnami https://charts.bitnami.com/bitnami Run `helm install --repo https://specterops.github.io/Nemesis/ nemesis-quickstart quickstart` - If you want to edit any of the password values for Nemesis, edit them in [values.yaml](../helm/quickstart/values.yaml). + If you want to edit any of the password values for Nemesis, edit them in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/quickstart/values.yaml). ``` curl https://raw.githubusercontent.com/SpecterOps/Nemesis/helm/helm/quickstart/values.yaml -o quickstart-values.yaml diff --git a/docs/setup.md b/docs/setup.md index 90f50be..b009a94 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,5 +1,5 @@ # Nemesis Installation and Setup -1. Ensure the [requisite software/hardware is installed](./requirements.md). +1. Ensure the [requisite software/hardware is installed](requirements.md). 2. Run the [`quickstart` Helm chart](quickstart_chart.md) to configure Nemesis's services and secrets. @@ -15,22 +15,22 @@ If you run into any issues, please see [troubleshooting.md](troubleshooting.md) Once Nemesis is running, data first needs to be ingested into the platform. Ingestion into Nemesis can occur in muliple ways, including * [Auto-ingesting data from C2 platorms.](#nemesis-c2-connector-setup) * Manually uploading files on the "File Upload" page in the Nemesis's Dashboard UI. -* Using the [submit_to_nemesis](./submit_to_nemesis.md) CLI tool to submit files. +* Using the [submit_to_nemesis](submit_to_nemesis.md) CLI tool to submit files. * Writing custom tools to interact with [Nemesis's API](new_connector.md). ## Nemesis C2 Connector Setup Nemesis includes connectors for various C2 platorms. The connectors hook into the C2 platforms and transfer data automatically into Nemesis. The `./cmd/connectors/` folder contains the following C2 connectors: -- [Cobalt Strike](../cmd/connectors/cobaltstrike-nemesis-connector/README.md) -- [Mythic](../cmd/connectors/mythic-connector/README.md) -- [Sliver](../cmd/connectors/sliver-connector/README.md) -- [OST Stage1](../cmd/connectors/stage1-connector/README.md) -- [Metasploit](../cmd/connectors/metasploit-connector/README.md) -- [Chrome Extension](../cmd/connectors/chrome-extension/README.md) +- [Cobalt Strike](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/cobaltstrike-nemesis-connector#readme) +- [Mythic](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/mythic-connector#readme) +- [Sliver](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/sliver-connector#readme) +- [OST Stage1](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/stage1-connector#readme) +- [Metasploit](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/metasploit-connector#readme) +- [Chrome Extension](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/chrome-extension#readme) ***Note: not all connectors have the same level of completeness! We intended to show the range of connectors possible, but there is not yet feature parity.*** -If you'd like to ingest data from another platform, see the documentation for [adding a new connector](./new_connector.md). +If you'd like to ingest data from another platform, see the documentation for [adding a new connector](new_connector.md). # Nemesis Service Endpoints @@ -72,7 +72,7 @@ metricsServer: enabled: true ``` -If you have not installed Nemesis yet, see [Nemesis Chart](./nemesis_chart.md) or upgrade the installation: +If you have not installed Nemesis yet, see [Nemesis Chart](nemesis_chart.md) or upgrade the installation: ```bash helm upgrade --repo https://specterops.github.io/Nemesis/ [chart name] nemesis @@ -84,20 +84,20 @@ Elasticsearch, PostgreSQL, and Minio (if using instead of AWS S3) have persisten ## File Storage Backend -Nemesis can use AWS S3 (in conjunction with KMS for file encryption) for file storage by modifying the `storage` setting in [values.yaml](../helm/nemesis/values.yaml) and configuring the `aws` block. +Nemesis can use AWS S3 (in conjunction with KMS for file encryption) for file storage by modifying the `storage` setting in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) and configuring the `aws` block. By default, Nemesis uses Minio for file storage with a default storage size of `30Gi`. -To change the size, modify the `minio.persistence.size` value in [values.yaml](../helm/nemesis/values.yaml) file. +To change the size, modify the `minio.persistence.size` value in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) file. ## Elasticsearch -The default storage size is 20Gi. To change this, modify the `elasticsearch.storage` value in [values.yaml](../helm/nemesis/values.yaml). +The default storage size is 20Gi. To change this, modify the `elasticsearch.storage` value in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml). ## PostgreSQL -The default storage size is 20Gi. To change this, modify the `postgres.storage` value in [values.yaml](../helm/nemesis/values.yaml). +The default storage size is 20Gi. To change this, modify the `postgres.storage` value in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml). # (Optional) Change Nemesis's Listening Port diff --git a/docs/submit_to_nemesis.md b/docs/submit_to_nemesis.md index f5999f2..63d9350 100644 --- a/docs/submit_to_nemesis.md +++ b/docs/submit_to_nemesis.md @@ -3,7 +3,7 @@ # Docker -If you want to use the pre-build Docker container to submit artifacts to Nemesis, run [monitor_folder_docker.sh](../scripts/monitor_folder_docker.sh). The only requirement for the script is Docker and wget. +If you want to use the pre-build Docker container to submit artifacts to Nemesis, run [monitor_folder_docker.sh](https://github.com/SpecterOps/Nemesis/blob/main/scripts/monitor_folder_docker.sh). The only requirement for the script is Docker and wget. # Requirements Install with the instructions below. @@ -70,7 +70,7 @@ poetry -C ./cmd/enrichment/ install # Configuring -To use `submit_to_nemesis`, one must edit the YAML configuration file found in `cmd/enrichment/enrichment/cli/submit_to_nemesis/submit_to_nemesis.yaml` ([link to YAML file](../cmd/enrichment/enrichment/cli/submit_to_nemesis/submit_to_nemesis.yaml)). This config file includes the credentials to authenticate to Nemesis, the location of the Nemesis server, and information about the operation that Nemesis will tag each uploaded file with (operator name, project, network, etc.). +To use `submit_to_nemesis`, one must edit the YAML configuration file found in `cmd/enrichment/enrichment/cli/submit_to_nemesis/submit_to_nemesis.yaml` ([link to YAML file](https://github.com/SpecterOps/Nemesis/blob/main/cmd/enrichment/enrichment/cli/submit_to_nemesis/submit_to_nemesis.yaml)). This config file includes the credentials to authenticate to Nemesis, the location of the Nemesis server, and information about the operation that Nemesis will tag each uploaded file with (operator name, project, network, etc.). # Usage Once configured, in the root Nemesis directory run @@ -100,4 +100,4 @@ Below are some example usage scenarios: * Stress test the Nemesis installation by submitting a folder of files 100 times with 30 workers: ``` ./scripts/submit_to_nemesis.sh --folder sample_files/ -w 30 -r 100 -``` \ No newline at end of file +``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 4f3c224..3e2ba1b 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -26,7 +26,7 @@ Then, reinstall everything but specify a higher timeout period (e.g., 90 minutes helm install --repo https://specterops.github.io/Nemesis/ nemesis nemesis --timeout '90m'` ``` -While Nemesis is deploying, you can quickly monitor deployed pods by running the [watch_pods.sh](../scripts/watch_pods.sh) script. If the image is still pulling, usually the pod's status will be `ContainerCreating` or `Init: #/#`. You can run `kubectl describe pods ` to view some details about the pod, and if it's still pulling the image there will be an event similar to this: +While Nemesis is deploying, you can quickly monitor deployed pods by running the [watch_pods.sh](https://github.com/SpecterOps/Nemesis/blob/main/scripts/watch_pods.sh) script. If the image is still pulling, usually the pod's status will be `ContainerCreating` or `Init: #/#`. You can run `kubectl describe pods ` to view some details about the pod, and if it's still pulling the image there will be an event similar to this: ``` Events: Type Reason Age From Message @@ -69,4 +69,4 @@ If minikube can connect to the internet but DNS isn't working, add the following ## Need additional help? -Please [file an issue](https://github.com/SpecterOps/Nemesis/issues) or feel free to ask questions in the [#nemesis-chat channel](https://bloodhoundhq.slack.com/archives/C05KN15CCGP) in the Bloodhound Slack ([click here to join](https://ghst.ly/BHSlack)). \ No newline at end of file +Please [file an issue](https://github.com/SpecterOps/Nemesis/issues) or feel free to ask questions in the [#nemesis-chat channel](https://bloodhoundhq.slack.com/archives/C05KN15CCGP) in the Bloodhound Slack ([click here to join](https://ghst.ly/BHSlack)). diff --git a/docs/usage_guide.md b/docs/usage_guide.md index bcc0e57..a2bc069 100644 --- a/docs/usage_guide.md +++ b/docs/usage_guide.md @@ -32,12 +32,12 @@ Once Nemesis is running, data first needs to be ingested into the platform. Inge ## Nemesis C2 Connector Setup Nemesis includes connectors for various C2 platorms. The connectors hook into the C2 platforms and transfer data automatically into Nemesis. The `./cmd/connectors/` folder contains the following C2 connectors: -- [Cobalt Strike](../cmd/connectors/cobaltstrike-nemesis-connector/README.md) -- [Mythic](../cmd/connectors/mythic-connector/README.md) -- [Sliver](../cmd/connectors/sliver-connector/README.md) -- [OST Stage1](../cmd/connectors/stage1-connector/README.md) -- [Metasploit](../cmd/connectors/metasploit-connector/README.md) -- [Chrome Extension](../cmd/connectors/chrome-extension/README.md) +- [Cobalt Strike](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/cobaltstrike-nemesis-connector#readme) +- [Mythic](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/mythic-connector#readme) +- [Sliver](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/sliver-connector#readme) +- [OST Stage1](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/stage1-connector#readme) +- [Metasploit](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/metasploit-connector#readme) +- [Chrome Extension](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/chrome-extension#readme) ***Note: not all connectors have the same level of completeness! We intended to show the range of connectors possible, but there is not yet feature parity.*** @@ -117,7 +117,7 @@ The **Source Code Search** tab on the top of the page functions similarly to the ### Snippet Search -The **Snippet Search** tab operates a bit differently. In addition to being normally indexed in Elasticsearch, all text extracted from plaintext documents by Nemesis are also broken into chunks and run through a small [embedding model](https://www.elastic.co/what-is/vector-embedding) to produce fixed-length vector embeddings. The model currently being used is [gte-tiny](https://huggingface.co/TaylorAI/gte-tiny) but this can be modified in the [nlp.deployment.yaml](./helm/nemesis/templates/nlp.deployment.yaml) of the NLP container. These embeddings are stored in Elasticsearch along with the associated chunked text, allowing for [sematic search](https://en.wikipedia.org/wiki/Semantic_search) over indexed text. +The **Snippet Search** tab operates a bit differently. In addition to being normally indexed in Elasticsearch, all text extracted from plaintext documents by Nemesis are also broken into chunks and run through a small [embedding model](https://www.elastic.co/what-is/vector-embedding) to produce fixed-length vector embeddings. The model currently being used is [gte-tiny](https://huggingface.co/TaylorAI/gte-tiny) but this can be modified in the [nlp.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/templates/nlp.yaml) of the NLP container. These embeddings are stored in Elasticsearch along with the associated chunked text, allowing for [sematic search](https://en.wikipedia.org/wiki/Semantic_search) over indexed text. In addition, we also exploit the BM25 text search of Elasticsearch over the sparse indexed text. The two lists of results are fused with [Reciprocal Rank Fusion (RRF)](https://learn.microsoft.com/en-us/azure/search/hybrid-search-ranking) and the reordered list of snippets is presented to the user: From c2ec2fd4a0466e6b5c450deeb810bef0254d2c63 Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 16:57:53 -0400 Subject: [PATCH 04/16] Add code syntax highlighting --- docs/stylesheets/colors.css | 24 +++++++++++------------- mkdocs.yml | 5 +++++ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/docs/stylesheets/colors.css b/docs/stylesheets/colors.css index f5a2f43..57a719a 100644 --- a/docs/stylesheets/colors.css +++ b/docs/stylesheets/colors.css @@ -17,24 +17,22 @@ --md-code-fg-color: hsla(var(--md-hue), 18%, 86%, 0.82); --md-code-bg-color: hsla(var(--md-hue), 15%, 18%, 1); + --md-code-hl-number-color: #619a8c; + --md-code-hl-special-color: var(--md-code-hl-number-color); + --md-code-hl-function-color: #5382db; + --md-code-hl-string-color: #61a242; + --md-code-hl-operator-color: #965822; + --md-code-hl-name-color: #e4e4e4; + --md-code-hl-keyword-color: #4b74c6; + --md-code-hl-punctuation-color: #808080; + --md-code-hl-constant-color: hsla(250, 62%, 70%, 1); + --md-code-hl-comment-color: rgb(98, 103, 119); + --md-code-hl-variable-color: #ffffff; --md-code-hl-color: hsla(#{hex2hsl($clr-blue-a400)}, 1); --md-code-hl-color--light: hsla(#{hex2hsl($clr-blue-a400)}, 0.1); - - --md-code-hl-number-color: hsla(6, 74%, 63%, 1); - --md-code-hl-special-color: hsla(340, 83%, 66%, 1); - --md-code-hl-function-color: hsla(291, 57%, 65%, 1); - --md-code-hl-constant-color: hsla(250, 62%, 70%, 1); - --md-code-hl-keyword-color: hsla(219, 66%, 64%, 1); - --md-code-hl-string-color: hsla(150, 58%, 44%, 1); - --md-code-hl-name-color: var(--md-code-fg-color); - --md-code-hl-operator-color: var(--md-default-fg-color--light); - --md-code-hl-punctuation-color: var(--md-default-fg-color--light); - --md-code-hl-comment-color: var(--md-default-fg-color--light); --md-code-hl-generic-color: var(--md-default-fg-color--light); - --md-code-hl-variable-color: var(--md-default-fg-color--light); --md-typeset-color: var(--md-default-fg-color); - --md-typeset-a-color: #498AFB; --md-typeset-kbd-color: hsla(var(--md-hue), 15%, 90%, 0.12); diff --git a/mkdocs.yml b/mkdocs.yml index c4436cb..619fb35 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,3 +28,8 @@ theme: extra_css: - stylesheets/colors.css + +markdown_extensions: + - pymdownx.superfences + - pymdownx.highlight: + use_pygments: true From b3acd380797b8dde227f3ea99e6a7f50c7cb7e01 Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 17:03:38 -0400 Subject: [PATCH 05/16] Add mkdocs python requirements --- docs/requirements.txt | 2 ++ mkdocs.yml | 3 +++ 2 files changed, 5 insertions(+) create mode 100644 docs/requirements.txt diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..2e612b1 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,2 @@ +mkdocs==1.5.3 +mkdocs-material==9.5.17 diff --git a/mkdocs.yml b/mkdocs.yml index 619fb35..b5fbf8a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -4,6 +4,9 @@ repo_url: https://github.com/SpecterOps/Nemesis repo_name: SpecterOps/Nemesis edit_uri: edit/main/docs/ +exclude_docs: | + /requirements.txt + theme: name: material locale: en From de6f1a1015ea5139bb7da90b91ad509d2a6946ec Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 17:19:13 -0400 Subject: [PATCH 06/16] Add mkdocs site deployment directory to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index bfde74b..5986748 100644 --- a/.gitignore +++ b/.gitignore @@ -8,4 +8,5 @@ __pycache__ nemesis.config config.yml submit_to_nemesis.yaml -submit/ \ No newline at end of file +submit/ +site From ef3a1d1c2280af2fadf8fd1701fa008ba1937693 Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 17:22:16 -0400 Subject: [PATCH 07/16] Add docs ci workflow --- .github/workflows/docs.yml | 66 ++++++++++++++++++++++++++++++++++++++ mkdocs.yml | 2 +- 2 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/docs.yml diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..46aaf68 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,66 @@ +name: Build documentation + +on: + push: + branches: [main] + + # Only trigger workflow when documentation files are are changed + paths: + - 'docs/**' + - 'mkdocs.yml' + - '.github/workflows/docs.yml' + +jobs: + docs: + runs-on: ubuntu-latest + + permissions: + pages: write + id-token: write + + environment: + name: github-pages + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Python virtualenv + run: | + pip install --upgrade pip + python -m venv env + source env/bin/activate + pip install -r docs/requirements.txt + + - name: Build documentation + run: | + source env/bin/activate + mkdocs build + + - name: Add existing Helm repository index.yml file + env: + GH_TOKEN: ${{ github.token }} + run: | + PAGES_URL=$(gh api \ + -H "Accept: application/vnd.github+json" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + /repos/${{ github.repository }}/pages \ + | jq -r '.html_url') + + if [[ "$PAGES_URL" != "null" ]]; then + HTTP_STATUS=$(curl -sL -w '%{http_code}' "${PAGES_URL%/}/index.yaml" -o site/index.yaml) + if [[ "$HTTP_STATUS" != "200" ]]; then + rm site/index.yaml + fi + fi + + - name: Setup Github pages + uses: actions/configure-pages@v4 + + - name: Create Github pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: site + + - name: Deploy documentation to Github pages + uses: actions/deploy-pages@v4 diff --git a/mkdocs.yml b/mkdocs.yml index b5fbf8a..6945f84 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Nemesis Documentation -site_url: https://specterops.github.io/Nemesis/ +site_url: https://mehrn00.github.io/Nemesis/ repo_url: https://github.com/SpecterOps/Nemesis repo_name: SpecterOps/Nemesis edit_uri: edit/main/docs/ From d5c343caa0658ec771f879cb03aaa8073946e5cc Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 18:33:29 -0400 Subject: [PATCH 08/16] Add pygments to requirements.txt --- docs/requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/requirements.txt b/docs/requirements.txt index 2e612b1..bfcc47b 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,2 +1,3 @@ mkdocs==1.5.3 mkdocs-material==9.5.17 +pygments==2.17.2 From 8d87c5205d254dc45f4030e32d3c8443bd00521c Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 21:40:23 -0400 Subject: [PATCH 09/16] Integrate documentation workflow with Helm repository workflow --- .github/workflows/docs.yml | 5 + .github/workflows/helm-release.yml | 152 +++++++++++++++++++---------- 2 files changed, 107 insertions(+), 50 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 46aaf68..74d543c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -10,6 +10,11 @@ on: - 'mkdocs.yml' - '.github/workflows/docs.yml' +# Prevent this workflow from running concurrently with the docs.yml workflow +concurrency: + group: "pages" + cancel-in-progress: false + jobs: docs: runs-on: ubuntu-latest diff --git a/.github/workflows/helm-release.yml b/.github/workflows/helm-release.yml index 73951f5..c1bff8d 100644 --- a/.github/workflows/helm-release.yml +++ b/.github/workflows/helm-release.yml @@ -14,17 +14,16 @@ on: default: 'helm' type: string -env: - PACKAGE_DIR: dist - -# Only allow one instance of this workflow to run at a time +# Prevent this workflow from running concurrently with the docs.yml workflow concurrency: group: "pages" - cancel-in-progress: true + cancel-in-progress: false jobs: verify: + name: Verify release + runs-on: ubuntu-latest steps: @@ -70,38 +69,42 @@ jobs: false fi - release: + docs: + name: Build documentation needs: verify - - # Provision a Github token with repository and pages write permissions - permissions: - contents: write - pages: write - id-token: write - - # Use the github-pages environment. The actions/deploy-pages workflow fails with a - # "Invalid environment node id" error if an environment is not specified. - # https://github.com/actions/deploy-pages/issues/271 - environment: - name: github-pages - runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - - name: Configure git + - name: Setup Python virtualenv run: | - git config user.name "$GITHUB_ACTOR" - git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + pip install --upgrade pip + python -m venv env + source env/bin/activate + pip install -r docs/requirements.txt - - name: Create a git tag for the release - uses: EndBug/add-and-commit@v9 + - name: Build documentation + run: | + source env/bin/activate + mkdocs build + + - name: Store built documentation artifacts + uses: actions/upload-artifact@v4 with: - message: "Nemesis v${{ inputs.version }}" - push: true - tag: "v${{ inputs.version }}" + name: docs + path: site + + helm: + name: Package Helm charts + needs: verify + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 - name: Install Helm env: @@ -121,17 +124,15 @@ jobs: done done - - name: Package Helm charts + - name: Create Chart packages env: - PACKAGE_DIR: ${{ env.PACKAGE_DIR }} CHARTS_DIR: ${{ inputs.charts_dir }} run: | - mkdir -p $PACKAGE_DIR - find $CHARTS_DIR -maxdepth 2 -mindepth 2 -type f -name "Chart.yaml" -printf '%h\n' | xargs -I % bash -c "helm package -d $PACKAGE_DIR %" + mkdir -p dist + find $CHARTS_DIR -maxdepth 2 -mindepth 2 -type f -name "Chart.yaml" -printf '%h\n' | xargs -I % bash -c "helm package -d dist %" - name: Pull in previous index.yaml file if it exists env: - PACKAGE_DIR: ${{ env.PACKAGE_DIR }} GH_TOKEN: ${{ github.token }} run: | PAGES_URL=$(gh api \ @@ -141,34 +142,79 @@ jobs: | jq -r '.html_url') if [[ "$PAGES_URL" != "null" ]]; then - HTTP_STATUS=$(curl -sL -w '%{http_code}' "${PAGES_URL%/}/index.yaml" -o ${PACKAGE_DIR}/index.yaml) + HTTP_STATUS=$(curl -sL -w '%{http_code}' "${PAGES_URL%/}/index.yaml" -o dist/index.yaml) if [[ "$HTTP_STATUS" != "200" ]]; then - rm ${PACKAGE_DIR}/index.yaml + rm dist/index.yaml fi fi - name: Update Helm repository index.yaml file env: - PACKAGE_DIR: ${{ env.PACKAGE_DIR }} CHART_BASE_URL: ${{ github.server_url }}/${{ github.repository }}/releases/download/v${{ inputs.version }} run: | - if [ -f ${PACKAGE_DIR}/index.yaml ]; then - helm repo index $PACKAGE_DIR --merge ${PACKAGE_DIR}/index.yaml --url $CHART_BASE_URL + if [ -f dist/index.yaml ]; then + helm repo index dist --merge dist/index.yaml --url $CHART_BASE_URL else - helm repo index $PACKAGE_DIR --url $CHART_BASE_URL + helm repo index dist --url $CHART_BASE_URL fi - - name: Create Github release with the Helm charts - env: - PACKAGE_DIR: ${{ env.PACKAGE_DIR }} - VERSION: v${{ inputs.version }} - GH_TOKEN: ${{ github.token }} - run: gh release create ${VERSION} -R ${{ github.repository }} -t "Nemesis $VERSION" -n "Nemesis $VERSION release" $PACKAGE_DIR/*.tgz + - name: Store Helm chart artifacts + uses: actions/upload-artifact@v4 + with: + name: charts + path: dist - - name: Remove packaged Helm charts - env: - PACKAGE_DIR: ${{ env.PACKAGE_DIR }} - run: rm -f ${PACKAGE_DIR}/*.tgz + release: + name: Publish and release files + needs: + - verify + - docs + - helm + + # Provision a Github token with repository and pages write permissions + permissions: + contents: write + pages: write + id-token: write + + # Use the github-pages environment. The actions/deploy-pages workflow fails with a + # "Invalid environment node id" error if an environment is not specified. + # https://github.com/actions/deploy-pages/issues/271 + environment: + name: github-pages + + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Configure git + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + + - name: Create a git tag for the release + uses: EndBug/add-and-commit@v9 + with: + message: "Nemesis v${{ inputs.version }}" + push: true + tag: "v${{ inputs.version }}" + + - name: Download documentation site files + uses: actions/download-artifact@v4 + with: + name: docs + path: site + + - name: Download Helm chart files + uses: actions/download-artifact@v4 + with: + name: charts + path: dist + + - name: Merge Chart index.yaml file with documentation files + run: mv dist/index.yaml site/index.yaml - name: Setup Github pages uses: actions/configure-pages@v4 @@ -176,11 +222,17 @@ jobs: - name: Create Github pages artifact uses: actions/upload-pages-artifact@v3 with: - path: ${{ env.PACKAGE_DIR }} + path: site - - name: Deploy Helm chart repository to Github pages + - name: Deploy Github pages site uses: actions/deploy-pages@v4 + - name: Create Github release with the Helm charts + env: + VERSION: v${{ inputs.version }} + GH_TOKEN: ${{ github.token }} + run: gh release create ${VERSION} -R ${{ github.repository }} -t "Nemesis $VERSION" -n "Nemesis $VERSION release" dist/*.tgz + - name: Remove Github release and tag on failure continue-on-error: true if: ${{ failure() }} From c1a69df36db131ef9946bf95f7d8c2ff3b6ee3bf Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 22:48:34 -0400 Subject: [PATCH 10/16] Fix site_url --- mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mkdocs.yml b/mkdocs.yml index 6945f84..b5fbf8a 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,5 +1,5 @@ site_name: Nemesis Documentation -site_url: https://mehrn00.github.io/Nemesis/ +site_url: https://specterops.github.io/Nemesis/ repo_url: https://github.com/SpecterOps/Nemesis repo_name: SpecterOps/Nemesis edit_uri: edit/main/docs/ From a39d87fe10d67649a98ad2ee5ec93d3ce985d2e9 Mon Sep 17 00:00:00 2001 From: Matt Ehrnschwender Date: Fri, 12 Apr 2024 22:49:38 -0400 Subject: [PATCH 11/16] Fix typos in docs workflow --- .github/workflows/docs.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 74d543c..261ea2e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -4,13 +4,13 @@ on: push: branches: [main] - # Only trigger workflow when documentation files are are changed + # Only trigger workflow when documentation files are changed paths: - 'docs/**' - 'mkdocs.yml' - '.github/workflows/docs.yml' -# Prevent this workflow from running concurrently with the docs.yml workflow +# Prevent this workflow from running concurrently with the helm-release.yml workflow concurrency: group: "pages" cancel-in-progress: false From a06e7a65250d0b4fbbe5c96f958854b01004c99a Mon Sep 17 00:00:00 2001 From: Max Harley Date: Thu, 25 Apr 2024 15:43:43 -0400 Subject: [PATCH 12/16] Update nav; update links; create toc for setup.md --- docs/nemesis_chart.md | 2 +- docs/overview.md | 2 +- docs/requirements_docker_desktop.md | 2 +- docs/setup.md | 47 ++++++++++++++++++----------- 4 files changed, 33 insertions(+), 20 deletions(-) diff --git a/docs/nemesis_chart.md b/docs/nemesis_chart.md index 3fb6da5..f5da564 100644 --- a/docs/nemesis_chart.md +++ b/docs/nemesis_chart.md @@ -30,7 +30,7 @@ $ curl -u $(kubectl get secret basic-auth -o jsonpath='{.data.username}' | base6 ## Customizing the Deployment -If you want customize the deployment (e.g., HTTP server URI, pod CPU/memory resources, Minio disk size), you need to download the `nemesis` chart's [values.yaml](../helm/nemesis/values.yaml) file, edit it, and then run the `nemesis` chart using the customize values. You can do so with the following commands: +If you want customize the deployment (e.g., HTTP server URI, pod CPU/memory resources, Minio disk size), you need to download the `nemesis` chart's [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) file, edit it, and then run the `nemesis` chart using the customize values. You can do so with the following commands: 1. Download the quickstart chart's `values.yaml`: ```bash diff --git a/docs/overview.md b/docs/overview.md index 061ef0f..24ebbe0 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -40,7 +40,7 @@ with protobuf. ### ODR The data that can be input into Nemesis is strictly defined in the [Operational Data -Reference (ODR)](#odr/README.md). +Reference (ODR)](odr/README.md). #### ODR Protobuf diff --git a/docs/requirements_docker_desktop.md b/docs/requirements_docker_desktop.md index 7926007..f3021b3 100644 --- a/docs/requirements_docker_desktop.md +++ b/docs/requirements_docker_desktop.md @@ -52,7 +52,7 @@ helm repo add bitnami https://charts.bitnami.com/bitnami Run `helm install --repo https://specterops.github.io/Nemesis/ nemesis-quickstart quickstart` - If you want to edit any of the password values for Nemesis, edit them in [values.yaml](../helm/quickstart/values.yaml). + If you want to edit any of the password values for Nemesis, edit them in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/quickstart/values.yaml) ``` helm show values --repo https://specterops.github.io/Nemesis/ nemesis > quickstart-values.yaml diff --git a/docs/setup.md b/docs/setup.md index b1cd338..fdbfb3f 100644 --- a/docs/setup.md +++ b/docs/setup.md @@ -1,4 +1,5 @@ # Nemesis Installation and Setup + 1. Ensure the [requisite software/hardware is installed](requirements.md). 2. Run the [`quickstart` Helm chart](quickstart_chart.md) to configure Nemesis's services and secrets. @@ -11,14 +12,19 @@ If you run into any issues, please see [troubleshooting.md](troubleshooting.md) for common errors/issues. -# Data Ingestion + +## Data Ingestion + Once Nemesis is running, data first needs to be ingested into the platform. Ingestion into Nemesis can occur in muliple ways, including + * [Auto-ingesting data from C2 platorms.](#nemesis-c2-connector-setup) * Manually uploading files on the "File Upload" page in the Nemesis's Dashboard UI. * Using the [submit_to_nemesis](submit_to_nemesis.md) CLI tool to submit files. * Writing custom tools to interact with [Nemesis's API](new_connector.md). -## Nemesis C2 Connector Setup + +### Nemesis C2 Connector Setup + Nemesis includes connectors for various C2 platorms. The connectors hook into the C2 platforms and transfer data automatically into Nemesis. The `./cmd/connectors/` folder contains the following C2 connectors: - [Cobalt Strike](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/cobaltstrike-nemesis-connector#readme) @@ -32,7 +38,8 @@ Nemesis includes connectors for various C2 platorms. The connectors hook into th If you'd like to ingest data from another platform, see the documentation for [adding a new connector](new_connector.md). -# Nemesis Service Endpoints + +## Nemesis Service Endpoints All Nemesis services are exposed through a single HTTP endpoint (defined in the NEMESIS_HTTP_SERVER environment variable) protected by HTTP basic auth credentials configured through the `BASIC_AUTH_USER` and `BASIC_AUTH_PASSWORD` settings. @@ -53,12 +60,14 @@ To see a basic landing page with exposed services, go to http `NEMESIS_HTTP_SERV | yara | /yara/ | N/A | N/A | | crack-list | /crack-list/ | N/A | N/A | -# (Optional) Install logging and monitoring services by running the following: + +## (Optional) Install logging and monitoring services by running the following: ```bash helm install --repo https://specterops.github.io/Nemesis/ monitoring monitoring ``` -# (Optional) Install Metrics Server + +## (Optional) Install Metrics Server Metrics Server is available but not installed by default. Enable it with the following: ```bash @@ -78,11 +87,13 @@ If you have not installed Nemesis yet, see [Nemesis Chart](nemesis_chart.md) or helm upgrade --repo https://specterops.github.io/Nemesis/ [chart name] nemesis ``` -# (Optional) Changing Persistent File Storage + +## (Optional) Changing Persistent File Storage Elasticsearch, PostgreSQL, and Minio (if using instead of AWS S3) have persistent storage volumes in the cluster. -## File Storage Backend + +### File Storage Backend Nemesis can use AWS S3 (in conjunction with KMS for file encryption) for file storage by modifying the `storage` setting in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) and configuring the `aws` block. @@ -90,17 +101,17 @@ By default, Nemesis uses Minio for file storage with a default storage size of ` To change the size, modify the `minio.persistence.size` value in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) file. -## Elasticsearch +### Elasticsearch The default storage size is 20Gi. To change this, modify the `elasticsearch.storage` value in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml). -## PostgreSQL +### PostgreSQL The default storage size is 20Gi. To change this, modify the `postgres.storage` value in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml). -# (Optional) Change Nemesis's Listening Port +## (Optional) Change Nemesis's Listening Port Create the `traefik-config.yaml` manifest with the following content: @@ -119,15 +130,18 @@ spec: exposedPort: 8443 ``` -# (Optional) Deleting Running Pods +## (Optional) Deleting Running Pods + -## Using Helm +### Using Helm `helm uninstall nemesis && kubectl delete all --all -n default` -## Using Skaffold + +### Using Skaffold `skaffold delete` -# (Optional) Running Helm local charts + +## (Optional) Running Helm local charts If you do not want to run the Helm charts hosted on `https://specterops.github.io/Nemesis/`, you can run them locally. For example: ```bash helm install nemesis-quickstart ./helm/quickstart @@ -136,10 +150,9 @@ helm install nemesis-monitoring ./helm/monitoring ``` -# Troubleshooting, Common Errors, and Support - +## Troubleshooting, Common Errors, and Support -## Need additional help? +### Need additional help? If you run into any issues, please see [troubleshooting.md](troubleshooting.md) for common errors/issues. Otherwise, [file an issue](https://github.com/SpecterOps/Nemesis/issues) or feel free to ask questions in the [#nemesis-chat channel](https://bloodhoundhq.slack.com/archives/C05KN15CCGP) in the Bloodhound Slack ([click here to join](https://ghst.ly/BHSlack)). From 23b375b09780db9d0bc4dbaf979fbdb3210a0a9b Mon Sep 17 00:00:00 2001 From: Max Harley Date: Thu, 25 Apr 2024 15:46:28 -0400 Subject: [PATCH 13/16] Add toc to quickstart_chart.md --- docs/quickstart_chart.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/quickstart_chart.md b/docs/quickstart_chart.md index 70b9876..ce32c78 100644 --- a/docs/quickstart_chart.md +++ b/docs/quickstart_chart.md @@ -12,7 +12,7 @@ echo "Basic Auth Username: ${BASIC_AUTH_USER}" echo "Basic Auth Password: ${BASIC_AUTH_PASSWORD}" ``` -# Customizing the Configuration +## Customizing the Configuration If you want customize any of the services' secrets, you need to download the `quickstart` chart's [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/quickstart/values.yaml) file, edit it, and then run the `quickstart` chart using the customized values. You can do so with the following commands: 1. Download the quickstart chart's `values.yaml`: From 35e3bdc5232d2cf7b7ed5b09bd358b20f1cf0bfe Mon Sep 17 00:00:00 2001 From: Max Harley Date: Thu, 25 Apr 2024 15:55:59 -0400 Subject: [PATCH 14/16] Fix headings; add toc --- docs/development.md | 12 ++++++------ docs/elasticsearch-kibana.md | 21 +++++++++++---------- docs/hasura.md | 2 +- docs/kubernetes.md | 6 +++--- docs/overview.md | 2 +- docs/postgres.md | 4 ++-- docs/rabbitmq.md | 10 +++++----- docs/submit_to_nemesis.md | 17 +++++++++-------- docs/usage_guide.md | 27 ++++++++++++++------------- 9 files changed, 52 insertions(+), 49 deletions(-) diff --git a/docs/development.md b/docs/development.md index 700657c..d8afc49 100644 --- a/docs/development.md +++ b/docs/development.md @@ -20,13 +20,13 @@ sudo unzip protoc-21.5-linux-x86_64.zip -d /usr/local/ **Also ensure you have minikube and skaffold setup from the [setup](./setup.md) guide.** -# Running Nemesis during Dev +## Running Nemesis during Dev If you're doing general development, if you set the **operation.environment** variable in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) to *test* which will deploy everything without persistent storage. Then running `skaffold dev -m nemesis` will build the images and kick everything off. If you want to perform remote debugging for the `enrichment` container (see [remote_debugging.md](remote_debugging.md)) set the **operation.environment** variable in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) to *development* for the Helm chart which will deploy everything but the `enrichment` container without persistent storage. Run `skaffold dev -m nemesis` (or `helm install nemesis ./helm/nemesis --timeout '45m'` to use the public images) and then launching `skaffold dev -m enrichment` via VS Code will kick off the separate chart for just the enrichment container. -# Service Development +## Service Development The recommended way to develop a new (or modify a current) service is with VS Code and a remote workspace. This allows you to write and debug code without having to @@ -51,7 +51,7 @@ Once the remote session has been established: **Note:** If you want to reset your Poetry environment, [see this post](https://stackoverflow.com/a/70064450). -# Building and Troubleshooting Docker Images +## Building and Troubleshooting Docker Images You can build and troubleshoot Nemesis's docker containers using the docker CLI. For example, to troublehshoot the enrichment image you can do the following: 1. Build the image and give it a name of "test" @@ -69,7 +69,7 @@ To build the images inside of k8s, you can use skaffold: skaffold build ``` -# Testing file processing +## Testing file processing One can test file processing using the `./scripts/submit_to_nemesis.sh` script. To configure the script, modify the settings in `./cmd/enrichment/enrichment/cli/submit_to_nemesis/submit_to_nemesis.yaml`. The `./sample_files/` folder contains many examples of files that Nemesis can process. For example, to test Nemesis's ability to extract a .ZIP file and process all the files inside of the zip, configure the YAML file and then run (make sure to specify the absolute path): @@ -80,7 +80,7 @@ The `./sample_files/` folder contains many examples of files that Nemesis can pr To see a list of all command line arguments run `./scripts/submit_to_nemesis.sh -h`. -# kubectl / kubernetes version skews +## kubectl / kubernetes version skews According to [kubernetes](https://kubernetes.io/releases/version-skew-policy/#kubectl) it's the best practice to keep kubectl and the kubernetes image used by minikube in sync. You can tell the versions of both with: @@ -110,7 +110,7 @@ You can then specify the kubernetes imge pulled by minikube with: minikube start --kubernetes-version v1.25.4 ``` -# ./scripts/ +## ./scripts/ The following describes the files in the ./scripts/ directory: diff --git a/docs/elasticsearch-kibana.md b/docs/elasticsearch-kibana.md index e20d1c1..aa5a271 100644 --- a/docs/elasticsearch-kibana.md +++ b/docs/elasticsearch-kibana.md @@ -1,4 +1,5 @@ -# Infra +# ELK + Elastic is deployed using [the Elastic operator and is installed via helm](https://www.elastic.co/guide/en/cloud-on-k8s/master/k8s-install-helm.html). The operator runs inside the `elastic-system` namespace and its logs can be viewed with the following command: ```bash kubectl logs -n elastic-system sts/elastic-operator @@ -6,13 +7,13 @@ kubectl logs -n elastic-system sts/elastic-operator The custom resource definitions that the helm chart installs can all be found [here](https://github.com/elastic/cloud-on-k8s/tree/2.3/config/samples). The helm chart can be found [here](https://github.com/elastic/cloud-on-k8s/tree/main/deploy). -# Storage +## Storage By default this Elasticsearch instance uses a persistent data store. The size of the datastore can be adjusted in the [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) file by modifying the `storage: 20Gi` in the `elasticsearch` config section. To use temporary storage that is wiped on every run, set "environment" to "test" at the top of [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml). -# Accessing Elastic/Kibana +## Accessing Elastic/Kibana To login to Elastic or Kibana, run `./scripts/get_service_credentials.sh` to get the basic auth credentials. Example to confirm elastic is working: @@ -22,11 +23,11 @@ export BASIC_AUTH_PASSWORD=$(kubectl get secret basic-auth -o jsonpath="{.data.p curl -k -u "$BASIC_AUTH_USER:$BASIC_AUTH_PASSWORD" "https://localhost:8080/elastic/" ``` -# Troubleshooting -## Helm can't install the elastic operator +## Troubleshooting +### Helm can't install the elastic operator See https://github.com/elastic/cloud-on-k8s/issues/5325#issuecomment-1124682097. Confirmed that fix worked, as did upgrading minikube to at least v1.26.1 -## Elastic endpoints for troubleshooting +### Elastic endpoints for troubleshooting - Any issues with cluster allocation: - https://localhost:8080/elastic/_cluster/allocation/explain @@ -40,12 +41,12 @@ See https://github.com/elastic/cloud-on-k8s/issues/5325#issuecomment-1124682097. By default, Elastic will not allocate additional shard if 90%+ of the hard disk in the pod is allocated. If Minikube fills up, the drive space will be reflected as also filled in the Elastic container. This causes Kibana to fail in object creation, and Kibana will be stuck in a non-functional startup look. The solution is to ssh into minikube with `minikube ssh` and then run `docker system prune` to free up resources. -# Example queries: +## Example queries: An "easy" way to build elastic search queries is to do the search in Kibana's "Discover" page, click the "Inspect" button in the top right, and click on "Request": ![Getting ES query from Kibana](images/kibana-get-es-request.png) -## 1 - Simple search via query string for a process name and message GUID: +### 1 - Simple search via query string for a process name and message GUID: ``` curl -k -u nemesis:Qwerty12345 -XGET 'https://192.168.230.52:8080/elastic/process_category/_search?q=name:explorer.exe%20AND%20metadata.messageId:75f07c40-a4fe-4eb7-bfe6-4b1b04c79d4f&pretty' ``` @@ -53,7 +54,7 @@ curl -k -u nemesis:Qwerty12345 -XGET 'https://192.168.230.52:8080/elastic/proces More info about the query string syntax [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html#query-string-syntax). -## Same search as above, but only returning the name/category fields +### Same search as above, but only returning the name/category fields ```bash curl -k -X POST -u nemesis:Qwerty12345 -H 'Content-Type: application/json' 'https://192.168.230.52:8080/elastic/process_category/_search?pretty' -d ' { @@ -80,7 +81,7 @@ curl -k -X POST -u nemesis:Qwerty12345 -H 'Content-Type: application/json' 'http ' ``` -# Backup and Restore Data +## Backup and Restore Data **Use case:** If the minikube node ever needs to be deleted (e.g., something goes wrong to start fresh or you're done with an op) and you want to backup Elastic's data. **Backup** diff --git a/docs/hasura.md b/docs/hasura.md index b7e6af4..19424d9 100644 --- a/docs/hasura.md +++ b/docs/hasura.md @@ -1,4 +1,4 @@ -# Overview +# Hasura Overview Nemesis uses Hasura to wrap the PostgreSQL backend to easily build a GraphQL and REST API for the structure Nemesis data model. diff --git a/docs/kubernetes.md b/docs/kubernetes.md index 4a7c6b5..64749ee 100644 --- a/docs/kubernetes.md +++ b/docs/kubernetes.md @@ -1,4 +1,4 @@ -# Basics +# Kubernetes Basics | Description | command | |----------------------------|--------------------------------------------------------------------------------------------------------| @@ -12,7 +12,7 @@ May consider aliasing `kubectl` to `k` just so you don't have to type it out each time. -# Pods keep restarting due to OOMError +## Pods keep restarting due to OOMError `OOOError` = Out of memory error Reasons why it might happen include: @@ -20,7 +20,7 @@ Reasons why it might happen include: * The kubernetes node doesn't have enough memory to deploy the pods. Solution: If you're using Minikube, make sure the VM has enough memory and that minikube is configured to start with more memory (see the preq instructions for Minikube for how to configure the memory). * The application has a memory leak, and over time, consumes all the available memory dedicated to it. Solution: Fix the memory leak. -# CrashLoopBackOff Error +## CrashLoopBackOff Error To get information about the pod: `kubectl describe pod [name] --namespace=[namespace]` diff --git a/docs/overview.md b/docs/overview.md index 24ebbe0..a2ea7ce 100644 --- a/docs/overview.md +++ b/docs/overview.md @@ -1,4 +1,4 @@ -# Goal +# Overview The goal of Nemesis is to create an extensible data-processing system for Advesary Simulation operations which takes data collected from C2 agents and diff --git a/docs/postgres.md b/docs/postgres.md index a8cd9e5..3d989bc 100644 --- a/docs/postgres.md +++ b/docs/postgres.md @@ -1,4 +1,4 @@ -# Overview +# PostgreSQL Overview In addition to Elasticsearch for an unstructed/NoSQL approach, we are using PostgreSQL to store structured data such as DPAPI blobs/masterkeys/etc. @@ -6,7 +6,7 @@ The database schema for Postgres is at `./helm/nemesis/files/postgres/nemesis.sq We do not recommend interacting with Postgres directly- instead, use the [`/hasura/`](hasura.md) endpoint -# Storage +## Storage By default this PostgreSQL instance uses a persistent data store. The size of the datastore can be adjusted in [values.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/values.yaml) by modifying the `storage: 15Gi` in the postgres section. diff --git a/docs/rabbitmq.md b/docs/rabbitmq.md index 56b3ee4..ef50cd0 100644 --- a/docs/rabbitmq.md +++ b/docs/rabbitmq.md @@ -1,25 +1,25 @@ -# Design decisions +# RabbitMQ Overview * Didn't go with the RabbitMQ operator because it doesn't play nicely with skaffold and is a fairly simple wrapper for the statefulset * Doesn't delete pods/statefulsets during shutdown * Didn't go with just the docker container because to scale up you need to deploy them one at a time (so used a statefulset). -# Scaling +## Scaling Rabbitmq scales vertifcally better (so give it more CPU + RAM) Add more replicas by editing `./helm/nemesis/templates/rabbitmq.service.yaml`. -# Performance testing +## Performance testing ``` kubectl run perf-test -it --rm --image=pivotalrabbitmq/perf-test -- --uri amqp://rabbit:Qwerty12345@nemesis-rabbitmq-svc ``` -# Plumber example usage: +## Plumber example usage: plumber read rabbit --address="amqp://nemesis:Qwerty12345@192.168.230.42:5672" --queue-name="file_data" --exchange-name="nemesis" --binding-key=" " --queue-durable -f -# References +## References * https://blog.rabbitmq.com/posts/2020/08/deploying-rabbitmq-to-kubernetes-whats-involved/ * https://github.com/GoogleCloudPlatform/click-to-deploy/tree/master/k8s/rabbitmq \ No newline at end of file diff --git a/docs/submit_to_nemesis.md b/docs/submit_to_nemesis.md index 63d9350..c9c594c 100644 --- a/docs/submit_to_nemesis.md +++ b/docs/submit_to_nemesis.md @@ -1,11 +1,12 @@ -# Overview of submit_to_nemesis +# submit_to_nemesis + `submit_to_nemesis` is a CLI tool used to upload files to Nemesis. Its targeted audience is operators who want to upload files using the CLI and Nemesis developers who want to quickly test sample files. -# Docker +## Docker If you want to use the pre-build Docker container to submit artifacts to Nemesis, run [monitor_folder_docker.sh](https://github.com/SpecterOps/Nemesis/blob/main/scripts/monitor_folder_docker.sh). The only requirement for the script is Docker and wget. -# Requirements +## Requirements Install with the instructions below.
@@ -14,7 +15,7 @@ Python, Pyenv, and Poetry To get Nemesis running, Python 3.11.2 is needed, as well as Pyenv/Poetry. -## Install Pyenv +### Install Pyenv **Purpose:** Manages python environments in a sane way. 1. Install the [relevant prereqs specified by PyEnv](https://github.com/pyenv/pyenv/wiki#suggested-build-environment). @@ -44,7 +45,7 @@ eval "$(pyenv init -)" **Validation:** Running `python3 --version` should show version 3.11.2. -## Install Poetry +### Install Poetry **Purpose:** Python package and dependency management tool. ```bash python3 -c 'from urllib.request import urlopen; print(urlopen("https://install.python-poetry.org").read().decode())' | python3 - @@ -59,7 +60,7 @@ Restart your shell. **Validation:** Running `poetry --version` from the shell should output the current version. -## Install Poetry Environment for Artifact Submission +### Install Poetry Environment for Artifact Submission **Purpose:** Install the Poetry environment for ./scripts/submit_to_nemesis.sh `./scripts/submit_to_nemesis.sh` uses code from a Nemesis module that needs its Poetry environment installed first. @@ -69,10 +70,10 @@ poetry -C ./cmd/enrichment/ install ```
-# Configuring +## Configuring To use `submit_to_nemesis`, one must edit the YAML configuration file found in `cmd/enrichment/enrichment/cli/submit_to_nemesis/submit_to_nemesis.yaml` ([link to YAML file](https://github.com/SpecterOps/Nemesis/blob/main/cmd/enrichment/enrichment/cli/submit_to_nemesis/submit_to_nemesis.yaml)). This config file includes the credentials to authenticate to Nemesis, the location of the Nemesis server, and information about the operation that Nemesis will tag each uploaded file with (operator name, project, network, etc.). -# Usage +## Usage Once configured, in the root Nemesis directory run ``` ./scripts/submit_to_nemesis.sh -h diff --git a/docs/usage_guide.md b/docs/usage_guide.md index a2bc069..7663e57 100644 --- a/docs/usage_guide.md +++ b/docs/usage_guide.md @@ -21,15 +21,16 @@ For a general overview of the Nemesis project structure, see [overview.md](overv - [Elasticsearch/Kibana](#elasticsearchkibana) -# Data Ingestion +## Data Ingestion Once Nemesis is running, data first needs to be ingested into the platform. Ingestion into Nemesis can occur in muliple ways, including: + * [Auto-ingesting data from C2 platorms.](#nemesis-c2-connector-setup) * [Manually uploading files on the "File Upload" page in the Nemesis's Dashboard UI.](#manual-file-upload) * Using the [submit_to_nemesis](submit_to_nemesis.md) CLI tool to submit files. * Writing custom tools to interact with [Nemesis's API](new_connector.md). -## Nemesis C2 Connector Setup +### Nemesis C2 Connector Setup Nemesis includes connectors for various C2 platorms. The connectors hook into the C2 platforms and transfer data automatically into Nemesis. The `./cmd/connectors/` folder contains the following C2 connectors: - [Cobalt Strike](https://github.com/SpecterOps/Nemesis/tree/main/cmd/connectors/cobaltstrike-nemesis-connector#readme) @@ -42,7 +43,7 @@ Nemesis includes connectors for various C2 platorms. The connectors hook into th ***Note: not all connectors have the same level of completeness! We intended to show the range of connectors possible, but there is not yet feature parity.*** -# Nemesis Dashboard +## Nemesis Dashboard The main method for operators/analysts to interact with Nemesis data is through the Nemesis Dashboard. The dashboard can be accessed at `http://NEMESIS_IP:8080/dashboard/`. The initial display shows details about the number of processed files: @@ -65,7 +66,7 @@ For each file entry, the file path, download timestamp, size (in bytes), SHA1 ha The top icons by each file will let you download a file, view the raw file bytes in a new browser tab, optionally view a PDF of the file (if applicable) in a new browser tab, download the decompiled .NET source code for an assembly (if applicable), and view the [File Details](#file-details) for the file. -### File Triage +#### File Triage At the top right of each file card, there is a **Triage** section with 👍 , 👎, and ❓ icons. These icons correspond to "Interesting", "Not Interesting", and "Unknown", respectively. When an icon is clicked, the triage value for the file is stored in the Nemesis backend and the file card is hidden (hidden cards can be reshown via the search filters). The selected icon will be reflected in the [File Details](#file-details) page for the file. This default behavior allows for multiple operators/analysts to easily triage large numbers of files without duplicated effort. @@ -73,7 +74,7 @@ Additionally, comments can be saved for the file, which are also persistently sa ![Nemesis Dashboard File Notes](images/nemesis-dashboard-files-notes.png) -## File Details +### File Details The **File Details** page for an individual file will display the same file card (containing path/timestamp/size/SHA1/magic type/tags/comments, triage icons, and comments) as [Files](#files) page. Beneath the card the bytes of the file (or plaintext if not binary) are displayed in a [Monaco editor](https://microsoft.github.io/monaco-editor/), a Visual Studio Code-light component that contains appropriate syntax highlighting and VSCode shortcuts. @@ -91,7 +92,7 @@ Likewise, if there are any Yara matches, a **Yara Matches** tab will appear. Thi ![Nemesis Dashboard File Notes](images/nemesis-dashboard-file-viewer-yara.png) -## Manual File Upload +### Manual File Upload Files can be manually uploaded through the Nemesis dashboard via the `File Upload` tab on the left navigation bar. After navigating to the page, information such as the operator ID, ..., needs to be completed. This information is saved via browser cookies and does not need to be entered every time. The "Original File Path" is optional but recommended. Files can be dragged/dropped into the upload modal, and on successful submission Nemesis will display the following message: @@ -99,23 +100,23 @@ Files can be manually uploaded through the Nemesis dashboard via the `File Uploa The file will then be displayed in the [Files](#files) page as soon as it's done processing. -## Document Search +### Document Search Nemesis will extract plaintext from any files that can have ASCII/Unicode text extracted and indexes the text into the Elasticsearch backend. The **Document Search** page on the left navigation bar allows operators/analysts to search through any indexed text. Search can accomplished via **Full Document Search** and **Snippet Search**. -### Full Document Search +#### Full Document Search The default search will match the supplied search term(s) in indexed plaintext documents, displaying paginated searches along with some details about the orignating document: ![Nemesis Dashboard File Search](images/nemesis-dashboard-document-search-full.png) -#### Source Code Search +##### Source Code Search The **Source Code Search** tab on the top of the page functions similarly to the raw text search, but only searches indexed source code documents, as opposed to "regular" plaintext documents. To search source code, change the search index in the dropdown search filters to `source_code`: ![Nemesis Dashboard Source Code Search](images/nemesis-dashboard-document-search-source-code.png) -### Snippet Search +#### Snippet Search The **Snippet Search** tab operates a bit differently. In addition to being normally indexed in Elasticsearch, all text extracted from plaintext documents by Nemesis are also broken into chunks and run through a small [embedding model](https://www.elastic.co/what-is/vector-embedding) to produce fixed-length vector embeddings. The model currently being used is [gte-tiny](https://huggingface.co/TaylorAI/gte-tiny) but this can be modified in the [nlp.yaml](https://github.com/SpecterOps/Nemesis/blob/main/helm/nemesis/templates/nlp.yaml) of the NLP container. These embeddings are stored in Elasticsearch along with the associated chunked text, allowing for [sematic search](https://en.wikipedia.org/wiki/Semantic_search) over indexed text. @@ -127,7 +128,7 @@ If you want to _only_ use the more traditional/fuzzy BM25 search and now the vec See [this Twitter thread for more background on this approach](https://x.com/harmj0y/status/1757511877255471299). -### Search Filtering +#### Search Filtering Both "Full Document Search" and "Snippet Search" allow for file paths/patterns to include or exclude in searches. These can be wildcard paths or extensions, and multiple `|` delineated terms can be specified. @@ -139,13 +140,13 @@ To exclude .pdfs from searching: ![Nemesis Dashboard Exclude Filtering](images/nemesis-dashboard-document-search-exclude-filter.png) -# Alerting +## Alerting If Slack alerting is enabled, alerts on "interesting" files (e.g., parsed credentials, Nosey Parker hits, DPAPI data discovery, etc.) will be pushed to the configuered Slack webhook/channel with **Nemesis** as the bot user. These messages will contain the alert name, sanitized file name, file SHA1, download timestamp, agent ID, message details, and a link to the [file details](#file-details) in the dashboard: ![Nemesis Slack Alerting](images/nemesis-slack-alerting.png) -# Elasticsearch/Kibana +## Elasticsearch/Kibana Navigating to `http://NEMESIS_IP:8080/kibana/` will lead to the main Kibana dashboard for all indexed data (creds are set in `nemesis.config`) From e2e52ac5ed3023c740ed01bdae5f7f7e2f4e07f4 Mon Sep 17 00:00:00 2001 From: Max Harley Date: Thu, 25 Apr 2024 16:00:41 -0400 Subject: [PATCH 15/16] Add toc for new_connector.md --- docs/new_connector.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/new_connector.md b/docs/new_connector.md index 382afc7..827db14 100644 --- a/docs/new_connector.md +++ b/docs/new_connector.md @@ -11,11 +11,11 @@ Regardless of the connector actions, it will need to somehow save the following | Project name | PROJECT-X | | Expiration days (or date) | 100 (or 01/01/2024) | -# Download Processing +## Download Processing File processing is the one flow that differs from other structured data ingestion. First, the file bytes need to be uploaded to Nemesis, and second, a metadata message needs to be posted to kick off processing. -## Step 1 - File Upload +### Step 1 - File Upload For a file to be processed, the raw file bytes first need to be posted to the correct API route for storage in the data lake. This is accomplished by POSTing the file bytes to the `https:///api/file` which returns a simple JSON response with an `object_id` field containing a UUID that references the uploaded file. For example, to do this in Python (as shown in [mythic-connector](https://github.com/SpecterOps/Nemesis/blob/main/cmd/connectors/mythic-connector/sync.py)), you would run something like this: @@ -32,7 +32,7 @@ curl -H "Content-Type: application/octet-stream" -v --user 'nemesis:Qwerty12345' The `nemesis_file_id` is used in the `file_data` message in Step 2 below. This UUID is the unique reference for the file in Nemesis. -## Step 2 - File Data Message +### Step 2 - File Data Message After the file is uploaded to Nemesis, a [file_data](odr/references/file_data.md) ODR message needs to be posted with file metadata information. The example from the [mythic-connector](https://github.com/SpecterOps/Nemesis/blob/main/cmd/connectors/mythic-connector/sync.py) is: @@ -62,7 +62,7 @@ r = requests.request("POST", f"{NEMESIS_URL}/data", auth=basic, data=data, heade *Note that timestamps need to be in ISO 8601 UTC form, e.g., 2023-08-01T22:51:35* -# Other Structured Data +## Other Structured Data For other types of structured data, only a single message needs to be posted to the `http:///api/data` API route, e.g. Step 2 in the downloading processing example. The `metadata["data_type"]` field should be one of the types defined in the [ODR](odr/references/). The appropriate ODR document will also define the fields and structure needed for the datatype. From 98ab8bf3bcbca50074a4bda24eacbbfb925d1a7b Mon Sep 17 00:00:00 2001 From: Max Harley Date: Thu, 25 Apr 2024 16:10:00 -0400 Subject: [PATCH 16/16] Update index.md and README.md --- README.md | 8 ++++---- docs/index.md | 10 +++++----- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d93ee18..9c6b1b5 100644 --- a/README.md +++ b/README.md @@ -33,15 +33,15 @@ Built on Kubernetes with scale in mind, our goal with Nemesis was to create a ce Nemesis aims to automate a number of repetitive tasks operators encounter on engagements, empower operators’ analytic capabilities and collective knowledge, and create structured and unstructured data stores of as much operational data as possible to help guide future research and facilitate offensive data analysis. -# Setup / Installation +## Setup / Installation Follow the [quickstart guide](docs/quickstart.md) Or see the full [setup instructions](docs/setup.md) -# Usage +## Usage See the [Nemesis Usage Guide](docs/usage_guide.md). -# Contributing / Development Environment Setup +## Contributing / Development Environment Setup See [development.md](./docs/development.md) ## Further Reading @@ -54,7 +54,7 @@ See [development.md](./docs/development.md) | *On (Structured) Data* | Jul 26, 2023 | https://posts.specterops.io/on-structured-data-707b7d9876c6 | -# Acknowledgments +## Acknowledgments Nemesis is built on large chunk of other people's work. Throughout the codebase we've provided citations, references, and applicable licenses for anything used or adapted from public sources. If we're forgotten proper credit anywhere, please let us know or submit a pull request! diff --git a/docs/index.md b/docs/index.md index 88d794f..43298b7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -25,7 +25,7 @@
-# Overview +## Overview Nemesis is an offensive data enrichment pipeline and operator support system. @@ -33,13 +33,13 @@ Built on Kubernetes with scale in mind, our goal with Nemesis was to create a ce Nemesis aims to automate a number of repetitive tasks operators encounter on engagements, empower operators’ analytic capabilities and collective knowledge, and create structured and unstructured data stores of as much operational data as possible to help guide future research and facilitate offensive data analysis. -# Setup / Installation +## Setup / Installation See the [setup instructions](setup.md). -# Usage +## Usage See the [Nemesis Usage Guide](usage_guide.md). -# Contributing / Development Environment Setup +## Contributing / Development Environment Setup See [development.md](development.md) ## Further Reading @@ -52,7 +52,7 @@ See [development.md](development.md) | *On (Structured) Data* | Jul 26, 2023 | https://posts.specterops.io/on-structured-data-707b7d9876c6 | -# Acknowledgments +## Acknowledgments Nemesis is built on large chunk of other people's work. Throughout the codebase we've provided citations, references, and applicable licenses for anything used or adapted from public sources. If we're forgotten proper credit anywhere, please let us know or submit a pull request!