Skip to content
60 changes: 60 additions & 0 deletions getting_started/attestations.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,66 @@ Currently, we support the following types of evidence:
Nothing in the SBOM is checked against the artifact. It is recorded as reported, so the
attestation says what the SBOM claims, not whether the claim is true.

**What you get depends on the tool, not just the format.** Kosli records what the document
declares, and tools fill the same fields differently. Three differences catch people out.

*The subject's digest is often absent from CycloneDX.* Kosli fills `subject.sha256` from
the subject's SHA-256 checksum. Each format spells that differently: `hashes` in
CycloneDX, and `checksums` in SPDX, written `PackageChecksum` in the tag-value form. Syft's
SPDX output fills it. Snyk's and Syft's CycloneDX output does not: both leave `hashes`
empty and put the digest in `version`, where it reads as a version string rather than a
checksum. Kosli does not infer a checksum from a version, so `subject.sha256` is empty for
those two, and the digest they wrote is in `subject.version`. That is the tool's choice
rather than a limit of CycloneDX, so check what yours writes instead of assuming either
way.

*A digest that is present is not automatically the artifact's.* The subject identifies what
the generator scanned. Point one at a tag and it records whatever that tag resolved to on
that machine. For a multi-architecture image that is a single architecture, and it can be a
local image id rather than a registry digest. Kosli does not check the subject against the
artifact, so a digest that is present can still belong to something else. Compare the two
only where your pipeline pointed the generator at the exact artifact it attests. Otherwise
check it in the pipeline, where the build can fail, rather than in a policy.

*Package counts are not comparable between formats.* `package_count` counts what each
format calls a package. A CycloneDX component with `type: file` is skipped, while the SPDX
package describing that same file is counted. Syft reports one package for
`kosli_Linux_arm64.rpm` in CycloneDX and two for the same file in SPDX.

