hydrasight 4.1.0__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.
- hydrasight/__init__.py +27 -0
- hydrasight/__main__.py +25 -0
- hydrasight/cli/__init__.py +37 -0
- hydrasight/cli/display.py +236 -0
- hydrasight/cli/shell.py +393 -0
- hydrasight/cli/shell_handlers.py +752 -0
- hydrasight/cli/shell_renderer.py +897 -0
- hydrasight/config/__init__.py +35 -0
- hydrasight/config/defaults.py +229 -0
- hydrasight/config/loader.py +99 -0
- hydrasight/constants.py +37 -0
- hydrasight/core/__init__.py +3 -0
- hydrasight/core/builtin_actions.py +202 -0
- hydrasight/core/command_builder.py +93 -0
- hydrasight/core/engine.py +1101 -0
- hydrasight/core/hash_crack.py +75 -0
- hydrasight/core/planner.py +317 -0
- hydrasight/core/profiles.py +77 -0
- hydrasight/core/registry.py +46 -0
- hydrasight/integrations/__init__.py +17 -0
- hydrasight/integrations/exploit_db.py +192 -0
- hydrasight/integrations/exploit_suggestion.py +534 -0
- hydrasight/integrations/kali_api.py +120 -0
- hydrasight/models/__init__.py +15 -0
- hydrasight/models/actions.py +64 -0
- hydrasight/models/commands.py +154 -0
- hydrasight/models/finding_confidence.py +9 -0
- hydrasight/models/finding_record.py +411 -0
- hydrasight/models/findings.py +461 -0
- hydrasight/models/planner_state.py +182 -0
- hydrasight/models/report_model.py +279 -0
- hydrasight/models/roe.py +170 -0
- hydrasight/models/timeline.py +32 -0
- hydrasight/parsers/__init__.py +5 -0
- hydrasight/parsers/base_parser.py +246 -0
- hydrasight/reporting/__init__.py +7 -0
- hydrasight/reporting/json_reporter.py +32 -0
- hydrasight/reporting/outcome.py +78 -0
- hydrasight/reporting/pdf_reporter.py +522 -0
- hydrasight/reporting/remediation.py +197 -0
- hydrasight/security/__init__.py +1 -0
- hydrasight/security/audit.py +356 -0
- hydrasight/security/authorization.py +255 -0
- hydrasight/security/command_sanitizer.py +556 -0
- hydrasight/services/__init__.py +35 -0
- hydrasight/services/action_planner.py +235 -0
- hydrasight/services/ai_client.py +192 -0
- hydrasight/services/chat_ai_client.py +133 -0
- hydrasight/services/chat_controller.py +137 -0
- hydrasight/services/command_router.py +133 -0
- hydrasight/services/confirmation_manager.py +143 -0
- hydrasight/services/context_builder.py +157 -0
- hydrasight/services/dispatcher.py +390 -0
- hydrasight/services/execution_policy.py +153 -0
- hydrasight/services/intent_classifier.py +549 -0
- hydrasight/services/intent_router.py +90 -0
- hydrasight/services/post_access/__init__.py +38 -0
- hydrasight/services/post_access/base.py +70 -0
- hydrasight/services/post_access/factory.py +70 -0
- hydrasight/services/post_access/ftp_handler.py +99 -0
- hydrasight/services/post_access/msf_handlers.py +148 -0
- hydrasight/services/post_access/ssh_handler.py +53 -0
- hydrasight/services/post_access/types.py +48 -0
- hydrasight/services/post_access/web_handler.py +144 -0
- hydrasight/services/session_manager.py +127 -0
- hydrasight/services/verifier.py +323 -0
- hydrasight/utils/__init__.py +6 -0
- hydrasight/utils/ip_utils.py +49 -0
- hydrasight/utils/time_utils.py +8 -0
- hydrasight-4.1.0.dist-info/METADATA +678 -0
- hydrasight-4.1.0.dist-info/RECORD +75 -0
- hydrasight-4.1.0.dist-info/WHEEL +5 -0
- hydrasight-4.1.0.dist-info/entry_points.txt +2 -0
- hydrasight-4.1.0.dist-info/licenses/LICENSE +32 -0
- hydrasight-4.1.0.dist-info/top_level.txt +1 -0
hydrasight/__init__.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""HydraSight — AI-Orchestrated, authorization-gated Penetration Testing Framework."""
|
|
2
|
+
|
|
3
|
+
from hydrasight.constants import (
|
|
4
|
+
APP_NAME,
|
|
5
|
+
CODENAME,
|
|
6
|
+
VERSION,
|
|
7
|
+
__app_name__,
|
|
8
|
+
__author__,
|
|
9
|
+
__codename__,
|
|
10
|
+
__license__,
|
|
11
|
+
__summary__,
|
|
12
|
+
__title__,
|
|
13
|
+
__version__,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"VERSION",
|
|
18
|
+
"CODENAME",
|
|
19
|
+
"APP_NAME",
|
|
20
|
+
"__version__",
|
|
21
|
+
"__codename__",
|
|
22
|
+
"__app_name__",
|
|
23
|
+
"__title__",
|
|
24
|
+
"__license__",
|
|
25
|
+
"__author__",
|
|
26
|
+
"__summary__",
|
|
27
|
+
]
|
hydrasight/__main__.py
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Entry point — python -m hydrasight"""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import traceback
|
|
5
|
+
|
|
6
|
+
from hydrasight.cli.shell import Shell
|
|
7
|
+
from hydrasight.config.loader import load_config
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def main() -> None:
|
|
11
|
+
|
|
12
|
+
cfg = load_config()
|
|
13
|
+
try:
|
|
14
|
+
Shell(cfg).run()
|
|
15
|
+
except KeyboardInterrupt:
|
|
16
|
+
print("\n[!] interrupted")
|
|
17
|
+
sys.exit(130)
|
|
18
|
+
except Exception: # noqa: BLE001
|
|
19
|
+
print("\n[!] fatal error:")
|
|
20
|
+
traceback.print_exc()
|
|
21
|
+
sys.exit(1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
if __name__ == "__main__":
|
|
25
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""CLI package."""
|
|
2
|
+
|
|
3
|
+
from hydrasight.cli.display import (
|
|
4
|
+
analysis_panel,
|
|
5
|
+
console,
|
|
6
|
+
div,
|
|
7
|
+
err,
|
|
8
|
+
hit,
|
|
9
|
+
info,
|
|
10
|
+
label,
|
|
11
|
+
ok,
|
|
12
|
+
phase_header,
|
|
13
|
+
raw_output,
|
|
14
|
+
result_line,
|
|
15
|
+
spinner,
|
|
16
|
+
stats_line,
|
|
17
|
+
task_line,
|
|
18
|
+
warn,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"console",
|
|
23
|
+
"div",
|
|
24
|
+
"ok",
|
|
25
|
+
"warn",
|
|
26
|
+
"info",
|
|
27
|
+
"err",
|
|
28
|
+
"hit",
|
|
29
|
+
"label",
|
|
30
|
+
"spinner",
|
|
31
|
+
"phase_header",
|
|
32
|
+
"task_line",
|
|
33
|
+
"result_line",
|
|
34
|
+
"analysis_panel",
|
|
35
|
+
"raw_output",
|
|
36
|
+
"stats_line",
|
|
37
|
+
]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All Rich terminal UI helpers — panels, spinners, stats line, phase headers.
|
|
3
|
+
|
|
4
|
+
The module-level `console` object is the single shared Rich Console
|
|
5
|
+
used throughout the entire application.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import re
|
|
10
|
+
from typing import TYPE_CHECKING, Any
|
|
11
|
+
|
|
12
|
+
from rich import box
|
|
13
|
+
from rich.console import Console
|
|
14
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn, TimeElapsedColumn
|
|
15
|
+
from rich.rule import Rule
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
|
|
18
|
+
from hydrasight.config.defaults import TOOL_LABELS, P
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from hydrasight.models.findings import Findings
|
|
22
|
+
|
|
23
|
+
# ── shared console ────────────────────────────────────────────────────────────
|
|
24
|
+
console = Console(highlight=False)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ── dividers ──────────────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def div(label: str = "") -> None:
|
|
31
|
+
if label:
|
|
32
|
+
console.print(Rule(f"[{P.MUTED}] {label} [/]", style=P.DIM))
|
|
33
|
+
else:
|
|
34
|
+
console.print(Rule(style=P.DIM))
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
# ── one-liner status messages ─────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def ok(msg: str) -> None:
|
|
41
|
+
console.print(f" [{P.PRIMARY}][+][/] [{P.TEXT}]{msg}[/]")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def warn(msg: str) -> None:
|
|
45
|
+
console.print(f" [{P.AMBER}][!][/] [{P.AMBER}]{msg}[/]")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def info(msg: str) -> None:
|
|
49
|
+
console.print(f" [{P.DIM}][>][/] [{P.MUTED}]{msg}[/]")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def err(msg: str) -> None:
|
|
53
|
+
console.print(f" [{P.RED}][x][/] [{P.RED}]{msg}[/]")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def hit(msg: str) -> None:
|
|
57
|
+
console.print(f" [{P.BRIGHT}][*][/] [{P.BRIGHT}]{msg}[/]")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def label(key: str, val: str, kw: int = 14) -> None:
|
|
61
|
+
console.print(f" [{P.MUTED}]{key.ljust(kw)}[/] [{P.TEXT}]{val}[/]")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ── spinners ──────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def spinner(text: str) -> Progress:
|
|
68
|
+
return Progress(
|
|
69
|
+
SpinnerColumn(spinner_name="line", style=P.PRIMARY),
|
|
70
|
+
TextColumn(f"[{P.MUTED}]{text}[/]"),
|
|
71
|
+
TimeElapsedColumn(),
|
|
72
|
+
console=console,
|
|
73
|
+
transient=True,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ── phase header ──────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def phase_header(phase_id: str, phase_label: str, color: str, idx: int, total: int) -> None:
|
|
81
|
+
pct = int((idx / total) * 100)
|
|
82
|
+
filled = int((idx / total) * 24)
|
|
83
|
+
progress_bar = f"[{P.PRIMARY}]{'█' * filled}[/][{P.DIM}]{'─' * (24 - filled)}[/]"
|
|
84
|
+
console.print()
|
|
85
|
+
console.print(
|
|
86
|
+
f" [{P.DIM}]┌──[/] [bold {color}]{phase_label.upper()}[/]"
|
|
87
|
+
f" [{P.DIM}]│[/] [{P.MUTED}]phase {idx}/{total}[/]"
|
|
88
|
+
f" [{P.DIM}]│[/] [{P.MUTED}]{phase_id}[/]"
|
|
89
|
+
)
|
|
90
|
+
console.print(f" [{P.DIM}]│[/] {progress_bar} [{P.PRIMARY}]{pct}%[/]")
|
|
91
|
+
console.print(f" [{P.DIM}]└──────────────────────────────[/]")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ── tool execution lines ──────────────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def task_line(tool: str) -> None:
|
|
98
|
+
console.print(f"\n [{P.MUTED}]exec[/] [{P.PRIMARY}]{TOOL_LABELS.get(tool, tool)}[/]")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def result_line(tool: str, elapsed: float, chars: int, warnings: list[str]) -> None:
|
|
102
|
+
lbl = TOOL_LABELS.get(tool, tool)
|
|
103
|
+
char_s = f"{chars:,}" if chars else "0"
|
|
104
|
+
color = P.PRIMARY if chars else P.AMBER
|
|
105
|
+
console.print(
|
|
106
|
+
f" [{P.PRIMARY}][+][/] [{P.MUTED}]{lbl}[/]"
|
|
107
|
+
f" [{P.DIM}]│[/] [{P.DIM}]{elapsed:.1f}s[/]"
|
|
108
|
+
f" [{P.DIM}]│[/] [{color}]{char_s} bytes[/]"
|
|
109
|
+
)
|
|
110
|
+
for w in warnings:
|
|
111
|
+
warn(w)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# ── AI analysis panel ─────────────────────────────────────────────────────────
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def analysis_panel(text: str) -> None:
|
|
118
|
+
section_map = {
|
|
119
|
+
"PORTS": (P.BLUE, "ports "),
|
|
120
|
+
"VULNS": (P.AMBER, "vulns "),
|
|
121
|
+
"CREDS": (P.PRIMARY, "creds "),
|
|
122
|
+
"SESSIONS": (P.PRIMARY, "session "),
|
|
123
|
+
"NOTES": (P.MUTED, "notes "),
|
|
124
|
+
}
|
|
125
|
+
lines_out: list[tuple[str, str, str]] = []
|
|
126
|
+
clean = re.sub(r"```(?:json)?|```", "", text).strip()
|
|
127
|
+
try:
|
|
128
|
+
data = json.loads(clean)
|
|
129
|
+
if isinstance(data, dict):
|
|
130
|
+
for key, (color, lbl) in section_map.items():
|
|
131
|
+
val: Any = data.get(key) or data.get(key.lower())
|
|
132
|
+
if val is None:
|
|
133
|
+
val = "—"
|
|
134
|
+
elif isinstance(val, list):
|
|
135
|
+
parts: list[str] = []
|
|
136
|
+
for item in val:
|
|
137
|
+
if isinstance(item, dict):
|
|
138
|
+
sev = item.get("severity", "")
|
|
139
|
+
desc = item.get(
|
|
140
|
+
"description",
|
|
141
|
+
item.get("name", str(item)),
|
|
142
|
+
)
|
|
143
|
+
parts.append(f"[{sev.upper()}] {desc}" if sev else str(desc))
|
|
144
|
+
else:
|
|
145
|
+
parts.append(str(item))
|
|
146
|
+
val = " │ ".join(parts) if parts else "—"
|
|
147
|
+
elif str(val).lower() in ("null", "none", ""):
|
|
148
|
+
val = "—"
|
|
149
|
+
lines_out.append((color, lbl, str(val)))
|
|
150
|
+
except (json.JSONDecodeError, TypeError):
|
|
151
|
+
for line in text.splitlines():
|
|
152
|
+
line = line.strip()
|
|
153
|
+
if not line:
|
|
154
|
+
continue
|
|
155
|
+
matched = False
|
|
156
|
+
for key, (color, lbl) in section_map.items():
|
|
157
|
+
if line.upper().startswith(key + ":"):
|
|
158
|
+
val_str = line[len(key) + 1 :].strip() or "—"
|
|
159
|
+
lines_out.append((color, lbl, val_str))
|
|
160
|
+
matched = True
|
|
161
|
+
break
|
|
162
|
+
if not matched and lines_out:
|
|
163
|
+
c, lb, v = lines_out[-1]
|
|
164
|
+
lines_out[-1] = (c, lb, f"{v} {line}")
|
|
165
|
+
if not lines_out:
|
|
166
|
+
return
|
|
167
|
+
console.print()
|
|
168
|
+
console.print(f" [{P.DIM}]┌──[/] [{P.MUTED}]intelligence[/]")
|
|
169
|
+
for color, lbl, val in lines_out:
|
|
170
|
+
if len(val) > 92:
|
|
171
|
+
val = val[:89] + "…"
|
|
172
|
+
console.print(f" [{P.DIM}]│[/] [{P.MUTED}]{lbl}[/] [{color}]{val}[/]")
|
|
173
|
+
console.print(f" [{P.DIM}]└────────────────────────────────[/]")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ── raw output preview ────────────────────────────────────────────────────────
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def raw_output(output: str, verbosity: int) -> None:
|
|
180
|
+
if verbosity >= 2 and output:
|
|
181
|
+
preview = output[:1200] + ("…" if len(output) > 1200 else "")
|
|
182
|
+
console.print()
|
|
183
|
+
console.print(f" [{P.DIM}]┌──[/] [{P.MUTED}]raw output[/]")
|
|
184
|
+
for line in preview.splitlines()[:35]:
|
|
185
|
+
console.print(f" [{P.DIM}]│[/] [{P.MUTED}]{line}[/]")
|
|
186
|
+
if len(output) > 1200:
|
|
187
|
+
console.print(f" [{P.DIM}]│ … truncated[/]")
|
|
188
|
+
console.print(f" [{P.DIM}]└────────────────────────────────[/]")
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ── stats line ────────────────────────────────────────────────────────────────
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def stats_line(findings: "Findings") -> None:
|
|
195
|
+
rc = findings.overall_risk
|
|
196
|
+
risk_color = {
|
|
197
|
+
"CRITICAL": P.RED,
|
|
198
|
+
"HIGH": P.AMBER,
|
|
199
|
+
"MEDIUM": P.YELLOW,
|
|
200
|
+
"LOW": P.BLUE,
|
|
201
|
+
"NONE": P.DIM,
|
|
202
|
+
}.get(rc, P.DIM)
|
|
203
|
+
console.print(
|
|
204
|
+
f"\n [{P.DIM}]│[/] [{P.MUTED}]ports[/] "
|
|
205
|
+
f"[{P.TEXT}]{len(findings.ports):>3}[/]"
|
|
206
|
+
f" [{P.DIM}]│[/] [{P.MUTED}]vulns[/] "
|
|
207
|
+
f"[{P.AMBER}]{len(findings.vulns):>3}[/]"
|
|
208
|
+
f" [{P.DIM}]│[/] [{P.MUTED}]crit[/] "
|
|
209
|
+
f"[{P.RED}]{findings.critical_count:>3}[/]"
|
|
210
|
+
f" [{P.DIM}]│[/] [{P.MUTED}]creds[/] "
|
|
211
|
+
f"[{P.BRIGHT}]{len(findings.credentials):>2}[/]"
|
|
212
|
+
f" [{P.DIM}]│[/] [{P.MUTED}]sess[/] "
|
|
213
|
+
f"[{P.BRIGHT}]{len(findings.sessions):>2}[/]"
|
|
214
|
+
f" [{P.DIM}]│[/] [{P.MUTED}]risk[/] "
|
|
215
|
+
f"[{risk_color}]{rc}[/]"
|
|
216
|
+
f" [{P.DIM}]│[/]"
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# ── findings table helper ─────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def make_table(*cols: tuple) -> Table:
|
|
224
|
+
t = Table(
|
|
225
|
+
box=box.SIMPLE,
|
|
226
|
+
show_header=True,
|
|
227
|
+
header_style=P.MUTED,
|
|
228
|
+
border_style=P.DIM,
|
|
229
|
+
padding=(0, 2),
|
|
230
|
+
)
|
|
231
|
+
for name, style, width in cols:
|
|
232
|
+
kw: dict[str, Any] = {"style": style}
|
|
233
|
+
if width:
|
|
234
|
+
kw["width"] = width
|
|
235
|
+
t.add_column(name, **kw)
|
|
236
|
+
return t
|