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.
Files changed (183) hide show
  1. hackagent/__init__.py +12 -0
  2. hackagent/agent.py +214 -0
  3. hackagent/api/__init__.py +1 -0
  4. hackagent/api/agent/__init__.py +1 -0
  5. hackagent/api/agent/agent_create.py +347 -0
  6. hackagent/api/agent/agent_destroy.py +140 -0
  7. hackagent/api/agent/agent_list.py +242 -0
  8. hackagent/api/agent/agent_partial_update.py +361 -0
  9. hackagent/api/agent/agent_retrieve.py +235 -0
  10. hackagent/api/agent/agent_update.py +361 -0
  11. hackagent/api/apilogs/__init__.py +1 -0
  12. hackagent/api/apilogs/apilogs_list.py +170 -0
  13. hackagent/api/apilogs/apilogs_retrieve.py +162 -0
  14. hackagent/api/attack/__init__.py +1 -0
  15. hackagent/api/attack/attack_create.py +275 -0
  16. hackagent/api/attack/attack_destroy.py +146 -0
  17. hackagent/api/attack/attack_list.py +254 -0
  18. hackagent/api/attack/attack_partial_update.py +289 -0
  19. hackagent/api/attack/attack_retrieve.py +247 -0
  20. hackagent/api/attack/attack_update.py +289 -0
  21. hackagent/api/checkout/__init__.py +1 -0
  22. hackagent/api/checkout/checkout_create.py +225 -0
  23. hackagent/api/generate/__init__.py +1 -0
  24. hackagent/api/generate/generate_create.py +253 -0
  25. hackagent/api/judge/__init__.py +1 -0
  26. hackagent/api/judge/judge_create.py +253 -0
  27. hackagent/api/key/__init__.py +1 -0
  28. hackagent/api/key/key_create.py +179 -0
  29. hackagent/api/key/key_destroy.py +103 -0
  30. hackagent/api/key/key_list.py +170 -0
  31. hackagent/api/key/key_retrieve.py +162 -0
  32. hackagent/api/organization/__init__.py +1 -0
  33. hackagent/api/organization/organization_create.py +208 -0
  34. hackagent/api/organization/organization_destroy.py +104 -0
  35. hackagent/api/organization/organization_list.py +170 -0
  36. hackagent/api/organization/organization_me_retrieve.py +126 -0
  37. hackagent/api/organization/organization_partial_update.py +222 -0
  38. hackagent/api/organization/organization_retrieve.py +163 -0
  39. hackagent/api/organization/organization_update.py +222 -0
  40. hackagent/api/prompt/__init__.py +1 -0
  41. hackagent/api/prompt/prompt_create.py +171 -0
  42. hackagent/api/prompt/prompt_destroy.py +104 -0
  43. hackagent/api/prompt/prompt_list.py +185 -0
  44. hackagent/api/prompt/prompt_partial_update.py +185 -0
  45. hackagent/api/prompt/prompt_retrieve.py +163 -0
  46. hackagent/api/prompt/prompt_update.py +185 -0
  47. hackagent/api/result/__init__.py +1 -0
  48. hackagent/api/result/result_create.py +175 -0
  49. hackagent/api/result/result_destroy.py +106 -0
  50. hackagent/api/result/result_list.py +249 -0
  51. hackagent/api/result/result_partial_update.py +193 -0
  52. hackagent/api/result/result_retrieve.py +167 -0
  53. hackagent/api/result/result_trace_create.py +177 -0
  54. hackagent/api/result/result_update.py +189 -0
  55. hackagent/api/run/__init__.py +1 -0
  56. hackagent/api/run/run_create.py +187 -0
  57. hackagent/api/run/run_destroy.py +112 -0
  58. hackagent/api/run/run_list.py +291 -0
  59. hackagent/api/run/run_partial_update.py +201 -0
  60. hackagent/api/run/run_result_create.py +177 -0
  61. hackagent/api/run/run_retrieve.py +179 -0
  62. hackagent/api/run/run_run_tests_create.py +187 -0
  63. hackagent/api/run/run_update.py +201 -0
  64. hackagent/api/user/__init__.py +1 -0
  65. hackagent/api/user/user_create.py +212 -0
  66. hackagent/api/user/user_destroy.py +106 -0
  67. hackagent/api/user/user_list.py +174 -0
  68. hackagent/api/user/user_me_retrieve.py +126 -0
  69. hackagent/api/user/user_me_update.py +196 -0
  70. hackagent/api/user/user_partial_update.py +226 -0
  71. hackagent/api/user/user_retrieve.py +167 -0
  72. hackagent/api/user/user_update.py +226 -0
  73. hackagent/attacks/AdvPrefix/__init__.py +41 -0
  74. hackagent/attacks/AdvPrefix/completions.py +416 -0
  75. hackagent/attacks/AdvPrefix/config.py +259 -0
  76. hackagent/attacks/AdvPrefix/evaluation.py +745 -0
  77. hackagent/attacks/AdvPrefix/evaluators.py +564 -0
  78. hackagent/attacks/AdvPrefix/generate.py +711 -0
  79. hackagent/attacks/AdvPrefix/utils.py +307 -0
  80. hackagent/attacks/__init__.py +35 -0
  81. hackagent/attacks/advprefix.py +507 -0
  82. hackagent/attacks/base.py +106 -0
  83. hackagent/attacks/strategies.py +906 -0
  84. hackagent/cli/__init__.py +19 -0
  85. hackagent/cli/commands/__init__.py +20 -0
  86. hackagent/cli/commands/agent.py +100 -0
  87. hackagent/cli/commands/attack.py +417 -0
  88. hackagent/cli/commands/config.py +301 -0
  89. hackagent/cli/commands/results.py +327 -0
  90. hackagent/cli/config.py +249 -0
  91. hackagent/cli/main.py +515 -0
  92. hackagent/cli/tui/__init__.py +31 -0
  93. hackagent/cli/tui/actions_logger.py +200 -0
  94. hackagent/cli/tui/app.py +288 -0
  95. hackagent/cli/tui/base.py +137 -0
  96. hackagent/cli/tui/logger.py +318 -0
  97. hackagent/cli/tui/views/__init__.py +33 -0
  98. hackagent/cli/tui/views/agents.py +488 -0
  99. hackagent/cli/tui/views/attacks.py +624 -0
  100. hackagent/cli/tui/views/config.py +244 -0
  101. hackagent/cli/tui/views/dashboard.py +307 -0
  102. hackagent/cli/tui/views/results.py +1210 -0
  103. hackagent/cli/tui/widgets/__init__.py +24 -0
  104. hackagent/cli/tui/widgets/actions.py +346 -0
  105. hackagent/cli/tui/widgets/logs.py +435 -0
  106. hackagent/cli/utils.py +276 -0
  107. hackagent/client.py +286 -0
  108. hackagent/errors.py +37 -0
  109. hackagent/logger.py +83 -0
  110. hackagent/models/__init__.py +109 -0
  111. hackagent/models/agent.py +223 -0
  112. hackagent/models/agent_request.py +129 -0
  113. hackagent/models/api_token_log.py +184 -0
  114. hackagent/models/attack.py +154 -0
  115. hackagent/models/attack_request.py +82 -0
  116. hackagent/models/checkout_session_request_request.py +76 -0
  117. hackagent/models/checkout_session_response.py +59 -0
  118. hackagent/models/choice.py +81 -0
  119. hackagent/models/choice_message.py +67 -0
  120. hackagent/models/evaluation_status_enum.py +14 -0
  121. hackagent/models/generate_error_response.py +59 -0
  122. hackagent/models/generate_request_request.py +212 -0
  123. hackagent/models/generate_success_response.py +115 -0
  124. hackagent/models/generic_error_response.py +70 -0
  125. hackagent/models/message_request.py +67 -0
  126. hackagent/models/organization.py +102 -0
  127. hackagent/models/organization_minimal.py +68 -0
  128. hackagent/models/organization_request.py +71 -0
  129. hackagent/models/paginated_agent_list.py +123 -0
  130. hackagent/models/paginated_api_token_log_list.py +123 -0
  131. hackagent/models/paginated_attack_list.py +123 -0
  132. hackagent/models/paginated_organization_list.py +123 -0
  133. hackagent/models/paginated_prompt_list.py +123 -0
  134. hackagent/models/paginated_result_list.py +123 -0
  135. hackagent/models/paginated_run_list.py +123 -0
  136. hackagent/models/paginated_user_api_key_list.py +123 -0
  137. hackagent/models/paginated_user_profile_list.py +123 -0
  138. hackagent/models/patched_agent_request.py +128 -0
  139. hackagent/models/patched_attack_request.py +92 -0
  140. hackagent/models/patched_organization_request.py +71 -0
  141. hackagent/models/patched_prompt_request.py +125 -0
  142. hackagent/models/patched_result_request.py +237 -0
  143. hackagent/models/patched_run_request.py +138 -0
  144. hackagent/models/patched_user_profile_request.py +99 -0
  145. hackagent/models/prompt.py +220 -0
  146. hackagent/models/prompt_request.py +126 -0
  147. hackagent/models/result.py +294 -0
  148. hackagent/models/result_list_evaluation_status.py +14 -0
  149. hackagent/models/result_request.py +232 -0
  150. hackagent/models/run.py +233 -0
  151. hackagent/models/run_list_status.py +12 -0
  152. hackagent/models/run_request.py +133 -0
  153. hackagent/models/status_enum.py +12 -0
  154. hackagent/models/step_type_enum.py +14 -0
  155. hackagent/models/trace.py +121 -0
  156. hackagent/models/trace_request.py +94 -0
  157. hackagent/models/usage.py +75 -0
  158. hackagent/models/user_api_key.py +201 -0
  159. hackagent/models/user_api_key_request.py +73 -0
  160. hackagent/models/user_profile.py +135 -0
  161. hackagent/models/user_profile_minimal.py +76 -0
  162. hackagent/models/user_profile_request.py +99 -0
  163. hackagent/router/__init__.py +25 -0
  164. hackagent/router/adapters/__init__.py +20 -0
  165. hackagent/router/adapters/base.py +63 -0
  166. hackagent/router/adapters/google_adk.py +671 -0
  167. hackagent/router/adapters/litellm_adapter.py +524 -0
  168. hackagent/router/adapters/openai_adapter.py +426 -0
  169. hackagent/router/router.py +969 -0
  170. hackagent/router/types.py +54 -0
  171. hackagent/tracking/__init__.py +42 -0
  172. hackagent/tracking/context.py +163 -0
  173. hackagent/tracking/decorators.py +299 -0
  174. hackagent/tracking/tracker.py +441 -0
  175. hackagent/types.py +54 -0
  176. hackagent/utils.py +194 -0
  177. hackagent/vulnerabilities/__init__.py +13 -0
  178. hackagent/vulnerabilities/prompts.py +81 -0
  179. hackagent-0.3.1.dist-info/METADATA +122 -0
  180. hackagent-0.3.1.dist-info/RECORD +183 -0
  181. hackagent-0.3.1.dist-info/WHEEL +4 -0
  182. hackagent-0.3.1.dist-info/entry_points.txt +2 -0
  183. 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)