humanbound 2.0.0__tar.gz → 2.0.2__tar.gz
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.
- {humanbound-2.0.0 → humanbound-2.0.2}/PKG-INFO +1 -7
- {humanbound-2.0.0 → humanbound-2.0.2}/README.md +0 -6
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound/__init__.py +1 -1
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound.egg-info/PKG-INFO +1 -7
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound.egg-info/SOURCES.txt +9 -1
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/connect.py +365 -16
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/test.py +54 -10
- humanbound-2.0.2/humanbound_cli/engine/compliance.py +192 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/local_runner.py +4 -1
- humanbound-2.0.2/humanbound_cli/templates/compliance/banking.yaml +19 -0
- humanbound-2.0.2/humanbound_cli/templates/compliance/ecommerce.yaml +18 -0
- humanbound-2.0.2/humanbound_cli/templates/compliance/eu-ai-act.yaml +12 -0
- humanbound-2.0.2/humanbound_cli/templates/compliance/healthcare.yaml +18 -0
- humanbound-2.0.2/humanbound_cli/templates/compliance/insurance.yaml +19 -0
- humanbound-2.0.2/humanbound_cli/templates/compliance/legal.yaml +18 -0
- humanbound-2.0.2/humanbound_cli/templates/report_base.html +238 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/pyproject.toml +7 -3
- {humanbound-2.0.0 → humanbound-2.0.2}/LICENSE +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound/bot.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound/callbacks.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound/orchestrators.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound/py.typed +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound/runner.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound/schemas.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound.egg-info/dependency_links.txt +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound.egg-info/entry_points.txt +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound.egg-info/requires.txt +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound.egg-info/top_level.txt +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/adapters/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/adapters/promptfoo.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/adapters/pyrit.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/client.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/_report_helper.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/api_keys.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/assessments.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/auth.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/campaigns.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/completion.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/config_cmd.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/docs.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/experiments.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/findings.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/firewall.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/guardrails.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/logs.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/mcp.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/members.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/monitor.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/orgs.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/posture.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/projects.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/providers.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/redteam.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/report.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/scan.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/sentinel.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/upload_logs.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/commands/webhooks.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/config.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/connectors/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/connectors/microsoft.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/bot.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/callbacks.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/llm/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/llm/azureopenai.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/llm/claude.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/llm/gemini.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/llm/grok.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/llm/ollama.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/llm/openai.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/base.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/behavioral_qa/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/behavioral_qa/config.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/behavioral_qa/generator.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/behavioral_qa/judge.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/behavioral_qa/orchestrator.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_agentic/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_agentic/config.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_agentic/generator.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_agentic/judge.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_agentic/orchestrator.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_single_turn/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_single_turn/config.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_single_turn/generator.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_single_turn/judge.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/orchestrators/owasp_single_turn/orchestrator.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/platform_runner.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/presenter.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/runner.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/schemas.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/engine/scope.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/exceptions.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/extractors/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/extractors/openapi.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/extractors/repo.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/main.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/mcp_server.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/py.typed +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/pytest_plugin/__init__.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/pytest_plugin/report.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/report.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/report_builder.py +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/humanbound_cli/templates/logo.svg +0 -0
- {humanbound-2.0.0 → humanbound-2.0.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: humanbound
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Humanbound — open-source AI agent red-team engine, SDK, and CLI.
|
|
5
5
|
Author-email: Humanbound <hello@humanbound.ai>
|
|
6
6
|
Maintainer-email: Demetris Gerogiannis <hello@humanbound.ai>, Kostas Siabanis <hello@humanbound.ai>
|
|
@@ -196,9 +196,3 @@ trademark policy. The code is open; the name is not.
|
|
|
196
196
|
The sibling project [`humanbound-firewall`](https://github.com/humanbound/humanbound-firewall)
|
|
197
197
|
is dual-licensed (AGPL-3.0 + commercial) — different product, different
|
|
198
198
|
license strategy.
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
<p align="center">
|
|
203
|
-
<sub><em>Humanbound is the trading name of AI and Me Single-Member Private Company, incorporated in Greece.</em></sub>
|
|
204
|
-
</p>
|
|
@@ -134,9 +134,3 @@ trademark policy. The code is open; the name is not.
|
|
|
134
134
|
The sibling project [`humanbound-firewall`](https://github.com/humanbound/humanbound-firewall)
|
|
135
135
|
is dual-licensed (AGPL-3.0 + commercial) — different product, different
|
|
136
136
|
license strategy.
|
|
137
|
-
|
|
138
|
-
---
|
|
139
|
-
|
|
140
|
-
<p align="center">
|
|
141
|
-
<sub><em>Humanbound is the trading name of AI and Me Single-Member Private Company, incorporated in Greece.</em></sub>
|
|
142
|
-
</p>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: humanbound
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: Humanbound — open-source AI agent red-team engine, SDK, and CLI.
|
|
5
5
|
Author-email: Humanbound <hello@humanbound.ai>
|
|
6
6
|
Maintainer-email: Demetris Gerogiannis <hello@humanbound.ai>, Kostas Siabanis <hello@humanbound.ai>
|
|
@@ -196,9 +196,3 @@ trademark policy. The code is open; the name is not.
|
|
|
196
196
|
The sibling project [`humanbound-firewall`](https://github.com/humanbound/humanbound-firewall)
|
|
197
197
|
is dual-licensed (AGPL-3.0 + commercial) — different product, different
|
|
198
198
|
license strategy.
|
|
199
|
-
|
|
200
|
-
---
|
|
201
|
-
|
|
202
|
-
<p align="center">
|
|
203
|
-
<sub><em>Humanbound is the trading name of AI and Me Single-Member Private Company, incorporated in Greece.</em></sub>
|
|
204
|
-
</p>
|
|
@@ -60,6 +60,7 @@ humanbound_cli/connectors/microsoft.py
|
|
|
60
60
|
humanbound_cli/engine/__init__.py
|
|
61
61
|
humanbound_cli/engine/bot.py
|
|
62
62
|
humanbound_cli/engine/callbacks.py
|
|
63
|
+
humanbound_cli/engine/compliance.py
|
|
63
64
|
humanbound_cli/engine/local_runner.py
|
|
64
65
|
humanbound_cli/engine/platform_runner.py
|
|
65
66
|
humanbound_cli/engine/presenter.py
|
|
@@ -96,4 +97,11 @@ humanbound_cli/extractors/repo.py
|
|
|
96
97
|
humanbound_cli/pytest_plugin/__init__.py
|
|
97
98
|
humanbound_cli/pytest_plugin/fixtures.py
|
|
98
99
|
humanbound_cli/pytest_plugin/report.py
|
|
99
|
-
humanbound_cli/templates/logo.svg
|
|
100
|
+
humanbound_cli/templates/logo.svg
|
|
101
|
+
humanbound_cli/templates/report_base.html
|
|
102
|
+
humanbound_cli/templates/compliance/banking.yaml
|
|
103
|
+
humanbound_cli/templates/compliance/ecommerce.yaml
|
|
104
|
+
humanbound_cli/templates/compliance/eu-ai-act.yaml
|
|
105
|
+
humanbound_cli/templates/compliance/healthcare.yaml
|
|
106
|
+
humanbound_cli/templates/compliance/insurance.yaml
|
|
107
|
+
humanbound_cli/templates/compliance/legal.yaml
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"""Connect command — unified entry point for agent and platform onboarding."""
|
|
4
4
|
|
|
5
5
|
import json
|
|
6
|
+
import random
|
|
7
|
+
import threading
|
|
6
8
|
import time
|
|
7
9
|
from pathlib import Path
|
|
8
10
|
from urllib.parse import urlparse
|
|
@@ -13,11 +15,216 @@ from rich.panel import Panel
|
|
|
13
15
|
|
|
14
16
|
from ..client import HumanboundClient
|
|
15
17
|
from ..exceptions import APIError, NotAuthenticatedError
|
|
18
|
+
from .test import _load_integration, _resolve_context
|
|
16
19
|
|
|
17
20
|
console = Console()
|
|
18
21
|
|
|
19
22
|
SCAN_TIMEOUT = 180
|
|
20
23
|
|
|
24
|
+
# -- Scan progress UI ----------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
_SCAN_PHASES = {
|
|
27
|
+
"endpoint": [
|
|
28
|
+
"Connecting to your bot...",
|
|
29
|
+
"Chatting with your bot...",
|
|
30
|
+
"Exploring capabilities...",
|
|
31
|
+
"Wrapping up conversation...",
|
|
32
|
+
],
|
|
33
|
+
"text": [
|
|
34
|
+
"Reading your prompt...",
|
|
35
|
+
],
|
|
36
|
+
"agentic": [
|
|
37
|
+
"Analysing agent tools...",
|
|
38
|
+
],
|
|
39
|
+
"reflect": [
|
|
40
|
+
"Extracting scope...",
|
|
41
|
+
"Classifying intents...",
|
|
42
|
+
"Assessing risk profile...",
|
|
43
|
+
"Finalising analysis...",
|
|
44
|
+
],
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_PLAYFUL_MESSAGES = [
|
|
48
|
+
"Kicking the tires...",
|
|
49
|
+
"Poking around...",
|
|
50
|
+
"Asking nicely...",
|
|
51
|
+
"Pretending to be a user...",
|
|
52
|
+
"Checking under the hood...",
|
|
53
|
+
"Shaking the tree...",
|
|
54
|
+
"Looking for breadcrumbs...",
|
|
55
|
+
"Testing the waters...",
|
|
56
|
+
"Playing twenty questions...",
|
|
57
|
+
"Seeing what sticks...",
|
|
58
|
+
"Connecting the dots...",
|
|
59
|
+
"Reading between the lines...",
|
|
60
|
+
"Following the clues...",
|
|
61
|
+
"Pulling on threads...",
|
|
62
|
+
"Mapping the terrain...",
|
|
63
|
+
"Snooping around...",
|
|
64
|
+
"Warming up the neurons...",
|
|
65
|
+
"Brewing some insights...",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _scan_with_progress(client: HumanboundClient, sources: list, timeout: int, phases: list):
|
|
70
|
+
"""Run POST /scan with rotating status messages."""
|
|
71
|
+
result: dict = {}
|
|
72
|
+
error: Exception | None = None
|
|
73
|
+
|
|
74
|
+
def do_scan():
|
|
75
|
+
nonlocal result, error
|
|
76
|
+
try:
|
|
77
|
+
result = client.post(
|
|
78
|
+
"scan",
|
|
79
|
+
data={"sources": sources},
|
|
80
|
+
include_project=False,
|
|
81
|
+
timeout=timeout,
|
|
82
|
+
)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
error = e
|
|
85
|
+
|
|
86
|
+
thread = threading.Thread(target=do_scan)
|
|
87
|
+
scan_start = time.time()
|
|
88
|
+
thread.start()
|
|
89
|
+
|
|
90
|
+
playful = _PLAYFUL_MESSAGES.copy()
|
|
91
|
+
random.shuffle(playful)
|
|
92
|
+
|
|
93
|
+
phase_idx = 0
|
|
94
|
+
playful_idx = 0
|
|
95
|
+
rotate_interval = 4
|
|
96
|
+
|
|
97
|
+
with console.status("") as status:
|
|
98
|
+
while thread.is_alive():
|
|
99
|
+
elapsed = time.time() - scan_start
|
|
100
|
+
phase = phases[phase_idx % len(phases)] if phases else "Scanning..."
|
|
101
|
+
fun = playful[playful_idx % len(playful)]
|
|
102
|
+
status.update(
|
|
103
|
+
f"[bold]{phase}[/bold] [dim]({elapsed:.0f}s)[/dim]\n"
|
|
104
|
+
f" [dim italic]{fun}[/dim italic]"
|
|
105
|
+
)
|
|
106
|
+
thread.join(timeout=rotate_interval)
|
|
107
|
+
playful_idx += 1
|
|
108
|
+
if playful_idx % 2 == 0:
|
|
109
|
+
phase_idx += 1
|
|
110
|
+
|
|
111
|
+
if error:
|
|
112
|
+
raise error
|
|
113
|
+
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _display_scope(scope: dict):
|
|
118
|
+
"""Render scope: business context + permitted/restricted intents."""
|
|
119
|
+
business_scope = scope.get("overall_business_scope", "")
|
|
120
|
+
intents = scope.get("intents", {})
|
|
121
|
+
permitted = intents.get("permitted", [])
|
|
122
|
+
restricted = intents.get("restricted", [])
|
|
123
|
+
|
|
124
|
+
parts = []
|
|
125
|
+
if business_scope:
|
|
126
|
+
parts.append(business_scope[:500] + ("..." if len(business_scope) > 500 else ""))
|
|
127
|
+
|
|
128
|
+
if permitted and isinstance(permitted, list):
|
|
129
|
+
parts.append("")
|
|
130
|
+
parts.append("[bold green]Permitted:[/bold green]")
|
|
131
|
+
for intent in permitted[:10]:
|
|
132
|
+
parts.append(f" [green]•[/green] {str(intent)[:80]}")
|
|
133
|
+
if len(permitted) > 10:
|
|
134
|
+
parts.append(f" [dim]... and {len(permitted) - 10} more[/dim]")
|
|
135
|
+
|
|
136
|
+
if restricted and isinstance(restricted, list):
|
|
137
|
+
parts.append("")
|
|
138
|
+
parts.append("[bold red]Restricted:[/bold red]")
|
|
139
|
+
for intent in restricted[:10]:
|
|
140
|
+
parts.append(f" [red]•[/red] {str(intent)[:80]}")
|
|
141
|
+
if len(restricted) > 10:
|
|
142
|
+
parts.append(f" [dim]... and {len(restricted) - 10} more[/dim]")
|
|
143
|
+
|
|
144
|
+
if not permitted and not restricted:
|
|
145
|
+
parts.append("")
|
|
146
|
+
parts.append("[dim]No intents extracted — the LLM may need more context.[/dim]")
|
|
147
|
+
parts.append("[dim]Try adding --prompt with a system prompt file for better results.[/dim]")
|
|
148
|
+
|
|
149
|
+
console.print(
|
|
150
|
+
Panel(
|
|
151
|
+
"\n".join(parts),
|
|
152
|
+
title="Scope",
|
|
153
|
+
border_style="blue",
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _risk_bar(level: str) -> str:
|
|
159
|
+
fill_map = {"LOW": 4, "MEDIUM": 8, "HIGH": 12}
|
|
160
|
+
color_map = {"LOW": "green", "MEDIUM": "yellow", "HIGH": "red"}
|
|
161
|
+
filled = fill_map.get(level, 8)
|
|
162
|
+
color = color_map.get(level, "white")
|
|
163
|
+
return f"[{color}]{'█' * filled}[/{color}][dim]{'░' * (12 - filled)}[/dim]"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _display_dashboard(name: str, risk_profile: dict, has_integration: bool, has_telemetry: bool):
|
|
167
|
+
"""Compact risk dashboard rendered after a successful Platform scan."""
|
|
168
|
+
risk_level = risk_profile.get("risk_level", "?")
|
|
169
|
+
industry = risk_profile.get("industry", "unknown")
|
|
170
|
+
risk_color = {"HIGH": "red", "MEDIUM": "yellow", "LOW": "green"}.get(risk_level, "white")
|
|
171
|
+
|
|
172
|
+
lines = []
|
|
173
|
+
lines.append(
|
|
174
|
+
f" Risk {_risk_bar(risk_level)} "
|
|
175
|
+
f"[{risk_color}][bold]{risk_level}[/bold][/{risk_color}] · {industry}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
pii = risk_profile.get("handles_pii", False)
|
|
179
|
+
fin = risk_profile.get("handles_financial_data", False)
|
|
180
|
+
health = risk_profile.get("handles_health_data", False)
|
|
181
|
+
pii_i = "[yellow]⚠ PII[/yellow]" if pii else "[dim]○ PII[/dim]"
|
|
182
|
+
fin_i = "[yellow]⚠ Financial[/yellow]" if fin else "[dim]○ Financial[/dim]"
|
|
183
|
+
hea_i = "[yellow]⚠ Health[/yellow]" if health else "[dim]○ Health[/dim]"
|
|
184
|
+
lines.append(f" Data {pii_i} {fin_i} {hea_i}")
|
|
185
|
+
|
|
186
|
+
integ = "[green]✓ configured[/green]" if has_integration else "[dim]✗ none[/dim]"
|
|
187
|
+
tele = "[green]✓ enabled[/green]" if has_telemetry else "[dim]✗ disabled[/dim]"
|
|
188
|
+
lines.append(f" Integ {integ} Telemetry {tele}")
|
|
189
|
+
|
|
190
|
+
regs = risk_profile.get("applicable_regulations", [])
|
|
191
|
+
if regs:
|
|
192
|
+
reg_str = " ".join(f"[cyan]{r.upper()}[/cyan]" for r in regs)
|
|
193
|
+
lines.append(f" Regs {reg_str}")
|
|
194
|
+
|
|
195
|
+
rationale = risk_profile.get("risk_rationale", "")
|
|
196
|
+
if rationale:
|
|
197
|
+
if len(rationale) > 120:
|
|
198
|
+
rationale = rationale[:117] + "..."
|
|
199
|
+
lines.append("")
|
|
200
|
+
lines.append(f" [dim italic]{rationale}[/dim italic]")
|
|
201
|
+
|
|
202
|
+
console.print(
|
|
203
|
+
Panel(
|
|
204
|
+
"\n".join(lines),
|
|
205
|
+
title=f"[bold]{name}[/bold]",
|
|
206
|
+
border_style=risk_color,
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _get_source_description(prompt: str, endpoint: str, repo: str, openapi: str) -> str:
|
|
212
|
+
"""Short human description of which --flag sources were used."""
|
|
213
|
+
sources = []
|
|
214
|
+
if prompt:
|
|
215
|
+
sources.append(f"prompt ({Path(prompt).name})")
|
|
216
|
+
if endpoint:
|
|
217
|
+
path = Path(endpoint)
|
|
218
|
+
if path.is_file():
|
|
219
|
+
sources.append(f"endpoint ({path.name})")
|
|
220
|
+
else:
|
|
221
|
+
sources.append("endpoint (inline)")
|
|
222
|
+
if repo:
|
|
223
|
+
sources.append(f"repo ({Path(repo).name})")
|
|
224
|
+
if openapi:
|
|
225
|
+
sources.append(f"openapi ({Path(openapi).name})")
|
|
226
|
+
return ", ".join(sources)
|
|
227
|
+
|
|
21
228
|
|
|
22
229
|
def _print_next(suggestions: list):
|
|
23
230
|
"""Print Next: suggestions block."""
|
|
@@ -187,29 +394,34 @@ def connect_command(
|
|
|
187
394
|
|
|
188
395
|
|
|
189
396
|
def _connect_agent(endpoint, name, prompt, repo, openapi, context, level, yes, timeout):
|
|
190
|
-
"""Agent path:
|
|
191
|
-
client = HumanboundClient()
|
|
397
|
+
"""Agent path: dispatch to Platform flow (authenticated) or local flow (anonymous).
|
|
192
398
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if not client.organisation_id:
|
|
198
|
-
console.print("[yellow]No organisation selected.[/yellow]")
|
|
199
|
-
console.print("Use 'hb switch <id>' to select an organisation first.")
|
|
200
|
-
raise SystemExit(1)
|
|
201
|
-
|
|
202
|
-
# Count extraction sources
|
|
399
|
+
Platform flow creates a project on humanbound.ai with full scope, risk profile,
|
|
400
|
+
regulatory mapping, and auto-test. Local flow derives scope + lightweight
|
|
401
|
+
compliance from the user's configured LLM provider and writes scope.yaml.
|
|
402
|
+
"""
|
|
203
403
|
source_flags = [prompt, endpoint, repo, openapi]
|
|
204
404
|
if not any(source_flags):
|
|
205
405
|
console.print("[yellow]No extraction source provided.[/yellow]")
|
|
206
406
|
console.print("Use --endpoint, --prompt, --repo, or --openapi to specify a source.")
|
|
207
407
|
raise SystemExit(1)
|
|
208
408
|
|
|
209
|
-
# Default name from hostname if not provided
|
|
210
409
|
if not name:
|
|
211
|
-
name = _derive_agent_name(endpoint)
|
|
410
|
+
name = _derive_agent_name(endpoint) if endpoint else "local-agent"
|
|
411
|
+
|
|
412
|
+
client = HumanboundClient()
|
|
413
|
+
if client.is_authenticated() and client.organisation_id:
|
|
414
|
+
_connect_agent_platform(
|
|
415
|
+
client, endpoint, name, prompt, repo, openapi, context, level, yes, timeout
|
|
416
|
+
)
|
|
417
|
+
else:
|
|
418
|
+
_connect_agent_local(endpoint, name, prompt, repo, openapi, context, level, yes, timeout)
|
|
419
|
+
|
|
212
420
|
|
|
421
|
+
def _connect_agent_platform(
|
|
422
|
+
client, endpoint, name, prompt, repo, openapi, context, level, yes, timeout
|
|
423
|
+
):
|
|
424
|
+
"""Platform flow: POST /scan -> create project -> auto-test -> dashboard."""
|
|
213
425
|
console.print(f"\n[bold]Connecting agent:[/bold] {name}\n")
|
|
214
426
|
|
|
215
427
|
try:
|
|
@@ -388,6 +600,144 @@ def _connect_agent(endpoint, name, prompt, repo, openapi, context, level, yes, t
|
|
|
388
600
|
raise SystemExit(1)
|
|
389
601
|
|
|
390
602
|
|
|
603
|
+
# -- Agent path (local) --------------------------------------------------------
|
|
604
|
+
|
|
605
|
+
|
|
606
|
+
def _connect_agent_local(endpoint, name, prompt, repo, openapi, context, level, yes, timeout):
|
|
607
|
+
"""Local flow: use the user's LLM to derive scope + lightweight compliance.
|
|
608
|
+
|
|
609
|
+
Writes ./scope.yaml in the current directory. Does not create a project,
|
|
610
|
+
does not call the Platform. Prints a note after completion pointing at
|
|
611
|
+
`hb login` for regulatory compliance + persistence + team features.
|
|
612
|
+
"""
|
|
613
|
+
from ..engine.llm import get_llm_pinger
|
|
614
|
+
from ..engine.local_runner import _resolve_provider
|
|
615
|
+
from ..engine.scope import resolve as resolve_scope
|
|
616
|
+
|
|
617
|
+
console.print("\n[dim](not authenticated — running local scope extraction)[/dim]")
|
|
618
|
+
console.print(f"[bold]Connecting agent:[/bold] {name}\n")
|
|
619
|
+
|
|
620
|
+
try:
|
|
621
|
+
provider = _resolve_provider()
|
|
622
|
+
llm = get_llm_pinger(provider)
|
|
623
|
+
except ValueError as e:
|
|
624
|
+
console.print(f"[red]{e}[/red]")
|
|
625
|
+
raise SystemExit(1)
|
|
626
|
+
|
|
627
|
+
integration = None
|
|
628
|
+
if endpoint:
|
|
629
|
+
try:
|
|
630
|
+
integration = _load_integration(endpoint)
|
|
631
|
+
chat_ep = integration.get("chat_completion", {}).get("endpoint", "")
|
|
632
|
+
console.print(
|
|
633
|
+
f" [green]✓[/green] Endpoint source: [dim]{chat_ep or '(from config)'}[/dim]"
|
|
634
|
+
)
|
|
635
|
+
except Exception as e:
|
|
636
|
+
console.print(f" [yellow]![/yellow] Endpoint could not be loaded: {e}")
|
|
637
|
+
|
|
638
|
+
if repo:
|
|
639
|
+
console.print(f" [green]✓[/green] Scanning repository: [dim]{repo}[/dim]")
|
|
640
|
+
if prompt:
|
|
641
|
+
console.print(f" [green]✓[/green] Loading prompt: [dim]{Path(prompt).name}[/dim]")
|
|
642
|
+
if openapi:
|
|
643
|
+
console.print(f" [green]✓[/green] Parsing OpenAPI: [dim]{Path(openapi).name}[/dim]")
|
|
644
|
+
|
|
645
|
+
console.print()
|
|
646
|
+
|
|
647
|
+
with console.status("[dim]Extracting scope via local LLM...[/dim]"):
|
|
648
|
+
try:
|
|
649
|
+
scope = resolve_scope(
|
|
650
|
+
repo_path=repo,
|
|
651
|
+
prompt_path=prompt,
|
|
652
|
+
scope_path=None,
|
|
653
|
+
integration=integration,
|
|
654
|
+
llm_pinger=llm,
|
|
655
|
+
)
|
|
656
|
+
except Exception as e:
|
|
657
|
+
console.print(f"[red]Scope extraction failed:[/red] {e}")
|
|
658
|
+
raise SystemExit(1)
|
|
659
|
+
|
|
660
|
+
# Lightweight compliance overlay — detect domain, apply template + EU AI Act
|
|
661
|
+
from ..engine.compliance import (
|
|
662
|
+
apply_eu_ai_act_only,
|
|
663
|
+
apply_template,
|
|
664
|
+
detect_domain,
|
|
665
|
+
domain_label,
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
domain = detect_domain(scope)
|
|
669
|
+
if domain:
|
|
670
|
+
console.print(f" [green]✓[/green] Detected domain: [bold]{domain_label(domain)}[/bold]")
|
|
671
|
+
scope = apply_template(scope, domain, include_eu_ai_act=True)
|
|
672
|
+
console.print(
|
|
673
|
+
" [green]✓[/green] Applied compliance overlay ([dim]domain template + EU AI Act[/dim])"
|
|
674
|
+
)
|
|
675
|
+
else:
|
|
676
|
+
scope = apply_eu_ai_act_only(scope)
|
|
677
|
+
console.print(
|
|
678
|
+
" [dim]No domain-specific template matched — EU AI Act overlay applied.[/dim]"
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
console.print()
|
|
682
|
+
_display_scope(scope)
|
|
683
|
+
|
|
684
|
+
output_path = Path.cwd() / "scope.yaml"
|
|
685
|
+
try:
|
|
686
|
+
_write_scope_yaml(scope, output_path)
|
|
687
|
+
console.print(f"\n [green]✓[/green] Wrote [bold]{output_path.name}[/bold]")
|
|
688
|
+
except Exception as e:
|
|
689
|
+
console.print(f"\n [yellow]Could not write scope.yaml:[/yellow] {e}")
|
|
690
|
+
|
|
691
|
+
_print_platform_note()
|
|
692
|
+
|
|
693
|
+
_print_next(
|
|
694
|
+
[
|
|
695
|
+
(f"hb test --scope {output_path.name}", "Run a security test with this scope"),
|
|
696
|
+
("hb login", "Sign in for regulatory compliance + team features"),
|
|
697
|
+
]
|
|
698
|
+
)
|
|
699
|
+
|
|
700
|
+
|
|
701
|
+
def _write_scope_yaml(scope: dict, path: Path):
|
|
702
|
+
"""Serialize scope dict to a .yaml file in the canonical template shape."""
|
|
703
|
+
try:
|
|
704
|
+
import yaml
|
|
705
|
+
except ImportError:
|
|
706
|
+
path.write_text(json.dumps(scope, indent=2))
|
|
707
|
+
return
|
|
708
|
+
|
|
709
|
+
intents = scope.get("intents", {})
|
|
710
|
+
document = {
|
|
711
|
+
"business_scope": scope.get("overall_business_scope", ""),
|
|
712
|
+
"permitted": intents.get("permitted", []),
|
|
713
|
+
"restricted": intents.get("restricted", []),
|
|
714
|
+
"more_info": scope.get("more_info", ""),
|
|
715
|
+
}
|
|
716
|
+
path.write_text(yaml.safe_dump(document, sort_keys=False, default_flow_style=False))
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _print_platform_note():
|
|
720
|
+
"""Note shown after local scope extraction summarising the Platform features.
|
|
721
|
+
|
|
722
|
+
Kept factual and community-neutral — this is an OSS CLI, not a sales page.
|
|
723
|
+
"""
|
|
724
|
+
lines = [
|
|
725
|
+
"Scope & policies saved locally.",
|
|
726
|
+
"",
|
|
727
|
+
"Regulatory mapping (FCA, EU AI Act, HIPAA, IDD, CRA 2015), threat",
|
|
728
|
+
"prioritisation with citations, and persistent project history are",
|
|
729
|
+
"available when signed in with [bold]hb login[/bold].",
|
|
730
|
+
]
|
|
731
|
+
console.print()
|
|
732
|
+
console.print(
|
|
733
|
+
Panel(
|
|
734
|
+
"\n".join(lines),
|
|
735
|
+
title="[bold]Local result[/bold]",
|
|
736
|
+
border_style="cyan",
|
|
737
|
+
)
|
|
738
|
+
)
|
|
739
|
+
|
|
740
|
+
|
|
391
741
|
# -- Platform path -------------------------------------------------------------
|
|
392
742
|
|
|
393
743
|
|
|
@@ -669,8 +1019,7 @@ def _auto_test(client, project_id, default_integration, context=None, level="uni
|
|
|
669
1019
|
# Build configuration with integration + optional context
|
|
670
1020
|
configuration = {"integration": default_integration}
|
|
671
1021
|
if context:
|
|
672
|
-
|
|
673
|
-
ctx_value = ctx_path.read_text().strip() if ctx_path.is_file() else context
|
|
1022
|
+
ctx_value = _resolve_context(context)
|
|
674
1023
|
if len(ctx_value) > 1500:
|
|
675
1024
|
console.print(
|
|
676
1025
|
f"[red]Context too long ({len(ctx_value)} chars). Maximum is 1,500.[/red]"
|
|
@@ -128,6 +128,26 @@ def _load_integration(value: str) -> dict:
|
|
|
128
128
|
raise SystemExit(1)
|
|
129
129
|
|
|
130
130
|
|
|
131
|
+
def _resolve_context(value: str) -> str:
|
|
132
|
+
"""Return the literal --context string, or the file contents if value is a path.
|
|
133
|
+
|
|
134
|
+
Wraps Path.is_file() in a try/except because stat() raises OSError with
|
|
135
|
+
errno ENAMETOOLONG (63) on macOS / ENAMETOOLONG on Linux when the argument
|
|
136
|
+
is a long string (e.g. a multi-line prompt passed inline). Before this
|
|
137
|
+
helper existed, `hb test --context "<long literal>"` crashed at the
|
|
138
|
+
Path(value).is_file() call.
|
|
139
|
+
"""
|
|
140
|
+
if not value:
|
|
141
|
+
return ""
|
|
142
|
+
try:
|
|
143
|
+
path = Path(value)
|
|
144
|
+
if path.is_file():
|
|
145
|
+
return path.read_text().strip()
|
|
146
|
+
except OSError:
|
|
147
|
+
pass
|
|
148
|
+
return value
|
|
149
|
+
|
|
150
|
+
|
|
131
151
|
def _print_next(suggestions: list):
|
|
132
152
|
"""Print Next: suggestions block."""
|
|
133
153
|
console.print("\n[dim]Next:[/dim]")
|
|
@@ -303,7 +323,7 @@ def test_command(
|
|
|
303
323
|
category_short = test_category.split("/")[-1]
|
|
304
324
|
name = f"cli-{category_short}-{timestamp}"
|
|
305
325
|
|
|
306
|
-
# --- Runner selection (login is the
|
|
326
|
+
# --- Runner selection (login + project is the switch) ---
|
|
307
327
|
try:
|
|
308
328
|
runner = get_runner(force_local=local)
|
|
309
329
|
except Exception:
|
|
@@ -323,6 +343,33 @@ def test_command(
|
|
|
323
343
|
console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
|
|
324
344
|
console.print("[dim]Local engine coming soon in the open-core release.[/dim]")
|
|
325
345
|
raise SystemExit(1)
|
|
346
|
+
elif not local:
|
|
347
|
+
# Runner fell to LocalTestRunner without the user asking for it. The only
|
|
348
|
+
# way that happens after the auth check above is "signed in but no
|
|
349
|
+
# project selected." Without this guard the user gets a misleading
|
|
350
|
+
# "No LLM provider configured" error from LocalRunner, when what they
|
|
351
|
+
# actually need is to select a project.
|
|
352
|
+
from ..client import HumanboundClient
|
|
353
|
+
|
|
354
|
+
_c = HumanboundClient()
|
|
355
|
+
if _c.is_authenticated():
|
|
356
|
+
console.print(
|
|
357
|
+
"[yellow]You're signed in, but no project is selected for this test.[/yellow]"
|
|
358
|
+
)
|
|
359
|
+
console.print()
|
|
360
|
+
console.print(" Choose one:")
|
|
361
|
+
console.print(" [bold]hb projects list[/bold] see your projects")
|
|
362
|
+
console.print(" [bold]hb projects use <id>[/bold] use an existing project")
|
|
363
|
+
console.print(
|
|
364
|
+
" [bold]hb connect --endpoint X[/bold] create a new project "
|
|
365
|
+
"from an agent config"
|
|
366
|
+
)
|
|
367
|
+
console.print(
|
|
368
|
+
" [bold]hb test --local ...[/bold] run against a local LLM "
|
|
369
|
+
"(requires HB_PROVIDER + HB_API_KEY)"
|
|
370
|
+
)
|
|
371
|
+
console.print()
|
|
372
|
+
raise SystemExit(1)
|
|
326
373
|
|
|
327
374
|
console.print(f"\n[bold]Starting security test:[/bold] {name}\n")
|
|
328
375
|
console.print(f" Category: {test_category}")
|
|
@@ -366,15 +413,12 @@ def test_command(
|
|
|
366
413
|
console.print(" Depth: [yellow]blackbox[/yellow]")
|
|
367
414
|
|
|
368
415
|
# Context: string or path to .txt file (max 1500 chars)
|
|
369
|
-
ctx_value = ""
|
|
370
|
-
if
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
f"[red]Context too long ({len(ctx_value)} chars). Maximum is 1,500.[/red]"
|
|
376
|
-
)
|
|
377
|
-
raise SystemExit(1)
|
|
416
|
+
ctx_value = _resolve_context(context) if context else ""
|
|
417
|
+
if ctx_value and len(ctx_value) > 1500:
|
|
418
|
+
console.print(
|
|
419
|
+
f"[red]Context too long ({len(ctx_value)} chars). Maximum is 1,500.[/red]"
|
|
420
|
+
)
|
|
421
|
+
raise SystemExit(1)
|
|
378
422
|
|
|
379
423
|
# Build TestConfig (canonical shape — same for both runners)
|
|
380
424
|
config = TestConfig(
|