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
28 changes: 21 additions & 7 deletions .github/actions/attach-scan-report/action.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
# Composite action: attach-scan-report
#
# Attach an empty OCI scan-report referrer to a promoted image with `oras
# attach`. The referrer records how and when the image was cleared: its scan
# method, source, tag, threshold, excepted CVEs, and scanner name/version. It
# carries annotations only (no layer blobs); its subject is the promoted image.
# Attach an OCI vulnerability-attestation referrer to a promoted image with
# `oras attach`. The referrer carries the scan result as an in-toto
# vulnerability attestation (a Cosign vuln scan record, predicate type
# https://cosign.sigstore.dev/attestation/vuln/v1) in its payload layer, and
# records how and when the image was cleared as manifest annotations: its scan
# method, source, tag, threshold, excepted CVEs, and scanner name/version. Its
# subject is the promoted image.
#
# Terminology: see docs/reference/workflow-actions.md.
name: attach-scan-report
description: Attach an empty OCI scan-report referrer to a promoted image.
description: Attach an OCI vulnerability-attestation referrer to a promoted image.

inputs:
image-ref:
description: "Promoted image to attach the referrer to (repo:tag)."
required: true
attestation-file:
description: "Path to the in-toto vulnerability attestation to attach as the referrer payload."
required: true
source-repo:
description: "Repository the image was promoted from (recorded in the referrer)."
required: true
Expand Down Expand Up @@ -61,6 +67,7 @@ runs:
shell: bash
env:
IMAGE_REF: "${{ inputs.image-ref }}"
ATTESTATION_FILE: "${{ inputs.attestation-file }}"
SOURCE_REPO: "${{ inputs.source-repo }}"
TAG: "${{ inputs.tag }}"
THRESHOLD: "${{ inputs.threshold }}"
Expand All @@ -79,8 +86,14 @@ runs:
created="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
echo "Attaching scan report to ${IMAGE_REF}"

if [ ! -s "${ATTESTATION_FILE}" ]; then
echo "::error::attestation-file '${ATTESTATION_FILE}' is missing or empty" >&2
exit 1
fi

args=(
--artifact-type application/vnd.cssc.scan-report.v1+json
--artifact-type application/vnd.in-toto+json
--annotation "in-toto.io/predicate-type=https://cosign.sigstore.dev/attestation/vuln/v1"
--annotation "org.opencontainers.image.created=${created}"
--annotation "com.cssc.scan.source=${SOURCE_REPO}"
--annotation "com.cssc.scan.tag=${TAG}"
Expand Down Expand Up @@ -111,4 +124,5 @@ runs:
)
fi

oras attach "${args[@]}" "${IMAGE_REF}"
oras attach "${args[@]}" --disable-path-validation \
"${IMAGE_REF}" "${ATTESTATION_FILE}:application/vnd.in-toto+json"
34 changes: 31 additions & 3 deletions .github/actions/scan-image/action.yml
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Composite action: scan-image
#
# Scan a single container image's filesystem with `trivy image` and write the
# JSON report plus the de-duplicated list of vulnerability IDs found at or above
# the requested severity set. Suited to images that carry package-manager
# metadata; for distroless / hardened images use scan-sbom instead.
# JSON report, the de-duplicated list of vulnerability IDs found at or above the
# requested severity set, and an in-toto vulnerability attestation (a Cosign
# vuln scan record wrapped in an in-toto Statement) for the scanned image.
# Suited to images that carry package-manager metadata; for distroless /
# hardened images use scan-sbom instead.
#
# Terminology: see docs/reference/workflow-actions.md.
name: scan-image
Expand All @@ -27,6 +29,9 @@ outputs:
blocking-ids-path:
description: "Path to the newline-separated, de-duplicated list of vulnerability IDs found."
value: ${{ steps.scan.outputs.blocking-ids-path }}
attestation-path:
description: "Path to the in-toto vulnerability attestation (Cosign vuln predicate) for the scanned image."
value: ${{ steps.scan.outputs.attestation-path }}

runs:
using: composite
Expand All @@ -45,6 +50,7 @@ runs:
mkdir -p "${OUTPUT_DIR}"
report="${OUTPUT_DIR}/report.json"
ids="${OUTPUT_DIR}/blocking-ids.txt"
attestation="${OUTPUT_DIR}/vuln-attestation.json"

echo "::group::Scanning ${IMAGE_REF}"
trivy image --quiet --scanners vuln --severity "${SEVERITIES}" \
Expand All @@ -54,5 +60,27 @@ runs:
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' \
"${report}" | sort -u > "${ids}"

# Build a vulnerability attestation from the report we already have: a
# Cosign vulnerability scan record (`trivy convert`, no re-scan) wrapped
# in an in-toto Statement whose subject is the scanned image digest.
vuln="${OUTPUT_DIR}/cosign-vuln.json"
trivy convert --quiet --format cosign-vuln --output "${vuln}" "${report}"

ref_digest="$(crane digest "${IMAGE_REF}")"
algo="${ref_digest%%:*}"
hexv="${ref_digest#*:}"
jq -n \
--arg name "${IMAGE_REF%:*}" \
--arg algo "${algo}" \
--arg hexv "${hexv}" \
--slurpfile pred "${vuln}" \
'{
_type: "https://in-toto.io/Statement/v1",
subject: [ { name: $name, digest: { ($algo): $hexv } } ],
predicateType: "https://cosign.sigstore.dev/attestation/vuln/v1",
predicate: $pred[0]
}' > "${attestation}"

