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,200 @@
|
|
|
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
|
+
TUI Actions Logger
|
|
17
|
+
|
|
18
|
+
Custom logging handler that extracts and displays agent actions (tool calls, HTTP requests)
|
|
19
|
+
in the TUI actions viewer.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
from typing import Any, Callable
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TUIActionsHandler(logging.Handler):
|
|
28
|
+
"""
|
|
29
|
+
Custom logging handler that extracts agent actions from log messages
|
|
30
|
+
and displays them in the TUI actions viewer.
|
|
31
|
+
|
|
32
|
+
This handler parses log messages to identify:
|
|
33
|
+
- HTTP requests to agent endpoints
|
|
34
|
+
- Tool/function calls with arguments
|
|
35
|
+
- ADK agent events (tool_call, tool_result, llm_response)
|
|
36
|
+
- API responses
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(
|
|
40
|
+
self,
|
|
41
|
+
actions_callback: Callable,
|
|
42
|
+
app_callback: Callable,
|
|
43
|
+
level: int = logging.INFO,
|
|
44
|
+
):
|
|
45
|
+
"""
|
|
46
|
+
Initialize the actions handler.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
actions_callback: Callback function to add actions to the viewer
|
|
50
|
+
Signature: (action_type: str, **kwargs)
|
|
51
|
+
app_callback: Callback to call actions from thread-safe context
|
|
52
|
+
level: Logging level
|
|
53
|
+
"""
|
|
54
|
+
super().__init__(level)
|
|
55
|
+
self.actions_callback = actions_callback
|
|
56
|
+
self.app_callback = app_callback
|
|
57
|
+
|
|
58
|
+
def emit(self, record: logging.LogRecord) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Process a log record and extract action information.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
record: The log record to process
|
|
64
|
+
"""
|
|
65
|
+
try:
|
|
66
|
+
message = record.getMessage()
|
|
67
|
+
|
|
68
|
+
# Pattern 1: HTTP requests to agent endpoints
|
|
69
|
+
# Example: "🌐 Sending request to agent endpoint: http://localhost:8000/run"
|
|
70
|
+
if "Sending request to agent endpoint:" in message or "🌐" in message:
|
|
71
|
+
url_match = re.search(r"(https?://[^\s]+)", message)
|
|
72
|
+
if url_match:
|
|
73
|
+
url = url_match.group(1)
|
|
74
|
+
method = "POST" # Most agent requests are POST
|
|
75
|
+
self.app_callback(
|
|
76
|
+
self.actions_callback,
|
|
77
|
+
"http_request",
|
|
78
|
+
method=method,
|
|
79
|
+
url=url,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Pattern 2: Tool calls (from _log_agent_actions in completions.py)
|
|
83
|
+
# Example: "🔧 Agent actions for prefix #1:"
|
|
84
|
+
elif "🔧 Agent actions" in message or "Tool:" in message:
|
|
85
|
+
# Extract tool name if present
|
|
86
|
+
tool_match = re.search(r"Tool:\s*(\w+)", message)
|
|
87
|
+
if tool_match:
|
|
88
|
+
tool_name = tool_match.group(1)
|
|
89
|
+
self.app_callback(
|
|
90
|
+
self.actions_callback,
|
|
91
|
+
"tool_call",
|
|
92
|
+
tool_name=tool_name,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Pattern 3: ADK events
|
|
96
|
+
# Example: "🤖 ADK Agent actions for prefix #1:"
|
|
97
|
+
elif "🤖 ADK Agent actions" in message or "ADK" in message:
|
|
98
|
+
# Check subsequent messages for event details
|
|
99
|
+
if "Tool Call:" in message:
|
|
100
|
+
tool_match = re.search(r"Tool Call:\s*(\w+)", message)
|
|
101
|
+
if tool_match:
|
|
102
|
+
tool_name = tool_match.group(1)
|
|
103
|
+
self.app_callback(
|
|
104
|
+
self.actions_callback,
|
|
105
|
+
"adk_event",
|
|
106
|
+
event_type="tool_call",
|
|
107
|
+
tool_name=tool_name,
|
|
108
|
+
)
|
|
109
|
+
elif "Tool Result:" in message:
|
|
110
|
+
tool_match = re.search(r"Tool Result:\s*(\w+)", message)
|
|
111
|
+
if tool_match:
|
|
112
|
+
tool_name = tool_match.group(1)
|
|
113
|
+
self.app_callback(
|
|
114
|
+
self.actions_callback,
|
|
115
|
+
"adk_event",
|
|
116
|
+
event_type="tool_result",
|
|
117
|
+
tool_name=tool_name,
|
|
118
|
+
)
|
|
119
|
+
elif "LLM Response:" in message:
|
|
120
|
+
content_match = re.search(r"LLM Response:\s*(.+)", message)
|
|
121
|
+
content = content_match.group(1) if content_match else ""
|
|
122
|
+
self.app_callback(
|
|
123
|
+
self.actions_callback,
|
|
124
|
+
"adk_event",
|
|
125
|
+
event_type="llm_response",
|
|
126
|
+
content=content,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Pattern 4: Agent responses
|
|
130
|
+
# Example: "✅ Agent responded with status 200"
|
|
131
|
+
elif "Agent responded" in message or "✅" in message:
|
|
132
|
+
status_match = re.search(r"status\s+(\d+)", message)
|
|
133
|
+
if status_match:
|
|
134
|
+
# This indicates a successful HTTP response
|
|
135
|
+
pass # Could add response visualization here
|
|
136
|
+
|
|
137
|
+
# Pattern 5: Model queries (LiteLLM)
|
|
138
|
+
# Example: "🌐 Querying model gpt-4"
|
|
139
|
+
elif "Querying model" in message:
|
|
140
|
+
model_match = re.search(r"model\s+(\S+)", message)
|
|
141
|
+
if model_match:
|
|
142
|
+
model_name = model_match.group(1)
|
|
143
|
+
self.app_callback(
|
|
144
|
+
self.actions_callback,
|
|
145
|
+
"llm_query",
|
|
146
|
+
model_name=model_name,
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
except Exception:
|
|
150
|
+
# Silently fail to avoid breaking the logging system
|
|
151
|
+
pass
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def extract_action_data_from_log(
|
|
155
|
+
actions_viewer: Any,
|
|
156
|
+
action_type: str,
|
|
157
|
+
**kwargs: Any,
|
|
158
|
+
) -> None:
|
|
159
|
+
"""
|
|
160
|
+
Extract and display action data in the actions viewer.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
actions_viewer: The AgentActionsViewer widget
|
|
164
|
+
action_type: Type of action (http_request, tool_call, adk_event, etc.)
|
|
165
|
+
**kwargs: Action-specific parameters
|
|
166
|
+
"""
|
|
167
|
+
try:
|
|
168
|
+
if action_type == "http_request":
|
|
169
|
+
actions_viewer.add_http_request(
|
|
170
|
+
method=kwargs.get("method", "POST"),
|
|
171
|
+
url=kwargs.get("url", ""),
|
|
172
|
+
headers=kwargs.get("headers"),
|
|
173
|
+
payload=kwargs.get("payload"),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
elif action_type == "tool_call":
|
|
177
|
+
actions_viewer.add_tool_call(
|
|
178
|
+
tool_name=kwargs.get("tool_name", "unknown"),
|
|
179
|
+
arguments=kwargs.get("arguments"),
|
|
180
|
+
result=kwargs.get("result"),
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
elif action_type == "adk_event":
|
|
184
|
+
event_type = kwargs.get("event_type", "unknown")
|
|
185
|
+
event_data = {
|
|
186
|
+
"tool_name": kwargs.get("tool_name", ""),
|
|
187
|
+
"tool_input": kwargs.get("tool_input", {}),
|
|
188
|
+
"result": kwargs.get("result", ""),
|
|
189
|
+
"content": kwargs.get("content", ""),
|
|
190
|
+
}
|
|
191
|
+
actions_viewer.add_adk_event(event_type, event_data)
|
|
192
|
+
|
|
193
|
+
elif action_type == "llm_query":
|
|
194
|
+
# Display LLM query as a special kind of action
|
|
195
|
+
model_name = kwargs.get("model_name", "unknown")
|
|
196
|
+
actions_viewer.add_step_separator(f"LLM Query: {model_name}")
|
|
197
|
+
|
|
198
|
+
except Exception:
|
|
199
|
+
# Silently fail to avoid breaking the UI
|
|
200
|
+
pass
|
hackagent/cli/tui/app.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
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
|
+
Main TUI Application
|
|
17
|
+
|
|
18
|
+
Full-screen tabbed interface for HackAgent.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from rich.text import Text
|
|
24
|
+
from textual.app import App, ComposeResult
|
|
25
|
+
from textual.binding import Binding
|
|
26
|
+
from textual.containers import Container
|
|
27
|
+
from textual.widgets import Footer, Static, TabbedContent, TabPane
|
|
28
|
+
|
|
29
|
+
from hackagent.cli.config import CLIConfig
|
|
30
|
+
from hackagent.cli.tui.views.agents import AgentsTab
|
|
31
|
+
from hackagent.cli.tui.views.attacks import AttacksTab
|
|
32
|
+
from hackagent.cli.tui.views.config import ConfigTab
|
|
33
|
+
from hackagent.cli.tui.views.results import ResultsTab
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HackAgentHeader(Container):
|
|
37
|
+
"""Custom header with ASCII logo"""
|
|
38
|
+
|
|
39
|
+
DEFAULT_CSS = """
|
|
40
|
+
HackAgentHeader {
|
|
41
|
+
dock: top;
|
|
42
|
+
width: 100%;
|
|
43
|
+
height: 7;
|
|
44
|
+
padding: 0 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
HackAgentHeader Static {
|
|
48
|
+
color: #ff0000;
|
|
49
|
+
text-style: bold;
|
|
50
|
+
width: 100%;
|
|
51
|
+
content-align: center middle;
|
|
52
|
+
}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
def compose(self) -> ComposeResult:
|
|
56
|
+
from hackagent.utils import HACKAGENT
|
|
57
|
+
|
|
58
|
+
# Display the ASCII logo as-is (now side-by-side format)
|
|
59
|
+
logo_text = Text(HACKAGENT, style="bold red")
|
|
60
|
+
yield Static(logo_text)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class HackAgentTUI(App):
|
|
64
|
+
"""HackAgent Terminal User Interface Application"""
|
|
65
|
+
|
|
66
|
+
CSS = """
|
|
67
|
+
Screen {
|
|
68
|
+
background: $surface;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
Header {
|
|
72
|
+
background: #8b0000; /* dark red - HackAgent brand color */
|
|
73
|
+
color: #ffffff;
|
|
74
|
+
height: 3;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
Footer {
|
|
78
|
+
background: #2b0000; /* darker red */
|
|
79
|
+
color: #ffffff;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
TabbedContent {
|
|
83
|
+
height: 100%;
|
|
84
|
+
border: solid #ff0000; /* red - HackAgent brand color */
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
TabPane {
|
|
88
|
+
padding: 1 2;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
TabbedContent > ContentSwitcher > * > * {
|
|
92
|
+
background: $surface;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
Tabs {
|
|
96
|
+
background: #2b0000;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
Tab {
|
|
100
|
+
color: #cccccc;
|
|
101
|
+
background: #2b0000;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
Tab.-active {
|
|
105
|
+
color: #ffffff;
|
|
106
|
+
background: #8b0000; /* dark red when active */
|
|
107
|
+
text-style: bold;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
Tab:hover {
|
|
111
|
+
background: #5b0000;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.title-bar {
|
|
115
|
+
dock: top;
|
|
116
|
+
width: 100%;
|
|
117
|
+
background: #8b0000;
|
|
118
|
+
color: #ffffff;
|
|
119
|
+
height: 3;
|
|
120
|
+
content-align: center middle;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.section {
|
|
124
|
+
border: solid #ff0000;
|
|
125
|
+
padding: 1;
|
|
126
|
+
margin: 1;
|
|
127
|
+
height: auto;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
.info-box {
|
|
131
|
+
background: $panel;
|
|
132
|
+
border: solid #ff0000;
|
|
133
|
+
padding: 1;
|
|
134
|
+
margin: 1;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
Button {
|
|
138
|
+
margin: 1;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
Button.-primary {
|
|
142
|
+
background: #8b0000;
|
|
143
|
+
color: #ffffff;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
Button.-primary:hover {
|
|
147
|
+
background: #ff0000;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
DataTable {
|
|
151
|
+
height: 100%;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
DataTable > .datatable--header {
|
|
155
|
+
background: #8b0000;
|
|
156
|
+
color: #ffffff;
|
|
157
|
+
text-style: bold;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
DataTable > .datatable--cursor {
|
|
161
|
+
background: #5b0000;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* Results tab specific styles - horizontal split 20-80 */
|
|
165
|
+
ResultsTab #results-left-panel {
|
|
166
|
+
border-right: solid #ff0000;
|
|
167
|
+
background: $panel;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
ResultsTab #results-right-panel {
|
|
171
|
+
background: $panel;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
ResultsTab #results-title {
|
|
175
|
+
height: 3;
|
|
176
|
+
width: 100%;
|
|
177
|
+
text-align: center;
|
|
178
|
+
background: #8b0000;
|
|
179
|
+
color: #ffffff;
|
|
180
|
+
padding: 1;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
ResultsTab #details-title {
|
|
184
|
+
height: 3;
|
|
185
|
+
width: 100%;
|
|
186
|
+
text-align: center;
|
|
187
|
+
background: #8b0000;
|
|
188
|
+
color: #ffffff;
|
|
189
|
+
padding: 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
ResultsTab .toolbar {
|
|
193
|
+
height: 3;
|
|
194
|
+
width: 100%;
|
|
195
|
+
padding: 0 1;
|
|
196
|
+
}
|
|
197
|
+
"""
|
|
198
|
+
|
|
199
|
+
TITLE = "🔴 HACKAGENT 🔴 - AI Security Testing Toolkit"
|
|
200
|
+
SUB_TITLE = "Red Team Security Interface"
|
|
201
|
+
|
|
202
|
+
BINDINGS = [
|
|
203
|
+
Binding("q", "quit", "Quit", priority=True),
|
|
204
|
+
Binding("a", "switch_tab('agents')", "Agents", show=False),
|
|
205
|
+
Binding("k", "switch_tab('attacks')", "Attacks", show=False),
|
|
206
|
+
Binding("r", "switch_tab('results')", "Results", show=False),
|
|
207
|
+
Binding("c", "switch_tab('config')", "Config", show=False),
|
|
208
|
+
Binding("f5", "refresh", "Refresh", show=True),
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
def __init__(
|
|
212
|
+
self,
|
|
213
|
+
cli_config: CLIConfig,
|
|
214
|
+
initial_tab: str = "agents",
|
|
215
|
+
initial_data: dict[Any, Any] | None = None,
|
|
216
|
+
):
|
|
217
|
+
"""Initialize the TUI application.
|
|
218
|
+
|
|
219
|
+
Args:
|
|
220
|
+
cli_config: CLI configuration object
|
|
221
|
+
initial_tab: Which tab to show initially (default: "agents")
|
|
222
|
+
initial_data: Initial data to pre-fill in the tab (default: None)
|
|
223
|
+
"""
|
|
224
|
+
super().__init__()
|
|
225
|
+
self.cli_config = cli_config
|
|
226
|
+
self.initial_tab = initial_tab
|
|
227
|
+
self.initial_data = initial_data or {}
|
|
228
|
+
self.dark = True # Use dark theme by default
|
|
229
|
+
|
|
230
|
+
def compose(self) -> ComposeResult:
|
|
231
|
+
"""Compose the UI layout."""
|
|
232
|
+
yield HackAgentHeader()
|
|
233
|
+
|
|
234
|
+
with TabbedContent(initial=self.initial_tab):
|
|
235
|
+
with TabPane("Agents", id="agents"):
|
|
236
|
+
yield AgentsTab(self.cli_config)
|
|
237
|
+
|
|
238
|
+
with TabPane("Attacks", id="attacks"):
|
|
239
|
+
yield AttacksTab(self.cli_config, initial_data=self.initial_data)
|
|
240
|
+
|
|
241
|
+
with TabPane("Results", id="results"):
|
|
242
|
+
yield ResultsTab(self.cli_config)
|
|
243
|
+
|
|
244
|
+
with TabPane("Config", id="config"):
|
|
245
|
+
yield ConfigTab(self.cli_config)
|
|
246
|
+
|
|
247
|
+
yield Footer()
|
|
248
|
+
|
|
249
|
+
def action_switch_tab(self, tab_id: str) -> None:
|
|
250
|
+
"""Switch to a specific tab.
|
|
251
|
+
|
|
252
|
+
Args:
|
|
253
|
+
tab_id: ID of the tab to switch to
|
|
254
|
+
"""
|
|
255
|
+
tabs = self.query_one(TabbedContent)
|
|
256
|
+
tabs.active = tab_id
|
|
257
|
+
|
|
258
|
+
def action_refresh(self) -> None:
|
|
259
|
+
"""Refresh the current tab's data."""
|
|
260
|
+
tabs = self.query_one(TabbedContent)
|
|
261
|
+
active_pane = tabs.get_pane(tabs.active)
|
|
262
|
+
if active_pane and hasattr(active_pane, "refresh_data"):
|
|
263
|
+
# Get the first child of the TabPane (our custom tab widget)
|
|
264
|
+
for child in active_pane.children:
|
|
265
|
+
if hasattr(child, "refresh_data"):
|
|
266
|
+
child.refresh_data()
|
|
267
|
+
break
|
|
268
|
+
|
|
269
|
+
def on_mount(self) -> None:
|
|
270
|
+
"""Called when the app is mounted."""
|
|
271
|
+
self.title = self.TITLE
|
|
272
|
+
self.sub_title = self.SUB_TITLE
|
|
273
|
+
|
|
274
|
+
def show_success(self, message: str) -> None:
|
|
275
|
+
"""Show success notification with checkmark."""
|
|
276
|
+
pass
|
|
277
|
+
|
|
278
|
+
def show_error(self, message: str) -> None:
|
|
279
|
+
"""Show error notification with X mark."""
|
|
280
|
+
pass
|
|
281
|
+
|
|
282
|
+
def show_warning(self, message: str) -> None:
|
|
283
|
+
"""Show warning notification with warning sign."""
|
|
284
|
+
pass
|
|
285
|
+
|
|
286
|
+
def show_info(self, message: str) -> None:
|
|
287
|
+
"""Show info notification with info icon."""
|
|
288
|
+
pass
|
|
@@ -0,0 +1,137 @@
|
|
|
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
|
+
Base Tab Class
|
|
17
|
+
|
|
18
|
+
Base class for all TUI tabs with common functionality.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
import httpx
|
|
22
|
+
from textual.containers import Container
|
|
23
|
+
|
|
24
|
+
from hackagent.cli.config import CLIConfig
|
|
25
|
+
from hackagent.client import AuthenticatedClient
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseTab(Container):
|
|
29
|
+
"""Base class for all TUI tabs.
|
|
30
|
+
|
|
31
|
+
Provides common functionality:
|
|
32
|
+
- CLI configuration access
|
|
33
|
+
- API client creation with timeout
|
|
34
|
+
- Error handling helpers
|
|
35
|
+
- Refresh mechanism
|
|
36
|
+
|
|
37
|
+
Subclasses should implement refresh_data() method.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
# Default API timeout (can be overridden by subclasses)
|
|
41
|
+
API_TIMEOUT = 5.0
|
|
42
|
+
|
|
43
|
+
def __init__(self, cli_config: CLIConfig, **kwargs):
|
|
44
|
+
"""Initialize base tab.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
cli_config: CLI configuration instance
|
|
48
|
+
**kwargs: Additional arguments passed to Container
|
|
49
|
+
"""
|
|
50
|
+
super().__init__(**kwargs)
|
|
51
|
+
self.cli_config = cli_config
|
|
52
|
+
self._refresh_interval = None
|
|
53
|
+
|
|
54
|
+
def create_api_client(self, timeout: float | None = None) -> AuthenticatedClient:
|
|
55
|
+
"""Create an authenticated API client with timeout.
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
timeout: Optional timeout override (uses API_TIMEOUT by default)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
Configured AuthenticatedClient instance
|
|
62
|
+
"""
|
|
63
|
+
if timeout is None:
|
|
64
|
+
timeout = self.API_TIMEOUT
|
|
65
|
+
|
|
66
|
+
return AuthenticatedClient(
|
|
67
|
+
base_url=self.cli_config.base_url,
|
|
68
|
+
token=self.cli_config.api_key,
|
|
69
|
+
prefix="Bearer",
|
|
70
|
+
timeout=httpx.Timeout(timeout, connect=timeout),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def handle_api_error(self, error: Exception, context: str = "API call") -> str:
|
|
74
|
+
"""Format API error messages for display.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
error: The exception that occurred
|
|
78
|
+
context: Description of what operation failed
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Formatted error message
|
|
82
|
+
"""
|
|
83
|
+
from rich.markup import escape
|
|
84
|
+
|
|
85
|
+
if isinstance(error, httpx.TimeoutException):
|
|
86
|
+
return f"[red]Timeout:[/red] {context} took too long"
|
|
87
|
+
elif isinstance(error, httpx.HTTPStatusError):
|
|
88
|
+
if error.response.status_code == 401:
|
|
89
|
+
return (
|
|
90
|
+
"[red]Authentication Failed[/red]\n\n"
|
|
91
|
+
"[yellow]Your API key is invalid or expired[/yellow]\n\n"
|
|
92
|
+
"[cyan]To fix:[/cyan]\n"
|
|
93
|
+
"Run: hackagent config set --api-key YOUR_KEY\n\n"
|
|
94
|
+
"[dim]Press F5 to retry after updating[/dim]"
|
|
95
|
+
)
|
|
96
|
+
else:
|
|
97
|
+
return f"[red]HTTP {error.response.status_code}:[/red] {context} failed"
|
|
98
|
+
else:
|
|
99
|
+
# Escape error message to prevent Rich markup issues
|
|
100
|
+
error_text = escape(str(error))
|
|
101
|
+
return f"[red]Error:[/red] {error_text}"
|
|
102
|
+
|
|
103
|
+
def refresh_data(self) -> None:
|
|
104
|
+
"""Refresh tab data from API.
|
|
105
|
+
|
|
106
|
+
Should be overridden by subclasses that need data refresh functionality.
|
|
107
|
+
Default implementation does nothing.
|
|
108
|
+
"""
|
|
109
|
+
pass
|
|
110
|
+
|
|
111
|
+
def enable_auto_refresh(self, interval: float = 5.0) -> None:
|
|
112
|
+
"""Enable automatic data refresh at specified interval.
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
interval: Refresh interval in seconds (default: 5.0)
|
|
116
|
+
"""
|
|
117
|
+
if self._refresh_interval is not None:
|
|
118
|
+
# Remove existing refresh timer
|
|
119
|
+
self._refresh_interval = None
|
|
120
|
+
|
|
121
|
+
self._refresh_interval = self.set_interval(
|
|
122
|
+
interval, self.refresh_data, name=f"{self.__class__.__name__}-refresh"
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def disable_auto_refresh(self) -> None:
|
|
126
|
+
"""Disable automatic data refresh."""
|
|
127
|
+
if self._refresh_interval is not None:
|
|
128
|
+
self._refresh_interval = None
|
|
129
|
+
|
|
130
|
+
def on_mount(self) -> None:
|
|
131
|
+
"""Called when tab is mounted.
|
|
132
|
+
|
|
133
|
+
Subclasses can override to add custom mounting behavior,
|
|
134
|
+
but should call super().on_mount() to ensure proper initialization.
|
|
135
|
+
"""
|
|
136
|
+
# Defer initial load to ensure DOM is ready
|
|
137
|
+
self.call_after_refresh(self.refresh_data)
|