humanbound-cli 0.3.5__tar.gz → 0.4.1__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.
Files changed (61) hide show
  1. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/PKG-INFO +75 -2
  2. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/README.md +72 -1
  3. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/client.py +190 -3
  4. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/__init__.py +6 -1
  5. humanbound_cli-0.4.1/humanbound_cli/commands/completion.py +69 -0
  6. humanbound_cli-0.4.1/humanbound_cli/commands/connectors.py +328 -0
  7. humanbound_cli-0.4.1/humanbound_cli/commands/discover.py +1840 -0
  8. humanbound_cli-0.4.1/humanbound_cli/commands/inventory.py +1215 -0
  9. humanbound_cli-0.4.1/humanbound_cli/commands/logs.py +479 -0
  10. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/sentinel.py +2 -2
  11. humanbound_cli-0.4.1/humanbound_cli/connectors/__init__.py +1 -0
  12. humanbound_cli-0.4.1/humanbound_cli/connectors/microsoft.py +1613 -0
  13. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/main.py +8 -0
  14. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/report.py +172 -48
  15. humanbound_cli-0.4.1/humanbound_cli/report_builder.py +765 -0
  16. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli.egg-info/PKG-INFO +75 -2
  17. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli.egg-info/SOURCES.txt +7 -0
  18. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli.egg-info/requires.txt +2 -0
  19. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/pyproject.toml +3 -1
  20. humanbound_cli-0.3.5/humanbound_cli/commands/logs.py +0 -240
  21. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/LICENSE +0 -0
  22. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/__init__.py +0 -0
  23. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/api_keys.py +0 -0
  24. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/auth.py +0 -0
  25. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/campaigns.py +0 -0
  26. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/coverage.py +0 -0
  27. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/docs.py +0 -0
  28. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/experiments.py +0 -0
  29. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/findings.py +0 -0
  30. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/guardrails.py +0 -0
  31. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/init.py +0 -0
  32. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/members.py +0 -0
  33. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/orgs.py +0 -0
  34. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/posture.py +0 -0
  35. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/projects.py +0 -0
  36. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/providers.py +0 -0
  37. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/scan.py +0 -0
  38. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/test.py +0 -0
  39. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/commands/upload_logs.py +0 -0
  40. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/config.py +0 -0
  41. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/exceptions.py +0 -0
  42. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/extractors/__init__.py +0 -0
  43. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/extractors/openapi.py +0 -0
  44. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/extractors/repo.py +0 -0
  45. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/pytest_plugin/__init__.py +0 -0
  46. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
  47. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/pytest_plugin/report.py +0 -0
  48. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/serve/__init__.py +0 -0
  49. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/serve/config_builder.py +0 -0
  50. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/serve/local_server.py +0 -0
  51. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/serve/runtime_detector.py +0 -0
  52. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli/serve/tunnel_client.py +0 -0
  53. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli.egg-info/dependency_links.txt +0 -0
  54. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli.egg-info/entry_points.txt +0 -0
  55. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/humanbound_cli.egg-info/top_level.txt +0 -0
  56. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/relay/relay.py +0 -0
  57. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/setup.cfg +0 -0
  58. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/tests/__init__.py +0 -0
  59. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/tests/cli_integration_test.py +0 -0
  60. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/tests/conftest.py +0 -0
  61. {humanbound_cli-0.3.5 → humanbound_cli-0.4.1}/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.5
