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
sandbox_cli/__main__.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
from http import HTTPStatus
|
|
3
|
+
|
|
4
|
+
import aiohttp
|
|
5
|
+
import aiohttp.client_exceptions
|
|
6
|
+
|
|
7
|
+
from sandbox_cli.cli import app
|
|
8
|
+
from sandbox_cli.console import console
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main() -> None:
|
|
12
|
+
if sys.platform == "win32":
|
|
13
|
+
import asyncio
|
|
14
|
+
|
|
15
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
app()
|
|
19
|
+
except aiohttp.client_exceptions.ClientResponseError as e:
|
|
20
|
+
# global handler for 401 error
|
|
21
|
+
if e.status == HTTPStatus.UNAUTHORIZED:
|
|
22
|
+
console.error(f"The specified token is not valid. {e}")
|
|
23
|
+
except Exception:
|
|
24
|
+
console.print_exception()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
if __name__ == "__main__":
|
|
28
|
+
main()
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Register all commands here
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from cyclopts import App
|
|
6
|
+
|
|
7
|
+
from sandbox_cli.cli.reporter import generate_report
|
|
8
|
+
from sandbox_cli.cli.unpack import unpack_logs
|
|
9
|
+
from sandbox_cli.console import console
|
|
10
|
+
from sandbox_cli.internal.config import configpath, settings
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_version() -> str:
|
|
14
|
+
import importlib.metadata
|
|
15
|
+
|
|
16
|
+
version = importlib.metadata.version("sandbox-cli")
|
|
17
|
+
return f"sandbox-cli {version}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
app = App(
|
|
21
|
+
name="sandbox-cli",
|
|
22
|
+
help="Work with sandbox like a pro"
|
|
23
|
+
+ (
|
|
24
|
+
f"\n\nTo access other commands, specify at least one sandbox in the config at **{configpath}**"
|
|
25
|
+
if len(settings.sandbox_keys) == 0
|
|
26
|
+
else ""
|
|
27
|
+
),
|
|
28
|
+
help_format="markdown",
|
|
29
|
+
version=get_version,
|
|
30
|
+
console=console,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
app.command(name=["conv", "unpack"])(unpack_logs)
|
|
34
|
+
app.command(name="report")(generate_report)
|
|
35
|
+
|
|
36
|
+
if len(settings.sandbox_keys) > 0:
|
|
37
|
+
from sandbox_cli.cli.downloader import download_command, download_email
|
|
38
|
+
from sandbox_cli.cli.images import get_images
|
|
39
|
+
from sandbox_cli.cli.rules import rules
|
|
40
|
+
from sandbox_cli.cli.scanner import scanner
|
|
41
|
+
|
|
42
|
+
app.command(scanner)
|
|
43
|
+
app.command(rules)
|
|
44
|
+
app.command(name="images")(get_images)
|
|
45
|
+
app.command(name="download")(download_command)
|
|
46
|
+
app.command(name="email")(download_email)
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from collections.abc import Coroutine
|
|
3
|
+
from http import HTTPStatus
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Any
|
|
6
|
+
from urllib.parse import urlparse
|
|
7
|
+
from uuid import UUID
|
|
8
|
+
|
|
9
|
+
import aiofiles
|
|
10
|
+
import aiohttp
|
|
11
|
+
import aiohttp.client_exceptions
|
|
12
|
+
from cyclopts import Parameter
|
|
13
|
+
from ptsandbox import Sandbox
|
|
14
|
+
from ptsandbox.models import Artifact, SandboxBaseTaskResponse
|
|
15
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
16
|
+
|
|
17
|
+
from sandbox_cli.console import console
|
|
18
|
+
from sandbox_cli.internal.config import settings
|
|
19
|
+
from sandbox_cli.internal.helpers import (
|
|
20
|
+
get_key_by_name,
|
|
21
|
+
get_sandbox_key_by_host,
|
|
22
|
+
validate_key,
|
|
23
|
+
)
|
|
24
|
+
from sandbox_cli.utils.downloader import download
|
|
25
|
+
from sandbox_cli.utils.unpack import Unpack
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_key_and_task(key: str, task: str) -> tuple[Sandbox, UUID] | tuple[None, None]:
|
|
29
|
+
try:
|
|
30
|
+
uuid = UUID(task)
|
|
31
|
+
sandbox_key = get_key_by_name(key)
|
|
32
|
+
return Sandbox(sandbox_key), uuid
|
|
33
|
+
except ValueError:
|
|
34
|
+
url = urlparse(task)
|
|
35
|
+
if not (url.scheme and url.path and "/tasks" in url.path):
|
|
36
|
+
console.warning(f"Invalid task_id: {task}")
|
|
37
|
+
return None, None
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
uuid = UUID(url.path.split("/")[2])
|
|
41
|
+
sandbox_key = get_sandbox_key_by_host(url.hostname or "")
|
|
42
|
+
return Sandbox(sandbox_key), uuid
|
|
43
|
+
except ValueError:
|
|
44
|
+
console.error(f"Invalid task id: {task}")
|
|
45
|
+
return None, None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def download_command(
|
|
49
|
+
tasks_id: Annotated[
|
|
50
|
+
list[str],
|
|
51
|
+
Parameter(
|
|
52
|
+
help="Links to tasks or task ids",
|
|
53
|
+
negative="",
|
|
54
|
+
required=True,
|
|
55
|
+
),
|
|
56
|
+
],
|
|
57
|
+
/,
|
|
58
|
+
*,
|
|
59
|
+
key: Annotated[
|
|
60
|
+
str,
|
|
61
|
+
Parameter(
|
|
62
|
+
name=["--key", "-k"],
|
|
63
|
+
help=f"The key to access the sandbox **{'**,**'.join(x.name for x in settings.sandbox_keys)}**",
|
|
64
|
+
validator=validate_key,
|
|
65
|
+
group="Sandbox",
|
|
66
|
+
),
|
|
67
|
+
] = settings.sandbox_keys[0].name,
|
|
68
|
+
out_dir: Annotated[
|
|
69
|
+
Path,
|
|
70
|
+
Parameter(
|
|
71
|
+
name=["--out", "-o"],
|
|
72
|
+
help="Output directory",
|
|
73
|
+
),
|
|
74
|
+
] = Path("./downloads"),
|
|
75
|
+
decompress: Annotated[
|
|
76
|
+
bool,
|
|
77
|
+
Parameter(
|
|
78
|
+
name=["--decompress", "-D"],
|
|
79
|
+
help="Decompress downloaded files",
|
|
80
|
+
negative="",
|
|
81
|
+
),
|
|
82
|
+
] = False,
|
|
83
|
+
unpack: Annotated[
|
|
84
|
+
bool,
|
|
85
|
+
Parameter(
|
|
86
|
+
name=["--unpack", "-U"],
|
|
87
|
+
help="Unpack downloaded files",
|
|
88
|
+
negative="",
|
|
89
|
+
),
|
|
90
|
+
] = False,
|
|
91
|
+
all: Annotated[
|
|
92
|
+
bool,
|
|
93
|
+
Parameter(
|
|
94
|
+
name=["--all", "-a"],
|
|
95
|
+
help="Download all artifacts",
|
|
96
|
+
negative="",
|
|
97
|
+
group="Download options",
|
|
98
|
+
),
|
|
99
|
+
] = False,
|
|
100
|
+
debug: Annotated[
|
|
101
|
+
bool,
|
|
102
|
+
Parameter(
|
|
103
|
+
name=["--debug", "-d"],
|
|
104
|
+
help="Download debug artifacts",
|
|
105
|
+
negative="",
|
|
106
|
+
group="Download options",
|
|
107
|
+
),
|
|
108
|
+
] = False,
|
|
109
|
+
artifacts: Annotated[
|
|
110
|
+
bool,
|
|
111
|
+
Parameter(
|
|
112
|
+
name=["--artifacts", "-A"],
|
|
113
|
+
help="Download artifacts",
|
|
114
|
+
negative="",
|
|
115
|
+
group="Download options",
|
|
116
|
+
),
|
|
117
|
+
] = False,
|
|
118
|
+
files: Annotated[
|
|
119
|
+
bool,
|
|
120
|
+
Parameter(
|
|
121
|
+
name=["--files", "-f"],
|
|
122
|
+
help="Download files",
|
|
123
|
+
negative="",
|
|
124
|
+
group="Download options",
|
|
125
|
+
),
|
|
126
|
+
] = False,
|
|
127
|
+
crashdumps: Annotated[
|
|
128
|
+
bool,
|
|
129
|
+
Parameter(
|
|
130
|
+
name=["--crashdumps", "-c"],
|
|
131
|
+
help="Download crashdumps (maybe be more 1GB)",
|
|
132
|
+
negative="",
|
|
133
|
+
group="Download options",
|
|
134
|
+
),
|
|
135
|
+
] = False,
|
|
136
|
+
procdumps: Annotated[
|
|
137
|
+
bool,
|
|
138
|
+
Parameter(
|
|
139
|
+
name=["--procdumps", "-p"],
|
|
140
|
+
help="Download procdumps",
|
|
141
|
+
negative="",
|
|
142
|
+
group="Download options",
|
|
143
|
+
),
|
|
144
|
+
] = False,
|
|
145
|
+
video: Annotated[
|
|
146
|
+
bool,
|
|
147
|
+
Parameter(
|
|
148
|
+
name=["--video", "-v"],
|
|
149
|
+
help="Download video",
|
|
150
|
+
negative="",
|
|
151
|
+
group="Download options",
|
|
152
|
+
),
|
|
153
|
+
] = False,
|
|
154
|
+
logs: Annotated[
|
|
155
|
+
bool,
|
|
156
|
+
Parameter(
|
|
157
|
+
name=["--logs", "-l"],
|
|
158
|
+
help="Download logs",
|
|
159
|
+
negative="",
|
|
160
|
+
group="Download options",
|
|
161
|
+
),
|
|
162
|
+
] = False,
|
|
163
|
+
) -> None:
|
|
164
|
+
"""
|
|
165
|
+
Download any artifact from the sandbox.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
async def worker(
|
|
169
|
+
report: SandboxBaseTaskResponse.LongReport,
|
|
170
|
+
sandbox: Sandbox,
|
|
171
|
+
out_dir: Path,
|
|
172
|
+
all: bool = False,
|
|
173
|
+
artifacts: bool = False,
|
|
174
|
+
crashdumps: bool = False,
|
|
175
|
+
debug: bool = False,
|
|
176
|
+
decompress: bool = False,
|
|
177
|
+
files: bool = False,
|
|
178
|
+
logs: bool = False,
|
|
179
|
+
procdumps: bool = False,
|
|
180
|
+
video: bool = False,
|
|
181
|
+
progress: Progress | None = None,
|
|
182
|
+
) -> None:
|
|
183
|
+
await download(
|
|
184
|
+
report=report,
|
|
185
|
+
sandbox=sandbox,
|
|
186
|
+
out_dir=out_dir,
|
|
187
|
+
all=all,
|
|
188
|
+
debug=debug,
|
|
189
|
+
artifacts=artifacts,
|
|
190
|
+
files=files,
|
|
191
|
+
crashdumps=crashdumps,
|
|
192
|
+
procdumps=procdumps,
|
|
193
|
+
video=video,
|
|
194
|
+
logs=logs,
|
|
195
|
+
decompress=decompress,
|
|
196
|
+
progress=progress,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if unpack and out_dir.exists():
|
|
200
|
+
Unpack(out_dir).run()
|
|
201
|
+
|
|
202
|
+
progress = Progress(
|
|
203
|
+
SpinnerColumn(),
|
|
204
|
+
TextColumn("{task.description}"),
|
|
205
|
+
TimeElapsedColumn(),
|
|
206
|
+
console=console,
|
|
207
|
+
transient=True,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
tasks: list[Coroutine[Any, Any, Artifact.EngineResult | None]] = []
|
|
211
|
+
|
|
212
|
+
for task in tasks_id:
|
|
213
|
+
sandbox, task_id = get_key_and_task(key, task)
|
|
214
|
+
if not sandbox or not task_id:
|
|
215
|
+
continue
|
|
216
|
+
|
|
217
|
+
try:
|
|
218
|
+
result = await sandbox.get_report(task_id=task_id)
|
|
219
|
+
except aiohttp.client_exceptions.ClientResponseError as e:
|
|
220
|
+
if e.status == HTTPStatus.NOT_FOUND:
|
|
221
|
+
console.warning(
|
|
222
|
+
f"The report cannot be accessed from {task_id}.\n"
|
|
223
|
+
"Read more about it in: https://security-experts-community.github.io/py-ptsandbox/usage/public-api/download-files/"
|
|
224
|
+
)
|
|
225
|
+
continue
|
|
226
|
+
|
|
227
|
+
if (report := result.get_long_report()) is None:
|
|
228
|
+
console.warning(
|
|
229
|
+
f"The report cannot be accessed from {task_id}.\n"
|
|
230
|
+
"Read more about it in: https://security-experts-community.github.io/py-ptsandbox/usage/public-api/download-files/"
|
|
231
|
+
)
|
|
232
|
+
continue
|
|
233
|
+
|
|
234
|
+
tasks.append(
|
|
235
|
+
worker(
|
|
236
|
+
report=report,
|
|
237
|
+
sandbox=sandbox,
|
|
238
|
+
out_dir=out_dir / str(task_id),
|
|
239
|
+
all=all,
|
|
240
|
+
debug=debug,
|
|
241
|
+
artifacts=artifacts,
|
|
242
|
+
files=files,
|
|
243
|
+
crashdumps=crashdumps,
|
|
244
|
+
procdumps=procdumps,
|
|
245
|
+
video=video,
|
|
246
|
+
logs=logs,
|
|
247
|
+
decompress=decompress,
|
|
248
|
+
progress=progress,
|
|
249
|
+
)
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
with progress:
|
|
253
|
+
await asyncio.gather(*tasks)
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def download_email(
|
|
257
|
+
emails: Annotated[
|
|
258
|
+
list[Path],
|
|
259
|
+
Parameter(
|
|
260
|
+
help="The path to the email files",
|
|
261
|
+
),
|
|
262
|
+
],
|
|
263
|
+
/,
|
|
264
|
+
*,
|
|
265
|
+
out_dir: Annotated[
|
|
266
|
+
Path,
|
|
267
|
+
Parameter(
|
|
268
|
+
name=["--out", "-o"],
|
|
269
|
+
help="Output directory",
|
|
270
|
+
),
|
|
271
|
+
] = Path("./downloads"),
|
|
272
|
+
key: Annotated[
|
|
273
|
+
str,
|
|
274
|
+
Parameter(
|
|
275
|
+
name=["--key", "-k"],
|
|
276
|
+
help=f"The key to access the sandbox **{'**,**'.join(x.name for x in settings.sandbox_keys)}**",
|
|
277
|
+
validator=validate_key,
|
|
278
|
+
group="Sandbox",
|
|
279
|
+
),
|
|
280
|
+
] = settings.sandbox_keys[0].name,
|
|
281
|
+
) -> None:
|
|
282
|
+
"""
|
|
283
|
+
Upload an email and get its headers.
|
|
284
|
+
"""
|
|
285
|
+
|
|
286
|
+
# some path prepartions
|
|
287
|
+
out_dir.mkdir(exist_ok=True, parents=True)
|
|
288
|
+
out_dir = out_dir.expanduser().resolve()
|
|
289
|
+
|
|
290
|
+
async def _func(out_dir: Path) -> None:
|
|
291
|
+
sandbox = Sandbox(get_key_by_name(key))
|
|
292
|
+
|
|
293
|
+
async def _internal(email: Path, out_dir: Path) -> None:
|
|
294
|
+
async with aiofiles.open(out_dir / f"{email}.headers", "wb") as fd:
|
|
295
|
+
async for chunk in sandbox.get_email_headers(email):
|
|
296
|
+
await fd.write(chunk)
|
|
297
|
+
|
|
298
|
+
# small validation
|
|
299
|
+
files: list[Path] = []
|
|
300
|
+
for email in emails:
|
|
301
|
+
if not email.exists():
|
|
302
|
+
console.warning(f"{email} doesn't exists")
|
|
303
|
+
continue
|
|
304
|
+
files.append(email)
|
|
305
|
+
|
|
306
|
+
await asyncio.gather(*(_internal(email, out_dir) for email in files))
|
|
307
|
+
|
|
308
|
+
asyncio.run(_func(out_dir))
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from typing import Annotated
|
|
2
|
+
|
|
3
|
+
from cyclopts import Parameter
|
|
4
|
+
from ptsandbox import Sandbox
|
|
5
|
+
from rich.table import Table
|
|
6
|
+
|
|
7
|
+
from sandbox_cli.console import console
|
|
8
|
+
from sandbox_cli.internal.config import settings
|
|
9
|
+
from sandbox_cli.internal.helpers import get_key_by_name, validate_key
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def get_images(
|
|
13
|
+
*,
|
|
14
|
+
key: Annotated[
|
|
15
|
+
str,
|
|
16
|
+
Parameter(
|
|
17
|
+
name=["--key", "-k"],
|
|
18
|
+
help=f"The key to access the sandbox **{'**,**'.join(x.name for x in settings.sandbox_keys)}**",
|
|
19
|
+
validator=validate_key,
|
|
20
|
+
group="Sandbox",
|
|
21
|
+
),
|
|
22
|
+
] = settings.sandbox_keys[0].name,
|
|
23
|
+
) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Get available images in the sandbox.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
sandbox = Sandbox(get_key_by_name(key))
|
|
29
|
+
images = await sandbox.get_images()
|
|
30
|
+
|
|
31
|
+
table = Table()
|
|
32
|
+
table.add_column("Name")
|
|
33
|
+
table.add_column("Image ID", style="turquoise2")
|
|
34
|
+
table.add_column("Version")
|
|
35
|
+
table.add_column("Product version")
|
|
36
|
+
table.add_column("Locale")
|
|
37
|
+
|
|
38
|
+
for image in images:
|
|
39
|
+
if not image.os:
|
|
40
|
+
console.warning(f"{image.image_id} doesn't contain OS information")
|
|
41
|
+
continue
|
|
42
|
+
|
|
43
|
+
table.add_row(image.os.name, image.image_id, image.os.version, image.version, image.os.locale)
|
|
44
|
+
|
|
45
|
+
console.print(table)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import gzip
|
|
3
|
+
import textwrap
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated, Literal, TypedDict
|
|
6
|
+
|
|
7
|
+
from cyclopts import Parameter
|
|
8
|
+
from ptsandbox.models import SandboxBaseTaskResponse
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
|
|
11
|
+
from sandbox_cli.console import console
|
|
12
|
+
from sandbox_cli.utils.extractors import (
|
|
13
|
+
extract_memory,
|
|
14
|
+
extract_network_from_trace,
|
|
15
|
+
extract_static,
|
|
16
|
+
extract_verdict_from_trace,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class TableData(TypedDict):
|
|
21
|
+
sample: str
|
|
22
|
+
image: str
|
|
23
|
+
sandbox: str
|
|
24
|
+
verdict: str
|
|
25
|
+
static: str
|
|
26
|
+
memory: str
|
|
27
|
+
network: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def generate_report(
|
|
31
|
+
src: Annotated[
|
|
32
|
+
list[Path],
|
|
33
|
+
Parameter(
|
|
34
|
+
help="Folder(s) with sandbox reports (recursive search will be used)",
|
|
35
|
+
),
|
|
36
|
+
],
|
|
37
|
+
/,
|
|
38
|
+
*,
|
|
39
|
+
mode: Annotated[
|
|
40
|
+
Literal["cli", "md"],
|
|
41
|
+
Parameter(
|
|
42
|
+
name=["--mode", "-m"],
|
|
43
|
+
help="Report output format",
|
|
44
|
+
),
|
|
45
|
+
] = "cli",
|
|
46
|
+
latest: Annotated[
|
|
47
|
+
bool,
|
|
48
|
+
Parameter(
|
|
49
|
+
name=["--latest", "-l"],
|
|
50
|
+
help="Reports created in the last 2 hours",
|
|
51
|
+
negative="",
|
|
52
|
+
),
|
|
53
|
+
] = False,
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Generate short report from sandbox scans.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
data: list[TableData] = []
|
|
60
|
+
match mode:
|
|
61
|
+
case "md":
|
|
62
|
+
delimeter = " "
|
|
63
|
+
case _:
|
|
64
|
+
delimeter = "\n"
|
|
65
|
+
|
|
66
|
+
for directory in src:
|
|
67
|
+
for root in directory.rglob("*"):
|
|
68
|
+
if not root.is_dir():
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
report_file = root / "report.json"
|
|
72
|
+
if not report_file.exists():
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
if latest and (
|
|
76
|
+
datetime.datetime.now() - datetime.datetime.fromtimestamp(report_file.stat().st_mtime)
|
|
77
|
+
) > datetime.timedelta(hours=2):
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
with open(report_file, encoding="utf-8") as fd:
|
|
81
|
+
report_data = fd.read()
|
|
82
|
+
scan_data = SandboxBaseTaskResponse.model_validate_json(report_data)
|
|
83
|
+
|
|
84
|
+
if (report := scan_data.get_long_report()) is None:
|
|
85
|
+
console.warning(f"A report without behavioral analysis: {root}")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
corr_trace_path = root / "events-correlated.log.gz"
|
|
89
|
+
if not corr_trace_path.exists():
|
|
90
|
+
corr_trace_path = root / "raw" / "events-correlated.log.gz"
|
|
91
|
+
|
|
92
|
+
if not corr_trace_path.exists():
|
|
93
|
+
console.error(f"Can't find events-correlated.log.gz: {root}")
|
|
94
|
+
continue
|
|
95
|
+
|
|
96
|
+
corr_trace = gzip.open(corr_trace_path, "rb").read()
|
|
97
|
+
image = root.name
|
|
98
|
+
try:
|
|
99
|
+
image = report.artifacts[0].find_sandbox_result().details.sandbox.image.image_id # type: ignore
|
|
100
|
+
finally:
|
|
101
|
+
if image is None:
|
|
102
|
+
image = root.name
|
|
103
|
+
|
|
104
|
+
data.append(
|
|
105
|
+
{
|
|
106
|
+
"sample": report.artifacts[0].file_info.file_path, # type: ignore
|
|
107
|
+
"image": image,
|
|
108
|
+
"verdict": delimeter.join(extract_verdict_from_trace(corr_trace)),
|
|
109
|
+
"static": delimeter.join(extract_static(report)),
|
|
110
|
+
"memory": delimeter.join(extract_memory(report)),
|
|
111
|
+
"network": delimeter.join(extract_network_from_trace(corr_trace)),
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
match mode:
|
|
116
|
+
case "md":
|
|
117
|
+
md_report_head = textwrap.dedent(
|
|
118
|
+
"""
|
|
119
|
+
| Sample | Image | Verdict | Static | Memory | Network |
|
|
120
|
+
| --- | --- | --- | --- | --- | --- |"""
|
|
121
|
+
).strip()
|
|
122
|
+
|
|
123
|
+
md_report_base = "|{sample}|{image}|{verdict}|{static}|{memory}|{network}|"
|
|
124
|
+
table_md = md_report_head + "\n"
|
|
125
|
+
table_md += "\n".join(md_report_base.format(**d) for d in data)
|
|
126
|
+
print(table_md)
|
|
127
|
+
case "cli":
|
|
128
|
+
table = Table(highlight=True, show_lines=True)
|
|
129
|
+
table.add_column("File", overflow="fold")
|
|
130
|
+
table.add_column("Image", overflow="fold")
|
|
131
|
+
table.add_column("Verdict", overflow="fold", style="bold")
|
|
132
|
+
table.add_column("Static", overflow="fold", style="bold")
|
|
133
|
+
table.add_column("Memory", overflow="fold")
|
|
134
|
+
table.add_column("Network", overflow="fold")
|
|
135
|
+
for d in data:
|
|
136
|
+
table.add_row(
|
|
137
|
+
d["sample"],
|
|
138
|
+
d["image"],
|
|
139
|
+
d["verdict"],
|
|
140
|
+
d["static"],
|
|
141
|
+
d["memory"],
|
|
142
|
+
d["network"],
|
|
143
|
+
)
|
|
144
|
+
console.print(table)
|
|
145
|
+
case _:
|
|
146
|
+
pass
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
from cyclopts import App, Parameter, validators
|
|
5
|
+
|
|
6
|
+
from sandbox_cli.console import console
|
|
7
|
+
from sandbox_cli.utils.compiler import compile_rules_internal, test_rules_internal
|
|
8
|
+
|
|
9
|
+
rules = App(
|
|
10
|
+
name="rules",
|
|
11
|
+
help="Working with raw sandbox rules.",
|
|
12
|
+
help_format="markdown",
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@rules.command(name="compile")
|
|
17
|
+
async def compile_rules(
|
|
18
|
+
*,
|
|
19
|
+
rules: Annotated[
|
|
20
|
+
Path,
|
|
21
|
+
Parameter(
|
|
22
|
+
name=["--rules", "-r"],
|
|
23
|
+
help="The path to the folder with the rules",
|
|
24
|
+
validator=validators.Path(exists=True),
|
|
25
|
+
),
|
|
26
|
+
],
|
|
27
|
+
out: Annotated[
|
|
28
|
+
Path,
|
|
29
|
+
Parameter(
|
|
30
|
+
name=["--out", "-o"],
|
|
31
|
+
help="The path where to save the compiled rules",
|
|
32
|
+
required=False,
|
|
33
|
+
),
|
|
34
|
+
] = Path("compiled-rules.local.tmp"),
|
|
35
|
+
is_local: Annotated[
|
|
36
|
+
bool,
|
|
37
|
+
Parameter(
|
|
38
|
+
name=["--local", "-l"],
|
|
39
|
+
negative="",
|
|
40
|
+
help="The rules will be compiled locally using Docker (unix only)",
|
|
41
|
+
),
|
|
42
|
+
] = False,
|
|
43
|
+
) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Get compiled rules for working with third-party services.
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
out.mkdir(exist_ok=True, parents=True)
|
|
49
|
+
out = out.expanduser().resolve()
|
|
50
|
+
|
|
51
|
+
with console.status(f"{console.INFO} Waiting for the rules to be compiled"):
|
|
52
|
+
data = await compile_rules_internal(rules_dir=rules, is_local=is_local, compiled_rules_dir=out)
|
|
53
|
+
(out / "compiled-rules.local.tar.gz").write_bytes(data)
|
|
54
|
+
|
|
55
|
+
console.info(f"Rules saved to {out / 'compiled-rules.local.tar.gz'}")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@rules.command(name="test")
|
|
59
|
+
async def test_rules(
|
|
60
|
+
*,
|
|
61
|
+
rules: Annotated[
|
|
62
|
+
Path,
|
|
63
|
+
Parameter(
|
|
64
|
+
name=["--rules", "-r"],
|
|
65
|
+
help="The path to the folder with the rules",
|
|
66
|
+
validator=validators.Path(exists=True),
|
|
67
|
+
),
|
|
68
|
+
],
|
|
69
|
+
is_local: Annotated[
|
|
70
|
+
bool,
|
|
71
|
+
Parameter(
|
|
72
|
+
name=["--local", "-l"],
|
|
73
|
+
negative="",
|
|
74
|
+
help="The rules will be compiled locally using Docker (unix only)",
|
|
75
|
+
),
|
|
76
|
+
] = False,
|
|
77
|
+
) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Testing written rules.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
with console.status(f"{console.INFO} Testing rules"):
|
|
83
|
+
await test_rules_internal(rules=rules, is_local=is_local)
|