echo "report-path=${report}" >> "${GITHUB_OUTPUT}"
echo "blocking-ids-path=${ids}" >> "${GITHUB_OUTPUT}"
echo "attestation-path=${attestation}" >> "${GITHUB_OUTPUT}"
47 changes: 46 additions & 1 deletion .github/actions/scan-sbom/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@
#
# Emits the union of vulnerability IDs across all platforms, the number of
# platforms whose SBOM could not be found (a blocking condition), the platform
# count, and a Markdown fragment with the per-platform breakdown.
# count, a Markdown fragment with the per-platform breakdown, and an image-level
# in-toto vulnerability attestation (a Cosign vuln scan record built from the
# merged per-platform findings).
#
# Terminology: see docs/reference/workflow-actions.md.
name: scan-sbom
Expand Down Expand Up @@ -48,6 +50,9 @@ outputs:
platform-detail-path:
description: "Path to a Markdown fragment with the per-platform findings table rows."
value: ${{ steps.scan.outputs.platform-detail-path }}
attestation-path:
description: "Path to the image-level in-toto vulnerability attestation (Cosign vuln predicate) built from the merged per-platform findings."
value: ${{ steps.scan.outputs.attestation-path }}

runs:
using: composite
Expand Down Expand Up @@ -120,6 +125,8 @@ runs:
: > "${tag_blocking}"
plat_detail="${OUTPUT_DIR}/platdetail.md"
: > "${plat_detail}"
reports_list="${OUTPUT_DIR}/reports.txt" # per-platform report paths
: > "${reports_list}"
missing_sbom=0
platform_count=0

Expand All @@ -139,6 +146,7 @@ runs:
report="${OUTPUT_DIR}/report-$(printf '%s' "${plat_name}" | tr '/' '_').json"
trivy sbom --quiet --scanners vuln --severity "${SEVERITIES}" \
--format json --output "${report}" "${sbom_file}"
printf '%s\n' "${report}" >> "${reports_list}"

plat_ids="${OUTPUT_DIR}/ids.txt"
jq -r '[.Results[]?.Vulnerabilities[]?.VulnerabilityID] | unique | .[]' \
Expand All @@ -163,7 +171,44 @@ runs:
# platforms is reported once (matches the documented output semantics).
sort -u "${tag_blocking}" -o "${tag_blocking}"

# Build one image-level vulnerability attestation: merge the per-platform
# Trivy JSON reports into a single report, convert it to a Cosign vuln
# scan record (no re-scan), and wrap it in an in-toto Statement whose
# subject is the image (index) digest.
merged="${OUTPUT_DIR}/merged-report.json"
attestation="${OUTPUT_DIR}/vuln-attestation.json"
vuln="${OUTPUT_DIR}/cosign-vuln.json"
if [ -s "${reports_list}" ]; then
# shellcheck disable=SC2046
jq -s '
{ SchemaVersion: (.[0].SchemaVersion // 2),
ArtifactName: (.[0].ArtifactName // ""),
ArtifactType: (.[0].ArtifactType // ""),
Metadata: (.[0].Metadata // {}),
Results: ( map(.Results // []) | add | unique ) }
' $(tr '\n' ' ' < "${reports_list}") > "${merged}"
else
jq -n '{SchemaVersion: 2, Results: []}' > "${merged}"
fi
trivy convert --quiet --format cosign-vuln --output "${vuln}" "${merged}"

ref_digest="$(crane digest "${src}")"
algo="${ref_digest%%:*}"
hexv="${ref_digest#*:}"
jq -n \
--arg name "${src%:*}" \
--arg algo "${algo}" \
--arg hexv "${hexv}" \
--slurpfile pred "${vuln}" \
'{
_type: "https://in-toto.io/Statement/v1",
subject: [ { name: $name, digest: { ($algo): $hexv } } ],
predicateType: "https://cosign.sigstore.dev/attestation/vuln/v1",
predicate: $pred[0]
}' > "${attestation}"

echo "missing-sbom-count=${missing_sbom}" >> "${GITHUB_OUTPUT}"
echo "platform-count=${platform_count}" >> "${GITHUB_OUTPUT}"
echo "blocking-ids-path=${tag_blocking}" >> "${GITHUB_OUTPUT}"
echo "platform-detail-path=${plat_detail}" >> "${GITHUB_OUTPUT}"
echo "attestation-path=${attestation}" >> "${GITHUB_OUTPUT}"
1 change: 1 addition & 0 deletions .github/workflows/_promote-from-quarantine-sbom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,7 @@ jobs:
uses: ./.github/actions/attach-scan-report
with:
image-ref: ${{ inputs.dest_repo }}:${{ matrix.tag }}
attestation-file: ${{ steps.scan.outputs.attestation-path }}
source-repo: ${{ inputs.source_repo }}
tag: ${{ matrix.tag }}
threshold: ${{ inputs.severity_threshold }}
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/_promote-from-quarantine.yml
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,7 @@ jobs:
uses: ./.github/actions/attach-scan-report
with:
image-ref: ${{ inputs.dest_repo }}:${{ matrix.tag }}
attestation-file: ${{ steps.scan.outputs.attestation-path }}
source-repo: ${{ inputs.source_repo }}
tag: ${{ matrix.tag }}
threshold: ${{ inputs.severity_threshold }}
Expand Down
4 changes: 4 additions & 0 deletions docs/architecture/catalog/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,7 @@ on a vulnerability policy, and promoting the ones that pass into `golden/*`.
- [Promotion override approval](promote-from-quarantine-override-approval.md) —
the human-in-the-loop approve/deny path for promoting an image that a policy
would otherwise block.
- [Vulnerability-attestation scan report](promote-from-quarantine-vuln-attestation.md)
— replacing the empty scan-report referrer with a content-bearing in-toto
vulnerability attestation while keeping the `com.cssc.scan.*` summary
annotations. (Also serves **Supply Chain Observability**.)
Loading
Loading