humanbound-cli 0.3.5__tar.gz → 0.4.0__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.5 → humanbound_cli-0.4.0}/PKG-INFO +75 -2
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/README.md +72 -1
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/client.py +149 -1
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/__init__.py +6 -1
- humanbound_cli-0.4.0/humanbound_cli/commands/completion.py +69 -0
- humanbound_cli-0.4.0/humanbound_cli/commands/connectors.py +328 -0
- humanbound_cli-0.4.0/humanbound_cli/commands/discover.py +1840 -0
- humanbound_cli-0.4.0/humanbound_cli/commands/inventory.py +1215 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/sentinel.py +2 -2
- humanbound_cli-0.4.0/humanbound_cli/connectors/__init__.py +1 -0
- humanbound_cli-0.4.0/humanbound_cli/connectors/microsoft.py +1613 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/main.py +8 -0
- humanbound_cli-0.4.0/humanbound_cli/report_builder.py +765 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/PKG-INFO +75 -2
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/SOURCES.txt +7 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/requires.txt +2 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/top_level.txt +1 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/pyproject.toml +3 -1
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/LICENSE +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/__init__.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/api_keys.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/auth.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/campaigns.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/coverage.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/docs.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/experiments.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/findings.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/guardrails.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/init.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/logs.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/members.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/orgs.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/posture.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/projects.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/providers.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/scan.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/test.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/upload_logs.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/config.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/exceptions.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/extractors/__init__.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/extractors/openapi.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/extractors/repo.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/pytest_plugin/__init__.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/pytest_plugin/report.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/report.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/__init__.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/config_builder.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/local_server.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/runtime_detector.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/tunnel_client.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/dependency_links.txt +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/entry_points.txt +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/relay/relay.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/setup.cfg +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/tests/__init__.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/tests/cli_integration_test.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/tests/conftest.py +0 -0
- {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/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
|
+
Version: 0.4.0
|
|
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
|
|
@@ -19,6 +19,8 @@ Requires-Dist: click>=8.1.0
|
|
|
19
19
|
Requires-Dist: rich>=13.0.0
|
|
20
20
|
Requires-Dist: requests>=2.32.0
|
|
21
21
|
Requires-Dist: pyyaml>=6.0.0
|
|
22
|
+
Requires-Dist: msal>=1.31.0
|
|
23
|
+
Requires-Dist: pyperclip>=1.8.0
|
|
22
24
|
Provides-Extra: serve
|
|
23
25
|
Requires-Dist: websockets>=12.0; extra == "serve"
|
|
24
26
|
Provides-Extra: pytest
|
|
@@ -33,7 +35,7 @@ Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
|
33
35
|
|
|
34
36
|
[](https://pypi.org/project/humanbound-cli/)
|
|
35
37
|
[]()
|
|
36
|
-
[]()
|
|
37
39
|
|
|
38
40
|
```
|
|
39
41
|
pip install humanbound-cli
|
|
@@ -54,6 +56,7 @@ Humanbound runs automated adversarial attacks against your bot's live endpoint,
|
|
|
54
56
|
| **Adversarial Testing** | OWASP-aligned attack scenarios: single-turn, multi-turn, adaptive, and agentic. |
|
|
55
57
|
| **Behavioral Testing** | Validate intent boundaries, response quality, and functional correctness. |
|
|
56
58
|
| **Posture Scoring** | Quantified 0-100 security score with breakdown by findings, coverage, and resilience. Track over time. |
|
|
59
|
+
| **Shadow AI Discovery** | Scan cloud tenants for AI services, assess risk with 15 SAI threat classes, and govern your AI inventory. |
|
|
57
60
|
| **Guardrails Export** | Generate protection rules from test findings. Export to OpenAI, Azure AI Content Safety, AWS Bedrock, or Humanbound format. |
|
|
58
61
|
|
|
59
62
|
### Why Humanbound?
|
|
@@ -394,6 +397,58 @@ Continuous security assurance with automated campaign management (ASCAM).
|
|
|
394
397
|
|
|
395
398
|
ASCAM phases: Reconnaissance → Hardening → Red Teaming → Analysis → Monitoring
|
|
396
399
|
|
|
400
|
+
### Shadow AI Discovery
|
|
401
|
+
|
|
402
|
+
Discover, assess, and govern AI services across your cloud environment.
|
|
403
|
+
|
|
404
|
+
| Command | Description |
|
|
405
|
+
|---------|-------------|
|
|
406
|
+
| `discover` | Scan cloud tenant for AI services |
|
|
407
|
+
|
|
408
|
+
Options: `--save` (persist to inventory), `--report` (HTML report), `--json` (JSON output), `--verbose` (raw API responses)
|
|
409
|
+
|
|
410
|
+
### Cloud Connectors
|
|
411
|
+
|
|
412
|
+
Register cloud connectors for persistent, repeatable discovery.
|
|
413
|
+
|
|
414
|
+
| Command | Description |
|
|
415
|
+
|---------|-------------|
|
|
416
|
+
| `connectors` | List registered connectors |
|
|
417
|
+
| `connectors add` | Register a new cloud connector |
|
|
418
|
+
| `connectors test <id>` | Test connector connectivity |
|
|
419
|
+
| `connectors update <id>` | Update connector credentials |
|
|
420
|
+
| `connectors remove <id>` | Remove connector |
|
|
421
|
+
|
|
422
|
+
<details>
|
|
423
|
+
<summary><code>connectors add</code> options</summary>
|
|
424
|
+
|
|
425
|
+
```
|
|
426
|
+
--vendor Cloud vendor (default: microsoft)
|
|
427
|
+
--tenant-id Cloud tenant ID (required)
|
|
428
|
+
--client-id App registration client ID (required)
|
|
429
|
+
--client-secret App registration client secret (prompted)
|
|
430
|
+
--name Display name for the connector
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
</details>
|
|
434
|
+
|
|
435
|
+
### AI Inventory
|
|
436
|
+
|
|
437
|
+
View and govern discovered AI assets.
|
|
438
|
+
|
|
439
|
+
| Command | Description |
|
|
440
|
+
|---------|-------------|
|
|
441
|
+
| `inventory` | List all inventory assets |
|
|
442
|
+
| `inventory view <id>` | View asset details |
|
|
443
|
+
| `inventory update <id>` | Update governance fields |
|
|
444
|
+
| `inventory posture` | View shadow AI posture score |
|
|
445
|
+
| `inventory onboard <id>` | Create security testing project from asset |
|
|
446
|
+
| `inventory archive <id>` | Archive an asset |
|
|
447
|
+
|
|
448
|
+
Options for `inventory`: `--category`, `--risk-level`, `--json`
|
|
449
|
+
|
|
450
|
+
Options for `inventory update`: `--sanctioned / --unsanctioned`, `--owner`, `--department`, `--business-purpose`, `--has-policy / --no-policy`, `--has-risk-assessment / --no-risk-assessment`
|
|
451
|
+
|
|
397
452
|
### Upload Conversation Logs
|
|
398
453
|
|
|
399
454
|
Evaluate real production conversations against security judges.
|
|
@@ -518,6 +573,24 @@ hb init -n "My Bot" -e ./bot-config.json
|
|
|
518
573
|
hb test -e ./bot-config.json
|
|
519
574
|
```
|
|
520
575
|
|
|
576
|
+
### Shadow AI discovery & governance
|
|
577
|
+
|
|
578
|
+
```bash
|
|
579
|
+
# Register a cloud connector
|
|
580
|
+
hb connectors add --tenant-id abc --client-id def --client-secret
|
|
581
|
+
|
|
582
|
+
# Scan, save to inventory, and export report
|
|
583
|
+
hb discover --save --report
|
|
584
|
+
|
|
585
|
+
# Review and govern assets
|
|
586
|
+
hb inventory
|
|
587
|
+
hb inventory update <id> --sanctioned --owner "security@company.com"
|
|
588
|
+
|
|
589
|
+
# Onboard high-risk asset for security testing
|
|
590
|
+
hb inventory onboard <id>
|
|
591
|
+
hb test
|
|
592
|
+
```
|
|
593
|
+
|
|
521
594
|
### Export guardrails
|
|
522
595
|
|
|
523
596
|
```bash
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
[](https://pypi.org/project/humanbound-cli/)
|
|
6
6
|
[]()
|
|
7
|
-
[]()
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
pip install humanbound-cli
|
|
@@ -25,6 +25,7 @@ Humanbound runs automated adversarial attacks against your bot's live endpoint,
|
|
|
25
25
|
| **Adversarial Testing** | OWASP-aligned attack scenarios: single-turn, multi-turn, adaptive, and agentic. |
|
|
26
26
|
| **Behavioral Testing** | Validate intent boundaries, response quality, and functional correctness. |
|
|
27
27
|
| **Posture Scoring** | Quantified 0-100 security score with breakdown by findings, coverage, and resilience. Track over time. |
|
|
28
|
+
| **Shadow AI Discovery** | Scan cloud tenants for AI services, assess risk with 15 SAI threat classes, and govern your AI inventory. |
|
|
28
29
|
| **Guardrails Export** | Generate protection rules from test findings. Export to OpenAI, Azure AI Content Safety, AWS Bedrock, or Humanbound format. |
|
|
29
30
|
|
|
30
31
|
### Why Humanbound?
|
|
@@ -365,6 +366,58 @@ Continuous security assurance with automated campaign management (ASCAM).
|
|
|
365
366
|
|
|
366
367
|
ASCAM phases: Reconnaissance → Hardening → Red Teaming → Analysis → Monitoring
|
|
367
368
|
|
|
369
|
+
### Shadow AI Discovery
|
|
370
|
+
|
|
371
|
+
Discover, assess, and govern AI services across your cloud environment.
|
|
372
|
+
|
|
373
|
+
| Command | Description |
|
|
374
|
+
|---------|-------------|
|
|
375
|
+
| `discover` | Scan cloud tenant for AI services |
|
|
376
|
+
|
|
377
|
+
Options: `--save` (persist to inventory), `--report` (HTML report), `--json` (JSON output), `--verbose` (raw API responses)
|
|
378
|
+
|
|
379
|
+
### Cloud Connectors
|
|
380
|
+
|
|
381
|
+
Register cloud connectors for persistent, repeatable discovery.
|
|
382
|
+
|
|
383
|
+
| Command | Description |
|
|
384
|
+
|---------|-------------|
|
|
385
|
+
| `connectors` | List registered connectors |
|
|
386
|
+
| `connectors add` | Register a new cloud connector |
|
|
387
|
+
| `connectors test <id>` | Test connector connectivity |
|
|
388
|
+
| `connectors update <id>` | Update connector credentials |
|
|
389
|
+
| `connectors remove <id>` | Remove connector |
|
|
390
|
+
|
|
391
|
+
<details>
|
|
392
|
+
<summary><code>connectors add</code> options</summary>
|
|
393
|
+
|
|
394
|
+
```
|
|
395
|
+
--vendor Cloud vendor (default: microsoft)
|
|
396
|
+
--tenant-id Cloud tenant ID (required)
|
|
397
|
+
--client-id App registration client ID (required)
|
|
398
|
+
--client-secret App registration client secret (prompted)
|
|
399
|
+
--name Display name for the connector
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
</details>
|
|
403
|
+
|
|
404
|
+
### AI Inventory
|
|
405
|
+
|
|
406
|
+
View and govern discovered AI assets.
|
|
407
|
+
|
|
408
|
+
| Command | Description |
|
|
409
|
+
|---------|-------------|
|
|
410
|
+
| `inventory` | List all inventory assets |
|
|
411
|
+
| `inventory view <id>` | View asset details |
|
|
412
|
+
| `inventory update <id>` | Update governance fields |
|
|
413
|
+
| `inventory posture` | View shadow AI posture score |
|
|
414
|
+
| `inventory onboard <id>` | Create security testing project from asset |
|
|
415
|
+
| `inventory archive <id>` | Archive an asset |
|
|
416
|
+
|
|
417
|
+
Options for `inventory`: `--category`, `--risk-level`, `--json`
|
|
418
|
+
|
|
419
|
+
Options for `inventory update`: `--sanctioned / --unsanctioned`, `--owner`, `--department`, `--business-purpose`, `--has-policy / --no-policy`, `--has-risk-assessment / --no-risk-assessment`
|
|
420
|
+
|
|
368
421
|
### Upload Conversation Logs
|
|
369
422
|
|
|
370
423
|
Evaluate real production conversations against security judges.
|
|
@@ -489,6 +542,24 @@ hb init -n "My Bot" -e ./bot-config.json
|
|
|
489
542
|
hb test -e ./bot-config.json
|
|
490
543
|
```
|
|
491
544
|
|
|
545
|
+
### Shadow AI discovery & governance
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
# Register a cloud connector
|
|
549
|
+
hb connectors add --tenant-id abc --client-id def --client-secret
|
|
550
|
+
|
|
551
|
+
# Scan, save to inventory, and export report
|
|
552
|
+
hb discover --save --report
|
|
553
|
+
|
|
554
|
+
# Review and govern assets
|
|
555
|
+
hb inventory
|
|
556
|
+
hb inventory update <id> --sanctioned --owner "security@company.com"
|
|
557
|
+
|
|
558
|
+
# Onboard high-risk asset for security testing
|
|
559
|
+
hb inventory onboard <id>
|
|
560
|
+
hb test
|
|
561
|
+
```
|
|
562
|
+
|
|
492
563
|
### Export guardrails
|
|
493
564
|
|
|
494
565
|
```bash
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Humanbound API client with OAuth authentication."""
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
+
import os
|
|
4
5
|
import time
|
|
5
6
|
import webbrowser
|
|
6
7
|
import http.server
|
|
@@ -16,6 +17,7 @@ from pathlib import Path
|
|
|
16
17
|
import requests
|
|
17
18
|
|
|
18
19
|
from .config import (
|
|
20
|
+
DEFAULT_BASE_URL,
|
|
19
21
|
get_base_url,
|
|
20
22
|
get_auth0_domain,
|
|
21
23
|
get_auth0_client_id,
|
|
@@ -547,7 +549,8 @@ class HumanboundClient:
|
|
|
547
549
|
self._default_organisation_id = credentials.get("default_organisation_id")
|
|
548
550
|
# Restore saved base_url unless explicitly overridden via --base-url or env var
|
|
549
551
|
saved_url = credentials.get("base_url")
|
|
550
|
-
|
|
552
|
+
env_override = os.environ.get("HUMANBOUND_BASE_URL")
|
|
553
|
+
if saved_url and not env_override and self.base_url == DEFAULT_BASE_URL.rstrip("/"):
|
|
551
554
|
self.base_url = saved_url.rstrip("/")
|
|
552
555
|
|
|
553
556
|
def _load_credentials_file(self) -> dict:
|
|
@@ -1081,6 +1084,7 @@ class HumanboundClient:
|
|
|
1081
1084
|
data["lang"] = lang
|
|
1082
1085
|
return self.post(f"projects/{project_id}/datasets/conversations", data=data, include_project=True)
|
|
1083
1086
|
|
|
1087
|
+
|
|
1084
1088
|
# -------------------------------------------------------------------------
|
|
1085
1089
|
# Subscription Methods
|
|
1086
1090
|
# -------------------------------------------------------------------------
|
|
@@ -1145,6 +1149,150 @@ class HumanboundClient:
|
|
|
1145
1149
|
data["event_type"] = event_type
|
|
1146
1150
|
return self.post(f"organisations/{org_id}/webhooks/{webhook_id}/replay", data=data, include_org=False)
|
|
1147
1151
|
|
|
1152
|
+
# -------------------------------------------------------------------------
|
|
1153
|
+
# Connector & Inventory Methods (Shadow AI Discovery)
|
|
1154
|
+
# -------------------------------------------------------------------------
|
|
1155
|
+
|
|
1156
|
+
def create_connector(self, vendor: str, tenant_id: str, client_id: str, client_secret: str,
|
|
1157
|
+
display_name: Optional[str] = None, scopes: Optional[List[str]] = None) -> dict:
|
|
1158
|
+
"""Register a new cloud connector."""
|
|
1159
|
+
if not self._organisation_id:
|
|
1160
|
+
raise ValidationError("No organisation selected.")
|
|
1161
|
+
data = {
|
|
1162
|
+
"vendor": vendor,
|
|
1163
|
+
"credentials": {
|
|
1164
|
+
"tenant_id": tenant_id,
|
|
1165
|
+
"client_id": client_id,
|
|
1166
|
+
"client_secret": client_secret,
|
|
1167
|
+
},
|
|
1168
|
+
}
|
|
1169
|
+
if display_name:
|
|
1170
|
+
data["display_name"] = display_name
|
|
1171
|
+
if scopes:
|
|
1172
|
+
data["scopes"] = scopes
|
|
1173
|
+
return self.post("connectors", data=data)
|
|
1174
|
+
|
|
1175
|
+
def list_connectors(self) -> list:
|
|
1176
|
+
"""List all connectors for the current organisation."""
|
|
1177
|
+
if not self._organisation_id:
|
|
1178
|
+
raise ValidationError("No organisation selected.")
|
|
1179
|
+
return self.get("connectors")
|
|
1180
|
+
|
|
1181
|
+
def get_connector(self, connector_id: str) -> dict:
|
|
1182
|
+
"""Get a single connector."""
|
|
1183
|
+
if not self._organisation_id:
|
|
1184
|
+
raise ValidationError("No organisation selected.")
|
|
1185
|
+
return self.get(f"connectors/{connector_id}")
|
|
1186
|
+
|
|
1187
|
+
def update_connector(self, connector_id: str, data: dict) -> dict:
|
|
1188
|
+
"""Update a connector."""
|
|
1189
|
+
if not self._organisation_id:
|
|
1190
|
+
raise ValidationError("No organisation selected.")
|
|
1191
|
+
return self.put(f"connectors/{connector_id}", data=data)
|
|
1192
|
+
|
|
1193
|
+
def delete_connector(self, connector_id: str) -> None:
|
|
1194
|
+
"""Delete a connector."""
|
|
1195
|
+
if not self._organisation_id:
|
|
1196
|
+
raise ValidationError("No organisation selected.")
|
|
1197
|
+
self.delete(f"connectors/{connector_id}")
|
|
1198
|
+
|
|
1199
|
+
def test_connector(self, connector_id: str) -> dict:
|
|
1200
|
+
"""Test connection validity for a connector."""
|
|
1201
|
+
if not self._organisation_id:
|
|
1202
|
+
raise ValidationError("No organisation selected.")
|
|
1203
|
+
return self.post(f"connectors/{connector_id}/test")
|
|
1204
|
+
|
|
1205
|
+
def trigger_discovery(self, connector_id: str) -> dict:
|
|
1206
|
+
"""Trigger a discovery scan for a connector."""
|
|
1207
|
+
if not self._organisation_id:
|
|
1208
|
+
raise ValidationError("No organisation selected.")
|
|
1209
|
+
return self.post(
|
|
1210
|
+
"discover",
|
|
1211
|
+
data={"connector_id": connector_id},
|
|
1212
|
+
timeout=LONG_TIMEOUT,
|
|
1213
|
+
)
|
|
1214
|
+
|
|
1215
|
+
def list_inventory(self, category: Optional[str] = None, vendor: Optional[str] = None,
|
|
1216
|
+
risk_level: Optional[str] = None, is_sanctioned: Optional[bool] = None,
|
|
1217
|
+
page: int = 1, size: int = 50) -> dict:
|
|
1218
|
+
"""List discovered inventory assets."""
|
|
1219
|
+
if not self._organisation_id:
|
|
1220
|
+
raise ValidationError("No organisation selected.")
|
|
1221
|
+
params: Dict[str, Any] = {"page": page, "size": size}
|
|
1222
|
+
if category:
|
|
1223
|
+
params["category"] = category
|
|
1224
|
+
if vendor:
|
|
1225
|
+
params["vendor"] = vendor
|
|
1226
|
+
if risk_level:
|
|
1227
|
+
params["risk_level"] = risk_level
|
|
1228
|
+
if is_sanctioned is not None:
|
|
1229
|
+
params["is_sanctioned"] = str(is_sanctioned).lower()
|
|
1230
|
+
return self.get("inventory", params=params)
|
|
1231
|
+
|
|
1232
|
+
def get_inventory_asset(self, asset_id: str) -> dict:
|
|
1233
|
+
"""Get a single inventory asset."""
|
|
1234
|
+
if not self._organisation_id:
|
|
1235
|
+
raise ValidationError("No organisation selected.")
|
|
1236
|
+
return self.get(f"inventory/{asset_id}")
|
|
1237
|
+
|
|
1238
|
+
def update_inventory_asset(self, asset_id: str, data: dict) -> dict:
|
|
1239
|
+
"""Update governance fields on an inventory asset."""
|
|
1240
|
+
if not self._organisation_id:
|
|
1241
|
+
raise ValidationError("No organisation selected.")
|
|
1242
|
+
return self.put(f"inventory/{asset_id}", data=data)
|
|
1243
|
+
|
|
1244
|
+
def archive_inventory_asset(self, asset_id: str) -> dict:
|
|
1245
|
+
"""Archive an inventory asset."""
|
|
1246
|
+
if not self._organisation_id:
|
|
1247
|
+
raise ValidationError("No organisation selected.")
|
|
1248
|
+
return self.put(f"inventory/{asset_id}/archive")
|
|
1249
|
+
|
|
1250
|
+
def get_shadow_posture(self) -> dict:
|
|
1251
|
+
"""Get shadow AI posture from the org posture endpoint."""
|
|
1252
|
+
org_id = self._organisation_id
|
|
1253
|
+
if not org_id:
|
|
1254
|
+
raise ValidationError("No organisation selected.")
|
|
1255
|
+
result = self.get(f"organisations/{org_id}/posture", include_org=False)
|
|
1256
|
+
shadow = result.get("dimensions", {}).get("shadow_ai")
|
|
1257
|
+
if not shadow:
|
|
1258
|
+
return {"score": 100.0, "grade": "A", "total_assets": 0,
|
|
1259
|
+
"shadow_count": 0, "sanctioned_count": 0, "domain_scores": {}}
|
|
1260
|
+
return shadow
|
|
1261
|
+
|
|
1262
|
+
def persist_discovery(self, nonce: str) -> dict:
|
|
1263
|
+
"""Persist analysed discovery results to inventory.
|
|
1264
|
+
|
|
1265
|
+
POST /organisations/{org_id}/analyse/persist with x-nonce header.
|
|
1266
|
+
|
|
1267
|
+
Args:
|
|
1268
|
+
nonce: Single-use nonce from the /analyse response.
|
|
1269
|
+
|
|
1270
|
+
Returns:
|
|
1271
|
+
Persistence summary dict.
|
|
1272
|
+
"""
|
|
1273
|
+
org_id = self._organisation_id
|
|
1274
|
+
if not org_id:
|
|
1275
|
+
raise ValidationError("No organisation selected.")
|
|
1276
|
+
self._ensure_authenticated()
|
|
1277
|
+
headers = self._get_headers(include_org=False)
|
|
1278
|
+
headers["x-nonce"] = nonce
|
|
1279
|
+
response = requests.post(
|
|
1280
|
+
f"{self.base_url}/organisations/{org_id}/analyse/persist",
|
|
1281
|
+
headers=headers,
|
|
1282
|
+
json={},
|
|
1283
|
+
timeout=LONG_TIMEOUT,
|
|
1284
|
+
)
|
|
1285
|
+
return self._handle_response(response)
|
|
1286
|
+
|
|
1287
|
+
def onboard_inventory_asset(self, asset_id: str, project_name: Optional[str] = None) -> dict:
|
|
1288
|
+
"""Create a project from an inventory asset."""
|
|
1289
|
+
if not self._organisation_id:
|
|
1290
|
+
raise ValidationError("No organisation selected.")
|
|
1291
|
+
data = {}
|
|
1292
|
+
if project_name:
|
|
1293
|
+
data["name"] = project_name
|
|
1294
|
+
return self.post(f"inventory/{asset_id}/onboard", data=data)
|
|
1295
|
+
|
|
1148
1296
|
|
|
1149
1297
|
# Import ValidationError to this module
|
|
1150
1298
|
from .exceptions import ValidationError
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
from . import (
|
|
4
4
|
auth, orgs, projects, experiments, init, test, logs, posture,
|
|
5
5
|
guardrails, docs, providers, findings, api_keys, members,
|
|
6
|
-
coverage, campaigns, upload_logs, sentinel,
|
|
6
|
+
coverage, campaigns, upload_logs, sentinel, discover,
|
|
7
|
+
connectors, inventory, completion,
|
|
7
8
|
)
|
|
8
9
|
|
|
9
10
|
__all__ = [
|
|
@@ -25,4 +26,8 @@ __all__ = [
|
|
|
25
26
|
"campaigns",
|
|
26
27
|
"upload_logs",
|
|
27
28
|
"sentinel",
|
|
29
|
+
"discover",
|
|
30
|
+
"connectors",
|
|
31
|
+
"inventory",
|
|
32
|
+
"completion",
|
|
28
33
|
]
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Shell tab-completion setup for the hb CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
import click
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
_SHELL_ENV_VARS = {
|
|
10
|
+
"bash": "_HB_COMPLETE=bash_source",
|
|
11
|
+
"zsh": "_HB_COMPLETE=zsh_source",
|
|
12
|
+
"fish": "_HB_COMPLETE=fish_source",
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
_INSTALL_HINTS = {
|
|
16
|
+
"bash": 'eval "$(hb completion bash)" # or: hb completion bash >> ~/.bashrc',
|
|
17
|
+
"zsh": 'eval "$(hb completion zsh)" # or: hb completion zsh >> ~/.zshrc',
|
|
18
|
+
"fish": "hb completion fish > ~/.config/fish/completions/hb.fish",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _detect_shell() -> str | None:
|
|
23
|
+
"""Detect the current shell from $SHELL."""
|
|
24
|
+
shell_path = os.environ.get("SHELL", "")
|
|
25
|
+
basename = os.path.basename(shell_path)
|
|
26
|
+
if basename in _SHELL_ENV_VARS:
|
|
27
|
+
return basename
|
|
28
|
+
return None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@click.command("completion")
|
|
32
|
+
@click.argument("shell", required=False, type=click.Choice(["bash", "zsh", "fish"]))
|
|
33
|
+
def completion_command(shell: str | None):
|
|
34
|
+
"""Enable shell tab completion for hb.
|
|
35
|
+
|
|
36
|
+
Prints the completion script for your shell. Add it to your profile:
|
|
37
|
+
|
|
38
|
+
\b
|
|
39
|
+
hb completion bash >> ~/.bashrc
|
|
40
|
+
hb completion zsh >> ~/.zshrc
|
|
41
|
+
hb completion fish > ~/.config/fish/completions/hb.fish
|
|
42
|
+
"""
|
|
43
|
+
if not shell:
|
|
44
|
+
shell = _detect_shell()
|
|
45
|
+
if not shell:
|
|
46
|
+
raise click.ClickException(
|
|
47
|
+
"Could not detect shell. Specify one explicitly: hb completion bash|zsh|fish"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
env_var = _SHELL_ENV_VARS[shell]
|
|
51
|
+
key, value = env_var.split("=", 1)
|
|
52
|
+
|
|
53
|
+
env = {**os.environ, key: value}
|
|
54
|
+
result = subprocess.run(
|
|
55
|
+
["hb"],
|
|
56
|
+
env=env,
|
|
57
|
+
capture_output=True,
|
|
58
|
+
text=True,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
if result.stdout:
|
|
62
|
+
click.echo(result.stdout)
|
|
63
|
+
else:
|
|
64
|
+
raise click.ClickException(
|
|
65
|
+
f"Failed to generate {shell} completion script. "
|
|
66
|
+
"Make sure 'hb' is installed and on your PATH."
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
click.echo(f"# Add to your profile: {_INSTALL_HINTS[shell]}", err=True)
|