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:
145
.github/workflows/examples/example-container-sign.yml
vendored
Normal file
145
.github/workflows/examples/example-container-sign.yml
vendored
Normal 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
|
||||
184
.github/workflows/examples/example-sbom-sign.yml
vendored
Normal file
184
.github/workflows/examples/example-sbom-sign.yml
vendored
Normal 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
|
||||
191
.github/workflows/examples/example-verdict-sign.yml
vendored
Normal file
191
.github/workflows/examples/example-verdict-sign.yml
vendored
Normal 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
|
||||
175
.github/workflows/examples/example-verification-gate.yml
vendored
Normal file
175
.github/workflows/examples/example-verification-gate.yml
vendored
Normal 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
|
||||
216
.github/workflows/examples/stellaops-sign.yml
vendored
Normal file
216
.github/workflows/examples/stellaops-sign.yml
vendored
Normal 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
|
||||
219
.github/workflows/examples/stellaops-verify.yml
vendored
Normal file
219
.github/workflows/examples/stellaops-verify.yml
vendored
Normal 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
|
||||
232
.github/workflows/stellaops-gate-example.yml
vendored
Normal file
232
.github/workflows/stellaops-gate-example.yml
vendored
Normal file
@@ -0,0 +1,232 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# stellaops-gate-example.yml
|
||||
# Sprint: SPRINT_20251226_001_BE_cicd_gate_integration
|
||||
# Task: CICD-GATE-07 - GitHub Actions example workflow using stella gate evaluate
|
||||
# Description: Example workflow demonstrating StellaOps release gate integration
|
||||
# -----------------------------------------------------------------------------
|
||||
#
|
||||
# This workflow demonstrates how to integrate StellaOps release gates into your
|
||||
# GitHub Actions CI/CD pipeline. The gate evaluates security drift between your
|
||||
# current build and the approved baseline, blocking releases that introduce new
|
||||
# reachable vulnerabilities.
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. StellaOps CLI installed (see setup step below)
|
||||
# 2. STELLAOPS_API_TOKEN secret configured
|
||||
# 3. Container image built and pushed to registry
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 = Pass - Release may proceed
|
||||
# 1 = Warn - Release may proceed with warnings (configurable)
|
||||
# 2 = Fail - Release blocked due to security policy violation
|
||||
#
|
||||
name: StellaOps Release Gate Example
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, release/*]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
IMAGE_NAME: ${{ github.repository }}
|
||||
STELLAOPS_BACKEND_URL: ${{ vars.STELLAOPS_BACKEND_URL || 'https://stellaops.internal' }}
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: Build Container Image
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image_digest: ${{ steps.build.outputs.digest }}
|
||||
image_ref: ${{ steps.build.outputs.image_ref }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to Container Registry
|
||||
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=sha,prefix=
|
||||
type=ref,event=branch
|
||||
type=ref,event=pr
|
||||
|
||||
- name: Build and push
|
||||
id: build
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Output image reference
|
||||
id: output
|
||||
run: |
|
||||
echo "digest=${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
|
||||
echo "image_ref=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}@${{ steps.build.outputs.digest }}" >> $GITHUB_OUTPUT
|
||||
|
||||
gate:
|
||||
name: StellaOps Release Gate
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Continue on gate failure to allow override workflow
|
||||
continue-on-error: ${{ github.event_name == 'pull_request' }}
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
id-token: write # Required for OIDC token acquisition
|
||||
|
||||
outputs:
|
||||
gate_status: ${{ steps.gate.outputs.status }}
|
||||
gate_decision_id: ${{ steps.gate.outputs.decision_id }}
|
||||
|
||||
steps:
|
||||
- name: Install StellaOps CLI
|
||||
run: |
|
||||
# Download and install the StellaOps CLI
|
||||
curl -sSL https://get.stella-ops.org/cli | bash
|
||||
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Acquire OIDC Token (Keyless)
|
||||
id: oidc
|
||||
if: ${{ vars.STELLAOPS_USE_KEYLESS == 'true' }}
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
script: |
|
||||
const token = await core.getIDToken('stellaops');
|
||||
core.setSecret(token);
|
||||
core.setOutput('token', token);
|
||||
|
||||
- name: Evaluate Release Gate
|
||||
id: gate
|
||||
env:
|
||||
STELLAOPS_API_TOKEN: ${{ secrets.STELLAOPS_API_TOKEN }}
|
||||
STELLAOPS_OIDC_TOKEN: ${{ steps.oidc.outputs.token }}
|
||||
run: |
|
||||
# Determine baseline strategy based on branch
|
||||
if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
|
||||
BASELINE="production"
|
||||
elif [[ "${{ github.ref }}" == refs/heads/release/* ]]; then
|
||||
BASELINE="last-approved"
|
||||
else
|
||||
BASELINE="previous-build"
|
||||
fi
|
||||
|
||||
echo "Evaluating gate for image: ${{ needs.build.outputs.image_digest }}"
|
||||
echo "Baseline strategy: ${BASELINE}"
|
||||
|
||||
# Run gate evaluation
|
||||
# --output json provides machine-readable output
|
||||
# --ci-context identifies the CI system for audit logging
|
||||
RESULT=$(stella gate evaluate \
|
||||
--image "${{ needs.build.outputs.image_digest }}" \
|
||||
--baseline "${BASELINE}" \
|
||||
--output json \
|
||||
--ci-context "github-actions" \
|
||||
--repository "${{ github.repository }}" \
|
||||
--tag "${{ github.sha }}" \
|
||||
2>&1) || EXIT_CODE=$?
|
||||
|
||||
EXIT_CODE=${EXIT_CODE:-0}
|
||||
|
||||
# Parse JSON output for decision details
|
||||
DECISION_ID=$(echo "$RESULT" | jq -r '.decisionId // "unknown"')
|
||||
STATUS=$(echo "$RESULT" | jq -r '.status // "unknown"')
|
||||
SUMMARY=$(echo "$RESULT" | jq -r '.summary // "No summary available"')
|
||||
|
||||
echo "decision_id=${DECISION_ID}" >> $GITHUB_OUTPUT
|
||||
echo "status=${STATUS}" >> $GITHUB_OUTPUT
|
||||
echo "exit_code=${EXIT_CODE}" >> $GITHUB_OUTPUT
|
||||
|
||||
# Create summary
|
||||
echo "## StellaOps Gate Evaluation" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Decision ID | \`${DECISION_ID}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Status | **${STATUS}** |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Image | \`${{ needs.build.outputs.image_digest }}\` |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Baseline | ${BASELINE} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "" >> $GITHUB_STEP_SUMMARY
|
||||
echo "### Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "${SUMMARY}" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# Exit with the gate's exit code
|
||||
exit ${EXIT_CODE}
|
||||
|
||||
- name: Gate Status Badge
|
||||
if: always()
|
||||
run: |
|
||||
case "${{ steps.gate.outputs.status }}" in
|
||||
Pass)
|
||||
echo "::notice::Gate PASSED - Release may proceed"
|
||||
;;
|
||||
Warn)
|
||||
echo "::warning::Gate PASSED WITH WARNINGS - Review recommended"
|
||||
;;
|
||||
Fail)
|
||||
echo "::error::Gate BLOCKED - Security policy violation detected"
|
||||
;;
|
||||
esac
|
||||
|
||||
deploy:
|
||||
name: Deploy to Staging
|
||||
needs: [build, gate]
|
||||
if: ${{ needs.gate.outputs.gate_status == 'Pass' || needs.gate.outputs.gate_status == 'Warn' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
|
||||
steps:
|
||||
- name: Deploy to staging
|
||||
run: |
|
||||
echo "Deploying ${{ needs.build.outputs.image_ref }} to staging..."
|
||||
# Add your deployment commands here
|
||||
|
||||
# Optional: Manual override for blocked releases (requires elevated permissions)
|
||||
override:
|
||||
name: Request Gate Override
|
||||
needs: [build, gate]
|
||||
if: ${{ failure() && needs.gate.outputs.gate_status == 'Fail' }}
|
||||
runs-on: ubuntu-latest
|
||||
environment: security-override # Requires manual approval
|
||||
|
||||
steps:
|
||||
- name: Install StellaOps CLI
|
||||
run: |
|
||||
curl -sSL https://get.stella-ops.org/cli | bash
|
||||
echo "$HOME/.stellaops/bin" >> $GITHUB_PATH
|
||||
|
||||
- name: Request Override with Justification
|
||||
env:
|
||||
STELLAOPS_API_TOKEN: ${{ secrets.STELLAOPS_OVERRIDE_TOKEN }}
|
||||
run: |
|
||||
# This requires the security-override environment approval
|
||||
# and a separate token with override permissions
|
||||
stella gate evaluate \
|
||||
--image "${{ needs.build.outputs.image_digest }}" \
|
||||
--baseline "last-approved" \
|
||||
--allow-override \
|
||||
--justification "Emergency release approved by ${{ github.actor }} - see PR #${{ github.event.pull_request.number }}" \
|
||||
--ci-context "github-actions-override"
|
||||
Reference in New Issue
Block a user