- 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.
220 lines
7.1 KiB
YAML
220 lines
7.1 KiB
YAML
# .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
|