humanbound-cli 0.6.1__tar.gz → 0.6.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_cli-0.6.1 → humanbound_cli-0.6.2}/PKG-INFO +1 -1
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/connect.py +11 -9
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/posture.py +67 -36
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/test.py +2 -2
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/mcp_server.py +163 -1
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/PKG-INFO +1 -1
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/top_level.txt +1 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/pyproject.toml +1 -1
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/LICENSE +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/README.md +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/__init__.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/client.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/__init__.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/api_keys.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/assessments.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/auth.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/campaigns.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/completion.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/connectors.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/coverage.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/discover.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/docs.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/experiments.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/findings.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/guardrails.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/init.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/inventory.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/logs.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/mcp.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/members.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/monitor.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/orgs.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/projects.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/providers.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/report.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/scan.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/sentinel.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/upload_logs.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/webhooks.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/config.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/connectors/__init__.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/connectors/microsoft.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/exceptions.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/extractors/__init__.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/extractors/openapi.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/extractors/repo.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/main.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/pytest_plugin/__init__.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/pytest_plugin/report.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/report.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/report_builder.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/__init__.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/config_builder.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/local_server.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/runtime_detector.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/tunnel_client.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/SOURCES.txt +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/dependency_links.txt +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/entry_points.txt +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/requires.txt +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/relay/relay.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/setup.cfg +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/tests/__init__.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/tests/cli_integration_test.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/tests/conftest.py +0 -0
- {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/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.6.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
@@ -43,7 +43,7 @@ def _derive_agent_name(endpoint: str) -> str:
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
@click.command("connect")
|
|
46
|
-
@click.option("--endpoint", "-e", help="
|
|
46
|
+
@click.option("--endpoint", "-e", help="Agent config JSON or file path (agent path)")
|
|
47
47
|
@click.option(
|
|
48
48
|
"--vendor", "-v",
|
|
49
49
|
type=click.Choice(["microsoft"]),
|
|
@@ -53,7 +53,7 @@ def _derive_agent_name(endpoint: str) -> str:
|
|
|
53
53
|
@click.option("--prompt", "-p", type=click.Path(exists=True), help="System prompt file (agent path)")
|
|
54
54
|
@click.option("--repo", "-r", type=click.Path(exists=True), help="Repository path (agent path)")
|
|
55
55
|
@click.option("--openapi", "-o", type=click.Path(exists=True), help="OpenAPI spec file (agent path)")
|
|
56
|
-
@click.option("--serve", "-s", is_flag=True, help="Launch repo
|
|
56
|
+
@click.option("--serve", "-s", is_flag=True, help="Launch repo agent locally (agent path, requires --repo)")
|
|
57
57
|
@click.option("--tenant", help="Azure tenant ID (platform path, bypasses browser)")
|
|
58
58
|
@click.option("--client-id", "client_id", help="Service principal client ID (platform path)")
|
|
59
59
|
@click.option("--client-secret", "client_secret", help="Service principal secret (platform path)")
|
|
@@ -69,7 +69,7 @@ def connect_command(endpoint, vendor, name, prompt, repo, openapi, serve,
|
|
|
69
69
|
\b
|
|
70
70
|
Agent path (--endpoint):
|
|
71
71
|
hb connect --endpoint ./bot-config.json
|
|
72
|
-
Probes your
|
|
72
|
+
Probes your agent, extracts scope, creates project, runs first test.
|
|
73
73
|
|
|
74
74
|
\b
|
|
75
75
|
Platform path (--vendor):
|
|
@@ -97,10 +97,10 @@ def connect_command(endpoint, vendor, name, prompt, repo, openapi, serve,
|
|
|
97
97
|
else:
|
|
98
98
|
console.print("[yellow]Specify a path:[/yellow]")
|
|
99
99
|
console.print()
|
|
100
|
-
console.print(" [bold]Agent:[/bold]
|
|
100
|
+
console.print(" [bold]Agent:[/bold] hb connect --endpoint ./bot-config.json")
|
|
101
101
|
console.print(" [bold]Platform:[/bold] hb connect --vendor microsoft")
|
|
102
102
|
console.print()
|
|
103
|
-
console.print("[dim]Use --endpoint to connect
|
|
103
|
+
console.print("[dim]Use --endpoint to connect your AI agent, or --vendor to scan your cloud.[/dim]")
|
|
104
104
|
raise SystemExit(1)
|
|
105
105
|
|
|
106
106
|
|
|
@@ -192,7 +192,7 @@ def _connect_agent(endpoint, name, prompt, repo, openapi, serve, context, yes, t
|
|
|
192
192
|
|
|
193
193
|
if runtime_info and not serve and not endpoint and not yes:
|
|
194
194
|
console.print()
|
|
195
|
-
console.print(f" [cyan]i[/cyan] Detected runnable
|
|
195
|
+
console.print(f" [cyan]i[/cyan] Detected runnable agent: [bold]{runtime_info.framework.title()}[/bold] ({runtime_info.entry_point})")
|
|
196
196
|
console.print(f" Start command: [dim]{runtime_info.start_cmd.replace('{port}', str(runtime_info.port))}[/dim]")
|
|
197
197
|
from rich.prompt import Confirm
|
|
198
198
|
if Confirm.ask(" Launch it for live probing?", default=False):
|
|
@@ -209,7 +209,7 @@ def _connect_agent(endpoint, name, prompt, repo, openapi, serve, context, yes, t
|
|
|
209
209
|
if spec_result:
|
|
210
210
|
operations = spec_result.get("operations", [])
|
|
211
211
|
console.print(f" [green]\u2713[/green] OpenAPI spec: {len(operations)} operations")
|
|
212
|
-
summary_parts = [spec_result.get("description", "API-based
|
|
212
|
+
summary_parts = [spec_result.get("description", "API-based agent")]
|
|
213
213
|
for op in operations:
|
|
214
214
|
summary_parts.append(
|
|
215
215
|
f"- {op.get('method', 'GET')} {op.get('path', '')}: {op.get('summary', '')}"
|
|
@@ -228,7 +228,7 @@ def _connect_agent(endpoint, name, prompt, repo, openapi, serve, context, yes, t
|
|
|
228
228
|
# -- Serve lifecycle: start server + tunnel ----------------------------
|
|
229
229
|
if serve and repo:
|
|
230
230
|
if not runtime_info:
|
|
231
|
-
console.print("[yellow]Could not detect a runnable
|
|
231
|
+
console.print("[yellow]Could not detect a runnable agent in the repository.[/yellow]")
|
|
232
232
|
console.print("[dim]Continuing with static analysis only.[/dim]")
|
|
233
233
|
else:
|
|
234
234
|
_server, _tunnel, serve_source = _start_serve(
|
|
@@ -513,7 +513,7 @@ def _connect_platform(vendor, name, tenant, client_id, client_secret, yes, timeo
|
|
|
513
513
|
def _auto_test(client, project_id, default_integration, context=None):
|
|
514
514
|
"""Run first test automatically and show results inline."""
|
|
515
515
|
if not default_integration:
|
|
516
|
-
console.print("\n[dim]No
|
|
516
|
+
console.print("\n[dim]No agent integration configured -- skipping auto-test.[/dim]")
|
|
517
517
|
console.print("[dim]Run 'hb test -e ./bot-config.json' to test manually.[/dim]")
|
|
518
518
|
return
|
|
519
519
|
|
|
@@ -569,5 +569,7 @@ def _auto_test(client, project_id, default_integration, context=None):
|
|
|
569
569
|
console.print(f" [dim]View logs:[/dim] hb logs {exp_id}")
|
|
570
570
|
|
|
571
571
|
except Exception as e:
|
|
572
|
+
import traceback
|
|
572
573
|
console.print(f"\n[yellow]Auto-test failed:[/yellow] {e}")
|
|
574
|
+
console.print(f"[dim]{traceback.format_exc()}[/dim]")
|
|
573
575
|
console.print("[dim]Run 'hb test' to try again.[/dim]")
|
|
@@ -18,7 +18,9 @@ console = Console()
|
|
|
18
18
|
@click.option("--trends", is_flag=True, help="Show posture history over time")
|
|
19
19
|
@click.option("--org", is_flag=True, help="Show org-level posture (3 dimensions)")
|
|
20
20
|
@click.option("--coverage", is_flag=True, help="Include test coverage breakdown")
|
|
21
|
-
def posture_command(
|
|
21
|
+
def posture_command(
|
|
22
|
+
project: str, as_json: bool, trends: bool, org: bool, coverage: bool
|
|
23
|
+
):
|
|
22
24
|
"""View security posture score for a project.
|
|
23
25
|
|
|
24
26
|
The posture score is a composite metric (0-100) reflecting:
|
|
@@ -52,10 +54,13 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
|
|
|
52
54
|
raise SystemExit(1)
|
|
53
55
|
|
|
54
56
|
with console.status("Calculating organisation posture..."):
|
|
55
|
-
response = client.get(
|
|
57
|
+
response = client.get(
|
|
58
|
+
f"organisations/{org_id}/posture", include_project=False
|
|
59
|
+
)
|
|
56
60
|
|
|
57
61
|
if as_json:
|
|
58
62
|
import json
|
|
63
|
+
|
|
59
64
|
print(json.dumps(response, indent=2, default=str))
|
|
60
65
|
return
|
|
61
66
|
|
|
@@ -82,6 +87,7 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
|
|
|
82
87
|
|
|
83
88
|
if as_json:
|
|
84
89
|
import json
|
|
90
|
+
|
|
85
91
|
print(json.dumps(response, indent=2, default=str))
|
|
86
92
|
return
|
|
87
93
|
|
|
@@ -90,10 +96,13 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
|
|
|
90
96
|
|
|
91
97
|
# Get posture from API
|
|
92
98
|
with console.status("Calculating posture..."):
|
|
93
|
-
response = client.get(
|
|
99
|
+
response = client.get(
|
|
100
|
+
f"projects/{project_id}/posture", include_project=True
|
|
101
|
+
)
|
|
94
102
|
|
|
95
103
|
if as_json:
|
|
96
104
|
import json
|
|
105
|
+
|
|
97
106
|
print(json.dumps(response, indent=2, default=str))
|
|
98
107
|
return
|
|
99
108
|
|
|
@@ -125,7 +134,7 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
|
|
|
125
134
|
|
|
126
135
|
def _display_posture(posture: dict):
|
|
127
136
|
"""Display posture score with visual breakdown."""
|
|
128
|
-
score = posture.get("
|
|
137
|
+
score = posture.get("overall_score", 0)
|
|
129
138
|
grade = posture.get("grade", _score_to_grade(score))
|
|
130
139
|
|
|
131
140
|
# Color based on score
|
|
@@ -140,16 +149,21 @@ def _display_posture(posture: dict):
|
|
|
140
149
|
emoji = "✗"
|
|
141
150
|
|
|
142
151
|
# Main score panel
|
|
143
|
-
console.print(
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
console.print(
|
|
153
|
+
Panel(
|
|
154
|
+
f"[bold {score_color}]{emoji} {score}/100[/bold {score_color}] [dim]Grade: {grade}[/dim]",
|
|
155
|
+
title="Security Posture",
|
|
156
|
+
border_style=score_color,
|
|
157
|
+
padding=(1, 4),
|
|
158
|
+
)
|
|
159
|
+
)
|
|
149
160
|
|
|
150
161
|
# Breakdown table
|
|
151
|
-
|
|
152
|
-
|
|
162
|
+
finding_metrics = posture.get("finding_metrics", {})
|
|
163
|
+
coverage_metrics = posture.get("coverage_metrics", {})
|
|
164
|
+
resilience_metrics = posture.get("resilience_metrics", {})
|
|
165
|
+
|
|
166
|
+
if finding_metrics or coverage_metrics or resilience_metrics:
|
|
153
167
|
console.print("\n[bold]Score Breakdown:[/bold]\n")
|
|
154
168
|
|
|
155
169
|
table = Table(show_header=True, header_style="bold")
|
|
@@ -159,15 +173,19 @@ def _display_posture(posture: dict):
|
|
|
159
173
|
table.add_column("Bar", width=30)
|
|
160
174
|
|
|
161
175
|
components = [
|
|
162
|
-
("Findings",
|
|
163
|
-
("Confidence",
|
|
164
|
-
("Coverage",
|
|
165
|
-
("
|
|
176
|
+
("Findings", finding_metrics.get("score", 0), "40%"),
|
|
177
|
+
("Confidence", finding_metrics.get("avg_confidence", 0), "25%"),
|
|
178
|
+
("Coverage", coverage_metrics.get("score", 0), "20%"),
|
|
179
|
+
("Resilience", resilience_metrics.get("score", 0), "15%"),
|
|
166
180
|
]
|
|
167
181
|
|
|
168
182
|
for name, comp_score, weight in components:
|
|
169
183
|
bar = _score_bar(comp_score)
|
|
170
|
-
color =
|
|
184
|
+
color = (
|
|
185
|
+
"green"
|
|
186
|
+
if comp_score >= 80
|
|
187
|
+
else ("yellow" if comp_score >= 60 else "red")
|
|
188
|
+
)
|
|
171
189
|
table.add_row(
|
|
172
190
|
name,
|
|
173
191
|
f"[{color}]{comp_score:.0f}[/{color}]",
|
|
@@ -309,14 +327,11 @@ def _calculate_fallback_posture(client: HumanboundClient, project_id: str):
|
|
|
309
327
|
score = min(100, pass_rate)
|
|
310
328
|
|
|
311
329
|
posture = {
|
|
312
|
-
"
|
|
330
|
+
"overall_score": score,
|
|
313
331
|
"grade": _score_to_grade(score),
|
|
314
|
-
"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
"coverage_score": 70, # Placeholder
|
|
318
|
-
"drift_score": 85, # Placeholder
|
|
319
|
-
},
|
|
332
|
+
"finding_metrics": {"score": pass_rate},
|
|
333
|
+
"coverage_metrics": {"score": 70},
|
|
334
|
+
"resilience_metrics": {"score": 85},
|
|
320
335
|
"recommendations": [],
|
|
321
336
|
"last_tested": latest.get("created_at", "")[:10],
|
|
322
337
|
}
|
|
@@ -353,12 +368,14 @@ def _display_org_posture(response: dict):
|
|
|
353
368
|
emoji = "✗"
|
|
354
369
|
|
|
355
370
|
# Main score panel
|
|
356
|
-
console.print(
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
371
|
+
console.print(
|
|
372
|
+
Panel(
|
|
373
|
+
f"[bold {score_color}]{emoji} {score}/100[/bold {score_color}] [dim]Grade: {grade}[/dim]",
|
|
374
|
+
title="Organisation Posture",
|
|
375
|
+
border_style=score_color,
|
|
376
|
+
padding=(1, 4),
|
|
377
|
+
)
|
|
378
|
+
)
|
|
362
379
|
|
|
363
380
|
# Dimension breakdown
|
|
364
381
|
dimensions = response.get("dimensions", {})
|
|
@@ -378,9 +395,13 @@ def _display_org_posture(response: dict):
|
|
|
378
395
|
|
|
379
396
|
for key, label in dimension_labels.items():
|
|
380
397
|
dim_data = dimensions.get(key, {})
|
|
381
|
-
dim_score =
|
|
398
|
+
dim_score = (
|
|
399
|
+
dim_data.get("score", 0) if isinstance(dim_data, dict) else dim_data
|
|
400
|
+
)
|
|
382
401
|
bar = _score_bar(dim_score)
|
|
383
|
-
color =
|
|
402
|
+
color = (
|
|
403
|
+
"green" if dim_score >= 80 else ("yellow" if dim_score >= 60 else "red")
|
|
404
|
+
)
|
|
384
405
|
table.add_row(
|
|
385
406
|
label,
|
|
386
407
|
f"[{color}]{dim_score:.0f}[/{color}]",
|
|
@@ -410,7 +431,9 @@ def _display_coverage_section(response: dict):
|
|
|
410
431
|
empty = bar_width - filled
|
|
411
432
|
bar = f"[{cov_color}]{'█' * filled}[/{cov_color}][dim]{'░' * empty}[/dim]"
|
|
412
433
|
|
|
413
|
-
console.print(
|
|
434
|
+
console.print(
|
|
435
|
+
f"\n[bold]Test Coverage:[/bold] {bar} [{cov_color}]{overall:.0f}%[/{cov_color}]"
|
|
436
|
+
)
|
|
414
437
|
|
|
415
438
|
# Category breakdown
|
|
416
439
|
categories = response.get("categories", response.get("by_category", []))
|
|
@@ -427,7 +450,9 @@ def _display_coverage_section(response: dict):
|
|
|
427
450
|
|
|
428
451
|
if total > 0:
|
|
429
452
|
rate = (passed / total) * 100
|
|
430
|
-
rate_color =
|
|
453
|
+
rate_color = (
|
|
454
|
+
"green" if rate >= 80 else ("yellow" if rate >= 50 else "red")
|
|
455
|
+
)
|
|
431
456
|
rate_str = f"[{rate_color}]{rate:.0f}%[/{rate_color}]"
|
|
432
457
|
else:
|
|
433
458
|
rate_str = "[dim]-[/dim]"
|
|
@@ -441,7 +466,11 @@ def _display_coverage_section(response: dict):
|
|
|
441
466
|
if gap_list:
|
|
442
467
|
console.print(f"\n[yellow]Gaps ({len(gap_list)} untested):[/yellow]")
|
|
443
468
|
for gap in gap_list[:5]:
|
|
444
|
-
name =
|
|
469
|
+
name = (
|
|
470
|
+
gap.get("category", gap.get("name", str(gap)))
|
|
471
|
+
if isinstance(gap, dict)
|
|
472
|
+
else str(gap)
|
|
473
|
+
)
|
|
445
474
|
console.print(f" - {name}")
|
|
446
475
|
if len(gap_list) > 5:
|
|
447
476
|
console.print(f" [dim]... and {len(gap_list) - 5} more[/dim]")
|
|
@@ -457,7 +486,9 @@ def _print_next(org: bool = False, has_coverage: bool = False):
|
|
|
457
486
|
suggestions.append("hb report --org Generate org posture report")
|
|
458
487
|
else:
|
|
459
488
|
if not has_coverage:
|
|
460
|
-
suggestions.append(
|
|
489
|
+
suggestions.append(
|
|
490
|
+
"hb posture --coverage Include test coverage breakdown"
|
|
491
|
+
)
|
|
461
492
|
suggestions.append("hb posture --trends View posture over time")
|
|
462
493
|
suggestions.append("hb posture --org View org-level posture")
|
|
463
494
|
suggestions.append("hb test Run tests to improve posture")
|
|
@@ -104,8 +104,8 @@ def _print_next(suggestions: list):
|
|
|
104
104
|
)
|
|
105
105
|
@click.option(
|
|
106
106
|
"--endpoint", "-e",
|
|
107
|
-
help="
|
|
108
|
-
"Same shape as 'hb
|
|
107
|
+
help="Agent integration config — JSON string or path to JSON file. "
|
|
108
|
+
"Same shape as 'hb connect --endpoint'. Overrides the project's default integration."
|
|
109
109
|
)
|
|
110
110
|
@click.option(
|
|
111
111
|
"--category",
|
|
@@ -48,6 +48,8 @@ mcp = FastMCP(
|
|
|
48
48
|
"the user wants a quick one-off scan or has no connector set up.\n\n"
|
|
49
49
|
|
|
50
50
|
"CORE WORKFLOWS:\n"
|
|
51
|
+
" One-shot agent onboarding (fastest):\n"
|
|
52
|
+
" hb_connect — scan + create project + auto-test in a single call\n"
|
|
51
53
|
" Discovery → Testing:\n"
|
|
52
54
|
" hb_trigger_discovery → hb_list_inventory → hb_onboard_inventory_asset → hb_run_test\n"
|
|
53
55
|
" Security Testing:\n"
|
|
@@ -62,7 +64,7 @@ mcp = FastMCP(
|
|
|
62
64
|
|
|
63
65
|
"CLI-ONLY COMMANDS (suggest when relevant):\n"
|
|
64
66
|
" • 'hb discover' — browser-based shadow AI discovery (no connector needed)\n"
|
|
65
|
-
" • 'hb
|
|
67
|
+
" • 'hb connect --vendor microsoft' — browser-based platform discovery\n"
|
|
66
68
|
" • 'hb sentinel setup' — configure continuous monitoring sentinel"
|
|
67
69
|
),
|
|
68
70
|
)
|
|
@@ -1303,6 +1305,166 @@ def hb_break_campaign(campaign_id: str, project_id: Optional[str] = None) -> str
|
|
|
1303
1305
|
return _err(e)
|
|
1304
1306
|
|
|
1305
1307
|
|
|
1308
|
+
# =========================================================================
|
|
1309
|
+
# CONNECT — one-shot agent onboarding (scan → project → test)
|
|
1310
|
+
# =========================================================================
|
|
1311
|
+
|
|
1312
|
+
@mcp.tool()
|
|
1313
|
+
def hb_connect(
|
|
1314
|
+
endpoint_config: str,
|
|
1315
|
+
name: Optional[str] = None,
|
|
1316
|
+
prompt_text: Optional[str] = None,
|
|
1317
|
+
context: Optional[str] = None,
|
|
1318
|
+
timeout: int = 180,
|
|
1319
|
+
) -> str:
|
|
1320
|
+
"""Connect an AI agent: probe it, create a project, and run the first security test — all in one call.
|
|
1321
|
+
|
|
1322
|
+
This is the fastest way to onboard an agent. It replicates the CLI
|
|
1323
|
+
command ``hb connect --endpoint <config> --yes``.
|
|
1324
|
+
|
|
1325
|
+
Flow:
|
|
1326
|
+
1. Build extraction sources from the endpoint config (and optional prompt).
|
|
1327
|
+
2. POST /scan — the backend probes the agent and extracts its scope.
|
|
1328
|
+
3. Create a project with the extracted scope.
|
|
1329
|
+
4. Auto-start the first security test (owasp_agentic, unit level).
|
|
1330
|
+
|
|
1331
|
+
After this tool returns, poll hb_get_experiment_status with the
|
|
1332
|
+
returned experiment_id until status is "Finished", then call
|
|
1333
|
+
hb_get_experiment_logs and hb_get_posture for results.
|
|
1334
|
+
|
|
1335
|
+
Args:
|
|
1336
|
+
endpoint_config: JSON string with the agent's chat-completion config.
|
|
1337
|
+
Example: {"streaming": false, "chat_completion": {"endpoint": "https://...",
|
|
1338
|
+
"headers": {"Authorization": "Bearer ..."}, "payload": {"content": "$PROMPT"}}}
|
|
1339
|
+
name: Project name (auto-derived from endpoint hostname if omitted).
|
|
1340
|
+
prompt_text: Optional system prompt text to include as an additional
|
|
1341
|
+
extraction source (improves scope quality).
|
|
1342
|
+
context: Extra context for the judge, e.g. "Authenticated as Alice,
|
|
1343
|
+
her PII is expected" (max 1500 chars).
|
|
1344
|
+
timeout: Request timeout in seconds for the scan call (default 180).
|
|
1345
|
+
"""
|
|
1346
|
+
try:
|
|
1347
|
+
client = _get_client()
|
|
1348
|
+
|
|
1349
|
+
if not client.is_authenticated():
|
|
1350
|
+
return _err(ValueError("Not authenticated. The user must run 'hb login' first."))
|
|
1351
|
+
if not client.organisation_id:
|
|
1352
|
+
return _err(ValueError("No organisation selected. Use hb_set_organisation first."))
|
|
1353
|
+
|
|
1354
|
+
# -- Parse endpoint config ----------------------------------------
|
|
1355
|
+
try:
|
|
1356
|
+
bot_config = json.loads(endpoint_config)
|
|
1357
|
+
except json.JSONDecodeError as e:
|
|
1358
|
+
return _err(ValueError(f"Invalid JSON in endpoint_config: {e}"))
|
|
1359
|
+
|
|
1360
|
+
# -- Build sources array ------------------------------------------
|
|
1361
|
+
sources = [{"source": "endpoint", "data": bot_config}]
|
|
1362
|
+
|
|
1363
|
+
if prompt_text:
|
|
1364
|
+
sources.append({"source": "text", "data": {"text": prompt_text}})
|
|
1365
|
+
|
|
1366
|
+
# -- Derive project name ------------------------------------------
|
|
1367
|
+
if not name:
|
|
1368
|
+
try:
|
|
1369
|
+
ep_url = bot_config.get("chat_completion", {}).get("endpoint", "")
|
|
1370
|
+
if ep_url:
|
|
1371
|
+
from urllib.parse import urlparse
|
|
1372
|
+
hostname = urlparse(ep_url).hostname
|
|
1373
|
+
if hostname:
|
|
1374
|
+
name = hostname
|
|
1375
|
+
except Exception:
|
|
1376
|
+
pass
|
|
1377
|
+
if not name:
|
|
1378
|
+
name = "My Agent"
|
|
1379
|
+
|
|
1380
|
+
# -- POST /scan ---------------------------------------------------
|
|
1381
|
+
scan_response = client.post(
|
|
1382
|
+
"scan",
|
|
1383
|
+
data={"sources": sources},
|
|
1384
|
+
include_project=False,
|
|
1385
|
+
timeout=timeout,
|
|
1386
|
+
)
|
|
1387
|
+
|
|
1388
|
+
scope = scan_response.get("scope", {})
|
|
1389
|
+
risk_profile = scan_response.get("risk_profile", {})
|
|
1390
|
+
default_integration = scan_response.get("default_integration")
|
|
1391
|
+
|
|
1392
|
+
# -- Create project -----------------------------------------------
|
|
1393
|
+
project_data = {
|
|
1394
|
+
"name": name,
|
|
1395
|
+
"description": f"Project created via MCP hb_connect",
|
|
1396
|
+
"scope": scope,
|
|
1397
|
+
}
|
|
1398
|
+
if default_integration:
|
|
1399
|
+
project_data["default_integration"] = default_integration
|
|
1400
|
+
|
|
1401
|
+
project_result = client.post("projects", data=project_data)
|
|
1402
|
+
project_id = project_result.get("id")
|
|
1403
|
+
|
|
1404
|
+
# Auto-select the project
|
|
1405
|
+
client.set_project(project_id)
|
|
1406
|
+
|
|
1407
|
+
# -- Auto-test ----------------------------------------------------
|
|
1408
|
+
experiment_id = None
|
|
1409
|
+
auto_test_error = None
|
|
1410
|
+
|
|
1411
|
+
if default_integration:
|
|
1412
|
+
providers = client.list_providers()
|
|
1413
|
+
if providers:
|
|
1414
|
+
provider = next((p for p in providers if p.get("is_default")), providers[0])
|
|
1415
|
+
|
|
1416
|
+
configuration = {}
|
|
1417
|
+
if context:
|
|
1418
|
+
if len(context) > 1500:
|
|
1419
|
+
return _err(ValueError(f"Context too long ({len(context)} chars). Maximum is 1,500."))
|
|
1420
|
+
configuration["context"] = context
|
|
1421
|
+
|
|
1422
|
+
import time as _time
|
|
1423
|
+
experiment_data = {
|
|
1424
|
+
"name": f"connect-{_time.strftime('%Y%m%d-%H%M%S')}",
|
|
1425
|
+
"description": "Initial assessment from hb_connect (MCP)",
|
|
1426
|
+
"test_category": "humanbound/adversarial/owasp_agentic",
|
|
1427
|
+
"testing_level": "unit",
|
|
1428
|
+
"provider_id": provider.get("id"),
|
|
1429
|
+
"auto_start": True,
|
|
1430
|
+
"configuration": configuration,
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
try:
|
|
1434
|
+
exp_result = client.post("experiments", data=experiment_data, include_project=True)
|
|
1435
|
+
experiment_id = exp_result.get("id")
|
|
1436
|
+
except Exception as e:
|
|
1437
|
+
auto_test_error = str(e)
|
|
1438
|
+
else:
|
|
1439
|
+
auto_test_error = "No providers configured. Add one with hb_add_provider, then run hb_run_test."
|
|
1440
|
+
else:
|
|
1441
|
+
auto_test_error = "No agent integration detected from scan — skipped auto-test. Run hb_run_test manually."
|
|
1442
|
+
|
|
1443
|
+
# -- Build response -----------------------------------------------
|
|
1444
|
+
result = {
|
|
1445
|
+
"project_id": project_id,
|
|
1446
|
+
"project_name": name,
|
|
1447
|
+
"scope": scope,
|
|
1448
|
+
"risk_profile": risk_profile,
|
|
1449
|
+
"has_integration": bool(default_integration),
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1452
|
+
if experiment_id:
|
|
1453
|
+
result["experiment_id"] = experiment_id
|
|
1454
|
+
result["next_step"] = (
|
|
1455
|
+
f"Poll hb_get_experiment_status(experiment_id='{experiment_id}') "
|
|
1456
|
+
"until status is 'Finished', then call hb_get_experiment_logs "
|
|
1457
|
+
"and hb_get_posture for results."
|
|
1458
|
+
)
|
|
1459
|
+
elif auto_test_error:
|
|
1460
|
+
result["auto_test_skipped"] = auto_test_error
|
|
1461
|
+
|
|
1462
|
+
return _ok(result)
|
|
1463
|
+
|
|
1464
|
+
except HumanboundError as e:
|
|
1465
|
+
return _err(e)
|
|
1466
|
+
|
|
1467
|
+
|
|
1306
1468
|
# =========================================================================
|
|
1307
1469
|
# UPLOAD
|
|
1308
1470
|
# =========================================================================
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: humanbound-cli
|
|
3
|
-
Version: 0.6.
|
|
3
|
+
Version: 0.6.2
|
|
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
|
|
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
|