sandbox-cli 0.2.26__py3-none-any.whl

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 (36) hide show
  1. sandbox_cli/__main__.py +28 -0
  2. sandbox_cli/cli/__init__.py +46 -0
  3. sandbox_cli/cli/downloader.py +308 -0
  4. sandbox_cli/cli/images.py +45 -0
  5. sandbox_cli/cli/reporter.py +146 -0
  6. sandbox_cli/cli/rules/__init__.py +83 -0
  7. sandbox_cli/cli/scanner/__init__.py +681 -0
  8. sandbox_cli/cli/unpack.py +50 -0
  9. sandbox_cli/console.py +24 -0
  10. sandbox_cli/internal/__init__.py +0 -0
  11. sandbox_cli/internal/config.py +143 -0
  12. sandbox_cli/internal/helpers.py +32 -0
  13. sandbox_cli/models/__init__.py +0 -0
  14. sandbox_cli/models/detections.py +91 -0
  15. sandbox_cli/utils/__init__.py +0 -0
  16. sandbox_cli/utils/compiler/__init__.py +60 -0
  17. sandbox_cli/utils/compiler/abc.py +51 -0
  18. sandbox_cli/utils/compiler/docker.py +136 -0
  19. sandbox_cli/utils/compiler/ssh.py +203 -0
  20. sandbox_cli/utils/downloader/__init__.py +178 -0
  21. sandbox_cli/utils/extractors.py +66 -0
  22. sandbox_cli/utils/merge_dll_hooks.py +84 -0
  23. sandbox_cli/utils/scanner/__init__.py +310 -0
  24. sandbox_cli/utils/scanner/advanced.py +360 -0
  25. sandbox_cli/utils/scanner/rescan.py +258 -0
  26. sandbox_cli/utils/unpack/__init__.py +96 -0
  27. sandbox_cli/utils/unpack/plugins/__init__.py +0 -0
  28. sandbox_cli/utils/unpack/plugins/abc.py +18 -0
  29. sandbox_cli/utils/unpack/plugins/correlation.py +52 -0
  30. sandbox_cli/utils/unpack/plugins/sort_by_plugins.py +30 -0
  31. sandbox_cli-0.2.26.dist-info/METADATA +139 -0
  32. sandbox_cli-0.2.26.dist-info/RECORD +36 -0
  33. sandbox_cli-0.2.26.dist-info/WHEEL +4 -0
  34. sandbox_cli-0.2.26.dist-info/entry_points.txt +2 -0
  35. sandbox_cli-0.2.26.dist-info/licenses/LICENSE +21 -0
  36. sandbox_cli-0.2.26.dist-info/licenses/NOTICE +322 -0
