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.
- sandbox_cli/__main__.py +28 -0
- sandbox_cli/cli/__init__.py +46 -0
- sandbox_cli/cli/downloader.py +308 -0
- sandbox_cli/cli/images.py +45 -0
- sandbox_cli/cli/reporter.py +146 -0
- sandbox_cli/cli/rules/__init__.py +83 -0
- sandbox_cli/cli/scanner/__init__.py +681 -0
- sandbox_cli/cli/unpack.py +50 -0
- sandbox_cli/console.py +24 -0
- sandbox_cli/internal/__init__.py +0 -0
- sandbox_cli/internal/config.py +143 -0
- sandbox_cli/internal/helpers.py +32 -0
- sandbox_cli/models/__init__.py +0 -0
- sandbox_cli/models/detections.py +91 -0
- sandbox_cli/utils/__init__.py +0 -0
- sandbox_cli/utils/compiler/__init__.py +60 -0
- sandbox_cli/utils/compiler/abc.py +51 -0
- sandbox_cli/utils/compiler/docker.py +136 -0
- sandbox_cli/utils/compiler/ssh.py +203 -0
- sandbox_cli/utils/downloader/__init__.py +178 -0
- sandbox_cli/utils/extractors.py +66 -0
- sandbox_cli/utils/merge_dll_hooks.py +84 -0
- sandbox_cli/utils/scanner/__init__.py +310 -0
- sandbox_cli/utils/scanner/advanced.py +360 -0
- sandbox_cli/utils/scanner/rescan.py +258 -0
- sandbox_cli/utils/unpack/__init__.py +96 -0
- sandbox_cli/utils/unpack/plugins/__init__.py +0 -0
- sandbox_cli/utils/unpack/plugins/abc.py +18 -0
- sandbox_cli/utils/unpack/plugins/correlation.py +52 -0
- sandbox_cli/utils/unpack/plugins/sort_by_plugins.py +30 -0
- sandbox_cli-0.2.26.dist-info/METADATA +139 -0
- sandbox_cli-0.2.26.dist-info/RECORD +36 -0
- sandbox_cli-0.2.26.dist-info/WHEEL +4 -0
- sandbox_cli-0.2.26.dist-info/entry_points.txt +2 -0
- sandbox_cli-0.2.26.dist-info/licenses/LICENSE +21 -0
- 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()
|