Add property-based tests for SBOM/VEX document ordering and Unicode normalization determinism

- Implement `SbomVexOrderingDeterminismProperties` for testing component list and vulnerability metadata hash consistency.
- Create `UnicodeNormalizationDeterminismProperties` to validate NFC normalization and Unicode string handling.
- Add project file for `StellaOps.Testing.Determinism.Properties` with necessary dependencies.
- Introduce CI/CD template validation tests including YAML syntax checks and documentation content verification.
- Create validation script for CI/CD templates ensuring all required files and structures are present.
This commit is contained in:
StellaOps Bot
2025-12-26 15:17:15 +02:00
parent 7792749bb4
commit 907783f625
354 changed files with 79727 additions and 1346 deletions

View File

@@ -0,0 +1,145 @@
# .github/workflows/examples/example-container-sign.yml
# Example: Sign container image with keyless signing
#
# This example shows how to:
# 1. Build a container image
# 2. Push to registry
# 3. Sign using StellaOps keyless signing
# 4. Attach attestation to image
#
# Adapt to your repository by:
# - Updating the registry URL
# - Adjusting Dockerfile path
# - Adding your specific build args
name: Build and Sign Container
on:
push:
branches: [main]
tags: ['v*']
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.build.outputs.digest }}
image: ${{ steps.build.outputs.image }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and Push
id: build
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Output Image Digest
if: github.event_name != 'pull_request'
run: |
echo "digest=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
echo "image=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
sign:
needs: build
if: github.event_name != 'pull_request'
uses: ./.github/workflows/examples/stellaops-sign.yml
with:
artifact-digest: ${{ needs.build.outputs.digest }}
artifact-type: image
push-attestation: true
permissions:
id-token: write
contents: read
packages: write
verify:
needs: [build, sign]
if: github.event_name != 'pull_request'
uses: ./.github/workflows/examples/stellaops-verify.yml
with:
artifact-digest: ${{ needs.build.outputs.digest }}
certificate-identity: 'repo:${{ github.repository }}:ref:${{ github.ref }}'
certificate-oidc-issuer: 'https://token.actions.githubusercontent.com'
require-rekor: true
strict: true
permissions:
contents: read
packages: read
summary:
needs: [build, sign, verify]
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Generate Release Summary
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
## Container Image Published
**Image:** \`${{ needs.build.outputs.image }}\`
### Pull Command
\`\`\`bash
docker pull ${{ needs.build.outputs.image }}
\`\`\`
### Verify Signature
\`\`\`bash
stella attest verify \\
--artifact "${{ needs.build.outputs.digest }}" \\
--certificate-identity "repo:${{ github.repository }}:ref:${{ github.ref }}" \\
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
\`\`\`
### Attestations
| Type | Digest |
|------|--------|
| Signature | \`${{ needs.sign.outputs.attestation-digest }}\` |
| Rekor | \`${{ needs.sign.outputs.rekor-uuid }}\` |
EOF

View File

@@ -0,0 +1,184 @@
# .github/workflows/examples/example-sbom-sign.yml
# Example: Generate and sign SBOM with keyless signing
#
# This example shows how to:
# 1. Generate SBOM using Syft
# 2. Sign the SBOM with StellaOps
# 3. Attach SBOM attestation to container image
#
# The signed SBOM provides:
# - Proof of SBOM generation time
# - Binding to CI/CD identity (repo, branch, workflow)
# - Transparency log entry for audit
name: Generate and Sign SBOM
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
inputs:
image:
description: 'Container image to scan (with digest)'
required: true
type: string
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
generate-sbom:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
sbom-digest: ${{ steps.sbom.outputs.digest }}
image-digest: ${{ steps.resolve.outputs.digest }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Syft
uses: anchore/sbom-action/download-syft@v0
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve Image Digest
id: resolve
run: |
if [[ -n "${{ github.event.inputs.image }}" ]]; then
IMAGE="${{ github.event.inputs.image }}"
else
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}"
fi
# Resolve to digest if not already
if [[ ! "$IMAGE" =~ @sha256: ]]; then
DIGEST=$(docker manifest inspect "$IMAGE" -v | jq -r '.Descriptor.digest')
IMAGE="${IMAGE%%:*}@${DIGEST}"
else
DIGEST="${IMAGE##*@}"
fi
echo "image=${IMAGE}" >> $GITHUB_OUTPUT
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
echo "Resolved image: $IMAGE"
- name: Generate SBOM
id: sbom
run: |
set -euo pipefail
IMAGE="${{ steps.resolve.outputs.image }}"
SBOM_FILE="sbom.cdx.json"
echo "::group::Generating SBOM for $IMAGE"
syft "$IMAGE" \
--output cyclonedx-json="${SBOM_FILE}" \
--source-name "${{ github.repository }}" \
--source-version "${{ github.sha }}"
echo "::endgroup::"
# Calculate SBOM digest
SBOM_DIGEST="sha256:$(sha256sum "${SBOM_FILE}" | cut -d' ' -f1)"
echo "digest=${SBOM_DIGEST}" >> $GITHUB_OUTPUT
echo "SBOM digest: ${SBOM_DIGEST}"
# Store for upload
echo "${SBOM_DIGEST}" > sbom-digest.txt
- name: Upload SBOM
uses: actions/upload-artifact@v4
with:
name: sbom
path: |
sbom.cdx.json
sbom-digest.txt
if-no-files-found: error
sign-sbom:
needs: generate-sbom
uses: ./.github/workflows/examples/stellaops-sign.yml
with:
artifact-digest: ${{ needs.generate-sbom.outputs.sbom-digest }}
artifact-type: sbom
predicate-type: 'https://cyclonedx.org/bom/1.5'
push-attestation: true
permissions:
id-token: write
contents: read
packages: write
attach-to-image:
needs: [generate-sbom, sign-sbom]
runs-on: ubuntu-latest
permissions:
packages: write
steps:
- name: Download SBOM
uses: actions/download-artifact@v4
with:
name: sbom
- name: Install StellaOps CLI
uses: stella-ops/setup-cli@v1
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Attach SBOM to Image
env:
IMAGE_DIGEST: ${{ needs.generate-sbom.outputs.image-digest }}
ATTESTATION_DIGEST: ${{ needs.sign-sbom.outputs.attestation-digest }}
run: |
echo "::group::Attaching SBOM attestation to image"
stella attest attach \
--image "${IMAGE_DIGEST}" \
--attestation "${ATTESTATION_DIGEST}" \
--type sbom
echo "::endgroup::"
- name: Summary
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
## SBOM Signed and Attached
| Field | Value |
|-------|-------|
| **Image** | \`${{ needs.generate-sbom.outputs.image-digest }}\` |
| **SBOM Digest** | \`${{ needs.generate-sbom.outputs.sbom-digest }}\` |
| **Attestation** | \`${{ needs.sign-sbom.outputs.attestation-digest }}\` |
| **Rekor UUID** | \`${{ needs.sign-sbom.outputs.rekor-uuid }}\` |
### Verify SBOM
\`\`\`bash
stella attest verify \\
--artifact "${{ needs.generate-sbom.outputs.sbom-digest }}" \\
--certificate-identity "repo:${{ github.repository }}:ref:${{ github.ref }}" \\
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
\`\`\`
### Download SBOM
\`\`\`bash
stella sbom download \\
--image "${{ needs.generate-sbom.outputs.image-digest }}" \\
--output sbom.cdx.json
\`\`\`
EOF

View File

@@ -0,0 +1,191 @@
# .github/workflows/examples/example-verdict-sign.yml
# Example: Sign policy verdict with keyless signing
#
# This example shows how to:
# 1. Run StellaOps policy evaluation
# 2. Sign the verdict with keyless signing
# 3. Use verdict in deployment gate
#
# Policy verdicts provide:
# - Cryptographic proof of policy evaluation result
# - Binding to specific image and policy version
# - Evidence for audit and compliance
name: Policy Verdict Gate
on:
push:
branches: [main]
workflow_dispatch:
inputs:
image:
description: 'Container image to evaluate (with digest)'
required: true
type: string
policy:
description: 'Policy pack ID'
required: false
default: 'default'
type: string
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
evaluate:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
verdict: ${{ steps.eval.outputs.verdict }}
verdict-digest: ${{ steps.eval.outputs.verdict-digest }}
image-digest: ${{ steps.resolve.outputs.digest }}
passed: ${{ steps.eval.outputs.passed }}
steps:
- name: Install StellaOps CLI
uses: stella-ops/setup-cli@v1
- name: Log in to Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Resolve Image
id: resolve
run: |
if [[ -n "${{ github.event.inputs.image }}" ]]; then
IMAGE="${{ github.event.inputs.image }}"
else
IMAGE="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}"
fi
# Resolve to digest
if [[ ! "$IMAGE" =~ @sha256: ]]; then
DIGEST=$(docker manifest inspect "$IMAGE" -v | jq -r '.Descriptor.digest')
IMAGE="${IMAGE%%:*}@${DIGEST}"
else
DIGEST="${IMAGE##*@}"
fi
echo "image=${IMAGE}" >> $GITHUB_OUTPUT
echo "digest=${DIGEST}" >> $GITHUB_OUTPUT
- name: Run Policy Evaluation
id: eval
env:
STELLAOPS_URL: 'https://api.stella-ops.org'
run: |
set -euo pipefail
IMAGE="${{ steps.resolve.outputs.image }}"
POLICY="${{ github.event.inputs.policy || 'default' }}"
echo "::group::Evaluating policy '${POLICY}' against ${IMAGE}"
RESULT=$(stella policy evaluate \
--image "${IMAGE}" \
--policy "${POLICY}" \
--output json)
echo "$RESULT" | jq .
echo "::endgroup::"
# Extract verdict
VERDICT=$(echo "$RESULT" | jq -r '.verdict')
VERDICT_DIGEST=$(echo "$RESULT" | jq -r '.verdictDigest')
PASSED=$(echo "$RESULT" | jq -r '.passed')
echo "verdict=${VERDICT}" >> $GITHUB_OUTPUT
echo "verdict-digest=${VERDICT_DIGEST}" >> $GITHUB_OUTPUT
echo "passed=${PASSED}" >> $GITHUB_OUTPUT
# Save verdict for signing
echo "$RESULT" > verdict.json
- name: Upload Verdict
uses: actions/upload-artifact@v4
with:
name: verdict
path: verdict.json
sign-verdict:
needs: evaluate
uses: ./.github/workflows/examples/stellaops-sign.yml
with:
artifact-digest: ${{ needs.evaluate.outputs.verdict-digest }}
artifact-type: verdict
predicate-type: 'verdict.stella/v1'
push-attestation: true
permissions:
id-token: write
contents: read
packages: write
gate:
needs: [evaluate, sign-verdict]
runs-on: ubuntu-latest
steps:
- name: Check Verdict
run: |
PASSED="${{ needs.evaluate.outputs.passed }}"
VERDICT="${{ needs.evaluate.outputs.verdict }}"
if [[ "$PASSED" != "true" ]]; then
echo "::error::Policy verdict: ${VERDICT}"
echo "::error::Deployment blocked by policy"
exit 1
fi
echo "Policy verdict: ${VERDICT} - Proceeding with deployment"
- name: Summary
run: |
PASSED="${{ needs.evaluate.outputs.passed }}"
if [[ "$PASSED" == "true" ]]; then
ICON="white_check_mark"
STATUS="PASSED"
else
ICON="x"
STATUS="BLOCKED"
fi
cat >> $GITHUB_STEP_SUMMARY << EOF
## :${ICON}: Policy Verdict: ${STATUS}
| Field | Value |
|-------|-------|
| **Image** | \`${{ needs.evaluate.outputs.image-digest }}\` |
| **Verdict** | \`${{ needs.evaluate.outputs.verdict }}\` |
| **Verdict Digest** | \`${{ needs.evaluate.outputs.verdict-digest }}\` |
| **Attestation** | \`${{ needs.sign-verdict.outputs.attestation-digest }}\` |
| **Rekor UUID** | \`${{ needs.sign-verdict.outputs.rekor-uuid }}\` |
### Verify Verdict
\`\`\`bash
stella attest verify \\
--artifact "${{ needs.evaluate.outputs.verdict-digest }}" \\
--certificate-identity "repo:${{ github.repository }}:ref:${{ github.ref }}" \\
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
\`\`\`
EOF
# Example deployment job - only runs if gate passes
deploy:
needs: [evaluate, gate]
if: needs.evaluate.outputs.passed == 'true'
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy
run: |
echo "Deploying ${{ needs.evaluate.outputs.image-digest }}"
echo "Policy verdict verified and signed"
# Add your deployment commands here

View File

@@ -0,0 +1,175 @@
# .github/workflows/examples/example-verification-gate.yml
# Example: Verification gate before deployment
#
# This example shows how to:
# 1. Verify all required attestations exist
# 2. Validate identity constraints
# 3. Block deployment on verification failure
#
# Use this pattern for:
# - Production deployment gates
# - Promotion between environments
# - Audit compliance checkpoints
name: Deployment Verification Gate
on:
workflow_dispatch:
inputs:
image:
description: 'Container image to deploy (with digest)'
required: true
type: string
environment:
description: 'Target environment'
required: true
type: choice
options:
- staging
- production
require-sbom:
description: 'Require SBOM attestation'
required: false
default: true
type: boolean
require-verdict:
description: 'Require passing policy verdict'
required: false
default: true
type: boolean
env:
# Identity patterns for trusted signers
TRUSTED_IDENTITY_STAGING: 'repo:${{ github.repository }}:ref:refs/heads/.*'
TRUSTED_IDENTITY_PRODUCTION: 'repo:${{ github.repository }}:ref:refs/heads/main|repo:${{ github.repository }}:ref:refs/tags/v.*'
TRUSTED_ISSUER: 'https://token.actions.githubusercontent.com'
jobs:
pre-flight:
runs-on: ubuntu-latest
outputs:
identity-pattern: ${{ steps.config.outputs.identity-pattern }}
steps:
- name: Configure Identity Constraints
id: config
run: |
ENV="${{ github.event.inputs.environment }}"
if [[ "$ENV" == "production" ]]; then
echo "identity-pattern=${TRUSTED_IDENTITY_PRODUCTION}" >> $GITHUB_OUTPUT
echo "Using production identity constraints"
else
echo "identity-pattern=${TRUSTED_IDENTITY_STAGING}" >> $GITHUB_OUTPUT
echo "Using staging identity constraints"
fi
verify-signature:
needs: pre-flight
uses: ./.github/workflows/examples/stellaops-verify.yml
with:
artifact-digest: ${{ github.event.inputs.image }}
certificate-identity: ${{ needs.pre-flight.outputs.identity-pattern }}
certificate-oidc-issuer: 'https://token.actions.githubusercontent.com'
require-rekor: true
require-sbom: ${{ github.event.inputs.require-sbom == 'true' }}
require-verdict: ${{ github.event.inputs.require-verdict == 'true' }}
strict: true
permissions:
contents: read
packages: read
verify-provenance:
needs: pre-flight
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
provenance-valid: ${{ steps.verify.outputs.valid }}
steps:
- name: Install StellaOps CLI
uses: stella-ops/setup-cli@v1
- name: Verify Build Provenance
id: verify
env:
STELLAOPS_URL: 'https://api.stella-ops.org'
run: |
set -euo pipefail
IMAGE="${{ github.event.inputs.image }}"
echo "::group::Verifying build provenance"
RESULT=$(stella provenance verify \
--artifact "${IMAGE}" \
--require-source-repo "${{ github.repository }}" \
--output json)
echo "$RESULT" | jq .
echo "::endgroup::"
VALID=$(echo "$RESULT" | jq -r '.valid')
echo "valid=${VALID}" >> $GITHUB_OUTPUT
if [[ "$VALID" != "true" ]]; then
echo "::error::Provenance verification failed"
exit 1
fi
audit-log:
needs: [verify-signature, verify-provenance]
runs-on: ubuntu-latest
steps:
- name: Install StellaOps CLI
uses: stella-ops/setup-cli@v1
- name: Create Audit Entry
env:
STELLAOPS_URL: 'https://api.stella-ops.org'
run: |
stella audit log \
--event "deployment-gate" \
--artifact "${{ github.event.inputs.image }}" \
--environment "${{ github.event.inputs.environment }}" \
--verified true \
--attestations "${{ needs.verify-signature.outputs.attestation-count }}" \
--actor "${{ github.actor }}" \
--workflow "${{ github.workflow }}" \
--run-id "${{ github.run_id }}"
deploy:
needs: [verify-signature, verify-provenance, audit-log]
runs-on: ubuntu-latest
environment: ${{ github.event.inputs.environment }}
steps:
- name: Deployment Approved
run: |
echo "All verifications passed"
echo "Image: ${{ github.event.inputs.image }}"
echo "Environment: ${{ github.event.inputs.environment }}"
echo ""
echo "Proceeding with deployment..."
# Add your deployment steps here
# - name: Deploy to Kubernetes
# run: kubectl set image deployment/app app=${{ github.event.inputs.image }}
- name: Summary
run: |
cat >> $GITHUB_STEP_SUMMARY << EOF
## Deployment Completed
| Field | Value |
|-------|-------|
| **Image** | \`${{ github.event.inputs.image }}\` |
| **Environment** | \`${{ github.event.inputs.environment }}\` |
| **Signature Verified** | ${{ needs.verify-signature.outputs.verified }} |
| **Provenance Verified** | ${{ needs.verify-provenance.outputs.provenance-valid }} |
| **Attestations** | ${{ needs.verify-signature.outputs.attestation-count }} |
| **Deployed By** | @${{ github.actor }} |
| **Workflow Run** | [#${{ github.run_id }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) |
EOF

View File

@@ -0,0 +1,216 @@
# .github/workflows/examples/stellaops-sign.yml
# StellaOps Keyless Sign Reusable Workflow
#
# This reusable workflow enables keyless signing of artifacts using Sigstore Fulcio.
# It uses OIDC identity tokens from GitHub Actions to obtain ephemeral signing certificates.
#
# Usage:
# jobs:
# sign:
# uses: stella-ops/templates/.github/workflows/stellaops-sign.yml@v1
# with:
# artifact-digest: sha256:abc123...
# artifact-type: image
# permissions:
# id-token: write
# contents: read
#
# Prerequisites:
# - StellaOps API accessible from runner
# - OIDC token permissions granted
#
# See: docs/modules/signer/guides/keyless-signing.md
name: StellaOps Keyless Sign
on:
workflow_call:
inputs:
artifact-digest:
description: 'SHA256 digest of artifact to sign (e.g., sha256:abc123...)'
required: true
type: string
artifact-type:
description: 'Type of artifact: image, sbom, verdict, report'
required: false
type: string
default: 'image'
stellaops-url:
description: 'StellaOps API URL'
required: false
type: string
default: 'https://api.stella-ops.org'
push-attestation:
description: 'Push attestation to OCI registry'
required: false
type: boolean
default: true
predicate-type:
description: 'Custom predicate type URI (optional)'
required: false
type: string
default: ''
include-rekor:
description: 'Log signature to Rekor transparency log'
required: false
type: boolean
default: true
cli-version:
description: 'StellaOps CLI version to use'
required: false
type: string
default: 'latest'
outputs:
attestation-digest:
description: 'Digest of created attestation'
value: ${{ jobs.sign.outputs.attestation-digest }}
rekor-uuid:
description: 'Rekor transparency log UUID (if logged)'
value: ${{ jobs.sign.outputs.rekor-uuid }}
certificate-identity:
description: 'OIDC identity bound to certificate'
value: ${{ jobs.sign.outputs.certificate-identity }}
signed-at:
description: 'Signing timestamp (UTC ISO-8601)'
value: ${{ jobs.sign.outputs.signed-at }}
jobs:
sign:
runs-on: ubuntu-latest
permissions:
id-token: write # Required for OIDC token
contents: read # Required for checkout
packages: write # Required if pushing to GHCR
outputs:
attestation-digest: ${{ steps.sign.outputs.attestation-digest }}
rekor-uuid: ${{ steps.sign.outputs.rekor-uuid }}
certificate-identity: ${{ steps.sign.outputs.certificate-identity }}
signed-at: ${{ steps.sign.outputs.signed-at }}
steps:
- name: Validate Inputs
run: |
if [[ ! "${{ inputs.artifact-digest }}" =~ ^sha256:[a-f0-9]{64}$ ]] && \
[[ ! "${{ inputs.artifact-digest }}" =~ ^sha512:[a-f0-9]{128}$ ]]; then
echo "::error::Invalid artifact-digest format. Expected sha256:... or sha512:..."
exit 1
fi
VALID_TYPES="image sbom verdict report binary"
if [[ ! " $VALID_TYPES " =~ " ${{ inputs.artifact-type }} " ]]; then
echo "::error::Invalid artifact-type. Must be one of: $VALID_TYPES"
exit 1
fi
- name: Install StellaOps CLI
uses: stella-ops/setup-cli@v1
with:
version: ${{ inputs.cli-version }}
- name: Get OIDC Token
id: oidc
run: |
set -euo pipefail
# Request OIDC token with sigstore audience
OIDC_TOKEN=$(curl -sLS "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sigstore" \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
| jq -r '.value')
if [[ -z "$OIDC_TOKEN" || "$OIDC_TOKEN" == "null" ]]; then
echo "::error::Failed to obtain OIDC token"
exit 1
fi
# Mask token in logs
echo "::add-mask::${OIDC_TOKEN}"
echo "token=${OIDC_TOKEN}" >> $GITHUB_OUTPUT
# Extract identity for logging (non-sensitive)
IDENTITY=$(echo "$OIDC_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq -r '.sub // "unknown"' 2>/dev/null || echo "unknown")
echo "identity=${IDENTITY}" >> $GITHUB_OUTPUT
- name: Keyless Sign
id: sign
env:
STELLAOPS_OIDC_TOKEN: ${{ steps.oidc.outputs.token }}
STELLAOPS_URL: ${{ inputs.stellaops-url }}
run: |
set -euo pipefail
SIGN_ARGS=(
--keyless
--artifact "${{ inputs.artifact-digest }}"
--type "${{ inputs.artifact-type }}"
--output json
)
# Add optional predicate type
if [[ -n "${{ inputs.predicate-type }}" ]]; then
SIGN_ARGS+=(--predicate-type "${{ inputs.predicate-type }}")
fi
# Add Rekor logging option
if [[ "${{ inputs.include-rekor }}" == "true" ]]; then
SIGN_ARGS+=(--rekor)
fi
echo "::group::Signing artifact"
RESULT=$(stella attest sign "${SIGN_ARGS[@]}")
echo "$RESULT" | jq .
echo "::endgroup::"
# Extract outputs
ATTESTATION_DIGEST=$(echo "$RESULT" | jq -r '.attestationDigest // empty')
REKOR_UUID=$(echo "$RESULT" | jq -r '.rekorUuid // empty')
CERT_IDENTITY=$(echo "$RESULT" | jq -r '.certificateIdentity // empty')
SIGNED_AT=$(echo "$RESULT" | jq -r '.signedAt // empty')
if [[ -z "$ATTESTATION_DIGEST" ]]; then
echo "::error::Signing failed - no attestation digest returned"
exit 1
fi
echo "attestation-digest=${ATTESTATION_DIGEST}" >> $GITHUB_OUTPUT
echo "rekor-uuid=${REKOR_UUID}" >> $GITHUB_OUTPUT
echo "certificate-identity=${CERT_IDENTITY}" >> $GITHUB_OUTPUT
echo "signed-at=${SIGNED_AT}" >> $GITHUB_OUTPUT
- name: Push Attestation
if: ${{ inputs.push-attestation }}
env:
STELLAOPS_URL: ${{ inputs.stellaops-url }}
run: |
set -euo pipefail
echo "::group::Pushing attestation to registry"
stella attest push \
--attestation "${{ steps.sign.outputs.attestation-digest }}" \
--registry "${{ github.repository }}"
echo "::endgroup::"
- name: Generate Summary
run: |
cat >> $GITHUB_STEP_SUMMARY << 'EOF'
## Attestation Created
| Field | Value |
|-------|-------|
| **Artifact** | `${{ inputs.artifact-digest }}` |
| **Type** | `${{ inputs.artifact-type }}` |
| **Attestation** | `${{ steps.sign.outputs.attestation-digest }}` |
| **Rekor UUID** | `${{ steps.sign.outputs.rekor-uuid || 'N/A' }}` |
| **Certificate Identity** | `${{ steps.sign.outputs.certificate-identity }}` |
| **Signed At** | `${{ steps.sign.outputs.signed-at }}` |
| **Signing Mode** | Keyless (Fulcio) |
### Verification Command
```bash
stella attest verify \
--artifact "${{ inputs.artifact-digest }}" \
--certificate-identity "${{ steps.sign.outputs.certificate-identity }}" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com"
```
EOF

View File

@@ -0,0 +1,219 @@
# .github/workflows/examples/stellaops-verify.yml
# StellaOps Verification Gate Reusable Workflow
#
# This reusable workflow verifies attestations before deployment.
# Use it as a gate in your CI/CD pipeline to ensure only properly
# signed artifacts are deployed.
#
# Usage:
# jobs:
# verify:
# uses: stella-ops/templates/.github/workflows/stellaops-verify.yml@v1
# with:
# artifact-digest: sha256:abc123...
# certificate-identity: 'repo:myorg/myrepo:ref:refs/heads/main'
# certificate-oidc-issuer: 'https://token.actions.githubusercontent.com'
#
# See: docs/modules/signer/guides/keyless-signing.md
name: StellaOps Verify Gate
on:
workflow_call:
inputs:
artifact-digest:
description: 'SHA256 digest of artifact to verify'
required: true
type: string
stellaops-url:
description: 'StellaOps API URL'
required: false
type: string
default: 'https://api.stella-ops.org'
certificate-identity:
description: 'Expected OIDC identity pattern (supports regex)'
required: true
type: string
certificate-oidc-issuer:
description: 'Expected OIDC issuer URL'
required: true
type: string
require-rekor:
description: 'Require Rekor transparency log inclusion proof'
required: false
type: boolean
default: true
strict:
description: 'Fail workflow on any verification issue'
required: false
type: boolean
default: true
max-cert-age-hours:
description: 'Maximum age of signing certificate in hours (0 = no limit)'
required: false
type: number
default: 0
require-sbom:
description: 'Require SBOM attestation'
required: false
type: boolean
default: false
require-verdict:
description: 'Require passing policy verdict attestation'
required: false
type: boolean
default: false
cli-version:
description: 'StellaOps CLI version to use'
required: false
type: string
default: 'latest'
outputs:
verified:
description: 'Whether all verifications passed'
value: ${{ jobs.verify.outputs.verified }}
attestation-count:
description: 'Number of attestations found'
value: ${{ jobs.verify.outputs.attestation-count }}
verification-details:
description: 'JSON details of verification results'
value: ${{ jobs.verify.outputs.verification-details }}
jobs:
verify:
runs-on: ubuntu-latest
permissions:
contents: read
packages: read
outputs:
verified: ${{ steps.verify.outputs.verified }}
attestation-count: ${{ steps.verify.outputs.attestation-count }}
verification-details: ${{ steps.verify.outputs.verification-details }}
steps:
- name: Validate Inputs
run: |
if [[ ! "${{ inputs.artifact-digest }}" =~ ^sha256:[a-f0-9]{64}$ ]] && \
[[ ! "${{ inputs.artifact-digest }}" =~ ^sha512:[a-f0-9]{128}$ ]]; then
echo "::error::Invalid artifact-digest format. Expected sha256:... or sha512:..."
exit 1
fi
if [[ -z "${{ inputs.certificate-identity }}" ]]; then
echo "::error::certificate-identity is required"
exit 1
fi
if [[ -z "${{ inputs.certificate-oidc-issuer }}" ]]; then
echo "::error::certificate-oidc-issuer is required"
exit 1
fi
- name: Install StellaOps CLI
uses: stella-ops/setup-cli@v1
with:
version: ${{ inputs.cli-version }}
- name: Verify Attestation
id: verify
env:
STELLAOPS_URL: ${{ inputs.stellaops-url }}
run: |
set +e # Don't exit on error - we handle it
VERIFY_ARGS=(
--artifact "${{ inputs.artifact-digest }}"
--certificate-identity "${{ inputs.certificate-identity }}"
--certificate-oidc-issuer "${{ inputs.certificate-oidc-issuer }}"
--output json
)
# Add optional flags
if [[ "${{ inputs.require-rekor }}" == "true" ]]; then
VERIFY_ARGS+=(--require-rekor)
fi
if [[ "${{ inputs.max-cert-age-hours }}" -gt 0 ]]; then
VERIFY_ARGS+=(--max-cert-age-hours "${{ inputs.max-cert-age-hours }}")
fi
if [[ "${{ inputs.require-sbom }}" == "true" ]]; then
VERIFY_ARGS+=(--require-sbom)
fi
if [[ "${{ inputs.require-verdict }}" == "true" ]]; then
VERIFY_ARGS+=(--require-verdict)
fi
echo "::group::Verifying attestations"
RESULT=$(stella attest verify "${VERIFY_ARGS[@]}" 2>&1)
EXIT_CODE=$?
echo "$RESULT" | jq . 2>/dev/null || echo "$RESULT"
echo "::endgroup::"
set -e
# Parse results
VERIFIED=$(echo "$RESULT" | jq -r '.valid // false')
ATTESTATION_COUNT=$(echo "$RESULT" | jq -r '.attestationCount // 0')
echo "verified=${VERIFIED}" >> $GITHUB_OUTPUT
echo "attestation-count=${ATTESTATION_COUNT}" >> $GITHUB_OUTPUT
echo "verification-details=$(echo "$RESULT" | jq -c '.')" >> $GITHUB_OUTPUT
# Handle verification failure
if [[ "$VERIFIED" != "true" ]]; then
echo "::warning::Verification failed"
# Extract and report issues
ISSUES=$(echo "$RESULT" | jq -r '.issues[]? | "\(.code): \(.message)"' 2>/dev/null)
if [[ -n "$ISSUES" ]]; then
while IFS= read -r issue; do
echo "::error::$issue"
done <<< "$ISSUES"
fi
if [[ "${{ inputs.strict }}" == "true" ]]; then
echo "::error::Verification failed in strict mode"
exit 1
fi
fi
- name: Generate Summary
if: always()
run: |
VERIFIED="${{ steps.verify.outputs.verified }}"
if [[ "$VERIFIED" == "true" ]]; then
ICON="white_check_mark"
STATUS="Passed"
else
ICON="x"
STATUS="Failed"
fi
cat >> $GITHUB_STEP_SUMMARY << EOF
## :${ICON}: Verification ${STATUS}
| Field | Value |
|-------|-------|
| **Artifact** | \`${{ inputs.artifact-digest }}\` |
| **Expected Identity** | \`${{ inputs.certificate-identity }}\` |
| **Expected Issuer** | \`${{ inputs.certificate-oidc-issuer }}\` |
| **Attestations Found** | ${{ steps.verify.outputs.attestation-count }} |
| **Rekor Required** | ${{ inputs.require-rekor }} |
| **Strict Mode** | ${{ inputs.strict }} |
EOF
# Add issues if any
DETAILS='${{ steps.verify.outputs.verification-details }}'
ISSUES=$(echo "$DETAILS" | jq -r '.issues[]? | "- **\(.code)**: \(.message)"' 2>/dev/null)
if [[ -n "$ISSUES" ]]; then
cat >> $GITHUB_STEP_SUMMARY << EOF
### Issues
$ISSUES
EOF
fi