@@ -0,0 +1,310 @@
1
+ import asyncio
2
+ import sys
3
+ from collections.abc import Coroutine
4
+ from pathlib import Path
5
+ from typing import Any, overload
6
+
7
+ import aiofiles
8
+ from ptsandbox import Sandbox
9
+ from ptsandbox.models import (
10
+ SandboxBaseScanTaskRequest,
11
+ SandboxBaseTaskResponse,
12
+ SandboxKey,
13
+ SandboxOptions,
14
+ )
15
+
16
+ from sandbox_cli.console import console
17
+ from sandbox_cli.internal.config import VMImage, settings
18
+ from sandbox_cli.internal.helpers import get_key_by_name
19
+ from sandbox_cli.utils.compiler import compile_rules_internal
20
+ from sandbox_cli.utils.downloader import download
21
+ from sandbox_cli.utils.merge_dll_hooks import merge_dll_hooks
22
+ from sandbox_cli.utils.unpack import Unpack
23
+
24
+
25
+ @overload
26
+ def format_link(
27
+ report: SandboxBaseTaskResponse,
28
+ *,
29
+ sandbox: Sandbox,
30
+ key: SandboxKey | None = None,
31
+ ) -> str: ...
32
+
33
+
34
+ @overload
35
+ def format_link(
36
+ report: SandboxBaseTaskResponse,
37
+ *,
38
+ sandbox: Sandbox | None = None,
39
+ key: SandboxKey,
40
+ ) -> str: ...
41
+
42
+
43
+ def format_link(
44
+ report: SandboxBaseTaskResponse,
45
+ *,
46
+ sandbox: Sandbox | None = None,
47
+ key: SandboxKey | None = None,
48
+ ) -> str:
49
+ key = key or (sandbox.api.key if sandbox else None)
50
+
51
+ if not key:
52
+ console.error("Key not provided")
53
+ sys.exit(1)
54
+
55
+ if not (short_report := report.get_short_report()):
56
+ return "Unknown"
57
+
58
+ return f"https://{key.host}/tasks/{short_report.scan_id}"
59
+
60
+
61
+ async def _get_compiled_rules(rules_dir: Path | None, is_local: bool) -> bytes | None:
62
+ if not rules_dir:
63
+ return None
64
+
65
+ text = "Compiling rules locally" if is_local else f"Compiling rules on the remote: {settings.sandbox[0].host}"
66
+ console.info(text)
67
+
68
+ return await compile_rules_internal(rules_dir=rules_dir, is_local=is_local)
69
+
70
+
71
+ async def _prepare_scan_options(
72
+ scan_images: set[VMImage],
73
+ rules_dir: Path | None,
74
+ sandbox_key: SandboxKey,
75
+ is_local: bool,
76
+ analysis_duration: int,
77
+ syscall_hooks: Path | None,
78
+ dll_hooks_dir: Path | None,
79
+ custom_command: str | None,
80
+ ) -> tuple[Sandbox, SandboxBaseScanTaskRequest.Options, set[VMImage | str]]:
81
+ sandbox = Sandbox(key=sandbox_key)
82
+
83
+ # detect correct image
84
+ available_images: set[VMImage | str] = set()
85
+ for check_image in (await sandbox.api.get_images()).data:
86
+ if not check_image.image_id:
87
+ continue
88
+
89
+ try:
90
+ available_images.add(VMImage(check_image.image_id))
91
+ except ValueError:
92
+ # maybe it is custom image?
93
+ available_images.add(check_image.image_id)
94
+
95
+ images: set[VMImage | str] = set()
96
+ sandbox_image = VMImage.WIN10_1803_X64
97
+ for image in scan_images:
98
+ match image:
99
+ case VMImage.LINUX:
100
+ sandbox_image = VMImage.UBUNTU_JAMMY_X64
101
+ images = available_images & settings.linux_images
102
+ if len(images) == 0:
103
+ console.error("Sandbox doesn't support linux images")
104
+ sys.exit(1)
105
+
106
+ console.info(f"Scanning on: [cyan]{', '.join(images)}[/]")
107
+ case VMImage.WINDOWS:
108
+ sandbox_image = VMImage.WIN10_1803_X64
109
+ images = available_images & settings.windows_images
110
+
111
+ if len(images) == 0:
112
+ console.error("Sandbox doesn't support windows images")
113
+ sys.exit(1)
114
+
115
+ console.info(f"Scanning on: [cyan]{', '.join(images)}[/]")
116
+ case _:
117
+ if image not in available_images:
118
+ console.error(f"Sandbox doesn't support {image}.")
119
+ console.info(f"Available: [turquoise2]{', '.join(available_images)}[/]")
120
+ sys.exit(1)
121
+
122
+ images.add(image)
123
+ sandbox_image = image
124
+
125
+ sandbox_options = SandboxBaseScanTaskRequest.Options(
126
+ analysis_depth=2,
127
+ passwords_for_unpack=settings.passwords,
128
+ sandbox=SandboxOptions(
129
+ image_id=sandbox_image.value if isinstance(sandbox_image, VMImage) else sandbox_image,
130
+ analysis_duration=analysis_duration,
131
+ ),
132
+ )
133
+
134
+ # some enabled options by default
135
+ # All debug options available in library
136
+ sandbox_options.sandbox.debug_options["save_debug_files"] = True
137
+ sandbox_options.sandbox.debug_options["extract_crashdumps"] = True
138
+
139
+ # process custom options
140
+ compiled_rules = await _get_compiled_rules(rules_dir=rules_dir, is_local=is_local)
141
+
142
+ if compiled_rules:
143
+ rules_uri = (await sandbox.api.upload_file(compiled_rules)).data.file_uri
144
+ sandbox_options.sandbox.debug_options["rules_url"] = rules_uri
145
+
146
+ if syscall_hooks:
147
+ console.info(f"Uploading syscall hooks by {syscall_hooks}")
148
+ async with aiofiles.open(syscall_hooks, mode="rb") as fd:
149
+ data = await fd.read()
150
+
151
+ syscall_hooks_uri = (await sandbox.api.upload_file(data)).data.file_uri
152
+ sandbox_options.sandbox.debug_options["custom_syscall_hooks"] = syscall_hooks_uri
153
+
154
+ if dll_hooks_dir:
155
+ console.info(f"Uploading dll hooks from directory {dll_hooks_dir}")
156
+ data = merge_dll_hooks(Path(dll_hooks_dir))
157
+ dll_hooks_uri = (await sandbox.api.upload_file(data)).data.file_uri
158
+ sandbox_options.sandbox.debug_options["custom_dll_hooks"] = dll_hooks_uri
159
+
160
+ if custom_command:
161
+ console.info(f"Using custom command: {custom_command}")
162
+ sandbox_options.sandbox.custom_command = custom_command
163
+
164
+ # add here some commands if new options available
165
+
166
+ return (sandbox, sandbox_options, images)
167
+
168
+
169
+ async def scan_internal(
170
+ *, # no not keyword args
171
+ files: list[Path],
172
+ scan_images: set[VMImage],
173
+ rules_dir: Path | None,
174
+ out_dir: Path,
175
+ key_name: str,
176
+ is_local: bool,
177
+ analysis_duration: int,
178
+ syscall_hooks: Path | None,
179
+ dll_hooks_dir: Path | None,
180
+ custom_command: str | None,
181
+ fake_name: str | None,
182
+ unpack: bool,
183
+ upload_timeout: int,
184
+ all: bool,
185
+ debug: bool,
186
+ artifacts: bool,
187
+ download_files: bool,
188
+ crashdumps: bool,
189
+ procdumps: bool,
190
+ decompress: bool,
191
+ ) -> None:
192
+ key = get_key_by_name(key_name)
193
+ sandbox_sem = asyncio.Semaphore(value=key.max_workers)
194
+
195
+ async def process_file(
196
+ sandbox_options: SandboxBaseScanTaskRequest.Options,
197
+ file_path: Path,
198
+ out_dir: Path,
199
+ idx: str,
200
+ ) -> None:
201
+ idx = f"[cyan]{idx}[/]" # make fancy
202
+ async with sandbox_sem:
203
+ console.info(f"{idx} Scanning [yellow]{file_path.name}[/]. Output: {out_dir}")
204
+ wait_time = sandbox_options.sandbox.analysis_duration * 4 + (
205
+ 300 if sandbox_options.sandbox.analysis_duration < 80 else 120
206
+ )
207
+
208
+ try:
209
+ scan_result = await sandbox.create_scan(
210
+ file_path,
211
+ file_name=fake_name or file_path.name,
212
+ options=sandbox_options,
213
+ rules=None, # we handle rules in sb_options, not inside library
214
+ read_timeout=wait_time,
215
+ upload_timeout=upload_timeout,
216
+ async_result=True,
217
+ )
218
+ except TimeoutError:
219
+ console.error(f"{idx} Timeout for {file_path.name}")
220
+ return
221
+
222
+ console.info(
223
+ rf"{idx} [magenta]\[{sandbox_options.sandbox.image_id}][/magenta] Waiting [yellow]{file_path.name}[/]: {format_link(scan_result, key=key)}"
224
+ )
225
+ awaited_report = await sandbox.wait_for_report(scan_result, wait_time)
226
+ if not awaited_report:
227
+ console.error(f"{idx} Scan [yellow]{file_path.name}[/] failed: {scan_result=}")
228
+ return
229
+ scan_result = awaited_report
230
+
231
+ # write report.json
232
+ (out_dir / settings.report_name).write_text(scan_result.model_dump_json(indent=4), encoding="utf-8")
233
+
234
+ long_report = scan_result.get_long_report()
235
+ if not long_report:
236
+ console.error("Can't get full report")
237
+ return
238
+
239
+ await download(
240
+ long_report,
241
+ sandbox,
242
+ out_dir,
243
+ all=all,
244
+ debug=debug,
245
+ artifacts=artifacts,
246
+ files=download_files,
247
+ crashdumps=crashdumps,
248
+ procdumps=procdumps,
249
+ video=True,
250
+ logs=True,
251
+ decompress=decompress,
252
+ )
253
+ console.info(
254
+ rf"\[[magenta]{sandbox_options.sandbox.image_id}[/magenta]] Scan [yellow]{file_path.name}[/] completed. {format_link(scan_result, key=key)}"
255
+ )
256
+
257
+ if unpack:
258
+ Unpack(out_dir).run()
259
+
260
+ async def wrapper(
261
+ sandbox_options: SandboxBaseScanTaskRequest.Options,
262
+ file_path: Path,
263
+ out_dir: Path,
264
+ idx: str,
265
+ ) -> None:
266
+ # try:
267
+ await process_file(sandbox_options, file_path, out_dir, idx)
268
+ # except Exception as ex:
269
+ # console.log(f"[cyan]{idx}[/] {file_path} Error: {ex!r}")
270
+
271
+ console.info(f"Using key: name={key.name} max_workers={key.max_workers}")
272
+
273
+ tasks: list[Coroutine[Any, Any, None]] = []
274
+ sandbox, sandbox_options, images = await _prepare_scan_options(
275
+ scan_images,
276
+ rules_dir,
277
+ key,
278
+ is_local,
279
+ analysis_duration,
280
+ syscall_hooks,
281
+ dll_hooks_dir,
282
+ custom_command,
283
+ )
284
+ for i, image_id in enumerate(images):
285
+ options = sandbox_options.model_copy(deep=True)
286
+ options.sandbox.image_id = image_id
287
+
288
+ if len(files) == 1:
289
+ local_out_dir = out_dir / f"{image_id}"
290
+ local_out_dir.mkdir(parents=True, exist_ok=True)
291
+ tasks.append(wrapper(options, files[0], local_out_dir, f"{i + 1}/{len(images)}"))
292
+ else:
293
+ for j, file in enumerate(files):
294
+ local_out_dir = out_dir / f"{file.stem}" / f"{image_id}"
295
+ local_out_dir.mkdir(parents=True, exist_ok=True)
296
+ idx = f"{(i + 1) * (j + 1)}/{len(files) * len(images)}"
297
+ tasks.append(wrapper(options, file, local_out_dir, idx))
298
+
299
+ # handle case with specific image
300
+ if not images:
301
+ if len(files) == 1:
302
+ tasks.append(wrapper(sandbox_options, files[0], out_dir, "1/1"))
303
+ else:
304
+ for i, file in enumerate(files):
305
+ local_out_dir = out_dir / f"{file.stem}"
306
+ local_out_dir.mkdir(parents=True, exist_ok=True)
307
+ tasks.append(wrapper(sandbox_options, file, local_out_dir, f"{i + 1}/{len(files)}"))
308
+
309
+ await asyncio.gather(*tasks)
310
+ await sandbox.api.session.close()
@@ -0,0 +1,360 @@
1
+ import asyncio
2
+ import sys
3
+ from collections.abc import Coroutine
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import aiofiles
8
+ import aiohttp
9
+ import aiohttp.client_exceptions
10
+ from ptsandbox import Sandbox, SandboxKey
11
+ from ptsandbox.models import SandboxOptionsAdvanced, SandboxUploadException, VNCMode
12
+ from rich.markup import escape
13
+ from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
14
+
15
+ from sandbox_cli.console import console
16
+ from sandbox_cli.internal.config import VMImage, settings
17
+ from sandbox_cli.internal.helpers import get_key_by_name
18
+ from sandbox_cli.utils.compiler import compile_rules_internal
19
+ from sandbox_cli.utils.downloader import download
20
+ from sandbox_cli.utils.merge_dll_hooks import merge_dll_hooks
21
+ from sandbox_cli.utils.scanner import format_link
22
+ from sandbox_cli.utils.unpack import Unpack
23
+
24
+ DELIMETER = "\n"
25
+
26
+
27
+ async def _get_compiled_rules(progress: Progress, rules_dir: Path | None, is_local: bool) -> bytes | None:
28
+ if not rules_dir:
29
+ progress.disable = False
30
+ progress.start()
31
+ return None
32
+
33
+ inner_progress = Progress(
34
+ TextColumn(console.INFO),
35
+ SpinnerColumn(),
36
+ TextColumn(text_format="{task.description}"),
37
+ "•",
38
+ TimeElapsedColumn(),
39
+ console=console,
40
+ )
41
+ task_id: TaskID
42
+
43
+ text = (
44
+ "Compiling rules locally"
45
+ if is_local
46
+ else f"Compiling rules on the remote • [medium_purple]{settings.sandbox[0].host}[/]"
47
+ )
48
+ task_id = inner_progress.add_task(text)
49
+
50
+ with inner_progress:
51
+ result = await compile_rules_internal(rules_dir=rules_dir, is_local=is_local)
52
+ inner_progress.stop_task(task_id=task_id)
53
+
54
+ inner_progress.stop()
55
+
56
+ progress.disable = False
57
+ progress.start()
58
+
59
+ return result
60
+
61
+
62
+ async def _prepare_sandbox_new_scan(
63
+ progress: Progress,
64
+ scan_images: set[VMImage | str],
65
+ rules_dir: Path | None,
66
+ sandbox_key: SandboxKey,
67
+ is_local: bool,
68
+ analysis_duration: int,
69
+ syscall_hooks: Path | None,
70
+ dll_hooks_dir: Path | None,
71
+ custom_command: str | None,
72
+ procdump_new_processes_on_finish: bool,
73
+ bootkitmon: bool,
74
+ bootkitmon_duration: int,
75
+ mitm_disabled: bool,
76
+ disable_clicker: bool,
77
+ skip_sample_run: bool,
78
+ vnc_mode: VNCMode,
79
+ ) -> tuple[Sandbox, SandboxOptionsAdvanced, set[VMImage | str]]:
80
+ sandbox = Sandbox(key=sandbox_key)
81
+
82
+ # detect correct image
83
+ available_images: set[VMImage | str] = set()
84
+ for check_image in (await sandbox.api.get_images()).data:
85
+ if not check_image.image_id:
86
+ continue
87
+
88
+ try:
89
+ available_images.add(VMImage(check_image.image_id))
90
+ except ValueError:
91
+ # maybe it is custom image?
92
+ available_images.add(check_image.image_id)
93
+
94
+ images: set[VMImage | str] = set()
95
+ sandbox_image: VMImage | str = settings.default_image
96
+ for image in scan_images:
97
+ match image:
98
+ case VMImage.LINUX:
99
+ sandbox_image = VMImage.UBUNTU_JAMMY_X64
100
+ images = available_images & settings.linux_images
101
+ if len(images) == 0:
102
+ console.log("Sandbox doesn't support linux images", style="bold red")
103
+ sys.exit(1)
104
+
105
+ console.info(f"Scanning on: [turquoise2]{', '.join(images)}[/]")
106
+ case VMImage.WINDOWS:
107
+ sandbox_image = VMImage.WIN10_1803_X64
108
+ images = available_images & settings.windows_images
109
+
110
+ if len(images) == 0:
111
+ console.log("Sandbox doesn't support windows images", style="bold red")
112
+ sys.exit(1)
113
+
114
+ console.info(f"Scanning on: [turquoise2]{', '.join(images)}[/]")
115
+ case _:
116
+ if image not in available_images:
117
+ console.error(f"Sandbox doesn't support {image}.")
118
+ console.info(f"Available: [turquoise2]{', '.join(available_images)}[/]")
119
+ sys.exit(1)
120
+
121
+ images.add(image)
122
+ sandbox_image = image
123
+
124
+ sandbox_options = SandboxOptionsAdvanced(
125
+ image_id=sandbox_image.value if isinstance(sandbox_image, VMImage) else sandbox_image,
126
+ analysis_duration=analysis_duration,
127
+ )
128
+
129
+ # some enabled options by default
130
+ # All debug options available in library
131
+ sandbox_options.debug_options["save_debug_files"] = True
132
+ sandbox_options.debug_options["extract_crashdumps"] = True
133
+
134
+ # process custom options
135
+ compiled_rules = await _get_compiled_rules(rules_dir=rules_dir, is_local=is_local, progress=progress)
136
+
137
+ if compiled_rules:
138
+ rules_uri = (await sandbox.api.upload_file(compiled_rules)).data.file_uri
139
+ sandbox_options.debug_options["rules_url"] = rules_uri
140
+
141
+ if syscall_hooks:
142
+ progress.console.print(f"{console.INFO} Upload syscall hooks: {syscall_hooks}")
143
+ async with aiofiles.open(syscall_hooks, mode="rb") as fd:
144
+ data = await fd.read()
145
+ syscall_hooks_uri = (await sandbox.api.upload_file(data)).data.file_uri
146
+ sandbox_options.debug_options["custom_syscall_hooks"] = syscall_hooks_uri
147
+
148
+ if dll_hooks_dir:
149
+ progress.console.print(f"{console.INFO} Upload dll hooks: {dll_hooks_dir}")
150
+ data = merge_dll_hooks(Path(dll_hooks_dir))
151
+ dll_hooks_uri = (await sandbox.api.upload_file(data)).data.file_uri
152
+ sandbox_options.debug_options["custom_dll_hooks"] = dll_hooks_uri
153
+
154
+ if custom_command:
155
+ progress.console.print(f"{console.INFO} Commandline: {custom_command}")
156
+ sandbox_options.custom_command = custom_command
157
+
158
+ # add extra options
159
+ sandbox_options.procdump_new_processes_on_finish = procdump_new_processes_on_finish
160
+ sandbox_options.bootkitmon = bootkitmon
161
+ sandbox_options.analysis_duration_bootkitmon = bootkitmon_duration
162
+ sandbox_options.mitm_enabled = not mitm_disabled
163
+ sandbox_options.disable_clicker = disable_clicker
164
+ sandbox_options.skip_sample_run = skip_sample_run
165
+ sandbox_options.vnc_mode = vnc_mode
166
+
167
+ # add here some commands if new options available
168
+
169
+ return (sandbox, sandbox_options, images)
170
+
171
+
172
+ async def scan_internal_advanced(
173
+ *, # no not keyword args
174
+ files: list[Path],
175
+ scan_images: set[VMImage | str],
176
+ rules_dir: Path | None,
177
+ out_dir: Path,
178
+ key_name: str,
179
+ is_local: bool,
180
+ analysis_duration: int,
181
+ syscall_hooks: Path | None,
182
+ dll_hooks_dir: Path | None,
183
+ custom_command: str | None,
184
+ fake_name: str | None,
185
+ unpack: bool,
186
+ priority: int,
187
+ procdump_new_processes_on_finish: bool,
188
+ bootkitmon: bool,
189
+ bootkitmon_duration: int,
190
+ mitm_disabled: bool,
191
+ disable_clicker: bool,
192
+ skip_sample_run: bool,
193
+ vnc_mode: VNCMode,
194
+ extra_files: list[Path] | None,
195
+ upload_timeout: int,
196
+ all: bool,
197
+ debug: bool,
198
+ artifacts: bool,
199
+ download_files: bool,
200
+ crashdumps: bool,
201
+ procdumps: bool,
202
+ decompress: bool,
203
+ ) -> None:
204
+ key = get_key_by_name(key_name)
205
+ sandbox_sem = asyncio.Semaphore(value=key.max_workers)
206
+ progress = Progress(
207
+ SpinnerColumn(),
208
+ TextColumn("{task.fields[idx]}"),
209
+ "•",
210
+ TextColumn("{task.fields[image]}"),
211
+ "•",
212
+ TextColumn("{task.description}"),
213
+ "•",
214
+ TextColumn("{task.fields[url]}"),
215
+ "•",
216
+ TimeElapsedColumn(),
217
+ console=console,
218
+ disable=True,
219
+ transient=True,
220
+ )
221
+ max_image_length = 0
222
+
223
+ async def process_file(
224
+ sandbox_options: SandboxOptionsAdvanced,
225
+ file_path: Path,
226
+ out_dir: Path,
227
+ idx: str,
228
+ ) -> None:
229
+ idx = f"[turquoise2 bold]{idx}[/]"
230
+ formatted_image = f"{escape(f'[{sandbox_options.image_id}]')}"
231
+ image_string = rf"\[{sandbox_options.image_id}]".ljust(max_image_length + 3)
232
+
233
+ async with sandbox_sem:
234
+ task_id = progress.add_task(description="Creating task", idx=idx, image=formatted_image, url="...")
235
+
236
+ wait_time = sandbox_options.analysis_duration * 4 + (300 if sandbox_options.analysis_duration < 80 else 120)
237
+
238
+ try:
239
+ scan_result = await sandbox.create_advanced_scan(
240
+ file_path,
241
+ file_name=fake_name or file_path.name,
242
+ extra_files=extra_files,
243
+ async_result=True,
244
+ priority=priority,
245
+ upload_timeout=upload_timeout,
246
+ sandbox=sandbox_options,
247
+ )
248
+ except SandboxUploadException as e:
249
+ console.error(
250
+ f"{image_string} • [yellow]{file_path.name}[/] • an error occurred when uploading a file to the server • {e}"
251
+ )
252
+ progress.remove_task(task_id)
253
+ return
254
+ except aiohttp.client_exceptions.ClientResponseError as e:
255
+ console.error(f"{image_string} • [yellow]{file_path.name}[/] • {e}")
256
+ progress.remove_task(task_id)
257
+ return
258
+
259
+ formatted_link = f"[medium_purple]{format_link(scan_result, key=key)}[/]"
260
+ final_output = f"{image_string} • [yellow]{file_path.name}[/] • {formatted_link}"
261
+
262
+ progress.update(
263
+ task_id=task_id,
264
+ description=f"Waiting [yellow]{file_path.name}[/]",
265
+ url=formatted_link,
266
+ )
267
+ if not (awaited_report := await sandbox.wait_for_report(scan_result, wait_time)):
268
+ console.error(f"{final_output} • scan failed")
269
+ progress.remove_task(task_id)
270
+ return
271
+
272
+ scan_result = awaited_report
273
+
274
+ # write report.json
275
+ (out_dir / settings.report_name).write_text(scan_result.model_dump_json(indent=4), encoding="utf-8")
276
+
277
+ if not (long_report := scan_result.get_long_report()):
278
+ console.error(f"{final_output} • full report not available")
279
+ progress.remove_task(task_id)
280
+ return
281
+
282
+ progress.update(task_id=task_id, description="Downloading results...")
283
+
284
+ await download(
285
+ long_report,
286
+ sandbox,
287
+ out_dir,
288
+ all=all,
289
+ artifacts=artifacts,
290
+ crashdumps=crashdumps,
291
+ debug=debug,
292
+ decompress=decompress,
293
+ files=download_files,
294
+ logs=True, # by default download logs
295
+ procdumps=procdumps,
296
+ progress=progress,
297
+ video=True, # by default download video
298
+ idx=idx,
299
+ image=formatted_image,
300
+ link=formatted_link,
301
+ )
302
+
303
+ console.done(final_output)
304
+
305
+ progress.remove_task(task_id)
306
+
307
+ if unpack:
308
+ Unpack(out_dir).run()
309
+
310
+ async def wrapper(
311
+ sandbox_options: SandboxOptionsAdvanced,
312
+ file_path: Path,
313
+ out_dir: Path,
314
+ idx: str,
315
+ ) -> None:
316
+ # try:
317
+ await process_file(sandbox_options, file_path, out_dir, idx)
318
+ # except Exception as ex:
319
+ # console.log(f"[cyan]{idx}[/] {file_path} Error: {ex!r}")
320
+
321
+ console.info(f"Using key: name={key.name} max_workers={key.max_workers}")
322
+
323
+ tasks: list[Coroutine[Any, Any, None]] = []
324
+ with progress:
325
+ sandbox, sandbox_options, images = await _prepare_sandbox_new_scan(
326
+ progress,
327
+ scan_images,
328
+ rules_dir,
329
+ key,
330
+ is_local,
331
+ analysis_duration,
332
+ syscall_hooks,
333
+ dll_hooks_dir,
334
+ custom_command,
335
+ procdump_new_processes_on_finish,
336
+ bootkitmon,
337
+ bootkitmon_duration,
338
+ mitm_disabled,
339
+ disable_clicker,
340
+ skip_sample_run,
341
+ vnc_mode,
342
+ )
343
+ max_image_length = max(len(x) for x in images)
344
+ for i, image_id in enumerate(images):
345
+ options = sandbox_options.model_copy(deep=True)
346
+ options.image_id = image_id
347
+
348
+ if len(files) == 1:
349
+ local_out_dir = out_dir / f"{image_id}"
350
+ local_out_dir.mkdir(parents=True, exist_ok=True)
351
+ tasks.append(wrapper(options, files[0], local_out_dir, f"{i + 1}/{len(images)}"))
352
+ else:
353
+ for j, file in enumerate(files):
354
+ local_out_dir = out_dir / f"{file.stem}" / f"{image_id}"
355
+ local_out_dir.mkdir(parents=True, exist_ok=True)
356
+ idx = f"{(i + 1) * (j + 1)}/{len(files) * len(images)}"
357
+ tasks.append(wrapper(options, file, local_out_dir, idx))
358
+
359
+ await asyncio.gather(*tasks)
360
+ await sandbox.api.session.close()