Skip to content
Open
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
44 changes: 44 additions & 0 deletions getting_started/attestations.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,50 @@ 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 reads the subject's SHA-256
`hashes` entry into `subject.sha256`, in both formats. Syft's SPDX output fills it. Snyk's
and Syft's CycloneDX output does not: both leave `hashes` empty and write the digest into
`version`, where it reads as a version string rather than a checksum. Kosli does not infer
a checksum from a version, so the field is empty for those two. 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.

Of the two policy mechanisms, only [Rego](/policy-reference/rego_policy#input-data) can read
these fields. Environment policy expressions expose the artifact's name and fingerprint,
not attestation content. Evaluation copies an attestation's own fields onto its status
entry, so the summary sits under `attestation_data`:
Comment on lines +333 to +336

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 page is now the site's only source for the attestation_data.document wrapper, and the reference it links to describes a different shape.

policy-reference/rego_policy.mdx:145 says an attestations_statuses value "contains the attestation's data, including type-specific fields enriched via --attestations" and illustrates it with pull_requests sitting directly on the object; its own example reads snyk.processed_snyk_results.results (:252) with no wrapper. attestation_data and document appear in no other .md/.mdx on the site. A reader who follows the #input-data link this sentence gives them, then writes sbom.subject.sha256 or sbom.document.subject.sha256 off the reference's description, gets the silent non-match this section was written to prevent.

Since the path itself has been verified, the cheap durable fix is on the other side: extend the attestations_statuses ParamField in rego_policy.mdx's ## Input data to note that some attestation types nest their summary under attestation_data (SBOM under attestation_data.document, with subject.sha256 and package_count on it), while others expose their fields directly. Then this accordion's one-sentence claim has a reference source behind it instead of standing alone.

Fix this →


```rego

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 — the snippet will not parse or run as written, and it is the only Rego block on the site that omits the preamble.

  • if in a rule head requires import rego.v1 under OPA before 1.0. Every other Rego block on the site carries package policy + import rego.v1policy-reference/rego_policy.mdx:212, :244, tutorials/evaluate_trails_with_opa.mdx:37, :152, :213. This one has neither, so a reader who pastes it gets a parse error rather than the field path they came for.
  • artifact arrives unbound and sbom_describes has no call site. The site's idiom shows where it comes from: every name, artifact in trail.compliance_status.artifacts_statuses (rego_policy.mdx:258-262).

Either show the two preamble lines and the every binding, or say in one clause that the fragment plugs into the Snyk example's shape at /policy-reference/rego_policy#examples — the latter keeps the accordion short and gives the reader a runnable whole to drop it into.

Fix this →

sbom_attestation_name := data.params.sbom_attestation_name

sbom_describes(artifact) if {
sbom := artifact.attestations_statuses[sbom_attestation_name]
sbom.attestation_data.document.subject.sha256 == artifact.artifact_fingerprint

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.

Improvementartifact.artifact_fingerprint is the one field in this snippet that nothing on the site backs up, and it is the field the comparison turns on.

  • rego_policy.mdx:148-150 documents an artifacts_statuses entry as having "its own attestations_statuses map" and names no other field on it, so the fingerprint key is undocumented there.
  • The only in-repo occurrences of artifact_fingerprint are in the trail events array (client_reference/kosli_get_trail.md:83, :404) — a different object from an artifact status entry, so it is not evidence for this path.
  • The nearest documented spelling is the environment-policy one, artifact.fingerprint (policy-reference/environment_policy.mdx:144).

If the key on a status entry is fingerprint (or absent, with the name-keyed map being the only artifact identity in the input), the == compares against undefined, sbom_describes never fires, and the reader gets the silent non-match this whole section was written to prevent — from the example meant to show them the way. Worth pinning with kosli evaluate trail --show-input --output json | jq '.input.trail.compliance_status.artifacts_statuses[] | keys' on a real trail rather than a hand-built input, and then documenting the confirmed key in rego_policy.mdx's ## Input data so the example has a second source.

Fix this →

}
Comment on lines +341 to +344

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 — the only code on the page does the one thing the paragraph three lines above tells the reader not to do, and it has no guard for the case the section opens with.

Lines 324-326: "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… rather than in a policy." The snippet is then a policy-side digest comparison, presented without restating that precondition. A reader who skims to the code block — which is what a reader looking for a path does — takes away the pattern the prose just cautioned against, and a multi-arch image gives them a rule that denies a correctly built artifact.

Separately, on a Snyk or Syft CycloneDX SBOM — the case the section exists to warn about — subject.sha256 is absent, so the == is undefined and sbom_describes does not fire. Under a positive allow that fails safe; inside a violations rule it is the silent skip policy-reference/rego_policy.mdx:84 warns about. The snippet shows neither call site, so the reader cannot tell which they get. The earlier draft carried "assert the field is present before comparing it"; commit 1ef31d2 removed that sentence, and the snippet never grew the check, so nothing on the page now covers it.

Suggested change
sbom_describes(artifact) if {
sbom := artifact.attestations_statuses[sbom_attestation_name]
sbom.attestation_data.document.subject.sha256 == artifact.artifact_fingerprint
}
sbom_describes(artifact) if {
sbom := artifact.attestations_statuses[sbom_attestation_name]
digest := sbom.attestation_data.document.subject.sha256
digest != ""
digest == artifact.artifact_fingerprint
}

Worth a clause above the block saying the comparison holds only under the precondition in the paragraph above — or making the example read package_count, which is always present, so the code demonstrates reaching the fields without also modelling the comparison the prose discourages.

Fix this →

```
Comment on lines +338 to +345

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 — the snippet does not do the thing the paragraph above it advises, and it can't be run as written.

Line 318 tells the reader to "assert the field is present before comparing it, so a missing digest fails the rule instead of skipping it". The snippet compares straight away. On a Snyk or Syft CycloneDX SBOM — the case the section is about — subject.sha256 is absent, the == is undefined, and the body does not fire. Under a positive allow that fails safe; inside a violations rule it is the silent skip rego_policy.mdx:84 warns about. Since the snippet shows neither call site, the reader can't tell which they get. A presence check makes the advice visible in the code:

sbom_describes(artifact) if {
    sbom := artifact.attestations_statuses[sbom_attestation_name]
    digest := sbom.attestation_data.document.subject.sha256
    digest != ""
    digest == artifact.artifact_fingerprint
}

Two smaller things in the same block: if in a rule head needs import rego.v1, which both examples in rego_policy.mdx (:212, :245) and the tutorial carry but this fragment doesn't mention; and artifact arrives unbound, where the site's idiom shows the every name, artifact in trail.compliance_status.artifacts_statuses that produces it (rego_policy.mdx:258-262). Either add that line or say the fragment plugs into the Snyk example's shape.

Line 340 then writes the trail-scoped path as attestations_statuses[name], using name where the snippet aliases sbom_attestation_name, and dropping the input. prefix the reference uses throughout.

Fix this →


A trail-scoped SBOM sits at `trail.compliance_status.attestations_statuses[name]` instead.

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 trail-scoped path stops short of the fields, and swaps the key name the snippet just established.

The snippet aliases the attestation name as sbom_attestation_name (deliberately, per rego_policy.mdx's params rule); this line writes [name], an identifier that is undefined in the surrounding example. And it ends at the status entry, leaving the reader to append the .attestation_data.document step themselves — the exact step that has no second source on the site, so it is the one they are least able to reconstruct.

Suggested change
A trail-scoped SBOM sits at `trail.compliance_status.attestations_statuses[name]` instead.
A trail-scoped SBOM sits at `trail.compliance_status.attestations_statuses[sbom_attestation_name].attestation_data.document` instead.

Fix this →


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.

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