diff --git a/limacharlie/commands/cloudsec.py b/limacharlie/commands/cloudsec.py index 58dd3b81..e00d887b 100644 --- a/limacharlie/commands/cloudsec.py +++ b/limacharlie/commands/cloudsec.py @@ -1602,16 +1602,29 @@ def code_rescan(ctx, repo, ref, provider) -> None: @click.option("--commit", default=None, help="The revision the document describes. Recorded, not verified.") @click.option("--ref", default=None, help="The branch or tag, for context.") +@click.option("--default-branch", "default_branch", default=None, + help="The repository's shipping branch. Only worth sending for a " + "repository LimaCharlie does not collect — nothing else can " + "state it there.") @click.option("--provider", default=None, help="Source-control provider the key belongs to (default github).") @pass_context -def code_ingest(ctx, repo, source, file_path, commit, ref, provider) -> None: +def code_ingest(ctx, repo, source, file_path, commit, ref, default_branch, provider) -> None: """Push scan results your own pipeline produced for one repository. Findings are deduplicated against the hosted scan by IDENTITY, so pushing something the hosted scanner also found updates it rather than duplicating it, and re-pushing an identical document writes nothing. + The repository does not have to be one LimaCharlie collects. Pushing for + a repository no connected source-control organization covers creates it, + carrying only what the push vouches for, and 'cloudsec code repos' shows + it with source 'ingest' (and 'both' once a connection collects it too). + Such a repository counts against the free tier's repository quota exactly + like a collected one, and is removed with its findings if it goes a month + with no push that moves its commit. It must still be selected by an + enabled code_scanning policy. + Two things in the response are worth reading rather than skimming. 'notes' lists what the FORMAT could not carry — 'secrets_not_ingestable' means credential findings were deliberately dropped, because a pushed @@ -1635,7 +1648,8 @@ def code_ingest(ctx, repo, source, file_path, commit, ref, provider) -> None: "sections separately." % (file_path, len(document), MAX_CODE_INGEST_BYTES)) cs = _get_cloudsec(ctx) _output(ctx, cs.ingest_code_results( - repo, source, document, commit=commit, ref=ref, provider=provider)) + repo, source, document, commit=commit, ref=ref, + default_branch=default_branch, provider=provider)) @code_group.command("scan") diff --git a/limacharlie/sdk/cloudsec.py b/limacharlie/sdk/cloudsec.py index 58fd30f0..ebbe204e 100644 --- a/limacharlie/sdk/cloudsec.py +++ b/limacharlie/sdk/cloudsec.py @@ -1333,6 +1333,7 @@ def ingest_code_results( *, commit: str | None = None, ref: str | None = None, + default_branch: str | None = None, provider: str | None = None, ) -> dict[str, Any]: """Push results your own pipeline produced for one repository. @@ -1345,9 +1346,16 @@ def ingest_code_results( Args: repo: the ``"/"`` key :meth:`list_code_repos` - returns. It must already be in the org's collected - inventory and be selected by an enabled ``code_scanning`` - policy — the same switch the hosted lane uses. + returns. It must be selected by an enabled ``code_scanning`` + policy — the same switch the hosted lane uses — but it does + NOT have to be in the org's collected inventory: pushing for + a repository no connected source-control organization covers + creates it, with only the facts the push vouches for and + ``source: "ingest"`` on :meth:`list_code_repos`. Such a + repository counts against the free tier's repository quota + exactly like a collected one, and is removed along with its + findings if it goes a month with no push that moves its + commit. source: ``"sarif"``, ``"cyclonedx"`` or ``"report"`` (the LimaCharlie scanner's own ``report/v1`` document, which is loss-free and therefore dedupes exactly). @@ -1356,6 +1364,10 @@ def ingest_code_results( commit: the revision the document describes. Recorded, not verified, and worth sending: it is what tells somebody reading a finding which checkout produced it. + default_branch: the repository's shipping branch. Only worth + sending for a repository LimaCharlie does not collect — + nothing else can state it there, and it is left unset rather + than guessed when you do not know it. Returns: ``{"result": {...}}`` — what landed: ``findings``, the SoR @@ -1386,6 +1398,8 @@ def ingest_code_results( body["commit"] = commit if ref is not None: body["ref"] = ref + if default_branch is not None: + body["default_branch"] = default_branch if provider is not None: body["provider"] = provider return self._post("code/ingest", body) diff --git a/tests/unit/test_sdk_cloudsec.py b/tests/unit/test_sdk_cloudsec.py index f4efb507..3408c1d9 100644 --- a/tests/unit/test_sdk_cloudsec.py +++ b/tests/unit/test_sdk_cloudsec.py @@ -1003,6 +1003,15 @@ def test_ingest_code_results_sends_an_object_as_an_object(self, cs, mock_org): assert body["document"] == {"version": "2.1.0", "runs": []} assert "document_b64" not in body + def test_ingest_code_results_sends_the_default_branch(self, cs, mock_org): + """The shipping branch reaches the backend, because for a repository + LimaCharlie does not collect nothing else can state it — and it is + omitted, never guessed, when the caller does not know it.""" + mock_org.client.request.return_value = {"result": {}} + cs.ingest_code_results("byo-only/demo", "sarif", "{}", default_branch="trunk") + _, body = _post_call(mock_org) + assert body["default_branch"] == "trunk" + def test_ingest_code_results_omits_what_was_not_given(self, cs, mock_org): """An absent commit must not become an empty one: '' is a value, and a finding stamped with a blank revision is worse than one with none."""