misp-modules 2.4.196__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (194) hide show
  1. misp_modules/__init__.py +379 -0
  2. misp_modules/helpers/__init__.py +1 -0
  3. misp_modules/helpers/cache.py +84 -0
  4. misp_modules/lib/__init__.py +4 -0
  5. misp_modules/lib/_vmray/__init__.py +0 -0
  6. misp_modules/lib/_vmray/parser.py +1417 -0
  7. misp_modules/lib/_vmray/rest_api.py +148 -0
  8. misp_modules/lib/cof2misp/LICENSE-2.0.txt +202 -0
  9. misp_modules/lib/cof2misp/__init__.py +0 -0
  10. misp_modules/lib/cof2misp/cof.py +165 -0
  11. misp_modules/lib/joe_mapping.py +114 -0
  12. misp_modules/lib/joe_parser.py +573 -0
  13. misp_modules/lib/lastline_api.py +841 -0
  14. misp_modules/lib/qintel_helper.py +263 -0
  15. misp_modules/lib/stix2misp.py +2080 -0
  16. misp_modules/lib/stix2misp_mapping.py +460 -0
  17. misp_modules/lib/synonymsToTagNames.json +1 -0
  18. misp_modules/lib/vt_graph_parser/__init__.py +8 -0
  19. misp_modules/lib/vt_graph_parser/errors.py +20 -0
  20. misp_modules/lib/vt_graph_parser/helpers/__init__.py +7 -0
  21. misp_modules/lib/vt_graph_parser/helpers/parsers.py +88 -0
  22. misp_modules/lib/vt_graph_parser/helpers/rules.py +304 -0
  23. misp_modules/lib/vt_graph_parser/helpers/wrappers.py +58 -0
  24. misp_modules/lib/vt_graph_parser/importers/__init__.py +7 -0
  25. misp_modules/lib/vt_graph_parser/importers/base.py +98 -0
  26. misp_modules/lib/vt_graph_parser/importers/pymisp_response.py +73 -0
  27. misp_modules/modules/__init__.py +4 -0
  28. misp_modules/modules/action_mod/__init__.py +1 -0
  29. misp_modules/modules/action_mod/_utils/__init__.py +1 -0
  30. misp_modules/modules/action_mod/_utils/utils.py +70 -0
  31. misp_modules/modules/action_mod/mattermost.py +113 -0
  32. misp_modules/modules/action_mod/slack.py +96 -0
  33. misp_modules/modules/action_mod/testaction.py +68 -0
  34. misp_modules/modules/expansion/__init__.py +36 -0
  35. misp_modules/modules/expansion/_dnsdb_query/COPYRIGHT +27 -0
  36. misp_modules/modules/expansion/_dnsdb_query/LICENSE +202 -0
  37. misp_modules/modules/expansion/_dnsdb_query/README.md +162 -0
  38. misp_modules/modules/expansion/_dnsdb_query/__init__.py +0 -0
  39. misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py +327 -0
  40. misp_modules/modules/expansion/_ransomcoindb/__init__.py +0 -0
  41. misp_modules/modules/expansion/_ransomcoindb/ransomcoindb.py +96 -0
  42. misp_modules/modules/expansion/_vulnerability_parser/__init__.py +0 -0
  43. misp_modules/modules/expansion/_vulnerability_parser/vulnerability_parser.py +118 -0
  44. misp_modules/modules/expansion/abuseipdb.py +146 -0
  45. misp_modules/modules/expansion/apiosintds.py +365 -0
  46. misp_modules/modules/expansion/apivoid.py +133 -0
  47. misp_modules/modules/expansion/assemblyline_query.py +179 -0
  48. misp_modules/modules/expansion/assemblyline_submit.py +100 -0
  49. misp_modules/modules/expansion/backscatter_io.py +84 -0
  50. misp_modules/modules/expansion/btc_scam_check.py +54 -0
  51. misp_modules/modules/expansion/btc_steroids.py +238 -0
  52. misp_modules/modules/expansion/censys_enrich.py +287 -0
  53. misp_modules/modules/expansion/circl_passivedns.py +85 -0
  54. misp_modules/modules/expansion/circl_passivessl.py +113 -0
  55. misp_modules/modules/expansion/clamav.py +129 -0
  56. misp_modules/modules/expansion/cluster25_expand.py +239 -0
  57. misp_modules/modules/expansion/countrycode.py +69 -0
  58. misp_modules/modules/expansion/cpe.py +140 -0
  59. misp_modules/modules/expansion/crowdsec.py +235 -0
  60. misp_modules/modules/expansion/crowdstrike_falcon.py +148 -0
  61. misp_modules/modules/expansion/cuckoo_submit.py +161 -0
  62. misp_modules/modules/expansion/cve.py +57 -0
  63. misp_modules/modules/expansion/cve_advanced.py +185 -0
  64. misp_modules/modules/expansion/cytomic_orion.py +196 -0
  65. misp_modules/modules/expansion/dbl_spamhaus.py +78 -0
  66. misp_modules/modules/expansion/dns.py +71 -0
  67. misp_modules/modules/expansion/docx_enrich.py +71 -0
  68. misp_modules/modules/expansion/domaintools.py +290 -0
  69. misp_modules/modules/expansion/eql.py +91 -0
  70. misp_modules/modules/expansion/eupi.py +86 -0
  71. misp_modules/modules/expansion/extract_url_components.py +83 -0
  72. misp_modules/modules/expansion/farsight_passivedns.py +243 -0
  73. misp_modules/modules/expansion/geoip_asn.py +75 -0
  74. misp_modules/modules/expansion/geoip_city.py +75 -0
  75. misp_modules/modules/expansion/geoip_country.py +73 -0
  76. misp_modules/modules/expansion/google_safe_browsing.py +86 -0
  77. misp_modules/modules/expansion/google_search.py +57 -0
  78. misp_modules/modules/expansion/google_threat_intelligence.py +453 -0
  79. misp_modules/modules/expansion/greynoise.py +346 -0
  80. misp_modules/modules/expansion/hashdd.py +53 -0
  81. misp_modules/modules/expansion/hashlookup.py +118 -0
  82. misp_modules/modules/expansion/hibp.py +62 -0
  83. misp_modules/modules/expansion/html_to_markdown.py +63 -0
  84. misp_modules/modules/expansion/hyasinsight.py +881 -0
  85. misp_modules/modules/expansion/intel471.py +73 -0
  86. misp_modules/modules/expansion/intelmq_eventdb.py.experimental +67 -0
  87. misp_modules/modules/expansion/ip2locationio.py +90 -0
  88. misp_modules/modules/expansion/ipasn.py +70 -0
  89. misp_modules/modules/expansion/ipinfo.py +112 -0
  90. misp_modules/modules/expansion/ipqs_fraud_and_risk_scoring.py +633 -0
  91. misp_modules/modules/expansion/iprep.py +98 -0
  92. misp_modules/modules/expansion/jinja_template_rendering.py +54 -0
  93. misp_modules/modules/expansion/joesandbox_query.py +91 -0
  94. misp_modules/modules/expansion/joesandbox_submit.py +147 -0
  95. misp_modules/modules/expansion/lastline_query.py +148 -0
  96. misp_modules/modules/expansion/lastline_submit.py +180 -0
  97. misp_modules/modules/expansion/macaddress_io.py +130 -0
  98. misp_modules/modules/expansion/macvendors.py +55 -0
  99. misp_modules/modules/expansion/malshare_upload.py +107 -0
  100. misp_modules/modules/expansion/malwarebazaar.py +67 -0
  101. misp_modules/modules/expansion/mcafee_insights_enrich.py +249 -0
  102. misp_modules/modules/expansion/mmdb_lookup.py +138 -0
  103. misp_modules/modules/expansion/module.py.skeleton +44 -0
  104. misp_modules/modules/expansion/mwdb.py +152 -0
  105. misp_modules/modules/expansion/ocr_enrich.py +74 -0
  106. misp_modules/modules/expansion/ods_enrich.py +71 -0
  107. misp_modules/modules/expansion/odt_enrich.py +61 -0
  108. misp_modules/modules/expansion/onyphe.py +243 -0
  109. misp_modules/modules/expansion/onyphe_full.py +387 -0
  110. misp_modules/modules/expansion/otx.py +167 -0
  111. misp_modules/modules/expansion/passive_ssh.py +150 -0
  112. misp_modules/modules/expansion/passivetotal.py +358 -0
  113. misp_modules/modules/expansion/pdf_enrich.py +58 -0
  114. misp_modules/modules/expansion/pptx_enrich.py +65 -0
  115. misp_modules/modules/expansion/qintel_qsentry.py +229 -0
  116. misp_modules/modules/expansion/qrcode.py +99 -0
  117. misp_modules/modules/expansion/ransomcoindb.py +82 -0
  118. misp_modules/modules/expansion/rbl.py +126 -0
  119. misp_modules/modules/expansion/recordedfuture.py +544 -0
  120. misp_modules/modules/expansion/reversedns.py +77 -0
  121. misp_modules/modules/expansion/securitytrails.py +571 -0
  122. misp_modules/modules/expansion/shodan.py +244 -0
  123. misp_modules/modules/expansion/sigma_queries.py +61 -0
  124. misp_modules/modules/expansion/sigma_syntax_validator.py +49 -0
  125. misp_modules/modules/expansion/sigmf_expand.py +303 -0
  126. misp_modules/modules/expansion/socialscan.py +108 -0
  127. misp_modules/modules/expansion/sophoslabs_intelix.py +146 -0
  128. misp_modules/modules/expansion/sourcecache.py +57 -0
  129. misp_modules/modules/expansion/stairwell.py +156 -0
  130. misp_modules/modules/expansion/stix2_pattern_syntax_validator.py +56 -0
  131. misp_modules/modules/expansion/threatcrowd.py +170 -0
  132. misp_modules/modules/expansion/threatfox.py +75 -0
  133. misp_modules/modules/expansion/threatminer.py +152 -0
  134. misp_modules/modules/expansion/triage_submit.py +119 -0
  135. misp_modules/modules/expansion/trustar_enrich.py +235 -0
  136. misp_modules/modules/expansion/urlhaus.py +164 -0
  137. misp_modules/modules/expansion/urlscan.py +269 -0
  138. misp_modules/modules/expansion/variotdbs.py +133 -0
  139. misp_modules/modules/expansion/virustotal.py +311 -0
  140. misp_modules/modules/expansion/virustotal_public.py +272 -0
  141. misp_modules/modules/expansion/virustotal_upload.py +91 -0
  142. misp_modules/modules/expansion/vmray_submit.py +161 -0
  143. misp_modules/modules/expansion/vmware_nsx.py +628 -0
  144. misp_modules/modules/expansion/vulndb.py +292 -0
  145. misp_modules/modules/expansion/vulnerability_lookup.py +324 -0
  146. misp_modules/modules/expansion/vulners.py +81 -0
  147. misp_modules/modules/expansion/vysion.py +221 -0
  148. misp_modules/modules/expansion/whois.py +72 -0
  149. misp_modules/modules/expansion/whoisfreaks.py +232 -0
  150. misp_modules/modules/expansion/wiki.py +58 -0
  151. misp_modules/modules/expansion/xforceexchange.py +189 -0
  152. misp_modules/modules/expansion/xlsx_enrich.py +63 -0
  153. misp_modules/modules/expansion/yara_query.py +70 -0
  154. misp_modules/modules/expansion/yara_syntax_validator.py +49 -0
  155. misp_modules/modules/expansion/yeti.py +196 -0
  156. misp_modules/modules/export_mod/__init__.py +3 -0
  157. misp_modules/modules/export_mod/cef_export.py +93 -0
  158. misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py +150 -0
  159. misp_modules/modules/export_mod/defender_endpoint_export.py +141 -0
  160. misp_modules/modules/export_mod/goamlexport.py +255 -0
  161. misp_modules/modules/export_mod/liteexport.py +97 -0
  162. misp_modules/modules/export_mod/mass_eql_export.py +106 -0
  163. misp_modules/modules/export_mod/nexthinkexport.py +131 -0
  164. misp_modules/modules/export_mod/osqueryexport.py +125 -0
  165. misp_modules/modules/export_mod/pdfexport.py +107 -0
  166. misp_modules/modules/export_mod/testexport.py +73 -0
  167. misp_modules/modules/export_mod/threatStream_misp_export.py +114 -0
  168. misp_modules/modules/export_mod/threat_connect_export.py +125 -0
  169. misp_modules/modules/export_mod/virustotal_collections.py +141 -0
  170. misp_modules/modules/export_mod/vt_graph.py +120 -0
  171. misp_modules/modules/export_mod/yara_export.py +290 -0
  172. misp_modules/modules/import_mod/__init__.py +22 -0
  173. misp_modules/modules/import_mod/cof2misp.py +264 -0
  174. misp_modules/modules/import_mod/csvimport.py +319 -0
  175. misp_modules/modules/import_mod/cuckooimport.py +749 -0
  176. misp_modules/modules/import_mod/email_import.py +294 -0
  177. misp_modules/modules/import_mod/goamlimport.py +188 -0
  178. misp_modules/modules/import_mod/import_blueprint.py +93 -0
  179. misp_modules/modules/import_mod/joe_import.py +74 -0
  180. misp_modules/modules/import_mod/lastline_import.py +160 -0
  181. misp_modules/modules/import_mod/mispjson.py +73 -0
  182. misp_modules/modules/import_mod/ocr.py +122 -0
  183. misp_modules/modules/import_mod/openiocimport.py +99 -0
  184. misp_modules/modules/import_mod/taxii21.py +383 -0
  185. misp_modules/modules/import_mod/testimport.py +73 -0
  186. misp_modules/modules/import_mod/threatanalyzer_import.py +552 -0
  187. misp_modules/modules/import_mod/url_import.py +97 -0
  188. misp_modules/modules/import_mod/vmray_import.py +96 -0
  189. misp_modules/modules/import_mod/vmray_summary_json_import.py +87 -0
  190. misp_modules-2.4.196.dist-info/LICENSE +661 -0
  191. misp_modules-2.4.196.dist-info/METADATA +281 -0
  192. misp_modules-2.4.196.dist-info/RECORD +194 -0
  193. misp_modules-2.4.196.dist-info/WHEEL +4 -0
  194. misp_modules-2.4.196.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,1417 @@