3
+ Version: 0.4.1
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
  [![PyPI](https://img.shields.io/pypi/v/humanbound-cli)](https://pypi.org/project/humanbound-cli/)
35
37
  [![License](https://img.shields.io/badge/license-proprietary-blue)]()
36
- [![Version](https://img.shields.io/badge/version-0.2.0-green)]()
38
+ [![Version](https://img.shields.io/badge/version-0.4.0-green)]()
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
  [![PyPI](https://img.shields.io/pypi/v/humanbound-cli)](https://pypi.org/project/humanbound-cli/)
6
6
  [![License](https://img.shields.io/badge/license-proprietary-blue)]()
7
- [![Version](https://img.shields.io/badge/version-0.2.0-green)]()
7
+ [![Version](https://img.shields.io/badge/version-0.4.0-green)]()
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,21 +1,21 @@
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
7
8
  import socketserver
8
- import threading
9
9
  import secrets
10
10
  import hashlib
11
11
  import base64
12
12
  import urllib.parse
13
13
  from typing import Optional, Dict, Any, List
14
- from pathlib import Path
15
14
 
16
15
  import requests
17
16
 
18
17
  from .config import (
18
+ DEFAULT_BASE_URL,
19
19
  get_base_url,
20
20
  get_auth0_domain,
21
21
  get_auth0_client_id,
@@ -547,7 +547,8 @@ class HumanboundClient:
547
547
  self._default_organisation_id = credentials.get("default_organisation_id")
548
548
  # Restore saved base_url unless explicitly overridden via --base-url or env var
549
549
  saved_url = credentials.get("base_url")
550
- if saved_url and self.base_url == get_base_url().rstrip("/"):
550
+ env_override = os.environ.get("HUMANBOUND_BASE_URL")
551
+ if saved_url and not env_override and self.base_url == DEFAULT_BASE_URL.rstrip("/"):
551
552
  self.base_url = saved_url.rstrip("/")
552
553
 
553
554
  def _load_credentials_file(self) -> dict:
@@ -903,6 +904,47 @@ class HumanboundClient:
903
904
  include_project=True,
904
905
  )
905
906
 
907
+ def get_project_logs(
908
+ self,
909
+ page: int = 1,
910
+ size: int = 50,
911
+ result: Optional[str] = None,
912
+ from_date: Optional[str] = None,
913
+ until_date: Optional[str] = None,
914
+ test_category: Optional[str] = None,
915
+ last: Optional[int] = None,
916
+ ) -> dict:
917
+ """Get logs for the current project with optional filters.
918
+
919
+ Args:
920
+ page: Page number.
921
+ size: Items per page.
922
+ result: Filter by result (pass/fail).
923
+ from_date: Start date (ISO 8601).
924
+ until_date: End date (ISO 8601).
925
+ test_category: Filter by test category (substring match).
926
+ last: Limit to last N experiments.
927
+
928
+ Returns:
929
+ Paginated response with logs.
930
+ """
931
+ if not self._project_id:
932
+ raise ValidationError("No project selected. Use set_project() first.")
933
+
934
+ params: Dict[str, Any] = {"page": page, "size": size}
935
+ if result:
936
+ params["result"] = result
937
+ if from_date:
938
+ params["from"] = from_date
939
+ if until_date:
940
+ params["until"] = until_date
941
+ if test_category:
942
+ params["test_category"] = test_category
943
+ if last:
944
+ params["last"] = last
945
+
946
+ return self.get("logs", params=params, include_project=True)
947
+
906
948
  # -------------------------------------------------------------------------
907
949
  # Provider Methods
908
950
  # -------------------------------------------------------------------------
@@ -1081,6 +1123,7 @@ class HumanboundClient:
1081
1123
  data["lang"] = lang
1082
1124
  return self.post(f"projects/{project_id}/datasets/conversations", data=data, include_project=True)
1083
1125
 
1126
+
1084
1127
  # -------------------------------------------------------------------------
1085
1128
  # Subscription Methods
1086
1129
  # -------------------------------------------------------------------------
@@ -1145,6 +1188,150 @@ class HumanboundClient:
1145
1188
  data["event_type"] = event_type
1146
1189
  return self.post(f"organisations/{org_id}/webhooks/{webhook_id}/replay", data=data, include_org=False)
1147
1190
 
1191
+ # -------------------------------------------------------------------------
1192
+ # Connector & Inventory Methods (Shadow AI Discovery)
1193
+ # -------------------------------------------------------------------------
1194
+
1195
+ def create_connector(self, vendor: str, tenant_id: str, client_id: str, client_secret: str,
1196
+ display_name: Optional[str] = None, scopes: Optional[List[str]] = None) -> dict:
1197
+ """Register a new cloud connector."""
1198
+ if not self._organisation_id:
1199
+ raise ValidationError("No organisation selected.")
1200
+ data = {
1201
+ "vendor": vendor,
1202
+ "credentials": {
1203
+ "tenant_id": tenant_id,
1204
+ "client_id": client_id,
1205
+ "client_secret": client_secret,
1206
+ },
1207
+ }
1208
+ if display_name:
1209
+ data["display_name"] = display_name
1210
+ if scopes:
1211
+ data["scopes"] = scopes
1212
+ return self.post("connectors", data=data)
1213
+
1214
+ def list_connectors(self) -> list:
1215
+ """List all connectors for the current organisation."""
1216
+ if not self._organisation_id:
1217
+ raise ValidationError("No organisation selected.")
1218
+ return self.get("connectors")
1219
+
1220
+ def get_connector(self, connector_id: str) -> dict:
1221
+ """Get a single connector."""
1222
+ if not self._organisation_id:
1223
+ raise ValidationError("No organisation selected.")
1224
+ return self.get(f"connectors/{connector_id}")
1225
+
1226
+ def update_connector(self, connector_id: str, data: dict) -> dict:
1227
+ """Update a connector."""
1228
+ if not self._organisation_id:
1229
+ raise ValidationError("No organisation selected.")
1230
+ return self.put(f"connectors/{connector_id}", data=data)
1231
+
1232
+ def delete_connector(self, connector_id: str) -> None:
1233
+ """Delete a connector."""
1234
+ if not self._organisation_id:
1235
+ raise ValidationError("No organisation selected.")
1236
+ self.delete(f"connectors/{connector_id}")
1237
+
1238
+ def test_connector(self, connector_id: str) -> dict:
1239
+ """Test connection validity for a connector."""
1240
+ if not self._organisation_id:
1241
+ raise ValidationError("No organisation selected.")
1242
+ return self.post(f"connectors/{connector_id}/test")
1243
+
1244
+ def trigger_discovery(self, connector_id: str) -> dict:
1245
+ """Trigger a discovery scan for a connector."""
1246
+ if not self._organisation_id:
1247
+ raise ValidationError("No organisation selected.")
1248
+ return self.post(
1249
+ "discover",
1250
+ data={"connector_id": connector_id},
1251
+ timeout=LONG_TIMEOUT,
1252
+ )
1253
+
1254
+ def list_inventory(self, category: Optional[str] = None, vendor: Optional[str] = None,
1255
+ risk_level: Optional[str] = None, is_sanctioned: Optional[bool] = None,
1256
+ page: int = 1, size: int = 50) -> dict:
1257
+ """List discovered inventory assets."""
1258
+ if not self._organisation_id:
1259
+ raise ValidationError("No organisation selected.")
1260
+ params: Dict[str, Any] = {"page": page, "size": size}
1261
+ if category:
1262
+ params["category"] = category
1263
+ if vendor:
1264
+ params["vendor"] = vendor
1265
+ if risk_level:
1266
+ params["risk_level"] = risk_level
1267
+ if is_sanctioned is not None:
1268
+ params["is_sanctioned"] = str(is_sanctioned).lower()
1269
+ return self.get("inventory", params=params)
1270
+
1271
+ def get_inventory_asset(self, asset_id: str) -> dict:
1272
+ """Get a single inventory asset."""
1273
+ if not self._organisation_id:
1274
+ raise ValidationError("No organisation selected.")
1275
+ return self.get(f"inventory/{asset_id}")
1276
+
1277
+ def update_inventory_asset(self, asset_id: str, data: dict) -> dict:
1278
+ """Update governance fields on an inventory asset."""
1279
+ if not self._organisation_id:
1280
+ raise ValidationError("No organisation selected.")
1281
+ return self.put(f"inventory/{asset_id}", data=data)
1282
+
1283
+ def archive_inventory_asset(self, asset_id: str) -> dict:
1284
+ """Archive an inventory asset."""
1285
+ if not self._organisation_id:
1286
+ raise ValidationError("No organisation selected.")
1287
+ return self.put(f"inventory/{asset_id}/archive")
1288
+
1289
+ def get_shadow_posture(self) -> dict:
1290
+ """Get shadow AI posture from the org posture endpoint."""
1291
+ org_id = self._organisation_id
1292
+ if not org_id:
1293
+ raise ValidationError("No organisation selected.")
1294
+ result = self.get(f"organisations/{org_id}/posture", include_org=False)
1295
+ shadow = result.get("dimensions", {}).get("shadow_ai")
1296
+ if not shadow:
1297
+ return {"score": 100.0, "grade": "A", "total_assets": 0,
1298
+ "shadow_count": 0, "sanctioned_count": 0, "domain_scores": {}}
1299
+ return shadow
1300
+
1301
+ def persist_discovery(self, nonce: str) -> dict:
1302
+ """Persist analysed discovery results to inventory.
1303
+
1304
+ POST /organisations/{org_id}/analyse/persist with x-nonce header.
1305
+
1306
+ Args:
1307
+ nonce: Single-use nonce from the /analyse response.
1308
+
1309
+ Returns:
1310
+ Persistence summary dict.
1311
+ """
1312
+ org_id = self._organisation_id
1313
+ if not org_id:
1314
+ raise ValidationError("No organisation selected.")
1315
+ self._ensure_authenticated()
1316
+ headers = self._get_headers(include_org=False)
1317
+ headers["x-nonce"] = nonce
1318
+ response = requests.post(
1319
+ f"{self.base_url}/organisations/{org_id}/analyse/persist",
1320
+ headers=headers,
1321
+ json={},
1322
+ timeout=LONG_TIMEOUT,
1323
+ )
1324
+ return self._handle_response(response)
1325
+
1326
+ def onboard_inventory_asset(self, asset_id: str, project_name: Optional[str] = None) -> dict:
1327
+ """Create a project from an inventory asset."""
1328
+ if not self._organisation_id:
1329
+ raise ValidationError("No organisation selected.")
1330
+ data = {}
1331
+ if project_name:
1332
+ data["name"] = project_name
1333
+ return self.post(f"inventory/{asset_id}/onboard", data=data)
1334
+
1148
1335
 
1149
1336
  # Import ValidationError to this module
1150
1337
  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)