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.
Files changed (60) hide show
  1. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/PKG-INFO +75 -2
  2. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/README.md +72 -1
  3. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/client.py +149 -1
  4. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/__init__.py +6 -1
  5. humanbound_cli-0.4.0/humanbound_cli/commands/completion.py +69 -0
  6. humanbound_cli-0.4.0/humanbound_cli/commands/connectors.py +328 -0
  7. humanbound_cli-0.4.0/humanbound_cli/commands/discover.py +1840 -0
  8. humanbound_cli-0.4.0/humanbound_cli/commands/inventory.py +1215 -0
  9. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/sentinel.py +2 -2
  10. humanbound_cli-0.4.0/humanbound_cli/connectors/__init__.py +1 -0
  11. humanbound_cli-0.4.0/humanbound_cli/connectors/microsoft.py +1613 -0
  12. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/main.py +8 -0
  13. humanbound_cli-0.4.0/humanbound_cli/report_builder.py +765 -0
  14. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/PKG-INFO +75 -2
  15. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/SOURCES.txt +7 -0
  16. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/requires.txt +2 -0
  17. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/top_level.txt +1 -0
  18. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/pyproject.toml +3 -1
  19. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/LICENSE +0 -0
  20. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/__init__.py +0 -0
  21. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/api_keys.py +0 -0
  22. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/auth.py +0 -0
  23. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/campaigns.py +0 -0
  24. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/coverage.py +0 -0
  25. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/docs.py +0 -0
  26. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/experiments.py +0 -0
  27. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/findings.py +0 -0
  28. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/guardrails.py +0 -0
  29. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/init.py +0 -0
  30. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/logs.py +0 -0
  31. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/members.py +0 -0
  32. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/orgs.py +0 -0
  33. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/posture.py +0 -0
  34. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/projects.py +0 -0
  35. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/providers.py +0 -0
  36. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/scan.py +0 -0
  37. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/test.py +0 -0
  38. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/commands/upload_logs.py +0 -0
  39. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/config.py +0 -0
  40. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/exceptions.py +0 -0
  41. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/extractors/__init__.py +0 -0
  42. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/extractors/openapi.py +0 -0
  43. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/extractors/repo.py +0 -0
  44. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/pytest_plugin/__init__.py +0 -0
  45. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
  46. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/pytest_plugin/report.py +0 -0
  47. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/report.py +0 -0
  48. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/__init__.py +0 -0
  49. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/config_builder.py +0 -0
  50. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/local_server.py +0 -0
  51. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/runtime_detector.py +0 -0
  52. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli/serve/tunnel_client.py +0 -0
  53. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/dependency_links.txt +0 -0
  54. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/humanbound_cli.egg-info/entry_points.txt +0 -0
  55. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/relay/relay.py +0 -0
  56. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/setup.cfg +0 -0
  57. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/tests/__init__.py +0 -0
  58. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/tests/cli_integration_test.py +0 -0
  59. {humanbound_cli-0.3.5 → humanbound_cli-0.4.0}/tests/conftest.py +0 -0
  60. {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.5
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
  [![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,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
- if saved_url and self.base_url == get_base_url().rstrip("/"):
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)