hackagent 0.3.1__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.
- hackagent/__init__.py +12 -0
- hackagent/agent.py +214 -0
- hackagent/api/__init__.py +1 -0
- hackagent/api/agent/__init__.py +1 -0
- hackagent/api/agent/agent_create.py +347 -0
- hackagent/api/agent/agent_destroy.py +140 -0
- hackagent/api/agent/agent_list.py +242 -0
- hackagent/api/agent/agent_partial_update.py +361 -0
- hackagent/api/agent/agent_retrieve.py +235 -0
- hackagent/api/agent/agent_update.py +361 -0
- hackagent/api/apilogs/__init__.py +1 -0
- hackagent/api/apilogs/apilogs_list.py +170 -0
- hackagent/api/apilogs/apilogs_retrieve.py +162 -0
- hackagent/api/attack/__init__.py +1 -0
- hackagent/api/attack/attack_create.py +275 -0
- hackagent/api/attack/attack_destroy.py +146 -0
- hackagent/api/attack/attack_list.py +254 -0
- hackagent/api/attack/attack_partial_update.py +289 -0
- hackagent/api/attack/attack_retrieve.py +247 -0
- hackagent/api/attack/attack_update.py +289 -0
- hackagent/api/checkout/__init__.py +1 -0
- hackagent/api/checkout/checkout_create.py +225 -0
- hackagent/api/generate/__init__.py +1 -0
- hackagent/api/generate/generate_create.py +253 -0
- hackagent/api/judge/__init__.py +1 -0
- hackagent/api/judge/judge_create.py +253 -0
- hackagent/api/key/__init__.py +1 -0
- hackagent/api/key/key_create.py +179 -0
- hackagent/api/key/key_destroy.py +103 -0
- hackagent/api/key/key_list.py +170 -0
- hackagent/api/key/key_retrieve.py +162 -0
- hackagent/api/organization/__init__.py +1 -0
- hackagent/api/organization/organization_create.py +208 -0
- hackagent/api/organization/organization_destroy.py +104 -0
- hackagent/api/organization/organization_list.py +170 -0
- hackagent/api/organization/organization_me_retrieve.py +126 -0
- hackagent/api/organization/organization_partial_update.py +222 -0
- hackagent/api/organization/organization_retrieve.py +163 -0
- hackagent/api/organization/organization_update.py +222 -0
- hackagent/api/prompt/__init__.py +1 -0
- hackagent/api/prompt/prompt_create.py +171 -0
- hackagent/api/prompt/prompt_destroy.py +104 -0
- hackagent/api/prompt/prompt_list.py +185 -0
- hackagent/api/prompt/prompt_partial_update.py +185 -0
- hackagent/api/prompt/prompt_retrieve.py +163 -0
- hackagent/api/prompt/prompt_update.py +185 -0
- hackagent/api/result/__init__.py +1 -0
- hackagent/api/result/result_create.py +175 -0
- hackagent/api/result/result_destroy.py +106 -0
- hackagent/api/result/result_list.py +249 -0
- hackagent/api/result/result_partial_update.py +193 -0
- hackagent/api/result/result_retrieve.py +167 -0
- hackagent/api/result/result_trace_create.py +177 -0
- hackagent/api/result/result_update.py +189 -0
- hackagent/api/run/__init__.py +1 -0
- hackagent/api/run/run_create.py +187 -0
- hackagent/api/run/run_destroy.py +112 -0
- hackagent/api/run/run_list.py +291 -0
- hackagent/api/run/run_partial_update.py +201 -0
- hackagent/api/run/run_result_create.py +177 -0
- hackagent/api/run/run_retrieve.py +179 -0
- hackagent/api/run/run_run_tests_create.py +187 -0
- hackagent/api/run/run_update.py +201 -0
- hackagent/api/user/__init__.py +1 -0
- hackagent/api/user/user_create.py +212 -0
- hackagent/api/user/user_destroy.py +106 -0
- hackagent/api/user/user_list.py +174 -0
- hackagent/api/user/user_me_retrieve.py +126 -0
- hackagent/api/user/user_me_update.py +196 -0
- hackagent/api/user/user_partial_update.py +226 -0
- hackagent/api/user/user_retrieve.py +167 -0
- hackagent/api/user/user_update.py +226 -0
- hackagent/attacks/AdvPrefix/__init__.py +41 -0
- hackagent/attacks/AdvPrefix/completions.py +416 -0
- hackagent/attacks/AdvPrefix/config.py +259 -0
- hackagent/attacks/AdvPrefix/evaluation.py +745 -0
- hackagent/attacks/AdvPrefix/evaluators.py +564 -0
- hackagent/attacks/AdvPrefix/generate.py +711 -0
- hackagent/attacks/AdvPrefix/utils.py +307 -0
- hackagent/attacks/__init__.py +35 -0
- hackagent/attacks/advprefix.py +507 -0
- hackagent/attacks/base.py +106 -0
- hackagent/attacks/strategies.py +906 -0
- hackagent/cli/__init__.py +19 -0
- hackagent/cli/commands/__init__.py +20 -0
- hackagent/cli/commands/agent.py +100 -0
- hackagent/cli/commands/attack.py +417 -0
- hackagent/cli/commands/config.py +301 -0
- hackagent/cli/commands/results.py +327 -0
- hackagent/cli/config.py +249 -0
- hackagent/cli/main.py +515 -0
- hackagent/cli/tui/__init__.py +31 -0
- hackagent/cli/tui/actions_logger.py +200 -0
- hackagent/cli/tui/app.py +288 -0
- hackagent/cli/tui/base.py +137 -0
- hackagent/cli/tui/logger.py +318 -0
- hackagent/cli/tui/views/__init__.py +33 -0
- hackagent/cli/tui/views/agents.py +488 -0
- hackagent/cli/tui/views/attacks.py +624 -0
- hackagent/cli/tui/views/config.py +244 -0
- hackagent/cli/tui/views/dashboard.py +307 -0
- hackagent/cli/tui/views/results.py +1210 -0
- hackagent/cli/tui/widgets/__init__.py +24 -0
- hackagent/cli/tui/widgets/actions.py +346 -0
- hackagent/cli/tui/widgets/logs.py +435 -0
- hackagent/cli/utils.py +276 -0
- hackagent/client.py +286 -0
- hackagent/errors.py +37 -0
- hackagent/logger.py +83 -0
- hackagent/models/__init__.py +109 -0
- hackagent/models/agent.py +223 -0
- hackagent/models/agent_request.py +129 -0
- hackagent/models/api_token_log.py +184 -0
- hackagent/models/attack.py +154 -0
- hackagent/models/attack_request.py +82 -0
- hackagent/models/checkout_session_request_request.py +76 -0
- hackagent/models/checkout_session_response.py +59 -0
- hackagent/models/choice.py +81 -0
- hackagent/models/choice_message.py +67 -0
- hackagent/models/evaluation_status_enum.py +14 -0
- hackagent/models/generate_error_response.py +59 -0
- hackagent/models/generate_request_request.py +212 -0
- hackagent/models/generate_success_response.py +115 -0
- hackagent/models/generic_error_response.py +70 -0
- hackagent/models/message_request.py +67 -0
- hackagent/models/organization.py +102 -0
- hackagent/models/organization_minimal.py +68 -0
- hackagent/models/organization_request.py +71 -0
- hackagent/models/paginated_agent_list.py +123 -0
- hackagent/models/paginated_api_token_log_list.py +123 -0
- hackagent/models/paginated_attack_list.py +123 -0
- hackagent/models/paginated_organization_list.py +123 -0
- hackagent/models/paginated_prompt_list.py +123 -0
- hackagent/models/paginated_result_list.py +123 -0
- hackagent/models/paginated_run_list.py +123 -0
- hackagent/models/paginated_user_api_key_list.py +123 -0
- hackagent/models/paginated_user_profile_list.py +123 -0
- hackagent/models/patched_agent_request.py +128 -0
- hackagent/models/patched_attack_request.py +92 -0
- hackagent/models/patched_organization_request.py +71 -0
- hackagent/models/patched_prompt_request.py +125 -0
- hackagent/models/patched_result_request.py +237 -0
- hackagent/models/patched_run_request.py +138 -0
- hackagent/models/patched_user_profile_request.py +99 -0
- hackagent/models/prompt.py +220 -0
- hackagent/models/prompt_request.py +126 -0
- hackagent/models/result.py +294 -0
- hackagent/models/result_list_evaluation_status.py +14 -0
- hackagent/models/result_request.py +232 -0
- hackagent/models/run.py +233 -0
- hackagent/models/run_list_status.py +12 -0
- hackagent/models/run_request.py +133 -0
- hackagent/models/status_enum.py +12 -0
- hackagent/models/step_type_enum.py +14 -0
- hackagent/models/trace.py +121 -0
- hackagent/models/trace_request.py +94 -0
- hackagent/models/usage.py +75 -0
- hackagent/models/user_api_key.py +201 -0
- hackagent/models/user_api_key_request.py +73 -0
- hackagent/models/user_profile.py +135 -0
- hackagent/models/user_profile_minimal.py +76 -0
- hackagent/models/user_profile_request.py +99 -0
- hackagent/router/__init__.py +25 -0
- hackagent/router/adapters/__init__.py +20 -0
- hackagent/router/adapters/base.py +63 -0
- hackagent/router/adapters/google_adk.py +671 -0
- hackagent/router/adapters/litellm_adapter.py +524 -0
- hackagent/router/adapters/openai_adapter.py +426 -0
- hackagent/router/router.py +969 -0
- hackagent/router/types.py +54 -0
- hackagent/tracking/__init__.py +42 -0
- hackagent/tracking/context.py +163 -0
- hackagent/tracking/decorators.py +299 -0
- hackagent/tracking/tracker.py +441 -0
- hackagent/types.py +54 -0
- hackagent/utils.py +194 -0
- hackagent/vulnerabilities/__init__.py +13 -0
- hackagent/vulnerabilities/prompts.py +81 -0
- hackagent-0.3.1.dist-info/METADATA +122 -0
- hackagent-0.3.1.dist-info/RECORD +183 -0
- hackagent-0.3.1.dist-info/WHEEL +4 -0
- hackagent-0.3.1.dist-info/entry_points.txt +2 -0
- hackagent-0.3.1.dist-info/licenses/LICENSE +202 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# Copyright 2025 - AI4I. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
Configuration Commands
|
|
17
|
+
|
|
18
|
+
Manage HackAgent CLI configuration settings.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import click
|
|
22
|
+
from rich.console import Console
|
|
23
|
+
from rich.table import Table
|
|
24
|
+
|
|
25
|
+
from hackagent.cli.config import CLIConfig
|
|
26
|
+
from hackagent.cli.utils import display_info, display_success, handle_errors
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.group()
|
|
32
|
+
def config():
|
|
33
|
+
"""🔧 Manage HackAgent CLI configuration"""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@config.command()
|
|
38
|
+
@click.option("--api-key", help="HackAgent API key")
|
|
39
|
+
@click.option("--base-url", help="HackAgent API base URL")
|
|
40
|
+
@click.option(
|
|
41
|
+
"--output-format",
|
|
42
|
+
type=click.Choice(["table", "json", "csv"]),
|
|
43
|
+
help="Default output format",
|
|
44
|
+
)
|
|
45
|
+
@click.option(
|
|
46
|
+
"--verbose",
|
|
47
|
+
type=str,
|
|
48
|
+
help="Default verbosity level: 0/error, 1/warning, 2/info, 3/debug",
|
|
49
|
+
)
|
|
50
|
+
@click.pass_context
|
|
51
|
+
@handle_errors
|
|
52
|
+
def set(ctx, api_key, base_url, output_format, verbose):
|
|
53
|
+
"""Set configuration values"""
|
|
54
|
+
|
|
55
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
56
|
+
|
|
57
|
+
updated = False
|
|
58
|
+
|
|
59
|
+
if api_key:
|
|
60
|
+
cli_config.api_key = api_key
|
|
61
|
+
updated = True
|
|
62
|
+
if cli_config.should_show_info():
|
|
63
|
+
display_success("API key updated")
|
|
64
|
+
|
|
65
|
+
if base_url:
|
|
66
|
+
cli_config.base_url = base_url
|
|
67
|
+
updated = True
|
|
68
|
+
if cli_config.should_show_info():
|
|
69
|
+
display_success(f"Base URL updated to: {base_url}")
|
|
70
|
+
|
|
71
|
+
if output_format:
|
|
72
|
+
cli_config.output_format = output_format
|
|
73
|
+
updated = True
|
|
74
|
+
if cli_config.should_show_info():
|
|
75
|
+
display_success(f"Output format updated to: {output_format}")
|
|
76
|
+
|
|
77
|
+
if verbose is not None:
|
|
78
|
+
from hackagent.cli.config import VERBOSITY_LEVELS, VERBOSITY_NAMES
|
|
79
|
+
|
|
80
|
+
# Try to parse as integer first, then as name
|
|
81
|
+
try:
|
|
82
|
+
verbose_int = int(verbose)
|
|
83
|
+
if 0 <= verbose_int <= 3:
|
|
84
|
+
cli_config.verbose = verbose_int
|
|
85
|
+
updated = True
|
|
86
|
+
if cli_config.verbose > 0:
|
|
87
|
+
display_success(
|
|
88
|
+
f"Verbosity level updated to: {verbose_int} ({VERBOSITY_NAMES[verbose_int]})"
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
display_info("Verbosity level must be between 0 and 3")
|
|
92
|
+
except ValueError:
|
|
93
|
+
# Try as name
|
|
94
|
+
verbose_lower = verbose.lower()
|
|
95
|
+
if verbose_lower in VERBOSITY_LEVELS:
|
|
96
|
+
verbose_int = VERBOSITY_LEVELS[verbose_lower]
|
|
97
|
+
cli_config.verbose = verbose_int
|
|
98
|
+
updated = True
|
|
99
|
+
if cli_config.verbose > 0:
|
|
100
|
+
display_success(
|
|
101
|
+
f"Verbosity level updated to: {verbose_int} ({VERBOSITY_NAMES[verbose_int]})"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
display_info(
|
|
105
|
+
f"Invalid verbosity level. Use: 0-3 or {', '.join(VERBOSITY_LEVELS.keys())}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
if updated:
|
|
109
|
+
cli_config.save()
|
|
110
|
+
display_success("✅ Configuration saved")
|
|
111
|
+
else:
|
|
112
|
+
display_info("No configuration changes made")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@config.command()
|
|
116
|
+
@click.pass_context
|
|
117
|
+
@handle_errors
|
|
118
|
+
def show(ctx):
|
|
119
|
+
"""Show current configuration"""
|
|
120
|
+
|
|
121
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
122
|
+
|
|
123
|
+
table = Table(
|
|
124
|
+
title="HackAgent Configuration", show_header=True, header_style="bold cyan"
|
|
125
|
+
)
|
|
126
|
+
table.add_column("Setting", style="cyan")
|
|
127
|
+
table.add_column("Value", style="green")
|
|
128
|
+
table.add_column("Source", style="dim")
|
|
129
|
+
|
|
130
|
+
# Determine sources
|
|
131
|
+
api_key_source = "Not set"
|
|
132
|
+
if cli_config.api_key:
|
|
133
|
+
if ctx.params.get("api_key"):
|
|
134
|
+
api_key_source = "CLI argument"
|
|
135
|
+
elif cli_config.config_file:
|
|
136
|
+
api_key_source = f"Config file ({cli_config.config_file})"
|
|
137
|
+
else:
|
|
138
|
+
api_key_source = "Environment/Default config"
|
|
139
|
+
|
|
140
|
+
base_url_source = "Default"
|
|
141
|
+
if cli_config.base_url != "https://api.hackagent.dev":
|
|
142
|
+
if ctx.params.get("base_url"):
|
|
143
|
+
base_url_source = "CLI argument"
|
|
144
|
+
elif cli_config.config_file:
|
|
145
|
+
base_url_source = f"Config file ({cli_config.config_file})"
|
|
146
|
+
else:
|
|
147
|
+
base_url_source = "Environment/Default config"
|
|
148
|
+
|
|
149
|
+
# Add rows
|
|
150
|
+
api_key_display = (
|
|
151
|
+
cli_config.api_key[:8] + "..." if cli_config.api_key else "Not set"
|
|
152
|
+
)
|
|
153
|
+
from hackagent.cli.config import VERBOSITY_NAMES
|
|
154
|
+
|
|
155
|
+
table.add_row("API Key", api_key_display, api_key_source)
|
|
156
|
+
table.add_row("Base URL", cli_config.base_url, base_url_source)
|
|
157
|
+
table.add_row("Output Format", cli_config.output_format, "Default/Config")
|
|
158
|
+
verbosity_display = (
|
|
159
|
+
f"{cli_config.verbose} ({VERBOSITY_NAMES.get(cli_config.verbose, 'UNKNOWN')})"
|
|
160
|
+
)
|
|
161
|
+
table.add_row("Verbosity", verbosity_display, "Default/Config")
|
|
162
|
+
table.add_row(
|
|
163
|
+
"Config File", str(cli_config.default_config_path), "Default location"
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
console.print(table)
|
|
167
|
+
|
|
168
|
+
# Show config file status only in info mode or higher
|
|
169
|
+
if cli_config.should_show_info():
|
|
170
|
+
if cli_config.default_config_path.exists():
|
|
171
|
+
display_info(f"Configuration file: {cli_config.default_config_path}")
|
|
172
|
+
else:
|
|
173
|
+
display_info(
|
|
174
|
+
"No configuration file found. Use 'hackagent config set' to create one."
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
@config.command()
|
|
179
|
+
@click.option("--confirm", is_flag=True, help="Skip confirmation prompt")
|
|
180
|
+
@click.pass_context
|
|
181
|
+
@handle_errors
|
|
182
|
+
def reset(ctx, confirm):
|
|
183
|
+
"""Reset configuration to defaults"""
|
|
184
|
+
|
|
185
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
186
|
+
|
|
187
|
+
if not confirm:
|
|
188
|
+
if not click.confirm(
|
|
189
|
+
"⚠️ This will reset all configuration to defaults. Continue?"
|
|
190
|
+
):
|
|
191
|
+
display_info("Configuration reset cancelled")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
# Remove config file if it exists
|
|
195
|
+
if cli_config.default_config_path.exists():
|
|
196
|
+
cli_config.default_config_path.unlink()
|
|
197
|
+
display_success("✅ Configuration reset to defaults")
|
|
198
|
+
if cli_config.should_show_info():
|
|
199
|
+
display_info(
|
|
200
|
+
"API key will need to be set again using environment variable or 'hackagent config set --api-key'"
|
|
201
|
+
)
|
|
202
|
+
else:
|
|
203
|
+
display_info("No configuration file to reset")
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@config.command()
|
|
207
|
+
@click.pass_context
|
|
208
|
+
@handle_errors
|
|
209
|
+
def validate(ctx):
|
|
210
|
+
"""Validate current configuration"""
|
|
211
|
+
|
|
212
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
213
|
+
|
|
214
|
+
try:
|
|
215
|
+
cli_config.validate()
|
|
216
|
+
|
|
217
|
+
# Test API connection
|
|
218
|
+
if cli_config.should_show_info():
|
|
219
|
+
with console.status("[bold green]Testing API connection..."):
|
|
220
|
+
from hackagent.client import AuthenticatedClient
|
|
221
|
+
|
|
222
|
+
client = AuthenticatedClient(
|
|
223
|
+
base_url=cli_config.base_url,
|
|
224
|
+
token=cli_config.api_key,
|
|
225
|
+
prefix="Bearer",
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# Try to make a simple API call to test connection
|
|
229
|
+
from hackagent.api.key import key_list
|
|
230
|
+
|
|
231
|
+
response = key_list.sync_detailed(client=client)
|
|
232
|
+
else:
|
|
233
|
+
from hackagent.client import AuthenticatedClient
|
|
234
|
+
|
|
235
|
+
client = AuthenticatedClient(
|
|
236
|
+
base_url=cli_config.base_url, token=cli_config.api_key, prefix="Bearer"
|
|
237
|
+
)
|
|
238
|
+
from hackagent.api.key import key_list
|
|
239
|
+
|
|
240
|
+
response = key_list.sync_detailed(client=client)
|
|
241
|
+
|
|
242
|
+
if response.status_code == 200:
|
|
243
|
+
display_success("✅ Configuration valid - API connection successful")
|
|
244
|
+
else:
|
|
245
|
+
console.print(
|
|
246
|
+
f"[yellow]⚠️ Configuration valid but API connection issue: Status {response.status_code}"
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
except ValueError as e:
|
|
250
|
+
console.print(f"[red]❌ Configuration validation failed: {e}")
|
|
251
|
+
console.print("\n[cyan]💡 Quick fixes:")
|
|
252
|
+
console.print(" • Set API key: hackagent config set --api-key YOUR_KEY")
|
|
253
|
+
console.print(
|
|
254
|
+
" • Set base URL: hackagent config set --base-url https://api.hackagent.dev"
|
|
255
|
+
)
|
|
256
|
+
raise click.ClickException("Configuration validation failed")
|
|
257
|
+
except Exception as e:
|
|
258
|
+
console.print(f"[yellow]⚠️ Could not test API connection: {e}")
|
|
259
|
+
display_info(
|
|
260
|
+
"Configuration appears valid, but API connection could not be tested"
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
@config.command()
|
|
265
|
+
@click.argument("config_file", type=click.Path(exists=True))
|
|
266
|
+
@click.pass_context
|
|
267
|
+
@handle_errors
|
|
268
|
+
def import_config(ctx, config_file):
|
|
269
|
+
"""Import configuration from a file"""
|
|
270
|
+
|
|
271
|
+
from hackagent.cli.utils import load_config_file
|
|
272
|
+
|
|
273
|
+
try:
|
|
274
|
+
config_data = load_config_file(config_file)
|
|
275
|
+
|
|
276
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
277
|
+
|
|
278
|
+
# Update configuration
|
|
279
|
+
updated_fields = []
|
|
280
|
+
if "api_key" in config_data:
|
|
281
|
+
cli_config.api_key = config_data["api_key"]
|
|
282
|
+
updated_fields.append("API key")
|
|
283
|
+
|
|
284
|
+
if "base_url" in config_data:
|
|
285
|
+
cli_config.base_url = config_data["base_url"]
|
|
286
|
+
updated_fields.append("Base URL")
|
|
287
|
+
|
|
288
|
+
if "output_format" in config_data:
|
|
289
|
+
cli_config.output_format = config_data["output_format"]
|
|
290
|
+
updated_fields.append("Output format")
|
|
291
|
+
|
|
292
|
+
if updated_fields:
|
|
293
|
+
cli_config.save()
|
|
294
|
+
display_success(f"✅ Configuration imported: {', '.join(updated_fields)}")
|
|
295
|
+
if cli_config.should_show_info():
|
|
296
|
+
display_info(f"Saved to: {cli_config.default_config_path}")
|
|
297
|
+
else:
|
|
298
|
+
display_info("No valid configuration found in file")
|
|
299
|
+
|
|
300
|
+
except Exception as e:
|
|
301
|
+
raise click.ClickException(f"Failed to import configuration: {e}")
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Copyright 2025 - AI4I. All rights reserved.
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
Results Commands
|
|
18
|
+
|
|
19
|
+
View and manage attack results.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from datetime import datetime
|
|
23
|
+
|
|
24
|
+
import click
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.table import Table
|
|
27
|
+
|
|
28
|
+
from hackagent.cli.config import CLIConfig
|
|
29
|
+
from hackagent.cli.utils import handle_errors, launch_tui
|
|
30
|
+
|
|
31
|
+
console = Console()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@click.group()
|
|
35
|
+
def results():
|
|
36
|
+
"""📊 View and manage attack results"""
|
|
37
|
+
# Show logo when results commands are used
|
|
38
|
+
_show_logo_once()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _show_logo_once():
|
|
42
|
+
"""Show the logo once per session"""
|
|
43
|
+
if not hasattr(_show_logo_once, "_shown"):
|
|
44
|
+
from hackagent.utils import display_hackagent_splash
|
|
45
|
+
|
|
46
|
+
display_hackagent_splash()
|
|
47
|
+
_show_logo_once._shown = True
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@results.command()
|
|
51
|
+
@click.option("--limit", default=10, help="Number of results to show")
|
|
52
|
+
@click.option(
|
|
53
|
+
"--status",
|
|
54
|
+
type=click.Choice(["pending", "running", "completed", "failed"]),
|
|
55
|
+
help="Filter by status",
|
|
56
|
+
)
|
|
57
|
+
@click.option("--agent", help="Filter by agent name")
|
|
58
|
+
@click.option("--attack-type", help="Filter by attack type")
|
|
59
|
+
@click.pass_context
|
|
60
|
+
@handle_errors
|
|
61
|
+
def list(ctx, limit, status, agent, attack_type):
|
|
62
|
+
"""List recent attack results"""
|
|
63
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
64
|
+
cli_config.validate()
|
|
65
|
+
launch_tui(cli_config, initial_tab="results")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@results.command()
|
|
69
|
+
@click.argument("result_id")
|
|
70
|
+
@click.pass_context
|
|
71
|
+
@handle_errors
|
|
72
|
+
def show(ctx, result_id):
|
|
73
|
+
"""Show detailed information about a specific result"""
|
|
74
|
+
|
|
75
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
76
|
+
cli_config.validate()
|
|
77
|
+
|
|
78
|
+
try:
|
|
79
|
+
from hackagent.api.result import result_retrieve
|
|
80
|
+
from hackagent.client import AuthenticatedClient
|
|
81
|
+
|
|
82
|
+
client = AuthenticatedClient(
|
|
83
|
+
base_url=cli_config.base_url, token=cli_config.api_key, prefix="Bearer"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
with console.status(f"[bold green]Fetching result {result_id}..."):
|
|
87
|
+
response = result_retrieve.sync_detailed(client=client, id=result_id)
|
|
88
|
+
|
|
89
|
+
if response.status_code == 200 and response.parsed:
|
|
90
|
+
result = response.parsed
|
|
91
|
+
_display_result_details(result)
|
|
92
|
+
|
|
93
|
+
else:
|
|
94
|
+
raise click.ClickException(f"Result not found: {result_id}")
|
|
95
|
+
|
|
96
|
+
except Exception as e:
|
|
97
|
+
raise click.ClickException(f"Failed to fetch result: {e}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _display_result_details(result) -> None:
|
|
101
|
+
"""Display detailed information about a result"""
|
|
102
|
+
|
|
103
|
+
# Basic info table
|
|
104
|
+
table = Table(title="Result Details", show_header=True, header_style="bold cyan")
|
|
105
|
+
table.add_column("Property", style="cyan")
|
|
106
|
+
table.add_column("Value", style="green")
|
|
107
|
+
|
|
108
|
+
table.add_row("ID", str(result.id))
|
|
109
|
+
|
|
110
|
+
if hasattr(result, "agent_name"):
|
|
111
|
+
table.add_row("Agent", result.agent_name)
|
|
112
|
+
|
|
113
|
+
if hasattr(result, "attack_type"):
|
|
114
|
+
table.add_row("Attack Type", result.attack_type)
|
|
115
|
+
|
|
116
|
+
if hasattr(result, "evaluation_status"):
|
|
117
|
+
status = result.evaluation_status
|
|
118
|
+
if hasattr(status, "value"):
|
|
119
|
+
status = status.value
|
|
120
|
+
table.add_row("Status", str(status))
|
|
121
|
+
|
|
122
|
+
# Format dates
|
|
123
|
+
if hasattr(result, "created_at") and result.created_at:
|
|
124
|
+
try:
|
|
125
|
+
if isinstance(result.created_at, datetime):
|
|
126
|
+
created = result.created_at.strftime("%Y-%m-%d %H:%M:%S")
|
|
127
|
+
else:
|
|
128
|
+
created = str(result.created_at)
|
|
129
|
+
except (AttributeError, ValueError, TypeError):
|
|
130
|
+
created = str(result.created_at)
|
|
131
|
+
table.add_row("Created", created)
|
|
132
|
+
|
|
133
|
+
console.print(table)
|
|
134
|
+
|
|
135
|
+
# Show additional data if available
|
|
136
|
+
if hasattr(result, "data") and result.data:
|
|
137
|
+
console.print("\n[bold cyan]📋 Result Data:")
|
|
138
|
+
try:
|
|
139
|
+
import json
|
|
140
|
+
|
|
141
|
+
if isinstance(result.data, dict):
|
|
142
|
+
data_str = json.dumps(result.data, indent=2)
|
|
143
|
+
else:
|
|
144
|
+
data_str = str(result.data)
|
|
145
|
+
console.print(f"[dim]{data_str}")
|
|
146
|
+
except (json.JSONDecodeError, TypeError, AttributeError):
|
|
147
|
+
console.print(f"[dim]{result.data}")
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
@results.command()
|
|
151
|
+
@click.option(
|
|
152
|
+
"--status",
|
|
153
|
+
type=click.Choice(["pending", "running", "completed", "failed"]),
|
|
154
|
+
help="Filter by status",
|
|
155
|
+
)
|
|
156
|
+
@click.option("--agent", help="Filter by agent name")
|
|
157
|
+
@click.option("--attack-type", help="Filter by attack type")
|
|
158
|
+
@click.option("--days", default=7, help="Number of days to include (default: 7)")
|
|
159
|
+
@click.pass_context
|
|
160
|
+
@handle_errors
|
|
161
|
+
def summary(ctx, status, agent, attack_type, days):
|
|
162
|
+
"""Show summary statistics of attack results"""
|
|
163
|
+
|
|
164
|
+
cli_config: CLIConfig = ctx.obj["config"]
|
|
165
|
+
cli_config.validate()
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
from hackagent.api.result import result_list
|
|
169
|
+
from hackagent.client import AuthenticatedClient
|
|
170
|
+
|
|
171
|
+
client = AuthenticatedClient(
|
|
172
|
+
base_url=cli_config.base_url, token=cli_config.api_key, prefix="Bearer"
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Fetch results (using a larger limit for statistics)
|
|
176
|
+
params = {"limit": 1000}
|
|
177
|
+
if status:
|
|
178
|
+
params["evaluation_status"] = status.upper()
|
|
179
|
+
|
|
180
|
+
with console.status("[bold green]Analyzing results..."):
|
|
181
|
+
response = result_list.sync_detailed(client=client, **params)
|
|
182
|
+
|
|
183
|
+
if response.status_code == 200 and response.parsed:
|
|
184
|
+
results_list = response.parsed.results
|
|
185
|
+
|
|
186
|
+
# Filter by date range
|
|
187
|
+
from datetime import datetime, timedelta
|
|
188
|
+
|
|
189
|
+
cutoff_date = datetime.now() - timedelta(days=days)
|
|
190
|
+
|
|
191
|
+
filtered_results = []
|
|
192
|
+
for result in results_list:
|
|
193
|
+
if hasattr(result, "created_at") and result.created_at:
|
|
194
|
+
try:
|
|
195
|
+
created_date = result.created_at
|
|
196
|
+
if isinstance(created_date, str):
|
|
197
|
+
created_date = datetime.fromisoformat(
|
|
198
|
+
created_date.replace("Z", "+00:00")
|
|
199
|
+
)
|
|
200
|
+
if created_date >= cutoff_date:
|
|
201
|
+
filtered_results.append(result)
|
|
202
|
+
except (ValueError, TypeError, AttributeError):
|
|
203
|
+
filtered_results.append(result) # Include if date parsing fails
|
|
204
|
+
|
|
205
|
+
# Apply additional filters
|
|
206
|
+
if agent or attack_type:
|
|
207
|
+
temp_results = []
|
|
208
|
+
for result in filtered_results:
|
|
209
|
+
if (
|
|
210
|
+
agent
|
|
211
|
+
and hasattr(result, "agent_name")
|
|
212
|
+
and agent.lower() not in result.agent_name.lower()
|
|
213
|
+
):
|
|
214
|
+
continue
|
|
215
|
+
if (
|
|
216
|
+
attack_type
|
|
217
|
+
and hasattr(result, "attack_type")
|
|
218
|
+
and attack_type.lower() not in result.attack_type.lower()
|
|
219
|
+
):
|
|
220
|
+
continue
|
|
221
|
+
temp_results.append(result)
|
|
222
|
+
filtered_results = temp_results
|
|
223
|
+
|
|
224
|
+
# Generate statistics
|
|
225
|
+
stats = _generate_result_statistics(filtered_results, days)
|
|
226
|
+
_display_result_summary(stats)
|
|
227
|
+
|
|
228
|
+
else:
|
|
229
|
+
raise click.ClickException(
|
|
230
|
+
f"Failed to fetch results: Status {response.status_code}"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
except Exception as e:
|
|
234
|
+
raise click.ClickException(f"Failed to generate summary: {e}")
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _generate_result_statistics(results, days: int) -> dict:
|
|
238
|
+
"""Generate statistics from results list"""
|
|
239
|
+
|
|
240
|
+
total_results = len(results)
|
|
241
|
+
|
|
242
|
+
# Count by status
|
|
243
|
+
status_counts = {}
|
|
244
|
+
agent_counts = {}
|
|
245
|
+
attack_counts = {}
|
|
246
|
+
|
|
247
|
+
for result in results:
|
|
248
|
+
# Status statistics
|
|
249
|
+
if hasattr(result, "evaluation_status"):
|
|
250
|
+
status = result.evaluation_status
|
|
251
|
+
if hasattr(status, "value"):
|
|
252
|
+
status = status.value
|
|
253
|
+
else:
|
|
254
|
+
status = str(status)
|
|
255
|
+
status_counts[status] = status_counts.get(status, 0) + 1
|
|
256
|
+
|
|
257
|
+
# Agent statistics
|
|
258
|
+
if hasattr(result, "agent_name"):
|
|
259
|
+
agent = result.agent_name
|
|
260
|
+
agent_counts[agent] = agent_counts.get(agent, 0) + 1
|
|
261
|
+
|
|
262
|
+
# Attack type statistics
|
|
263
|
+
if hasattr(result, "attack_type"):
|
|
264
|
+
attack = result.attack_type
|
|
265
|
+
attack_counts[attack] = attack_counts.get(attack, 0) + 1
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
"period_days": days,
|
|
269
|
+
"total_results": total_results,
|
|
270
|
+
"status_breakdown": status_counts,
|
|
271
|
+
"agent_breakdown": agent_counts,
|
|
272
|
+
"attack_type_breakdown": attack_counts,
|
|
273
|
+
"generated_at": str(datetime.now()),
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _display_result_summary(stats: dict) -> None:
|
|
278
|
+
"""Display result statistics summary"""
|
|
279
|
+
|
|
280
|
+
console.print(f"\n[bold cyan]📊 Results Summary (Last {stats['period_days']} days)")
|
|
281
|
+
console.print(f"[green]Total Results: {stats['total_results']}")
|
|
282
|
+
|
|
283
|
+
# Status breakdown
|
|
284
|
+
if stats["status_breakdown"]:
|
|
285
|
+
console.print("\n[bold cyan]📈 By Status:")
|
|
286
|
+
status_table = Table(show_header=True, header_style="bold cyan")
|
|
287
|
+
status_table.add_column("Status", style="cyan")
|
|
288
|
+
status_table.add_column("Count", style="green")
|
|
289
|
+
status_table.add_column("Percentage", style="yellow")
|
|
290
|
+
|
|
291
|
+
for status, count in stats["status_breakdown"].items():
|
|
292
|
+
percentage = (
|
|
293
|
+
(count / stats["total_results"]) * 100
|
|
294
|
+
if stats["total_results"] > 0
|
|
295
|
+
else 0
|
|
296
|
+
)
|
|
297
|
+
status_table.add_row(status, str(count), f"{percentage:.1f}%")
|
|
298
|
+
|
|
299
|
+
console.print(status_table)
|
|
300
|
+
|
|
301
|
+
# Top agents
|
|
302
|
+
if stats["agent_breakdown"]:
|
|
303
|
+
console.print("\n[bold cyan]🤖 By Agent:")
|
|
304
|
+
agent_table = Table(show_header=True, header_style="bold cyan")
|
|
305
|
+
agent_table.add_column("Agent", style="cyan")
|
|
306
|
+
agent_table.add_column("Count", style="green")
|
|
307
|
+
|
|
308
|
+
# Sort by count and show top 5
|
|
309
|
+
sorted_agents = sorted(
|
|
310
|
+
stats["agent_breakdown"].items(), key=lambda x: x[1], reverse=True
|
|
311
|
+
)
|
|
312
|
+
for agent, count in sorted_agents[:5]:
|
|
313
|
+
agent_table.add_row(agent, str(count))
|
|
314
|
+
|
|
315
|
+
console.print(agent_table)
|
|
316
|
+
|
|
317
|
+
# Attack types
|
|
318
|
+
if stats["attack_type_breakdown"]:
|
|
319
|
+
console.print("\n[bold cyan]🎯 By Attack Type:")
|
|
320
|
+
attack_table = Table(show_header=True, header_style="bold cyan")
|
|
321
|
+
attack_table.add_column("Attack Type", style="cyan")
|
|
322
|
+
attack_table.add_column("Count", style="green")
|
|
323
|
+
|
|
324
|
+
for attack_type, count in stats["attack_type_breakdown"].items():
|
|
325
|
+
attack_table.add_row(attack_type, str(count))
|
|
326
|
+
|
|
327
|
+
console.print(attack_table)
|