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`. 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}