Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<owner/name>` 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`.
Expand Down
15 changes: 7 additions & 8 deletions limacharlie/sdk/cloudsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -2324,22 +2324,21 @@ 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:
The document is served straight from object storage, so the link
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,
Expand Down
23 changes: 11 additions & 12 deletions tests/unit/test_sdk_cloudsec.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading