From 9a5a7eefb0f19e43a4c08dafdd4a9827f39309cc Mon Sep 17 00:00:00 2001 From: Jay Kumar <70096901+35C4n0r@users.noreply.github.com> Date: Sun, 15 Sep 2024 20:06:01 +0530 Subject: [PATCH 1/3] feat: add sumologic provider (#1924) Signed-off-by: 35C4n0r Co-authored-by: Tal --- README.md | 2 + docs/mint.json | 1 + .../documentation/sumologic-provider.mdx | 36 ++ docs/providers/overview.mdx | 6 + keep-ui/public/icons/sumologic-icon.png | Bin 0 -> 13535 bytes keep/providers/sumologic_provider/__init__.py | 0 .../connection_template.json | 20 + .../sumologic_provider/sumologic_provider.py | 439 ++++++++++++++++++ 8 files changed, 504 insertions(+) create mode 100644 docs/providers/documentation/sumologic-provider.mdx create mode 100644 keep-ui/public/icons/sumologic-icon.png create mode 100644 keep/providers/sumologic_provider/__init__.py create mode 100644 keep/providers/sumologic_provider/connection_template.json create mode 100644 keep/providers/sumologic_provider/sumologic_provider.py diff --git a/README.md b/README.md index 04db95c11..2a655a673 100644 --- a/README.md +++ b/README.md @@ -117,6 +117,8 @@ Workflow triggers can either be executed manually when an alert is activated or                       + +                       diff --git a/docs/mint.json b/docs/mint.json index 1f874c84e..b7022144e 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -167,6 +167,7 @@ "providers/documentation/squadcast-provider", "providers/documentation/ssh-provider", "providers/documentation/statuscake-provider", + "providers/documentation/sumologic-provider", "providers/documentation/teams-provider", "providers/documentation/telegram-provider", "providers/documentation/template", diff --git a/docs/providers/documentation/sumologic-provider.mdx b/docs/providers/documentation/sumologic-provider.mdx new file mode 100644 index 000000000..6e1be21b2 --- /dev/null +++ b/docs/providers/documentation/sumologic-provider.mdx @@ -0,0 +1,36 @@ +--- +title: "SumoLogic Provider" +sidebarTitle: "SumoLogic Provider" +description: "The SumoLogic provider enables webhook installations for receiving alerts in keep" +--- + +## Overview + +The SumoLogic provider facilitates receiving alerts from Monitors in SumoLogic using a Webhook Connection. + +## Authentication Parameters + +- `sumoLogicAccessId`: API key for authenticating with SumoLogic's API. +- `sumoLogicAccessKey`: API key for authenticating with SumoLogic's API. +- `deployment`: API key for authenticating with SumoLogic's API. + +## Scopes + +- `authenticated`: Mandatory for all operations, ensures the user is authenticated. +- `authorized`: Mandatory for querying incidents, ensures the user has read access. + +## Connecting with the Provider + +1. Follow the instructions [here](https://help.sumologic.com/docs/manage/security/access-keys/) to get your Access Key & Access ID +2. Make sure the user has roles with the following capabilities: + - `manageScheduledViews` + - `manageConnections` + - `manageUsersAndRoles` +3. Find your `deployment` from [here](https://api.sumologic.com/docs/#section/Getting-Started/API-Endpoints), keep will automatically figure out your endpoint. + +## Useful Links + +- [SumoLogic API Documentation](https://api.sumologic.com/docs/#section/Getting-Started) +- [SumoLogic Access_Keys](https://help.sumologic.com/docs/manage/security/access-keys/) +- [SumoLogic Roles Management](https://help.sumologic.com/docs/manage/users-roles/roles/create-manage-roles/) +- [SumoLogic Deployments](https://api.sumologic.com/docs/#section/Getting-Started/API-Endpoints) diff --git a/docs/providers/overview.mdx b/docs/providers/overview.mdx index 36beeb12a..60f754022 100644 --- a/docs/providers/overview.mdx +++ b/docs/providers/overview.mdx @@ -354,6 +354,12 @@ By leveraging Keep Providers, users are able to deeply integrate Keep with the t icon={ } > + } +> + YnHHnXWn0r_b~gt*NengGq@A0054XqTJhOS^nRFj`V!4C4N2o zEFhMOZ`A<6ml*({VE}OdTm}6H03Ue(;J_RJMAHC(%r)z`miTi6>U$LhIRNor$!#l1 zd9GpdlsE9SvhcJPwRE?B762a)p8zM12q*6=9bO($0e(>dK6V}+Q68S+21V2V4dCoz zWpCs2{|3zeJKcH)u>MaEp7zex9-bD?uKyQ|;Q!x=XYCc&Ge-D7%xf2CXX_81!1EYD zEL}c)vUc~hwt{%sd)h(lJUv}KM7g=GeQoV+xGY_qxxK94yK;KEaJqi5HLg4!c-BGq zpAKyo8&59_cWdB(?#%6}GXMa7S0y^3_iWhAYp%_QUn5)CO+71$n*se+;Dj9mNc0@*sxvEX z^%HuRkHV}xAh7mnE;IGErqn+AF(xB*p`&zg>;}g+wxsClDNqT!no9|>#nff0)Nyq9 ziF?vNi9-pI&jcF$po~ywl3YHsb@adUk3O*#2xV89>iA_pJ|Adg1g8_P2c|j%E~H%T zSfTqM-`%{&@ZXX2Y*ysd!OXh>V7^Jl?CqBcNk}^PQ7;v z%4_~r1f7`r`>gNTs8sUb_1D}VGW^y%>KBc{^^DJU9wn!?r`aprlf7E}`6{q@Ja8}w zk;??)yn~amVJHG@RgbNO$J|^8sZS!_|JmU^cp?ioK3|tAwO6Vod&OB3q%}q=obj^3%(OhPRW|qmFy<#+?6a?w-*$@@zD7F zOfK=$aPWf(dcKK^C>4QB9X$m5+v=!ioDQi03i5}(FU^`*o_GuEAKG}pQNiRu9?0Fa z3F{szT92{%8}Y>cQkRmJcFE-`@b&B0NjS_bEMBp3>gtT6Z(RHD)wV6S`?xN~km&J^ zd!<=LRI-RyjROCf3e>BG=kC2Q_cnAcmt1pFV|@!qqhBIdk|PV`>XtE!*Qt0Evzr4Q zPUhb3TU-^TwWV3+omTomHejb8w+i=aH4pw^l97cn0~XN( zr3u&=pv~JpcAnp_KdjcN2b^iatNxVloC2VHSK6%&nl17q6sQ{-YplE+IZcmOhjGWo z1NYNqzdmxLfhj5m2`V~(jsfU)cyV%h;Xst*i2?o$t%krgg*Bgh_R(9$h)A)KXaL33 z{oo_wu_1@x@negL@52 zzZ?&I#bm|&pRfAz-0cmsbV0Kehc%II$-Vzhq6O~e3yEL4>agE^RG(rz!zMCte6jVb z09n_0xQ9xI6AF~H|GKYCF?(D^h}^BzaT^5no8%d?P2s%~9rra0WJg7wa)_f8BMfKH z#}sf$Lly?HPdLS1_9JVLdK$rEY>pfO?CzNcsk~NQM#@)w7)T~g32l`FVXqs^pU88WrT|q zp`tvvu|g&iVA<(;C*SNg6%@rm%n7Gg8Lkt=TM+v~uD1%V5Io&rmzPG;Gy`vNI2T6N zV_0*>&^4KKU1T|ys}LgN64z7Xk7~~7Ycg-ciz)WVyCc%#K(>$L=E$<`&OBgi?>8}% z05fsm9j5=*H21>_*|>2VqTGb>%bFO&H;WDQRQy__A;~cOu-c{<7SN9@EqEaGEMmW` z#cbri1<>T9C#(_5T(XRyc#=lIn2LEYm8TE^hgDT@MmEmKffaHAAQ4cuYU}x;(H3Qa z+s%_(i;iy&OzW|+B+H42?+PDveA;Q`Tvoy+0M&arB1ZbyOA6DI7#U|2Q58G?@v8zD zQJsu(s5~jLN(l!?ZOY1ppT7t*p;F*WfARd(jifpCdJQJ?zRAZoS5XsV%pYDcPYVVd zsQy;?n6XKdn!CHI2^<#hn3K!I@`Tl(Bk>e#gkgHqF>|)=#B#6t4m(efph!x<&n3O` z`2?ViKd|UA3^v&^v-rjgOJ4%YP`#QT--n&HJC%5|VsHq6+Ksnw{f7jVu|6cgDBvhi zObYgX$;}Pc5F{(rYm*oDD1z0!O{)>(f&V$x`<|*m56iZ-(jLyL$`u#HtJlMKVYbx% zODc4Tv;y8WFvTTGDYEuQ^6q{qcX$&woTNIL6+m~$1us$U=kS_xs7OJlu1TZ?@Al&K zrR!cnAz-_9%S=fm0r*02^2$gBc7q%kDFu`01rTyXq-nEiWbpG+=f2JR1*UqiG$?#9 z!i)M1?XO*12tSrUzwIEihvq8ry}u<-+Z5v_ zemtKT$tgY#M;wt!ik$^}NI4z_uF0=fJel58{*DG!K&4fU<5sD3E3Lj4%1VvS5l_iV z@Il#}{z{y?UoNaFln*%p=(*L4*Y>T8I$a8qw^x31k(S$h(Z!Lr-dj4y3t!d+)>al2 zk7jfb;W_s?mI(o?i?DlMEL>A-mv#c%4tS^=qWZ07<&E{DBkkJH>K7Og)Lt+Hc_N5l zDdzTrc(iIU1fPI6jTQbVS!AkkSL*xf57cGaoZ2`-!@)Azw-m$?m8)*VxsHvbvn?}g z?i5yOC?@RMTQY{28ys)HWSQR@twQwnUek;YDk&9p1fg$?Jm@PfhkHSXhLCrRo&GMXD($4AlBiQ0;V(0m^T;Z2h#yRwf?|FxOxw!UTft zP?uOegSo1BWTHt#t}W+<#-6l++-_y*fg@0n0tdAhDdAy1|Cn{Ftd}S$n)554be6K2AN98xr0ruiLjgEO|6e_i^(VrADV}l_=soEevfs0yY{Z>D5A~n zRa61kI<#Dhs&d=f?>63SbdBx>DX&X=-d~C|VW+ zh};fP`(qjZhbafqUqDy)$=|`U3l}eW#U_x>UZ4L{dZ|Hcb7tJ}#rJ(rChfP}Xk8%^ z5tx(XS!wu5BSX$4dJPAU<%JFFqe2Ne9w$r+Quj2r-|u!{{(cuZus2rt5$2tD?}7xw z$2&Nk-d(!yQu{1y-gcsm{j1>N{*qIlwH*TW8S<9>t*IElT8y%Gt2Ovk&e0owp#z}B z(H|Mls@)9exBq%d9)IAAzo!kD*-Q|yRw3Io9vA7m9RBl&yivjU*>A_GuRsljB<~QJHHSeL(=l96xRVR?k+>2ET6hv?`~8ylylAu@ zM}Y9_aJxCrw>(NftojEH`@0$q;e(-J!~|2pg}f0Tvsz}S2ZB2bt%rcky=BxB(>xs= zJ-^ofTa-n1Hgr5QV@@BfiBRVGpqgO@6Lem0Vj_mco z)^^b-6?9lDmUhrj+jMfNFMa_tZTh(rwE#`QrlH6;ia{n(RbBa!0kEzV`{%s37UdD^ zQ|E940}bdzAJZVZ0u&_a$I3l5T3FbX@17<`#`wRL+M?G!?BclwNxkM2@cT?bY4A$4 z9R%>u-iQdsf>ZMRk1?NeHvjDJ#~3vPkX|SQTSx6j*7gW%Ub2 z=Q5FtY=$A@1uHK7XTjJr#Yp zw1xTpVb?HUau_96`tB}YZek)X1ztHF^!E5HLcMBwsZATuRVU@O#ta?n$Z}K3D~F)( z{4iJ(QthnM2$XBcfKpAznn3Y0I=vhCo@zTJEbh#yJ{xc(!Q)ANg9e*9l94rVu+JBI z>BfXFX68H2HgOW#7xZ7={w-1e{clU?$Gc`u+?Brnb z$AEvZ&^+-c_PQOaP}c6wPJrm_pPrQ z2$KLw9C6C*XPR=|kX*wUg>nCz7_$fc1Tat!wfgxqP7<1}=`wd~(+^pYy=I z_rML16!5;oe${!A-E^PfD0#ZepZ)P40d@cK4~ppSns$c*J*7&T?{+Hfi6 zOri6-_KUB2OWGrYSpg|P=bWPryFn~(Kkb_&z-PzLw%J`;56UYVot~F=B3c!*df41_ zrMl`qCv4oJS6ZQ=)dUOPiPA`bsd!I4;fn7tr<{$2-s2It-1oR)o_X-MgIqXl-BmJ0 z2*sj7>LGKUL+a5QcE8_X3|p&+6@zaWj)*i0zz;Ah}v{s@CHRta0#{7B_a?pNkn8<1PN&{ z4{1P0M)%>?-V(sxIy!Oa*8405>(vN!=lAr%U=yPwB_3^w+646Sj9r?0UdaStFMaJbjwkB;OBmiw{*6@^D-vn=h)u$8Qw z?g1Bl^Jc&IX^++;qruzDOgCYei2w!?*j4&7E2_9dfB|pVCOtN8uHNe+OdA- zOWq#|OCLwM=cR58ZG%qOm#0J{9I{j)6ric@c6xblFqxQ$9s>&20*NJhgk=aufv~Qs z2yi=~-~vxbFdjafmmmKZ!>8F6uXVqx|`P ztZfg5gzEY*(0hB*Y_r{ycXZ%XJ;D)zhsIBq%Z=#djivV4tCqAyENJXjeOZ%z-sPn_ zk?nqe5^@l&H_mHL5s|75NFcdQ3vmjWPYdUp!Cjlxyx~o4I}sXYrv;qGKhQgo%VLOm z>oGAP&3;Nc!FN#_t2hS~GyHWS@&G9Ghd}dALZ<1HK3(95C7#IRWs17b6R6_)bwRjw zk0KbNr5hYo9`*U9F_C5>>r9tXZ0vLXV>NL9j~RT`MJS0D9UBk~|K?AUCmZ>j+XL#k zE<+q|A>rxChjEs87%1qwg!MGeZu;>+Y|ZUW;LW!FpW)44i=iD^u-q|2dX1V>3Y7XNA zb;Ku}VRUr;bI!j`ew#fUWn^|-7nvD~$W0>yUK19Qu?m%Hid3ma5sCuWe3x*k*dlU5 zvoi;sw(=7$Cr_{SqS5QHk(uQ&|LKrgDL-FyVNDtxH0oa9RRj}|9!ZR(hEkS0sn0tb zSbmvqJ$%2dT$-;AvJPHtwTJJjEi<0EIm>&LfAxE03hM(%c&gB=rNMyW@?z})MpW;w}R2XP(3kErIFG?ri(k1M_fV@u4nR5?kA8j<*Xb= zDjf zgip;IM=+pTfRqF(pWaoj`Ss0|U7bU@2ny@KXEImK-Zo8AwIaDVn{K%+`Me4I@4fiN zG|pz%jUN~E9TJXEccsWg7_2Jw-_VZh#?v$c6_GbU>v#~cbr-Fi91o_zbQpDTE@m~o zPOC#fI#R}OQhc^tpEY(1Z_w{e6f7*nzOGFb7ub(BNxQljdi7RJG&P2i>mRF(GELMY z#{~O{ID1dnDAgJDo`>ezf_;vwJ*^8Ip9DIN&(!Oq+`>`wx@sgyg6IA{t?f28U^bTf z(&U`CiZJEE+_!i4XCR8_xX_LrZEo8z=fhVv|i_DqDLU@cI?#s*Q$7qwA7sXp~L~ zme9^d!uy>G{|~toEr`{{<|E1D?IVUF+=V|)$DH$_iOOzj62+#>o@nkf%dx!r@tdqU zs#u?g>yB)^)7~rT9=pTFm~*Ei<|-}L{iyZYsNOVx@B2nZBrUmo?ACO`Jo>6K3NlmY zj=R(MMYcFN-Cjj4FwV|?7q%j22+621EWybbn=@Cp=2y^94dwUNNaj7-vM!11a7S0n zc4!=sniXVWC6S_UgHd2yS9VE6!CVpmmSe9m{`ZY7r8Cz-Gyk9kj3z6`l3DoR9y@thMoIqe>hEH?jNX`yMRq>v==glFNMGSF1D| zW+DpQ)|&%D>BLRLM1Xb(I>~WtCWge-gGG{%ax}^0x^Ftg)vrn0F%jec#4ld$le!|S zY}40dBU;Uhk7R1HdgqJ3lsh!n+ZSe8pnXT`Ls|R+i!~SAM-Vz_{DIGHX#v!0u|l@6 zP*iLvv}DnNR(pLEzVg*W4|(V`-k<=VID9R}qpW-h5>~6yri}TE!4m|j-fFkX{spVg zJsQ{Z1$<7e1&=W`KuAA;#)2B#+yLwMcqoq8Isw_Q{6flbfBMPPE#gU;muk#Pf9ESh z4VQ|@)0LL~I04p1ax2q6lgu>9__$oSFadq_I#zN|X)~C#l

5eO0SDZTPb^jb+5& z3%wdcednSdlf8HQ|3)1EFV9>ZyH`##{o3KjIo@_17-s(W)28s>+jyejN92F7*K>R7 z9(EH0xDO{bSvPqxLAPyYIF~fSB1KvT>B<@O<&Vg}Pd3k_vd;4NvTL)jCh&P{Jy`%R&Rr+0w-;TojUKz((IYi zc)-zxhS>E$L3!wF9#R>4z3_5MmJVaQ?k1L0Bj9C7PF!MydAe*hrc<0nq2l;x`r~h^ME_u>e?y zl-2BkhS{ZyMi8p`Z(?n>B3)HubI9k@KPpXG7vl(@y@(p(*+6W$ue;4KzhSENS|=+c zH>TlsBTCRJr(^8qD(vjB7&7H)l>O2qW{CU0e&cul;yOCSx@8)MGS2#}tl(5X z7cH-E`u;6ThUkfs%G(K=BZEQRxwH`}S7n3km@h4S2bS80+s}Tog0}(xbJL{ z_ohP*kA-^#d0)IoxnNv$4!TR;?zs9+CV9jvU}P?9XYs4cmGRDFIMUa-`8Nulj;tjJ z601ekDFPGQtT4O(cfO;ITc9fl`Sx0w7Mr3?tktDrIGe2|c6Jya0GZ>HF9@Um+5YI_ zf9|0!+1u1P0^|ddqxhHU$&9%IF?d5>&X|}R)au@Srwu|mH@7AE;e)IWK$&t+m@)x9 zSQc7ic}Pr5s~#<{`08^(@KYc^?`N-U_~mv^jdJxW4@eDose_F)>(2nuwQQK{_GUOV zgWt0RJ!tor1$+Ff==O;ccAT2Hp9mXCUF0)Fx895M9bSUeonE2fck<+;Tb*`)n^2Gp zxIdMCU8MfbijkWF^*X{+C?3UDYRElDV}}8w>}n)n4y6{708j*hCs_~6sz;g z-Z6G-LN`FVVEPd9Vg31FUo&#v=Q!yHphsj^H}FM6E1>p62rK`~QgRj^km^x{{uZZg zJIBWtO36F1LnC70UF8c&NB~P{-eab|-pu&x~ z!q?GgRqtw+h8eYWqC?_M^f>ELkC9~|>B(XVe&9yE~;rO5B@j#Scxah)z?%z1XZ~U}} zOSIbzcJGbSv$V;#TZ8*aq<$SH$8-c7j^4z_2|jV$f*Rse3!~T zM7$lN$;3xc9YFQ-wrf{K5`J~n1Nfdfy3Pq!xGw-iO(Hu+s?VdG?Z(-=Cw=nzL42tRRwqRZ9tIQ z+XZD!48Dta`ub`4U~{G}MWUd)d_2T$#(KB_HV!BVZW8n`__4|V=s|@We$XH9&sZdn z?c4`fB_&FDkswZeTht)V+e}hwZJlq99XunJTPA*A+?4(4AA$LsAKZj$0E;3zM28B8 z%GiHDhHCxx0I$MKoj93xNKS2|_;9~?eeLPbc*oTj?F653G&A4Ru6I^GID9v+$vZ&- z!>7R)PiHgJW7M=pO_qnPv#wc=U~4w5>$VOl$JfvQ;Zl(|j_RsfPnM43=E@ljIctN; z*$F#1l6Dv!O|R!|*WAQngFBI%pcMD4l|-^0Ta}yh zA&L;bh;?Z_`$%{G1VrkV{Zy^ynsHZQRO1!qC>Rt1!@H6>|1gL$)e)W zb-~TzgRk6(c@1YUgRXE0Qun`q_UjLQsjz^X-@=l1L-M#u5X~8I&(#~mMrT3b*)Zp~ zpdite+~bvHmn(cK_n#=3lV{9DYtF$G$jtHHw@WNEXu#jW`~=1@onZ1(zL6rbHP^vE z`U;bx2?ky5!iB=n(sW!?kE@wXvzxnn;mVky9amC23H2Hu~P|f%vo;nFEVr zz$uy3py>nv?9{KH{U-{xz7GMUH;goKf(_j6gj{~2z=fEpK&)5V%tW}>C_+geXqVfq z;X3TWzJxSksG2Ei%$QC~Q597WDS44IRGNIrMR9+@@2U>T(}IV7H2r9r`I(C9uxy*? zyp~M(As2@kog5@RN4^g>EuN4|N{keJcRHElvdS}IJKyN3x)I=J$BX-=x2E&_-%J|s z^PGF9m16@_N3}AqeYh%QfpJklG5*3gS(Li)b#q9x&a%ELxyCP~ zj3TYaBB29#d4dDanG?N?pXbea*|zpU9Tp$69U7F@UUWzHDH6Xt8z!#ZPppK;;_A02JXa_^5t$UnyL2>b&w7R>WS#u|4E&9=?#a%-aQwAN} zwBxaoW0Wm$;zQNl67Q4?^!kL_Du7OsR!(kXfs4r2fny_lrdS`UR%5}&FJl)ZhKXwG=1ZA_%}#wK z9GxF_8Q1yaFFV(z^rgO7X&TF#{DI0G%E}gkvYTu(_UJ9G)%${qWwPD6t6d0w zl=N1GzxdL=UZqeFio^rd2S}NuGcA27Lozu)KZ`}00jIf{6ANgX583rO@-l)s(9N+L z{`{OJj*)G#M{O$zsK5T!!X*~lZI_Ml#&$Ytw@}qG$N6~OID?a`W}}%X_?3MIDS3V6 zRY51~FGF>kPLWcm$FI6D&1J3+woYf+aGjfVw45FOuk z;G%IzCv5&N28={vS$lD?>J=Ywc-UdbgnrD^DT^OL80uda z9t_F~+T>RkyBgeZ?i4hqvV1NURF;{Wam#N4+jn#OSjL0;`86tMB+8|kq$QNX6{Em! zh`k;N%$~Nq;N`HxSvgkp(X~@l`onP~;`sAn6$(}#OPxPI=BqHC&k*T% zU1Z#1o6A!EH*6&|P8CgprzyWaQ5~s;_IOD=@b05Y`c5STUbm;1RhKiBX1qJD2n&97 zD$Orb5zfDM84bm&(D-t1NAI(492C^_STf9^4$x;JFO#{mwRCXbsY>jnOM3jW87x(( z`wr97uq)yG-iw9L+FOL0iZ4?@^Son}yKP@dBhV^CddsQBPA(=`{w z-MmL)e;__dI9~q;rNKuaHvpiD7kCQP)MULO(j9t#JUl;$f!|tvUABQ41>OdRN)chhv63$#a3ss0Lsb7qtycvb*fwuNn z@~us<`kylWoYw;H`iTsN&cEh)nWO8;m<|#fNNMY#kS7;0Xc2e^I~EA&#$vLyMryR- z{j%}dQBi;F=YCha3IqqM@E7vE(ju!jN`&TvPdm-cf}RINnHZUzmi!tiVpQW!KBeo3uy%PYA*K*^Id0D0~7WS+GFfC?UdzknLSE^yu8W z2lI1Z~vyRO(yzGUL-!zIuQ_f z$JHpEW~z&q*%VL} zFq6&DTaSuHusv7MT^s1(~&Jxs)`im`oedmcD61xUqZgWC@+sTWde|nOXMm zL>S>KJzp3V_)5B>kYIeI zF0=U>IlnZb2X;F>_bto!x*);u;z&C_pv%6W+B3K4155C3?sl~-#C)6e=@cz1;OxKr zQo)CD?^pAae}UVvc#0wWKTsB&;}klDWXn zPlEx0_DJpbWs%JIceOdDlR>_CiV11>(G!&~%_t zb$6w!Z^|!fdc%f=RJ!wEh*Aw1r)J^{I{RL?UR1g}seN6CIjZN;B6je7IOmIw=eHd( zm(T{yf2YS%{HI~_*h%ydw(6zts$wfRbCd3)boKo4?n$v9NEu1L;TKaV3&@w_OJn4d zgQgttJ83V=<#n2&y{ zJy8n{^4mK~E;_C)ez6R;+jYKarTXeVga7$65o({Sd1XUtRzga>p+=^n1U@CMrDgb_ z*joq`l)~!sH66C>aj>|we$ui?`s(}f@(1RRyn)3IoYO&5IDhR!k8Lx2cTY0D`=`eN zR#5);DsOa+m6nvvLwTxsi^}v_$S?BMFp&5#bMi3?@yE$C8iagd_hyRM&##6vRqD|> z)1IoMLD4DtbU&HD?aF z!nrxztLG~PSY`qx?Up8*6Q*5)`A>e~*!bliO)CYeAM7u2`weI=_Kd@Zzsft>_TP=S z+9tg%R-BH1kNVVsX9aU6ch9hZiN^Z#kx{qoe@7tUdVfc_5TdP(-iDF4qJpOa(> zQ~2Izy>xcG#i>2`^~Lv1d?gOnF51Zu65>^y4CmdGEHiz4V-H3 zdfrE2&v0_6OzYz10KZ>z%KN&C^4)p_Z1{?if0_UB_lIk;8<-VohM(tY>`g$fsab0) z;G=c3J2X;@o5j#8llU#SL|1&R~_2U=pN<& zG}}SRolt){{q=?4n{LUSeHa|t*6tt2J==Y9`YpNaKGinoasYu>dHVXE7msZHnVAD9 M$*ar#lr<0eUm~~i&Hw-a literal 0 HcmV?d00001 diff --git a/keep/providers/sumologic_provider/__init__.py b/keep/providers/sumologic_provider/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/keep/providers/sumologic_provider/connection_template.json b/keep/providers/sumologic_provider/connection_template.json new file mode 100644 index 000000000..639d40c08 --- /dev/null +++ b/keep/providers/sumologic_provider/connection_template.json @@ -0,0 +1,20 @@ +{ + "name": "{{Name}}", + "description": "{{Description}}", + "monitorType": "{{MonitorType}}", + "query": "{{Query}}", + "queryURL": "{{QueryURL}}", + "resultsJson": "{{ResultsJson}}", + "numQueryResults": "{{NumQueryResults}}", + "id": "{{Id}}", + "detectionMethod": "{{DetectionMethod}}", + "triggerType": "{{TriggerType}}", + "triggerTimeRange": "{{TriggerTimeRange}}", + "triggerTime": "{{TriggerTime}}", + "triggerCondition": "{{TriggerCondition}}", + "triggerValue": "{{TriggerValue}}", + "triggerTimeStart": "{{TriggerTimeStart}}", + "triggerTimeEnd": "{{TriggerTimeEnd}}", + "sourceURL": "{{SourceURL}}", + "alertResponseUrl": "{{AlertResponseUrl}}" +} diff --git a/keep/providers/sumologic_provider/sumologic_provider.py b/keep/providers/sumologic_provider/sumologic_provider.py new file mode 100644 index 000000000..802c62814 --- /dev/null +++ b/keep/providers/sumologic_provider/sumologic_provider.py @@ -0,0 +1,439 @@ +""" +SumoLogic Provider is a class that allows to install webhooks in SumoLogic. +""" + +import dataclasses +from datetime import datetime +from pathlib import Path +from typing import List, Optional +from urllib.parse import urlencode, urljoin, urlparse + +import pydantic +import requests + +from keep.api.models.alert import AlertDto, AlertSeverity, AlertStatus +from keep.contextmanager.contextmanager import ContextManager +from keep.providers.base.base_provider import BaseProvider +from keep.providers.models.provider_config import ProviderConfig, ProviderScope + + +class ResourceAlreadyExists(Exception): + def __init__(self, *args): + super().__init__(*args) + + +@pydantic.dataclasses.dataclass +class SumologicProviderAuthConfig: + """ + SumoLogic authentication configuration. + """ + + sumoAccessId: str = dataclasses.field( + metadata={ + "required": True, + "description": "SumoLogic Access ID", + "hint": "Your AccessID", + }, + ) + sumoAccessKey: str = dataclasses.field( + metadata={ + "required": True, + "description": "SumoLogic Access Key", + "hint": "SumoLogic Access Key ", + "sensitive": True, + }, + ) + + deployment: str = dataclasses.field( + metadata={ + "required": True, + "description": "Deployment Region", + "hint": "Your deployment Region: AU | CA | DE | EU | FED | IN | JP | KR | US1 | US2", + }, + ) + + +class SumologicProvider(BaseProvider): + """Install Webhooks and receive alerts from SumoLogic.""" + + PROVIDER_DISPLAY_NAME = "SumoLogic" + + PROVIDER_SCOPES = [ + ProviderScope( + name="authenticated", + description="User is Authorized", + mandatory=True, + mandatory_for_webhook=True, + alias="Rules Reader", + ), + ProviderScope( + name="authorized", + description="Required privileges", + mandatory=True, + mandatory_for_webhook=True, + alias="Rules Reader", + ), + ] + + def __init__( + self, context_manager: ContextManager, provider_id: str, config: ProviderConfig + ): + super().__init__(context_manager, provider_id, config) + + def dispose(self): + """ + Dispose the provider. + """ + pass + + def validate_config(self): + """ + Validates required configuration for SumoLogic provider. + + """ + self.authentication_config = SumologicProviderAuthConfig( + **self.config.authentication + ) + + def __get_headers(self): + return { + "Content-Type": "application/json", + "Accept": "application/json", + } + + def __get_url(self, paths: List[str] = [], query_params: dict = None, **kwargs): + """ + Helper method to build the url for SumoLogic api requests. + + Example: + + paths = ["issue", "createmeta"] + query_params = {"projectKeys": "key1"} + url = __get_url("test", paths, query_params) + # url = https://api.sumologic.com/api/v1/issue/createmeta?projectKeys=key1 + """ + if self.authentication_config.deployment.lower() != "us1": + host = f"https://api.{self.authentication_config.deployment.lower()}.sumologic.com/api/v1/" + else: + host = "https://api.sumologic.com/api/v1/" + url = urljoin( + host, + "/".join(str(path) for path in paths), + ) + + # add query params + if query_params: + url = f"{url}?{urlencode(query_params)}" + + return url + + def validate_scopes(self) -> dict[str, bool | str]: + perms = {"manageScheduledViews", "manageConnections", "manageUsersAndRoles"} + self.logger.info("Validating SumoLogic authentication.") + try: + account_owner_response = requests.get( + url=self.__get_url(paths=["account", "accountOwner"]), + auth=self.__get_auth(), + headers=self.__get_headers(), + ) + + if account_owner_response.status_code == 200: + authenticated = True + user_id = account_owner_response.json() + self.logger.info( + "Successfully retrieved user_id", extra={"user_id": user_id} + ) + else: + account_owner_response = account_owner_response.json() + self.logger.error( + "Error while getting UserID", + extra={"error": str(account_owner_response)}, + ) + return { + "authenticated": str(account_owner_response), + "authorized": "Unauthorized", + } + + self.logger.info("Fetching account info...", extra={"user_id": user_id}) + account_info_response = requests.get( + url=self.__get_url(paths=["users", user_id]), + auth=self.__get_auth(), + headers=self.__get_headers(), + ) + + if account_info_response.status_code == 200: + role_ids = account_info_response.json()["roleIds"] + self.logger.info( + "Successfully fetched account info", extra={"roles": role_ids} + ) + else: + account_info_response = account_info_response.json() + self.logger.error( + "Error while getting account info", + extra={"error": str(account_info_response)}, + ) + return { + "authenticated": authenticated, + "authorized": str(account_info_response), + } + + # Checking if the required permissions exists + for role_id in role_ids: + role_info_response = requests.get( + url=self.__get_url(paths=["roles", role_id]), + auth=self.__get_auth(), + headers=self.__get_headers(), + ) + if role_info_response.status_code == 200: + role_info_response = role_info_response.json() + self.logger.info(f"Successfully fetched role: {role_id}") + for capability in role_info_response["capabilities"]: + if capability in perms: + perms.remove(capability) + else: + role_info_response = role_info_response.json() + self.logger.error( + f"Error while getting role: {role_id}", + extra={"error": str(role_info_response)}, + ) + return { + "authenticated": True, + "authorized": str(role_info_response), + } + if len(perms) == 0: + self.logger.info("All required perms found, user is authorized :)") + return {"authenticated": True, "authorized": True} + + except Exception as e: + self.logger.error("Error while getting User ID " + str(e)) + return {"authenticated": str(e), "authorized": str(e)} + + def __get_auth(self) -> tuple[str, str]: + return ( + self.authentication_config.sumoAccessId, + self.authentication_config.sumoAccessKey, + ) + + def __get_connection_id(self, connection_name: str): + params = {"limit": 1000} + while True: + connections_response = requests.get( + url=self.__get_url(paths=["connections"]), + headers=self.__get_headers(), + params=params, + auth=self.__get_auth(), + ) + if connections_response.status_code != 200: + raise Exception(str(connections_response.json())) + connections_response = connections_response.json() + for connection in connections_response["data"]: + if connection["name"] == connection_name: + return connection["id"] + + if connections_response["next"] is None: + break + params["token"] = connections_response["next"] + return None + + def __update_existing_connection(self, connection_id: str, connection_payload): + self.logger.info(f"Updating the connection: {connection_id}") + connection_update_response = requests.put( + url=self.__get_url(paths=["connections", connection_id]), + headers=self.__get_headers(), + auth=self.__get_auth(), + json=connection_payload, + ) + if connection_update_response.status_code == 200: + self.logger.info(f"Successfully updated connection: {connection_id}") + return connection_update_response.json()["id"] + else: + connection_update_response = connection_update_response.json() + self.logger.error( + f"Error while updating connection: {connection_id}", + extra={"error": str(connection_update_response)}, + ) + raise Exception(str(connection_update_response)) + + def __create_connection(self, connection_payload, connection_name: str): + self.logger.info("Creating a Webhook connection with Sumo Logic") + + try: + connection_creation_response = requests.post( + url=self.__get_url(paths=["connections"]), + json=connection_payload, + headers=self.__get_headers(), + auth=self.__get_auth(), + ) + if connection_creation_response.status_code == 200: + self.logger.info("Successfully created Webhook connection") + return connection_creation_response.json()["id"] + if connection_creation_response.status_code == 400: + connection_creation_response = connection_creation_response.json() + if ( + connection_creation_response["errors"][0]["code"] + == "connection:name_already_exists" + ): + self.logger.info( + "Webhook connection already exists, attempting to update it" + ) + connection_id = self.__get_connection_id( + connection_name=connection_name + ) + return self.__update_existing_connection( + connection_payload=connection_payload, + connection_id=connection_id, + ) + + raise Exception(str(connection_creation_response)) + else: + connection_creation_response = connection_creation_response.json() + self.logger.error( + "Error while creating webhook connection", + extra={"error": str(connection_creation_response)}, + ) + raise Exception(connection_creation_response) + except Exception as e: + self.logger.error("Error while creating webhook connection " + str(e)) + raise e + + def __get_monitors_without_keep(self, connection_id: str): + monitors = [] + params = {"query": "type:monitor"} + monitors_response = requests.get( + url=self.__get_url(paths=["monitors", "search"]), + params=params, + headers=self.__get_headers(), + auth=self.__get_auth(), + ) + + if monitors_response.status_code == 200: + self.logger.info("Successfully fetched all monitors") + monitors_response = monitors_response.json() + for monitor in monitors_response: + print(monitor) + for notification in monitor["item"]["notifications"]: + if notification["notification"]["connectionId"] == connection_id: + break + else: + monitors.append(monitor["item"]) + return monitors + else: + monitors_response = monitors_response.json() + self.logger.error( + "Error while getting monitors", extra=str(monitors_response) + ) + raise Exception(str(monitors_response)) + + def __install_connection_in_monitor(self, monitor, connection_id: str): + self.logger.info(f"Installing connection to monitor: {monitor['name']}") + monitor["type"] = "MonitorsLibraryMonitorUpdate" + triggers = [trigger["triggerType"] for trigger in monitor["triggers"]] + keep_notification = { + "notification": { + "connectionType": "Webhook", + "connectionId": connection_id, + "payloadOverride": None, + "resolutionPayloadOverride": None, + }, + "runForTriggerTypes": triggers, + } + monitor["notifications"].append(keep_notification) + monitor_update_response = requests.put( + url=self.__get_url(paths=["monitors", monitor["id"]]), + headers=self.__get_headers(), + auth=self.__get_auth(), + json=monitor, + ) + if monitor_update_response.status_code == 200: + self.logger.info( + f"Successfully installed connection to monitor: {monitor['name']}" + ) + else: + raise Exception(str(monitor_update_response.json())) + + def setup_webhook( + self, tenant_id: str, keep_api_url: str, api_key: str, setup_alerts: bool = True + ): + try: + parsed_url = urlparse(keep_api_url) + + # Extract the query string + query_params = parsed_url.query + + # Find the provider_id in the query parameters + # connection_template.json is the payload that will be sent to keep as an event + provider_id = query_params.split("provider_id=")[-1] + connection_name = f"KeepHQ-{provider_id}" + connection_payload = { + "type": "WebhookDefinition", + "name": connection_name, + "description": "A webhook connection that pushes alerts to KeepHQ", + "url": keep_api_url, + "headers": [], + "customHeaders": [{"name": "X-API-KEY", "value": api_key}], + "defaultPayload": open( + rf"{Path(__file__).parent}/connection_template.json" + ).read(), + "webhookType": "Webhook", + "connectionSubtype": "Event", + "resolutionPayload": open( + rf"{Path(__file__).parent}/connection_template.json" + ).read(), + } + # Creating a sumo logic connection + connection_id = self.__create_connection( + connection_payload=connection_payload, connection_name=connection_name + ) + + # Monitors + monitors = self.__get_monitors_without_keep(connection_id=connection_id) + + # Install connections in monitors that don't have keep + for monitor in monitors: + self.__install_connection_in_monitor( + monitor=monitor, connection_id=connection_id + ) + except Exception as e: + raise e + + @staticmethod + def __extract_severity(severity: str): + if "critical" in severity.lower(): + return AlertSeverity.CRITICAL + elif "warning" in severity.lower(): + return AlertSeverity.WARNING + elif "missing" in severity.lower(): + return AlertSeverity.INFO + + @staticmethod + def __extract_status(status: str): + if "resolved" in status.lower(): + return AlertStatus.RESOLVED + else: + return AlertStatus.FIRING + + @staticmethod + def _format_alert( + event: dict, + provider_instance: Optional["SumologicProvider"] = None, + ) -> AlertDto: + return AlertDto( + id=event["id"], + name=event["name"], + severity=SumologicProvider.__extract_severity( + severity=event["triggerType"] + ), + fingerprint=event["id"], + status=SumologicProvider.__extract_status(status=event["triggerType"]), + lastReceived=datetime.utcfromtimestamp( + int(event["triggerTimeStart"]) / 1000 + ).isoformat() + + "Z", + firingTimeStart=datetime.utcfromtimestamp( + int(event["triggerTimeStart"]) / 1000 + ).isoformat() + + "Z", + description=event["description"], + url=event["alertResponseUrl"], + source=["sumologic"], + ) From 0a3262861da74cf774333b0194ba4cf8c0825651 Mon Sep 17 00:00:00 2001 From: Tal Date: Sun, 15 Sep 2024 17:52:13 +0300 Subject: [PATCH 2/3] fix(api): provider that fails to pull may sometime fail other providers (#1930) --- keep/api/routes/preset.py | 82 ++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/keep/api/routes/preset.py b/keep/api/routes/preset.py index bb4cb3c00..5af7768db 100644 --- a/keep/api/routes/preset.py +++ b/keep/api/routes/preset.py @@ -92,47 +92,59 @@ def pull_data_from_providers( provider_config=provider.details, ) - logger.info( - f"Pulling alerts from provider {provider.type} ({provider.id})", - extra=extra, - ) - sorted_provider_alerts_by_fingerprint = ( - provider_class.get_alerts_by_fingerprint(tenant_id=tenant_id) - ) - try: - if isinstance(provider_class, BaseTopologyProvider): - logger.info("Getting topology data", extra=extra) - topology_data = provider_class.pull_topology() - logger.info("Got topology data, processing", extra=extra) - process_topology(tenant_id, topology_data, provider.id, provider.type) - logger.info("Processed topology data", extra=extra) - except NotImplementedError: - logger.warning( - f"Provider {provider.type} ({provider.id}) does not support topology data", + logger.info( + f"Pulling alerts from provider {provider.type} ({provider.id})", extra=extra, ) - except Exception as e: - logger.error( - f"Unknown error pulling topology from provider {provider.type} ({provider.id})", - extra={**extra, "error": str(e)}, + sorted_provider_alerts_by_fingerprint = ( + provider_class.get_alerts_by_fingerprint(tenant_id=tenant_id) + ) + logger.info( + f"Pulling alerts from provider {provider.type} ({provider.id}) completed", + extra=extra, ) - # Even if we failed at processing some event, lets save the last pull time to not iterate this process over and over again. - update_provider_last_pull_time(tenant_id=tenant_id, provider_id=provider.id) - - for fingerprint, alert in sorted_provider_alerts_by_fingerprint.items(): - process_event( - {}, - tenant_id, - provider.type, - provider.id, - fingerprint, - None, - trace_id, - alert, - notify_client=False, + try: + if isinstance(provider_class, BaseTopologyProvider): + logger.info("Getting topology data", extra=extra) + topology_data = provider_class.pull_topology() + logger.info("Got topology data, processing", extra=extra) + process_topology( + tenant_id, topology_data, provider.id, provider.type + ) + logger.info("Processed topology data", extra=extra) + except NotImplementedError: + logger.warning( + f"Provider {provider.type} ({provider.id}) does not support topology data", + extra=extra, + ) + except Exception as e: + logger.error( + f"Unknown error pulling topology from provider {provider.type} ({provider.id})", + extra={**extra, "error": str(e)}, + ) + + for fingerprint, alert in sorted_provider_alerts_by_fingerprint.items(): + process_event( + {}, + tenant_id, + provider.type, + provider.id, + fingerprint, + None, + trace_id, + alert, + notify_client=False, + ) + except Exception: + logger.exception( + f"Unknown error pulling from provider {provider.type} ({provider.id})", + extra=extra, ) + finally: + # Even if we failed at processing some event, lets save the last pull time to not iterate this process over and over again. + update_provider_last_pull_time(tenant_id=tenant_id, provider_id=provider.id) @router.get( From 1f5a4d05b6aadbb92da8a01c165185b0ee6d82f1 Mon Sep 17 00:00:00 2001 From: Shahar Glazner Date: Sun, 15 Sep 2024 18:10:25 +0300 Subject: [PATCH 3/3] fix: calculate common path correctly (#1932) --- keep/workflowmanager/workflowstore.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/keep/workflowmanager/workflowstore.py b/keep/workflowmanager/workflowstore.py index 27fd8c898..af1ae746b 100644 --- a/keep/workflowmanager/workflowstore.py +++ b/keep/workflowmanager/workflowstore.py @@ -272,10 +272,10 @@ def provision_workflows_from_directory( # Check for workflows that are no longer in the directory or outside the workflows_dir and delete them for workflow in provisioned_workflows: - if ( - not os.path.exists(workflow.provisioned_file) - or not os.path.commonpath([workflows_dir, workflow.provisioned_file]) - == workflows_dir + if not os.path.exists( + workflow.provisioned_file + ) or not workflows_dir.endswith( + os.path.commonpath([workflows_dir, workflow.provisioned_file]) ): logger.info( f"Deprovisioning workflow {workflow.id} as its file no longer exists or is outside the workflows directory"