Skip to content

Commit 078affb

Browse files
Merge pull request #49 from MislavReversingLabs/main
2.6.1
2 parents eea710a + 6dd86bd commit 078affb

File tree

8 files changed

+1335
-13
lines changed

8 files changed

+1335
-13
lines changed

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ jobs:
2323
- name: Install dependencies
2424
run: |
2525
python -m pip install --upgrade pip
26-
pip install pytest requests
26+
pip install pytest requests unittest
2727
- name: Test with pytest
2828
run: |
2929
pytest

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -304,4 +304,11 @@ v2.5.1 (2024-04-02)
304304

305305
### Scheduled removals
306306
- **December 2024.**:
307-
- In the `ticloud.DynamicAnalysis.detonate_sample` method the `sample_sha1` parameter will be removed.
307+
- In the `ticloud.DynamicAnalysis.detonate_sample` method the `sample_sha1` parameter will be removed.
308+
309+
310+
2.6.1 (2024-07-03)
311+
-------------------
312+
313+
#### Improvements
314+
- Added more unit tests for all currently available modules.

ReversingLabs/SDK/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@
55
A Python SDK for communicating with ReversingLabs services.
66
"""
77

8-
__version__ = "2.6.0"
8+
__version__ = "2.6.1"

ReversingLabs/SDK/ticloud.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3255,15 +3255,19 @@ def __detonate(self, platform, sample_hash=None, url_string=None, is_archive=Fal
32553255
if not isinstance(internet_simulation, bool):
32563256
raise WrongInputError("internet_simulation parameter must be boolean.")
32573257

3258-
if internet_simulation:
3259-
post_json["rl"]["optional_parameters"] = "internet_simulation=true"
3260-
32613258
if sample_hash:
32623259
hash_type = HASH_LENGTH_MAP.get(len(sample_hash))
32633260
post_json["rl"][hash_type] = sample_hash
32643261

3262+
optional_parameters = []
3263+
32653264
if sample_name:
3266-
post_json["rl"]["sample_name"] = sample_name
3265+
optional_parameters.append(f"sample_name={sample_name}")
3266+
3267+
if internet_simulation:
3268+
optional_parameters.append("internet_simulation=true")
3269+
3270+
post_json["rl"]["optional_parameters"] = ", ".join(optional_parameters)
32673271

32683272
elif url_string:
32693273
post_json["rl"]["url"] = url_string

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
packages=packages,
2323
python_requires=">=3.6",
2424
install_requires=requires,
25-
extras_require={"test": ["pytest"]},
25+
extras_require={"test": ["pytest", "unittest"]},
2626
license="MIT",
2727
zip_safe=False,
2828
classifiers=[

tests/test_a1000.py

Lines changed: 314 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import pytest
2+
from unittest import mock
23
from ReversingLabs.SDK import __version__
34
from ReversingLabs.SDK.a1000 import CLASSIFICATIONS, AVAILABLE_PLATFORMS, A1000
4-
from ReversingLabs.SDK.helper import WrongInputError
5+
from ReversingLabs.SDK.helper import WrongInputError, DEFAULT_USER_AGENT
56

67

8+
MD5 = "512fca9e83c47fd9c36aa7d50a856396"
9+
SHA1 = "5377d0ed664246a604363f90a2764aa10fa63ad0"
10+
SHA256 = "00f8cd09187d311707b52a1c52018e7cfb5f2f78e47bf9200f16281098741422"
711
EXPECTED_PLATFORMS = ("windows7", "windows10", "macos_11", "windows11", "linux")
812

913

@@ -44,3 +48,312 @@ def test_a1000_object():
4448
authorization = a1000._headers.get("Authorization")
4549
assert authorization == f"Token {token}"
4650

51+
52+
@pytest.fixture
53+
def requests_mock():
54+
with mock.patch('ReversingLabs.SDK.a1000.requests', autospec=True) as requests_mock:
55+
yield requests_mock
56+
57+
58+
class TestA1000:
59+
host = "https://my.host"
60+
token = "token"
61+
fields = ("id", "sha1", "sha256", "sha512", "md5", "category", "file_type", "file_subtype",
62+
"identification_name", "identification_version", "file_size", "extracted_file_count",
63+
"local_first_seen", "local_last_seen", "classification_origin", "classification_reason",
64+
"classification_source", "classification", "riskscore", "classification_result", "ticore", "tags",
65+
"summary", "ticloud", "aliases", "networkthreatintelligence", "domainthreatintelligence"
66+
)
67+
68+
ticore_fields = "sha1, sha256, sha512, md5, imphash, info, application, protection, security, behaviour," \
69+
" certificate, document, mobile, media, web, email, strings, interesting_strings," \
70+
" classification, indicators, tags, attack, story"
71+
72+
@classmethod
73+
def setup_class(cls):
74+
cls.a1000 = A1000(cls.host, token=cls.token)
75+
76+
def test_sample_from_url(self, requests_mock):
77+
self.a1000.upload_sample_from_url(file_url="https://some.url")
78+
79+
expected_url = f"{self.host}/api/uploads/"
80+
81+
requests_mock.post.assert_called_with(
82+
url=expected_url,
83+
verify=True,
84+
proxies=None,
85+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
86+
params=None,
87+
json=None,
88+
data={"url": "https://some.url", "analysis": "cloud"},
89+
files=None
90+
)
91+
92+
def test_wrong_id(self, requests_mock):
93+
with pytest.raises(WrongInputError, match=r"task_id parameter must be a string."):
94+
self.a1000.get_submitted_url_report(task_id=123, retry=False)
95+
96+
assert not requests_mock.get.called
97+
98+
def test_classification(self, requests_mock):
99+
self.a1000.get_classification_v3(sample_hash=SHA1, local_only=True)
100+
101+
expected_url = f"{self.host}/api/samples/v3/{SHA1}/classification/?localonly=1&av_scanners=0"
102+
103+
requests_mock.get.assert_called_with(
104+
url=expected_url,
105+
verify=True,
106+
proxies=None,
107+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
108+
params=None
109+
)
110+
111+
def test_reanalyze(self, requests_mock):
112+
self.a1000.reanalyze_samples_v2(
113+
hash_input=SHA1,
114+
titanium_cloud=True
115+
)
116+
117+
data = {
118+
"hash_value": [SHA1],
119+
"analysis": "cloud",
120+
"rl_cloud_sandbox_platform": None
121+
}
122+
123+
requests_mock.post.assert_called_with(
124+
url=f"{self.host}/api/samples/v2/analyze_bulk/",
125+
verify=True,
126+
proxies=None,
127+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
128+
params=None,
129+
json=None,
130+
data=data,
131+
files=None
132+
)
133+
134+
def test_extracted_files(self, requests_mock):
135+
self.a1000.list_extracted_files_v2(SHA1)
136+
137+
requests_mock.get.assert_called_with(
138+
url=f"{self.host}/api/samples/v2/{SHA1}/extracted-files/",
139+
verify=True,
140+
proxies=None,
141+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
142+
params=None
143+
)
144+
145+
def test_download_extracted(self, requests_mock):
146+
self.a1000.download_extracted_files(SHA1)
147+
148+
requests_mock.get.assert_called_with(
149+
url=f"{self.host}/api/samples/{SHA1}/unpacked/",
150+
verify=True,
151+
proxies=None,
152+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
153+
params=None
154+
)
155+
156+
def test_delete_file(self, requests_mock):
157+
self.a1000.delete_samples([SHA1, SHA1])
158+
159+
data = {"hash_values": [SHA1, SHA1]}
160+
161+
requests_mock.post.assert_called_with(
162+
url=f"{self.host}/api/samples/v2/delete_bulk/",
163+
verify=True,
164+
proxies=None,
165+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
166+
params=None,
167+
json=None,
168+
data=data,
169+
files=None
170+
)
171+
172+
def test_pdf_report(self, requests_mock):
173+
self.a1000.create_pdf_report(SHA1)
174+
175+
requests_mock.get.assert_called_with(
176+
url=f"{self.host}/api/pdf/{SHA1}/create",
177+
verify=True,
178+
proxies=None,
179+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
180+
params=None
181+
)
182+
183+
def test_ticore_report(self, requests_mock):
184+
self.a1000.get_titanium_core_report_v2(SHA1)
185+
186+
expected_url = f"{self.host}/api/v2/samples/{SHA1}/ticore/?fields={self.ticore_fields}"
187+
188+
requests_mock.get.assert_called_with(
189+
url=expected_url,
190+
verify=True,
191+
proxies=None,
192+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
193+
params=None
194+
)
195+
196+
def test_dynamic_report(self, requests_mock):
197+
self.a1000.create_dynamic_analysis_report(SHA1, "pdf")
198+
199+
expected_url = f"{self.host}/api/rl_dynamic_analysis/export/summary/{SHA1}/pdf/create/"
200+
201+
requests_mock.get.assert_called_with(
202+
url=expected_url,
203+
verify=True,
204+
proxies=None,
205+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
206+
params=None
207+
)
208+
209+
def test_wrong_dynamic_params(self, requests_mock):
210+
with pytest.raises(WrongInputError, match=r"report_format parameter must be either 'html' or 'pdf'."):
211+
self.a1000.download_dynamic_analysis_report(SHA1, "xml")
212+
213+
assert not requests_mock.get.called
214+
215+
def test_set_classification(self, requests_mock):
216+
self.a1000.set_classification(SHA1, classification="malicious", system="local")
217+
218+
data = {
219+
"classification": "malicious",
220+
"analysis": "cloud"
221+
}
222+
223+
expected_url = f"{self.host}/api/samples/{SHA1}/setclassification/local/"
224+
225+
requests_mock.post.assert_called_with(
226+
url=expected_url,
227+
verify=True,
228+
proxies=None,
229+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
230+
params=None,
231+
json=None,
232+
data=data,
233+
files=None
234+
)
235+
236+
def test_user_tags(self, requests_mock):
237+
self.a1000.post_user_tags(SHA1, ["tag1", "tag2"])
238+
239+
post_json = {"tags": ["tag1", "tag2"]}
240+
241+
expected_url = f"{self.host}/api/tag/{SHA1}/"
242+
243+
requests_mock.post.assert_called_with(
244+
url=expected_url,
245+
verify=True,
246+
proxies=None,
247+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
248+
params=None,
249+
json=post_json,
250+
data=None,
251+
files=None
252+
)
253+
254+
def test_yara(self, requests_mock):
255+
self.a1000.get_yara_rulesets_on_the_appliance_v2(source="all")
256+
257+
expected_url = f"{self.host}/api/yara/v2/rulesets/?source=all"
258+
259+
requests_mock.get.assert_called_with(
260+
url=expected_url,
261+
verify=True,
262+
proxies=None,
263+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
264+
params=None
265+
)
266+
267+
def test_enable_yara(self, requests_mock):
268+
self.a1000.enable_or_disable_yara_ruleset(
269+
enabled=True,
270+
name="the_ruleset",
271+
publish=True
272+
)
273+
274+
data = {
275+
"name": "the_ruleset",
276+
"publish": True,
277+
"analysis": "cloud"
278+
}
279+
280+
expected_url = f"{self.host}/api/yara/ruleset/enable/"
281+
282+
requests_mock.post.assert_called_with(
283+
url=expected_url,
284+
verify=True,
285+
proxies=None,
286+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
287+
params=None,
288+
json=None,
289+
data=data,
290+
files=None
291+
)
292+
293+
def test_start_yara_retro(self, requests_mock):
294+
self.a1000.start_or_stop_yara_local_retro_scan("START")
295+
296+
requests_mock.post.assert_called_with(
297+
url=f"{self.host}/api/uploads/local-retro-hunt/",
298+
verify=True,
299+
proxies=None,
300+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
301+
params=None,
302+
json=None,
303+
data={"operation": "START"},
304+
files=None
305+
)
306+
307+
def test_wrong_operation(self, requests_mock):
308+
with pytest.raises(WrongInputError, match=r"operation parameter must be either 'START' or 'STOP'"):
309+
self.a1000.start_or_stop_yara_local_retro_scan("BEGIN")
310+
311+
assert not requests_mock.post.called
312+
313+
def test_advanced_search(self, requests_mock):
314+
self.a1000.advanced_search_v3(query_string="av-count:5 available:TRUE", sorting_criteria="sha1", sorting_order="desc", page_number=2, records_per_page=5)
315+
316+
post_json = {"query": "av-count:5 available:TRUE", "ticloud": False, "page": 2,
317+
"records_per_page": 5, "sort": "sha1 desc"}
318+
319+
requests_mock.post.assert_called_with(
320+
url=f"{self.host}/api/samples/v3/search/",
321+
verify=True,
322+
proxies=None,
323+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
324+
params=None,
325+
json=post_json,
326+
data=None,
327+
files=None
328+
)
329+
330+
def test_list_containers(self, requests_mock):
331+
self.a1000.list_containers_for_hashes([SHA1, SHA1])
332+
333+
data = {"hash_values": [SHA1, SHA1]}
334+
335+
requests_mock.post.assert_called_with(
336+
url=f"{self.host}/api/samples/containers/",
337+
verify=True,
338+
proxies=None,
339+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
340+
params=None,
341+
json=None,
342+
data=data,
343+
files=None
344+
)
345+
346+
def test_network_report(self, requests_mock):
347+
domain = "some.test.domain"
348+
349+
self.a1000.network_domain_report(domain)
350+
351+
expected_url = f"{self.host}/api/network-threat-intel/domain/{domain}/"
352+
353+
requests_mock.get.assert_called_with(
354+
url=expected_url,
355+
verify=True,
356+
proxies=None,
357+
headers={"User-Agent": DEFAULT_USER_AGENT, "Authorization": f"Token {self.token}"},
358+
params=None
359+
)

0 commit comments

Comments
 (0)