From b480b71d2db9a69dd4463ad9e528265d03e941a8 Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Fri, 25 Sep 2026 14:28:38 +0000 Subject: [PATCH 1/2] cloudsec: request a repository SBOM with GET code/sbom?repo= The SBOM export moves from a repository key embedded in the path (code/repos//sbom) to a repo query parameter on code/sbom. get_code_sbom and download_code_sbom keep their signatures; only the request they send changes. Also lists the no_sbom_for_this_repository reason in the docstring. Co-Authored-By: Claude Opus 5.5 (1M context) --- limacharlie/sdk/cloudsec.py | 15 +++++++-------- tests/unit/test_sdk_cloudsec.py | 23 +++++++++++------------ 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/limacharlie/sdk/cloudsec.py b/limacharlie/sdk/cloudsec.py index a660a0b9..33c6f958 100644 --- a/limacharlie/sdk/cloudsec.py +++ b/limacharlie/sdk/cloudsec.py @@ -2324,9 +2324,10 @@ def get_code_sbom( A repository with no SBOM yet is a SUCCESSFUL response with ``sbom`` ``None`` and a machine-readable ``reason`` - (``sbom_not_generated_yet`` or - ``code_lane_not_enabled_in_datacenter``) — only a repository the - org does not have is an error. Check ``sbom`` for ``None`` before + (``sbom_not_generated_yet``, ``no_sbom_for_this_repository`` or + ``code_lane_not_enabled_in_datacenter``; treat any other value + as terminal) — only a repository the org does not have is an + error. Check ``sbom`` for ``None`` before using it. Note: @@ -2334,12 +2335,10 @@ def get_code_sbom( leaves this API's auth boundary. It is deliberately short-lived; fetch it promptly and do not store it. """ - # The key contains a '/', so it is percent-encoded into ONE path - # segment. The gateway accepts either spelling, but encoding is what - # keeps the request unambiguous for anything in between. - quoted = _quote(repo, safe="") + # The key travels as a query value, where its '/' is an ordinary + # character rather than a path separator. return self._get( - f"code/repos/{quoted}/sbom", _query_pairs(provider=provider)) + "code/sbom", _query_pairs(repo=repo, provider=provider)) def download_code_sbom( self, repo: str, *, provider: str | None = None, diff --git a/tests/unit/test_sdk_cloudsec.py b/tests/unit/test_sdk_cloudsec.py index 3285eaa2..54da6fd5 100644 --- a/tests/unit/test_sdk_cloudsec.py +++ b/tests/unit/test_sdk_cloudsec.py @@ -981,25 +981,24 @@ def test_get_code_status(self, cs, mock_org): assert url == f"cloudsec/{OID}/code/status" assert qp is None - def test_get_code_sbom_percent_encodes_the_key(self, cs, mock_org): - """The repository key holds a '/', which must not become a path split. + def test_get_code_sbom_sends_the_key_as_a_query_value(self, cs, mock_org): + """The repository key goes in ``?repo=``, never in the path. - Left unencoded it would add a path segment and the route would not - match at all — a 404 with nothing on the client side to explain it. + The key holds a '/', so as a path segment it would need encoding + that intermediaries may undo; as a query value it is carried as-is. """ mock_org.client.request.return_value = {"sbom": None} - cs.get_code_sbom("refractionPOINT/lc-appsec-fixtures") + cs.get_code_sbom("acme/api") url, qp = _get_call(mock_org) - assert url == ( - f"cloudsec/{OID}/code/repos/" - "refractionPOINT%2Flc-appsec-fixtures/sbom") - assert qp is None + assert url == f"cloudsec/{OID}/code/sbom" + assert qp == [("repo", "acme/api")] def test_get_code_sbom_provider(self, cs, mock_org): mock_org.client.request.return_value = {"sbom": None} - cs.get_code_sbom("acme/api", provider="github") - _, qp = _get_call(mock_org) - assert qp == [("provider", "github")] + cs.get_code_sbom("acme/platform/api", provider="gitlab") + url, qp = _get_call(mock_org) + assert url == f"cloudsec/{OID}/code/sbom" + assert qp == [("repo", "acme/platform/api"), ("provider", "gitlab")] def test_rescan_code_repo_posts_the_trigger(self, cs, mock_org): mock_org.client.request.return_value = {"accepted": True} From 0eea426652c0ec5c9ecd56829f6b63bcb785348a Mon Sep 17 00:00:00 2001 From: Maxime Lamothe-Brassard Date: Fri, 25 Sep 2026 14:34:43 +0000 Subject: [PATCH 2/2] changelog: note the SBOM route change Co-Authored-By: Claude Opus 5.5 (1M context) --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5cc72475..7a6c6648 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +### Cloud Security — SBOM route + +- `CloudSec.get_code_sbom` / `download_code_sbom` (and `cloudsec code sbom`) + now call `GET /v1/cloudsec/{oid}/code/sbom?repo=` instead of + `GET /v1/cloudsec/{oid}/code/repos/{owner%2Fname}/sbom`. Signatures and + output are unchanged. Needs an API release that serves the new route. + ### Email Security onboarding - List `mailsec_provider`, `mailsec_policy`, and `dr-mail` in `hive list-types`.