sandbox-cli 0.2.37__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.37 → sandbox_cli-0.2.39}/PKG-INFO +1 -1
  2. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/pyproject.toml +9 -8
  3. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/cli/__init__.py +2 -0
  4. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/cli/downloader.py +5 -6
  5. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/cli/reporter.py +9 -1
  6. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/cli/rules/__init__.py +2 -2
  7. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/downloader/__init__.py +14 -3
  8. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/extractors.py +9 -11
  9. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/scanner/advanced.py +59 -26
  10. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/scanner/rescan.py +8 -2
  11. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/__init__.py +10 -3
  12. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/plugins/correlation.py +0 -2
  13. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/plugins/sort_by_plugins.py +0 -2
  14. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/.gitignore +0 -0
  15. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/LICENSE +0 -0
  16. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/NOTICE +0 -0
  17. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/README.md +0 -0
  18. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/__main__.py +0 -0
  19. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/cli/images.py +0 -0
  20. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/cli/scanner/__init__.py +0 -0
  21. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/cli/unpack.py +0 -0
  22. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/console.py +0 -0
  23. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/internal/__init__.py +0 -0
  24. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/internal/config.py +0 -0
  25. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/internal/helpers.py +0 -0
  26. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/models/__init__.py +0 -0
  27. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/models/detections.py +0 -0
  28. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/models/sandbox_arguments.py +0 -0
  29. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/__init__.py +0 -0
  30. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/__init__.py +0 -0
  31. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/abc.py +0 -0
  32. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/docker.py +0 -0
  33. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/compiler/ssh.py +0 -0
  34. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/merge_dll_hooks.py +0 -0
  35. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/scanner/__init__.py +0 -0
  36. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/plugins/__init__.py +0 -0
  37. {sandbox_cli-0.2.37 → sandbox_cli-0.2.39}/sandbox_cli/utils/unpack/plugins/abc.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sandbox-cli
3
- Version: 0.2.37
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.37"
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"
@@ -87,13 +87,14 @@ ignore_missing_imports = true
87
87
 
88
88
  [tool.ruff]
89
89
  lint.select = [
90
- "E", # pycodestyle errors
91
- "W", # pycodestyle warnings
92
- "F", # pyflakes
93
- "I", # isort
94
- "B", # flake8-bugbear
95
- "C4", # flake8-comprehensions
96
- "UP", # pyupgrade
90
+ "E", # pycodestyle errors
91
+ "W", # pycodestyle warnings
92
+ "F", # pyflakes
93
+ "I", # isort
94
+ "B", # flake8-bugbear
95
+ "C4", # flake8-comprehensions
96
+ "UP", # pyupgrade
97
+ "F401", # unused-imports
97
98
  ]
