sandbox-cli 0.2.38__tar.gz → 0.2.40__tar.gz
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.
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/PKG-INFO +1 -1
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/pyproject.toml +1 -1
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/reporter.py +9 -1
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/downloader/__init__.py +14 -2
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/extractors.py +9 -11
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/scanner/advanced.py +45 -24
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/.gitignore +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/LICENSE +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/NOTICE +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/README.md +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/__main__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/downloader.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/images.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/rules/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/scanner/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/unpack.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/console.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/internal/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/internal/config.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/internal/helpers.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/models/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/models/detections.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/models/sandbox_arguments.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/abc.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/docker.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/ssh.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/merge_dll_hooks.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/scanner/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/scanner/rescan.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/__init__.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/abc.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/correlation.py +0 -0
- {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/sort_by_plugins.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sandbox-cli
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.40
|
|
4
4
|
Summary: Command line tool for interaction with sandboxes
|
|
5
5
|
Project-URL: Homepage, https://github.com/Security-Experts-Community/sandbox-cli
|
|
6
6
|
Project-URL: Documentation, https://security-experts-community.github.io/sandbox-cli
|
|
@@ -53,6 +53,14 @@ def generate_report(
|
|
|
53
53
|
negative="",
|
|
54
54
|
),
|
|
55
55
|
] = False,
|
|
56
|
+
suspicious: Annotated[
|
|
57
|
+
bool,
|
|
58
|
+
Parameter(
|
|
59
|
+
name=["--supicious", "-s"],
|
|
60
|
+
help="Include suspicious detects",
|
|
61
|
+
negative="",
|
|
62
|
+
),
|
|
63
|
+
] = False,
|
|
56
64
|
) -> None:
|
|
57
65
|
"""
|
|
58
66
|
Generate short report from sandbox scans.
|
|
@@ -118,7 +126,7 @@ def generate_report(
|
|
|
118
126
|
{
|
|
119
127
|
"sample": report.artifacts[0].file_info.file_path, # type: ignore
|
|
120
128
|
"image": image,
|
|
121
|
-
"verdict": delimeter.join(extract_verdict_from_trace(corr_trace)),
|
|
129
|
+
"verdict": delimeter.join(extract_verdict_from_trace(corr_trace, suspicious)),
|
|
122
130
|
"static": delimeter.join(extract_static(report)),
|
|
123
131
|
"memory": delimeter.join(extract_memory(report)),
|
|
124
132
|
"network": delimeter.join(extract_network_from_trace(corr_trace)),
|
|
@@ -6,6 +6,7 @@ from typing import Any
|
|
|
6
6
|
from uuid import UUID
|
|
7
7
|
|
|
8
8
|
import aiofiles
|
|
9
|
+
import aiohttp
|
|
9
10
|
import zstandard
|
|
10
11
|
from ptsandbox import Sandbox
|
|
11
12
|
from ptsandbox.models import (
|
|
@@ -65,13 +66,20 @@ async def _save_artifact(
|
|
|
65
66
|
else:
|
|
66
67
|
task_id = progress.add_task(rf"\[[green1]{scan_id}[/]] {escape(path.name)}")
|
|
67
68
|
try:
|
|
68
|
-
|
|
69
|
+
# a 5-minute timeout for downloading large files
|
|
70
|
+
downloaded_data = await sandbox.get_file(uri, read_timeout=300)
|
|
69
71
|
except SandboxFileNotFoundException:
|
|
70
72
|
console.warning(f"File {path.name} not found in storage: {uri=} {scan_id=}")
|
|
71
73
|
if progress:
|
|
72
74
|
progress.stop_task(task_id)
|
|
73
75
|
progress.update(task_id=task_id, visible=False)
|
|
74
76
|
return
|
|
77
|
+
except aiohttp.SocketTimeoutError as e:
|
|
78
|
+
if progress:
|
|
79
|
+
progress.stop_task(task_id)
|
|
80
|
+
progress.update(task_id=task_id, visible=False)
|
|
81
|
+
|
|
82
|
+
raise e
|
|
75
83
|
|
|
76
84
|
if decompress:
|
|
77
85
|
try:
|
|
@@ -138,7 +146,11 @@ async def download(
|
|
|
138
146
|
if sandbox_result.details.sandbox is None:
|
|
139
147
|
continue
|
|
140
148
|
|
|
141
|
-
if
|
|
149
|
+
if (
|
|
150
|
+
sandbox_result.details.sandbox.image
|
|
151
|
+
and (image_id := sandbox_result.details.sandbox.image.image_id)
|
|
152
|
+
and out_dir.parts[-1] != image_id
|
|
153
|
+
):
|
|
142
154
|
output = out_dir / image_id
|
|
143
155
|
else:
|
|
144
156
|
output = out_dir
|
|
@@ -4,18 +4,16 @@ from ptsandbox.models import ArtifactType, EngineSubsystem, SandboxBaseTaskRespo
|
|
|
4
4
|
from sandbox_cli.models.detections import Detections
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
def extract_verdict_from_trace(trace: bytes) ->
|
|
8
|
-
non_generics: set[str] = set()
|
|
9
|
-
generics: set[str] = set()
|
|
10
|
-
|
|
7
|
+
def extract_verdict_from_trace(trace: bytes, suspicious: bool = False) -> list[str]:
|
|
11
8
|
d = Detections(trace)
|
|
12
|
-
for detect in d.malware
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
9
|
+
malware_detects: list[str] = list({detect.name for detect in d.malware})
|
|
10
|
+
suspicious_detects: set[str] = set()
|
|
11
|
+
if suspicious:
|
|
12
|
+
for detect in d.suspicious:
|
|
13
|
+
suspicious_detects.add(detect.name)
|
|
14
|
+
|
|
15
|
+
# malware detects always on top
|
|
16
|
+
return malware_detects + sorted(suspicious_detects)
|
|
19
17
|
|
|
20
18
|
|
|
21
19
|
def extract_network_from_trace(trace: bytes) -> set[str]:
|
|
@@ -15,7 +15,14 @@ from ptsandbox.models import (
|
|
|
15
15
|
VNCMode,
|
|
16
16
|
)
|
|
17
17
|
from rich.markup import escape
|
|
18
|
-
from rich.progress import
|
|
18
|
+
from rich.progress import (
|
|
19
|
+
Progress,
|
|
20
|
+
SpinnerColumn,
|
|
21
|
+
Task,
|
|
22
|
+
TaskID,
|
|
23
|
+
TextColumn,
|
|
24
|
+
TimeElapsedColumn,
|
|
25
|
+
)
|
|
19
26
|
|
|
20
27
|
from sandbox_cli.console import console
|
|
21
28
|
from sandbox_cli.internal.config import VMImage, settings
|
|
@@ -69,6 +76,13 @@ async def _get_compiled_rules(progress: Progress, rules_dir: Path | None, is_loc
|
|
|
69
76
|
return result
|
|
70
77
|
|
|
71
78
|
|
|
79
|
+
def get_elapsed_time(task: Task) -> str:
|
|
80
|
+
hours = int(task.elapsed) // 3600
|
|
81
|
+
minutes = (int(task.elapsed) % 3600) // 60
|
|
82
|
+
seconds = int(task.elapsed) % 60
|
|
83
|
+
return f"[yellow]{hours}:{minutes:02d}:{seconds:02d}[/]"
|
|
84
|
+
|
|
85
|
+
|
|
72
86
|
async def _prepare_sandbox_new_scan(
|
|
73
87
|
progress: Progress,
|
|
74
88
|
scan_images: set[VMImage | str],
|
|
@@ -246,6 +260,8 @@ async def scan_internal_advanced(
|
|
|
246
260
|
|
|
247
261
|
async with sandbox_sem:
|
|
248
262
|
task_id = progress.add_task(description="Creating task", idx=idx, image=formatted_image, url="...")
|
|
263
|
+
# because progress.tasks is .values() from dict, not an actual list
|
|
264
|
+
task = next(t for t in progress.tasks if t.id == task_id)
|
|
249
265
|
|
|
250
266
|
wait_time = sandbox_options.analysis_duration * 4 + (300 if sandbox_options.analysis_duration < 80 else 120)
|
|
251
267
|
|
|
@@ -266,7 +282,7 @@ async def scan_internal_advanced(
|
|
|
266
282
|
progress.remove_task(task_id)
|
|
267
283
|
return
|
|
268
284
|
except aiohttp.client_exceptions.ClientResponseError as e:
|
|
269
|
-
console.error(f"{image_string} • [yellow]{file_path.name}[/] • {e}")
|
|
285
|
+
console.error(f"{image_string} • [yellow]{file_path.name}[/] • {e} • {get_elapsed_time(task)}")
|
|
270
286
|
progress.remove_task(task_id)
|
|
271
287
|
return
|
|
272
288
|
|
|
@@ -283,11 +299,11 @@ async def scan_internal_advanced(
|
|
|
283
299
|
)
|
|
284
300
|
try:
|
|
285
301
|
if not (awaited_report := await sandbox.wait_for_report(scan_result, wait_time)):
|
|
286
|
-
console.error(f"{final_output} • scan failed")
|
|
302
|
+
console.error(f"{final_output} • scan failed • {get_elapsed_time(task)}")
|
|
287
303
|
progress.remove_task(task_id)
|
|
288
304
|
return
|
|
289
305
|
except SandboxWaitTimeoutException:
|
|
290
|
-
console.error(f"{final_output} • got timeout while waiting")
|
|
306
|
+
console.error(f"{final_output} • got timeout while waiting • {get_elapsed_time(task)}")
|
|
291
307
|
progress.remove_task(task_id)
|
|
292
308
|
return
|
|
293
309
|
|
|
@@ -297,32 +313,37 @@ async def scan_internal_advanced(
|
|
|
297
313
|
(out_dir / settings.report_name).write_text(scan_result.model_dump_json(indent=4), encoding="utf-8")
|
|
298
314
|
|
|
299
315
|
if not (long_report := scan_result.get_long_report()):
|
|
300
|
-
console.error(f"{final_output} • full report not available")
|
|
316
|
+
console.error(f"{final_output} • full report not available • {get_elapsed_time(task)}")
|
|
301
317
|
progress.remove_task(task_id)
|
|
302
318
|
return
|
|
303
319
|
|
|
304
320
|
progress.update(task_id=task_id, description="Downloading results...")
|
|
305
321
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
try:
|
|
323
|
+
await download(
|
|
324
|
+
long_report,
|
|
325
|
+
sandbox,
|
|
326
|
+
out_dir,
|
|
327
|
+
all=all,
|
|
328
|
+
artifacts=artifacts,
|
|
329
|
+
crashdumps=crashdumps,
|
|
330
|
+
debug=debug,
|
|
331
|
+
decompress=decompress,
|
|
332
|
+
files=download_files,
|
|
333
|
+
logs=True, # by default download logs
|
|
334
|
+
procdumps=procdumps,
|
|
335
|
+
progress=progress,
|
|
336
|
+
video=True, # by default download video
|
|
337
|
+
idx=idx,
|
|
338
|
+
image=formatted_image,
|
|
339
|
+
link=formatted_link,
|
|
340
|
+
)
|
|
341
|
+
except aiohttp.SocketTimeoutError:
|
|
342
|
+
console.error(f"{final_output} • got timeout while downloading results • {get_elapsed_time(task)}")
|
|
343
|
+
progress.remove_task(task_id)
|
|
344
|
+
return
|
|
324
345
|
|
|
325
|
-
console.done(final_output)
|
|
346
|
+
console.done(f"{final_output} • {get_elapsed_time(task)}")
|
|
326
347
|
|
|
327
348
|
progress.remove_task(task_id)
|
|
328
349
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/sort_by_plugins.py
RENAMED
|
File without changes
|