1
+ import base64
2
+ import json
3
+ import re
4
+
5
+ from abc import ABC, abstractmethod
6
+ from dataclasses import asdict, dataclass, field
7
+ from enum import Enum
8
+ from pathlib import PureWindowsPath
9
+ from typing import Any, Dict, Iterator, List, Optional, Tuple, Union
10
+
11
+ from pymisp import MISPAttribute, MISPEvent, MISPObject
12
+
13
+ from .rest_api import VMRayRESTAPI, VMRayRESTAPIError
14
+
15
+
16
+ USER_RE = re.compile(r".:.Users\\(.*?)\\", re.IGNORECASE)
17
+ DOC_RE = re.compile(r".:.DOCUME~1.\\(.*?)\\", re.IGNORECASE)
18
+ DOC_AND_SETTINGS_RE = re.compile(r".:.Documents and Settings\\(.*?)\\", re.IGNORECASE)
19
+ USERPROFILES = [USER_RE, DOC_RE, DOC_AND_SETTINGS_RE]
20
+
21
+
22
+ def classifications_to_str(classifications: List[str]) -> Optional[str]:
23
+ if classifications:
24
+ return "Classifications: " + ", ".join(classifications)
25
+ return None
26
+
27
+
28
+ def merge_lists(target: List[Any], source: List[Any]):
29
+ return list({*target, *source})
30
+
31
+
32
+ @dataclass
33
+ class Attribute:
34
+ type: str
35
+ value: str
36
+ category: Optional[str] = None
37
+ comment: Optional[str] = None
38
+ to_ids: bool = False
39
+
40
+ def __eq__(self, other: Dict[str, Any]) -> bool:
41
+ return asdict(self) == other
42
+
43
+
44
+ @dataclass
45
+ class Artifact:
46
+ is_ioc: bool
47
+ verdict: Optional[str]
48
+
49
+ @abstractmethod
50
+ def to_attributes(self) -> Iterator[Attribute]:
51
+ raise NotImplementedError()
52
+
53
+ @abstractmethod
54
+ def to_misp_object(self, tag: bool) -> MISPObject:
55
+ raise NotImplementedError()
56
+
57
+ @abstractmethod
58
+ def merge(self, other: "Artifact") -> None:
59
+ raise NotImplementedError()
60
+
61
+ @abstractmethod
62
+ def __eq__(self, other: "Artifact") -> bool:
63
+ raise NotImplementedError()
64
+
65
+ def tag_artifact_attribute(self, attribute: MISPAttribute) -> None:
66
+ if self.is_ioc:
67
+ attribute.add_tag('vmray:artifact="IOC"')
68
+
69
+ if self.verdict:
70
+ attribute.add_tag(f'vmray:verdict="{self.verdict}"')
71
+
72
+
73
+ @dataclass
74
+ class DomainArtifact(Artifact):
75
+ domain: str
76
+ sources: List[str]
77
+ ips: List[str] = field(default_factory=list)
78
+ classifications: List[str] = field(default_factory=list)
79
+
80
+ def to_attributes(self) -> Iterator[Attribute]:
81
+ value = self.domain
82
+ comment = ", ".join(self.sources) if self.sources else None
83
+
84
+ attr = Attribute(type="domain", value=value, comment=comment)
85
+ yield attr
86
+
87
+ def to_misp_object(self, tag: bool) -> MISPObject:
88
+ obj = MISPObject(name="domain-ip")
89
+
90
+ classifications = classifications_to_str(self.classifications)
91
+ attr = obj.add_attribute(
92
+ "domain", value=self.domain, to_ids=self.is_ioc, comment=classifications
93
+ )
94
+ if tag and attr:
95
+ self.tag_artifact_attribute(attr)
96
+
97
+ for ip in self.ips:
98
+ obj.add_attribute("ip", value=ip, to_ids=self.is_ioc)
99
+
100
+ return obj
101
+
102
+ def merge(self, other: Artifact) -> None:
103
+ if not isinstance(other, DomainArtifact):
104
+ return
105
+
106
+ self.ips = merge_lists(self.ips, other.ips)
107
+ self.classifications = merge_lists(self.classifications, other.classifications)
108
+
109
+ def __eq__(self, other: Artifact) -> bool:
110
+ if not isinstance(other, DomainArtifact):
111
+ return NotImplemented
112
+
113
+ return self.domain == other.domain
114
+
115
+
116
+ @dataclass
117
+ class EmailArtifact(Artifact):
118
+ sender: Optional[str]
119
+ subject: Optional[str]
120
+ recipients: List[str] = field(default_factory=list)
121
+ classifications: List[str] = field(default_factory=list)
122
+
123
+ def to_attributes(self) -> Iterator[Attribute]:
124
+ if self.sender:
125
+ classifications = classifications_to_str(self.classifications)
126
+ yield Attribute(
127
+ type="email-src", value=self.sender, comment=classifications
128
+ )
129
+
130
+ if self.subject:
131
+ yield Attribute(type="email-subject", value=self.subject, to_ids=False)
132
+
133
+ for recipient in self.recipients:
134
+ yield Attribute(type="email-dst", value=recipient, to_ids=False)
135
+
136
+ def to_misp_object(self, tag: bool) -> MISPObject:
137
+ obj = MISPObject(name="email")
138
+
139
+ if self.sender:
140
+ classifications = classifications_to_str(self.classifications)
141
+ attr = obj.add_attribute(
142
+ "from", value=self.sender, to_ids=self.is_ioc, comment=classifications
143
+ )
144
+ if tag and attr:
145
+ self.tag_artifact_attribute(attr)
146
+
147
+ if self.subject:
148
+ obj.add_attribute("subject", value=self.subject, to_ids=False)
149
+
150
+ for recipient in self.recipients:
151
+ obj.add_attribute("to", value=recipient, to_ids=False)
152
+
153
+ return obj
154
+
155
+ def merge(self, other: Artifact) -> None:
156
+ if not isinstance(other, EmailArtifact):
157
+ return
158
+
159
+ self.recipients = merge_lists(self.recipients, other.recipients)
160
+ self.classifications = merge_lists(self.classifications, other.classifications)
161
+
162
+ def __eq__(self, other: Artifact) -> bool:
163
+ if not isinstance(other, EmailArtifact):
164
+ return NotImplemented
165
+
166
+ return self.sender == other.sender and self.subject == other.subject
167
+
168
+
169
+ @dataclass
170
+ class FileArtifact(Artifact):
171
+ filenames: List[str]
172
+ operations: List[str]
173
+ md5: str
174
+ sha1: str
175
+ sha256: str
176
+ ssdeep: str
177
+ imphash: Optional[str]
178
+ classifications: List[str]
179
+ size: Optional[int]
180
+ mimetype: Optional[str] = None
181
+
182
+ def to_attributes(self) -> Iterator[Attribute]:
183
+ operations = ", ".join(self.operations)
184
+ comment = f"File operations: {operations}"
185
+
186
+ for filename in self.filenames:
187
+ attr = Attribute(type="filename", value=filename, comment=comment)
188
+ yield attr
189
+
190
+ for hash_type in ("md5", "sha1", "sha256", "ssdeep", "imphash"):
191
+ for filename in self.filenames:
192
+ value = getattr(self, hash_type)
193
+ if value is not None:
194
+ attr = Attribute(
195
+ type=f"filename|{hash_type}",
196
+ value=f"{filename}|{value}",
197
+ category="Payload delivery",
198
+ to_ids=True,
199
+ )
200
+ yield attr
201
+
202
+ def to_misp_object(self, tag: bool) -> MISPObject:
203
+ obj = MISPObject(name="file")
204
+
205
+ if self.size:
206
+ obj.add_attribute("size-in-bytes", value=self.size)
207
+
208
+ classifications = classifications_to_str(self.classifications)
209
+ hashes = [
210
+ ("md5", self.md5),
211
+ ("sha1", self.sha1),
212
+ ("sha256", self.sha256),
213
+ ("ssdeep", self.ssdeep),
214
+ ]
215
+ for (key, value) in hashes:
216
+ if not value:
217
+ continue
218
+
219
+ attr = obj.add_attribute(
220
+ key, value=value, to_ids=self.is_ioc, comment=classifications
221
+ )
222
+
223
+ if tag and attr:
224
+ self.tag_artifact_attribute(attr)
225
+
226
+ if self.mimetype:
227
+ obj.add_attribute("mimetype", value=self.mimetype, to_ids=False)
228
+
229
+ operations = None
230
+ if self.operations:
231
+ operations = "Operations: " + ", ".join(self.operations)
232
+
233
+ for filename in self.filenames:
234
+ filename = PureWindowsPath(filename)
235
+ obj.add_attribute("filename", value=filename.name, comment=operations)
236
+
237
+ fullpath = str(filename)
238
+ for regex in USERPROFILES:
239
+ fullpath = regex.sub(r"%USERPROFILE%\\", fullpath)
240
+
241
+ obj.add_attribute("fullpath", fullpath)
242
+
243
+ return obj
244
+
245
+ def merge(self, other: Artifact) -> None:
246
+ if not isinstance(other, FileArtifact):
247
+ return
248
+
249
+ self.filenames = merge_lists(self.filenames, other.filenames)
250
+ self.operations = merge_lists(self.operations, other.operations)
251
+ self.classifications = merge_lists(self.classifications, other.classifications)
252
+
253
+ def __eq__(self, other: Artifact) -> bool:
254
+ if not isinstance(other, FileArtifact):
255
+ return NotImplemented
256
+
257
+ return self.sha256 == other.sha256
258
+
259
+
260
+ @dataclass
261
+ class IpArtifact(Artifact):
262
+ ip: str
263
+ sources: List[str]
264
+ classifications: List[str] = field(default_factory=list)
265
+
266
+ def to_attributes(self) -> Iterator[Attribute]:
267
+ sources = ", ".join(self.sources)
268
+ comment = f"Found in: {sources}"
269
+
270
+ attr = Attribute(type="ip-dst", value=self.ip, comment=comment)
271
+ yield attr
272
+
273
+ def to_misp_object(self, tag: bool) -> MISPObject:
274
+ obj = MISPObject(name="ip-port")
275
+
276
+ classifications = classifications_to_str(self.classifications)
277
+ attr = obj.add_attribute(
278
+ "ip", value=self.ip, comment=classifications, to_ids=self.is_ioc
279
+ )
280
+ if tag and attr:
281
+ self.tag_artifact_attribute(attr)
282
+
283
+ return obj
284
+
285
+ def merge(self, other: Artifact) -> None:
286
+ if not isinstance(other, IpArtifact):
287
+ return
288
+
289
+ self.sources = merge_lists(self.sources, other.sources)
290
+ self.classifications = merge_lists(self.classifications, other.classifications)
291
+
292
+ def __eq__(self, other: Artifact) -> bool:
293
+ if not isinstance(other, IpArtifact):
294
+ return NotImplemented
295
+
296
+ return self.ip == other.ip
297
+
298
+
299
+ @dataclass
300
+ class MutexArtifact(Artifact):
301
+ name: str
302
+ operations: List[str]
303
+ classifications: List[str] = field(default_factory=list)
304
+
305
+ def to_attributes(self) -> Iterator[Attribute]:
306
+ operations = ", ".join(self.operations)
307
+ comment = f"Operations: {operations}"
308
+
309
+ attr = Attribute(type="mutex", value=self.name, comment=comment)
310
+ yield attr
311
+
312
+ def to_misp_object(self, tag: bool) -> MISPObject:
313
+ obj = MISPObject(name="mutex")
314
+
315
+ classifications = classifications_to_str(self.classifications)
316
+ attr = obj.add_attribute(
317
+ "name",
318
+ value=self.name,
319
+ category="External analysis",
320
+ to_ids=False,
321
+ comment=classifications,
322
+ )
323
+ if tag and attr:
324
+ self.tag_artifact_attribute(attr)
325
+
326
+ operations = None
327
+ if self.operations:
328
+ operations = "Operations: " + ", ".join(self.operations)
329
+ obj.add_attribute("description", value=operations, to_ids=False)
330
+
331
+ return obj
332
+
333
+ def merge(self, other: Artifact) -> None:
334
+ if not isinstance(other, MutexArtifact):
335
+ return
336
+
337
+ self.operations = merge_lists(self.operations, other.operations)
338
+ self.classifications = merge_lists(self.classifications, other.classifications)
339
+
340
+ def __eq__(self, other: Artifact) -> bool:
341
+ if not isinstance(other, MutexArtifact):
342
+ return NotImplemented
343
+
344
+ return self.name == other.name
345
+
346
+
347
+ @dataclass
348
+ class ProcessArtifact(Artifact):
349
+ filename: str
350
+ pid: Optional[int] = None
351
+ parent_pid: Optional[int] = None
352
+ cmd_line: Optional[str] = None
353
+ operations: List[str] = field(default_factory=list)
354
+ classifications: List[str] = field(default_factory=list)
355
+
356
+ def to_attributes(self) -> Iterator[Attribute]:
357
+ process_desc = f"Process created: {self.filename}\nPID: {self.pid}"
358
+ classifications = classifications_to_str(self.classifications)
359
+ yield Attribute(type="text", value=process_desc, comment=classifications)
360
+
361
+ def to_misp_object(self, tag: bool) -> MISPObject:
362
+ obj = MISPObject(name="process")
363
+
364
+ if self.pid:
365
+ obj.add_attribute("pid", value=self.pid, category="External analysis")
366
+
367
+ if self.parent_pid:
368
+ obj.add_attribute(
369
+ "parent-pid", value=self.parent_pid, category="External analysis"
370
+ )
371
+
372
+ classifications = classifications_to_str(self.classifications)
373
+ name_attr = obj.add_attribute(
374
+ "name", self.filename, category="External analysis", comment=classifications
375
+ )
376
+
377
+ cmd_attr = obj.add_attribute("command-line", value=self.cmd_line)
378
+
379
+ if tag:
380
+ if name_attr:
381
+ self.tag_artifact_attribute(name_attr)
382
+ if cmd_attr:
383
+ self.tag_artifact_attribute(cmd_attr)
384
+
385
+ return obj
386
+
387
+ def merge(self, other: Artifact) -> None:
388
+ if not isinstance(other, ProcessArtifact):
389
+ return
390
+
391
+ self.operations = merge_lists(self.operations, other.operations)
392
+ self.classifications = merge_lists(self.classifications, other.classifications)
393
+
394
+ def __eq__(self, other: Artifact) -> bool:
395
+ if not isinstance(other, ProcessArtifact):
396
+ return NotImplemented
397
+
398
+ return self.filename == other.filename and self.cmd_line == other.cmd_line
399
+
400
+
401
+ @dataclass
402
+ class RegistryArtifact(Artifact):
403
+ key: str
404
+ operations: List[str]
405
+
406
+ def to_attributes(self) -> Iterator[Attribute]:
407
+ operations = ", ".join(self.operations)
408
+ comment = f"Operations: {operations}"
409
+
410
+ attr = Attribute(type="regkey", value=self.key, comment=comment)
411
+ yield attr
412
+
413
+ def to_misp_object(self, tag: bool) -> MISPObject:
414
+ obj = MISPObject(name="registry-key")
415
+
416
+ operations = None
417
+ if self.operations:
418
+ operations = "Operations: " + ", ".join(self.operations)
419
+
420
+ attr = obj.add_attribute(
421
+ "key", value=self.key, to_ids=self.is_ioc, comment=operations
422
+ )
423
+ if tag and attr:
424
+ self.tag_artifact_attribute(attr)
425
+
426
+ return obj
427
+
428
+ def merge(self, other: Artifact) -> None:
429
+ if not isinstance(other, RegistryArtifact):
430
+ return
431
+
432
+ self.operations = merge_lists(self.operations, other.operations)
433
+
434
+ def __eq__(self, other: Artifact) -> bool:
435
+ if not isinstance(other, RegistryArtifact):
436
+ return NotImplemented
437
+
438
+ return self.key == other.key
439
+
440
+
441
+ @dataclass
442
+ class UrlArtifact(Artifact):
443
+ url: str
444
+ operations: List[str]
445
+ domain: Optional[str] = None
446
+ ips: List[str] = field(default_factory=list)
447
+
448
+ def to_attributes(self) -> Iterator[Attribute]:
449
+ operations = ", ".join(self.operations)
450
+ comment = f"Operations: {operations}"
451
+
452
+ attr = Attribute(type="url", value=self.url, comment=comment)
453
+ yield attr
454
+
455
+ def to_misp_object(self, tag: bool) -> MISPObject:
456
+ obj = MISPObject(name="url")
457
+
458
+ operations = None
459
+ if self.operations:
460
+ operations = "Operations: " + ", ".join(self.operations)
461
+
462
+ attr = obj.add_attribute(
463
+ "url",
464
+ value=self.url,
465
+ comment=operations,
466
+ category="External analysis",
467
+ to_ids=False,
468
+ )
469
+ if tag and attr:
470
+ self.tag_artifact_attribute(attr)
471
+
472
+ if self.domain:
473
+ obj.add_attribute(
474
+ "domain", self.domain, category="External analysis", to_ids=False
475
+ )
476
+
477
+ for ip in self.ips:
478
+ obj.add_attribute("ip", ip, category="External analysis", to_ids=False)
479
+
480
+ return obj
481
+
482
+ def merge(self, other: Artifact) -> None:
483
+ if not isinstance(other, UrlArtifact):
484
+ return
485
+
486
+ self.ips = merge_lists(self.ips, other.ips)
487
+ self.operations = merge_lists(self.operations, other.operations)
488
+
489
+ def __eq__(self, other: Artifact) -> bool:
490
+ if not isinstance(other, UrlArtifact):
491
+ return NotImplemented
492
+
493
+ return self.url == other.url and self.domain == other.domain
494
+
495
+
496
+ @dataclass
497
+ class MitreAttack:
498
+ description: str
499
+ id: str
500
+
501
+ def to_misp_galaxy(self) -> str:
502
+ return f'misp-galaxy:mitre-attack-pattern="{self.description} - {self.id}"'
503
+
504
+
505
+ @dataclass
506
+ class VTI:
507
+ category: str
508
+ operation: str
509
+ technique: str
510
+ score: int
511
+
512
+
513
+ class ReportVersion(Enum):
514
+ v1 = "v1"
515
+ v2 = "v2"
516
+
517
+
518
+ class VMRayParseError(Exception):
519
+ pass
520
+
521
+
522
+ class ReportParser(ABC):
523
+ @abstractmethod
524
+ def __init__(self, api: VMRayRESTAPI, analysis_id: int):
525
+ raise NotImplementedError()
526
+
527
+ @abstractmethod
528
+ def is_static_report(self) -> bool:
529
+ raise NotImplementedError()
530
+
531
+ @abstractmethod
532
+ def artifacts(self) -> Iterator[Artifact]:
533
+ raise NotImplementedError()
534
+
535
+ @abstractmethod
536
+ def classifications(self) -> Optional[str]:
537
+ raise NotImplementedError()
538
+
539
+ @abstractmethod
540
+ def details(self) -> Iterator[str]:
541
+ raise NotImplementedError()
542
+
543
+ @abstractmethod
544
+ def mitre_attacks(self) -> Iterator[MitreAttack]:
545
+ raise NotImplementedError()
546
+
547
+ @abstractmethod
548
+ def sandbox_type(self) -> str:
549
+ raise NotImplementedError()
550
+
551
+ @abstractmethod
552
+ def score(self) -> str:
553
+ raise NotImplementedError()
554
+
555
+ @abstractmethod
556
+ def vtis(self) -> Iterator[VTI]:
557
+ raise NotImplementedError()
558
+
559
+
560
+ class Summary(ReportParser):
561
+ def __init__(
562
+ self, analysis_id: int, api: VMRayRESTAPI = None, report: Dict[str, Any] = None
563
+ ):
564
+ self.analysis_id = analysis_id
565
+
566
+ if report:
567
+ self.report = report
568
+ else:
569
+ data = api.call(
570
+ "GET",
571
+ f"/rest/analysis/{analysis_id}/archive/logs/summary.json",
572
+ raw_data=True,
573
+ )
574
+ self.report = json.load(data)
575
+
576
+ @staticmethod
577
+ def to_verdict(score: Union[int, str]) -> Optional[str]:
578
+ if isinstance(score, int):
579
+ if 0 <= score <= 24:
580
+ return "clean"
581
+ if 25 <= score <= 74:
582
+ return "suspicious"
583
+ if 75 <= score <= 100:
584
+ return "malicious"
585
+ return "n/a"
586
+ if isinstance(score, str):
587
+ score = score.lower()
588
+ if score in ("not_suspicious", "whitelisted"):
589
+ return "clean"
590
+ if score == "blacklisted":
591
+ return "malicious"
592
+ if score in ("not_available", "unknown"):
593
+ return "n/a"
594
+ return score
595
+ return None
596
+
597
+ def is_static_report(self) -> bool:
598
+ return self.report["vti"]["vti_rule_type"] == "Static"
599
+
600
+ def artifacts(self) -> Iterator[Artifact]:
601
+ artifacts = self.report["artifacts"]
602
+ domains = artifacts.get("domains", [])
603
+ for domain in domains:
604
+ classifications = domain.get("classifications", [])
605
+ is_ioc = domain.get("ioc", False)
606
+ verdict = self.to_verdict(domain.get("severity"))
607
+ ips = domain.get("ip_addresses", [])
608
+ artifact = DomainArtifact(
609
+ domain=domain["domain"],
610
+ sources=domain["sources"],
611
+ ips=ips,
612
+ classifications=classifications,
613
+ is_ioc=is_ioc,
614
+ verdict=verdict,
615
+ )
616
+ yield artifact
617
+
618
+ emails = artifacts.get("emails", [])
619
+ for email in emails:
620
+ sender = email.get("sender")
621
+ subject = email.get("subject")
622
+ verdict = self.to_verdict(email.get("severity"))
623
+ recipients = email.get("recipients", [])
624
+ classifications = email.get("classifications", [])
625
+ is_ioc = email.get("ioc", False)
626
+
627
+ artifact = EmailArtifact(
628
+ sender=sender,
629
+ subject=subject,
630
+ verdict=verdict,
631
+ recipients=recipients,
632
+ classifications=classifications,
633
+ is_ioc=is_ioc,
634
+ )
635
+ yield artifact
636
+
637
+ files = artifacts.get("files", [])
638
+ for file_ in files:
639
+ if file_["filename"] is None:
640
+ continue
641
+
642
+ filenames = [file_["filename"]]
643
+ if "filenames" in file_:
644
+ filenames += file_["filenames"]
645
+
646
+ hashes = file_["hashes"]
647
+ classifications = file_.get("classifications", [])
648
+ operations = file_.get("operations", [])
649
+ is_ioc = file_.get("ioc", False)
650
+ mimetype = file_.get("mime_type")
651
+ verdict = self.to_verdict(file_.get("severity"))
652
+
653
+ for hash_dict in hashes:
654
+ imp = hash_dict.get("imp_hash")
655
+
656
+ artifact = FileArtifact(
657
+ filenames=filenames,
658
+ imphash=imp,
659
+ md5=hash_dict["md5_hash"],
660
+ ssdeep=hash_dict["ssdeep_hash"],
661
+ sha256=hash_dict["sha256_hash"],
662
+ sha1=hash_dict["sha1_hash"],
663
+ operations=operations,
664
+ classifications=classifications,
665
+ size=file_.get("file_size"),
666
+ is_ioc=is_ioc,
667
+ mimetype=mimetype,
668
+ verdict=verdict,
669
+ )
670
+ yield artifact
671
+
672
+ ips = artifacts.get("ips", [])
673
+ for ip in ips:
674
+ is_ioc = ip.get("ioc", False)
675
+ verdict = self.to_verdict(ip.get("severity"))
676
+ classifications = ip.get("classifications", [])
677
+ artifact = IpArtifact(
678
+ ip=ip["ip_address"],
679
+ sources=ip["sources"],
680
+ classifications=classifications,
681
+ verdict=verdict,
682
+ is_ioc=is_ioc,
683
+ )
684
+ yield artifact
685
+
686
+ mutexes = artifacts.get("mutexes", [])
687
+ for mutex in mutexes:
688
+ verdict = self.to_verdict(mutex.get("severity"))
689
+ is_ioc = mutex.get("ioc", False)
690
+ artifact = MutexArtifact(
691
+ name=mutex["mutex_name"],
692
+ operations=mutex["operations"],
693
+ classifications=[],
694
+ verdict=verdict,
695
+ is_ioc=is_ioc,
696
+ )
697
+ yield artifact
698
+
699
+ processes = artifacts.get("processes", [])
700
+ for process in processes:
701
+ classifications = process.get("classifications", [])
702
+ cmd_line = process.get("cmd_line")
703
+ name = process.get("image_name")
704
+ verdict = self.to_verdict(process.get("severity"))
705
+ is_ioc = process.get("ioc", False)
706
+
707
+ artifact = ProcessArtifact(
708
+ filename=name,
709
+ classifications=classifications,
710
+ cmd_line=cmd_line,
711
+ verdict=verdict,
712
+ is_ioc=is_ioc,
713
+ )
714
+ yield artifact
715
+
716
+ registry = artifacts.get("registry", [])
717
+ for reg in registry:
718
+ is_ioc = reg.get("ioc", False)
719
+ verdict = self.to_verdict(reg.get("severity"))
720
+ artifact = RegistryArtifact(
721
+ key=reg["reg_key_name"],
722
+ operations=reg["operations"],
723
+ verdict=verdict,
724
+ is_ioc=is_ioc,
725
+ )
726
+ yield artifact
727
+
728
+ urls = artifacts.get("urls", [])
729
+ for url in urls:
730
+ ips = url.get("ip_addresses", [])
731
+ is_ioc = url.get("ioc", False)
732
+ verdict = self.to_verdict(url.get("severity"))
733
+
734
+ artifact = UrlArtifact(
735
+ url=url["url"],
736
+ operations=url.get("operations", []),
737
+ ips=ips,
738
+ is_ioc=is_ioc,
739
+ verdict=verdict,
740
+ )
741
+ yield artifact
742
+
743
+ def classifications(self) -> Optional[str]:
744
+ classifications = self.report["classifications"]
745
+ if classifications:
746
+ str_classifications = ", ".join(classifications)
747
+ return f"Classifications: {str_classifications}"
748
+ return None
749
+
750
+ def details(self) -> Iterator[str]:
751
+ details = self.report["analysis_details"]
752
+ execution_successful = details["execution_successful"]
753
+ termination_reason = details["termination_reason"]
754
+ result = details["result_str"]
755
+
756
+ if self.analysis_id == 0:
757
+ analysis = ""
758
+ else:
759
+ analysis = f" {self.analysis_id}"
760
+
761
+ yield f"Analysis{analysis}: execution_successful: {execution_successful}"
762
+ yield f"Analysis{analysis}: termination_reason: {termination_reason}"
763
+ yield f"Analysis{analysis}: result: {result}"
764
+
765
+ def mitre_attacks(self) -> Iterator[MitreAttack]:
766
+ mitre_attack = self.report["mitre_attack"]
767
+ techniques = mitre_attack.get("techniques", [])
768
+
769
+ for technique in techniques:
770
+ mitre_attack = MitreAttack(
771
+ description=technique["description"], id=technique["id"]
772
+ )
773
+ yield mitre_attack
774
+
775
+ def sandbox_type(self) -> str:
776
+ vm_name = self.report["vm_and_analyzer_details"]["vm_name"]
777
+ sample_type = self.report["sample_details"]["sample_type"]
778
+ return f"{vm_name} | {sample_type}"
779
+
780
+ def score(self) -> str:
781
+ vti_score = self.report["vti"]["vti_score"]
782
+ return self.to_verdict(vti_score)
783
+
784
+ def vtis(self) -> Iterator[VTI]:
785
+ try:
786
+ vtis = self.report["vti"]["vti_rule_matches"]
787
+ except KeyError:
788
+ vtis = []
789
+
790
+ for vti in vtis:
791
+ new_vti = VTI(
792
+ category=vti["category_desc"],
793
+ operation=vti["operation_desc"],
794
+ technique=vti["technique_desc"],
795
+ score=vti["rule_score"],
796
+ )
797
+
798
+ yield new_vti
799
+
800
+
801
+ class SummaryV2(ReportParser):
802
+ def __init__(
803
+ self, analysis_id: int, api: VMRayRESTAPI = None, report: Dict[str, Any] = None
804
+ ):
805
+ self.analysis_id = analysis_id
806
+
807
+ if report:
808
+ self.report = report
809
+ else:
810
+ self.api = api
811
+ data = api.call(
812
+ "GET",
813
+ f"/rest/analysis/{analysis_id}/archive/logs/summary_v2.json",
814
+ raw_data=True,
815
+ )
816
+ self.report = json.load(data)
817
+
818
+ def _resolve_refs(
819
+ self, data: Union[List[Dict[str, Any]], Dict[str, Any]]
820
+ ) -> Iterator[Dict[str, Any]]:
821
+ if not data:
822
+ return []
823
+
824
+ if isinstance(data, dict):
825
+ data = [data]
826
+
827
+ for ref in data:
828
+ yield self._resolve_ref(ref)
829
+
830
+ def _resolve_ref(self, data: Dict[str, Any]) -> Dict[str, Any]:
831
+ if data == {}:
832
+ return {}
833
+
834
+ if data["_type"] != "reference" or data["source"] != "logs/summary_v2.json":
835
+ return {}
836
+
837
+ resolved_ref = self.report
838
+ paths = data["path"]
839
+ for path_part in paths:
840
+ try:
841
+ resolved_ref = resolved_ref[path_part]
842
+ except KeyError:
843
+ return {}
844
+
845
+ return resolved_ref
846
+
847
+ @staticmethod
848
+ def convert_verdict(verdict: Optional[str]) -> str:
849
+ if verdict == "not_available" or not verdict:
850
+ return "n/a"
851
+
852
+ return verdict
853
+
854
+ def is_static_report(self) -> bool:
855
+ return self.report["vti"]["score_type"] == "static"
856
+
857
+ def artifacts(self) -> Iterator[Artifact]:
858
+ artifacts = self.report["artifacts"]
859
+
860
+ ref_domains = artifacts.get("ref_domains", [])
861
+ for domain in self._resolve_refs(ref_domains):
862
+ classifications = domain.get("classifications", [])
863
+ artifact = DomainArtifact(
864
+ domain=domain["domain"],
865
+ sources=domain["sources"],
866
+ classifications=classifications,
867
+ is_ioc=domain["is_ioc"],
868
+ verdict=domain["verdict"],
869
+ )
870
+
871
+ ref_ip_addresses = domain.get("ref_ip_addresses", [])
872
+ if not ref_ip_addresses:
873
+ continue
874
+
875
+ for ip_address in self._resolve_refs(ref_ip_addresses):
876
+ ip = ip_address.get("ip_address")
877
+ if ip is not None:
878
+ artifact.ips.append(ip)
879
+
880
+ yield artifact
881
+
882
+ ref_emails = artifacts.get("ref_emails", [])
883
+ for email in self._resolve_refs(ref_emails):
884
+ sender = email.get("sender")
885
+ subject = email.get("subject")
886
+ recipients = email.get("recipients", [])
887
+ verdict = email["verdict"]
888
+ is_ioc = email["is_ioc"]
889
+ classifications = email.get("classifications", [])
890
+
891
+ artifact = EmailArtifact(
892
+ sender=sender,
893
+ subject=subject,
894
+ recipients=recipients,
895
+ classifications=classifications,
896
+ verdict=verdict,
897
+ is_ioc=is_ioc,
898
+ )
899
+
900
+ yield artifact
901
+
902
+ ref_files = artifacts.get("ref_files", [])
903
+ for file_ in self._resolve_refs(ref_files):
904
+ filenames = []
905
+
906
+ if "ref_filenames" in file_:
907
+ for filename in self._resolve_refs(file_["ref_filenames"]):
908
+ if not filename:
909
+ continue
910
+ filenames.append(filename["filename"])
911
+
912
+ artifact = FileArtifact(
913
+ operations=file_.get("operations", []),
914
+ md5=file_["hash_values"]["md5"],
915
+ sha1=file_["hash_values"]["sha1"],
916
+ sha256=file_["hash_values"]["sha256"],
917
+ ssdeep=file_["hash_values"]["ssdeep"],
918
+ imphash=None,
919
+ mimetype=file_.get("mime_type"),
920
+ filenames=filenames,
921
+ is_ioc=file_["is_ioc"],
922
+ classifications=file_.get("classifications", []),
923
+ size=file_["size"],
924
+ verdict=file_["verdict"],
925
+ )
926
+ yield artifact
927
+
928
+ ref_ip_addresses = artifacts.get("ref_ip_addresses", [])
929
+ for ip in self._resolve_refs(ref_ip_addresses):
930
+ classifications = ip.get("classifications", [])
931
+ verdict = ip["verdict"]
932
+ is_ioc = ip["is_ioc"]
933
+ artifact = IpArtifact(
934
+ ip=ip["ip_address"],
935
+ sources=ip["sources"],
936
+ classifications=classifications,
937
+ verdict=verdict,
938
+ is_ioc=is_ioc,
939
+ )
940
+ yield artifact
941
+
942
+ ref_mutexes = artifacts.get("ref_mutexes", [])
943
+ for mutex in self._resolve_refs(ref_mutexes):
944
+ is_ioc = mutex["is_ioc"]
945
+ classifications = mutex.get("classifications", [])
946
+ artifact = MutexArtifact(
947
+ name=mutex["name"],
948
+ operations=mutex["operations"],
949
+ verdict=mutex["verdict"],
950
+ classifications=classifications,
951
+ is_ioc=is_ioc,
952
+ )
953
+ yield artifact
954
+
955
+ ref_processes = artifacts.get("ref_processes", [])
956
+ for process in self._resolve_refs(ref_processes):
957
+ cmd_line = process.get("cmd_line")
958
+ classifications = process.get("classifications", [])
959
+ verdict = process.get("verdict")
960
+ artifact = ProcessArtifact(
961
+ pid=process["os_pid"],
962
+ parent_pid=process["origin_monitor_id"],
963
+ filename=process.get("filename"),
964
+ is_ioc=process["is_ioc"],
965
+ cmd_line=cmd_line,
966
+ classifications=classifications,
967
+ verdict=verdict,
968
+ )
969
+ yield artifact
970
+
971
+ ref_registry_records = artifacts.get("ref_registry_records", [])
972
+ for reg in self._resolve_refs(ref_registry_records):
973
+ artifact = RegistryArtifact(
974
+ key=reg["reg_key_name"],
975
+ operations=reg["operations"],
976
+ is_ioc=reg["is_ioc"],
977
+ verdict=reg["verdict"],
978
+ )
979
+ yield artifact
980
+
981
+ url_refs = artifacts.get("ref_urls", [])
982
+ for url in self._resolve_refs(url_refs):
983
+ domain = None
984
+ ref_domain = url.get("ref_domain", {})
985
+ if ref_domain and self._resolve_ref(ref_domain).get("domain") is not None:
986
+ domain = self._resolve_ref(ref_domain)["domain"]
987
+
988
+ ips = []
989
+ ref_ip_addresses = url.get("ref_ip_addresses", [])
990
+ for ip_address in self._resolve_refs(ref_ip_addresses):
991
+ ip = ip_address.get("ip_address")
992
+ if ip is not None:
993
+ ips.append(ip)
994
+
995
+ artifact = UrlArtifact(
996
+ url=url["url"],
997
+ operations=url.get("operations", []),
998
+ is_ioc=url["is_ioc"],
999
+ domain=domain,
1000
+ ips=ips,
1001
+ verdict=url["verdict"],
1002
+ )
1003
+ yield artifact
1004
+
1005
+ def classifications(self) -> Optional[str]:
1006
+ try:
1007
+ classifications = ", ".join(self.report["classifications"])
1008
+ return f"Classifications: {classifications}"
1009
+ except KeyError:
1010
+ return None
1011
+
1012
+ def details(self) -> Iterator[str]:
1013
+ details = self.report["analysis_metadata"]
1014
+ is_execution_successful = details["is_execution_successful"]
1015
+ termination_reason = details["termination_reason"]
1016
+ result = details["result_str"]
1017
+
1018
+ yield f"Analysis {self.analysis_id}: execution_successful: {is_execution_successful}"
1019
+ yield f"Analysis {self.analysis_id}: termination_reason: {termination_reason}"
1020
+ yield f"Analysis {self.analysis_id}: result: {result}"
1021
+
1022
+ def mitre_attacks(self) -> Iterator[MitreAttack]:
1023
+ mitre_attack = self.report["mitre_attack"]
1024
+ techniques = mitre_attack["v4"]["techniques"]
1025
+
1026
+ for technique_id, technique in techniques.items():
1027
+ mitre_attack = MitreAttack(
1028
+ description=technique["description"],
1029
+ id=technique_id.replace("technique_", ""),
1030
+ )
1031
+ yield mitre_attack
1032
+
1033
+ def sandbox_type(self) -> str:
1034
+ vm_information = self.report["virtual_machine"]["description"]
1035
+ sample_type = self.report["analysis_metadata"]["sample_type"]
1036
+ return f"{vm_information} | {sample_type}"
1037
+
1038
+ def score(self) -> str:
1039
+ verdict = self.report["analysis_metadata"]["verdict"]
1040
+ return self.convert_verdict(verdict)
1041
+
1042
+ def vtis(self) -> Iterator[VTI]:
1043
+ if "matches" not in self.report["vti"]:
1044
+ return
1045
+
1046
+ vti_matches = self.report["vti"]["matches"]
1047
+ for vti in vti_matches.values():
1048
+ new_vti = VTI(
1049
+ category=vti["category_desc"],
1050
+ operation=vti["operation_desc"],
1051
+ technique=vti["technique_desc"],
1052
+ score=vti["analysis_score"],
1053
+ )
1054
+
1055
+ yield new_vti
1056
+
1057
+
1058
+ class VMRayParser:
1059
+ def __init__(self) -> None:
1060
+ # required for api import
1061
+ self.api: Optional[VMRayRESTAPI] = None
1062
+ self.sample_id: Optional[int] = None
1063
+
1064
+ # required for file import
1065
+ self.report: Optional[Dict[str, Any]] = None
1066
+ self.report_name: Optional[str] = None
1067
+ self.include_report = False
1068
+
1069
+ # required by API import and file import
1070
+ self.report_version = ReportVersion.v2
1071
+
1072
+ self.use_misp_object = True
1073
+ self.ignore_analysis_finished = False
1074
+ self.tag_objects = True
1075
+
1076
+ self.include_analysis_id = True
1077
+ self.include_vti_details = True
1078
+ self.include_iocs = True
1079
+ self.include_all_artifacts = False
1080
+ self.include_analysis_details = True
1081
+
1082
+ # a new event if we use misp objects
1083
+ self.event = MISPEvent()
1084
+
1085
+ # new attributes if we don't use misp objects
1086
+ self.attributes: List[Attribute] = []
1087
+
1088
+ def from_api(self, config: Dict[str, Any]) -> None:
1089
+ url = self._read_config_key(config, "url")
1090
+ api_key = self._read_config_key(config, "apikey")
1091
+
1092
+ try:
1093
+ self.sample_id = int(self._read_config_key(config, "Sample ID"))
1094
+ except ValueError:
1095
+ raise VMRayParseError("Could not convert sample id to integer.")
1096
+
1097
+ self.api = VMRayRESTAPI(url, api_key, False)
1098
+
1099
+ self.ignore_analysis_finished = self._config_from_string(config.get("ignore_analysis_finished"))
1100
+ self._setup_optional_config(config)
1101
+ self.report_version = self._get_report_version()
1102
+
1103
+ def from_base64_string(
1104
+ self, config: Dict[str, Any], data: str, filename: str
1105
+ ) -> None:
1106
+ """ read base64 encoded summary json """
1107
+
1108
+ buffer = base64.b64decode(data)
1109
+ self.report = json.loads(buffer)
1110
+ self.report_name = filename
1111
+
1112
+ if "analysis_details" in self.report:
1113
+ self.report_version = ReportVersion.v1
1114
+ elif "analysis_metadata" in self.report:
1115
+ self.report_version = ReportVersion.v2
1116
+ else:
1117
+ raise VMRayParseError("Uploaded file is not a summary.json")
1118
+
1119
+ self._setup_optional_config(config)
1120
+ self.include_report = bool(int(config.get("Attach Report", "0")))
1121
+
1122
+ def _setup_optional_config(self, config: Dict[str, Any]) -> None:
1123
+ self.include_analysis_id = bool(int(config.get("Analysis ID", "1")))
1124
+ self.include_vti_details = bool(int(config.get("VTI", "1")))
1125
+ self.include_iocs = bool(int(config.get("IOCs", "1")))
1126
+ self.include_all_artifacts = bool(int(config.get("Artifacts", "0")))
1127
+ self.include_analysis_details = bool(int(config.get("Analysis Details", "1")))
1128
+
1129
+ self.use_misp_object = not self._config_from_string(
1130
+ config.get("disable_misp_objects")
1131
+ )
1132
+ self.tag_objects = not self._config_from_string(config.get("disable_tags"))
1133
+
1134
+ @staticmethod
1135
+ def _config_from_string(text: Optional[str]) -> bool:
1136
+ if not text:
1137
+ return False
1138
+
1139
+ text = text.lower()
1140
+ return text in ("yes", "true")
1141
+
1142
+ @staticmethod
1143
+ def _read_config_key(config: Dict[str, Any], key: str) -> str:
1144
+ try:
1145
+ value = config[key]
1146
+ return value
1147
+ except KeyError:
1148
+ raise VMRayParseError(f"VMRay config is missing a value for `{key}`.")
1149
+
1150
+ @staticmethod
1151
+ def _analysis_score_to_taxonomies(analysis_score: int) -> Optional[str]:
1152
+ mapping = {
1153
+ -1: "-1",
1154
+ 1: "1/5",
1155
+ 2: "2/5",
1156
+ 3: "3/5",
1157
+ 4: "4/5",
1158
+ 5: "5/5",
1159
+ }
1160
+
1161
+ try:
1162
+ return mapping[analysis_score]
1163
+ except KeyError:
1164
+ return None
1165
+
1166
+ def _get_report_version(self) -> ReportVersion:
1167
+ info = self._vmary_api_call("/rest/system_info")
1168
+ if info["version_major"] >= 4:
1169
+ return ReportVersion.v2
1170
+
1171
+ # version 3.2 an less do not tag artifacts as ICOs
1172
+ # so we extract all artifacts
1173
+ if info["version_major"] == 3 and info["version_minor"] < 3:
1174
+ self.include_all_artifacts = True
1175
+ return ReportVersion.v1
1176
+
1177
+ def _vmary_api_call(
1178
+ self, api_path: str, params: Dict[str, Any] = None, raw_data: bool = False
1179
+ ) -> Union[Dict[str, Any], bytes]:
1180
+ try:
1181
+ return self.api.call("GET", api_path, params=params, raw_data=raw_data)
1182
+ except (VMRayRESTAPIError, ValueError) as exc:
1183
+ raise VMRayParseError(str(exc))
1184
+
1185
+ def _get_analysis(self) -> Dict[str, Any]:
1186
+ return self._vmary_api_call(f"/rest/analysis/sample/{self.sample_id}")
1187
+
1188
+ def _analysis_finished(self) -> bool:
1189
+ result = self._vmary_api_call(f"/rest/submission/sample/{self.sample_id}")
1190
+
1191
+ all_finished = []
1192
+ for submission in result:
1193
+ finished = submission["submission_finished"]
1194
+ all_finished.append(finished)
1195
+
1196
+ return all(all_finished)
1197
+
1198
+ def _online_reports(self) -> Iterator[Tuple[ReportParser, str]]:
1199
+ # check if sample id exists
1200
+ try:
1201
+ self._vmary_api_call(f"/rest/sample/{self.sample_id}")
1202
+ except VMRayRESTAPIError:
1203
+ raise VMRayParseError(
1204
+ f"Could not find sample id `{self.sample_id}` on server."
1205
+ )
1206
+
1207
+ # check if all submission are finished
1208
+ if not self.ignore_analysis_finished and not self._analysis_finished():
1209
+ raise VMRayParseError(
1210
+ f"Not all analysis for `{self.sample_id}` are finished. "
1211
+ "Try it again in a few minutes."
1212
+ )
1213
+
1214
+ analysis_results = self._get_analysis()
1215
+ for analysis in analysis_results:
1216
+ analysis_id = analysis["analysis_id"]
1217
+ permalink = analysis["analysis_webif_url"]
1218
+
1219
+ # the summary json could not exist, due to a VM error
1220
+ try:
1221
+ if self.report_version == ReportVersion.v1:
1222
+ report_parser = Summary(api=self.api, analysis_id=analysis_id)
1223
+ else:
1224
+ report_parser = SummaryV2(api=self.api, analysis_id=analysis_id)
1225
+ except VMRayRESTAPIError:
1226
+ continue
1227
+
1228
+ yield report_parser, permalink
1229
+
1230
+ def _offline_report(self) -> ReportParser:
1231
+ if self.report_version == ReportVersion.v1:
1232
+ analysis_id = 0
1233
+ return Summary(report=self.report, analysis_id=analysis_id)
1234
+ else:
1235
+ analysis_id = self.report["analysis_metadata"]["analysis_id"]
1236
+ return SummaryV2(report=self.report, analysis_id=analysis_id)
1237
+
1238
+ def _reports(self) -> Iterator[Tuple[ReportParser, Optional[str]]]:
1239
+ if self.report:
1240
+ yield self._offline_report(), None
1241
+ else:
1242
+ yield from self._online_reports()
1243
+
1244
+ def _get_sample_verdict(self) -> Optional[str]:
1245
+ if self.report:
1246
+ if self.report_version == ReportVersion.v2:
1247
+ verdict = SummaryV2.convert_verdict(
1248
+ self.report["analysis_metadata"]["verdict"]
1249
+ )
1250
+ return verdict
1251
+ return None
1252
+
1253
+ data = self._vmary_api_call(f"/rest/sample/{self.sample_id}")
1254
+ if "sample_verdict" in data:
1255
+ verdict = SummaryV2.convert_verdict(data["sample_verdict"])
1256
+ return verdict
1257
+
1258
+ if "sample_severity" in data:
1259
+ verdict = Summary.to_verdict(data["sample_severity"])
1260
+ return verdict
1261
+
1262
+ return None
1263
+
1264
+ def parse(self) -> None:
1265
+ """ Convert analysis results to MISP Objects """
1266
+
1267
+ if self.use_misp_object:
1268
+ self.parse_as_misp_object()
1269
+ else:
1270
+ self.parse_as_attributes()
1271
+
1272
+ def parse_as_attributes(self) -> None:
1273
+ """
1274
+ Parse report as attributes
1275
+ This method is compatible with the implementation provided
1276
+ by Koen Van Impe
1277
+ """
1278
+
1279
+ for report, permalink in self._reports():
1280
+ if report.is_static_report():
1281
+ continue
1282
+
1283
+ if self.include_analysis_details:
1284
+ for detail in report.details():
1285
+ attr = Attribute(type="text", value=detail)
1286
+ self.attributes.append(attr)
1287
+
1288
+ classifications = report.classifications()
1289
+ if classifications:
1290
+ attr = Attribute(type="text", value=classifications)
1291
+ self.attributes.append(attr)
1292
+
1293
+ if self.include_vti_details:
1294
+ for vti in report.vtis():
1295
+ attr = Attribute(type="text", value=vti.operation)
1296
+ self.attributes.append(attr)
1297
+
1298
+ for artifact in report.artifacts():
1299
+ if self.include_all_artifacts or (
1300
+ self.include_iocs and artifact.is_ioc
1301
+ ):
1302
+ for attr in artifact.to_attributes():
1303
+ self.attributes.append(attr)
1304
+
1305
+ if self.include_analysis_id and permalink:
1306
+ attr = Attribute(type="link", value=permalink)
1307
+ self.attributes.append(attr)
1308
+
1309
+ def parse_as_misp_object(self):
1310
+ mitre_attacks = []
1311
+ vtis = []
1312
+ artifacts = []
1313
+
1314
+ # add sandbox signature
1315
+ sb_sig = MISPObject(name="sb-signature")
1316
+ sb_sig.add_attribute("software", "VMRay Platform")
1317
+
1318
+ for report, permalink in self._reports():
1319
+ if report.is_static_report():
1320
+ continue
1321
+
1322
+ # create sandbox object
1323
+ obj = MISPObject(name="sandbox-report")
1324
+ obj.add_attribute("on-premise-sandbox", "vmray")
1325
+
1326
+ if permalink:
1327
+ obj.add_attribute("permalink", permalink)
1328
+
1329
+ if self.include_report and self.report:
1330
+ report_data = base64.b64encode(
1331
+ json.dumps(self.report, indent=2).encode("utf-8")
1332
+ ).decode("utf-8")
1333
+ obj.add_attribute(
1334
+ "sandbox-file", value=self.report_name, data=report_data
1335
+ )
1336
+
1337
+ score = report.score()
1338
+ attr_score = obj.add_attribute("score", score)
1339
+
1340
+ if self.tag_objects:
1341
+ attr_score.add_tag(f'vmray:verdict="{score}"')
1342
+
1343
+ sandbox_type = report.sandbox_type()
1344
+ obj.add_attribute("sandbox-type", sandbox_type)
1345
+
1346
+ classifications = report.classifications()
1347
+ if classifications:
1348
+ obj.add_attribute("results", classifications)
1349
+
1350
+ self.event.add_object(obj)
1351
+
1352
+ if self.include_vti_details:
1353
+ for vti in report.vtis():
1354
+ if vti not in vtis:
1355
+ vtis.append(vti)
1356
+
1357
+ for artifact in report.artifacts():
1358
+ if self.include_all_artifacts or (
1359
+ self.include_iocs and artifact.is_ioc
1360
+ ):
1361
+ if artifact not in artifacts:
1362
+ artifacts.append(artifact)
1363
+ else:
1364
+ idx = artifacts.index(artifact)
1365
+ dup = artifacts[idx]
1366
+ dup.merge(artifact)
1367
+
1368
+ for mitre_attack in report.mitre_attacks():
1369
+ if mitre_attack not in mitre_attacks:
1370
+ mitre_attacks.append(mitre_attack)
1371
+
1372
+ # process VTI's
1373
+ for vti in vtis:
1374
+ vti_text = f"{vti.category}: {vti.operation}. {vti.technique}"
1375
+ vti_attr = sb_sig.add_attribute("signature", value=vti_text)
1376
+
1377
+ if self.tag_objects:
1378
+ value = self._analysis_score_to_taxonomies(vti.score)
1379
+ if value:
1380
+ vti_attr.add_tag(f'vmray:vti_analysis_score="{value}"')
1381
+
1382
+ self.event.add_object(sb_sig)
1383
+
1384
+ # process artifacts
1385
+ for artifact in artifacts:
1386
+ artifact_obj = artifact.to_misp_object(self.tag_objects)
1387
+ self.event.add_object(artifact_obj)
1388
+
1389
+ # tag event with Mitre Att&ck
1390
+ for mitre_attack in mitre_attacks:
1391
+ self.event.add_tag(mitre_attack.to_misp_galaxy())
1392
+
1393
+ # tag event
1394
+ if self.tag_objects:
1395
+ verdict = self._get_sample_verdict()
1396
+ if verdict:
1397
+ self.event.add_tag(f'vmray:verdict="{verdict}"')
1398
+
1399
+ def to_json(self) -> Dict[str, Any]:
1400
+ """ Convert parsed results into JSON """
1401
+
1402
+ if not self.use_misp_object:
1403
+ results = []
1404
+
1405
+ # remove duplicates
1406
+ for attribute in self.attributes:
1407
+ if attribute not in results:
1408
+ results.append(asdict(attribute))
1409
+
1410
+ # add attributes to event
1411
+ for attribute in results:
1412
+ self.event.add_attribute(**attribute)
1413
+
1414
+ self.event.run_expansions()
1415
+ event = json.loads(self.event.to_json())
1416
+
1417
+ return {"results": event}