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,624 @@
|
|
|
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
|
+
Attacks Tab
|
|
17
|
+
|
|
18
|
+
Execute and manage security attacks.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Optional
|
|
22
|
+
|
|
23
|
+
from textual.app import ComposeResult
|
|
24
|
+
from textual.binding import Binding
|
|
25
|
+
from textual.containers import Container, Horizontal, VerticalScroll
|
|
26
|
+
from textual.widgets import (
|
|
27
|
+
Button,
|
|
28
|
+
Input,
|
|
29
|
+
Label,
|
|
30
|
+
ProgressBar,
|
|
31
|
+
RichLog,
|
|
32
|
+
Select,
|
|
33
|
+
Static,
|
|
34
|
+
TabbedContent,
|
|
35
|
+
TabPane,
|
|
36
|
+
TextArea,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
from hackagent.cli.config import CLIConfig
|
|
40
|
+
from hackagent.cli.tui.widgets.actions import AgentActionsViewer
|
|
41
|
+
from hackagent.cli.tui.widgets.logs import AttackLogViewer
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class AttacksTab(Container):
|
|
45
|
+
"""Attacks tab for executing security attacks."""
|
|
46
|
+
|
|
47
|
+
DEFAULT_CSS = """
|
|
48
|
+
AttacksTab {
|
|
49
|
+
layout: horizontal;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
AttacksTab #attack-form-container {
|
|
53
|
+
width: 30%;
|
|
54
|
+
border-right: solid $primary;
|
|
55
|
+
padding: 1 2;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
AttacksTab #attack-monitor-container {
|
|
59
|
+
width: 70%;
|
|
60
|
+
}
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
BINDINGS = [
|
|
64
|
+
Binding("e", "execute_attack", "Execute"),
|
|
65
|
+
Binding("c", "clear_form", "Clear Form"),
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
def __init__(self, cli_config: CLIConfig, initial_data: Optional[dict] = None):
|
|
69
|
+
"""Initialize attacks tab.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
cli_config: CLI configuration object
|
|
73
|
+
initial_data: Initial data to pre-fill form fields
|
|
74
|
+
"""
|
|
75
|
+
super().__init__()
|
|
76
|
+
self.cli_config = cli_config
|
|
77
|
+
self.initial_data = initial_data or {}
|
|
78
|
+
|
|
79
|
+
def compose(self) -> ComposeResult:
|
|
80
|
+
"""Compose the attacks layout."""
|
|
81
|
+
# Split layout: Left side for form, Right side for logs
|
|
82
|
+
with Horizontal():
|
|
83
|
+
# Left side: Attack configuration form
|
|
84
|
+
with VerticalScroll(id="attack-form-container"):
|
|
85
|
+
yield Static("[bold cyan]Attack Configuration[/bold cyan]")
|
|
86
|
+
yield Static("") # Spacing
|
|
87
|
+
|
|
88
|
+
yield Label("Agent Name:")
|
|
89
|
+
yield Input(placeholder="e.g., weather-bot", id="agent-name")
|
|
90
|
+
yield Static("") # Spacing
|
|
91
|
+
|
|
92
|
+
yield Label("Agent Type:")
|
|
93
|
+
yield Select(
|
|
94
|
+
[
|
|
95
|
+
("Google ADK", "google-adk"),
|
|
96
|
+
("LiteLLM", "litellm"),
|
|
97
|
+
("LangChain", "langchain"),
|
|
98
|
+
("OpenAI SDK", "openai-sdk"),
|
|
99
|
+
("MCP", "mcp"),
|
|
100
|
+
("A2A", "a2a"),
|
|
101
|
+
],
|
|
102
|
+
id="agent-type",
|
|
103
|
+
value="google-adk",
|
|
104
|
+
)
|
|
105
|
+
yield Static("") # Spacing
|
|
106
|
+
|
|
107
|
+
yield Label("Endpoint URL:")
|
|
108
|
+
yield Input(
|
|
109
|
+
placeholder="e.g., http://localhost:8000", id="endpoint-url"
|
|
110
|
+
)
|
|
111
|
+
yield Static("") # Spacing
|
|
112
|
+
|
|
113
|
+
yield Label("Attack Strategy:")
|
|
114
|
+
yield Select(
|
|
115
|
+
[("AdvPrefix", "advprefix")],
|
|
116
|
+
id="attack-strategy",
|
|
117
|
+
value="advprefix",
|
|
118
|
+
)
|
|
119
|
+
yield Static("") # Spacing
|
|
120
|
+
|
|
121
|
+
yield Label("Goals (what you want the agent to do incorrectly):")
|
|
122
|
+
goals_area = TextArea("Return fake weather data", id="attack-goals")
|
|
123
|
+
goals_area.styles.height = 6
|
|
124
|
+
yield goals_area
|
|
125
|
+
yield Static("") # Spacing
|
|
126
|
+
|
|
127
|
+
yield Label("Timeout (seconds):")
|
|
128
|
+
yield Input(value="300", id="timeout")
|
|
129
|
+
yield Static("") # Spacing
|
|
130
|
+
yield Static("") # Extra spacing before buttons
|
|
131
|
+
|
|
132
|
+
yield Button("Execute Attack", id="execute-attack", variant="primary")
|
|
133
|
+
yield Button("Dry Run", id="dry-run", variant="default")
|
|
134
|
+
yield Button("Clear", id="clear-form", variant="error")
|
|
135
|
+
|
|
136
|
+
yield Static("") # Spacing
|
|
137
|
+
yield Static("") # Extra spacing after buttons
|
|
138
|
+
|
|
139
|
+
yield Static(
|
|
140
|
+
"[dim]Configure attack parameters and click Execute[/dim]",
|
|
141
|
+
id="execution-status",
|
|
142
|
+
)
|
|
143
|
+
yield ProgressBar(total=100, show_eta=True, id="attack-progress")
|
|
144
|
+
|
|
145
|
+
# Right side: Tabbed monitor with logs and actions
|
|
146
|
+
with Container(id="attack-monitor-container"):
|
|
147
|
+
with TabbedContent():
|
|
148
|
+
with TabPane("📋 Logs", id="logs-tab"):
|
|
149
|
+
yield AttackLogViewer(
|
|
150
|
+
title="Attack Execution Logs",
|
|
151
|
+
show_controls=True,
|
|
152
|
+
max_lines=1000,
|
|
153
|
+
id="attack-log-viewer",
|
|
154
|
+
)
|
|
155
|
+
with TabPane("🔧 Actions", id="actions-tab"):
|
|
156
|
+
yield AgentActionsViewer(
|
|
157
|
+
title="Agent Actions Inspector",
|
|
158
|
+
show_controls=True,
|
|
159
|
+
id="attack-actions-viewer",
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def on_mount(self) -> None:
|
|
163
|
+
"""Called when the tab is mounted."""
|
|
164
|
+
# Pre-fill form with initial data if provided
|
|
165
|
+
if self.initial_data:
|
|
166
|
+
self._prefill_form()
|
|
167
|
+
|
|
168
|
+
# Add initial messages after a short delay to ensure widgets are ready
|
|
169
|
+
self.call_after_refresh(self._add_initial_messages)
|
|
170
|
+
|
|
171
|
+
def _add_initial_messages(self) -> None:
|
|
172
|
+
"""Add initial welcome messages to the viewers."""
|
|
173
|
+
try:
|
|
174
|
+
log_viewer = self.query_one("#attack-log-viewer", AttackLogViewer)
|
|
175
|
+
actions_viewer = self.query_one(
|
|
176
|
+
"#attack-actions-viewer", AgentActionsViewer
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Get the RichLog directly to verify it exists
|
|
180
|
+
try:
|
|
181
|
+
rich_log = log_viewer.query_one("#attack-log-display", RichLog)
|
|
182
|
+
# Directly write to RichLog to test visibility
|
|
183
|
+
rich_log.write("[bold cyan]📋 Attack Log Viewer Ready[/bold cyan]")
|
|
184
|
+
rich_log.write(
|
|
185
|
+
"[yellow]Configure your attack and click Execute to begin[/yellow]"
|
|
186
|
+
)
|
|
187
|
+
except Exception:
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
# Try actions viewer
|
|
191
|
+
try:
|
|
192
|
+
actions_log = actions_viewer.query_one("#actions-display", RichLog)
|
|
193
|
+
actions_log.write(
|
|
194
|
+
"[bold green]🔧 Agent Actions Inspector Ready[/bold green]"
|
|
195
|
+
)
|
|
196
|
+
actions_log.write(
|
|
197
|
+
"[dim]Agent actions will appear here during execution[/dim]"
|
|
198
|
+
)
|
|
199
|
+
except Exception:
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
except Exception:
|
|
203
|
+
# If widgets aren't ready yet, skip initial messages
|
|
204
|
+
pass
|
|
205
|
+
|
|
206
|
+
def _prefill_form(self) -> None:
|
|
207
|
+
"""Pre-fill form fields with initial data."""
|
|
208
|
+
if "agent_name" in self.initial_data:
|
|
209
|
+
self.query_one("#agent-name", Input).value = self.initial_data["agent_name"]
|
|
210
|
+
if "agent_type" in self.initial_data:
|
|
211
|
+
self.query_one("#agent-type", Select).value = self.initial_data[
|
|
212
|
+
"agent_type"
|
|
213
|
+
]
|
|
214
|
+
if "endpoint" in self.initial_data:
|
|
215
|
+
self.query_one("#endpoint-url", Input).value = self.initial_data["endpoint"]
|
|
216
|
+
if "goals" in self.initial_data:
|
|
217
|
+
self.query_one("#attack-goals", TextArea).text = self.initial_data["goals"]
|
|
218
|
+
if "timeout" in self.initial_data:
|
|
219
|
+
self.query_one("#timeout", Input).value = str(self.initial_data["timeout"])
|
|
220
|
+
|
|
221
|
+
def on_button_pressed(self, event: Button.Pressed) -> None:
|
|
222
|
+
"""Handle button press events."""
|
|
223
|
+
if event.button.id == "execute-attack":
|
|
224
|
+
self._execute_attack(dry_run=False)
|
|
225
|
+
elif event.button.id == "dry-run":
|
|
226
|
+
self._execute_attack(dry_run=True)
|
|
227
|
+
elif event.button.id == "clear-form":
|
|
228
|
+
self._clear_form()
|
|
229
|
+
|
|
230
|
+
def _execute_attack(self, dry_run: bool = False) -> None:
|
|
231
|
+
"""Execute the configured attack.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
dry_run: Whether to run in dry-run mode
|
|
235
|
+
"""
|
|
236
|
+
# Get form values
|
|
237
|
+
from textual.widgets._select import NoSelection
|
|
238
|
+
|
|
239
|
+
agent_name = self.query_one("#agent-name", Input).value
|
|
240
|
+
agent_type_raw = self.query_one("#agent-type", Select).value
|
|
241
|
+
endpoint = self.query_one("#endpoint-url", Input).value
|
|
242
|
+
strategy_raw = self.query_one("#attack-strategy", Select).value
|
|
243
|
+
goals = self.query_one("#attack-goals", TextArea).text
|
|
244
|
+
timeout = self.query_one("#timeout", Input).value
|
|
245
|
+
|
|
246
|
+
# Validate inputs
|
|
247
|
+
if not agent_name:
|
|
248
|
+
return
|
|
249
|
+
if isinstance(agent_type_raw, NoSelection) or not agent_type_raw:
|
|
250
|
+
return
|
|
251
|
+
if not endpoint:
|
|
252
|
+
return
|
|
253
|
+
if isinstance(strategy_raw, NoSelection) or not strategy_raw:
|
|
254
|
+
return
|
|
255
|
+
if not goals:
|
|
256
|
+
return
|
|
257
|
+
|
|
258
|
+
# Validate timeout is a valid integer
|
|
259
|
+
try:
|
|
260
|
+
timeout_int = int(timeout)
|
|
261
|
+
if timeout_int <= 0:
|
|
262
|
+
return
|
|
263
|
+
except ValueError:
|
|
264
|
+
return
|
|
265
|
+
|
|
266
|
+
# Convert to strings (they should be strings after validation)
|
|
267
|
+
agent_type = str(agent_type_raw)
|
|
268
|
+
strategy = str(strategy_raw)
|
|
269
|
+
|
|
270
|
+
status_widget = self.query_one("#execution-status", Static)
|
|
271
|
+
progress_bar = self.query_one("#attack-progress", ProgressBar)
|
|
272
|
+
|
|
273
|
+
if dry_run:
|
|
274
|
+
status_widget.update(
|
|
275
|
+
f"""[bold yellow]Dry Run Mode[/bold yellow]
|
|
276
|
+
|
|
277
|
+
[bold]Agent:[/bold] {agent_name}
|
|
278
|
+
[bold]Type:[/bold] {agent_type}
|
|
279
|
+
[bold]Endpoint:[/bold] {endpoint}
|
|
280
|
+
[bold]Strategy:[/bold] {strategy}
|
|
281
|
+
[bold]Goals:[/bold] {goals}
|
|
282
|
+
[bold]Timeout:[/bold] {timeout}s
|
|
283
|
+
|
|
284
|
+
[green]✅ Configuration validation passed[/green]
|
|
285
|
+
[dim]Remove dry-run flag to execute the attack[/dim]"""
|
|
286
|
+
)
|
|
287
|
+
else:
|
|
288
|
+
# Actually execute the attack
|
|
289
|
+
status_widget.update(
|
|
290
|
+
f"""[bold cyan]🚀 Initializing Attack...[/bold cyan]
|
|
291
|
+
|
|
292
|
+
[bold]Agent:[/bold] {agent_name}
|
|
293
|
+
[bold]Type:[/bold] {agent_type}
|
|
294
|
+
[bold]Endpoint:[/bold] {endpoint}
|
|
295
|
+
[bold]Strategy:[/bold] {strategy}
|
|
296
|
+
[bold]Goals:[/bold] {goals}
|
|
297
|
+
[bold]Timeout:[/bold] {timeout}s
|
|
298
|
+
|
|
299
|
+
[yellow]⏳ Connecting to agent and preparing attack...[/yellow]"""
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
# Show immediate feedback - progress starting
|
|
303
|
+
progress_bar.update(progress=5)
|
|
304
|
+
status_widget.update(
|
|
305
|
+
f"""[bold cyan]🚀 Starting Attack...[/bold cyan]
|
|
306
|
+
|
|
307
|
+
[bold]Agent:[/bold] {agent_name}
|
|
308
|
+
[bold]Type:[/bold] {agent_type}
|
|
309
|
+
[bold]Endpoint:[/bold] {endpoint}
|
|
310
|
+
[bold]Strategy:[/bold] {strategy}
|
|
311
|
+
[bold]Goals:[/bold] {goals}
|
|
312
|
+
|
|
313
|
+
[yellow]⏳ Launching attack execution...[/yellow]
|
|
314
|
+
[dim]Progress: 5%[/dim]"""
|
|
315
|
+
)
|
|
316
|
+
|
|
317
|
+
# Run attack in background thread
|
|
318
|
+
# Use lambda to pass arguments to the worker function
|
|
319
|
+
try:
|
|
320
|
+
self.run_worker(
|
|
321
|
+
lambda: self._run_attack_async(
|
|
322
|
+
agent_name, agent_type, endpoint, goals, int(timeout)
|
|
323
|
+
),
|
|
324
|
+
thread=True,
|
|
325
|
+
exclusive=True,
|
|
326
|
+
name="attack-execution",
|
|
327
|
+
)
|
|
328
|
+
except Exception as e:
|
|
329
|
+
# If worker fails to start, show error immediately
|
|
330
|
+
status_widget.update(
|
|
331
|
+
f"""[bold red]❌ Failed to Start Attack[/bold red]
|
|
332
|
+
|
|
333
|
+
[bold]Error:[/bold] {str(e)}
|
|
334
|
+
|
|
335
|
+
[red]Could not start attack worker thread.[/red]
|
|
336
|
+
[dim]This might be a configuration or system issue.[/dim]"""
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
def _run_attack_async(
|
|
340
|
+
self, agent_name: str, agent_type: str, endpoint: str, goals: str, timeout: int
|
|
341
|
+
) -> None:
|
|
342
|
+
"""Run attack in background thread with progress updates.
|
|
343
|
+
|
|
344
|
+
Args:
|
|
345
|
+
agent_name: Name of the target agent
|
|
346
|
+
agent_type: Type of agent (google-adk, litellm)
|
|
347
|
+
endpoint: Agent endpoint URL
|
|
348
|
+
goals: Attack goals
|
|
349
|
+
timeout: Timeout in seconds
|
|
350
|
+
"""
|
|
351
|
+
import io
|
|
352
|
+
import logging
|
|
353
|
+
import os
|
|
354
|
+
import sys
|
|
355
|
+
import time
|
|
356
|
+
|
|
357
|
+
from hackagent import HackAgent
|
|
358
|
+
from hackagent.cli.utils import get_agent_type_enum
|
|
359
|
+
|
|
360
|
+
status_widget = self.query_one("#execution-status", Static)
|
|
361
|
+
progress_bar = self.query_one("#attack-progress", ProgressBar)
|
|
362
|
+
log_viewer = self.query_one("#attack-log-viewer", AttackLogViewer)
|
|
363
|
+
actions_viewer = self.query_one("#attack-actions-viewer", AgentActionsViewer)
|
|
364
|
+
|
|
365
|
+
# Clear previous logs and actions
|
|
366
|
+
self.app.call_from_thread(log_viewer.clear_logs)
|
|
367
|
+
self.app.call_from_thread(actions_viewer.clear_actions)
|
|
368
|
+
self.app.call_from_thread(
|
|
369
|
+
log_viewer.add_log,
|
|
370
|
+
f"🚀 Starting attack execution for agent: {agent_name}",
|
|
371
|
+
"INFO",
|
|
372
|
+
)
|
|
373
|
+
self.app.call_from_thread(
|
|
374
|
+
actions_viewer.add_step_separator,
|
|
375
|
+
f"Attack Initialization: {agent_name}",
|
|
376
|
+
1,
|
|
377
|
+
)
|
|
378
|
+
|
|
379
|
+
# CRITICAL: Comprehensive rich suppression to prevent black screen
|
|
380
|
+
# Multiple layers of defense to prevent ANY rich output during TUI mode
|
|
381
|
+
|
|
382
|
+
# 1. Set environment variable to disable rich features
|
|
383
|
+
saved_term = os.environ.get("TERM")
|
|
384
|
+
os.environ["TERM"] = "dumb" # Disable rich color/formatting
|
|
385
|
+
|
|
386
|
+
# 2. Set up custom logging handlers for TUI
|
|
387
|
+
hackagent_logger = logging.getLogger("hackagent")
|
|
388
|
+
saved_handlers = hackagent_logger.handlers.copy()
|
|
389
|
+
saved_level = hackagent_logger.level
|
|
390
|
+
|
|
391
|
+
# Remove existing handlers
|
|
392
|
+
for handler in hackagent_logger.handlers[:]:
|
|
393
|
+
hackagent_logger.removeHandler(handler)
|
|
394
|
+
|
|
395
|
+
# Add TUI-specific handlers
|
|
396
|
+
from hackagent.cli.tui.logger import TUILogHandler
|
|
397
|
+
|
|
398
|
+
# Handler for log messages
|
|
399
|
+
tui_log_handler = TUILogHandler(
|
|
400
|
+
app=self.app,
|
|
401
|
+
callback=log_viewer.add_log,
|
|
402
|
+
level=logging.INFO,
|
|
403
|
+
)
|
|
404
|
+
hackagent_logger.addHandler(tui_log_handler)
|
|
405
|
+
hackagent_logger.setLevel(logging.INFO)
|
|
406
|
+
|
|
407
|
+
# Suppress other noisy loggers
|
|
408
|
+
logging.getLogger("httpx").setLevel(logging.CRITICAL)
|
|
409
|
+
logging.getLogger("litellm").setLevel(logging.CRITICAL)
|
|
410
|
+
|
|
411
|
+
# 3. Disable Rich progress bars by setting environment variable
|
|
412
|
+
# This prevents Rich from trying to use terminal features that conflict with TUI
|
|
413
|
+
os.environ["FORCE_COLOR"] = "0"
|
|
414
|
+
os.environ["NO_COLOR"] = "1"
|
|
415
|
+
|
|
416
|
+
# 4. Redirect stdout/stderr as final safeguard
|
|
417
|
+
original_stdout = sys.stdout
|
|
418
|
+
original_stderr = sys.stderr
|
|
419
|
+
sys.stdout = io.StringIO()
|
|
420
|
+
sys.stderr = io.StringIO()
|
|
421
|
+
|
|
422
|
+
try:
|
|
423
|
+
# Convert agent type
|
|
424
|
+
agent_type_enum = get_agent_type_enum(agent_type)
|
|
425
|
+
|
|
426
|
+
# Update status - 10% progress
|
|
427
|
+
self.app.call_from_thread(progress_bar.update, progress=10)
|
|
428
|
+
self.app.call_from_thread(
|
|
429
|
+
status_widget.update,
|
|
430
|
+
f"""[bold cyan]🔧 Initializing HackAgent...[/bold cyan]
|
|
431
|
+
|
|
432
|
+
[bold]Agent:[/bold] {agent_name}
|
|
433
|
+
[bold]Type:[/bold] {agent_type}
|
|
434
|
+
[bold]Endpoint:[/bold] {endpoint}
|
|
435
|
+
|
|
436
|
+
[yellow]⏳ Setting up attack infrastructure...[/yellow]
|
|
437
|
+
[dim]Progress: 10%[/dim]""",
|
|
438
|
+
)
|
|
439
|
+
# Initialize HackAgent - 20% progress
|
|
440
|
+
self.app.call_from_thread(progress_bar.update, progress=20)
|
|
441
|
+
|
|
442
|
+
agent = HackAgent(
|
|
443
|
+
name=agent_name,
|
|
444
|
+
endpoint=endpoint,
|
|
445
|
+
agent_type=agent_type_enum,
|
|
446
|
+
api_key=self.cli_config.api_key,
|
|
447
|
+
base_url=self.cli_config.base_url,
|
|
448
|
+
timeout=5.0, # 5 second timeout for API calls
|
|
449
|
+
)
|
|
450
|
+
|
|
451
|
+
# Build attack configuration - 30% progress
|
|
452
|
+
self.app.call_from_thread(progress_bar.update, progress=30)
|
|
453
|
+
attack_config = {
|
|
454
|
+
"attack_type": "advprefix",
|
|
455
|
+
"goals": [goals],
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
# Update status - 40% progress, starting attack
|
|
459
|
+
self.app.call_from_thread(progress_bar.update, progress=40)
|
|
460
|
+
self.app.call_from_thread(
|
|
461
|
+
status_widget.update,
|
|
462
|
+
f"""[bold cyan]⚔️ Executing AdvPrefix Attack...[/bold cyan]
|
|
463
|
+
|
|
464
|
+
[bold]Agent:[/bold] {agent_name}
|
|
465
|
+
[bold]Goals:[/bold] {goals}
|
|
466
|
+
|
|
467
|
+
[yellow]⏳ Attack in progress... This may take several minutes...[/yellow]
|
|
468
|
+
[dim]Generating adversarial prefixes and testing against target agent...[/dim]
|
|
469
|
+
[dim]Progress: 40%[/dim]""",
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
start_time = time.time()
|
|
473
|
+
|
|
474
|
+
# Set up TUI logging callback
|
|
475
|
+
def log_callback(message: str, level: str) -> None:
|
|
476
|
+
"""Callback for TUI log handler"""
|
|
477
|
+
log_viewer.add_log(message, level)
|
|
478
|
+
|
|
479
|
+
# Create and attach TUI log handler to the attack
|
|
480
|
+
# This will be picked up by the @with_tui_logging decorator
|
|
481
|
+
|
|
482
|
+
# Execute attack - simulate progress from 50% to 90%
|
|
483
|
+
# Start a background thread to update progress
|
|
484
|
+
import threading
|
|
485
|
+
|
|
486
|
+
stop_progress = threading.Event()
|
|
487
|
+
|
|
488
|
+
def update_progress_gradually():
|
|
489
|
+
"""Gradually update progress during attack execution"""
|
|
490
|
+
for progress in range(50, 91, 5):
|
|
491
|
+
if stop_progress.is_set():
|
|
492
|
+
break
|
|
493
|
+
self.app.call_from_thread(progress_bar.update, progress=progress)
|
|
494
|
+
time.sleep(2) # Update every 2 seconds
|
|
495
|
+
|
|
496
|
+
progress_thread = threading.Thread(
|
|
497
|
+
target=update_progress_gradually, daemon=True
|
|
498
|
+
)
|
|
499
|
+
progress_thread.start()
|
|
500
|
+
|
|
501
|
+
# Attach TUI log handler before execution
|
|
502
|
+
# The attack strategy will internally call the attack class's run() method
|
|
503
|
+
# which has the @with_tui_logging decorator
|
|
504
|
+
try:
|
|
505
|
+
# Note: We need to access the attack instance to attach the handler
|
|
506
|
+
# This will be done by modifying the agent.hack() flow or by
|
|
507
|
+
# directly accessing the attack strategy instance
|
|
508
|
+
|
|
509
|
+
# For now, we'll execute the attack and the decorator will handle logging
|
|
510
|
+
# The handler attachment needs to happen in the strategy execution
|
|
511
|
+
results = agent.hack(
|
|
512
|
+
attack_config=attack_config,
|
|
513
|
+
run_config_override={"timeout": timeout},
|
|
514
|
+
fail_on_run_error=True,
|
|
515
|
+
# Pass TUI context for logging
|
|
516
|
+
_tui_app=self.app,
|
|
517
|
+
_tui_log_callback=log_callback,
|
|
518
|
+
)
|
|
519
|
+
finally:
|
|
520
|
+
stop_progress.set()
|
|
521
|
+
progress_thread.join(timeout=1)
|
|
522
|
+
# Restore stdout/stderr
|
|
523
|
+
sys.stdout = original_stdout
|
|
524
|
+
sys.stderr = original_stderr
|
|
525
|
+
|
|
526
|
+
# Remove TUI handler from all loggers
|
|
527
|
+
if tui_log_handler in hackagent_logger.handlers:
|
|
528
|
+
hackagent_logger.removeHandler(tui_log_handler)
|
|
529
|
+
|
|
530
|
+
# Restore logging configuration
|
|
531
|
+
hackagent_logger.setLevel(saved_level)
|
|
532
|
+
for handler in saved_handlers:
|
|
533
|
+
hackagent_logger.addHandler(handler)
|
|
534
|
+
|
|
535
|
+
# Restore environment variables
|
|
536
|
+
if saved_term is not None:
|
|
537
|
+
os.environ["TERM"] = saved_term
|
|
538
|
+
elif "TERM" in os.environ:
|
|
539
|
+
del os.environ["TERM"]
|
|
540
|
+
|
|
541
|
+
if "FORCE_COLOR" in os.environ:
|
|
542
|
+
del os.environ["FORCE_COLOR"]
|
|
543
|
+
if "NO_COLOR" in os.environ:
|
|
544
|
+
del os.environ["NO_COLOR"]
|
|
545
|
+
|
|
546
|
+
duration = time.time() - start_time
|
|
547
|
+
|
|
548
|
+
# Complete progress - 100%
|
|
549
|
+
self.app.call_from_thread(progress_bar.update, progress=100)
|
|
550
|
+
|
|
551
|
+
# Display success
|
|
552
|
+
result_count = len(results) if hasattr(results, "__len__") else "Unknown"
|
|
553
|
+
self.app.call_from_thread(
|
|
554
|
+
status_widget.update,
|
|
555
|
+
f"""[bold green]✅ Attack Completed Successfully![/bold green]
|
|
556
|
+
|
|
557
|
+
[bold]Agent:[/bold] {agent_name}
|
|
558
|
+
[bold]Duration:[/bold] {duration:.1f} seconds
|
|
559
|
+
[bold]Results Generated:[/bold] {result_count}
|
|
560
|
+
|
|
561
|
+
[green]Attack execution finished![/green]
|
|
562
|
+
[dim]Check the Results tab to view detailed attack results.[/dim]
|
|
563
|
+
[dim]Results have been saved to the HackAgent platform.[/dim]""",
|
|
564
|
+
)
|
|
565
|
+
|
|
566
|
+
except Exception as e:
|
|
567
|
+
# Display error
|
|
568
|
+
self.app.call_from_thread(progress_bar.update, progress=0)
|
|
569
|
+
self.app.call_from_thread(
|
|
570
|
+
status_widget.update,
|
|
571
|
+
f"""[bold red]❌ Attack Failed[/bold red]
|
|
572
|
+
|
|
573
|
+
[bold]Agent:[/bold] {agent_name}
|
|
574
|
+
[bold]Error:[/bold] {str(e)}
|
|
575
|
+
|
|
576
|
+
[red]Attack execution encountered an error.[/red]
|
|
577
|
+
[dim]Please check your configuration and try again.[/dim]
|
|
578
|
+
[dim]Ensure the agent endpoint is accessible and API key is valid.[/dim]""",
|
|
579
|
+
)
|
|
580
|
+
|
|
581
|
+
finally:
|
|
582
|
+
# Always restore stdout/stderr and clean up handlers
|
|
583
|
+
sys.stdout = original_stdout
|
|
584
|
+
sys.stderr = original_stderr
|
|
585
|
+
|
|
586
|
+
# Remove TUI handler from all loggers
|
|
587
|
+
try:
|
|
588
|
+
if tui_log_handler in hackagent_logger.handlers:
|
|
589
|
+
hackagent_logger.removeHandler(tui_log_handler)
|
|
590
|
+
except Exception:
|
|
591
|
+
pass # Handler cleanup errors shouldn't fail silently
|
|
592
|
+
|
|
593
|
+
# Restore logging configuration
|
|
594
|
+
hackagent_logger.setLevel(saved_level)
|
|
595
|
+
for handler in saved_handlers:
|
|
596
|
+
hackagent_logger.addHandler(handler)
|
|
597
|
+
|
|
598
|
+
# Restore environment variables
|
|
599
|
+
if saved_term is not None:
|
|
600
|
+
os.environ["TERM"] = saved_term
|
|
601
|
+
elif "TERM" in os.environ:
|
|
602
|
+
del os.environ["TERM"]
|
|
603
|
+
|
|
604
|
+
if "FORCE_COLOR" in os.environ:
|
|
605
|
+
del os.environ["FORCE_COLOR"]
|
|
606
|
+
if "NO_COLOR" in os.environ:
|
|
607
|
+
del os.environ["NO_COLOR"]
|
|
608
|
+
|
|
609
|
+
def _clear_form(self) -> None:
|
|
610
|
+
"""Clear all form fields."""
|
|
611
|
+
self.query_one("#agent-name", Input).value = ""
|
|
612
|
+
self.query_one("#endpoint-url", Input).value = ""
|
|
613
|
+
self.query_one("#attack-goals", TextArea).text = "Return fake weather data"
|
|
614
|
+
self.query_one("#timeout", Input).value = "300"
|
|
615
|
+
|
|
616
|
+
status_widget = self.query_one("#execution-status", Static)
|
|
617
|
+
progress_bar = self.query_one("#attack-progress", ProgressBar)
|
|
618
|
+
status_widget.update("[dim]Configure attack parameters and click Execute[/dim]")
|
|
619
|
+
progress_bar.update(progress=0)
|
|
620
|
+
|
|
621
|
+
def refresh_data(self) -> None:
|
|
622
|
+
"""Refresh attacks data."""
|
|
623
|
+
# No dynamic data to refresh for attacks list
|
|
624
|
+
pass
|