diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 56125da..7e86b0c 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -42,3 +42,51 @@ jobs: TAGS: ghcr.io/edera-dev/${{ matrix.component }}:nightly COSIGN_EXPERIMENTAL: "true" run: cosign sign --yes "${TAGS}@${DIGEST}" + + - name: Build `build` stage for source-commit capture + uses: docker/build-push-action@15560696de535e4014efeff63c48f16952e52dd1 # v6.2.0 + with: + file: ./Dockerfile.${{ matrix.component }} + target: build + platforms: linux/amd64 + load: true + tags: sbom-scan-${{ matrix.component }}:latest + - name: Generate and attest CycloneDX SBOM + env: + DIGEST: ${{ steps.push-step.outputs.digest }} + TAGS: ghcr.io/edera-dev/${{ matrix.component }}:nightly + COMPONENT: ${{ matrix.component }} + COSIGN_EXPERIMENTAL: "true" + run: | + set -euo pipefail + scan="sbom-scan-${COMPONENT}:latest" + + sha() { docker run --rm "${scan}" git -C "$1" rev-parse HEAD; } + desc() { docker run --rm "${scan}" git -C "$1" describe --tags --always; } + case "${COMPONENT}" in + squashfs-tools) + SQUASHFS_TOOLS_COMMIT="$(sha /usr/src/squashfs-tools)" + SQUASHFS_TOOLS_VERSION="$(desc /usr/src/squashfs-tools)" + ZLIB_NG_COMMIT="$(sha /usr/src/zlib-ng)" + ZLIB_NG_VERSION="$(desc /usr/src/zlib-ng)" + export SQUASHFS_TOOLS_COMMIT SQUASHFS_TOOLS_VERSION \ + ZLIB_NG_COMMIT ZLIB_NG_VERSION + ;; + mkfs) + E2FSPROGS_COMMIT="$(sha /usr/src/e2fsprogs)" + E2FSPROGS_VERSION="$(desc /usr/src/e2fsprogs)" + export E2FSPROGS_COMMIT E2FSPROGS_VERSION + ;; + esac + + python3 generate-sbom.py + + jq -e '.bomFormat == "CycloneDX" and ((.components // []) | length > 0)' \ + "${COMPONENT}.cdx.json" >/dev/null \ + || { echo "::error::SBOM for ${COMPONENT} is empty or invalid"; exit 1; } + jq '{component: env.COMPONENT, components: (.components | length)}' \ + "${COMPONENT}.cdx.json" + + cosign attest --yes --type cyclonedx \ + --predicate "${COMPONENT}.cdx.json" \ + "${TAGS}@${DIGEST}" diff --git a/generate-sbom.py b/generate-sbom.py new file mode 100644 index 0000000..a6c44bd --- /dev/null +++ b/generate-sbom.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python3 +"""Generate a curated CycloneDX SBOM for a published linux-utils-oci image. + +Reads from the environment (set by the SBOM workflow step, resolved from the +build-stage image): + COMPONENT squashfs-tools | mkfs + SQUASHFS_TOOLS_COMMIT resolved commit of plougher/squashfs-tools + SQUASHFS_TOOLS_VERSION git-describe of same + ZLIB_NG_COMMIT resolved commit of zlib-ng/zlib-ng + ZLIB_NG_VERSION git-describe of same + E2FSPROGS_COMMIT resolved commit of e2fsprogs + E2FSPROGS_VERSION git-describe of same (the build-discovered release tag) + +Writes .cdx.json (CycloneDX 1.6) in the current directory. +""" +import json +import os +import sys + + +def github_source(name, gh_path, commit, version, ctype="application"): + """A GitHub-hosted source component, pinned to commit in its purl.""" + purl = "pkg:github/%s" % gh_path + if commit: + purl = "%s@%s" % (purl, commit) + component = { + "bom-ref": purl, + "type": ctype, + "name": name, + "purl": purl, + "externalReferences": [ + {"type": "vcs", "url": "https://github.com/%s.git" % gh_path} + ], + } + if version: + component["version"] = version + return component + + +def e2fsprogs_source(commit, version): + """e2fsprogs lives on git.kernel.org (not GitHub), so it is a pkg:generic + component with the commit carried as a property (purls for generic sources + have no standard commit qualifier).""" + purl = "pkg:generic/e2fsprogs" + if version: + purl = "%s@%s" % (purl, version) + component = { + "bom-ref": purl, + "type": "application", + "name": "e2fsprogs", + "purl": purl, + "externalReferences": [ + { + "type": "vcs", + "url": "https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git", + } + ], + } + if version: + component["version"] = version + if commit: + component["properties"] = [ + {"name": "dev.edera.source.commit", "value": commit} + ] + return component + + +def build_sources(comp): + if comp == "squashfs-tools": + return [ + github_source( + "squashfs-tools", + "plougher/squashfs-tools", + os.environ.get("SQUASHFS_TOOLS_COMMIT", ""), + os.environ.get("SQUASHFS_TOOLS_VERSION", ""), + ), + # zlib-ng is built static (zlib-compat) and linked into the binaries. + github_source( + "zlib-ng", + "zlib-ng/zlib-ng", + os.environ.get("ZLIB_NG_COMMIT", ""), + os.environ.get("ZLIB_NG_VERSION", ""), + ctype="library", + ), + ] + if comp == "mkfs": + return [ + e2fsprogs_source( + os.environ.get("E2FSPROGS_COMMIT", ""), + os.environ.get("E2FSPROGS_VERSION", ""), + ) + ] + print("ERROR: unknown COMPONENT %r" % comp, file=sys.stderr) + sys.exit(1) + + +def main(): + comp = os.environ["COMPONENT"] + sources = build_sources(comp) + + image_ref = "%s@nightly" % comp + document = { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "version": 1, + "metadata": { + "component": { + "bom-ref": image_ref, + "type": "container", + "name": comp, + } + }, + "components": sources, + "dependencies": [ + { + "ref": image_ref, + "dependsOn": [c["bom-ref"] for c in sources if c.get("bom-ref")], + } + ], + } + + with open("%s.cdx.json" % comp, "w") as out: + json.dump(document, out, indent=2) + out.write("\n") + + print( + json.dumps( + {"component": comp, "components": len(sources)}, + indent=2, + ) + ) + + +if __name__ == "__main__": + main()