name: Buildx SBOM Demo on: workflow_dispatch: push: branches: [ demo/buildx ] jobs: buildx-sbom: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Set up .NET 10 preview uses: actions/setup-dotnet@v4 with: dotnet-version: '10.0.x' - name: Publish StellaOps BuildX generator run: | dotnet publish src/StellaOps.Scanner.Sbomer.BuildXPlugin/StellaOps.Scanner.Sbomer.BuildXPlugin.csproj \ -c Release \ -o out/buildx - name: Handshake CAS run: | dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll handshake \ --manifest out/buildx \ --cas out/cas - name: Build demo container image run: | docker buildx build --load -t stellaops/buildx-demo:ci samples/ci/buildx-demo - name: Capture image digest id: digest run: | DIGEST=$(docker image inspect stellaops/buildx-demo:ci --format '{{index .RepoDigests 0}}') echo "digest=$DIGEST" >> "$GITHUB_OUTPUT" - name: Generate SBOM from built image run: | mkdir -p out docker sbom stellaops/buildx-demo:ci --format cyclonedx-json > out/buildx-sbom.cdx.json - name: Start mock Attestor id: attestor run: | mkdir -p out cat <<'PY' > out/mock-attestor.py import json import os from http.server import BaseHTTPRequestHandler, HTTPServer class Handler(BaseHTTPRequestHandler): def do_POST(self): length = int(self.headers.get('Content-Length') or 0) body = self.rfile.read(length) with open(os.path.join('out', 'provenance-request.json'), 'wb') as fp: fp.write(body) self.send_response(202) self.end_headers() self.wfile.write(b'accepted') def log_message(self, format, *args): return if __name__ == '__main__': server = HTTPServer(('127.0.0.1', 8085), Handler) try: server.serve_forever() except KeyboardInterrupt: pass finally: server.server_close() PY touch out/provenance-request.json python3 out/mock-attestor.py & echo $! > out/mock-attestor.pid - name: Emit descriptor with provenance placeholder env: IMAGE_DIGEST: ${{ steps.digest.outputs.digest }} # Uncomment the next line and remove the mock Attestor block to hit a real service. # STELLAOPS_ATTESTOR_TOKEN: ${{ secrets.STELLAOPS_ATTESTOR_TOKEN }} run: | dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ --manifest out/buildx \ --image "$IMAGE_DIGEST" \ --sbom out/buildx-sbom.cdx.json \ --sbom-name buildx-sbom.cdx.json \ --artifact-type application/vnd.stellaops.sbom.layer+json \ --sbom-format cyclonedx-json \ --sbom-kind inventory \ --repository ${{ github.repository }} \ --build-ref ${{ github.sha }} \ --attestor http://127.0.0.1:8085/provenance \ > out/buildx-descriptor.json - name: Verify descriptor determinism env: IMAGE_DIGEST: ${{ steps.digest.outputs.digest }} run: | dotnet out/buildx/StellaOps.Scanner.Sbomer.BuildXPlugin.dll descriptor \ --manifest out/buildx \ --image "$IMAGE_DIGEST" \ --sbom out/buildx-sbom.cdx.json \ --sbom-name buildx-sbom.cdx.json \ --artifact-type application/vnd.stellaops.sbom.layer+json \ --sbom-format cyclonedx-json \ --sbom-kind inventory \ --repository ${{ github.repository }} \ --build-ref ${{ github.sha }} \ > out/buildx-descriptor-repeat.json python - <<'PY' import json def normalize(path: str) -> dict: with open(path, 'r', encoding='utf-8') as handle: data = json.load(handle) data.pop('generatedAt', None) return data baseline = normalize('out/buildx-descriptor.json') repeat = normalize('out/buildx-descriptor-repeat.json') if baseline != repeat: raise SystemExit('Descriptor output changed between runs.') PY - name: Stop mock Attestor if: always() run: | if [ -f out/mock-attestor.pid ]; then kill $(cat out/mock-attestor.pid) fi - name: Upload artifacts uses: actions/upload-artifact@v4 with: name: stellaops-buildx-demo path: | out/buildx-descriptor.json out/buildx-sbom.cdx.json out/provenance-request.json out/buildx-descriptor-repeat.json - name: Show descriptor summary run: | cat out/buildx-descriptor.json