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.
- misp_modules/__init__.py +379 -0
- misp_modules/helpers/__init__.py +1 -0
- misp_modules/helpers/cache.py +84 -0
- misp_modules/lib/__init__.py +4 -0
- misp_modules/lib/_vmray/__init__.py +0 -0
- misp_modules/lib/_vmray/parser.py +1417 -0
- misp_modules/lib/_vmray/rest_api.py +148 -0
- misp_modules/lib/cof2misp/LICENSE-2.0.txt +202 -0
- misp_modules/lib/cof2misp/__init__.py +0 -0
- misp_modules/lib/cof2misp/cof.py +165 -0
- misp_modules/lib/joe_mapping.py +114 -0
- misp_modules/lib/joe_parser.py +573 -0
- misp_modules/lib/lastline_api.py +841 -0
- misp_modules/lib/qintel_helper.py +263 -0
- misp_modules/lib/stix2misp.py +2080 -0
- misp_modules/lib/stix2misp_mapping.py +460 -0
- misp_modules/lib/synonymsToTagNames.json +1 -0
- misp_modules/lib/vt_graph_parser/__init__.py +8 -0
- misp_modules/lib/vt_graph_parser/errors.py +20 -0
- misp_modules/lib/vt_graph_parser/helpers/__init__.py +7 -0
- misp_modules/lib/vt_graph_parser/helpers/parsers.py +88 -0
- misp_modules/lib/vt_graph_parser/helpers/rules.py +304 -0
- misp_modules/lib/vt_graph_parser/helpers/wrappers.py +58 -0
- misp_modules/lib/vt_graph_parser/importers/__init__.py +7 -0
- misp_modules/lib/vt_graph_parser/importers/base.py +98 -0
- misp_modules/lib/vt_graph_parser/importers/pymisp_response.py +73 -0
- misp_modules/modules/__init__.py +4 -0
- misp_modules/modules/action_mod/__init__.py +1 -0
- misp_modules/modules/action_mod/_utils/__init__.py +1 -0
- misp_modules/modules/action_mod/_utils/utils.py +70 -0
- misp_modules/modules/action_mod/mattermost.py +113 -0
- misp_modules/modules/action_mod/slack.py +96 -0
- misp_modules/modules/action_mod/testaction.py +68 -0
- misp_modules/modules/expansion/__init__.py +36 -0
- misp_modules/modules/expansion/_dnsdb_query/COPYRIGHT +27 -0
- misp_modules/modules/expansion/_dnsdb_query/LICENSE +202 -0
- misp_modules/modules/expansion/_dnsdb_query/README.md +162 -0
- misp_modules/modules/expansion/_dnsdb_query/__init__.py +0 -0
- misp_modules/modules/expansion/_dnsdb_query/dnsdb_query.py +327 -0
- misp_modules/modules/expansion/_ransomcoindb/__init__.py +0 -0
- misp_modules/modules/expansion/_ransomcoindb/ransomcoindb.py +96 -0
- misp_modules/modules/expansion/_vulnerability_parser/__init__.py +0 -0
- misp_modules/modules/expansion/_vulnerability_parser/vulnerability_parser.py +118 -0
- misp_modules/modules/expansion/abuseipdb.py +146 -0
- misp_modules/modules/expansion/apiosintds.py +365 -0
- misp_modules/modules/expansion/apivoid.py +133 -0
- misp_modules/modules/expansion/assemblyline_query.py +179 -0
- misp_modules/modules/expansion/assemblyline_submit.py +100 -0
- misp_modules/modules/expansion/backscatter_io.py +84 -0
- misp_modules/modules/expansion/btc_scam_check.py +54 -0
- misp_modules/modules/expansion/btc_steroids.py +238 -0
- misp_modules/modules/expansion/censys_enrich.py +287 -0
- misp_modules/modules/expansion/circl_passivedns.py +85 -0
- misp_modules/modules/expansion/circl_passivessl.py +113 -0
- misp_modules/modules/expansion/clamav.py +129 -0
- misp_modules/modules/expansion/cluster25_expand.py +239 -0
- misp_modules/modules/expansion/countrycode.py +69 -0
- misp_modules/modules/expansion/cpe.py +140 -0
- misp_modules/modules/expansion/crowdsec.py +235 -0
- misp_modules/modules/expansion/crowdstrike_falcon.py +148 -0
- misp_modules/modules/expansion/cuckoo_submit.py +161 -0
- misp_modules/modules/expansion/cve.py +57 -0
- misp_modules/modules/expansion/cve_advanced.py +185 -0
- misp_modules/modules/expansion/cytomic_orion.py +196 -0
- misp_modules/modules/expansion/dbl_spamhaus.py +78 -0
- misp_modules/modules/expansion/dns.py +71 -0
- misp_modules/modules/expansion/docx_enrich.py +71 -0
- misp_modules/modules/expansion/domaintools.py +290 -0
- misp_modules/modules/expansion/eql.py +91 -0
- misp_modules/modules/expansion/eupi.py +86 -0
- misp_modules/modules/expansion/extract_url_components.py +83 -0
- misp_modules/modules/expansion/farsight_passivedns.py +243 -0
- misp_modules/modules/expansion/geoip_asn.py +75 -0
- misp_modules/modules/expansion/geoip_city.py +75 -0
- misp_modules/modules/expansion/geoip_country.py +73 -0
- misp_modules/modules/expansion/google_safe_browsing.py +86 -0
- misp_modules/modules/expansion/google_search.py +57 -0
- misp_modules/modules/expansion/google_threat_intelligence.py +453 -0
- misp_modules/modules/expansion/greynoise.py +346 -0
- misp_modules/modules/expansion/hashdd.py +53 -0
- misp_modules/modules/expansion/hashlookup.py +118 -0
- misp_modules/modules/expansion/hibp.py +62 -0
- misp_modules/modules/expansion/html_to_markdown.py +63 -0
- misp_modules/modules/expansion/hyasinsight.py +881 -0
- misp_modules/modules/expansion/intel471.py +73 -0
- misp_modules/modules/expansion/intelmq_eventdb.py.experimental +67 -0
- misp_modules/modules/expansion/ip2locationio.py +90 -0
- misp_modules/modules/expansion/ipasn.py +70 -0
- misp_modules/modules/expansion/ipinfo.py +112 -0
- misp_modules/modules/expansion/ipqs_fraud_and_risk_scoring.py +633 -0
- misp_modules/modules/expansion/iprep.py +98 -0
- misp_modules/modules/expansion/jinja_template_rendering.py +54 -0
- misp_modules/modules/expansion/joesandbox_query.py +91 -0
- misp_modules/modules/expansion/joesandbox_submit.py +147 -0
- misp_modules/modules/expansion/lastline_query.py +148 -0
- misp_modules/modules/expansion/lastline_submit.py +180 -0
- misp_modules/modules/expansion/macaddress_io.py +130 -0
- misp_modules/modules/expansion/macvendors.py +55 -0
- misp_modules/modules/expansion/malshare_upload.py +107 -0
- misp_modules/modules/expansion/malwarebazaar.py +67 -0
- misp_modules/modules/expansion/mcafee_insights_enrich.py +249 -0
- misp_modules/modules/expansion/mmdb_lookup.py +138 -0
- misp_modules/modules/expansion/module.py.skeleton +44 -0
- misp_modules/modules/expansion/mwdb.py +152 -0
- misp_modules/modules/expansion/ocr_enrich.py +74 -0
- misp_modules/modules/expansion/ods_enrich.py +71 -0
- misp_modules/modules/expansion/odt_enrich.py +61 -0
- misp_modules/modules/expansion/onyphe.py +243 -0
- misp_modules/modules/expansion/onyphe_full.py +387 -0
- misp_modules/modules/expansion/otx.py +167 -0
- misp_modules/modules/expansion/passive_ssh.py +150 -0
- misp_modules/modules/expansion/passivetotal.py +358 -0
- misp_modules/modules/expansion/pdf_enrich.py +58 -0
- misp_modules/modules/expansion/pptx_enrich.py +65 -0
- misp_modules/modules/expansion/qintel_qsentry.py +229 -0
- misp_modules/modules/expansion/qrcode.py +99 -0
- misp_modules/modules/expansion/ransomcoindb.py +82 -0
- misp_modules/modules/expansion/rbl.py +126 -0
- misp_modules/modules/expansion/recordedfuture.py +544 -0
- misp_modules/modules/expansion/reversedns.py +77 -0
- misp_modules/modules/expansion/securitytrails.py +571 -0
- misp_modules/modules/expansion/shodan.py +244 -0
- misp_modules/modules/expansion/sigma_queries.py +61 -0
- misp_modules/modules/expansion/sigma_syntax_validator.py +49 -0
- misp_modules/modules/expansion/sigmf_expand.py +303 -0
- misp_modules/modules/expansion/socialscan.py +108 -0
- misp_modules/modules/expansion/sophoslabs_intelix.py +146 -0
- misp_modules/modules/expansion/sourcecache.py +57 -0
- misp_modules/modules/expansion/stairwell.py +156 -0
- misp_modules/modules/expansion/stix2_pattern_syntax_validator.py +56 -0
- misp_modules/modules/expansion/threatcrowd.py +170 -0
- misp_modules/modules/expansion/threatfox.py +75 -0
- misp_modules/modules/expansion/threatminer.py +152 -0
- misp_modules/modules/expansion/triage_submit.py +119 -0
- misp_modules/modules/expansion/trustar_enrich.py +235 -0
- misp_modules/modules/expansion/urlhaus.py +164 -0
- misp_modules/modules/expansion/urlscan.py +269 -0
- misp_modules/modules/expansion/variotdbs.py +133 -0
- misp_modules/modules/expansion/virustotal.py +311 -0
- misp_modules/modules/expansion/virustotal_public.py +272 -0
- misp_modules/modules/expansion/virustotal_upload.py +91 -0
- misp_modules/modules/expansion/vmray_submit.py +161 -0
- misp_modules/modules/expansion/vmware_nsx.py +628 -0
- misp_modules/modules/expansion/vulndb.py +292 -0
- misp_modules/modules/expansion/vulnerability_lookup.py +324 -0
- misp_modules/modules/expansion/vulners.py +81 -0
- misp_modules/modules/expansion/vysion.py +221 -0
- misp_modules/modules/expansion/whois.py +72 -0
- misp_modules/modules/expansion/whoisfreaks.py +232 -0
- misp_modules/modules/expansion/wiki.py +58 -0
- misp_modules/modules/expansion/xforceexchange.py +189 -0
- misp_modules/modules/expansion/xlsx_enrich.py +63 -0
- misp_modules/modules/expansion/yara_query.py +70 -0
- misp_modules/modules/expansion/yara_syntax_validator.py +49 -0
- misp_modules/modules/expansion/yeti.py +196 -0
- misp_modules/modules/export_mod/__init__.py +3 -0
- misp_modules/modules/export_mod/cef_export.py +93 -0
- misp_modules/modules/export_mod/cisco_firesight_manager_ACL_rule_export.py +150 -0
- misp_modules/modules/export_mod/defender_endpoint_export.py +141 -0
- misp_modules/modules/export_mod/goamlexport.py +255 -0
- misp_modules/modules/export_mod/liteexport.py +97 -0
- misp_modules/modules/export_mod/mass_eql_export.py +106 -0
- misp_modules/modules/export_mod/nexthinkexport.py +131 -0
- misp_modules/modules/export_mod/osqueryexport.py +125 -0
- misp_modules/modules/export_mod/pdfexport.py +107 -0
- misp_modules/modules/export_mod/testexport.py +73 -0
- misp_modules/modules/export_mod/threatStream_misp_export.py +114 -0
- misp_modules/modules/export_mod/threat_connect_export.py +125 -0
- misp_modules/modules/export_mod/virustotal_collections.py +141 -0
- misp_modules/modules/export_mod/vt_graph.py +120 -0
- misp_modules/modules/export_mod/yara_export.py +290 -0
- misp_modules/modules/import_mod/__init__.py +22 -0
- misp_modules/modules/import_mod/cof2misp.py +264 -0
- misp_modules/modules/import_mod/csvimport.py +319 -0
- misp_modules/modules/import_mod/cuckooimport.py +749 -0
- misp_modules/modules/import_mod/email_import.py +294 -0
- misp_modules/modules/import_mod/goamlimport.py +188 -0
- misp_modules/modules/import_mod/import_blueprint.py +93 -0
- misp_modules/modules/import_mod/joe_import.py +74 -0
- misp_modules/modules/import_mod/lastline_import.py +160 -0
- misp_modules/modules/import_mod/mispjson.py +73 -0
- misp_modules/modules/import_mod/ocr.py +122 -0
- misp_modules/modules/import_mod/openiocimport.py +99 -0
- misp_modules/modules/import_mod/taxii21.py +383 -0
- misp_modules/modules/import_mod/testimport.py +73 -0
- misp_modules/modules/import_mod/threatanalyzer_import.py +552 -0
- misp_modules/modules/import_mod/url_import.py +97 -0
- misp_modules/modules/import_mod/vmray_import.py +96 -0
- misp_modules/modules/import_mod/vmray_summary_json_import.py +87 -0
- misp_modules-2.4.196.dist-info/LICENSE +661 -0
- misp_modules-2.4.196.dist-info/METADATA +281 -0
- misp_modules-2.4.196.dist-info/RECORD +194 -0
- misp_modules-2.4.196.dist-info/WHEEL +4 -0
- 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}
|