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,244 @@
|
|
|
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
|
+
Config Tab
|
|
17
|
+
|
|
18
|
+
Manage HackAgent configuration settings.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from textual.app import ComposeResult
|
|
22
|
+
from textual.binding import Binding
|
|
23
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
24
|
+
from textual.widgets import Button, Input, Label, Select, Static
|
|
25
|
+
|
|
26
|
+
from hackagent.cli.config import CLIConfig
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ConfigTab(VerticalScroll):
|
|
30
|
+
"""Config tab for managing settings with vertical scrolling."""
|
|
31
|
+
|
|
32
|
+
DEFAULT_CSS = ""
|
|
33
|
+
|
|
34
|
+
BINDINGS = [
|
|
35
|
+
Binding("s", "save_config", "Save"),
|
|
36
|
+
Binding("t", "test_connection", "Test Connection"),
|
|
37
|
+
Binding("r", "reset_config", "Reset"),
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
def __init__(self, cli_config: CLIConfig):
|
|
41
|
+
"""Initialize config tab.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
cli_config: CLI configuration object
|
|
45
|
+
"""
|
|
46
|
+
super().__init__()
|
|
47
|
+
self.cli_config = cli_config
|
|
48
|
+
|
|
49
|
+
def compose(self) -> ComposeResult:
|
|
50
|
+
"""Compose the config layout."""
|
|
51
|
+
yield Static(
|
|
52
|
+
"[bold cyan]HackAgent Configuration[/bold cyan]",
|
|
53
|
+
classes="config-section",
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
with Vertical(classes="config-section"):
|
|
57
|
+
yield Static("[bold]API Configuration[/bold]")
|
|
58
|
+
|
|
59
|
+
with Vertical(classes="form-group"):
|
|
60
|
+
yield Label("API Key:")
|
|
61
|
+
yield Input(
|
|
62
|
+
placeholder="Your HackAgent API key",
|
|
63
|
+
id="api-key",
|
|
64
|
+
password=True,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
with Vertical(classes="form-group"):
|
|
68
|
+
yield Label("Base URL:")
|
|
69
|
+
yield Input(
|
|
70
|
+
id="base_url",
|
|
71
|
+
placeholder="https://api.hackagent.dev",
|
|
72
|
+
classes="config-input",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
with Vertical(classes="form-group"):
|
|
76
|
+
yield Label("Output Format:")
|
|
77
|
+
yield Select(
|
|
78
|
+
[("Table", "table"), ("JSON", "json"), ("CSV", "csv")],
|
|
79
|
+
id="output-format",
|
|
80
|
+
value=self.cli_config.output_format,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
with Vertical(classes="config-section"):
|
|
84
|
+
yield Static("[bold]Configuration File[/bold]")
|
|
85
|
+
|
|
86
|
+
yield Static(
|
|
87
|
+
f"[dim]Location:[/dim] {self.cli_config.default_config_path}",
|
|
88
|
+
classes="info-box",
|
|
89
|
+
id="config-file-location",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
yield Static(
|
|
93
|
+
"[dim]Status: Checking...[/dim]",
|
|
94
|
+
classes="status-indicator",
|
|
95
|
+
id="config-status",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
with Horizontal(classes="button-group"):
|
|
99
|
+
yield Button("Save Configuration", id="save-config", variant="primary")
|
|
100
|
+
yield Button("Test Connection", id="test-connection", variant="default")
|
|
101
|
+
yield Button("Reset to Defaults", id="reset-config", variant="error")
|
|
102
|
+
yield Button("Validate Config", id="validate-config", variant="success")
|
|
103
|
+
|
|
104
|
+
with Vertical(classes="config-section"):
|
|
105
|
+
yield Static("[bold]System Information[/bold]")
|
|
106
|
+
|
|
107
|
+
yield Static(
|
|
108
|
+
f"""[dim]Python Version:[/dim] {self._get_python_version()}
|
|
109
|
+
[dim]CLI Version:[/dim] 0.2.5
|
|
110
|
+
[dim]Dependencies:[/dim] {self._check_dependencies()}""",
|
|
111
|
+
classes="info-box",
|
|
112
|
+
id="system-info",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def on_mount(self) -> None:
|
|
116
|
+
"""Called when the tab is mounted."""
|
|
117
|
+
self._load_config()
|
|
118
|
+
self._update_status()
|
|
119
|
+
|
|
120
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
121
|
+
"""Handle button press events."""
|
|
122
|
+
if event.button.id == "save-config":
|
|
123
|
+
self._save_config()
|
|
124
|
+
elif event.button.id == "test-connection":
|
|
125
|
+
self._test_connection()
|
|
126
|
+
elif event.button.id == "reset-config":
|
|
127
|
+
self._reset_config()
|
|
128
|
+
elif event.button.id == "validate-config":
|
|
129
|
+
self._validate_config()
|
|
130
|
+
|
|
131
|
+
def _load_config(self) -> None:
|
|
132
|
+
"""Load current configuration into form fields."""
|
|
133
|
+
# Set API key (masked)
|
|
134
|
+
if self.cli_config.api_key:
|
|
135
|
+
self.query_one("#api-key", Input).value = self.cli_config.api_key
|
|
136
|
+
|
|
137
|
+
# Set base URL
|
|
138
|
+
self.query_one("#base_url", Input).value = self.cli_config.base_url
|
|
139
|
+
|
|
140
|
+
# Set output format
|
|
141
|
+
self.query_one("#output-format", Select).value = self.cli_config.output_format
|
|
142
|
+
|
|
143
|
+
def _update_status(self) -> None:
|
|
144
|
+
"""Update configuration status display."""
|
|
145
|
+
status_widget = self.query_one("#config-status", Static)
|
|
146
|
+
|
|
147
|
+
if self.cli_config.default_config_path.exists():
|
|
148
|
+
status_widget.update("[green]✅ Configuration file exists[/green]")
|
|
149
|
+
else:
|
|
150
|
+
status_widget.update(
|
|
151
|
+
"[yellow]⚠️ No configuration file found. Save to create one.[/yellow]"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def _save_config(self) -> None:
|
|
155
|
+
"""Save configuration to file."""
|
|
156
|
+
try:
|
|
157
|
+
# Get values from form
|
|
158
|
+
api_key = self.query_one("#api-key", Input).value
|
|
159
|
+
base_url = self.query_one("#base_url", Input).value
|
|
160
|
+
output_format = self.query_one("#output-format", Select).value
|
|
161
|
+
|
|
162
|
+
# Update config
|
|
163
|
+
if api_key:
|
|
164
|
+
self.cli_config.api_key = api_key
|
|
165
|
+
if base_url:
|
|
166
|
+
self.cli_config.base_url = base_url
|
|
167
|
+
self.cli_config.output_format = output_format
|
|
168
|
+
|
|
169
|
+
# Save to file
|
|
170
|
+
self.cli_config.save()
|
|
171
|
+
|
|
172
|
+
self._update_status()
|
|
173
|
+
|
|
174
|
+
except Exception:
|
|
175
|
+
pass
|
|
176
|
+
|
|
177
|
+
def _test_connection(self) -> None:
|
|
178
|
+
"""Test API connection."""
|
|
179
|
+
try:
|
|
180
|
+
from hackagent.api.key import key_list
|
|
181
|
+
from hackagent.client import AuthenticatedClient
|
|
182
|
+
|
|
183
|
+
if not self.cli_config.api_key:
|
|
184
|
+
return
|
|
185
|
+
|
|
186
|
+
client = AuthenticatedClient(
|
|
187
|
+
base_url=self.cli_config.base_url,
|
|
188
|
+
token=self.cli_config.api_key,
|
|
189
|
+
prefix="Bearer",
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
key_list.sync_detailed(client=client)
|
|
193
|
+
|
|
194
|
+
except Exception:
|
|
195
|
+
pass
|
|
196
|
+
|
|
197
|
+
def _validate_config(self) -> None:
|
|
198
|
+
"""Validate current configuration."""
|
|
199
|
+
try:
|
|
200
|
+
self.cli_config.validate()
|
|
201
|
+
except ValueError:
|
|
202
|
+
pass
|
|
203
|
+
|
|
204
|
+
def _reset_config(self) -> None:
|
|
205
|
+
"""Reset configuration to defaults."""
|
|
206
|
+
try:
|
|
207
|
+
if self.cli_config.default_config_path.exists():
|
|
208
|
+
self.cli_config.default_config_path.unlink()
|
|
209
|
+
|
|
210
|
+
# Reset to defaults
|
|
211
|
+
self.cli_config.base_url = "https://api.hackagent.dev"
|
|
212
|
+
self.cli_config.output_format = "table"
|
|
213
|
+
self.cli_config.api_key = None
|
|
214
|
+
|
|
215
|
+
self._load_config()
|
|
216
|
+
self._update_status()
|
|
217
|
+
|
|
218
|
+
except Exception:
|
|
219
|
+
pass
|
|
220
|
+
|
|
221
|
+
def _get_python_version(self) -> str:
|
|
222
|
+
"""Get Python version string."""
|
|
223
|
+
import sys
|
|
224
|
+
|
|
225
|
+
return f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
|
226
|
+
|
|
227
|
+
def _check_dependencies(self) -> str:
|
|
228
|
+
"""Check if required dependencies are installed."""
|
|
229
|
+
import importlib.util
|
|
230
|
+
|
|
231
|
+
required_packages = ["pandas", "yaml", "litellm", "textual"]
|
|
232
|
+
missing = [
|
|
233
|
+
pkg for pkg in required_packages if importlib.util.find_spec(pkg) is None
|
|
234
|
+
]
|
|
235
|
+
|
|
236
|
+
if not missing:
|
|
237
|
+
return "[green]✅ All dependencies installed[/green]"
|
|
238
|
+
else:
|
|
239
|
+
return f"[yellow]⚠️ Some dependencies missing: {', '.join(missing)}[/yellow]"
|
|
240
|
+
|
|
241
|
+
def refresh_data(self) -> None:
|
|
242
|
+
"""Refresh config data."""
|
|
243
|
+
self._load_config()
|
|
244
|
+
self._update_status()
|
|
@@ -0,0 +1,307 @@
|
|
|
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
|
+
Dashboard Tab
|
|
17
|
+
|
|
18
|
+
Overview and statistics for HackAgent.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from textual.app import ComposeResult
|
|
22
|
+
from textual.containers import Horizontal, Vertical, VerticalScroll
|
|
23
|
+
from textual.widgets import Static
|
|
24
|
+
|
|
25
|
+
from hackagent.cli.config import CLIConfig
|
|
26
|
+
from hackagent.cli.tui.base import BaseTab
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class DashboardTab(BaseTab):
|
|
30
|
+
"""Dashboard tab showing overview and statistics."""
|
|
31
|
+
|
|
32
|
+
DEFAULT_CSS = ""
|
|
33
|
+
|
|
34
|
+
def __init__(self, cli_config: CLIConfig):
|
|
35
|
+
"""Initialize dashboard tab.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
cli_config: CLI configuration object
|
|
39
|
+
"""
|
|
40
|
+
super().__init__(cli_config)
|
|
41
|
+
self.stats = {
|
|
42
|
+
"agents": 0,
|
|
43
|
+
"attacks": 0,
|
|
44
|
+
"results": 0,
|
|
45
|
+
"success_rate": 0.0,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
def compose(self) -> ComposeResult:
|
|
49
|
+
"""Compose the dashboard layout."""
|
|
50
|
+
# Title section
|
|
51
|
+
yield Static(
|
|
52
|
+
"[bold cyan]━━━ Dashboard Overview ━━━[/bold cyan]", id="dashboard-title"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Statistics section with better formatting
|
|
56
|
+
yield Static("[bold yellow]📊 Statistics[/bold yellow]", id="stats-header")
|
|
57
|
+
|
|
58
|
+
with Horizontal():
|
|
59
|
+
with Vertical():
|
|
60
|
+
yield Static("🤖 [bold]Agents[/bold]\n[cyan]0[/cyan]", id="stat-agents")
|
|
61
|
+
yield Static(
|
|
62
|
+
"⚔️ [bold]Attacks[/bold]\n[green]0[/green]", id="stat-attacks"
|
|
63
|
+
)
|
|
64
|
+
with Vertical():
|
|
65
|
+
yield Static(
|
|
66
|
+
"📋 [bold]Results[/bold]\n[yellow]0[/yellow]", id="stat-results"
|
|
67
|
+
)
|
|
68
|
+
yield Static(
|
|
69
|
+
"✓ [bold]Success Rate[/bold]\n[magenta]0%[/magenta]",
|
|
70
|
+
id="stat-success",
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# Activity section
|
|
74
|
+
yield Static(
|
|
75
|
+
"\n[bold yellow]📝 Recent Activity[/bold yellow]", id="activity-header"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
with VerticalScroll():
|
|
79
|
+
yield Static("[dim]Waiting for data...[/dim]", id="activity-log")
|
|
80
|
+
|
|
81
|
+
def on_mount(self) -> None:
|
|
82
|
+
"""Called when the tab is mounted."""
|
|
83
|
+
# Call base class mount to handle initial refresh
|
|
84
|
+
super().on_mount()
|
|
85
|
+
|
|
86
|
+
# Enable auto-refresh every 5 seconds
|
|
87
|
+
self.enable_auto_refresh(interval=5.0)
|
|
88
|
+
|
|
89
|
+
def refresh_data(self) -> None:
|
|
90
|
+
"""Refresh dashboard data from API."""
|
|
91
|
+
try:
|
|
92
|
+
from hackagent.api.agent import agent_list
|
|
93
|
+
from hackagent.api.result import result_list
|
|
94
|
+
|
|
95
|
+
# Validate configuration
|
|
96
|
+
if not self.cli_config.api_key:
|
|
97
|
+
activity_log = self.query_one("#activity-log", Static)
|
|
98
|
+
activity_log.update(
|
|
99
|
+
"[red]API key not configured[/red]\n\n"
|
|
100
|
+
"[yellow]Run 'hackagent init' to set up your API key[/yellow]\n\n"
|
|
101
|
+
"[dim]You need to configure your HackAgent API key before you can use the TUI.[/dim]"
|
|
102
|
+
)
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
# Create API client using base class method
|
|
106
|
+
client = self.create_api_client()
|
|
107
|
+
|
|
108
|
+
agents_data = []
|
|
109
|
+
results_data = []
|
|
110
|
+
|
|
111
|
+
# Fetch agents count
|
|
112
|
+
agents_response = agent_list.sync_detailed(client=client)
|
|
113
|
+
if agents_response.status_code == 200 and agents_response.parsed:
|
|
114
|
+
agents_data = (
|
|
115
|
+
agents_response.parsed.results
|
|
116
|
+
if agents_response.parsed.results
|
|
117
|
+
else []
|
|
118
|
+
)
|
|
119
|
+
self.stats["agents"] = len(agents_data)
|
|
120
|
+
elif agents_response.status_code == 401:
|
|
121
|
+
activity_log = self.query_one("#activity-log", Static)
|
|
122
|
+
activity_log.update(
|
|
123
|
+
"[red]Authentication failed[/red]\n\n"
|
|
124
|
+
"[yellow]Your API key is invalid or expired[/yellow]\n\n"
|
|
125
|
+
"[dim]Run 'hackagent config set --api-key YOUR_KEY' to update[/dim]"
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
elif agents_response.status_code == 403:
|
|
129
|
+
activity_log = self.query_one("#activity-log", Static)
|
|
130
|
+
activity_log.update(
|
|
131
|
+
"[red]Access forbidden[/red]\n\n"
|
|
132
|
+
"[yellow]Your API key doesn't have permission to access this resource[/yellow]"
|
|
133
|
+
)
|
|
134
|
+
return
|
|
135
|
+
|
|
136
|
+
# Fetch results count
|
|
137
|
+
results_response = result_list.sync_detailed(client=client)
|
|
138
|
+
if results_response.status_code == 200 and results_response.parsed:
|
|
139
|
+
results_data = (
|
|
140
|
+
results_response.parsed.results
|
|
141
|
+
if results_response.parsed.results
|
|
142
|
+
else []
|
|
143
|
+
)
|
|
144
|
+
self.stats["results"] = len(results_data)
|
|
145
|
+
|
|
146
|
+
# Calculate success rate
|
|
147
|
+
if results_data:
|
|
148
|
+
completed = sum(
|
|
149
|
+
1
|
|
150
|
+
for r in results_data
|
|
151
|
+
if hasattr(r, "evaluation_status")
|
|
152
|
+
and str(
|
|
153
|
+
r.evaluation_status.value
|
|
154
|
+
if hasattr(r.evaluation_status, "value")
|
|
155
|
+
else r.evaluation_status
|
|
156
|
+
).upper()
|
|
157
|
+
== "COMPLETED"
|
|
158
|
+
)
|
|
159
|
+
self.stats["success_rate"] = (
|
|
160
|
+
(completed / len(results_data)) * 100
|
|
161
|
+
if len(results_data) > 0
|
|
162
|
+
else 0
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
# Update stat cards
|
|
166
|
+
self._update_stat_cards()
|
|
167
|
+
|
|
168
|
+
# Update activity log
|
|
169
|
+
if not agents_data and not results_data:
|
|
170
|
+
activity_log = self.query_one("#activity-log", Static)
|
|
171
|
+
activity_log.update(
|
|
172
|
+
"[yellow]No data found[/yellow]\n\n"
|
|
173
|
+
"[dim]Create agents and run attacks to see activity here.[/dim]\n\n"
|
|
174
|
+
"[cyan]Quick Start:[/cyan]\n"
|
|
175
|
+
"1. Go to Agents tab to create an agent\n"
|
|
176
|
+
"2. Go to Attacks tab to run security tests\n"
|
|
177
|
+
"3. Check Results tab to see outcomes"
|
|
178
|
+
)
|
|
179
|
+
else:
|
|
180
|
+
self._update_activity_log(agents_data, results_data)
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
# Display error in activity log with helpful context
|
|
184
|
+
activity_log = self.query_one("#activity-log", Static)
|
|
185
|
+
|
|
186
|
+
error_type = type(e).__name__
|
|
187
|
+
error_msg = str(e)
|
|
188
|
+
|
|
189
|
+
# Provide context-specific help
|
|
190
|
+
if "timeout" in error_msg.lower() or "TimeoutException" in error_type:
|
|
191
|
+
activity_log.update(
|
|
192
|
+
f"[red]⚠️ Connection Timeout[/red]\n\n"
|
|
193
|
+
f"[yellow]Cannot reach HackAgent API:[/yellow]\n{self.cli_config.base_url}\n\n"
|
|
194
|
+
f"[cyan]Possible causes:[/cyan]\n"
|
|
195
|
+
f"• API server is down or unreachable\n"
|
|
196
|
+
f"• Network connection issues\n"
|
|
197
|
+
f"• Firewall blocking the connection\n\n"
|
|
198
|
+
f"[dim]Press F5 to retry when connection is restored[/dim]\n\n"
|
|
199
|
+
f"[bold]Offline Mode:[/bold]\n"
|
|
200
|
+
f"You can still use the Attacks tab with local agents\n"
|
|
201
|
+
f"(results won't be synced to the platform)"
|
|
202
|
+
)
|
|
203
|
+
elif "401" in error_msg or "authentication" in error_msg.lower():
|
|
204
|
+
activity_log.update(
|
|
205
|
+
"[red]Authentication Failed[/red]\n\n"
|
|
206
|
+
"[yellow]Your API key is invalid or expired[/yellow]\n\n"
|
|
207
|
+
"[cyan]To fix:[/cyan]\n"
|
|
208
|
+
"Run: hackagent config set --api-key YOUR_KEY\n\n"
|
|
209
|
+
"[dim]Press F5 to retry after updating[/dim]"
|
|
210
|
+
)
|
|
211
|
+
else:
|
|
212
|
+
activity_log.update(
|
|
213
|
+
f"[red]Error loading data:[/red]\n{error_type}\n\n"
|
|
214
|
+
f"[yellow]Details:[/yellow]\n{error_msg}\n\n"
|
|
215
|
+
f"[dim]Press F5 to retry[/dim]"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def _update_stat_cards(self) -> None:
|
|
219
|
+
"""Update the statistics cards with current data."""
|
|
220
|
+
try:
|
|
221
|
+
# Get the values
|
|
222
|
+
agents_val = self.stats.get("agents", 0)
|
|
223
|
+
attacks_val = self.stats.get("attacks", 0)
|
|
224
|
+
results_val = self.stats.get("results", 0)
|
|
225
|
+
success_val = self.stats.get("success_rate", 0)
|
|
226
|
+
|
|
227
|
+
# Update each stat widget by ID with icons and formatting
|
|
228
|
+
stat_agents = self.query_one("#stat-agents", Static)
|
|
229
|
+
stat_agents.update(f"🤖 [bold]Agents[/bold]\n[cyan]{agents_val}[/cyan]")
|
|
230
|
+
|
|
231
|
+
stat_attacks = self.query_one("#stat-attacks", Static)
|
|
232
|
+
stat_attacks.update(
|
|
233
|
+
f"⚔️ [bold]Attacks[/bold]\n[green]{attacks_val}[/green]"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
stat_results = self.query_one("#stat-results", Static)
|
|
237
|
+
stat_results.update(
|
|
238
|
+
f"📋 [bold]Results[/bold]\n[yellow]{results_val}[/yellow]"
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
stat_success = self.query_one("#stat-success", Static)
|
|
242
|
+
stat_success.update(
|
|
243
|
+
f"✓ [bold]Success Rate[/bold]\n[magenta]{success_val:.1f}%[/magenta]"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
except Exception as e:
|
|
247
|
+
# Show error in activity log if update fails
|
|
248
|
+
try:
|
|
249
|
+
activity_log = self.query_one("#activity-log", Static)
|
|
250
|
+
activity_log.update(f"[red]Error updating stats: {str(e)}[/red]")
|
|
251
|
+
except Exception:
|
|
252
|
+
pass
|
|
253
|
+
|
|
254
|
+
def _update_activity_log(self, agents: list, results: list) -> None:
|
|
255
|
+
"""Update activity log with recent items.
|
|
256
|
+
|
|
257
|
+
Args:
|
|
258
|
+
agents: List of agents
|
|
259
|
+
results: List of results
|
|
260
|
+
"""
|
|
261
|
+
activity_log = self.query_one("#activity-log", Static)
|
|
262
|
+
log_lines = []
|
|
263
|
+
|
|
264
|
+
# Add recent agents with icons
|
|
265
|
+
if agents:
|
|
266
|
+
log_lines.append("[bold cyan]🤖 Recent Agents:[/bold cyan]")
|
|
267
|
+
for i, agent in enumerate(agents[:3], 1):
|
|
268
|
+
agent_type = (
|
|
269
|
+
agent.agent_type.value
|
|
270
|
+
if hasattr(agent.agent_type, "value")
|
|
271
|
+
else agent.agent_type
|
|
272
|
+
)
|
|
273
|
+
log_lines.append(
|
|
274
|
+
f" {i}. [cyan]{agent.name or 'Unnamed'}[/cyan] [dim]({agent_type})[/dim]"
|
|
275
|
+
)
|
|
276
|
+
log_lines.append("")
|
|
277
|
+
|
|
278
|
+
# Add recent results with status colors
|
|
279
|
+
if results:
|
|
280
|
+
log_lines.append("[bold green]📋 Recent Results:[/bold green]")
|
|
281
|
+
for i, result in enumerate(results[:5], 1):
|
|
282
|
+
status = "Unknown"
|
|
283
|
+
status_color = "dim"
|
|
284
|
+
|
|
285
|
+
if hasattr(result, "evaluation_status"):
|
|
286
|
+
status = (
|
|
287
|
+
result.evaluation_status.value
|
|
288
|
+
if hasattr(result.evaluation_status, "value")
|
|
289
|
+
else str(result.evaluation_status)
|
|
290
|
+
)
|
|
291
|
+
# Color code based on status
|
|
292
|
+
if status.upper() == "COMPLETED":
|
|
293
|
+
status_color = "green"
|
|
294
|
+
elif status.upper() == "RUNNING":
|
|
295
|
+
status_color = "yellow"
|
|
296
|
+
elif status.upper() == "FAILED":
|
|
297
|
+
status_color = "red"
|
|
298
|
+
|
|
299
|
+
attack_type = getattr(result, "attack_type", "Unknown")
|
|
300
|
+
log_lines.append(
|
|
301
|
+
f" {i}. [yellow]{attack_type}[/yellow] → [{status_color}]{status}[/{status_color}]"
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
if not log_lines:
|
|
305
|
+
log_lines = ["[dim]No recent activity yet...[/dim]"]
|
|
306
|
+
|
|
307
|
+
activity_log.update("\n".join(log_lines))
|