humanbound-cli 0.3.2__tar.gz → 0.3.3__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_cli-0.3.2 → humanbound_cli-0.3.3}/PKG-INFO +1 -1
- humanbound_cli-0.3.3/humanbound_cli/__init__.py +8 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/client.py +19 -7
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/api_keys.py +2 -2
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/auth.py +12 -2
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/campaigns.py +1 -1
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/experiments.py +106 -3
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/findings.py +2 -2
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/logs.py +1 -1
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/members.py +2 -2
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/providers.py +1 -1
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/test.py +13 -13
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/main.py +16 -1
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/PKG-INFO +1 -1
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/pyproject.toml +1 -1
- humanbound_cli-0.3.2/humanbound_cli/__init__.py +0 -3
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/LICENSE +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/README.md +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/__init__.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/coverage.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/docs.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/guardrails.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/init.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/orgs.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/posture.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/projects.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/scan.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/commands/upload_logs.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/config.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/exceptions.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/extractors/__init__.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/extractors/openapi.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/extractors/repo.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/pytest_plugin/__init__.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/pytest_plugin/report.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/report.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/__init__.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/config_builder.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/local_server.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/runtime_detector.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli/serve/tunnel_client.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/SOURCES.txt +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/dependency_links.txt +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/entry_points.txt +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/requires.txt +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/humanbound_cli.egg-info/top_level.txt +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/relay/relay.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/setup.cfg +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/__init__.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/cli_integration_test.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/conftest.py +0 -0
- {humanbound_cli-0.3.2 → humanbound_cli-0.3.3}/tests/test_cli_commands.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: humanbound-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Humanbound CLI - command line interface for AI agent security testing.
|
|
5
5
|
Author-email: Kostas Siabanis <hello@humanbound.ai>, Demetris Gerogiannis <hello@humanbound.ai>
|
|
6
6
|
License: Apache-2.0
|
|
@@ -471,16 +471,24 @@ class HumanboundClient:
|
|
|
471
471
|
|
|
472
472
|
def _exchange_for_api_token(self) -> None:
|
|
473
473
|
"""Exchange Auth0 token for API session token."""
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
474
|
+
try:
|
|
475
|
+
response = requests.get(
|
|
476
|
+
f"{self.base_url}/auth",
|
|
477
|
+
headers={"Authorization": f"Bearer {self._auth0_token}"},
|
|
478
|
+
timeout=DEFAULT_TIMEOUT,
|
|
479
|
+
)
|
|
480
|
+
except requests.ConnectionError:
|
|
481
|
+
raise AuthenticationError(f"Could not connect to {self.base_url}. Is the server running?")
|
|
482
|
+
except requests.Timeout:
|
|
483
|
+
raise AuthenticationError(f"Connection to {self.base_url} timed out.")
|
|
479
484
|
|
|
480
485
|
if response.status_code != 200:
|
|
481
|
-
raise AuthenticationError(f"API authentication failed: {response.text}")
|
|
486
|
+
raise AuthenticationError(f"API authentication failed ({response.status_code}): {response.text}")
|
|
482
487
|
|
|
483
|
-
|
|
488
|
+
try:
|
|
489
|
+
data = response.json()
|
|
490
|
+
except Exception:
|
|
491
|
+
raise AuthenticationError(f"API returned invalid response from {self.base_url}/auth")
|
|
484
492
|
self._api_token = data.get("access_token") or data.get("token")
|
|
485
493
|
if not self._api_token:
|
|
486
494
|
raise AuthenticationError("No access token in API response")
|
|
@@ -537,6 +545,10 @@ class HumanboundClient:
|
|
|
537
545
|
self._username = credentials.get("username")
|
|
538
546
|
self._email = credentials.get("email")
|
|
539
547
|
self._default_organisation_id = credentials.get("default_organisation_id")
|
|
548
|
+
# Restore saved base_url unless explicitly overridden via --base-url or env var
|
|
549
|
+
saved_url = credentials.get("base_url")
|
|
550
|
+
if saved_url and self.base_url == get_base_url().rstrip("/"):
|
|
551
|
+
self.base_url = saved_url.rstrip("/")
|
|
540
552
|
|
|
541
553
|
def _load_credentials_file(self) -> dict:
|
|
542
554
|
"""Load credentials file."""
|
|
@@ -54,7 +54,7 @@ def list_keys(as_json: bool):
|
|
|
54
54
|
return
|
|
55
55
|
|
|
56
56
|
table = Table(title="API Keys")
|
|
57
|
-
table.add_column("ID", style="dim"
|
|
57
|
+
table.add_column("ID", style="dim")
|
|
58
58
|
table.add_column("Name", style="bold")
|
|
59
59
|
table.add_column("Scopes")
|
|
60
60
|
table.add_column("Active", justify="center")
|
|
@@ -67,7 +67,7 @@ def list_keys(as_json: bool):
|
|
|
67
67
|
prefix = (key_val[:12] + "...") if len(str(key_val)) > 12 else str(key_val)
|
|
68
68
|
|
|
69
69
|
table.add_row(
|
|
70
|
-
str(key.get("id", ""))
|
|
70
|
+
str(key.get("id", "")),
|
|
71
71
|
key.get("name", ""),
|
|
72
72
|
key.get("scopes", ""),
|
|
73
73
|
active,
|
|
@@ -5,6 +5,7 @@ from rich.console import Console
|
|
|
5
5
|
from rich.panel import Panel
|
|
6
6
|
|
|
7
7
|
from ..client import HumanboundClient
|
|
8
|
+
from ..config import DEFAULT_BASE_URL
|
|
8
9
|
from ..exceptions import AuthenticationError, APIError
|
|
9
10
|
|
|
10
11
|
console = Console()
|
|
@@ -50,9 +51,14 @@ def login(base_url: str, port: int, force: bool):
|
|
|
50
51
|
except Exception as e:
|
|
51
52
|
org_display = f"{client.default_organisation_id} (could not resolve name: {e})"
|
|
52
53
|
|
|
54
|
+
base_url_line = ""
|
|
55
|
+
if client.base_url.rstrip("/") != DEFAULT_BASE_URL.rstrip("/"):
|
|
56
|
+
base_url_line = f"Base URL: {client.base_url}\n"
|
|
57
|
+
|
|
53
58
|
console.print(Panel(
|
|
54
59
|
f"[green]Login successful![/green]\n\n"
|
|
55
60
|
f"User: {client.username or 'unknown'}\n"
|
|
61
|
+
f"{base_url_line}"
|
|
56
62
|
f"Organisation: {org_display}",
|
|
57
63
|
title="Humanbound",
|
|
58
64
|
))
|
|
@@ -109,7 +115,7 @@ def logout(revoke: bool, port: int):
|
|
|
109
115
|
finally:
|
|
110
116
|
server.server_close()
|
|
111
117
|
|
|
112
|
-
console.print("[green]Browser session revoked.[/green]")
|
|
118
|
+
console.print("[green]Browser session revoked. Base URL reset to default.[/green]")
|
|
113
119
|
else:
|
|
114
120
|
console.print(
|
|
115
121
|
"[dim]Note: Your browser may still have an active Auth0 session. "
|
|
@@ -135,11 +141,15 @@ def whoami():
|
|
|
135
141
|
except Exception as e:
|
|
136
142
|
org_display = f"{client.organisation_id} [dim](could not resolve name: {e})[/dim]"
|
|
137
143
|
|
|
144
|
+
base_url_line = ""
|
|
145
|
+
if client.base_url.rstrip("/") != DEFAULT_BASE_URL.rstrip("/"):
|
|
146
|
+
base_url_line = f"Base URL: {client.base_url}\n"
|
|
147
|
+
|
|
138
148
|
console.print(Panel(
|
|
139
149
|
f"[green]Authenticated[/green]\n\n"
|
|
140
150
|
f"User: {client.username or '[dim]unknown[/dim]'}\n"
|
|
141
151
|
f"Email: {client.email or '[dim]unknown[/dim]'}\n"
|
|
142
|
-
f"
|
|
152
|
+
f"{base_url_line}"
|
|
143
153
|
f"Organisation: {org_display}\n"
|
|
144
154
|
f"Project: {client.project_id or '[dim]not set[/dim]'}",
|
|
145
155
|
title="Humanbound Status",
|
|
@@ -156,7 +156,7 @@ def break_campaign(force: bool):
|
|
|
156
156
|
return
|
|
157
157
|
|
|
158
158
|
if not force:
|
|
159
|
-
if not Confirm.ask(f"Break campaign [bold]{campaign_id
|
|
159
|
+
if not Confirm.ask(f"Break campaign [bold]{campaign_id}[/bold]? Running experiments will be stopped"):
|
|
160
160
|
console.print("[dim]Cancelled.[/dim]")
|
|
161
161
|
return
|
|
162
162
|
|
|
@@ -142,13 +142,20 @@ def show_experiment(experiment_id: str):
|
|
|
142
142
|
|
|
143
143
|
|
|
144
144
|
@experiments_group.command("status")
|
|
145
|
-
@click.argument("experiment_id")
|
|
145
|
+
@click.argument("experiment_id", required=False)
|
|
146
146
|
@click.option("--watch", "-w", is_flag=True, help="Watch status until completion")
|
|
147
147
|
@click.option("--interval", default=10, help="Polling interval in seconds (with --watch)")
|
|
148
|
-
|
|
148
|
+
@click.option("--all", "show_all", is_flag=True, help="Show status of all project experiments (polls every 60s)")
|
|
149
|
+
def experiment_status(experiment_id: str, watch: bool, interval: int, show_all: bool):
|
|
149
150
|
"""Check experiment status.
|
|
150
151
|
|
|
151
|
-
EXPERIMENT_ID: Experiment UUID.
|
|
152
|
+
EXPERIMENT_ID: Experiment UUID (optional with --all).
|
|
153
|
+
|
|
154
|
+
\b
|
|
155
|
+
Examples:
|
|
156
|
+
hb experiments status <id> # One-shot status
|
|
157
|
+
hb experiments status <id> --watch # Poll single experiment
|
|
158
|
+
hb experiments status --all # Dashboard of all experiments (polls 60s)
|
|
152
159
|
"""
|
|
153
160
|
client = HumanboundClient()
|
|
154
161
|
|
|
@@ -156,6 +163,14 @@ def experiment_status(experiment_id: str, watch: bool, interval: int):
|
|
|
156
163
|
console.print("[yellow]No project selected.[/yellow]")
|
|
157
164
|
raise SystemExit(1)
|
|
158
165
|
|
|
166
|
+
if show_all:
|
|
167
|
+
_poll_all_experiments(client)
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
if not experiment_id:
|
|
171
|
+
console.print("[red]Provide an experiment ID or use --all.[/red]")
|
|
172
|
+
raise SystemExit(1)
|
|
173
|
+
|
|
159
174
|
try:
|
|
160
175
|
if not watch:
|
|
161
176
|
status = client.get_experiment_status(experiment_id)
|
|
@@ -201,6 +216,94 @@ def experiment_status(experiment_id: str, watch: bool, interval: int):
|
|
|
201
216
|
console.print("\n[yellow]Stopped watching.[/yellow]")
|
|
202
217
|
|
|
203
218
|
|
|
219
|
+
ACTIVE_STATUSES = ["Running", "Generating", "Generated", "Pending"]
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _poll_all_experiments(client: HumanboundClient):
|
|
223
|
+
"""Poll all project experiments every 60s until none are active."""
|
|
224
|
+
try:
|
|
225
|
+
cycle = 0
|
|
226
|
+
while True:
|
|
227
|
+
# Fetch all experiments (up to 200)
|
|
228
|
+
all_exps = []
|
|
229
|
+
page = 1
|
|
230
|
+
while True:
|
|
231
|
+
response = client.list_experiments(page=page, size=100)
|
|
232
|
+
all_exps.extend(response.get("data", []))
|
|
233
|
+
if not response.get("has_next_page"):
|
|
234
|
+
break
|
|
235
|
+
page += 1
|
|
236
|
+
|
|
237
|
+
if not all_exps:
|
|
238
|
+
console.print("[yellow]No experiments found.[/yellow]")
|
|
239
|
+
return
|
|
240
|
+
|
|
241
|
+
active = [e for e in all_exps if e.get("status") in ACTIVE_STATUSES]
|
|
242
|
+
finished = [e for e in all_exps if e.get("status") == "Finished"]
|
|
243
|
+
failed = [e for e in all_exps if e.get("status") == "Failed"]
|
|
244
|
+
|
|
245
|
+
# Clear screen on subsequent cycles
|
|
246
|
+
if cycle > 0:
|
|
247
|
+
console.clear()
|
|
248
|
+
|
|
249
|
+
timestamp = time.strftime("%H:%M:%S")
|
|
250
|
+
console.print(f"[bold]Project Experiments[/bold] [dim]{timestamp}[/dim]\n")
|
|
251
|
+
console.print(
|
|
252
|
+
f" Total: {len(all_exps)} "
|
|
253
|
+
f"[green]Finished: {len(finished)}[/green] "
|
|
254
|
+
f"[yellow]Active: {len(active)}[/yellow] "
|
|
255
|
+
f"[red]Failed: {len(failed)}[/red]\n"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
table = Table()
|
|
259
|
+
table.add_column("Name", style="bold", max_width=35)
|
|
260
|
+
table.add_column("Status", width=12)
|
|
261
|
+
table.add_column("Category", width=20)
|
|
262
|
+
table.add_column("ID", style="dim")
|
|
263
|
+
|
|
264
|
+
for exp in all_exps:
|
|
265
|
+
status = exp.get("status", "Unknown")
|
|
266
|
+
status_style = {
|
|
267
|
+
"Finished": "[green]Finished[/green]",
|
|
268
|
+
"Running": "[yellow]Running[/yellow]",
|
|
269
|
+
"Failed": "[red]Failed[/red]",
|
|
270
|
+
"Generating": "[cyan]Generating[/cyan]",
|
|
271
|
+
"Generated": "[blue]Generated[/blue]",
|
|
272
|
+
"Pending": "[dim]Pending[/dim]",
|
|
273
|
+
"Terminated": "[red]Terminated[/red]",
|
|
274
|
+
}.get(status, status)
|
|
275
|
+
|
|
276
|
+
cat = exp.get("test_category", "")
|
|
277
|
+
if "/" in cat:
|
|
278
|
+
cat = cat.split("/")[-1]
|
|
279
|
+
|
|
280
|
+
table.add_row(
|
|
281
|
+
exp.get("name", ""),
|
|
282
|
+
status_style,
|
|
283
|
+
cat,
|
|
284
|
+
exp.get("id", ""),
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
console.print(table)
|
|
288
|
+
|
|
289
|
+
if not active:
|
|
290
|
+
console.print(f"\n[green]All experiments completed.[/green]")
|
|
291
|
+
return
|
|
292
|
+
|
|
293
|
+
console.print(f"\n[dim]{len(active)} active — polling every 60s (Ctrl+C to stop)[/dim]")
|
|
294
|
+
time.sleep(60)
|
|
295
|
+
cycle += 1
|
|
296
|
+
|
|
297
|
+
except NotAuthenticatedError:
|
|
298
|
+
console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
|
|
299
|
+
raise SystemExit(1)
|
|
300
|
+
except APIError as e:
|
|
301
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
302
|
+
raise SystemExit(1)
|
|
303
|
+
except KeyboardInterrupt:
|
|
304
|
+
console.print("\n[yellow]Stopped polling.[/yellow]")
|
|
305
|
+
|
|
306
|
+
|
|
204
307
|
def _print_status(status: dict):
|
|
205
308
|
"""Print experiment status."""
|
|
206
309
|
current_status = status.get("status", "Unknown")
|
|
@@ -76,7 +76,7 @@ def findings_group(ctx, status, severity, page, size, as_json):
|
|
|
76
76
|
return
|
|
77
77
|
|
|
78
78
|
table = Table(title="Findings")
|
|
79
|
-
table.add_column("ID", style="dim"
|
|
79
|
+
table.add_column("ID", style="dim")
|
|
80
80
|
table.add_column("Title", max_width=40)
|
|
81
81
|
table.add_column("Severity", width=10)
|
|
82
82
|
table.add_column("Status", width=10)
|
|
@@ -89,7 +89,7 @@ def findings_group(ctx, status, severity, page, size, as_json):
|
|
|
89
89
|
stat = str(finding.get("status", "")).lower()
|
|
90
90
|
|
|
91
91
|
table.add_row(
|
|
92
|
-
str(finding.get("id", ""))
|
|
92
|
+
str(finding.get("id", "")),
|
|
93
93
|
finding.get("title", finding.get("description", ""))[:40],
|
|
94
94
|
SEVERITY_STYLES.get(sev, sev),
|
|
95
95
|
STATUS_STYLES.get(stat, stat),
|
|
@@ -127,7 +127,7 @@ def _show_table(client: HumanboundClient, experiment_id: str, verdict: str, page
|
|
|
127
127
|
return
|
|
128
128
|
|
|
129
129
|
table = Table(title=f"Experiment Logs (page {page})")
|
|
130
|
-
table.add_column("ID", style="dim"
|
|
130
|
+
table.add_column("ID", style="dim")
|
|
131
131
|
table.add_column("Verdict", width=6)
|
|
132
132
|
table.add_column("Severity", width=8)
|
|
133
133
|
table.add_column("Category", width=15)
|
|
@@ -55,7 +55,7 @@ def list_members(as_json: bool):
|
|
|
55
55
|
return
|
|
56
56
|
|
|
57
57
|
table = Table(title="Organisation Members")
|
|
58
|
-
table.add_column("ID", style="dim"
|
|
58
|
+
table.add_column("ID", style="dim")
|
|
59
59
|
table.add_column("Email", style="bold")
|
|
60
60
|
table.add_column("Username")
|
|
61
61
|
table.add_column("Role")
|
|
@@ -70,7 +70,7 @@ def list_members(as_json: bool):
|
|
|
70
70
|
}.get(str(role).lower(), str(role))
|
|
71
71
|
|
|
72
72
|
table.add_row(
|
|
73
|
-
str(member.get("id", ""))
|
|
73
|
+
str(member.get("id", "")),
|
|
74
74
|
member.get("email", ""),
|
|
75
75
|
member.get("username", ""),
|
|
76
76
|
role_style,
|
|
@@ -47,7 +47,7 @@ def list_providers():
|
|
|
47
47
|
return
|
|
48
48
|
|
|
49
49
|
table = Table(title="Model Providers")
|
|
50
|
-
table.add_column("ID", style="dim"
|
|
50
|
+
table.add_column("ID", style="dim")
|
|
51
51
|
table.add_column("Name", style="bold")
|
|
52
52
|
table.add_column("Model", width=25)
|
|
53
53
|
table.add_column("Default", justify="center")
|
|
@@ -13,13 +13,8 @@ from ..exceptions import NotAuthenticatedError, APIError
|
|
|
13
13
|
|
|
14
14
|
console = Console()
|
|
15
15
|
|
|
16
|
-
#
|
|
17
|
-
|
|
18
|
-
"humanbound/adversarial/owasp_single_turn",
|
|
19
|
-
"humanbound/adversarial/owasp_multi_turn",
|
|
20
|
-
"humanbound/adversarial/owasp_agentic_multi_turn",
|
|
21
|
-
"humanbound/behavioral/behavioral",
|
|
22
|
-
]
|
|
16
|
+
# Default test category
|
|
17
|
+
DEFAULT_TEST_CATEGORY = "humanbound/adversarial/owasp_multi_turn"
|
|
23
18
|
|
|
24
19
|
# Testing levels (must match backend TestingLevel enum)
|
|
25
20
|
# unit (~20 min), system (~45 min), acceptance (~90 min)
|
|
@@ -62,7 +57,7 @@ def _load_integration(value: str) -> dict:
|
|
|
62
57
|
raise SystemExit(1)
|
|
63
58
|
|
|
64
59
|
try:
|
|
65
|
-
return json.loads(value)
|
|
60
|
+
return json.loads(value.strip())
|
|
66
61
|
except json.JSONDecodeError:
|
|
67
62
|
console.print(f"[red]--endpoint must be a JSON string or path to a JSON file.[/red]")
|
|
68
63
|
console.print("[dim]Example: --endpoint ./bot-config.json[/dim]")
|
|
@@ -73,9 +68,8 @@ def _load_integration(value: str) -> dict:
|
|
|
73
68
|
@click.command("test")
|
|
74
69
|
@click.option(
|
|
75
70
|
"--test-category", "-t",
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
help="Test category to run"
|
|
71
|
+
default=DEFAULT_TEST_CATEGORY,
|
|
72
|
+
help="Test category to run (e.g. humanbound/adversarial/owasp_multi_turn, humanbound/behavioral/qa)"
|
|
79
73
|
)
|
|
80
74
|
@click.option(
|
|
81
75
|
"--testing-level", "-l",
|
|
@@ -87,6 +81,11 @@ def _load_integration(value: str) -> dict:
|
|
|
87
81
|
"--name", "-n",
|
|
88
82
|
help="Experiment name (auto-generated if not provided)"
|
|
89
83
|
)
|
|
84
|
+
@click.option(
|
|
85
|
+
"--description", "-d",
|
|
86
|
+
default="",
|
|
87
|
+
help="Experiment description"
|
|
88
|
+
)
|
|
90
89
|
@click.option(
|
|
91
90
|
"--lang",
|
|
92
91
|
default="english",
|
|
@@ -117,8 +116,8 @@ def _load_integration(value: str) -> dict:
|
|
|
117
116
|
type=click.Choice(["critical", "high", "medium", "low", "any"]),
|
|
118
117
|
help="Exit with error if findings of this severity or higher are found"
|
|
119
118
|
)
|
|
120
|
-
def test_command(test_category: str, testing_level: str, name: str,
|
|
121
|
-
provider_id: str, endpoint: str,
|
|
119
|
+
def test_command(test_category: str, testing_level: str, name: str, description: str,
|
|
120
|
+
lang: str, provider_id: str, endpoint: str,
|
|
122
121
|
no_auto_start: bool,
|
|
123
122
|
wait: bool, fail_on: str):
|
|
124
123
|
"""Run security tests on the current project.
|
|
@@ -185,6 +184,7 @@ def test_command(test_category: str, testing_level: str, name: str, lang: str,
|
|
|
185
184
|
# Create experiment
|
|
186
185
|
experiment_data = {
|
|
187
186
|
"name": name,
|
|
187
|
+
"description": description,
|
|
188
188
|
"test_category": test_category,
|
|
189
189
|
"testing_level": testing_level,
|
|
190
190
|
"lang": lang,
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import click
|
|
4
4
|
from rich.console import Console
|
|
5
5
|
|
|
6
|
+
from . import __version__
|
|
6
7
|
from .client import HumanboundClient
|
|
7
8
|
from .config import get_base_url
|
|
8
9
|
|
|
@@ -35,6 +36,7 @@ def get_client() -> HumanboundClient:
|
|
|
35
36
|
|
|
36
37
|
|
|
37
38
|
@click.group()
|
|
39
|
+
@click.version_option(version=__version__, prog_name="hb")
|
|
38
40
|
@click.option(
|
|
39
41
|
"--base-url",
|
|
40
42
|
envvar="HUMANBOUND_BASE_URL",
|
|
@@ -149,12 +151,24 @@ def switch_org(org_id: str):
|
|
|
149
151
|
@cli.command("status")
|
|
150
152
|
@click.argument("experiment_id", required=False)
|
|
151
153
|
@click.option("--watch", "-w", is_flag=True, help="Watch status until completion")
|
|
154
|
+
@click.option("--all", "show_all", is_flag=True, help="Show all project experiments (polls every 60s)")
|
|
152
155
|
@click.pass_context
|
|
153
|
-
def status_alias(ctx, experiment_id: str, watch: bool):
|
|
156
|
+
def status_alias(ctx, experiment_id: str, watch: bool, show_all: bool):
|
|
154
157
|
"""Check experiment status (alias for 'experiments status').
|
|
155
158
|
|
|
156
159
|
If no experiment_id is provided, shows the most recent experiment.
|
|
160
|
+
Use --all to see a dashboard of all project experiments.
|
|
157
161
|
"""
|
|
162
|
+
if show_all:
|
|
163
|
+
ctx.invoke(
|
|
164
|
+
experiments.experiment_status,
|
|
165
|
+
experiment_id=None,
|
|
166
|
+
watch=False,
|
|
167
|
+
interval=10,
|
|
168
|
+
show_all=True,
|
|
169
|
+
)
|
|
170
|
+
return
|
|
171
|
+
|
|
158
172
|
client = HumanboundClient()
|
|
159
173
|
|
|
160
174
|
if not experiment_id:
|
|
@@ -180,6 +194,7 @@ def status_alias(ctx, experiment_id: str, watch: bool):
|
|
|
180
194
|
experiment_id=experiment_id,
|
|
181
195
|
watch=watch,
|
|
182
196
|
interval=10,
|
|
197
|
+
show_all=False,
|
|
183
198
|
)
|
|
184
199
|
|
|
185
200
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: humanbound-cli
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.3
|
|
4
4
|
Summary: Humanbound CLI - command line interface for AI agent security testing.
|
|
5
5
|
Author-email: Kostas Siabanis <hello@humanbound.ai>, Demetris Gerogiannis <hello@humanbound.ai>
|
|
6
6
|
License: Apache-2.0
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|