asp-cli 0.1.0__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.
- asp_cli/__init__.py +1 -0
- asp_cli/api_client.py +118 -0
- asp_cli/config.py +176 -0
- asp_cli/errors.py +26 -0
- asp_cli/main.py +1461 -0
- asp_cli/output.py +66 -0
- asp_cli/py.typed +0 -0
- asp_cli/spec/__init__.py +0 -0
- asp_cli/spec/operations.json +404 -0
- asp_cli-0.1.0.dist-info/METADATA +54 -0
- asp_cli-0.1.0.dist-info/RECORD +14 -0
- asp_cli-0.1.0.dist-info/WHEEL +4 -0
- asp_cli-0.1.0.dist-info/entry_points.txt +2 -0
- asp_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
asp_cli/main.py
ADDED
|
@@ -0,0 +1,1461 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Annotated
|
|
8
|
+
from urllib.parse import urlencode
|
|
9
|
+
|
|
10
|
+
if __package__ in {None, ""}:
|
|
11
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
|
12
|
+
__package__ = "asp_cli"
|
|
13
|
+
|
|
14
|
+
import httpx
|
|
15
|
+
import jmespath
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from . import __version__
|
|
21
|
+
from .config import (
|
|
22
|
+
clear_auth,
|
|
23
|
+
get_config_value,
|
|
24
|
+
read_settings,
|
|
25
|
+
redact_secret,
|
|
26
|
+
resolve_config,
|
|
27
|
+
save_auth,
|
|
28
|
+
set_config_value,
|
|
29
|
+
)
|
|
30
|
+
from .errors import CliError, EXIT_AUTH, EXIT_CONFIG, EXIT_NETWORK, EXIT_USAGE
|
|
31
|
+
from .api_client import AspClient
|
|
32
|
+
from .output import OutputFormat, emit_error, emit_success, key_value_table
|
|
33
|
+
|
|
34
|
+
console = Console()
|
|
35
|
+
err_console = Console(stderr=True)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class RuntimeOptions:
|
|
40
|
+
api_url: str | None
|
|
41
|
+
api_key: str | None
|
|
42
|
+
output: OutputFormat
|
|
43
|
+
query: str | None
|
|
44
|
+
verbose: bool
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
app = typer.Typer(no_args_is_help=True, help="ASP command line client.")
|
|
48
|
+
auth_app = typer.Typer(no_args_is_help=True, help="Authenticate and inspect the current ASP session.")
|
|
49
|
+
config_app = typer.Typer(no_args_is_help=True, help="Read and write ASP CLI settings.")
|
|
50
|
+
completion_app = typer.Typer(no_args_is_help=True, help="Show shell completion installation commands.")
|
|
51
|
+
case_app = typer.Typer(no_args_is_help=True, help="List, show, and update ASP cases.")
|
|
52
|
+
alert_app = typer.Typer(no_args_is_help=True, help="List and show ASP alerts.")
|
|
53
|
+
artifact_app = typer.Typer(no_args_is_help=True, help="List and show ASP artifacts.")
|
|
54
|
+
knowledge_app = typer.Typer(no_args_is_help=True, help="Search, show, and update ASP knowledge.")
|
|
55
|
+
comment_app = typer.Typer(no_args_is_help=True, help="List and add ASP comments.")
|
|
56
|
+
file_app = typer.Typer(no_args_is_help=True, help="Upload, inspect, download, and read ASP files.")
|
|
57
|
+
enrichment_app = typer.Typer(no_args_is_help=True, help="Create ASP enrichments.")
|
|
58
|
+
playbook_app = typer.Typer(no_args_is_help=True, help="List and run ASP playbooks.")
|
|
59
|
+
playbook_template_app = typer.Typer(no_args_is_help=True, help="List playbook templates.")
|
|
60
|
+
siem_app = typer.Typer(no_args_is_help=True, help="Search and query ASP SIEM integrations.")
|
|
61
|
+
siem_schema_app = typer.Typer(no_args_is_help=True, help="Explore SIEM schema metadata.")
|
|
62
|
+
siem_search_app = typer.Typer(no_args_is_help=True, help="Run SIEM search workflows.")
|
|
63
|
+
siem_query_app = typer.Typer(no_args_is_help=True, help="Run structured or raw SIEM queries.")
|
|
64
|
+
siem_fields_app = typer.Typer(no_args_is_help=True, help="Discover live SIEM fields.")
|
|
65
|
+
ti_app = typer.Typer(no_args_is_help=True, help="Query threat intelligence providers.")
|
|
66
|
+
cmdb_app = typer.Typer(no_args_is_help=True, help="Look up asset context from CMDB providers.")
|
|
67
|
+
dev_app = typer.Typer(no_args_is_help=True, help="Advanced developer and debugging commands.")
|
|
68
|
+
dev_stream_app = typer.Typer(no_args_is_help=True, help="Inspect Redis streams.")
|
|
69
|
+
|
|
70
|
+
app.add_typer(auth_app, name="auth")
|
|
71
|
+
app.add_typer(config_app, name="config")
|
|
72
|
+
app.add_typer(completion_app, name="completion")
|
|
73
|
+
app.add_typer(case_app, name="case")
|
|
74
|
+
app.add_typer(alert_app, name="alert")
|
|
75
|
+
app.add_typer(artifact_app, name="artifact")
|
|
76
|
+
app.add_typer(knowledge_app, name="knowledge")
|
|
77
|
+
app.add_typer(comment_app, name="comment")
|
|
78
|
+
app.add_typer(file_app, name="file")
|
|
79
|
+
app.add_typer(enrichment_app, name="enrichment")
|
|
80
|
+
playbook_app.add_typer(playbook_template_app, name="template")
|
|
81
|
+
app.add_typer(playbook_app, name="playbook")
|
|
82
|
+
siem_app.add_typer(siem_schema_app, name="schema")
|
|
83
|
+
siem_app.add_typer(siem_search_app, name="search")
|
|
84
|
+
siem_app.add_typer(siem_query_app, name="query")
|
|
85
|
+
siem_app.add_typer(siem_fields_app, name="fields")
|
|
86
|
+
app.add_typer(siem_app, name="siem")
|
|
87
|
+
app.add_typer(ti_app, name="ti")
|
|
88
|
+
app.add_typer(cmdb_app, name="cmdb")
|
|
89
|
+
dev_app.add_typer(dev_stream_app, name="stream")
|
|
90
|
+
app.add_typer(dev_app, name="dev")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@app.callback()
|
|
94
|
+
def main(
|
|
95
|
+
ctx: typer.Context,
|
|
96
|
+
version: Annotated[bool, typer.Option("--version", help="Show CLI version and exit.")] = False,
|
|
97
|
+
api_url: Annotated[str | None, typer.Option("--api-url", help="Temporarily override the ASP base URL.")] = None,
|
|
98
|
+
api_key: Annotated[str | None, typer.Option("--api-key", help="Temporarily override the ASP API key.")] = None,
|
|
99
|
+
output: Annotated[OutputFormat, typer.Option("--output", help="Output format.")] = OutputFormat.human,
|
|
100
|
+
query: Annotated[str | None, typer.Option("--query", help="JMESPath query applied to JSON data.")] = None,
|
|
101
|
+
verbose: Annotated[bool, typer.Option("--verbose", help="Show redacted request diagnostics.")] = False,
|
|
102
|
+
) -> None:
|
|
103
|
+
if version:
|
|
104
|
+
console.print(__version__)
|
|
105
|
+
raise typer.Exit()
|
|
106
|
+
ctx.obj = RuntimeOptions(api_url=api_url, api_key=api_key, output=output, query=query, verbose=verbose)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@auth_app.command("login")
|
|
110
|
+
def auth_login(
|
|
111
|
+
ctx: typer.Context,
|
|
112
|
+
api_url: Annotated[str, typer.Option("--api-url", help="ASP base URL, for example https://asp.example.com.")],
|
|
113
|
+
api_key: Annotated[str, typer.Option("--api-key", help="ASP user API key.")],
|
|
114
|
+
local: Annotated[bool, typer.Option("--local", help="Write .asp/settings.json in the current directory.")] = False,
|
|
115
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
116
|
+
) -> None:
|
|
117
|
+
run_command(ctx, "auth.login", output, lambda runtime, out: _auth_login(runtime, out, api_url, api_key, local))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@auth_app.command("status")
|
|
121
|
+
def auth_status(
|
|
122
|
+
ctx: typer.Context,
|
|
123
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
124
|
+
) -> None:
|
|
125
|
+
run_command(ctx, "auth.status", output, _auth_status)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@auth_app.command("logout")
|
|
129
|
+
def auth_logout(
|
|
130
|
+
ctx: typer.Context,
|
|
131
|
+
local: Annotated[bool, typer.Option("--local", help="Remove auth from local .asp/settings.json instead of global settings.")] = False,
|
|
132
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
133
|
+
) -> None:
|
|
134
|
+
run_command(ctx, "auth.logout", output, lambda runtime, out: _auth_logout(runtime, out, local))
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@config_app.command("list")
|
|
138
|
+
def config_list(
|
|
139
|
+
ctx: typer.Context,
|
|
140
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
141
|
+
) -> None:
|
|
142
|
+
run_command(ctx, "config.list", output, _config_list)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
@config_app.command("get")
|
|
146
|
+
def config_get(
|
|
147
|
+
ctx: typer.Context,
|
|
148
|
+
key: Annotated[str, typer.Argument(help="Config key: api_url or api_key.")],
|
|
149
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
run_command(ctx, "config.get", output, lambda runtime, out: _config_get(runtime, out, key))
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
@config_app.command("set")
|
|
155
|
+
def config_set(
|
|
156
|
+
ctx: typer.Context,
|
|
157
|
+
key: Annotated[str, typer.Argument(help="Config key: api_url or api_key.")],
|
|
158
|
+
value: Annotated[str, typer.Argument(help="Config value.")],
|
|
159
|
+
local: Annotated[bool, typer.Option("--local", help="Write local .asp/settings.json instead of global settings.")] = False,
|
|
160
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
161
|
+
) -> None:
|
|
162
|
+
run_command(ctx, "config.set", output, lambda runtime, out: _config_set(runtime, out, key, value, local))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command("doctor")
|
|
166
|
+
def doctor(
|
|
167
|
+
ctx: typer.Context,
|
|
168
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
169
|
+
) -> None:
|
|
170
|
+
run_command(ctx, "doctor", output, _doctor)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@case_app.command("list")
|
|
174
|
+
def case_list(
|
|
175
|
+
ctx: typer.Context,
|
|
176
|
+
status: Annotated[str | None, typer.Option("--status", help="Case status filter. Repeat values with commas.")] = None,
|
|
177
|
+
severity: Annotated[str | None, typer.Option("--severity", help="Case severity filter. Repeat values with commas.")] = None,
|
|
178
|
+
confidence: Annotated[str | None, typer.Option("--confidence", help="Case confidence filter. Repeat values with commas.")] = None,
|
|
179
|
+
verdict: Annotated[str | None, typer.Option("--verdict", help="Case verdict filter. Repeat values with commas.")] = None,
|
|
180
|
+
correlation_uid: Annotated[str | None, typer.Option("--correlation-uid", help="Case correlation UID.")] = None,
|
|
181
|
+
title: Annotated[str | None, typer.Option("--title", help="Title substring filter.")] = None,
|
|
182
|
+
tags: Annotated[str | None, typer.Option("--tags", help="Comma-separated tag filters.")] = None,
|
|
183
|
+
include_related: Annotated[bool, typer.Option("--include-related", help="Include related alerts.")] = False,
|
|
184
|
+
cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor.")] = None,
|
|
185
|
+
page_size: Annotated[int | None, typer.Option("--page-size", min=1, max=100, help="Page size.")] = None,
|
|
186
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
187
|
+
) -> None:
|
|
188
|
+
run_command(
|
|
189
|
+
ctx,
|
|
190
|
+
"case.list",
|
|
191
|
+
output,
|
|
192
|
+
lambda runtime, out: _list_cases(
|
|
193
|
+
runtime,
|
|
194
|
+
out,
|
|
195
|
+
status=status,
|
|
196
|
+
severity=severity,
|
|
197
|
+
confidence=confidence,
|
|
198
|
+
verdict=verdict,
|
|
199
|
+
correlation_uid=correlation_uid,
|
|
200
|
+
title=title,
|
|
201
|
+
tags=tags,
|
|
202
|
+
include_related=include_related,
|
|
203
|
+
cursor=cursor,
|
|
204
|
+
page_size=page_size,
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@case_app.command("show")
|
|
210
|
+
def case_show(
|
|
211
|
+
ctx: typer.Context,
|
|
212
|
+
case_id: Annotated[str, typer.Argument(help="Case ID, for example case_000001.")],
|
|
213
|
+
include_related: Annotated[bool, typer.Option("--include-related/--no-include-related", help="Include related alerts.")] = True,
|
|
214
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
215
|
+
) -> None:
|
|
216
|
+
run_command(ctx, "case.show", output, lambda runtime, out: _show_case(runtime, out, case_id, include_related))
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@case_app.command("update-ai")
|
|
220
|
+
def case_update_ai(
|
|
221
|
+
ctx: typer.Context,
|
|
222
|
+
case_id: Annotated[str, typer.Argument(help="Case ID, for example case_000001.")],
|
|
223
|
+
severity_ai: Annotated[str | None, typer.Option("--severity-ai", help="AI-assessed severity.")] = None,
|
|
224
|
+
confidence_ai: Annotated[str | None, typer.Option("--confidence-ai", help="AI-assessed confidence.")] = None,
|
|
225
|
+
impact_ai: Annotated[str | None, typer.Option("--impact-ai", help="AI-assessed impact.")] = None,
|
|
226
|
+
priority_ai: Annotated[str | None, typer.Option("--priority-ai", help="AI-assessed priority.")] = None,
|
|
227
|
+
verdict_ai: Annotated[str | None, typer.Option("--verdict-ai", help="AI-assessed verdict.")] = None,
|
|
228
|
+
summary: Annotated[str | None, typer.Option("--summary", help="Case summary.")] = None,
|
|
229
|
+
summary_file: Annotated[Path | None, typer.Option("--summary-file", help="Read summary from file.")] = None,
|
|
230
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
231
|
+
) -> None:
|
|
232
|
+
run_command(
|
|
233
|
+
ctx,
|
|
234
|
+
"case.update_ai",
|
|
235
|
+
output,
|
|
236
|
+
lambda runtime, out: _update_case_ai(
|
|
237
|
+
runtime,
|
|
238
|
+
out,
|
|
239
|
+
case_id,
|
|
240
|
+
severity_ai=severity_ai,
|
|
241
|
+
confidence_ai=confidence_ai,
|
|
242
|
+
impact_ai=impact_ai,
|
|
243
|
+
priority_ai=priority_ai,
|
|
244
|
+
verdict_ai=verdict_ai,
|
|
245
|
+
summary=summary,
|
|
246
|
+
summary_file=summary_file,
|
|
247
|
+
),
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
@alert_app.command("list")
|
|
252
|
+
def alert_list(
|
|
253
|
+
ctx: typer.Context,
|
|
254
|
+
status: Annotated[str | None, typer.Option("--status", help="Alert status filter. Repeat values with commas.")] = None,
|
|
255
|
+
severity: Annotated[str | None, typer.Option("--severity", help="Alert severity filter. Repeat values with commas.")] = None,
|
|
256
|
+
confidence: Annotated[str | None, typer.Option("--confidence", help="Alert confidence filter. Repeat values with commas.")] = None,
|
|
257
|
+
case_id: Annotated[str | None, typer.Option("--case-id", help="Linked case ID.")] = None,
|
|
258
|
+
correlation_uid: Annotated[str | None, typer.Option("--correlation-uid", help="Correlation UID.")] = None,
|
|
259
|
+
include_related: Annotated[bool, typer.Option("--include-related", help="Include related artifacts.")] = False,
|
|
260
|
+
cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor.")] = None,
|
|
261
|
+
page_size: Annotated[int | None, typer.Option("--page-size", min=1, max=100, help="Page size.")] = None,
|
|
262
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
263
|
+
) -> None:
|
|
264
|
+
run_command(
|
|
265
|
+
ctx,
|
|
266
|
+
"alert.list",
|
|
267
|
+
output,
|
|
268
|
+
lambda runtime, out: _list_alerts(
|
|
269
|
+
runtime,
|
|
270
|
+
out,
|
|
271
|
+
status=status,
|
|
272
|
+
severity=severity,
|
|
273
|
+
confidence=confidence,
|
|
274
|
+
case_id=case_id,
|
|
275
|
+
correlation_uid=correlation_uid,
|
|
276
|
+
include_related=include_related,
|
|
277
|
+
cursor=cursor,
|
|
278
|
+
page_size=page_size,
|
|
279
|
+
),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
@alert_app.command("show")
|
|
284
|
+
def alert_show(
|
|
285
|
+
ctx: typer.Context,
|
|
286
|
+
alert_id: Annotated[str, typer.Argument(help="Alert ID, for example alert_000001.")],
|
|
287
|
+
include_related: Annotated[bool, typer.Option("--include-related/--no-include-related", help="Include related artifacts.")] = True,
|
|
288
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
289
|
+
) -> None:
|
|
290
|
+
run_command(ctx, "alert.show", output, lambda runtime, out: _show_alert(runtime, out, alert_id, include_related))
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
@artifact_app.command("list")
|
|
294
|
+
def artifact_list(
|
|
295
|
+
ctx: typer.Context,
|
|
296
|
+
type: Annotated[str | None, typer.Option("--type", help="Artifact type filter. Repeat values with commas.")] = None,
|
|
297
|
+
role: Annotated[str | None, typer.Option("--role", help="Artifact role filter. Repeat values with commas.")] = None,
|
|
298
|
+
value: Annotated[str | None, typer.Option("--value", help="Exact artifact value.")] = None,
|
|
299
|
+
include_related: Annotated[bool, typer.Option("--include-related", help="Include related alerts.")] = False,
|
|
300
|
+
cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor.")] = None,
|
|
301
|
+
page_size: Annotated[int | None, typer.Option("--page-size", min=1, max=100, help="Page size.")] = None,
|
|
302
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
303
|
+
) -> None:
|
|
304
|
+
run_command(
|
|
305
|
+
ctx,
|
|
306
|
+
"artifact.list",
|
|
307
|
+
output,
|
|
308
|
+
lambda runtime, out: _list_artifacts(
|
|
309
|
+
runtime,
|
|
310
|
+
out,
|
|
311
|
+
type=type,
|
|
312
|
+
role=role,
|
|
313
|
+
value=value,
|
|
314
|
+
include_related=include_related,
|
|
315
|
+
cursor=cursor,
|
|
316
|
+
page_size=page_size,
|
|
317
|
+
),
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@artifact_app.command("show")
|
|
322
|
+
def artifact_show(
|
|
323
|
+
ctx: typer.Context,
|
|
324
|
+
artifact_id: Annotated[str, typer.Argument(help="Artifact ID, for example artifact_000001.")],
|
|
325
|
+
include_related: Annotated[bool, typer.Option("--include-related/--no-include-related", help="Include related alerts.")] = True,
|
|
326
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
327
|
+
) -> None:
|
|
328
|
+
run_command(ctx, "artifact.show", output, lambda runtime, out: _show_artifact(runtime, out, artifact_id, include_related))
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@knowledge_app.command("search")
|
|
332
|
+
def knowledge_search(
|
|
333
|
+
ctx: typer.Context,
|
|
334
|
+
keyword: Annotated[str | None, typer.Argument(help="Keyword to search in title, body, or tags.")] = None,
|
|
335
|
+
source: Annotated[str | None, typer.Option("--source", help="Knowledge source filter.")] = None,
|
|
336
|
+
case_id: Annotated[str | None, typer.Option("--case-id", help="Linked case ID.")] = None,
|
|
337
|
+
tags: Annotated[str | None, typer.Option("--tags", help="Comma-separated tag filters.")] = None,
|
|
338
|
+
cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor.")] = None,
|
|
339
|
+
page_size: Annotated[int | None, typer.Option("--page-size", min=1, max=100, help="Page size.")] = None,
|
|
340
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
341
|
+
) -> None:
|
|
342
|
+
run_command(
|
|
343
|
+
ctx,
|
|
344
|
+
"knowledge.search",
|
|
345
|
+
output,
|
|
346
|
+
lambda runtime, out: _search_knowledge(
|
|
347
|
+
runtime,
|
|
348
|
+
out,
|
|
349
|
+
keyword=keyword,
|
|
350
|
+
source=source,
|
|
351
|
+
case_id=case_id,
|
|
352
|
+
tags=tags,
|
|
353
|
+
cursor=cursor,
|
|
354
|
+
page_size=page_size,
|
|
355
|
+
),
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
@knowledge_app.command("show")
|
|
360
|
+
def knowledge_show(
|
|
361
|
+
ctx: typer.Context,
|
|
362
|
+
knowledge_id: Annotated[str, typer.Argument(help="Knowledge ID, for example knowledge_000001.")],
|
|
363
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
364
|
+
) -> None:
|
|
365
|
+
run_command(ctx, "knowledge.show", output, lambda runtime, out: _show_knowledge(runtime, out, knowledge_id))
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
@knowledge_app.command("update")
|
|
369
|
+
def knowledge_update(
|
|
370
|
+
ctx: typer.Context,
|
|
371
|
+
knowledge_id: Annotated[str, typer.Argument(help="Knowledge ID, for example knowledge_000001.")],
|
|
372
|
+
title: Annotated[str | None, typer.Option("--title", help="Knowledge title.")] = None,
|
|
373
|
+
body: Annotated[str | None, typer.Option("--body", help="Knowledge body.")] = None,
|
|
374
|
+
body_file: Annotated[Path | None, typer.Option("--body-file", help="Read body from file.")] = None,
|
|
375
|
+
expires_at: Annotated[str | None, typer.Option("--expires-at", help="ISO 8601 datetime with timezone.")] = None,
|
|
376
|
+
tags: Annotated[str | None, typer.Option("--tags", help="Comma-separated tags.")] = None,
|
|
377
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
378
|
+
) -> None:
|
|
379
|
+
run_command(
|
|
380
|
+
ctx,
|
|
381
|
+
"knowledge.update",
|
|
382
|
+
output,
|
|
383
|
+
lambda runtime, out: _update_knowledge(
|
|
384
|
+
runtime,
|
|
385
|
+
out,
|
|
386
|
+
knowledge_id,
|
|
387
|
+
title=title,
|
|
388
|
+
body=body,
|
|
389
|
+
body_file=body_file,
|
|
390
|
+
expires_at=expires_at,
|
|
391
|
+
tags=tags,
|
|
392
|
+
),
|
|
393
|
+
)
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@comment_app.command("list")
|
|
397
|
+
def comment_list(
|
|
398
|
+
ctx: typer.Context,
|
|
399
|
+
target_id: Annotated[str, typer.Argument(help="Target record ID, for example case_000001.")],
|
|
400
|
+
cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor.")] = None,
|
|
401
|
+
page_size: Annotated[int | None, typer.Option("--page-size", min=1, max=100, help="Page size.")] = None,
|
|
402
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
403
|
+
) -> None:
|
|
404
|
+
run_command(ctx, "comment.list", output, lambda runtime, out: _list_comments(runtime, out, target_id, cursor, page_size))
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
@comment_app.command("add")
|
|
408
|
+
def comment_add(
|
|
409
|
+
ctx: typer.Context,
|
|
410
|
+
target_id: Annotated[str, typer.Argument(help="Target record ID, for example case_000001.")],
|
|
411
|
+
body: Annotated[str | None, typer.Option("--body", help="Comment body.")] = None,
|
|
412
|
+
body_file: Annotated[Path | None, typer.Option("--body-file", help="Read comment body from file.")] = None,
|
|
413
|
+
file_key: Annotated[str | None, typer.Option("--file-key", help="Attachment file_key. Use commas for multiple keys.")] = None,
|
|
414
|
+
parent_id: Annotated[int | None, typer.Option("--parent-id", help="Parent comment ID for replies.")] = None,
|
|
415
|
+
mentions: Annotated[str | None, typer.Option("--mentions", help="Comma-separated usernames or user IDs.")] = None,
|
|
416
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
417
|
+
) -> None:
|
|
418
|
+
run_command(
|
|
419
|
+
ctx,
|
|
420
|
+
"comment.add",
|
|
421
|
+
output,
|
|
422
|
+
lambda runtime, out: _add_comment(
|
|
423
|
+
runtime,
|
|
424
|
+
out,
|
|
425
|
+
target_id,
|
|
426
|
+
body=body,
|
|
427
|
+
body_file=body_file,
|
|
428
|
+
file_key=file_key,
|
|
429
|
+
parent_id=parent_id,
|
|
430
|
+
mentions=mentions,
|
|
431
|
+
),
|
|
432
|
+
)
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
@file_app.command("upload")
|
|
436
|
+
def file_upload(
|
|
437
|
+
ctx: typer.Context,
|
|
438
|
+
path: Annotated[Path, typer.Argument(help="Local file path to upload.")],
|
|
439
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
440
|
+
) -> None:
|
|
441
|
+
run_command(ctx, "file.upload", output, lambda runtime, out: _upload_file(runtime, out, path))
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
@file_app.command("info")
|
|
445
|
+
def file_info(
|
|
446
|
+
ctx: typer.Context,
|
|
447
|
+
file_key: Annotated[str, typer.Argument(help="Attachment file_key.")],
|
|
448
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
449
|
+
) -> None:
|
|
450
|
+
run_command(ctx, "file.info", output, lambda runtime, out: _file_info(runtime, out, file_key))
|
|
451
|
+
|
|
452
|
+
|
|
453
|
+
@file_app.command("download")
|
|
454
|
+
def file_download(
|
|
455
|
+
ctx: typer.Context,
|
|
456
|
+
file_key: Annotated[str, typer.Argument(help="Attachment file_key.")],
|
|
457
|
+
output_path: Annotated[Path | None, typer.Option("--output-path", "-o", help="Local output path.")] = None,
|
|
458
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
459
|
+
) -> None:
|
|
460
|
+
run_command(ctx, "file.download", output, lambda runtime, out: _download_file(runtime, out, file_key, output_path))
|
|
461
|
+
|
|
462
|
+
|
|
463
|
+
@file_app.command("read-text")
|
|
464
|
+
def file_read_text(
|
|
465
|
+
ctx: typer.Context,
|
|
466
|
+
file_key: Annotated[str, typer.Argument(help="Attachment file_key.")],
|
|
467
|
+
max_bytes: Annotated[int, typer.Option("--max-bytes", min=1, max=262144, help="Maximum bytes to read.")] = 65536,
|
|
468
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
469
|
+
) -> None:
|
|
470
|
+
run_command(ctx, "file.read_text", output, lambda runtime, out: _read_file_text(runtime, out, file_key, max_bytes))
|
|
471
|
+
|
|
472
|
+
|
|
473
|
+
@enrichment_app.command("create")
|
|
474
|
+
def enrichment_create(
|
|
475
|
+
ctx: typer.Context,
|
|
476
|
+
target_id: Annotated[str, typer.Argument(help="Target case_, alert_, or artifact_ ID.")],
|
|
477
|
+
name: Annotated[str, typer.Option("--name", help="Enrichment name.")] = "",
|
|
478
|
+
type: Annotated[str, typer.Option("--type", help="Enrichment type.")] = "Other",
|
|
479
|
+
value: Annotated[str, typer.Option("--value", help="Enrichment value.")] = "",
|
|
480
|
+
uid: Annotated[str, typer.Option("--uid", help="Stable external identifier.")] = "",
|
|
481
|
+
desc: Annotated[str, typer.Option("--desc", help="Enrichment summary.")] = "",
|
|
482
|
+
data_json: Annotated[str | None, typer.Option("--data-json", help="JSON object string.")] = None,
|
|
483
|
+
data_file: Annotated[Path | None, typer.Option("--data-file", help="JSON object file.")] = None,
|
|
484
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
485
|
+
) -> None:
|
|
486
|
+
run_command(
|
|
487
|
+
ctx,
|
|
488
|
+
"enrichment.create",
|
|
489
|
+
output,
|
|
490
|
+
lambda runtime, out: _create_enrichment(
|
|
491
|
+
runtime,
|
|
492
|
+
out,
|
|
493
|
+
target_id,
|
|
494
|
+
name=name,
|
|
495
|
+
type=type,
|
|
496
|
+
value=value,
|
|
497
|
+
uid=uid,
|
|
498
|
+
desc=desc,
|
|
499
|
+
data_json=data_json,
|
|
500
|
+
data_file=data_file,
|
|
501
|
+
),
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
|
|
505
|
+
@playbook_template_app.command("list")
|
|
506
|
+
def playbook_template_list(
|
|
507
|
+
ctx: typer.Context,
|
|
508
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
509
|
+
) -> None:
|
|
510
|
+
run_command(ctx, "playbook.template.list", output, _list_playbook_templates)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
@playbook_app.command("list")
|
|
514
|
+
def playbook_list(
|
|
515
|
+
ctx: typer.Context,
|
|
516
|
+
case_id: Annotated[str | None, typer.Option("--case-id", help="Linked case ID.")] = None,
|
|
517
|
+
job_status: Annotated[str | None, typer.Option("--job-status", help="Job status filter. Repeat values with commas.")] = None,
|
|
518
|
+
include_related: Annotated[bool, typer.Option("--include-related", help="Include related case.")] = False,
|
|
519
|
+
cursor: Annotated[str | None, typer.Option("--cursor", help="Pagination cursor.")] = None,
|
|
520
|
+
page_size: Annotated[int | None, typer.Option("--page-size", min=1, max=100, help="Page size.")] = None,
|
|
521
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
522
|
+
) -> None:
|
|
523
|
+
run_command(
|
|
524
|
+
ctx,
|
|
525
|
+
"playbook.list",
|
|
526
|
+
output,
|
|
527
|
+
lambda runtime, out: _list_playbooks(
|
|
528
|
+
runtime,
|
|
529
|
+
out,
|
|
530
|
+
case_id=case_id,
|
|
531
|
+
job_status=job_status,
|
|
532
|
+
include_related=include_related,
|
|
533
|
+
cursor=cursor,
|
|
534
|
+
page_size=page_size,
|
|
535
|
+
),
|
|
536
|
+
)
|
|
537
|
+
|
|
538
|
+
|
|
539
|
+
@playbook_app.command("show")
|
|
540
|
+
def playbook_show(
|
|
541
|
+
ctx: typer.Context,
|
|
542
|
+
playbook_id: Annotated[str, typer.Argument(help="Playbook run ID, for example playbook_000001.")],
|
|
543
|
+
include_related: Annotated[bool, typer.Option("--include-related/--no-include-related", help="Include related case.")] = True,
|
|
544
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
545
|
+
) -> None:
|
|
546
|
+
run_command(ctx, "playbook.show", output, lambda runtime, out: _show_playbook(runtime, out, playbook_id, include_related))
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@playbook_app.command("run")
|
|
550
|
+
def playbook_run(
|
|
551
|
+
ctx: typer.Context,
|
|
552
|
+
name: Annotated[str, typer.Argument(help="Playbook template name.")],
|
|
553
|
+
case_id: Annotated[str, typer.Argument(help="Case ID, for example case_000001.")],
|
|
554
|
+
user_input: Annotated[str | None, typer.Option("--user-input", help="Playbook user input.")] = None,
|
|
555
|
+
user_input_file: Annotated[Path | None, typer.Option("--user-input-file", help="Read user input from file.")] = None,
|
|
556
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
557
|
+
) -> None:
|
|
558
|
+
run_command(
|
|
559
|
+
ctx,
|
|
560
|
+
"playbook.run",
|
|
561
|
+
output,
|
|
562
|
+
lambda runtime, out: _run_playbook(
|
|
563
|
+
runtime,
|
|
564
|
+
out,
|
|
565
|
+
name,
|
|
566
|
+
case_id,
|
|
567
|
+
user_input=user_input,
|
|
568
|
+
user_input_file=user_input_file,
|
|
569
|
+
),
|
|
570
|
+
)
|
|
571
|
+
|
|
572
|
+
|
|
573
|
+
@siem_schema_app.command("list")
|
|
574
|
+
def siem_schema_list(
|
|
575
|
+
ctx: typer.Context,
|
|
576
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
577
|
+
) -> None:
|
|
578
|
+
run_command(ctx, "siem.schema", output, lambda runtime, out: _siem_schema(runtime, out, None))
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
@siem_schema_app.command("show")
|
|
582
|
+
def siem_schema_show(
|
|
583
|
+
ctx: typer.Context,
|
|
584
|
+
target_index: Annotated[str, typer.Argument(help="Registered SIEM index/source name.")],
|
|
585
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
586
|
+
) -> None:
|
|
587
|
+
run_command(ctx, "siem.schema", output, lambda runtime, out: _siem_schema(runtime, out, target_index))
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
@siem_search_app.command("keyword")
|
|
591
|
+
def siem_search_keyword(
|
|
592
|
+
ctx: typer.Context,
|
|
593
|
+
keyword: Annotated[str, typer.Argument(help="Keyword or comma-separated AND keyword list.")],
|
|
594
|
+
time_range_start: Annotated[str, typer.Option("--from", help="ISO 8601 start time with timezone.")],
|
|
595
|
+
time_range_end: Annotated[str, typer.Option("--to", help="ISO 8601 end time with timezone.")],
|
|
596
|
+
time_field: Annotated[str, typer.Option("--time-field", help="Time field.")] = "@timestamp",
|
|
597
|
+
index_name: Annotated[str | None, typer.Option("--index-name", help="Optional target index/source.")] = None,
|
|
598
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
599
|
+
) -> None:
|
|
600
|
+
run_command(
|
|
601
|
+
ctx,
|
|
602
|
+
"siem.search.keyword",
|
|
603
|
+
output,
|
|
604
|
+
lambda runtime, out: _siem_keyword(runtime, out, keyword, time_range_start, time_range_end, time_field, index_name),
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
|
|
608
|
+
@siem_query_app.command("adaptive")
|
|
609
|
+
def siem_query_adaptive(
|
|
610
|
+
ctx: typer.Context,
|
|
611
|
+
index_name: Annotated[str, typer.Argument(help="Target SIEM index/source name.")],
|
|
612
|
+
time_range_start: Annotated[str, typer.Option("--from", help="ISO 8601 start time with timezone.")],
|
|
613
|
+
time_range_end: Annotated[str, typer.Option("--to", help="ISO 8601 end time with timezone.")],
|
|
614
|
+
time_field: Annotated[str, typer.Option("--time-field", help="Time field.")] = "@timestamp",
|
|
615
|
+
filters_json: Annotated[str | None, typer.Option("--filters-json", help="Exact-match filters JSON object.")] = None,
|
|
616
|
+
filters_file: Annotated[Path | None, typer.Option("--filters-file", help="Exact-match filters JSON file.")] = None,
|
|
617
|
+
aggregation_fields: Annotated[str | None, typer.Option("--aggregation-fields", help="Comma-separated aggregation fields.")] = None,
|
|
618
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
619
|
+
) -> None:
|
|
620
|
+
run_command(
|
|
621
|
+
ctx,
|
|
622
|
+
"siem.query.adaptive",
|
|
623
|
+
output,
|
|
624
|
+
lambda runtime, out: _siem_adaptive(runtime, out, index_name, time_range_start, time_range_end, time_field, filters_json, filters_file, aggregation_fields),
|
|
625
|
+
)
|
|
626
|
+
|
|
627
|
+
|
|
628
|
+
@siem_query_app.command("spl")
|
|
629
|
+
def siem_query_spl(
|
|
630
|
+
ctx: typer.Context,
|
|
631
|
+
query: Annotated[str, typer.Argument(help="Raw SPL query.")],
|
|
632
|
+
time_range_start: Annotated[str, typer.Option("--from", help="ISO 8601 start time with timezone.")],
|
|
633
|
+
time_range_end: Annotated[str, typer.Option("--to", help="ISO 8601 end time with timezone.")],
|
|
634
|
+
limit: Annotated[int, typer.Option("--limit", min=1, max=10000, help="Maximum records.")] = 100,
|
|
635
|
+
time_field: Annotated[str, typer.Option("--time-field", help="Time field.")] = "@timestamp",
|
|
636
|
+
index_name: Annotated[str | None, typer.Option("--index-name", help="Optional index/source label.")] = None,
|
|
637
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
638
|
+
) -> None:
|
|
639
|
+
run_command(ctx, "siem.query.spl", output, lambda runtime, out: _siem_raw_query(runtime, out, "spl", query, time_range_start, time_range_end, limit, time_field, index_name))
|
|
640
|
+
|
|
641
|
+
|
|
642
|
+
@siem_query_app.command("esql")
|
|
643
|
+
def siem_query_esql(
|
|
644
|
+
ctx: typer.Context,
|
|
645
|
+
query: Annotated[str, typer.Argument(help="Raw ES|QL query.")],
|
|
646
|
+
time_range_start: Annotated[str, typer.Option("--from", help="ISO 8601 start time with timezone.")],
|
|
647
|
+
time_range_end: Annotated[str, typer.Option("--to", help="ISO 8601 end time with timezone.")],
|
|
648
|
+
limit: Annotated[int, typer.Option("--limit", min=1, max=10000, help="Maximum records.")] = 100,
|
|
649
|
+
time_field: Annotated[str, typer.Option("--time-field", help="Time field.")] = "@timestamp",
|
|
650
|
+
index_name: Annotated[str | None, typer.Option("--index-name", help="Optional index/source label.")] = None,
|
|
651
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
652
|
+
) -> None:
|
|
653
|
+
run_command(ctx, "siem.query.esql", output, lambda runtime, out: _siem_raw_query(runtime, out, "esql", query, time_range_start, time_range_end, limit, time_field, index_name))
|
|
654
|
+
|
|
655
|
+
|
|
656
|
+
@siem_fields_app.command("discover")
|
|
657
|
+
def siem_fields_discover(
|
|
658
|
+
ctx: typer.Context,
|
|
659
|
+
index_name: Annotated[str, typer.Argument(help="Target SIEM index/source name.")],
|
|
660
|
+
backend: Annotated[str, typer.Argument(help="Backend: ELK or Splunk.")],
|
|
661
|
+
time_range_start: Annotated[str, typer.Option("--from", help="ISO 8601 start time with timezone.")],
|
|
662
|
+
time_range_end: Annotated[str, typer.Option("--to", help="ISO 8601 end time with timezone.")],
|
|
663
|
+
doc_limit: Annotated[int, typer.Option("--doc-limit", min=1, max=100000, help="Documents to sample.")] = 10000,
|
|
664
|
+
max_samples_per_field: Annotated[int, typer.Option("--max-samples-per-field", min=1, max=100, help="Samples per field.")] = 20,
|
|
665
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
666
|
+
) -> None:
|
|
667
|
+
run_command(
|
|
668
|
+
ctx,
|
|
669
|
+
"siem.fields.discover",
|
|
670
|
+
output,
|
|
671
|
+
lambda runtime, out: _siem_fields_discover(runtime, out, index_name, backend, time_range_start, time_range_end, doc_limit, max_samples_per_field),
|
|
672
|
+
)
|
|
673
|
+
|
|
674
|
+
|
|
675
|
+
@ti_app.command("query")
|
|
676
|
+
def ti_query(
|
|
677
|
+
ctx: typer.Context,
|
|
678
|
+
indicator: Annotated[str, typer.Argument(help="Indicator value: IP, domain, URL, hash, etc.")],
|
|
679
|
+
artifact_type: Annotated[str, typer.Option("--artifact-type", help="Artifact type hint.")] = "Unknown",
|
|
680
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Optional provider name.")] = None,
|
|
681
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
682
|
+
) -> None:
|
|
683
|
+
run_command(ctx, "ti.query", output, lambda runtime, out: _ti_query(runtime, out, indicator, artifact_type, provider))
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
@cmdb_app.command("lookup")
|
|
687
|
+
def cmdb_lookup(
|
|
688
|
+
ctx: typer.Context,
|
|
689
|
+
artifact_type: Annotated[str, typer.Argument(help="Artifact type.")],
|
|
690
|
+
artifact_value: Annotated[str, typer.Argument(help="Artifact value.")],
|
|
691
|
+
provider: Annotated[str | None, typer.Option("--provider", help="Optional provider name.")] = None,
|
|
692
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
693
|
+
) -> None:
|
|
694
|
+
run_command(ctx, "cmdb.lookup", output, lambda runtime, out: _cmdb_lookup(runtime, out, artifact_type, artifact_value, provider))
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
@dev_stream_app.command("head")
|
|
698
|
+
def dev_stream_head(
|
|
699
|
+
ctx: typer.Context,
|
|
700
|
+
stream_name: Annotated[str, typer.Argument(help="Redis stream name.")],
|
|
701
|
+
n: Annotated[int, typer.Option("-n", min=1, max=100, help="Number of messages.")] = 3,
|
|
702
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
703
|
+
) -> None:
|
|
704
|
+
run_command(ctx, "dev.stream.head", output, lambda runtime, out: _dev_stream_head(runtime, out, stream_name, n))
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
@dev_stream_app.command("read")
|
|
708
|
+
def dev_stream_read(
|
|
709
|
+
ctx: typer.Context,
|
|
710
|
+
stream_name: Annotated[str, typer.Argument(help="Redis stream name.")],
|
|
711
|
+
message_id: Annotated[str, typer.Argument(help="Redis stream message ID.")],
|
|
712
|
+
output: Annotated[OutputFormat | None, typer.Option("--output", help="Output format.")] = None,
|
|
713
|
+
) -> None:
|
|
714
|
+
run_command(ctx, "dev.stream.read", output, lambda runtime, out: _dev_stream_read(runtime, out, stream_name, message_id))
|
|
715
|
+
|
|
716
|
+
|
|
717
|
+
@completion_app.command("powershell")
|
|
718
|
+
def completion_powershell() -> None:
|
|
719
|
+
console.print("Run this command to install PowerShell completion:")
|
|
720
|
+
console.print("asp --install-completion powershell", style="cyan")
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@completion_app.command("bash")
|
|
724
|
+
def completion_bash() -> None:
|
|
725
|
+
console.print("Run this command to install Bash completion:")
|
|
726
|
+
console.print("asp --install-completion bash", style="cyan")
|
|
727
|
+
|
|
728
|
+
|
|
729
|
+
@completion_app.command("zsh")
|
|
730
|
+
def completion_zsh() -> None:
|
|
731
|
+
console.print("Run this command to install Zsh completion:")
|
|
732
|
+
console.print("asp --install-completion zsh", style="cyan")
|
|
733
|
+
|
|
734
|
+
|
|
735
|
+
def run_command(ctx: typer.Context, operation: str, output: OutputFormat | None, handler) -> None:
|
|
736
|
+
runtime = runtime_options(ctx)
|
|
737
|
+
out = output or runtime.output
|
|
738
|
+
try:
|
|
739
|
+
if runtime.query and out != OutputFormat.json:
|
|
740
|
+
raise CliError("query_requires_json", "--query requires --output json", {}, EXIT_USAGE)
|
|
741
|
+
handler(runtime, out)
|
|
742
|
+
except CliError as exc:
|
|
743
|
+
emit_error(err_console if out == OutputFormat.human else console, output=out, error=exc, operation=operation)
|
|
744
|
+
raise typer.Exit(exc.exit_code) from exc
|
|
745
|
+
|
|
746
|
+
|
|
747
|
+
def runtime_options(ctx: typer.Context) -> RuntimeOptions:
|
|
748
|
+
if isinstance(ctx.obj, RuntimeOptions):
|
|
749
|
+
return ctx.obj
|
|
750
|
+
parent = ctx.parent
|
|
751
|
+
while parent is not None:
|
|
752
|
+
if isinstance(parent.obj, RuntimeOptions):
|
|
753
|
+
return parent.obj
|
|
754
|
+
parent = parent.parent
|
|
755
|
+
return RuntimeOptions(api_url=None, api_key=None, output=OutputFormat.human, query=None, verbose=False)
|
|
756
|
+
|
|
757
|
+
|
|
758
|
+
def _auth_login(runtime: RuntimeOptions, output: OutputFormat, api_url: str, api_key: str, local: bool) -> None:
|
|
759
|
+
path = save_auth(api_url=api_url, api_key=api_key, local=local)
|
|
760
|
+
data = {
|
|
761
|
+
"scope": "local" if local else "global",
|
|
762
|
+
"settings_path": str(path),
|
|
763
|
+
"api_url": api_url.rstrip("/"),
|
|
764
|
+
"api_key": redact_secret(api_key),
|
|
765
|
+
}
|
|
766
|
+
if output == OutputFormat.human:
|
|
767
|
+
console.print(key_value_table("ASP auth saved", list(data.items())))
|
|
768
|
+
console.print("Next: run [cyan]asp doctor[/cyan].")
|
|
769
|
+
return
|
|
770
|
+
emit_runtime_success(runtime, output=output, operation="auth.login", data=data)
|
|
771
|
+
|
|
772
|
+
|
|
773
|
+
def _auth_status(runtime: RuntimeOptions, output: OutputFormat) -> None:
|
|
774
|
+
config = _require_config(runtime)
|
|
775
|
+
client = AspClient(api_url=config.api_url or "", api_key=config.api_key, verbose=runtime.verbose, console=err_console)
|
|
776
|
+
payload = client.version()
|
|
777
|
+
data = {
|
|
778
|
+
"api_url": config.api_url,
|
|
779
|
+
"api_url_source": config.sources.get("api_url"),
|
|
780
|
+
"api_key_source": config.sources.get("api_key"),
|
|
781
|
+
"api_key": redact_secret(config.api_key),
|
|
782
|
+
"server": payload.get("data", {}),
|
|
783
|
+
}
|
|
784
|
+
if output == OutputFormat.human:
|
|
785
|
+
server = data["server"]
|
|
786
|
+
user = server.get("user") or {}
|
|
787
|
+
console.print(
|
|
788
|
+
key_value_table(
|
|
789
|
+
"ASP auth status",
|
|
790
|
+
[
|
|
791
|
+
("API URL", data["api_url"]),
|
|
792
|
+
("API URL source", data["api_url_source"]),
|
|
793
|
+
("API key source", data["api_key_source"]),
|
|
794
|
+
("API key", data["api_key"]),
|
|
795
|
+
("User", user.get("username")),
|
|
796
|
+
("Role", user.get("role")),
|
|
797
|
+
("API version", server.get("api_version")),
|
|
798
|
+
],
|
|
799
|
+
)
|
|
800
|
+
)
|
|
801
|
+
return
|
|
802
|
+
emit_runtime_success(runtime, output=output, operation="auth.status", data=data)
|
|
803
|
+
|
|
804
|
+
|
|
805
|
+
def _auth_logout(runtime: RuntimeOptions, output: OutputFormat, local: bool) -> None:
|
|
806
|
+
path = clear_auth(local=local)
|
|
807
|
+
data = {"scope": "local" if local else "global", "settings_path": str(path)}
|
|
808
|
+
if output == OutputFormat.human:
|
|
809
|
+
console.print(key_value_table("ASP auth removed", list(data.items())))
|
|
810
|
+
return
|
|
811
|
+
emit_runtime_success(runtime, output=output, operation="auth.logout", data=data)
|
|
812
|
+
|
|
813
|
+
|
|
814
|
+
def _config_list(runtime: RuntimeOptions, output: OutputFormat) -> None:
|
|
815
|
+
config = resolve_config(api_url=runtime.api_url, api_key=runtime.api_key)
|
|
816
|
+
global_settings = _redacted_settings(read_settings(config.global_path))
|
|
817
|
+
local_settings = _redacted_settings(read_settings(config.local_path) if config.local_path else {})
|
|
818
|
+
data = {
|
|
819
|
+
"global_path": str(config.global_path),
|
|
820
|
+
"local_path": str(config.local_path) if config.local_path else None,
|
|
821
|
+
"global": global_settings,
|
|
822
|
+
"local": local_settings,
|
|
823
|
+
"resolved": {
|
|
824
|
+
"api_url": config.api_url,
|
|
825
|
+
"api_url_source": config.sources.get("api_url"),
|
|
826
|
+
"api_key": redact_secret(config.api_key),
|
|
827
|
+
"api_key_source": config.sources.get("api_key"),
|
|
828
|
+
},
|
|
829
|
+
}
|
|
830
|
+
if output == OutputFormat.human:
|
|
831
|
+
console.print(key_value_table("ASP config", [
|
|
832
|
+
("Global path", data["global_path"]),
|
|
833
|
+
("Local path", data["local_path"]),
|
|
834
|
+
("Resolved API URL", data["resolved"]["api_url"]),
|
|
835
|
+
("API URL source", data["resolved"]["api_url_source"]),
|
|
836
|
+
("Resolved API key", data["resolved"]["api_key"]),
|
|
837
|
+
("API key source", data["resolved"]["api_key_source"]),
|
|
838
|
+
]))
|
|
839
|
+
return
|
|
840
|
+
emit_runtime_success(runtime, output=output, operation="config.list", data=data)
|
|
841
|
+
|
|
842
|
+
|
|
843
|
+
def _config_get(runtime: RuntimeOptions, output: OutputFormat, key: str) -> None:
|
|
844
|
+
value, source = get_config_value(key, api_url=runtime.api_url, api_key=runtime.api_key)
|
|
845
|
+
display_value = redact_secret(value) if key == "api_key" else value
|
|
846
|
+
data = {"key": key, "value": display_value, "source": source}
|
|
847
|
+
if output == OutputFormat.human:
|
|
848
|
+
console.print(key_value_table("ASP config value", list(data.items())))
|
|
849
|
+
return
|
|
850
|
+
emit_runtime_success(runtime, output=output, operation="config.get", data=data)
|
|
851
|
+
|
|
852
|
+
|
|
853
|
+
def _config_set(runtime: RuntimeOptions, output: OutputFormat, key: str, value: str, local: bool) -> None:
|
|
854
|
+
path = set_config_value(key, value, local=local)
|
|
855
|
+
data = {"key": key, "scope": "local" if local else "global", "settings_path": str(path)}
|
|
856
|
+
if output == OutputFormat.human:
|
|
857
|
+
console.print(key_value_table("ASP config saved", list(data.items())))
|
|
858
|
+
return
|
|
859
|
+
emit_runtime_success(runtime, output=output, operation="config.set", data=data)
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
def _doctor(runtime: RuntimeOptions, output: OutputFormat) -> None:
|
|
863
|
+
config = _require_config(runtime)
|
|
864
|
+
client = AspClient(api_url=config.api_url or "", api_key=config.api_key, verbose=runtime.verbose, console=err_console)
|
|
865
|
+
checks = []
|
|
866
|
+
ok = True
|
|
867
|
+
|
|
868
|
+
try:
|
|
869
|
+
health = client.health()
|
|
870
|
+
checks.append({"name": "health", "ok": True, "detail": health.get("status", "ok")})
|
|
871
|
+
except CliError as exc:
|
|
872
|
+
ok = False
|
|
873
|
+
checks.append({"name": "health", "ok": False, "detail": exc.message})
|
|
874
|
+
|
|
875
|
+
version_payload = None
|
|
876
|
+
try:
|
|
877
|
+
version_payload = client.version()
|
|
878
|
+
checks.append({"name": "auth", "ok": True, "detail": "authenticated"})
|
|
879
|
+
checks.append({"name": "version", "ok": True, "detail": version_payload.get("data", {}).get("api_version")})
|
|
880
|
+
except CliError as exc:
|
|
881
|
+
ok = False
|
|
882
|
+
checks.append({"name": "auth", "ok": False, "detail": exc.message})
|
|
883
|
+
|
|
884
|
+
data = {
|
|
885
|
+
"ok": ok,
|
|
886
|
+
"api_url": config.api_url,
|
|
887
|
+
"api_url_source": config.sources.get("api_url"),
|
|
888
|
+
"api_key_source": config.sources.get("api_key"),
|
|
889
|
+
"checks": checks,
|
|
890
|
+
"server": version_payload.get("data") if version_payload else None,
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
if output == OutputFormat.human:
|
|
894
|
+
console.print(key_value_table("ASP doctor", [
|
|
895
|
+
("Overall", "ok" if ok else "failed"),
|
|
896
|
+
("API URL", data["api_url"]),
|
|
897
|
+
("API URL source", data["api_url_source"]),
|
|
898
|
+
("API key source", data["api_key_source"]),
|
|
899
|
+
]))
|
|
900
|
+
for check in checks:
|
|
901
|
+
style = "green" if check["ok"] else "red"
|
|
902
|
+
console.print(f"{check['name']}: {check['detail']}", style=style)
|
|
903
|
+
else:
|
|
904
|
+
emit_runtime_success(runtime, output=output, operation="doctor", data=data)
|
|
905
|
+
|
|
906
|
+
if not ok:
|
|
907
|
+
raise typer.Exit(1)
|
|
908
|
+
|
|
909
|
+
|
|
910
|
+
def _list_cases(runtime: RuntimeOptions, output: OutputFormat, **filters) -> None:
|
|
911
|
+
payload = _agent_get(runtime, "/api/agent/v1/cases/", _clean_params(filters))
|
|
912
|
+
_emit_agent_payload(runtime, output, "case.list", payload, lambda data, meta: _list_table(
|
|
913
|
+
"ASP cases",
|
|
914
|
+
["case_id", "title", "severity", "status", "verdict", "priority", "created_at"],
|
|
915
|
+
data,
|
|
916
|
+
meta,
|
|
917
|
+
))
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
def _show_case(runtime: RuntimeOptions, output: OutputFormat, case_id: str, include_related: bool) -> None:
|
|
921
|
+
payload = _agent_get(runtime, f"/api/agent/v1/cases/{case_id}/", {"include_related": include_related})
|
|
922
|
+
_emit_agent_payload(runtime, output, "case.show", payload, lambda data, _meta: _detail_table(
|
|
923
|
+
"ASP case",
|
|
924
|
+
data,
|
|
925
|
+
["case_id", "title", "severity", "confidence", "impact", "priority", "status", "verdict", "severity_ai", "confidence_ai", "impact_ai", "priority_ai", "verdict_ai", "summary", "correlation_uid", "tags", "created_at"],
|
|
926
|
+
))
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
def _update_case_ai(runtime: RuntimeOptions, output: OutputFormat, case_id: str, **fields) -> None:
|
|
930
|
+
summary_file = fields.pop("summary_file")
|
|
931
|
+
if summary_file is not None:
|
|
932
|
+
fields["summary"] = _read_text_file(summary_file)
|
|
933
|
+
body = {key: value for key, value in fields.items() if value is not None}
|
|
934
|
+
if not body:
|
|
935
|
+
raise CliError("missing_update_fields", "At least one AI analysis field is required", {}, EXIT_USAGE)
|
|
936
|
+
payload = _agent_request(runtime, "PATCH", f"/api/agent/v1/cases/{case_id}/ai-analysis/", json=body)
|
|
937
|
+
_emit_agent_payload(runtime, output, "case.update_ai", payload, lambda data, _meta: _detail_table(
|
|
938
|
+
"Updated ASP case AI analysis",
|
|
939
|
+
data,
|
|
940
|
+
["case_id", "severity_ai", "confidence_ai", "impact_ai", "priority_ai", "verdict_ai", "summary"],
|
|
941
|
+
))
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _list_alerts(runtime: RuntimeOptions, output: OutputFormat, **filters) -> None:
|
|
945
|
+
payload = _agent_get(runtime, "/api/agent/v1/alerts/", _clean_params(filters))
|
|
946
|
+
_emit_agent_payload(runtime, output, "alert.list", payload, lambda data, meta: _list_table(
|
|
947
|
+
"ASP alerts",
|
|
948
|
+
["alert_id", "case_id", "title", "severity", "status", "confidence", "created_at"],
|
|
949
|
+
data,
|
|
950
|
+
meta,
|
|
951
|
+
))
|
|
952
|
+
|
|
953
|
+
|
|
954
|
+
def _show_alert(runtime: RuntimeOptions, output: OutputFormat, alert_id: str, include_related: bool) -> None:
|
|
955
|
+
payload = _agent_get(runtime, f"/api/agent/v1/alerts/{alert_id}/", {"include_related": include_related})
|
|
956
|
+
_emit_agent_payload(runtime, output, "alert.show", payload, lambda data, _meta: _detail_table(
|
|
957
|
+
"ASP alert",
|
|
958
|
+
data,
|
|
959
|
+
["alert_id", "case_id", "title", "severity", "confidence", "impact", "status", "correlation_uid", "source_uid", "rule_id", "rule_name", "created_at"],
|
|
960
|
+
))
|
|
961
|
+
|
|
962
|
+
|
|
963
|
+
def _list_artifacts(runtime: RuntimeOptions, output: OutputFormat, **filters) -> None:
|
|
964
|
+
payload = _agent_get(runtime, "/api/agent/v1/artifacts/", _clean_params(filters))
|
|
965
|
+
_emit_agent_payload(runtime, output, "artifact.list", payload, lambda data, meta: _list_table(
|
|
966
|
+
"ASP artifacts",
|
|
967
|
+
["artifact_id", "type", "role", "name", "value", "created_at"],
|
|
968
|
+
data,
|
|
969
|
+
meta,
|
|
970
|
+
))
|
|
971
|
+
|
|
972
|
+
|
|
973
|
+
def _show_artifact(runtime: RuntimeOptions, output: OutputFormat, artifact_id: str, include_related: bool) -> None:
|
|
974
|
+
payload = _agent_get(runtime, f"/api/agent/v1/artifacts/{artifact_id}/", {"include_related": include_related})
|
|
975
|
+
_emit_agent_payload(runtime, output, "artifact.show", payload, lambda data, _meta: _detail_table(
|
|
976
|
+
"ASP artifact",
|
|
977
|
+
data,
|
|
978
|
+
["artifact_id", "type", "role", "name", "value", "created_at"],
|
|
979
|
+
))
|
|
980
|
+
|
|
981
|
+
|
|
982
|
+
def _search_knowledge(runtime: RuntimeOptions, output: OutputFormat, **filters) -> None:
|
|
983
|
+
payload = _agent_get(runtime, "/api/agent/v1/knowledge/", _clean_params(filters))
|
|
984
|
+
_emit_agent_payload(runtime, output, "knowledge.search", payload, lambda data, meta: _list_table(
|
|
985
|
+
"ASP knowledge",
|
|
986
|
+
["knowledge_id", "title", "source", "case_id", "tags", "created_at"],
|
|
987
|
+
data,
|
|
988
|
+
meta,
|
|
989
|
+
))
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def _show_knowledge(runtime: RuntimeOptions, output: OutputFormat, knowledge_id: str) -> None:
|
|
993
|
+
payload = _agent_get(runtime, f"/api/agent/v1/knowledge/{knowledge_id}/", {})
|
|
994
|
+
_emit_agent_payload(runtime, output, "knowledge.show", payload, lambda data, _meta: _detail_table(
|
|
995
|
+
"ASP knowledge",
|
|
996
|
+
data,
|
|
997
|
+
["knowledge_id", "title", "source", "case_id", "tags", "expires_at", "body", "created_at"],
|
|
998
|
+
))
|
|
999
|
+
|
|
1000
|
+
|
|
1001
|
+
def _update_knowledge(runtime: RuntimeOptions, output: OutputFormat, knowledge_id: str, **fields) -> None:
|
|
1002
|
+
body_file = fields.pop("body_file")
|
|
1003
|
+
if body_file is not None:
|
|
1004
|
+
fields["body"] = _read_text_file(body_file)
|
|
1005
|
+
if fields.get("tags") is not None:
|
|
1006
|
+
fields["tags"] = _split_csv(fields["tags"])
|
|
1007
|
+
body = {key: value for key, value in fields.items() if value is not None}
|
|
1008
|
+
if not body:
|
|
1009
|
+
raise CliError("missing_update_fields", "At least one knowledge field is required", {}, EXIT_USAGE)
|
|
1010
|
+
payload = _agent_request(runtime, "PATCH", f"/api/agent/v1/knowledge/{knowledge_id}/", json=body)
|
|
1011
|
+
_emit_agent_payload(runtime, output, "knowledge.update", payload, lambda data, _meta: _detail_table(
|
|
1012
|
+
"Updated ASP knowledge",
|
|
1013
|
+
data,
|
|
1014
|
+
["knowledge_id", "title", "source", "case_id", "tags", "expires_at", "body"],
|
|
1015
|
+
))
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
def _list_comments(runtime: RuntimeOptions, output: OutputFormat, target_id: str, cursor: str | None, page_size: int | None) -> None:
|
|
1019
|
+
payload = _agent_get(runtime, "/api/agent/v1/comments/", _clean_params({
|
|
1020
|
+
"target_id": target_id,
|
|
1021
|
+
"cursor": cursor,
|
|
1022
|
+
"page_size": page_size,
|
|
1023
|
+
}))
|
|
1024
|
+
_emit_agent_payload(runtime, output, "comment.list", payload, lambda data, meta: _list_table(
|
|
1025
|
+
"ASP comments",
|
|
1026
|
+
["id", "author", "body", "parent_id", "created_at"],
|
|
1027
|
+
data,
|
|
1028
|
+
meta,
|
|
1029
|
+
))
|
|
1030
|
+
|
|
1031
|
+
|
|
1032
|
+
def _add_comment(runtime: RuntimeOptions, output: OutputFormat, target_id: str, **fields) -> None:
|
|
1033
|
+
body_file = fields.pop("body_file")
|
|
1034
|
+
if body_file is not None:
|
|
1035
|
+
fields["body"] = _read_text_file(body_file)
|
|
1036
|
+
body = {
|
|
1037
|
+
"target_id": target_id,
|
|
1038
|
+
"body": fields.get("body") or "",
|
|
1039
|
+
"file_keys": _split_csv(fields.get("file_key")),
|
|
1040
|
+
"parent_id": fields.get("parent_id"),
|
|
1041
|
+
"mentions": _split_csv(fields.get("mentions")),
|
|
1042
|
+
}
|
|
1043
|
+
if not body["body"].strip() and not body["file_keys"]:
|
|
1044
|
+
raise CliError("missing_comment_content", "Comment body or file_key is required", {}, EXIT_USAGE)
|
|
1045
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/comments/", json=body)
|
|
1046
|
+
_emit_agent_payload(runtime, output, "comment.add", payload, lambda data, _meta: _detail_table(
|
|
1047
|
+
"ASP comment added",
|
|
1048
|
+
data,
|
|
1049
|
+
["id", "author", "body", "parent_id", "created_at"],
|
|
1050
|
+
))
|
|
1051
|
+
|
|
1052
|
+
|
|
1053
|
+
def _upload_file(runtime: RuntimeOptions, output: OutputFormat, path: Path) -> None:
|
|
1054
|
+
if not path.exists() or not path.is_file():
|
|
1055
|
+
raise CliError("file_not_found", f"File not found: {path}", {"path": str(path)}, EXIT_USAGE)
|
|
1056
|
+
with path.open("rb") as handle:
|
|
1057
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/files/", files={"file": (path.name, handle)})
|
|
1058
|
+
_emit_agent_payload(runtime, output, "file.upload", payload, lambda data, _meta: _detail_table(
|
|
1059
|
+
"ASP file uploaded",
|
|
1060
|
+
data,
|
|
1061
|
+
["file_key", "filename", "size", "content_type", "download_url"],
|
|
1062
|
+
))
|
|
1063
|
+
|
|
1064
|
+
|
|
1065
|
+
def _file_info(runtime: RuntimeOptions, output: OutputFormat, file_key: str) -> None:
|
|
1066
|
+
payload = _agent_get(runtime, f"/api/agent/v1/files/{file_key}/", {})
|
|
1067
|
+
_emit_agent_payload(runtime, output, "file.info", payload, lambda data, _meta: _detail_table(
|
|
1068
|
+
"ASP file",
|
|
1069
|
+
data,
|
|
1070
|
+
["file_key", "filename", "size", "content_type", "download_url", "uploaded_at"],
|
|
1071
|
+
))
|
|
1072
|
+
|
|
1073
|
+
|
|
1074
|
+
def _download_file(runtime: RuntimeOptions, output: OutputFormat, file_key: str, output_path: Path | None) -> None:
|
|
1075
|
+
info = _agent_get(runtime, f"/api/agent/v1/files/{file_key}/", {})
|
|
1076
|
+
data = info.get("data") or {}
|
|
1077
|
+
target_path = output_path or Path(data.get("filename") or file_key)
|
|
1078
|
+
try:
|
|
1079
|
+
response = httpx.get(data["download_url"], timeout=60.0)
|
|
1080
|
+
response.raise_for_status()
|
|
1081
|
+
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1082
|
+
target_path.write_bytes(response.content)
|
|
1083
|
+
except (KeyError, httpx.HTTPError, OSError) as exc:
|
|
1084
|
+
raise CliError("file_download_failed", f"Unable to download file: {file_key}", {"file_key": file_key}, EXIT_NETWORK) from exc
|
|
1085
|
+
result = {**data, "output_path": str(target_path)}
|
|
1086
|
+
if output == OutputFormat.human:
|
|
1087
|
+
console.print(key_value_table("ASP file downloaded", [
|
|
1088
|
+
("file_key", result.get("file_key")),
|
|
1089
|
+
("filename", result.get("filename")),
|
|
1090
|
+
("output_path", result.get("output_path")),
|
|
1091
|
+
("size", result.get("size")),
|
|
1092
|
+
]))
|
|
1093
|
+
return
|
|
1094
|
+
emit_runtime_success(runtime, output=output, operation="file.download", data=result, meta=info.get("meta"))
|
|
1095
|
+
|
|
1096
|
+
|
|
1097
|
+
def _read_file_text(runtime: RuntimeOptions, output: OutputFormat, file_key: str, max_bytes: int) -> None:
|
|
1098
|
+
payload = _agent_get(runtime, f"/api/agent/v1/files/{file_key}/read-text/", {"max_bytes": max_bytes})
|
|
1099
|
+
_emit_agent_payload(runtime, output, "file.read_text", payload, lambda data, _meta: data.get("text", ""))
|
|
1100
|
+
|
|
1101
|
+
|
|
1102
|
+
def _create_enrichment(runtime: RuntimeOptions, output: OutputFormat, target_id: str, **fields) -> None:
|
|
1103
|
+
data_file = fields.pop("data_file")
|
|
1104
|
+
data_json = fields.pop("data_json")
|
|
1105
|
+
body = {"target_id": target_id, **fields}
|
|
1106
|
+
if data_file is not None:
|
|
1107
|
+
body["data"] = _read_json_object_file(data_file)
|
|
1108
|
+
elif data_json is not None:
|
|
1109
|
+
body["data"] = _read_json_object_text(data_json)
|
|
1110
|
+
else:
|
|
1111
|
+
body["data"] = {}
|
|
1112
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/enrichments/", json=body)
|
|
1113
|
+
_emit_agent_payload(runtime, output, "enrichment.create", payload, lambda data, _meta: _detail_table(
|
|
1114
|
+
"ASP enrichment created",
|
|
1115
|
+
data,
|
|
1116
|
+
["enrichment_id", "target_id", "name", "type", "provider", "uid", "value", "desc", "created_at"],
|
|
1117
|
+
))
|
|
1118
|
+
|
|
1119
|
+
|
|
1120
|
+
def _list_playbook_templates(runtime: RuntimeOptions, output: OutputFormat) -> None:
|
|
1121
|
+
payload = _agent_get(runtime, "/api/agent/v1/playbooks/templates/", {})
|
|
1122
|
+
_emit_agent_payload(runtime, output, "playbook.template.list", payload, lambda data, _meta: _list_table(
|
|
1123
|
+
"ASP playbook templates",
|
|
1124
|
+
["name", "description", "tags"],
|
|
1125
|
+
data,
|
|
1126
|
+
))
|
|
1127
|
+
|
|
1128
|
+
|
|
1129
|
+
def _list_playbooks(runtime: RuntimeOptions, output: OutputFormat, **filters) -> None:
|
|
1130
|
+
payload = _agent_get(runtime, "/api/agent/v1/playbooks/", _clean_params(filters))
|
|
1131
|
+
_emit_agent_payload(runtime, output, "playbook.list", payload, lambda data, meta: _list_table(
|
|
1132
|
+
"ASP playbooks",
|
|
1133
|
+
["playbook_id", "case_id", "name", "job_status", "job_id", "created_at"],
|
|
1134
|
+
data,
|
|
1135
|
+
meta,
|
|
1136
|
+
))
|
|
1137
|
+
|
|
1138
|
+
|
|
1139
|
+
def _show_playbook(runtime: RuntimeOptions, output: OutputFormat, playbook_id: str, include_related: bool) -> None:
|
|
1140
|
+
payload = _agent_get(runtime, f"/api/agent/v1/playbooks/{playbook_id}/", {"include_related": include_related})
|
|
1141
|
+
_emit_agent_payload(runtime, output, "playbook.show", payload, lambda data, _meta: _detail_table(
|
|
1142
|
+
"ASP playbook",
|
|
1143
|
+
data,
|
|
1144
|
+
["playbook_id", "case_id", "name", "user_input", "job_status", "job_id", "remark", "created_at"],
|
|
1145
|
+
))
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
def _run_playbook(runtime: RuntimeOptions, output: OutputFormat, name: str, case_id: str, **fields) -> None:
|
|
1149
|
+
user_input_file = fields.get("user_input_file")
|
|
1150
|
+
user_input = _read_text_file(user_input_file) if user_input_file is not None else (fields.get("user_input") or "")
|
|
1151
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/playbooks/run/", json={
|
|
1152
|
+
"name": name,
|
|
1153
|
+
"case_id": case_id,
|
|
1154
|
+
"user_input": user_input,
|
|
1155
|
+
})
|
|
1156
|
+
_emit_agent_payload(runtime, output, "playbook.run", payload, lambda data, _meta: _detail_table(
|
|
1157
|
+
"ASP playbook queued",
|
|
1158
|
+
data,
|
|
1159
|
+
["playbook_id", "case_id", "name", "job_status", "created_at"],
|
|
1160
|
+
))
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _siem_schema(runtime: RuntimeOptions, output: OutputFormat, target_index: str | None) -> None:
|
|
1164
|
+
payload = _agent_get(runtime, "/api/agent/v1/siem/schema/", _clean_params({"target_index": target_index}))
|
|
1165
|
+
if target_index:
|
|
1166
|
+
renderer = lambda data, _meta: _siem_schema_detail(data)
|
|
1167
|
+
else:
|
|
1168
|
+
renderer = lambda data, _meta: _list_table(
|
|
1169
|
+
"ASP SIEM schema",
|
|
1170
|
+
["name", "backend", "description", "default_aggregation_fields"],
|
|
1171
|
+
data,
|
|
1172
|
+
)
|
|
1173
|
+
_emit_agent_payload(runtime, output, "siem.schema", payload, renderer)
|
|
1174
|
+
|
|
1175
|
+
|
|
1176
|
+
def _siem_keyword(runtime: RuntimeOptions, output: OutputFormat, keyword: str, time_range_start: str, time_range_end: str, time_field: str, index_name: str | None) -> None:
|
|
1177
|
+
body = {
|
|
1178
|
+
"keyword": _split_csv(keyword) if "," in keyword else keyword,
|
|
1179
|
+
"time_range_start": time_range_start,
|
|
1180
|
+
"time_range_end": time_range_end,
|
|
1181
|
+
"time_field": time_field,
|
|
1182
|
+
"index_name": index_name,
|
|
1183
|
+
}
|
|
1184
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/siem/search/keyword/", json=_clean_params(body))
|
|
1185
|
+
_emit_agent_payload(runtime, output, "siem.search.keyword", payload, _siem_query_table)
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def _siem_adaptive(
|
|
1189
|
+
runtime: RuntimeOptions,
|
|
1190
|
+
output: OutputFormat,
|
|
1191
|
+
index_name: str,
|
|
1192
|
+
time_range_start: str,
|
|
1193
|
+
time_range_end: str,
|
|
1194
|
+
time_field: str,
|
|
1195
|
+
filters_json: str | None,
|
|
1196
|
+
filters_file: Path | None,
|
|
1197
|
+
aggregation_fields: str | None,
|
|
1198
|
+
) -> None:
|
|
1199
|
+
filters = {}
|
|
1200
|
+
if filters_file is not None:
|
|
1201
|
+
filters = _read_json_object_file(filters_file)
|
|
1202
|
+
elif filters_json is not None:
|
|
1203
|
+
filters = _read_json_object_text(filters_json)
|
|
1204
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/siem/query/adaptive/", json={
|
|
1205
|
+
"index_name": index_name,
|
|
1206
|
+
"time_range_start": time_range_start,
|
|
1207
|
+
"time_range_end": time_range_end,
|
|
1208
|
+
"time_field": time_field,
|
|
1209
|
+
"filters": filters,
|
|
1210
|
+
"aggregation_fields": _split_csv(aggregation_fields),
|
|
1211
|
+
})
|
|
1212
|
+
_emit_agent_payload(runtime, output, "siem.query.adaptive", payload, _siem_query_table)
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
def _siem_raw_query(
|
|
1216
|
+
runtime: RuntimeOptions,
|
|
1217
|
+
output: OutputFormat,
|
|
1218
|
+
kind: str,
|
|
1219
|
+
query: str,
|
|
1220
|
+
time_range_start: str,
|
|
1221
|
+
time_range_end: str,
|
|
1222
|
+
limit: int,
|
|
1223
|
+
time_field: str,
|
|
1224
|
+
index_name: str | None,
|
|
1225
|
+
) -> None:
|
|
1226
|
+
payload = _agent_request(runtime, "POST", f"/api/agent/v1/siem/query/{kind}/", json=_clean_params({
|
|
1227
|
+
"query": query,
|
|
1228
|
+
"time_range_start": time_range_start,
|
|
1229
|
+
"time_range_end": time_range_end,
|
|
1230
|
+
"limit": limit,
|
|
1231
|
+
"time_field": time_field,
|
|
1232
|
+
"index_name": index_name,
|
|
1233
|
+
}))
|
|
1234
|
+
_emit_agent_payload(runtime, output, f"siem.query.{kind}", payload, _siem_query_table)
|
|
1235
|
+
|
|
1236
|
+
|
|
1237
|
+
def _siem_fields_discover(runtime: RuntimeOptions, output: OutputFormat, index_name: str, backend: str, time_range_start: str, time_range_end: str, doc_limit: int, max_samples_per_field: int) -> None:
|
|
1238
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/siem/fields/discover/", json={
|
|
1239
|
+
"index_name": index_name,
|
|
1240
|
+
"backend": backend,
|
|
1241
|
+
"time_range_start": time_range_start,
|
|
1242
|
+
"time_range_end": time_range_end,
|
|
1243
|
+
"doc_limit": doc_limit,
|
|
1244
|
+
"max_samples_per_field": max_samples_per_field,
|
|
1245
|
+
})
|
|
1246
|
+
_emit_agent_payload(runtime, output, "siem.fields.discover", payload, lambda data, _meta: _detail_table(
|
|
1247
|
+
"ASP SIEM fields",
|
|
1248
|
+
data,
|
|
1249
|
+
["backend", "index_name", "total_fields"],
|
|
1250
|
+
))
|
|
1251
|
+
|
|
1252
|
+
|
|
1253
|
+
def _ti_query(runtime: RuntimeOptions, output: OutputFormat, indicator: str, artifact_type: str, provider: str | None) -> None:
|
|
1254
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/threat-intel/query/", json=_clean_params({
|
|
1255
|
+
"indicator": indicator,
|
|
1256
|
+
"artifact_type": artifact_type,
|
|
1257
|
+
"provider": provider,
|
|
1258
|
+
}))
|
|
1259
|
+
_emit_agent_payload(runtime, output, "ti.query", payload, lambda data, _meta: _detail_table(
|
|
1260
|
+
"ASP threat intelligence",
|
|
1261
|
+
data,
|
|
1262
|
+
["indicator", "indicator_type", "aggregated_risk_level", "errors"],
|
|
1263
|
+
))
|
|
1264
|
+
|
|
1265
|
+
|
|
1266
|
+
def _cmdb_lookup(runtime: RuntimeOptions, output: OutputFormat, artifact_type: str, artifact_value: str, provider: str | None) -> None:
|
|
1267
|
+
payload = _agent_request(runtime, "POST", "/api/agent/v1/cmdb/lookup/", json=_clean_params({
|
|
1268
|
+
"artifact_type": artifact_type,
|
|
1269
|
+
"artifact_value": artifact_value,
|
|
1270
|
+
"provider": provider,
|
|
1271
|
+
}))
|
|
1272
|
+
_emit_agent_payload(runtime, output, "cmdb.lookup", payload, lambda data, _meta: _detail_table(
|
|
1273
|
+
"ASP CMDB lookup",
|
|
1274
|
+
data,
|
|
1275
|
+
["artifact_type", "artifact_value", "errors"],
|
|
1276
|
+
))
|
|
1277
|
+
|
|
1278
|
+
|
|
1279
|
+
def _dev_stream_head(runtime: RuntimeOptions, output: OutputFormat, stream_name: str, n: int) -> None:
|
|
1280
|
+
payload = _agent_get(runtime, "/api/agent/v1/dev/streams/head/", {"stream_name": stream_name, "n": n})
|
|
1281
|
+
_emit_agent_payload(runtime, output, "dev.stream.head", payload, lambda data, _meta: _list_table(
|
|
1282
|
+
"ASP stream head",
|
|
1283
|
+
["message_id", "data"],
|
|
1284
|
+
data,
|
|
1285
|
+
))
|
|
1286
|
+
|
|
1287
|
+
|
|
1288
|
+
def _dev_stream_read(runtime: RuntimeOptions, output: OutputFormat, stream_name: str, message_id: str) -> None:
|
|
1289
|
+
payload = _agent_get(runtime, "/api/agent/v1/dev/streams/message/", {"stream_name": stream_name, "message_id": message_id})
|
|
1290
|
+
_emit_agent_payload(runtime, output, "dev.stream.read", payload, lambda data, _meta: _detail_table(
|
|
1291
|
+
"ASP stream message",
|
|
1292
|
+
data,
|
|
1293
|
+
["message_id", "data"],
|
|
1294
|
+
))
|
|
1295
|
+
|
|
1296
|
+
|
|
1297
|
+
def _agent_get(runtime: RuntimeOptions, path: str, params: dict) -> dict:
|
|
1298
|
+
return _agent_request(runtime, "GET", _path_with_query(path, params))
|
|
1299
|
+
|
|
1300
|
+
|
|
1301
|
+
def _agent_request(runtime: RuntimeOptions, method: str, path: str, *, json=None, files=None) -> dict:
|
|
1302
|
+
config = _require_config(runtime)
|
|
1303
|
+
client = AspClient(api_url=config.api_url or "", api_key=config.api_key, verbose=runtime.verbose, console=err_console)
|
|
1304
|
+
return client.request(method, path, json=json, files=files)
|
|
1305
|
+
|
|
1306
|
+
|
|
1307
|
+
def _path_with_query(path: str, params: dict) -> str:
|
|
1308
|
+
cleaned = _clean_params(params)
|
|
1309
|
+
if not cleaned:
|
|
1310
|
+
return path
|
|
1311
|
+
return f"{path}?{urlencode(cleaned, doseq=True)}"
|
|
1312
|
+
|
|
1313
|
+
|
|
1314
|
+
def _clean_params(params: dict) -> dict:
|
|
1315
|
+
cleaned = {}
|
|
1316
|
+
for key, value in params.items():
|
|
1317
|
+
if value is None or value is False or value == "":
|
|
1318
|
+
continue
|
|
1319
|
+
if key == "tags":
|
|
1320
|
+
cleaned["tag"] = _split_csv(value)
|
|
1321
|
+
elif isinstance(value, str) and "," in value and key in {"status", "severity", "confidence", "verdict", "type", "role"}:
|
|
1322
|
+
cleaned[key] = _split_csv(value)
|
|
1323
|
+
else:
|
|
1324
|
+
cleaned[key] = value
|
|
1325
|
+
return cleaned
|
|
1326
|
+
|
|
1327
|
+
|
|
1328
|
+
def _emit_agent_payload(runtime: RuntimeOptions, output: OutputFormat, operation: str, payload: dict, human_renderer) -> None:
|
|
1329
|
+
data = payload.get("data")
|
|
1330
|
+
meta = payload.get("meta") or {}
|
|
1331
|
+
if output == OutputFormat.human:
|
|
1332
|
+
console.print(human_renderer(data, meta))
|
|
1333
|
+
return
|
|
1334
|
+
emit_runtime_success(runtime, output=output, operation=operation, data=data, meta=meta)
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def _list_table(title: str, columns: list[str], rows: list[dict], meta: dict | None = None) -> Table:
|
|
1338
|
+
table = Table(title=title)
|
|
1339
|
+
for column in columns:
|
|
1340
|
+
table.add_column(column)
|
|
1341
|
+
for row in rows or []:
|
|
1342
|
+
table.add_row(*[_format_cell(row.get(column)) for column in columns])
|
|
1343
|
+
pagination = (meta or {}).get("pagination") or {}
|
|
1344
|
+
if pagination.get("has_more"):
|
|
1345
|
+
table.caption = f"More results available. Continue with --cursor {pagination.get('next_cursor')}"
|
|
1346
|
+
return table
|
|
1347
|
+
|
|
1348
|
+
|
|
1349
|
+
def _detail_table(title: str, data: dict, fields: list[str]) -> Table:
|
|
1350
|
+
return key_value_table(title, [(field, _format_cell(data.get(field))) for field in fields if field in data])
|
|
1351
|
+
|
|
1352
|
+
|
|
1353
|
+
def _siem_schema_detail(data: dict) -> Table:
|
|
1354
|
+
table = _detail_table("ASP SIEM schema", data, ["name", "backend", "description"])
|
|
1355
|
+
fields = data.get("fields") or []
|
|
1356
|
+
table.caption = f"{len(fields)} fields. Use --output json for full field metadata."
|
|
1357
|
+
return table
|
|
1358
|
+
|
|
1359
|
+
|
|
1360
|
+
def _siem_query_table(data, _meta) -> Table:
|
|
1361
|
+
rows = data if isinstance(data, list) else [data]
|
|
1362
|
+
return _list_table(
|
|
1363
|
+
"ASP SIEM query",
|
|
1364
|
+
["backend", "index_name", "status", "total_hits", "returned_records", "truncated", "message"],
|
|
1365
|
+
rows,
|
|
1366
|
+
)
|
|
1367
|
+
|
|
1368
|
+
|
|
1369
|
+
def _format_cell(value) -> str:
|
|
1370
|
+
if value is None:
|
|
1371
|
+
return ""
|
|
1372
|
+
if isinstance(value, list):
|
|
1373
|
+
return ", ".join(str(item) for item in value)
|
|
1374
|
+
if isinstance(value, dict):
|
|
1375
|
+
return ", ".join(f"{key}={val}" for key, val in value.items())
|
|
1376
|
+
text = str(value)
|
|
1377
|
+
return text if len(text) <= 160 else f"{text[:157]}..."
|
|
1378
|
+
|
|
1379
|
+
|
|
1380
|
+
def _split_csv(value: str | None) -> list[str]:
|
|
1381
|
+
if not value:
|
|
1382
|
+
return []
|
|
1383
|
+
return [item.strip() for item in value.split(",") if item.strip()]
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
def _read_text_file(path: Path) -> str:
|
|
1387
|
+
try:
|
|
1388
|
+
return path.read_text(encoding="utf-8")
|
|
1389
|
+
except OSError as exc:
|
|
1390
|
+
raise CliError("file_read_failed", f"Unable to read file: {path}", {"path": str(path)}, EXIT_USAGE) from exc
|
|
1391
|
+
|
|
1392
|
+
|
|
1393
|
+
def _read_json_object_file(path: Path) -> dict:
|
|
1394
|
+
return _read_json_object_text(_read_text_file(path))
|
|
1395
|
+
|
|
1396
|
+
|
|
1397
|
+
def _read_json_object_text(text: str) -> dict:
|
|
1398
|
+
try:
|
|
1399
|
+
payload = json.loads(text)
|
|
1400
|
+
except json.JSONDecodeError as exc:
|
|
1401
|
+
raise CliError("invalid_json", "Expected a valid JSON object", {}, EXIT_USAGE) from exc
|
|
1402
|
+
if not isinstance(payload, dict):
|
|
1403
|
+
raise CliError("invalid_json", "Expected a valid JSON object", {}, EXIT_USAGE)
|
|
1404
|
+
return payload
|
|
1405
|
+
|
|
1406
|
+
|
|
1407
|
+
def _require_config(runtime: RuntimeOptions):
|
|
1408
|
+
config = resolve_config(api_url=runtime.api_url, api_key=runtime.api_key)
|
|
1409
|
+
missing = []
|
|
1410
|
+
if not config.api_url:
|
|
1411
|
+
missing.append("api_url")
|
|
1412
|
+
if not config.api_key:
|
|
1413
|
+
missing.append("api_key")
|
|
1414
|
+
if missing:
|
|
1415
|
+
raise CliError(
|
|
1416
|
+
"missing_config",
|
|
1417
|
+
"ASP API URL and API key are required. Run: asp auth login --api-url <url> --api-key <key>",
|
|
1418
|
+
{"missing": missing},
|
|
1419
|
+
EXIT_AUTH if "api_key" in missing else EXIT_CONFIG,
|
|
1420
|
+
)
|
|
1421
|
+
return config
|
|
1422
|
+
|
|
1423
|
+
|
|
1424
|
+
def _redacted_settings(settings: dict) -> dict:
|
|
1425
|
+
redacted = dict(settings)
|
|
1426
|
+
if "api_key" in redacted:
|
|
1427
|
+
redacted["api_key"] = redact_secret(str(redacted["api_key"]))
|
|
1428
|
+
return redacted
|
|
1429
|
+
|
|
1430
|
+
|
|
1431
|
+
def apply_query(data, query: str | None):
|
|
1432
|
+
if not query:
|
|
1433
|
+
return data
|
|
1434
|
+
return jmespath.search(query, data)
|
|
1435
|
+
|
|
1436
|
+
|
|
1437
|
+
def emit_runtime_success(
|
|
1438
|
+
runtime: RuntimeOptions,
|
|
1439
|
+
*,
|
|
1440
|
+
output: OutputFormat,
|
|
1441
|
+
operation: str,
|
|
1442
|
+
data,
|
|
1443
|
+
meta: dict | None = None,
|
|
1444
|
+
human: str | None = None,
|
|
1445
|
+
) -> None:
|
|
1446
|
+
emit_success(
|
|
1447
|
+
console,
|
|
1448
|
+
output=output,
|
|
1449
|
+
operation=operation,
|
|
1450
|
+
data=apply_query(data, runtime.query) if output == OutputFormat.json else data,
|
|
1451
|
+
meta=meta,
|
|
1452
|
+
human=human,
|
|
1453
|
+
)
|
|
1454
|
+
|
|
1455
|
+
|
|
1456
|
+
def run() -> None:
|
|
1457
|
+
app()
|
|
1458
|
+
|
|
1459
|
+
|
|
1460
|
+
if __name__ == "__main__":
|
|
1461
|
+
run()
|