Skip to content

Commit 3b2a671

Browse files
authored
Merge pull request #747 from MISP/semayellow-feature/dev-01-anyrun-sandbox-integration
Semayellow feature/dev 01 anyrun sandbox integration
2 parents 34fdfce + ebeacef commit 3b2a671

8 files changed

Lines changed: 653 additions & 122 deletions

File tree

misp_modules/lib/anyrun_sandbox/__init__.py

Whitespace-only changes.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class Config:
2+
INTEGRATION: str = 'MISP:0.1'
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import json
2+
from http import HTTPStatus
3+
from pymisp import MISPEvent, MISPObject
4+
5+
from anyrun import RunTimeException
6+
from anyrun.connectors.sandbox.operation_systems import WindowsConnector, LinuxConnector, AndroidConnector
7+
8+
9+
class AnyRunParser:
10+
"""
11+
Implements functionality for parsing ANY.RUN Sandbox reports into MISP objects and attributes
12+
"""
13+
def __init__(
14+
self,
15+
config: dict[str, str],
16+
analysis_uuid: str,
17+
connector: WindowsConnector | LinuxConnector | AndroidConnector
18+
) -> None:
19+
self._event = MISPEvent()
20+
self._connector = connector
21+
self._analysis_uuid = analysis_uuid
22+
self._config = config
23+
24+
def generate_results(self) -> dict[str, dict]:
25+
"""
26+
Enriches the results dictionary with ANY.RUN report objects
27+
28+
:return: MISP results dictionary
29+
"""
30+
self._check_analysis_completion()
31+
32+
summary = self._connector.get_analysis_report(self._analysis_uuid)
33+
34+
self._add_report()
35+
36+
if self._check_option("IOCs"):
37+
self._add_indicators()
38+
39+
if self._check_option("Tags"):
40+
self._add_tags(summary)
41+
42+
if self._check_option("MITRE"):
43+
self._add_mitre_galaxies(summary)
44+
45+
results = json.loads(self._event.to_json())
46+
47+
return {
48+
"results": {
49+
key: results[key]
50+
for key in ("Attribute", "Object", "Tag")
51+
if (key in results and results[key])
52+
}
53+
}
54+
55+
def _check_analysis_completion(self) -> None:
56+
"""
57+
Checks if ANY.RUN Sandbox analysis is completed
58+
59+
:raises RunTimeException: If analysis is not completed
60+
"""
61+
status = self._connector.get_analysis_report(self._analysis_uuid).get("data").get("status")
62+
63+
if status != "done":
64+
raise RunTimeException(
65+
f"Analysis is running. Please, wait a few minutes to request a report",
66+
HTTPStatus.BAD_REQUEST
67+
)
68+
69+
def _add_indicators(self) -> None:
70+
"""
71+
Converts ANY.RUN indicators to the MISP attributes
72+
"""
73+
if iocs := self._connector.get_analysis_report(self._analysis_uuid, report_format="ioc"):
74+
for ioc in iocs:
75+
if ioc.get("type") in ("domain", "ip", "sha256") and ioc.get("reputation") in (1, 2):
76+
ioc_type = ioc.get("type")
77+
ioc_reputation = {1: "Suspicious", 2: "Malicious"}.get(ioc.get("reputation"))
78+
attribute = self._event.add_attribute(
79+
type=ioc_type if ioc_type in ("domain", "sha256") else "ip-dst",
80+
value=ioc.get("ioc"),
81+
categories="Network activity" if ioc_type in ("ip", "domain") else "Payload delivery",
82+
comment=f"{ioc_reputation} IoC from https://app.any.run/tasks/{self._analysis_uuid}."
83+
)
84+
85+
attribute.add_tag("ANY.RUN Sandbox")
86+
87+
def _add_tags(self, summary: dict) -> None:
88+
"""
89+
Converts ANY.RUN analysis tags to the MISP tags
90+
91+
:param summary: ANY.RUN Sandbox report
92+
"""
93+
self._event.add_tag("ANY.RUN Sandbox")
94+
if tags := summary.get("data").get("analysis").get("tags"):
95+
for tag in tags:
96+
self._event.add_tag(tag.get("tag"))
97+
98+
def _add_mitre_galaxies(self, summary: dict) -> None:
99+
"""
100+
Converts ANY.RUN analysis mitre techniques to the MISP Galaxies
101+
102+
:param summary: ANY.RUN Sandbox report
103+
"""
104+
if mitre_techniques := summary.get("data").get("mitre"):
105+
for entry in mitre_techniques:
106+
if entry:
107+
mitre_name = entry.get("name")
108+
mitre_id = entry.get("id")
109+
self._event.add_tag(f'misp-galaxy:mitre-attack-pattern="{mitre_name} - {mitre_id}"')
110+
111+
def _add_report(self) -> None:
112+
"""
113+
Converts ANY.RUN analysis HTML report and external references to the MISP attributes
114+
115+
:return:
116+
"""
117+
report = self._connector.get_analysis_report(self._analysis_uuid, report_format="html")
118+
119+
self._event.add_attribute(
120+
type="text",
121+
value=f"ANY.RUN Sandbox verdict: {self._connector.get_analysis_verdict(self._analysis_uuid)}",
122+
categories="Other",
123+
comment="ANYRUN Sandbox Analysis verdict."
124+
)
125+
126+
self._event.add_attribute(
127+
type="link",
128+
value=f"https://app.any.run/tasks/{self._analysis_uuid}",
129+
categories="External analysis",
130+
comment="ANYRUN Sandbox Analysis report."
131+
)
132+
133+
self._event.add_attribute(
134+
type="attachment",
135+
value="report.html",
136+
data=report.encode(),
137+
comment="ANYRUN Sandbox Analysis report."
138+
)
139+
140+
def _check_option(self, option_name: str) -> bool:
141+
"""
142+
Checks if option is active
143+
144+
:param option_name: Option name to check
145+
:return: True if option is enabled else False
146+
"""
147+
return bool(int(self._config.get(option_name)))
Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import io
2+
import base64
3+
import zipfile
4+
5+
from anyrun import RunTimeException
6+
from anyrun.connectors import SandboxConnector
7+
from anyrun.connectors.sandbox.operation_systems import WindowsConnector, LinuxConnector, AndroidConnector
8+
9+
from anyrun_sandbox.config import Config
10+
11+
12+
class AnyRunSubmitter:
13+
"""
14+
Implements functionality for sending MISP entities to the ANY.RUN sandbox
15+
"""
16+
def __init__(self, request: dict) -> None:
17+
self._request = request
18+
self._config: dict = request.get("config")
19+
20+
def submit(self) -> str:
21+
"""
22+
Configures ANY.RUN Sandbox environment and executes the analysis
23+
24+
:return: ANY.RUN Sandbox analysis uuid
25+
"""
26+
self._parse_config()
27+
self._sanitize_config()
28+
29+
if self._os_type == "windows":
30+
with SandboxConnector.windows(self._token, Config.INTEGRATION) as connector:
31+
analysis_uuid = self._process_analysis(connector)
32+
elif self._os_type in ("ubuntu", "debian"):
33+
with SandboxConnector.linux(self._token, Config.INTEGRATION) as connector:
34+
analysis_uuid = self._process_analysis(connector)
35+
elif self._os_type == "android":
36+
with SandboxConnector.android(self._token, Config.INTEGRATION) as connector:
37+
analysis_uuid = self._process_analysis(connector)
38+
else:
39+
raise RunTimeException(
40+
f"Received invalid OS type: {self._os_type}. Supports: windows, ubuntu, debian, android"
41+
)
42+
43+
return analysis_uuid
44+
45+
def _parse_config(self) -> None:
46+
"""
47+
Configures analysis options according to the chosen environment
48+
49+
"""
50+
self._token = self._config.pop("api_key", "")
51+
self._os_type = self._config.pop("os_type", "")
52+
53+
if not any((self._token, self._os_type)):
54+
raise RunTimeException(f"ANYRUN Sandbox API-KEY and OS type must be specified.")
55+
56+
if "url" in self._request:
57+
self._prepare_url_params()
58+
self._config["obj_url"] = self._request.get("url")
59+
elif "malware-sample" in self._request:
60+
self._prepare_file_params()
61+
self._prepare_file_content("malware-sample")
62+
elif "attachment" in self._request:
63+
self._prepare_file_params()
64+
self._prepare_file_content("attachment")
65+
else:
66+
raise RunTimeException("Received invalid Object. Supports: url, attachment, malware-sample.")
67+
68+
def _process_analysis(self, connector: WindowsConnector | LinuxConnector | AndroidConnector) -> str:
69+
"""
70+
Executes analysis
71+
72+
:param connector: Instance of the ANY.RUN connector
73+
:return: ANY.RUN Sandbox analysis uuid
74+
"""
75+
connector.check_authorization()
76+
77+
if "obj_url" in self._config:
78+
analysis_uuid = connector.run_url_analysis(**self._config)
79+
else:
80+
analysis_uuid = connector.run_file_analysis(**self._config)
81+
return analysis_uuid
82+
83+
def _prepare_url_params(self) -> None:
84+
"""
85+
Prepares analysis configuration for the url submission
86+
"""
87+
if self._os_type != "windows":
88+
self._config.pop("env_version", "")
89+
self._config.pop("env_bitness", "")
90+
self._config.pop("env_type", "")
91+
92+
self._config.pop("obj_ext_extension", "")
93+
self._config.pop("obj_ext_startfolder", "")
94+
self._config.pop("obj_ext_cmd", "")
95+
self._config.pop("obj_force_elevation", "")
96+
self._config.pop("run_as_root", "")
97+
98+
99+
def _prepare_file_params(self) -> None:
100+
"""
101+
Prepares analysis configuration for the file submission
102+
"""
103+
self._config.pop("obj_ext_browser", "")
104+
105+
if self._os_type == "windows":
106+
self._config.pop("run_as_root", "")
107+
elif self._os_type in ("ubuntu", "debian"):
108+
self._config.pop("env_version", "")
109+
self._config.pop("env_bitness", "")
110+
self._config.pop("env_type", "")
111+
self._config.pop("obj_force_elevation", "")
112+
self._config["env_os"] = self._os_type
113+
elif self._os_type == "android":
114+
self._config.pop("env_version", "")
115+
self._config.pop("env_bitness", "")
116+
self._config.pop("env_type", "")
117+
self._config.pop("obj_force_elevation", "")
118+
self._config.pop("obj_ext_startfolder", "")
119+
self._config.pop("run_as_root", "")
120+
121+
def _prepare_file_content(self, sample_type: str) -> None:
122+
"""
123+
Prepares file content to the analysis
124+
125+
:param sample_type: Attachment or malware-sample MISP entity
126+
"""
127+
if sample_type == "attachment":
128+
filename = self._request.get("attachment")
129+
file_content = self._extract_file_content(self._request.get("data"))
130+
else:
131+
filename = self._request.get("malware-sample").split("|", 1)[0]
132+
file_content = self._extract_file_content(self._request.get("data"), is_encoded=True)
133+
134+
self._config["filename"] = filename
135+
self._config["file_content"] = file_content
136+
137+
def _sanitize_config(self) -> None:
138+
"""
139+
Removes empty parameters from the analysis configuration
140+
"""
141+
temp_config = dict()
142+
143+
for key, value in self._config.items():
144+
if value is not None:
145+
temp_config[key] = value
146+
147+
self._config = temp_config
148+
149+
@staticmethod
150+
def _extract_file_content(file_content: str, is_encoded: bool = False) -> bytes:
151+
"""
152+
Extracts file content from the **malware-sample** MISP entity
153+
154+
:param file_content: Base64 file content
155+
:param is_encoded: Marks if **malware-sample** or **attachment** entity received
156+
:return: File bytes payload
157+
"""
158+
data = base64.b64decode(file_content)
159+
160+
if is_encoded:
161+
with zipfile.ZipFile(io.BytesIO(data)) as file:
162+
data = file.read(file.namelist()[0], pwd=b"infected")
163+
164+
return data

0 commit comments

Comments
 (0)