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,258 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import sys
|
|
3
|
+
from collections.abc import Coroutine
|
|
4
|
+
from io import BytesIO
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
from zipfile import BadZipFile, ZipFile
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
import aiohttp.client_exceptions
|
|
11
|
+
from ptsandbox import Sandbox
|
|
12
|
+
from ptsandbox.models import (
|
|
13
|
+
SandboxBaseScanTaskRequest,
|
|
14
|
+
SandboxKey,
|
|
15
|
+
SandboxUploadException,
|
|
16
|
+
)
|
|
17
|
+
from rich.progress import Progress, SpinnerColumn, TaskID, TextColumn, TimeElapsedColumn
|
|
18
|
+
|
|
19
|
+
from sandbox_cli.console import console
|
|
20
|
+
from sandbox_cli.internal.config import settings
|
|
21
|
+
from sandbox_cli.internal.helpers import get_key_by_name
|
|
22
|
+
from sandbox_cli.utils.compiler import compile_rules_internal
|
|
23
|
+
from sandbox_cli.utils.downloader import download
|
|
24
|
+
from sandbox_cli.utils.scanner import format_link
|
|
25
|
+
from sandbox_cli.utils.unpack import Unpack
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
async def _get_compiled_rules(progress: Progress, rules_dir: Path | None, is_local: bool) -> bytes | None:
|
|
29
|
+
if not rules_dir:
|
|
30
|
+
progress.disable = False
|
|
31
|
+
progress.start()
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
inner_progress = Progress(
|
|
35
|
+
TextColumn(console.INFO),
|
|
36
|
+
SpinnerColumn(),
|
|
37
|
+
TextColumn(text_format="{task.description}"),
|
|
38
|
+
"•",
|
|
39
|
+
TimeElapsedColumn(),
|
|
40
|
+
console=console,
|
|
41
|
+
)
|
|
42
|
+
task_id: TaskID
|
|
43
|
+
|
|
44
|
+
text = (
|
|
45
|
+
"Compiling rules locally"
|
|
46
|
+
if is_local
|
|
47
|
+
else f"Compiling rules on the remote • [medium_purple]{settings.sandbox[0].host}[/]"
|
|
48
|
+
)
|
|
49
|
+
task_id = inner_progress.add_task(text)
|
|
50
|
+
|
|
51
|
+
with inner_progress:
|
|
52
|
+
result = await compile_rules_internal(rules_dir=rules_dir, is_local=is_local)
|
|
53
|
+
inner_progress.stop_task(task_id=task_id)
|
|
54
|
+
|
|
55
|
+
inner_progress.stop()
|
|
56
|
+
|
|
57
|
+
progress.disable = False
|
|
58
|
+
progress.start()
|
|
59
|
+
|
|
60
|
+
return result
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def _prepare_rescan_options(
|
|
64
|
+
progress: Progress,
|
|
65
|
+
rules_dir: Path | None,
|
|
66
|
+
sandbox_key: SandboxKey,
|
|
67
|
+
is_local: bool,
|
|
68
|
+
) -> tuple[Sandbox, SandboxBaseScanTaskRequest.Options]:
|
|
69
|
+
sandbox = Sandbox(sandbox_key)
|
|
70
|
+
|
|
71
|
+
sandbox_options = SandboxBaseScanTaskRequest.Options(analysis_depth=2, passwords_for_unpack=settings.passwords)
|
|
72
|
+
|
|
73
|
+
# process custom options
|
|
74
|
+
compiled_rules = await _get_compiled_rules(rules_dir=rules_dir, is_local=is_local, progress=progress)
|
|
75
|
+
|
|
76
|
+
if compiled_rules:
|
|
77
|
+
try:
|
|
78
|
+
rules_uri = (await sandbox.api.upload_file(compiled_rules)).data.file_uri
|
|
79
|
+
sandbox_options.sandbox.debug_options["rules_url"] = rules_uri
|
|
80
|
+
except aiohttp.client_exceptions.ClientResponseError:
|
|
81
|
+
console.error(f"Can't upload compiled rules {rules_dir} to sandbox")
|
|
82
|
+
|
|
83
|
+
return (sandbox, sandbox_options)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
async def rescan_internal(
|
|
87
|
+
*, # no not keyword args
|
|
88
|
+
traces: list[Path],
|
|
89
|
+
rules_dir: Path | None,
|
|
90
|
+
out_dir: Path,
|
|
91
|
+
key_name: str,
|
|
92
|
+
is_local: bool,
|
|
93
|
+
unpack: bool,
|
|
94
|
+
debug: bool,
|
|
95
|
+
) -> None:
|
|
96
|
+
key = get_key_by_name(key_name)
|
|
97
|
+
sandbox_sem = asyncio.Semaphore(value=key.max_workers)
|
|
98
|
+
|
|
99
|
+
progress = Progress(
|
|
100
|
+
SpinnerColumn(),
|
|
101
|
+
TextColumn("{task.fields[idx]}"),
|
|
102
|
+
"•",
|
|
103
|
+
TextColumn("{task.description}"),
|
|
104
|
+
"•",
|
|
105
|
+
TextColumn("{task.fields[url]}"),
|
|
106
|
+
"•",
|
|
107
|
+
TimeElapsedColumn(),
|
|
108
|
+
console=console,
|
|
109
|
+
disable=True,
|
|
110
|
+
transient=True,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
async def process_trace(
|
|
114
|
+
drakvuf_trace: Path | BytesIO,
|
|
115
|
+
tcpdump_pcap: Path | BytesIO,
|
|
116
|
+
trace: Path,
|
|
117
|
+
out_dir: Path,
|
|
118
|
+
idx: str,
|
|
119
|
+
) -> None:
|
|
120
|
+
idx = f"[turquoise2 bold]{idx}[/]"
|
|
121
|
+
|
|
122
|
+
async with sandbox_sem:
|
|
123
|
+
task_id = progress.add_task(description="Creating task", idx=idx, url="...")
|
|
124
|
+
|
|
125
|
+
wait_time = (
|
|
126
|
+
round(sandbox_options.sandbox.analysis_duration * 1.5)
|
|
127
|
+
if sandbox_options.sandbox.analysis_duration > 70
|
|
128
|
+
else 70
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
try:
|
|
132
|
+
rescan_result = await sandbox.create_rescan(
|
|
133
|
+
drakvuf_trace,
|
|
134
|
+
tcpdump_pcap,
|
|
135
|
+
options=sandbox_options,
|
|
136
|
+
rules=None,
|
|
137
|
+
read_timeout=wait_time,
|
|
138
|
+
)
|
|
139
|
+
except SandboxUploadException as e:
|
|
140
|
+
console.error(f"[yellow]{trace}[/] • an error occurred when uploading a file to the server • {e}")
|
|
141
|
+
progress.remove_task(task_id)
|
|
142
|
+
return
|
|
143
|
+
except aiohttp.client_exceptions.ClientResponseError as e:
|
|
144
|
+
console.error(f"[yellow]{trace}[/] • {e}")
|
|
145
|
+
progress.remove_task(task_id)
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
formatted_link = f"[medium_purple]{format_link(rescan_result, key=key)}[/]"
|
|
149
|
+
final_output = f"[yellow]{trace.name}[/] • {formatted_link}"
|
|
150
|
+
|
|
151
|
+
progress.update(
|
|
152
|
+
task_id=task_id,
|
|
153
|
+
description=f"Waiting for full report for [yellow]{trace.name}[/]",
|
|
154
|
+
url=formatted_link,
|
|
155
|
+
)
|
|
156
|
+
if not (awaited_report := await sandbox.wait_for_report(rescan_result, wait_time)):
|
|
157
|
+
console.error(f"Rescan failed for [yellow]{trace.name}[/] • {formatted_link} • {rescan_result}")
|
|
158
|
+
progress.remove_task(task_id)
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
rescan_result = awaited_report
|
|
162
|
+
|
|
163
|
+
# write report.json
|
|
164
|
+
(out_dir / settings.report_name).write_text(rescan_result.model_dump_json(indent=4), encoding="utf-8")
|
|
165
|
+
|
|
166
|
+
# get full report?
|
|
167
|
+
if not (long_report := rescan_result.get_long_report()):
|
|
168
|
+
console.error(f"{final_output} • full report not available")
|
|
169
|
+
progress.remove_task(task_id)
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
progress.update(task_id=task_id, description="Downloading results...")
|
|
173
|
+
|
|
174
|
+
await download(long_report, sandbox, out_dir, logs=True, debug=debug)
|
|
175
|
+
|
|
176
|
+
console.done(final_output)
|
|
177
|
+
|
|
178
|
+
progress.remove_task(task_id)
|
|
179
|
+
|
|
180
|
+
if unpack:
|
|
181
|
+
Unpack(out_dir).run()
|
|
182
|
+
|
|
183
|
+
async def wrapper(trace: Path, out_dir: Path, idx: str) -> None:
|
|
184
|
+
"""
|
|
185
|
+
Internal function for prepare raw traces for re-scan
|
|
186
|
+
|
|
187
|
+
:param trace
|
|
188
|
+
file - zip file with drakvuf-trace.log.gz/drakvuf-trace.log.zst and tcpdump.pcap inside
|
|
189
|
+
|
|
190
|
+
dir - directory with drakvuf-trace.log.gz/drakvuf-trace.log.zst and tcpdump.pcap files
|
|
191
|
+
|
|
192
|
+
:param out_dir - save dir for current trace
|
|
193
|
+
:param idx - just nice index for output
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
drakvuf_trace: Path | BytesIO
|
|
197
|
+
tcpdump_pcap: Path | BytesIO
|
|
198
|
+
if trace.is_dir():
|
|
199
|
+
drakvuf_trace = trace / "drakvuf-trace.log.gz"
|
|
200
|
+
if not drakvuf_trace.exists():
|
|
201
|
+
# handle case with modern zst format
|
|
202
|
+
drakvuf_trace = trace / "drakvuf-trace.log.zst"
|
|
203
|
+
if not drakvuf_trace.exists():
|
|
204
|
+
console.error(
|
|
205
|
+
f"drakvuf-trace.log.gz or drakvuf-trace.log.zst doesn't exists in {trace.expanduser().resolve()}"
|
|
206
|
+
)
|
|
207
|
+
sys.exit(1)
|
|
208
|
+
|
|
209
|
+
tcpdump_pcap = trace / "tcpdump.pcap"
|
|
210
|
+
if not tcpdump_pcap.exists():
|
|
211
|
+
console.error(f"tcpdump.pcap don't exists in {trace}")
|
|
212
|
+
sys.exit(1)
|
|
213
|
+
else:
|
|
214
|
+
try:
|
|
215
|
+
with ZipFile(trace) as zip:
|
|
216
|
+
log_file = ""
|
|
217
|
+
for file in zip.filelist:
|
|
218
|
+
if file.filename in {"drakvuf-trace.log.gz", "drakvuf-trace.log.zst"}:
|
|
219
|
+
log_file = file.filename
|
|
220
|
+
break
|
|
221
|
+
|
|
222
|
+
raw_trace = zip.read(log_file)
|
|
223
|
+
if raw_trace == b"":
|
|
224
|
+
console.error(f"Empty {log_file} in {trace}")
|
|
225
|
+
sys.exit(1)
|
|
226
|
+
|
|
227
|
+
drakvuf_trace = BytesIO(raw_trace)
|
|
228
|
+
tcpdump_pcap = BytesIO(zip.read("tcpdump.pcap"))
|
|
229
|
+
except BadZipFile:
|
|
230
|
+
console.error(f"{trace} not a zip file")
|
|
231
|
+
sys.exit(1)
|
|
232
|
+
|
|
233
|
+
await process_trace(drakvuf_trace, tcpdump_pcap, trace, out_dir, idx)
|
|
234
|
+
|
|
235
|
+
console.info(f"Using key: name={key.name} max_workers={key.max_workers}")
|
|
236
|
+
|
|
237
|
+
tasks: list[Coroutine[Any, Any, None]] = []
|
|
238
|
+
with progress:
|
|
239
|
+
sandbox, sandbox_options = await _prepare_rescan_options(progress, rules_dir, key, is_local)
|
|
240
|
+
|
|
241
|
+
if len(traces) == 1:
|
|
242
|
+
local_out_dir = out_dir / "rescan"
|
|
243
|
+
local_out_dir.mkdir(parents=True, exist_ok=True)
|
|
244
|
+
tasks.append(wrapper(traces[0], local_out_dir, "1/1"))
|
|
245
|
+
else:
|
|
246
|
+
for i, trace in enumerate(traces):
|
|
247
|
+
local_out_dir = out_dir / f"rescan_{i + 1}"
|
|
248
|
+
|
|
249
|
+
# nice names for zip files
|
|
250
|
+
if trace.suffix == ".zip":
|
|
251
|
+
local_out_dir = out_dir / trace.stem
|
|
252
|
+
|
|
253
|
+
local_out_dir.mkdir(parents=True, exist_ok=True)
|
|
254
|
+
idx = f"{i + 1}/{len(traces)}"
|
|
255
|
+
tasks.append(wrapper(trace, local_out_dir, idx))
|
|
256
|
+
|
|
257
|
+
await asyncio.gather(*tasks)
|
|
258
|
+
await sandbox.api.session.close()
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import shutil
|
|
2
|
+
from gzip import GzipFile
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from zipfile import ZipFile
|
|
5
|
+
|
|
6
|
+
import zstandard
|
|
7
|
+
|
|
8
|
+
from sandbox_cli.console import console
|
|
9
|
+
from sandbox_cli.utils.unpack.plugins.abc import BasePlugin
|
|
10
|
+
from sandbox_cli.utils.unpack.plugins.correlation import CorrelatedRules
|
|
11
|
+
from sandbox_cli.utils.unpack.plugins.sort_by_plugins import SortByPlugins
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Unpack:
|
|
15
|
+
def __init__(self, trace: Path) -> None:
|
|
16
|
+
if not trace.exists():
|
|
17
|
+
console.error(f"{trace} not exist")
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
# unpack zip file
|
|
21
|
+
if trace.is_file() and trace.suffix.endswith("zip"):
|
|
22
|
+
self.trace = Path(trace.with_suffix(""))
|
|
23
|
+
self.trace.mkdir(exist_ok=True)
|
|
24
|
+
|
|
25
|
+
with ZipFile(trace, mode="r") as zip:
|
|
26
|
+
zip.extractall(path=self.trace)
|
|
27
|
+
elif trace.is_dir():
|
|
28
|
+
self.trace = trace
|
|
29
|
+
else:
|
|
30
|
+
console.error(f"Unsupported file: {trace}")
|
|
31
|
+
return
|
|
32
|
+
|
|
33
|
+
self.plugins: list[BasePlugin] = [CorrelatedRules(self.trace), SortByPlugins(self.trace)]
|
|
34
|
+
self.logs = {
|
|
35
|
+
"drakvuf-trace": Path(""), # dynamic detect what extension is using
|
|
36
|
+
"correlated": Path(self.trace / "events-correlated.log.gz"),
|
|
37
|
+
"normalized": Path(self.trace / "events-normalized.log.gz"),
|
|
38
|
+
"network": Path(self.trace / "tcpdump.pcap"),
|
|
39
|
+
}
|
|
40
|
+
self.raw = Path(self.trace / "raw")
|
|
41
|
+
|
|
42
|
+
def _extract_logs(self) -> None:
|
|
43
|
+
def _extract(file: Path) -> None:
|
|
44
|
+
if file.exists() and file.suffix.endswith("zst"):
|
|
45
|
+
dctx = zstandard.ZstdDecompressor()
|
|
46
|
+
with open(file, mode="rb") as zst, open(file.with_suffix(""), "wb") as out:
|
|
47
|
+
dctx.copy_stream(zst, out)
|
|
48
|
+
|
|
49
|
+
if file.exists() and file.suffix.endswith("gz"):
|
|
50
|
+
with GzipFile(file, mode="rb") as gzip, open(file.with_suffix(""), "wb") as out:
|
|
51
|
+
out.write(gzip.read())
|
|
52
|
+
|
|
53
|
+
for log in self.logs.values():
|
|
54
|
+
_extract(log)
|
|
55
|
+
|
|
56
|
+
def _create_dirs(self) -> None:
|
|
57
|
+
def _create(dir: Path) -> None:
|
|
58
|
+
if dir.exists() and dir.is_dir():
|
|
59
|
+
shutil.rmtree(dir)
|
|
60
|
+
dir.mkdir(exist_ok=True)
|
|
61
|
+
|
|
62
|
+
for dir in self.logs:
|
|
63
|
+
_create(Path(self.trace / dir))
|
|
64
|
+
|
|
65
|
+
def _move_files(self) -> None:
|
|
66
|
+
self.raw.mkdir(exist_ok=True)
|
|
67
|
+
for log in self.logs.values():
|
|
68
|
+
if log.exists() and log.is_file():
|
|
69
|
+
shutil.copy(log, self.raw)
|
|
70
|
+
|
|
71
|
+
for dir, file in self.logs.items():
|
|
72
|
+
if not file.exists() or file.is_dir():
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if file.suffix.endswith("gz") or file.suffix.endswith("zst"):
|
|
76
|
+
shutil.move(file.with_suffix(""), self.trace / dir)
|
|
77
|
+
else:
|
|
78
|
+
shutil.move(file, self.trace / dir)
|
|
79
|
+
|
|
80
|
+
def run(self) -> None:
|
|
81
|
+
if Path(self.trace / "drakvuf-trace.log.gz").exists():
|
|
82
|
+
self.logs["drakvuf-trace"] = Path(self.trace / "drakvuf-trace.log.gz")
|
|
83
|
+
if Path(self.trace / "drakvuf-trace.log.zst").exists():
|
|
84
|
+
self.logs["drakvuf-trace"] = Path(self.trace / "drakvuf-trace.log.zst")
|
|
85
|
+
|
|
86
|
+
self._extract_logs()
|
|
87
|
+
self._create_dirs()
|
|
88
|
+
self._move_files()
|
|
89
|
+
|
|
90
|
+
# Run plugins
|
|
91
|
+
for plugin in self.plugins:
|
|
92
|
+
plugin.run()
|
|
93
|
+
|
|
94
|
+
# Remove files
|
|
95
|
+
for log in self.logs.values():
|
|
96
|
+
log.unlink(missing_ok=True)
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class BasePlugin(ABC):
|
|
6
|
+
"""
|
|
7
|
+
Base class for all plugins
|
|
8
|
+
|
|
9
|
+
It is necessary to designate a single entry point
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, trace: Path) -> None:
|
|
13
|
+
self.trace = trace
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def run(self) -> None:
|
|
17
|
+
"""Invoke plugin"""
|
|
18
|
+
...
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import orjson
|
|
5
|
+
|
|
6
|
+
from sandbox_cli.console import console
|
|
7
|
+
from sandbox_cli.models.detections import DetectionType
|
|
8
|
+
from sandbox_cli.utils.unpack.plugins.abc import BasePlugin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CorrelatedRules(BasePlugin):
|
|
12
|
+
def run(self) -> None:
|
|
13
|
+
base_path = self.trace / "correlated"
|
|
14
|
+
file = Path(base_path / "events-correlated.log")
|
|
15
|
+
if not file.exists():
|
|
16
|
+
console.warning(f"{file} not exist")
|
|
17
|
+
return
|
|
18
|
+
|
|
19
|
+
with open(file, errors="ignore", encoding="utf-8") as fd:
|
|
20
|
+
raw_trace = fd.readlines()
|
|
21
|
+
|
|
22
|
+
suspicious: defaultdict[str, list[str]] = defaultdict(list)
|
|
23
|
+
silent: defaultdict[str, list[str]] = defaultdict(list)
|
|
24
|
+
malware: list[str] = []
|
|
25
|
+
|
|
26
|
+
for line in raw_trace:
|
|
27
|
+
data: dict[str, str] = orjson.loads(line)
|
|
28
|
+
if data.get("detect.type"):
|
|
29
|
+
match data["detect.type"]:
|
|
30
|
+
case DetectionType.malware:
|
|
31
|
+
malware.append(line)
|
|
32
|
+
case DetectionType.suspicious:
|
|
33
|
+
suspicious[data["detect.name"]].append(line)
|
|
34
|
+
case DetectionType.silent:
|
|
35
|
+
silent[data["detect.name"]].append(line)
|
|
36
|
+
case _:
|
|
37
|
+
pass
|
|
38
|
+
|
|
39
|
+
if len(malware) > 0:
|
|
40
|
+
with open(f"{file}.malware", "w", encoding="utf-8") as fd:
|
|
41
|
+
for line in malware:
|
|
42
|
+
fd.write(line)
|
|
43
|
+
|
|
44
|
+
for key, lines in silent.items():
|
|
45
|
+
with open(f"{file}.silent.{key}", "w", encoding="utf-8") as fd:
|
|
46
|
+
for line in lines:
|
|
47
|
+
fd.write(line)
|
|
48
|
+
|
|
49
|
+
for key, lines in suspicious.items():
|
|
50
|
+
with open(f"{file}.suspicious.{key}", "w", encoding="utf-8") as fd:
|
|
51
|
+
for line in lines:
|
|
52
|
+
fd.write(line)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
import orjson
|
|
5
|
+
|
|
6
|
+
from sandbox_cli.console import console
|
|
7
|
+
from sandbox_cli.utils.unpack.plugins.abc import BasePlugin
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SortByPlugins(BasePlugin):
|
|
11
|
+
def run(self) -> None:
|
|
12
|
+
base_path = self.trace / "normalized"
|
|
13
|
+
file = Path(base_path / "events-normalized.log")
|
|
14
|
+
if not file.exists():
|
|
15
|
+
console.warning(f"{file} not exist")
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
with open(file, errors="ignore") as fd:
|
|
19
|
+
raw_trace = fd.readlines()
|
|
20
|
+
|
|
21
|
+
plugins: defaultdict[str, list[str]] = defaultdict(list)
|
|
22
|
+
for line in raw_trace:
|
|
23
|
+
data: dict[str, str] = orjson.loads(line)
|
|
24
|
+
if data.get("plugin"):
|
|
25
|
+
plugins[data["plugin"]].append(line)
|
|
26
|
+
|
|
27
|
+
for plugin, lines in plugins.items():
|
|
28
|
+
with open(f"{file}.{plugin}", "w", encoding="utf-8") as fd:
|
|
29
|
+
for line in lines:
|
|
30
|
+
fd.write(line)
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: sandbox-cli
|
|
3
|
+
Version: 0.2.26
|
|
4
|
+
Summary: Command line tool for interaction with sandboxes
|
|
5
|
+
Project-URL: Homepage, https://github.com/Security-Experts-Community/sandbox-cli
|
|
6
|
+
Project-URL: Documentation, https://security-experts-community.github.io/sandbox-cli
|
|
7
|
+
Project-URL: Repository, https://github.com/Security-Experts-Community/sandbox-cli
|
|
8
|
+
Project-URL: Issues, https://github.com/Security-Experts-Community/sandbox-cli/issues
|
|
9
|
+
Author: Alexey Kolesnikov
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
License-File: NOTICE
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Requires-Dist: aiofiles>=24.1.0
|
|
15
|
+
Requires-Dist: asyncssh>=2.19.0
|
|
16
|
+
Requires-Dist: cryptography>=44.0.0
|
|
17
|
+
Requires-Dist: cyclopts>=3.9.0
|
|
18
|
+
Requires-Dist: docker>=7.1.0
|
|
19
|
+
Requires-Dist: ptsandbox
|
|
20
|
+
Requires-Dist: pyzipper>=0.3.6
|
|
21
|
+
Requires-Dist: rich>=13.9.4
|
|
22
|
+
Requires-Dist: zstandard>=0.23.0
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<em>Work with PT Sandbox like a pro</em>
|
|
29
|
+
</p>
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
**Documentation**: <a href="https://security-experts-community.github.io/sandbox-cli">https://security-experts-community.github.io/sandbox-cli</a>
|
|
34
|
+
|
|
35
|
+
**Source Code**: <a href="https://github.com/Security-Experts-Community/sandbox-cli">https://github.com/Security-Experts-Community/sandbox-cli</a>
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
39
|
+
> [!NOTE]
|
|
40
|
+
> `python >= 3.11` is required.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
Using `pipx`:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
pipx install sandbox-cli
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Using `PyPi`:
|
|
51
|
+
|
|
52
|
+
```sh
|
|
53
|
+
pip install sandbox-cli
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
NixOS:
|
|
57
|
+
|
|
58
|
+
```
|
|
59
|
+
Add inputs.sandbox-cli.overlays.default to your nixpkgs overlay
|
|
60
|
+
TBA: ...
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Config
|
|
64
|
+
|
|
65
|
+
You must create default config file as described in `docs/config-examples/config.toml`:
|
|
66
|
+
|
|
67
|
+
Linux/MacOS:
|
|
68
|
+
|
|
69
|
+
```sh
|
|
70
|
+
~/.config/sandbox-cli/config.toml
|
|
71
|
+
or
|
|
72
|
+
$XDG_HOME_CONFIG_HOME/sandbox-cli/config.toml
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Windows:
|
|
76
|
+
|
|
77
|
+
```ps1
|
|
78
|
+
%APPDATA%\sandbox-cli\config.toml
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Available options
|
|
82
|
+
|
|
83
|
+
- `scanner` - Scan with the sandbox.
|
|
84
|
+
- `images` - Get available images in the sandbox.
|
|
85
|
+
- `download` - Download any artifact from the sandbox.
|
|
86
|
+
- `email` - Upload an email and get its headers.
|
|
87
|
+
- `report` - Generate short report from sandbox scans.
|
|
88
|
+
- `unpack`/`conv` - Convert sandbox logs into an analysis-friendly format.
|
|
89
|
+
- `rules` - Working with raw sandbox rules.
|
|
90
|
+
|
|
91
|
+
<p align="middle">
|
|
92
|
+
<img width="50%" src="https://raw.githubusercontent.com/Security-Experts-Community/sandbox-cli/refs/heads/main/docs/assets/pic_right.svg">
|
|
93
|
+
</p>
|
|
94
|
+
|
|
95
|
+
## Usage examples
|
|
96
|
+
|
|
97
|
+
### images
|
|
98
|
+
|
|
99
|
+
Get all availables images:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
sandbox-cli images
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
┏━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━┓
|
|
107
|
+
┃ Name ┃ ID ┃ Version ┃ Product version ┃
|
|
108
|
+
┡━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━┩
|
|
109
|
+
│ altlinux │ altworkstation-10-x64 │ ... │ ... │
|
|
110
|
+
│ astra │ astralinux-smolensk-x64 │ ... │ ... │
|
|
111
|
+
│ redos │ redos-murom-x64 │ ... │ ... │
|
|
112
|
+
│ ubuntu │ ubuntu-jammy-x64 │ ... │ ... │
|
|
113
|
+
│ Windows 10 Pro │ win10-1803-x64 │ ... │ ... │
|
|
114
|
+
│ Windows 10 Enterprise │ win10-22H2-x64 │ ... │ ... │
|
|
115
|
+
│ Windows 10 Pro │ win11-23H2-x64 │ ... │ ... │
|
|
116
|
+
│ Windows 7 Enterprise │ win7-sp1-x64 │ ... │ ... │
|
|
117
|
+
│ Windows 7 Enterprise │ win7-sp1-x64-ics │ ... │ ... │
|
|
118
|
+
└───────────────────────┴─────────────────────────┴────────────┴─────────────────┘
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### scanner
|
|
122
|
+
|
|
123
|
+
Scan the file on all available windows images with timeout 60s and with automatic logs unpacking:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
sandbox-cli scanner scan-new -i windows -t 60 -U malware.exe
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
<p align="middle">
|
|
130
|
+
<img width="50%" src="https://raw.githubusercontent.com/Security-Experts-Community/sandbox-cli/refs/heads/main/docs/assets/pic_left.svg">
|
|
131
|
+
</p>
|
|
132
|
+
|
|
133
|
+
## Development
|
|
134
|
+
|
|
135
|
+
`uv` is used to build the project.
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
uv sync
|
|
139
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
sandbox_cli/__main__.py,sha256=oTOdIxj9pwlwGF-QBzLjUrfqBcDl5sot0YtPeoPrwaE,657
|
|
2
|
+
sandbox_cli/console.py,sha256=i-jaDv2L8f2gubg2gy_FBOd8Mr12MalyuJ4gNw_ZnWw,724
|
|
3
|
+
sandbox_cli/cli/__init__.py,sha256=jdpd01ahycl3JKr4N-8R_CLSZvvoFFGDQEqP2op1K4A,1283
|
|
4
|
+
sandbox_cli/cli/downloader.py,sha256=cygTXJdVl8pkHFRULwwpNBpaiwRrjFQn_GHSilWj9J4,8633
|
|
5
|
+
sandbox_cli/cli/images.py,sha256=Tv-s6-7sDugeNOK0L1lmfeq8XqCF7avbBXGq_yuG1Q8,1263
|
|
6
|
+
sandbox_cli/cli/reporter.py,sha256=NLus8Zi8Gl7AUXLk69_lMnd2uJcHQG5golAY7J-uWio,4588
|
|
7
|
+
sandbox_cli/cli/unpack.py,sha256=zrgyh-ausbBMFc6XlY8qShF1zTrr3MvvjsaSu_g2WBg,1370
|
|
8
|
+
sandbox_cli/cli/rules/__init__.py,sha256=ASnj-zzYMYJ-K2X9-dvsDErL4FcdZ7qZyqsd3PQN7FA,2249
|
|
9
|
+
sandbox_cli/cli/scanner/__init__.py,sha256=FQICErnmNdo2EvmoCeWOOdcGIwEJzGmTUH_y9dV7nsI,18905
|
|
10
|
+
sandbox_cli/internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
11
|
+
sandbox_cli/internal/config.py,sha256=FUARsPCCdRhVWRXAH-qmzPw635O8OGSA7s5WkmzL5mI,3856
|
|
12
|
+
sandbox_cli/internal/helpers.py,sha256=r9bFq_aghCU-nHEXVXgv-LpS24prFtfYYzraHSv887E,839
|
|
13
|
+
sandbox_cli/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
|
+
sandbox_cli/models/detections.py,sha256=wn6yBevFDVFNT8T8EvvSAZY80NJUpvzGR6D9ejgyN4c,2377
|
|
15
|
+
sandbox_cli/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
16
|
+
sandbox_cli/utils/extractors.py,sha256=lFpIHy9hqNyqqdT9VyzGx5GJeqdXKA0E3VMU3vjAuvg,2111
|
|
17
|
+
sandbox_cli/utils/merge_dll_hooks.py,sha256=8prsx6OXDMZsQ7l2nju52IQovccvAMbSL2oLq0szln0,2203
|
|
18
|
+
sandbox_cli/utils/compiler/__init__.py,sha256=LXogwbXl9RjIf-ehiqCrliqxq39qVabthoqzi9v1m-o,1991
|
|
19
|
+
sandbox_cli/utils/compiler/abc.py,sha256=5kQPSks3bNn3HtL631Yj8JY37Jzb4xYU4btBZH0KEfI,1665
|
|
20
|
+
sandbox_cli/utils/compiler/docker.py,sha256=c85JKyRlof9twsn_FSS9jJi9nG96icU7UOfSneZc2Ps,5056
|
|
21
|
+
sandbox_cli/utils/compiler/ssh.py,sha256=e_ndOdat-nHrvuTEPXwuoCnTBrE9HgS57WvRVdDUBuY,7782
|
|
22
|
+
sandbox_cli/utils/downloader/__init__.py,sha256=OjxzpBqwg1z3BxFNXYud9FqKtsufbDtKB8O5v0RTatk,5780
|
|
23
|
+
sandbox_cli/utils/scanner/__init__.py,sha256=EHbuGJdd2HxOZgpHbJYPKqrpSMT1vuGjeyNaSAtqPqQ,10567
|
|
24
|
+
sandbox_cli/utils/scanner/advanced.py,sha256=kneArCv0_GoHH_BMAYaRJ-70oZ9pzd1Pw89wLf-xNA8,12671
|
|
25
|
+
sandbox_cli/utils/scanner/rescan.py,sha256=p7uAeIPja_IxaDwrF1htI7I82WUei4URsWuvMN8SIyI,8928
|
|
26
|
+
sandbox_cli/utils/unpack/__init__.py,sha256=GGjwn1PfBCWiCkDsn_PpUHbzjMkolU1FEKMRdXnmqWA,3414
|
|
27
|
+
sandbox_cli/utils/unpack/plugins/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
+
sandbox_cli/utils/unpack/plugins/abc.py,sha256=KO5PpLh8bOPGK8dP7kX09AlIOCOH5uiGzj9qEjT-G2E,349
|
|
29
|
+
sandbox_cli/utils/unpack/plugins/correlation.py,sha256=vFZ1w50bZNpJ6JNvHpgw8Bl521Gr3DfFxiybUbowFPg,1879
|
|
30
|
+
sandbox_cli/utils/unpack/plugins/sort_by_plugins.py,sha256=cOzvIipRfRkPzywBs3tSga5cTFziY6r07hWCT_4hmmQ,956
|
|
31
|
+
sandbox_cli-0.2.26.dist-info/METADATA,sha256=gNeDe1-7vsW2lv8mba9CDYAx5OtXCKpmEUNnRFeRfLc,4663
|
|
32
|
+
sandbox_cli-0.2.26.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
33
|
+
sandbox_cli-0.2.26.dist-info/entry_points.txt,sha256=MSNWgkywjiimrStOm1YiKvWRTF6AgsIeY2Pe1uH58UA,58
|
|
34
|
+
sandbox_cli-0.2.26.dist-info/licenses/LICENSE,sha256=ugWNDYq5trWIIzRbW1KIspTSOb5CdmIitkt2keSP4bY,1083
|
|
35
|
+
sandbox_cli-0.2.26.dist-info/licenses/NOTICE,sha256=dM9C3p-YClC8_a2Z7dEaHpU5cU4GxmNJKGFQrZ4ZiEQ,16036
|
|
36
|
+
sandbox_cli-0.2.26.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Security Experts Community
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|