sandbox-cli 0.2.38__tar.gz → 0.2.39__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.39}/PKG-INFO +1 -1
  2. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/pyproject.toml +1 -1
  3. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/cli/reporter.py +9 -1
  4. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/downloader/__init__.py +14 -2
  5. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/extractors.py +9 -11
  6. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/scanner/advanced.py +44 -24
  7. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/.gitignore +0 -0
  8. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/LICENSE +0 -0
  9. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/NOTICE +0 -0
  10. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/README.md +0 -0
  11. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/__main__.py +0 -0
  12. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/cli/__init__.py +0 -0
  13. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/cli/downloader.py +0 -0
  14. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/cli/images.py +0 -0
  15. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/cli/rules/__init__.py +0 -0
  16. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/cli/scanner/__init__.py +0 -0
  17. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/cli/unpack.py +0 -0
  18. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/console.py +0 -0
  19. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/internal/__init__.py +0 -0
  20. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/internal/config.py +0 -0
  21. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/internal/helpers.py +0 -0
  22. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/models/__init__.py +0 -0
  23. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/models/detections.py +0 -0
  24. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/models/sandbox_arguments.py +0 -0
  25. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/__init__.py +0 -0
  26. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/__init__.py +0 -0
  27. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/abc.py +0 -0
  28. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/docker.py +0 -0
  29. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/ssh.py +0 -0
  30. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/merge_dll_hooks.py +0 -0
  31. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/scanner/__init__.py +0 -0
  32. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/scanner/rescan.py +0 -0
  33. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/__init__.py +0 -0
  34. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/plugins/__init__.py +0 -0
  35. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/plugins/abc.py +0 -0
  36. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/plugins/correlation.py +0 -0
  37. {sandbox_cli-0.2.38 → sandbox_cli-0.2.39}/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.39
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.39"
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,7 @@ 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
+ task = progress.tasks[task_id]
249
264
 
250
265
  wait_time = sandbox_options.analysis_duration * 4 + (300 if sandbox_options.analysis_duration < 80 else 120)
251
266
 
@@ -266,7 +281,7 @@ async def scan_internal_advanced(
266
281
  progress.remove_task(task_id)
267
282
  return
268
283
  except aiohttp.client_exceptions.ClientResponseError as e:
269
- console.error(f"{image_string} • [yellow]{file_path.name}[/] • {e}")
284
+ console.error(f"{image_string} • [yellow]{file_path.name}[/] • {e} • {get_elapsed_time(task)}")
270
285
  progress.remove_task(task_id)
271
286
  return
272
287
 
@@ -283,11 +298,11 @@ async def scan_internal_advanced(
283
298
  )
284
299
  try:
285
300
  if not (awaited_report := await sandbox.wait_for_report(scan_result, wait_time)):
286
- console.error(f"{final_output} • scan failed")
301
+ console.error(f"{final_output} • scan failed • {get_elapsed_time(task)}")
287
302
  progress.remove_task(task_id)
288
303
  return
289
304
  except SandboxWaitTimeoutException:
290
- console.error(f"{final_output} • got timeout while waiting")
305
+ console.error(f"{final_output} • got timeout while waiting • {get_elapsed_time(task)}")
291
306
  progress.remove_task(task_id)
292
307
  return
293
308
 
@@ -297,32 +312,37 @@ async def scan_internal_advanced(
297
312
  (out_dir / settings.report_name).write_text(scan_result.model_dump_json(indent=4), encoding="utf-8")
298
313
 
299
314
  if not (long_report := scan_result.get_long_report()):
300
- console.error(f"{final_output} • full report not available")
315
+ console.error(f"{final_output} • full report not available • {get_elapsed_time(task)}")
301
316
  progress.remove_task(task_id)
302
317
  return
303
318
 
304
319
  progress.update(task_id=task_id, description="Downloading results...")
305
320
 
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
- )
321
+ try:
322
+ await download(
323
+ long_report,
324
+ sandbox,
325
+ out_dir,
326
+ all=all,
327
+ artifacts=artifacts,
328
+ crashdumps=crashdumps,
329
+ debug=debug,
330
+ decompress=decompress,
331
+ files=download_files,
332
+ logs=True, # by default download logs
333
+ procdumps=procdumps,
334
+ progress=progress,
335
+ video=True, # by default download video
336
+ idx=idx,
337
+ image=formatted_image,
338
+ link=formatted_link,
339
+ )
340
+ except aiohttp.SocketTimeoutError:
341
+ console.error(f"{final_output} • got timeout while downloading results • {get_elapsed_time(task)}")
342
+ progress.remove_task(task_id)
343
+ return
324
344
 
325
- console.done(final_output)
345
+ console.done(f"{final_output} • {get_elapsed_time(task)}")
326
346
 
327
347
  progress.remove_task(task_id)
328
348
 
File without changes
File without changes
File without changes
File without changes