98
99
  lint.ignore = [
99
100
  "E501", # line too long, handled by black
@@ -2,6 +2,7 @@
2
2
  Register all commands here
3
3
  """
4
4
 
5
+ from colorama import init
5
6
  from cyclopts import App
6
7
 
7
8
  from sandbox_cli.cli.reporter import generate_report, open_browser
@@ -9,6 +10,7 @@ from sandbox_cli.cli.unpack import unpack_logs
9
10
  from sandbox_cli.console import console
10
11
  from sandbox_cli.internal.config import configpath, settings
11
12
 
13
+ init() # colorama stuff for working on windows
12
14
 
13
15
  def get_version() -> str:
14
16
  import importlib.metadata
@@ -11,12 +11,10 @@ from uuid import UUID
11
11
  import aiofiles
12
12
  import aiohttp
13
13
  import aiohttp.client_exceptions
14
- import orjson
15
- from colorama import init
16
14
  from cyclopts import Parameter
17
15
  from ptsandbox import Sandbox
18
16
  from ptsandbox.models import Artifact, SandboxBaseTaskResponse
19
- from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
17
+ from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
20
18
 
21
19
  from sandbox_cli.console import console
22
20
  from sandbox_cli.internal.config import settings
@@ -51,12 +49,12 @@ def get_key_and_task(key: str, task: str) -> tuple[Sandbox, UUID] | tuple[None,
51
49
 
52
50
  async def download_command(
53
51
  tasks_id: Annotated[
54
- list[str],
52
+ list[str] | None,
55
53
  Parameter(
56
54
  help="Links to tasks or task ids",
57
55
  negative="",
58
56
  ),
59
- ] = [],
57
+ ] = None,
60
58
  /,
61
59
  *,
62
60
  key: Annotated[
@@ -184,7 +182,8 @@ async def download_command(
184
182
  Download any artifact from the sandbox.
185
183
  """
186
184
 
187
- init() # colorama stuff for working on windows
185
+ if tasks_id is None:
186
+ tasks_id = []
188
187
 
189
188
  progress = Progress(
190
189
  SpinnerColumn(),
@@ -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)),
@@ -15,7 +15,6 @@ rules = App(
15
15
 
16
16
  @rules.command(name="compile")
17
17
  async def compile_rules(
18
- *,
19
18
  rules: Annotated[
20
19
  Path,
21
20
  Parameter(
@@ -24,6 +23,7 @@ async def compile_rules(
24
23
  validator=validators.Path(exists=True),
25
24
  ),
26
25
  ],
26
+ /,
27
27
  out: Annotated[
28
28
  Path,
29
29
  Parameter(
@@ -57,7 +57,6 @@ async def compile_rules(
57
57
 
58
58
  @rules.command(name="test")
59
59
  async def test_rules(
60
- *,
61
60
  rules: Annotated[
62
61
  Path,
63
62
  Parameter(
@@ -66,6 +65,7 @@ async def test_rules(
66
65
  validator=validators.Path(exists=True),
67
66
  ),
68
67
  ],
68
+ /,
69
69
  is_local: Annotated[
70
70
  bool,
71
71
  Parameter(
@@ -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 (
@@ -18,7 +19,6 @@ from rich.markup import escape
18
19
  from rich.progress import Progress, TaskID
19
20
 
20
21
  from sandbox_cli.console import console
21
- from sandbox_cli.internal.config import settings
22
22
 
23
23
  semaphore = asyncio.Semaphore(value=16)
24
24
 
@@ -66,13 +66,20 @@ async def _save_artifact(
66
66
  else:
67
67
  task_id = progress.add_task(rf"\[[green1]{scan_id}[/]] {escape(path.name)}")
68
68
  try:
69
- 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)
70
71
  except SandboxFileNotFoundException:
71
72
  console.warning(f"File {path.name} not found in storage: {uri=} {scan_id=}")
72
73
  if progress:
73
74
  progress.stop_task(task_id)
74
75
  progress.update(task_id=task_id, visible=False)
75
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
76
83
 
77
84
  if decompress:
78
85
  try:
@@ -139,7 +146,11 @@ async def download(
139
146
  if sandbox_result.details.sandbox is None:
140
147
  continue
141
148
 
142
- 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
+ ):
143
154
  output = out_dir / image_id
144
155
  else:
145
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]:
@@ -8,9 +8,21 @@ import aiofiles
8
8
  import aiohttp
9
9
  import aiohttp.client_exceptions
10
10
  from ptsandbox import Sandbox, SandboxKey
11
- from ptsandbox.models import SandboxOptionsAdvanced, SandboxUploadException, VNCMode
11
+ from ptsandbox.models import (
12
+ SandboxOptionsAdvanced,
13
+ SandboxUploadException,
14
+ SandboxWaitTimeoutException,
15
+ VNCMode,
16
+ )
12
17
  from rich.markup import escape
13
- 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
+ )
14
26
 
15
27
  from sandbox_cli.console import console
16
28
  from sandbox_cli.internal.config import VMImage, settings
@@ -64,6 +76,13 @@ async def _get_compiled_rules(progress: Progress, rules_dir: Path | None, is_loc
64
76
  return result
65
77
 
66
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
+
67
86
  async def _prepare_sandbox_new_scan(
68
87
  progress: Progress,
69
88
  scan_images: set[VMImage | str],
@@ -133,7 +152,7 @@ async def _prepare_sandbox_new_scan(
133
152
  )
134
153
 
135
154
  # some enabled options by default
136
- # All debug options available in library
155
+ # all debug options available in library
137
156
  sandbox_options.debug_options["save_debug_files"] = True
138
157
  sandbox_options.debug_options["extract_crashdumps"] = True
139
158
 
@@ -241,6 +260,7 @@ async def scan_internal_advanced(
241
260
 
242
261
  async with sandbox_sem:
243
262
  task_id = progress.add_task(description="Creating task", idx=idx, image=formatted_image, url="...")
263
+ task = progress.tasks[task_id]
244
264
 
245
265
  wait_time = sandbox_options.analysis_duration * 4 + (300 if sandbox_options.analysis_duration < 80 else 120)
246
266
 
@@ -261,7 +281,7 @@ async def scan_internal_advanced(
261
281
  progress.remove_task(task_id)
262
282
  return
263
283
  except aiohttp.client_exceptions.ClientResponseError as e:
264
- 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)}")
265
285
  progress.remove_task(task_id)
266
286
  return
267
287
 
@@ -276,8 +296,13 @@ async def scan_internal_advanced(
276
296
  description=f"Waiting [yellow]{file_path.name}[/]",
277
297
  url=formatted_link,
278
298
  )
279
- if not (awaited_report := await sandbox.wait_for_report(scan_result, wait_time)):
280
- console.error(f"{final_output} scan failed")
299
+ try:
300
+ if not (awaited_report := await sandbox.wait_for_report(scan_result, wait_time)):
301
+ console.error(f"{final_output} • scan failed • {get_elapsed_time(task)}")
302
+ progress.remove_task(task_id)
303
+ return
304
+ except SandboxWaitTimeoutException:
305
+ console.error(f"{final_output} • got timeout while waiting • {get_elapsed_time(task)}")
281
306
  progress.remove_task(task_id)
282
307
  return
283
308
 
@@ -287,32 +312,37 @@ async def scan_internal_advanced(
287
312
  (out_dir / settings.report_name).write_text(scan_result.model_dump_json(indent=4), encoding="utf-8")
288
313
 
289
314
  if not (long_report := scan_result.get_long_report()):
290
- console.error(f"{final_output} • full report not available")
315
+ console.error(f"{final_output} • full report not available • {get_elapsed_time(task)}")
291
316
  progress.remove_task(task_id)
292
317
  return
293
318
 
294
319
  progress.update(task_id=task_id, description="Downloading results...")
295
320
 
296
- await download(
297
- long_report,
298
- sandbox,
299
- out_dir,
300
- all=all,
301
- artifacts=artifacts,
302
- crashdumps=crashdumps,
303
- debug=debug,
304
- decompress=decompress,
305
- files=download_files,
306
- logs=True, # by default download logs
307
- procdumps=procdumps,
308
- progress=progress,
309
- video=True, # by default download video
310
- idx=idx,
311
- image=formatted_image,
312
- link=formatted_link,
313
- )
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
314
344
 
315
- console.done(final_output)
345
+ console.done(f"{final_output} • {get_elapsed_time(task)}")
316
346
 
317
347
  progress.remove_task(task_id)
318
348
 
@@ -378,3 +408,6 @@ async def scan_internal_advanced(
378
408
 
379
409
  await asyncio.gather(*tasks)
380
410
  await sandbox.api.session.close()
411
+
412
+ # clear last line
413
+ sys.stdout.write("\033[F\033[K")
@@ -13,6 +13,7 @@ from ptsandbox.models import (
13
13
  SandboxBaseScanTaskRequest,
14
14
  SandboxKey,
15
15
  SandboxUploadException,
16
+ SandboxWaitTimeoutException,
16
17
  )
17
18
  from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
18
19
 
@@ -157,8 +158,13 @@ async def rescan_internal(
157
158
  description=f"Waiting for full report for [yellow]{trace.name}[/]",
158
159
  url=formatted_link,
159
160
  )
160
- if not (awaited_report := await sandbox.wait_for_report(rescan_result, timeout)):
161
- console.error(f"Rescan failed for [yellow]{trace.name}[/] • {formatted_link} • {rescan_result}")
161
+ try:
162
+ if not (awaited_report := await sandbox.wait_for_report(rescan_result, timeout)):
163
+ console.error(f"Rescan failed for [yellow]{trace.name}[/] • {formatted_link} • {rescan_result}")
164
+ progress.remove_task(task_id)
165
+ return
166
+ except SandboxWaitTimeoutException:
167
+ console.error(f"{final_output} • got timeout while waiting")
162
168
  progress.remove_task(task_id)
163
169
  return
164
170
 
@@ -80,17 +80,24 @@ class Unpack:
80
80
  def run(self) -> None:
81
81
  if Path(self.trace / "drakvuf-trace.log.gz").exists():
82
82
  self.logs["drakvuf-trace"] = Path(self.trace / "drakvuf-trace.log.gz")
83
- if Path(self.trace / "drakvuf-trace.log.zst").exists():
83
+ elif Path(self.trace / "drakvuf-trace.log.zst").exists():
84
84
  self.logs["drakvuf-trace"] = Path(self.trace / "drakvuf-trace.log.zst")
85
85
 
86
86
  self._extract_logs()
87
87
  self._create_dirs()
88
88
  self._move_files()
89
89
 
90
- # Run plugins
90
+ # run plugins
91
91
  for plugin in self.plugins:
92
92
  plugin.run()
93
93
 
94
- # Remove files
94
+ # remove files
95
95
  for log in self.logs.values():
96
+ # drakvuf trace not found
97
+ if log == Path(""):
98
+ continue
99
+
100
+ if not log.exists():
101
+ continue
102
+
96
103
  log.unlink(missing_ok=True)
@@ -3,7 +3,6 @@ from pathlib import Path
3
3
 
4
4
  import orjson
5
5
 
6
- from sandbox_cli.console import console
7
6
  from sandbox_cli.models.detections import DetectionType
8
7
  from sandbox_cli.utils.unpack.plugins.abc import BasePlugin
9
8
 
@@ -13,7 +12,6 @@ class CorrelatedRules(BasePlugin):
13
12
  base_path = self.trace / "correlated"
14
13
  file = Path(base_path / "events-correlated.log")
15
14
  if not file.exists():
16
- console.warning(f"{file} not exist")
17
15
  return
18
16
 
19
17
  with open(file, errors="ignore", encoding="utf-8") as fd:
@@ -3,7 +3,6 @@ from pathlib import Path
3
3
 
4
4
  import orjson
5
5
 
6
- from sandbox_cli.console import console
7
6
  from sandbox_cli.utils.unpack.plugins.abc import BasePlugin
8
7
 
9
8
 
@@ -12,7 +11,6 @@ class SortByPlugins(BasePlugin):
12
11
  base_path = self.trace / "normalized"
13
12
  file = Path(base_path / "events-normalized.log")
14
13
  if not file.exists():
15
- console.warning(f"{file} not exist")
16
14
  return
17
15
 
18
16
  with open(file, errors="ignore", encoding="utf-8") as fd:
File without changes
File without changes
File without changes
File without changes