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.
Files changed (37) hide show
  1. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/PKG-INFO +1 -1
  2. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/pyproject.toml +1 -1
  3. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/reporter.py +9 -1
  4. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/downloader/__init__.py +14 -2
  5. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/extractors.py +9 -11
  6. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/scanner/advanced.py +45 -24
  7. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/.gitignore +0 -0
  8. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/LICENSE +0 -0
  9. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/NOTICE +0 -0
  10. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/README.md +0 -0
  11. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/__main__.py +0 -0
  12. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/__init__.py +0 -0
  13. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/downloader.py +0 -0
  14. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/images.py +0 -0
  15. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/rules/__init__.py +0 -0
  16. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/scanner/__init__.py +0 -0
  17. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/cli/unpack.py +0 -0
  18. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/console.py +0 -0
  19. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/internal/__init__.py +0 -0
  20. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/internal/config.py +0 -0
  21. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/internal/helpers.py +0 -0
  22. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/models/__init__.py +0 -0
  23. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/models/detections.py +0 -0
  24. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/models/sandbox_arguments.py +0 -0
  25. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/__init__.py +0 -0
  26. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/__init__.py +0 -0
  27. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/abc.py +0 -0
  28. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/docker.py +0 -0
  29. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/compiler/ssh.py +0 -0
  30. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/merge_dll_hooks.py +0 -0
  31. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/scanner/__init__.py +0 -0
  32. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/scanner/rescan.py +0 -0
  33. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/__init__.py +0 -0
  34. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/__init__.py +0 -0
  35. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/abc.py +0 -0
  36. {sandbox_cli-0.2.38 → sandbox_cli-0.2.40}/sandbox_cli/utils/unpack/plugins/correlation.py +0 -0
  37. {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.38
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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "sandbox-cli"
3
- version = "0.2.38"
3
+ version = "0.2.40"
4
4
  description = "Command line tool for interaction with sandboxes"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -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
- downloaded_data = await sandbox.get_file(uri)
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 sandbox_result.details.sandbox.image and (image_id := sandbox_result.details.sandbox.image.image_id) and out_dir.parts[-1] != image_id:
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) -> set[str]:
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
- if detect.name.split(".")[-2] == "Generic":
14
- generics.add(detect.name)
15
- else:
16
- non_generics.add(detect.name)
17
-
18
- return non_generics if len(non_generics) != 0 else generics
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 Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
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
- await download(
307
- long_report,
308
- sandbox,
309
- out_dir,
310
- all=all,
311
- artifacts=artifacts,
312
- crashdumps=crashdumps,
313
- debug=debug,
314
- decompress=decompress,
315
- files=download_files,
316
- logs=True, # by default download logs
317
- procdumps=procdumps,
318
- progress=progress,
319
- video=True, # by default download video
320
- idx=idx,
321
- image=formatted_image,
322
- link=formatted_link,
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