Skip to content

feat(governance): add FlowSignal provider seam and TLS hardening #206

feat(governance): add FlowSignal provider seam and TLS hardening

feat(governance): add FlowSignal provider seam and TLS hardening #206

Workflow file for this run

# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
name: SBOM Generation
# POAM-006 / NIST SP 800-53 CM-8 / ISO 42001 §A.8.3
# Generates CycloneDX SBOMs for all CAGE container images after each build.
# Uploads SBOM artifacts to object storage and fails the build if CRITICAL
# CVEs are found (except allowlisted items in .trivyignore).
#
# Storage backend — technology-agnostic via the S3-compatible API:
# AWS S3 → set SBOM_S3_BUCKET + SBOM_S3_ACCESS_KEY + SBOM_S3_SECRET_KEY
# leave SBOM_S3_ENDPOINT unset (defaults to AWS)
# GCS → set SBOM_S3_BUCKET + SBOM_S3_ACCESS_KEY + SBOM_S3_SECRET_KEY
# (use a GCS HMAC key pair — Cloud Storage > Settings > Interoperability)
# set SBOM_S3_ENDPOINT=https://storage.googleapis.com
# MinIO/R2 → set all four secrets with your endpoint URL
#
# If none of the SBOM_S3_* secrets are configured the upload step is skipped;
# the SBOM is still generated and uploaded as a GitHub Actions artifact.
on:
push:
branches: [main, "feat/**"]
paths:
- "src/**"
- "Dockerfile*"
- "pyproject.toml"
- "uv.lock"
pull_request:
branches: [main]
paths:
- "src/**"
- "Dockerfile*"
- "pyproject.toml"
- "uv.lock"
workflow_dispatch:
env:
REGISTRY: gcr.io
# Fallback to a placeholder when GCP_PROJECT_ID is not configured (e.g. PRs
# from forks or CI contexts without GCP secrets). Without this, the
# interpolated value is an empty string, producing "gcr.io//<image>:<tag>"
# — an invalid Docker reference ("invalid reference format") that fails
# `docker build` before the image can even be built for SBOM/CVE scanning.
PROJECT_ID: ${{ secrets.GCP_PROJECT_ID || 'cage-ci-placeholder' }}
permissions:
contents: read
# id-token: write is NOT required — no Workload Identity Federation used here.
# SBOM upload uses the S3-compatible API with HMAC/access-key credentials.
jobs:
sbom-scan:
name: Generate SBOM and Scan for CVEs
runs-on: ubuntu-latest
strategy:
matrix:
service:
- name: gateway
dockerfile: Dockerfile
image_suffix: gateway
- name: compliance-bridge
dockerfile: src/compliance_bridge/Dockerfile
image_suffix: compliance-bridge
fail-fast: false
# Expose S3-compatible storage secrets as env vars so step-level `if:`
# conditions can test whether they are configured.
# secrets context is not available in `if:` expressions — env vars are.
#
# Backend selection:
# SBOM_S3_ENDPOINT unset → AWS S3 (aws cli default)
# SBOM_S3_ENDPOINT set → custom S3-compatible endpoint
# e.g. https://storage.googleapis.com (GCS)
# https://<account>.r2.cloudflarestorage.com
# http://minio:9000
env:
SBOM_S3_BUCKET: ${{ secrets.SBOM_S3_BUCKET }}
SBOM_S3_ACCESS_KEY: ${{ secrets.SBOM_S3_ACCESS_KEY }}
SBOM_S3_SECRET_KEY: ${{ secrets.SBOM_S3_SECRET_KEY }}
SBOM_S3_ENDPOINT: ${{ secrets.SBOM_S3_ENDPOINT }}
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install Trivy
run: |
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key \
| sudo apt-key add -
echo deb https://aquasecurity.github.io/trivy-repo/deb \
$(lsb_release -sc) main \
| sudo tee -a /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy
- name: Set image tag
id: tag
run: |
echo "IMAGE_TAG=${{ github.sha }}" >> $GITHUB_OUTPUT
echo "IMAGE_REF=${{ env.REGISTRY }}/${PROJECT_ID}/${{ matrix.service.image_suffix }}:${{ github.sha }}" >> $GITHUB_OUTPUT
- name: Build container image
run: |
docker build \
-f ${{ matrix.service.dockerfile }} \
-t ${STEPS_TAG_OUTPUTS_IMAGE_REF} \
.
env:
STEPS_TAG_OUTPUTS_IMAGE_REF: ${{ steps.tag.outputs.IMAGE_REF }}
# POAM-006: Generate CycloneDX SBOM for this container image
- name: Generate CycloneDX SBOM
run: |
trivy image \
--format cyclonedx \
--output sbom-${{ matrix.service.name }}.cdx.json \
${STEPS_TAG_OUTPUTS_IMAGE_REF}
echo "✅ SBOM generated: sbom-${{ matrix.service.name }}.cdx.json"
env:
STEPS_TAG_OUTPUTS_IMAGE_REF: ${{ steps.tag.outputs.IMAGE_REF }}
# POAM-006: Fail if CRITICAL CVEs are found (except .trivyignore allowlist)
- name: Scan for CRITICAL CVEs
run: |
trivy image \
--format table \
--severity CRITICAL,HIGH \
--ignorefile .trivyignore \
--exit-code 1 \
${STEPS_TAG_OUTPUTS_IMAGE_REF}
continue-on-error: false # Hard fail on CRITICAL CVEs
env:
STEPS_TAG_OUTPUTS_IMAGE_REF: ${{ steps.tag.outputs.IMAGE_REF }}
- name: Upload SBOM as GitHub Artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sbom-${{ matrix.service.name }}-${{ github.sha }}
path: sbom-${{ matrix.service.name }}.cdx.json
retention-days: 90
# Upload SBOM to object storage via the S3-compatible API.
# Works with AWS S3, GCS (HMAC), Cloudflare R2, MinIO, and any
# S3-compatible backend — no cloud-provider-specific auth action needed.
#
# GCS HMAC setup (one-time):
# gcloud storage hmac create <service-account-email> \
# --project=<project-id>
# → copy Access ID → SBOM_S3_ACCESS_KEY secret
# → copy Secret → SBOM_S3_SECRET_KEY secret
# Set SBOM_S3_ENDPOINT=https://storage.googleapis.com
# Set SBOM_S3_BUCKET=<your-gcs-bucket-name>
- name: Upload SBOM to object storage (S3-compatible)
if: env.SBOM_S3_BUCKET != '' && env.SBOM_S3_ACCESS_KEY != ''
env:
AWS_ACCESS_KEY_ID: ${{ secrets.SBOM_S3_ACCESS_KEY }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.SBOM_S3_SECRET_KEY }}
# AWS_DEFAULT_REGION is required by the AWS CLI even for non-AWS
# endpoints; 'auto' is accepted by most S3-compatible backends.
AWS_DEFAULT_REGION: ${{ secrets.SBOM_S3_REGION || 'auto' }}
run: |
DEST="s3://${SBOM_S3_BUCKET}/sboms/${{ github.sha }}/sbom-${{ matrix.service.name }}.cdx.json"
# Build endpoint flag — omit for native AWS S3
if [ -n "${SBOM_S3_ENDPOINT}" ]; then
ENDPOINT_FLAG="--endpoint-url ${SBOM_S3_ENDPOINT}"
else
ENDPOINT_FLAG=""
fi
aws s3 cp \
sbom-${{ matrix.service.name }}.cdx.json \
"$DEST" \
$ENDPOINT_FLAG \
--no-progress
echo "✅ SBOM uploaded to $DEST"
if [ -n "${SBOM_S3_ENDPOINT}" ]; then
echo " Endpoint: ${SBOM_S3_ENDPOINT}"
fi
- name: Summary
run: |
echo "## SBOM Generation Summary — ${{ matrix.service.name }}" >> $GITHUB_STEP_SUMMARY
echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY
echo "|---|---|" >> $GITHUB_STEP_SUMMARY
echo "| Image | \`${STEPS_TAG_OUTPUTS_IMAGE_REF}\` |" >> $GITHUB_STEP_SUMMARY
echo "| SBOM Format | CycloneDX |" >> $GITHUB_STEP_SUMMARY
echo "| POAM Reference | POAM-006 (CM-8 / ISO 42001 §A.8.3) |" >> $GITHUB_STEP_SUMMARY
echo "| Artifact | \`sbom-${{ matrix.service.name }}.cdx.json\` |" >> $GITHUB_STEP_SUMMARY
if [ -n "${SBOM_S3_BUCKET}" ] && [ -n "${SBOM_S3_ACCESS_KEY}" ]; then
ENDPOINT="${SBOM_S3_ENDPOINT}"
BACKEND="${ENDPOINT:-AWS S3}"
echo "| Storage backend | \`${BACKEND}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Storage path | \`s3://${SBOM_S3_BUCKET}/sboms/${{ github.sha }}/\` |" >> $GITHUB_STEP_SUMMARY
else
echo "| Storage backend | GitHub Artifact only (SBOM_S3_BUCKET not configured) |" >> $GITHUB_STEP_SUMMARY
fi
env:
STEPS_TAG_OUTPUTS_IMAGE_REF: ${{ steps.tag.outputs.IMAGE_REF }}