Only [Rego](/policy-reference/rego_policy#input-data) can read these fields. Environment
policy expressions get the artifact's name and fingerprint, nothing from inside an
attestation. Evaluation copies an attestation's own fields onto its status entry, so the
summary sits under `attestation_data`:

```rego
Comment thread
AlexKantor87 marked this conversation as resolved.
package policy

import rego.v1

sbom_attestation_name := data.params.sbom_attestation_name

default allow := false

sbom_lists_packages(artifact) if {
sbom := artifact.attestations_statuses[sbom_attestation_name]
sbom.attestation_data.document.package_count > 0
}

allow if {
every artifact in input.trail.compliance_status.artifacts_statuses {
sbom_lists_packages(artifact)
}
}
Comment on lines +354 to +358

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion — the policy denies without saying why, on a page whose subject is rules that give no signal.

policy-reference/rego_policy.mdx:28 calls violations "optional but recommended… Kosli displays these when allow is false", and every other whole policy on the site pairs the two: rego_policy.mdx:231 and :268, and the tutorial at tutorials/evaluate_trails_with_opa.mdx:170 and :235. This one defines only allow, so a reader who runs it against a trail where one artifact's SBOM is missing or reports package_count: 0 gets exit 1 and no message naming the artifact — and the two causes are indistinguishable, which is the distinction the section above spends three paragraphs on.

Suggested change
allow if {
every artifact in input.trail.compliance_status.artifacts_statuses {
sbom_lists_packages(artifact)
}
}
allow if {
every artifact in input.trail.compliance_status.artifacts_statuses {
sbom_lists_packages(artifact)
}
}
violations contains msg if {
some name, artifact in input.trail.compliance_status.artifacts_statuses
not sbom_lists_packages(artifact)
msg := sprintf("artifact '%v': no SBOM attestation named '%v' listing packages", [name, sbom_attestation_name])
}

Worth an opa eval before taking the wording — not sbom_lists_packages(artifact) is negation over a partial function, which behaves the way you want here but is not an idiom the other examples use.

Fix this →

```
Comment thread
AlexKantor87 marked this conversation as resolved.
Comment thread
AlexKantor87 marked this conversation as resolved.

Pass the attestation name in [`--params`](/policy-reference/rego_policy#params). Without it
the alias is undefined and no rule using it runs. A trail-scoped SBOM sits at
`input.trail.compliance_status.attestations_statuses[sbom_attestation_name].attestation_data.document`.

If you narrow the input with `kosli evaluate trail --attestations`, name the SBOM there too,
dot-qualified as `<artifact>.<name>` for an artifact-scoped one. Anything left out is absent
from the input, and a rule reading it does not match rather than failing.
Comment on lines +335 to +367

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Improvement — this is a complete, runnable Rego policy plus --params and --attestations guidance, sitting in an accordion on a getting-started page. It belongs in the reference, which already has a section shaped exactly like it.

The ## Attestation types accordions are uniformly short pointers: JUnit is 5 lines, Snyk 5, Jira 10, SonarQube 7, each ending in "see the CLI reference for usage details and examples". After this PR the SBOM accordion runs 293-373, 80 lines, and 33 of them (335-367) are policy authoring — a package/import preamble, a fail-safe default, an every loop, a params flag and an input-narrowing flag. A reader on this page is deciding what an SBOM attestation records; they meet a policy file before they reach the 9 MiB size limit at 369.

Meanwhile policy-reference/rego_policy.mdx:205 has ## Examples holding two entries of precisely this form — ### Check pull request approvals across multiple trails and ### Check Snyk scan results on a single trail, each a whole policy with a one-paragraph lede. A ### Check an SBOM lists packages there would sit beside them, inherit the surrounding contract and safe-design rules instead of restating them (361-362 re-explains the params aliasing already at rego_policy.mdx:110-115), and be found by readers writing policies rather than only by readers who opened this accordion.

What earns its place here is the part about what the attestation contains: the three tool/format differences (309-333), the two field paths, and the sentence that environment policy expressions cannot reach them. Suggest keeping those, replacing the code block with a link to the new example, and moving 361-367 with it.

Fix this →


The CLI refuses an SBOM file larger than 9 MiB, which leaves room for the attestation
itself within the 10 MB the server accepts. We are working on raising this.

Expand Down
4 changes: 3 additions & 1 deletion policy-reference/rego_policy.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ The policy receives `input.trail`, a single trail object.

<ParamField path="input.trail.compliance_status.attestations_statuses" type="object">
Map of attestation name to attestation status object. Each object contains the attestation's data, including type-specific fields enriched via `--attestations`. For example, a `pull-request` attestation includes a `pull_requests` array, each with an `approvers` array and a `url` string.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion — "purl and sha256 are often empty" states as a property of SBOM attestations the thing the other half of this PR spends a paragraph arguing is a property of the writing tool.

getting_started/attestations.md:315-320 is explicit: Syft's SPDX output fills the digest, Snyk's and Syft's CycloneDX output does not, "that is the tool's choice rather than a limit of CycloneDX, so check what yours writes instead of assuming either way." A reader who only reads this reference takes "often empty" as a reason to design around the field rather than to check their generator, which is the conclusion that earlier prose was rewritten to prevent.

The link you dropped last round also has a working anchor after all — ## Attestation types at getting_started/attestations.md:259 gives #attestation-types. It lands on the accordion list, one click from the SBOM entry, which is closer than no link.

Suggested change
Some types put their summary under `attestation_data` rather than on the object itself. An `sbom` attestation keeps it at `attestation_data.document`, which holds `package_count` and a `subject` object with `name`, `version`, `purl` and `sha256`. These carry only what the tool that wrote the SBOM recorded, so `purl` and `sha256` are empty for generators that leave them out — see [attestation types](/getting_started/attestations#attestation-types) for which tools fill the digest.

Fix this →

Some types put their summary under `attestation_data` rather than on the object itself. An `sbom` attestation keeps it at `attestation_data.document`, which holds `package_count` and a `subject` object with `name`, `version`, `purl` and `sha256`. `purl` and `sha256` are often empty, because they only carry what the tool that wrote the SBOM recorded.
</ParamField>

<ParamField path="input.trail.compliance_status.artifacts_statuses" type="object">
Expand Down Expand Up @@ -256,7 +258,7 @@ artifact_within_threshold(artifact) if {
}

trail_is_compliant(trail) if {
every name, artifact in trail.compliance_status.artifacts_statuses {
every artifact in trail.compliance_status.artifacts_statuses {
artifact_within_threshold(artifact)
}
}
Expand Down
Loading