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:
126
deploy/gitlab/README.md
Normal file
126
deploy/gitlab/README.md
Normal file
@@ -0,0 +1,126 @@
|
||||
# StellaOps GitLab CI Templates
|
||||
|
||||
Production-ready GitLab CI templates for keyless signing integration with StellaOps.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Include the templates in your `.gitlab-ci.yml`:
|
||||
|
||||
```yaml
|
||||
include:
|
||||
- project: 'stella-ops/templates'
|
||||
file: 'deploy/gitlab/examples/.gitlab-ci-stellaops.yml'
|
||||
|
||||
sign-my-image:
|
||||
extends: .stellaops-sign
|
||||
variables:
|
||||
ARTIFACT_DIGEST: $IMAGE_DIGEST
|
||||
ARTIFACT_TYPE: image
|
||||
```
|
||||
|
||||
## Available Templates
|
||||
|
||||
### `.stellaops-sign`
|
||||
|
||||
Signs artifacts using keyless signing with Fulcio certificates.
|
||||
|
||||
**Variables:**
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `ARTIFACT_DIGEST` | Yes | - | SHA256 digest of artifact to sign |
|
||||
| `ARTIFACT_TYPE` | No | `image` | Type: image, sbom, verdict, report |
|
||||
| `INCLUDE_REKOR` | No | `true` | Log to Rekor transparency log |
|
||||
| `PUSH_ATTESTATION` | No | `true` | Push attestation to registry |
|
||||
|
||||
**Outputs (dotenv):**
|
||||
- `ATTESTATION_DIGEST`: Digest of created attestation
|
||||
- `REKOR_UUID`: Rekor transparency log UUID
|
||||
- `CERTIFICATE_IDENTITY`: OIDC identity from certificate
|
||||
|
||||
### `.stellaops-verify`
|
||||
|
||||
Verifies attestations before deployment.
|
||||
|
||||
**Variables:**
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `ARTIFACT_DIGEST` | Yes | - | SHA256 digest to verify |
|
||||
| `CERTIFICATE_IDENTITY` | Yes | - | Expected identity pattern (regex) |
|
||||
| `CERTIFICATE_OIDC_ISSUER` | No | `https://gitlab.com` | Expected OIDC issuer |
|
||||
| `REQUIRE_REKOR` | No | `true` | Require Rekor proof |
|
||||
| `STRICT` | No | `true` | Fail on any issue |
|
||||
|
||||
**Outputs (dotenv):**
|
||||
- `VERIFIED`: Whether verification passed
|
||||
- `ATTESTATION_COUNT`: Number of attestations found
|
||||
|
||||
### `.stellaops-sbom`
|
||||
|
||||
Generates, signs, and attaches SBOM to image.
|
||||
|
||||
**Variables:**
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `IMAGE` | Yes | - | Image to generate SBOM for |
|
||||
| `SBOM_FORMAT` | No | `cyclonedx-json` | SBOM format |
|
||||
| `SBOM_OUTPUT` | No | `sbom.json` | Output filename |
|
||||
|
||||
### `.stellaops-verdict`
|
||||
|
||||
Evaluates policy and signs the verdict.
|
||||
|
||||
**Variables:**
|
||||
| Variable | Required | Default | Description |
|
||||
|----------|----------|---------|-------------|
|
||||
| `IMAGE` | Yes | - | Image to evaluate |
|
||||
| `POLICY` | No | `default` | Policy pack ID |
|
||||
| `FAIL_ON_BLOCK` | No | `true` | Fail job if blocked |
|
||||
|
||||
## Identity Patterns for GitLab
|
||||
|
||||
When verifying, use these identity patterns:
|
||||
|
||||
| Constraint | Pattern |
|
||||
|------------|---------|
|
||||
| Any ref in project | `project_path:<group>/<project>:.*` |
|
||||
| Main branch only | `project_path:<group>/<project>:ref_type:branch:ref:main` |
|
||||
| Protected refs | `project_path:<group>/<project>:ref_protected:true` |
|
||||
| Tags | `project_path:<group>/<project>:ref_type:tag:ref:.*` |
|
||||
|
||||
**OIDC Issuer:** Use `${CI_SERVER_URL}` for self-hosted GitLab, or `https://gitlab.com` for GitLab.com.
|
||||
|
||||
## Example Pipeline
|
||||
|
||||
See `examples/example-pipeline.gitlab-ci.yml` for a complete pipeline example.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### OIDC Token Not Available
|
||||
|
||||
Ensure your job has `id_tokens` configured:
|
||||
|
||||
```yaml
|
||||
my-job:
|
||||
id_tokens:
|
||||
STELLAOPS_OIDC_TOKEN:
|
||||
aud: sigstore
|
||||
```
|
||||
|
||||
### Permission Denied
|
||||
|
||||
Check that:
|
||||
1. The project has OIDC enabled (Settings > CI/CD > Token Access)
|
||||
2. Protected branch/tag settings if using protected pipelines
|
||||
|
||||
### Verification Fails
|
||||
|
||||
Common issues:
|
||||
- Identity pattern doesn't match (check `ref_type` and `ref`)
|
||||
- Wrong issuer (use `${CI_SERVER_URL}` for self-hosted)
|
||||
- Signature was created by different branch/tag
|
||||
|
||||
## Resources
|
||||
|
||||
- [Keyless Signing Guide](../../docs/modules/signer/guides/keyless-signing.md)
|
||||
- [Identity Constraints](../../docs/guides/identity-constraints.md)
|
||||
- [GitLab OIDC Documentation](https://docs.gitlab.com/ee/ci/secrets/id_token_authentication.html)
|
||||
305
deploy/gitlab/examples/.gitlab-ci-stellaops.yml
Normal file
305
deploy/gitlab/examples/.gitlab-ci-stellaops.yml
Normal file
@@ -0,0 +1,305 @@
|
||||
# deploy/gitlab/examples/.gitlab-ci-stellaops.yml
|
||||
# StellaOps Keyless Signing Templates for GitLab CI
|
||||
#
|
||||
# Include this file in your .gitlab-ci.yml to enable keyless signing:
|
||||
#
|
||||
# include:
|
||||
# - project: 'stella-ops/templates'
|
||||
# file: 'deploy/gitlab/examples/.gitlab-ci-stellaops.yml'
|
||||
#
|
||||
# sign-image:
|
||||
# extends: .stellaops-sign
|
||||
# variables:
|
||||
# ARTIFACT_DIGEST: $CI_REGISTRY_IMAGE@sha256:...
|
||||
# ARTIFACT_TYPE: image
|
||||
#
|
||||
# See: docs/modules/signer/guides/keyless-signing.md
|
||||
|
||||
# ==============================================================================
|
||||
# Base Configuration
|
||||
# ==============================================================================
|
||||
|
||||
variables:
|
||||
STELLAOPS_URL: "https://api.stella-ops.org"
|
||||
STELLAOPS_CLI_VERSION: "latest"
|
||||
|
||||
# ==============================================================================
|
||||
# Keyless Signing Job Template
|
||||
# ==============================================================================
|
||||
|
||||
.stellaops-sign:
|
||||
image: stella-ops/cli:${STELLAOPS_CLI_VERSION}
|
||||
id_tokens:
|
||||
STELLAOPS_OIDC_TOKEN:
|
||||
aud: sigstore
|
||||
variables:
|
||||
# Required - must be set by extending job
|
||||
ARTIFACT_DIGEST: ""
|
||||
# Optional - defaults to 'image'
|
||||
ARTIFACT_TYPE: "image"
|
||||
# Optional - include in Rekor transparency log
|
||||
INCLUDE_REKOR: "true"
|
||||
# Optional - push attestation to registry
|
||||
PUSH_ATTESTATION: "true"
|
||||
before_script:
|
||||
- |
|
||||
if [[ -z "${ARTIFACT_DIGEST}" ]]; then
|
||||
echo "ERROR: ARTIFACT_DIGEST must be set"
|
||||
exit 1
|
||||
fi
|
||||
script:
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
SIGN_ARGS=(
|
||||
--keyless
|
||||
--artifact "${ARTIFACT_DIGEST}"
|
||||
--type "${ARTIFACT_TYPE}"
|
||||
--output json
|
||||
)
|
||||
|
||||
if [[ "${INCLUDE_REKOR}" == "true" ]]; then
|
||||
SIGN_ARGS+=(--rekor)
|
||||
fi
|
||||
|
||||
echo "Signing artifact: ${ARTIFACT_DIGEST}"
|
||||
RESULT=$(stella attest sign "${SIGN_ARGS[@]}")
|
||||
|
||||
# Extract outputs for downstream jobs
|
||||
ATTESTATION_DIGEST=$(echo "$RESULT" | jq -r '.attestationDigest')
|
||||
REKOR_UUID=$(echo "$RESULT" | jq -r '.rekorUuid // empty')
|
||||
CERT_IDENTITY=$(echo "$RESULT" | jq -r '.certificateIdentity // empty')
|
||||
|
||||
echo "ATTESTATION_DIGEST=${ATTESTATION_DIGEST}" >> sign.env
|
||||
echo "REKOR_UUID=${REKOR_UUID}" >> sign.env
|
||||
echo "CERTIFICATE_IDENTITY=${CERT_IDENTITY}" >> sign.env
|
||||
|
||||
echo "Attestation created: ${ATTESTATION_DIGEST}"
|
||||
if [[ -n "${REKOR_UUID}" ]]; then
|
||||
echo "Rekor UUID: ${REKOR_UUID}"
|
||||
fi
|
||||
|
||||
# Push attestation if requested
|
||||
if [[ "${PUSH_ATTESTATION}" == "true" ]]; then
|
||||
echo "Pushing attestation to registry..."
|
||||
stella attest push \
|
||||
--attestation "${ATTESTATION_DIGEST}" \
|
||||
--registry "${CI_REGISTRY_IMAGE}"
|
||||
fi
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: sign.env
|
||||
|
||||
# ==============================================================================
|
||||
# Verification Job Template
|
||||
# ==============================================================================
|
||||
|
||||
.stellaops-verify:
|
||||
image: stella-ops/cli:${STELLAOPS_CLI_VERSION}
|
||||
variables:
|
||||
# Required - must be set by extending job
|
||||
ARTIFACT_DIGEST: ""
|
||||
CERTIFICATE_IDENTITY: ""
|
||||
CERTIFICATE_OIDC_ISSUER: "https://gitlab.com"
|
||||
# Optional - verification settings
|
||||
REQUIRE_REKOR: "true"
|
||||
STRICT: "true"
|
||||
REQUIRE_SBOM: "false"
|
||||
REQUIRE_VERDICT: "false"
|
||||
before_script:
|
||||
- |
|
||||
if [[ -z "${ARTIFACT_DIGEST}" ]]; then
|
||||
echo "ERROR: ARTIFACT_DIGEST must be set"
|
||||
exit 1
|
||||
fi
|
||||
if [[ -z "${CERTIFICATE_IDENTITY}" ]]; then
|
||||
echo "ERROR: CERTIFICATE_IDENTITY must be set"
|
||||
exit 1
|
||||
fi
|
||||
script:
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
VERIFY_ARGS=(
|
||||
--artifact "${ARTIFACT_DIGEST}"
|
||||
--certificate-identity "${CERTIFICATE_IDENTITY}"
|
||||
--certificate-oidc-issuer "${CERTIFICATE_OIDC_ISSUER}"
|
||||
--output json
|
||||
)
|
||||
|
||||
if [[ "${REQUIRE_REKOR}" == "true" ]]; then
|
||||
VERIFY_ARGS+=(--require-rekor)
|
||||
fi
|
||||
|
||||
if [[ "${REQUIRE_SBOM}" == "true" ]]; then
|
||||
VERIFY_ARGS+=(--require-sbom)
|
||||
fi
|
||||
|
||||
if [[ "${REQUIRE_VERDICT}" == "true" ]]; then
|
||||
VERIFY_ARGS+=(--require-verdict)
|
||||
fi
|
||||
|
||||
echo "Verifying artifact: ${ARTIFACT_DIGEST}"
|
||||
echo "Expected identity: ${CERTIFICATE_IDENTITY}"
|
||||
|
||||
set +e
|
||||
RESULT=$(stella attest verify "${VERIFY_ARGS[@]}" 2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
VERIFIED=$(echo "$RESULT" | jq -r '.valid // false')
|
||||
ATTESTATION_COUNT=$(echo "$RESULT" | jq -r '.attestationCount // 0')
|
||||
|
||||
echo "VERIFIED=${VERIFIED}" >> verify.env
|
||||
echo "ATTESTATION_COUNT=${ATTESTATION_COUNT}" >> verify.env
|
||||
|
||||
echo "Verified: ${VERIFIED}"
|
||||
echo "Attestations found: ${ATTESTATION_COUNT}"
|
||||
|
||||
if [[ "$VERIFIED" != "true" ]]; then
|
||||
echo "Verification issues:"
|
||||
echo "$RESULT" | jq -r '.issues[]? | " - \(.code): \(.message)"'
|
||||
|
||||
if [[ "${STRICT}" == "true" ]]; then
|
||||
echo "ERROR: Verification failed in strict mode"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: verify.env
|
||||
|
||||
# ==============================================================================
|
||||
# SBOM Generation and Signing Template
|
||||
# ==============================================================================
|
||||
|
||||
.stellaops-sbom:
|
||||
image: stella-ops/cli:${STELLAOPS_CLI_VERSION}
|
||||
id_tokens:
|
||||
STELLAOPS_OIDC_TOKEN:
|
||||
aud: sigstore
|
||||
variables:
|
||||
# Required - image to generate SBOM for
|
||||
IMAGE: ""
|
||||
# Optional - SBOM format
|
||||
SBOM_FORMAT: "cyclonedx-json"
|
||||
# Optional - output file
|
||||
SBOM_OUTPUT: "sbom.json"
|
||||
before_script:
|
||||
- |
|
||||
if [[ -z "${IMAGE}" ]]; then
|
||||
echo "ERROR: IMAGE must be set"
|
||||
exit 1
|
||||
fi
|
||||
script:
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Generating SBOM for: ${IMAGE}"
|
||||
|
||||
# Generate SBOM
|
||||
stella sbom generate \
|
||||
--image "${IMAGE}" \
|
||||
--format "${SBOM_FORMAT}" \
|
||||
--output "${SBOM_OUTPUT}"
|
||||
|
||||
# Calculate digest
|
||||
SBOM_DIGEST="sha256:$(sha256sum "${SBOM_OUTPUT}" | cut -d' ' -f1)"
|
||||
echo "SBOM digest: ${SBOM_DIGEST}"
|
||||
|
||||
# Sign SBOM
|
||||
echo "Signing SBOM..."
|
||||
RESULT=$(stella attest sign \
|
||||
--keyless \
|
||||
--artifact "${SBOM_DIGEST}" \
|
||||
--type sbom \
|
||||
--rekor \
|
||||
--output json)
|
||||
|
||||
ATTESTATION_DIGEST=$(echo "$RESULT" | jq -r '.attestationDigest')
|
||||
REKOR_UUID=$(echo "$RESULT" | jq -r '.rekorUuid // empty')
|
||||
|
||||
echo "SBOM_DIGEST=${SBOM_DIGEST}" >> sbom.env
|
||||
echo "SBOM_ATTESTATION_DIGEST=${ATTESTATION_DIGEST}" >> sbom.env
|
||||
echo "SBOM_REKOR_UUID=${REKOR_UUID}" >> sbom.env
|
||||
|
||||
# Attach to image
|
||||
echo "Attaching SBOM to image..."
|
||||
stella attest attach \
|
||||
--image "${IMAGE}" \
|
||||
--attestation "${ATTESTATION_DIGEST}" \
|
||||
--type sbom
|
||||
|
||||
echo "SBOM signed and attached successfully"
|
||||
artifacts:
|
||||
paths:
|
||||
- ${SBOM_OUTPUT}
|
||||
reports:
|
||||
dotenv: sbom.env
|
||||
|
||||
# ==============================================================================
|
||||
# Policy Verdict Template
|
||||
# ==============================================================================
|
||||
|
||||
.stellaops-verdict:
|
||||
image: stella-ops/cli:${STELLAOPS_CLI_VERSION}
|
||||
id_tokens:
|
||||
STELLAOPS_OIDC_TOKEN:
|
||||
aud: sigstore
|
||||
variables:
|
||||
# Required - image to evaluate
|
||||
IMAGE: ""
|
||||
# Optional - policy pack ID
|
||||
POLICY: "default"
|
||||
# Optional - fail on block verdict
|
||||
FAIL_ON_BLOCK: "true"
|
||||
before_script:
|
||||
- |
|
||||
if [[ -z "${IMAGE}" ]]; then
|
||||
echo "ERROR: IMAGE must be set"
|
||||
exit 1
|
||||
fi
|
||||
script:
|
||||
- |
|
||||
set -euo pipefail
|
||||
|
||||
echo "Evaluating policy '${POLICY}' for: ${IMAGE}"
|
||||
|
||||
RESULT=$(stella policy evaluate \
|
||||
--image "${IMAGE}" \
|
||||
--policy "${POLICY}" \
|
||||
--output json)
|
||||
|
||||
VERDICT=$(echo "$RESULT" | jq -r '.verdict')
|
||||
VERDICT_DIGEST=$(echo "$RESULT" | jq -r '.verdictDigest')
|
||||
PASSED=$(echo "$RESULT" | jq -r '.passed')
|
||||
|
||||
echo "Verdict: ${VERDICT}"
|
||||
echo "Passed: ${PASSED}"
|
||||
|
||||
# Sign verdict
|
||||
echo "Signing verdict..."
|
||||
SIGN_RESULT=$(stella attest sign \
|
||||
--keyless \
|
||||
--artifact "${VERDICT_DIGEST}" \
|
||||
--type verdict \
|
||||
--rekor \
|
||||
--output json)
|
||||
|
||||
ATTESTATION_DIGEST=$(echo "$SIGN_RESULT" | jq -r '.attestationDigest')
|
||||
REKOR_UUID=$(echo "$SIGN_RESULT" | jq -r '.rekorUuid // empty')
|
||||
|
||||
echo "VERDICT=${VERDICT}" >> verdict.env
|
||||
echo "VERDICT_DIGEST=${VERDICT_DIGEST}" >> verdict.env
|
||||
echo "VERDICT_PASSED=${PASSED}" >> verdict.env
|
||||
echo "VERDICT_ATTESTATION_DIGEST=${ATTESTATION_DIGEST}" >> verdict.env
|
||||
echo "VERDICT_REKOR_UUID=${REKOR_UUID}" >> verdict.env
|
||||
|
||||
# Check if we should fail
|
||||
if [[ "${PASSED}" != "true" && "${FAIL_ON_BLOCK}" == "true" ]]; then
|
||||
echo "ERROR: Policy verdict is ${VERDICT} - blocking deployment"
|
||||
exit 1
|
||||
fi
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: verdict.env
|
||||
195
deploy/gitlab/examples/example-pipeline.gitlab-ci.yml
Normal file
195
deploy/gitlab/examples/example-pipeline.gitlab-ci.yml
Normal file
@@ -0,0 +1,195 @@
|
||||
# deploy/gitlab/examples/example-pipeline.gitlab-ci.yml
|
||||
# Example GitLab CI pipeline with StellaOps keyless signing
|
||||
#
|
||||
# This example demonstrates:
|
||||
# - Building and pushing a container image
|
||||
# - Generating and signing SBOM
|
||||
# - Evaluating and signing policy verdict
|
||||
# - Verification gate before deployment
|
||||
#
|
||||
# To use, copy this file to your repository's .gitlab-ci.yml
|
||||
|
||||
include:
|
||||
- local: 'deploy/gitlab/examples/.gitlab-ci-stellaops.yml'
|
||||
# Or include from StellaOps templates project:
|
||||
# - project: 'stella-ops/templates'
|
||||
# file: 'deploy/gitlab/examples/.gitlab-ci-stellaops.yml'
|
||||
|
||||
stages:
|
||||
- build
|
||||
- scan
|
||||
- sign
|
||||
- verify
|
||||
- deploy
|
||||
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
IMAGE: ${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}
|
||||
|
||||
# ==============================================================================
|
||||
# Build Stage
|
||||
# ==============================================================================
|
||||
|
||||
build:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
before_script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
script:
|
||||
- |
|
||||
docker build -t ${IMAGE} .
|
||||
docker push ${IMAGE}
|
||||
|
||||
# Get digest
|
||||
DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' ${IMAGE} | cut -d@ -f2)
|
||||
echo "IMAGE_DIGEST=${DIGEST}" >> build.env
|
||||
echo "IMAGE_REF=${CI_REGISTRY_IMAGE}@${DIGEST}" >> build.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: build.env
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
# ==============================================================================
|
||||
# Scan Stage
|
||||
# ==============================================================================
|
||||
|
||||
generate-sbom:
|
||||
stage: scan
|
||||
extends: .stellaops-sbom
|
||||
needs:
|
||||
- build
|
||||
variables:
|
||||
IMAGE: ${IMAGE_REF}
|
||||
SBOM_FORMAT: "cyclonedx-json"
|
||||
SBOM_OUTPUT: "sbom.cdx.json"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
vulnerability-scan:
|
||||
stage: scan
|
||||
image: stella-ops/cli:latest
|
||||
needs:
|
||||
- build
|
||||
script:
|
||||
- |
|
||||
stella scan vulnerability \
|
||||
--image "${IMAGE_REF}" \
|
||||
--output json > vulnerabilities.json
|
||||
|
||||
# Extract summary
|
||||
CRITICAL=$(jq '.summary.critical // 0' vulnerabilities.json)
|
||||
HIGH=$(jq '.summary.high // 0' vulnerabilities.json)
|
||||
|
||||
echo "Critical: ${CRITICAL}, High: ${HIGH}"
|
||||
|
||||
if [[ "${CRITICAL}" -gt 0 ]]; then
|
||||
echo "WARNING: ${CRITICAL} critical vulnerabilities found"
|
||||
fi
|
||||
artifacts:
|
||||
paths:
|
||||
- vulnerabilities.json
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
# ==============================================================================
|
||||
# Sign Stage
|
||||
# ==============================================================================
|
||||
|
||||
sign-image:
|
||||
stage: sign
|
||||
extends: .stellaops-sign
|
||||
needs:
|
||||
- build
|
||||
variables:
|
||||
ARTIFACT_DIGEST: ${IMAGE_DIGEST}
|
||||
ARTIFACT_TYPE: "image"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
evaluate-policy:
|
||||
stage: sign
|
||||
extends: .stellaops-verdict
|
||||
needs:
|
||||
- build
|
||||
- vulnerability-scan
|
||||
variables:
|
||||
IMAGE: ${IMAGE_REF}
|
||||
POLICY: "production"
|
||||
FAIL_ON_BLOCK: "false" # Don't fail here, let verify stage handle it
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
# ==============================================================================
|
||||
# Verify Stage
|
||||
# ==============================================================================
|
||||
|
||||
verify-for-deployment:
|
||||
stage: verify
|
||||
extends: .stellaops-verify
|
||||
needs:
|
||||
- build
|
||||
- sign-image
|
||||
- generate-sbom
|
||||
- evaluate-policy
|
||||
variables:
|
||||
ARTIFACT_DIGEST: ${IMAGE_DIGEST}
|
||||
CERTIFICATE_IDENTITY: "project_path:${CI_PROJECT_PATH}:ref_type:branch:ref:${CI_COMMIT_REF_NAME}"
|
||||
CERTIFICATE_OIDC_ISSUER: "${CI_SERVER_URL}"
|
||||
REQUIRE_SBOM: "true"
|
||||
REQUIRE_VERDICT: "true"
|
||||
STRICT: "true"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
- if: $CI_COMMIT_TAG
|
||||
|
||||
# ==============================================================================
|
||||
# Deploy Stage
|
||||
# ==============================================================================
|
||||
|
||||
deploy-staging:
|
||||
stage: deploy
|
||||
needs:
|
||||
- build
|
||||
- verify-for-deployment
|
||||
environment:
|
||||
name: staging
|
||||
url: https://staging.example.com
|
||||
script:
|
||||
- |
|
||||
echo "Deploying ${IMAGE_REF} to staging"
|
||||
echo "All attestations verified:"
|
||||
echo " - Image signature: ${ATTESTATION_DIGEST}"
|
||||
echo " - SBOM: ${SBOM_ATTESTATION_DIGEST}"
|
||||
echo " - Policy verdict: ${VERDICT_ATTESTATION_DIGEST}"
|
||||
|
||||
# Add your deployment commands here
|
||||
# kubectl set image deployment/app app=${IMAGE_REF}
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
|
||||
|
||||
deploy-production:
|
||||
stage: deploy
|
||||
needs:
|
||||
- build
|
||||
- verify-for-deployment
|
||||
- deploy-staging
|
||||
environment:
|
||||
name: production
|
||||
url: https://example.com
|
||||
script:
|
||||
- |
|
||||
echo "Deploying ${IMAGE_REF} to production"
|
||||
echo "Policy verdict: ${VERDICT}"
|
||||
|
||||
# Add your deployment commands here
|
||||
rules:
|
||||
- if: $CI_COMMIT_TAG
|
||||
when: manual
|
||||
306
deploy/gitlab/stellaops-gate-example.gitlab-ci.yml
Normal file
306
deploy/gitlab/stellaops-gate-example.gitlab-ci.yml
Normal file
@@ -0,0 +1,306 @@
|
||||
# -----------------------------------------------------------------------------
|
||||
# stellaops-gate-example.gitlab-ci.yml
|
||||
# Sprint: SPRINT_20251226_001_BE_cicd_gate_integration
|
||||
# Task: CICD-GATE-08 - GitLab CI example workflow using stella gate evaluate
|
||||
# Description: Example GitLab CI configuration for StellaOps release gate integration
|
||||
# -----------------------------------------------------------------------------
|
||||
#
|
||||
# This configuration demonstrates how to integrate StellaOps release gates into
|
||||
# your GitLab CI/CD pipeline. The gate evaluates security drift between your
|
||||
# current build and the approved baseline, blocking releases that introduce new
|
||||
# reachable vulnerabilities.
|
||||
#
|
||||
# Usage:
|
||||
# Include this file in your .gitlab-ci.yml:
|
||||
# include:
|
||||
# - project: 'stellaops/ci-templates'
|
||||
# file: '/templates/stellaops-gate.gitlab-ci.yml'
|
||||
#
|
||||
# Prerequisites:
|
||||
# 1. STELLAOPS_API_TOKEN variable configured in CI/CD settings
|
||||
# 2. STELLAOPS_BACKEND_URL variable configured (or use default)
|
||||
# 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
|
||||
#
|
||||
|
||||
variables:
|
||||
STELLAOPS_BACKEND_URL: ${STELLAOPS_BACKEND_URL:-https://stellaops.internal}
|
||||
STELLAOPS_CLI_VERSION: "latest"
|
||||
# Registry configuration
|
||||
REGISTRY: ${CI_REGISTRY}
|
||||
IMAGE_NAME: ${CI_REGISTRY_IMAGE}
|
||||
|
||||
stages:
|
||||
- build
|
||||
- scan
|
||||
- gate
|
||||
- deploy
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Build Stage: Build and push container image
|
||||
# -----------------------------------------------------------------------------
|
||||
build:
|
||||
stage: build
|
||||
image: docker:24
|
||||
services:
|
||||
- docker:24-dind
|
||||
variables:
|
||||
DOCKER_TLS_CERTDIR: "/certs"
|
||||
before_script:
|
||||
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
|
||||
script:
|
||||
- |
|
||||
# Build with BuildKit for better caching
|
||||
export DOCKER_BUILDKIT=1
|
||||
|
||||
# Generate image tag based on commit
|
||||
IMAGE_TAG="${CI_REGISTRY_IMAGE}:${CI_COMMIT_SHORT_SHA}"
|
||||
|
||||
# Build and push
|
||||
docker build \
|
||||
--label "org.opencontainers.image.revision=${CI_COMMIT_SHA}" \
|
||||
--label "org.opencontainers.image.source=${CI_PROJECT_URL}" \
|
||||
-t "${IMAGE_TAG}" \
|
||||
.
|
||||
|
||||
docker push "${IMAGE_TAG}"
|
||||
|
||||
# Get the digest
|
||||
IMAGE_DIGEST=$(docker inspect --format='{{index .RepoDigests 0}}' "${IMAGE_TAG}" | cut -d'@' -f2)
|
||||
echo "IMAGE_DIGEST=${IMAGE_DIGEST}" >> build.env
|
||||
echo "IMAGE_REF=${CI_REGISTRY_IMAGE}@${IMAGE_DIGEST}" >> build.env
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: build.env
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Gate Stage: Evaluate StellaOps release gate
|
||||
# -----------------------------------------------------------------------------
|
||||
.stellaops-gate-base:
|
||||
stage: gate
|
||||
image: alpine:3.19
|
||||
variables:
|
||||
# Baseline strategy: auto-detect based on branch
|
||||
BASELINE_STRATEGY: "auto"
|
||||
# Allow warnings to pass by default
|
||||
ALLOW_WARNINGS: "true"
|
||||
before_script:
|
||||
- |
|
||||
# Install dependencies
|
||||
apk add --no-cache curl jq bash
|
||||
|
||||
# Install StellaOps CLI
|
||||
curl -sSL https://get.stella-ops.org/cli | bash
|
||||
export PATH="$HOME/.stellaops/bin:$PATH"
|
||||
|
||||
# Verify installation
|
||||
stella --version
|
||||
|
||||
stellaops-gate:
|
||||
extends: .stellaops-gate-base
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
script:
|
||||
- |
|
||||
# Determine baseline strategy based on branch
|
||||
if [ "$BASELINE_STRATEGY" = "auto" ]; then
|
||||
case "$CI_COMMIT_REF_NAME" in
|
||||
main|master)
|
||||
BASELINE="production"
|
||||
;;
|
||||
release/*)
|
||||
BASELINE="last-approved"
|
||||
;;
|
||||
*)
|
||||
BASELINE="previous-build"
|
||||
;;
|
||||
esac
|
||||
else
|
||||
BASELINE="$BASELINE_STRATEGY"
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo "StellaOps Release Gate Evaluation"
|
||||
echo "============================================"
|
||||
echo "Image Digest: ${IMAGE_DIGEST}"
|
||||
echo "Baseline Strategy: ${BASELINE}"
|
||||
echo "Branch: ${CI_COMMIT_REF_NAME}"
|
||||
echo "============================================"
|
||||
|
||||
# Run gate evaluation
|
||||
set +e
|
||||
RESULT=$(stella gate evaluate \
|
||||
--image "${IMAGE_DIGEST}" \
|
||||
--baseline "${BASELINE}" \
|
||||
--output json \
|
||||
--ci-context "gitlab-ci" \
|
||||
--repository "${CI_PROJECT_PATH}" \
|
||||
--tag "${CI_COMMIT_SHORT_SHA}" \
|
||||
2>&1)
|
||||
EXIT_CODE=$?
|
||||
set -e
|
||||
|
||||
# Parse results
|
||||
DECISION_ID=$(echo "$RESULT" | jq -r '.decisionId // "unknown"')
|
||||
STATUS=$(echo "$RESULT" | jq -r '.status // "unknown"')
|
||||
SUMMARY=$(echo "$RESULT" | jq -r '.summary // "No summary"')
|
||||
|
||||
# Store for downstream jobs
|
||||
echo "GATE_DECISION_ID=${DECISION_ID}" >> gate.env
|
||||
echo "GATE_STATUS=${STATUS}" >> gate.env
|
||||
echo "GATE_EXIT_CODE=${EXIT_CODE}" >> gate.env
|
||||
|
||||
# Display results
|
||||
echo ""
|
||||
echo "============================================"
|
||||
echo "Gate Result: ${STATUS}"
|
||||
echo "Decision ID: ${DECISION_ID}"
|
||||
echo "============================================"
|
||||
echo "${SUMMARY}"
|
||||
echo "============================================"
|
||||
|
||||
# Handle exit codes
|
||||
case $EXIT_CODE in
|
||||
0)
|
||||
echo "Gate PASSED - Release may proceed"
|
||||
;;
|
||||
1)
|
||||
echo "Gate PASSED WITH WARNINGS"
|
||||
if [ "$ALLOW_WARNINGS" = "true" ]; then
|
||||
echo "Warnings allowed - continuing pipeline"
|
||||
exit 0
|
||||
else
|
||||
echo "Warnings not allowed - blocking pipeline"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
2)
|
||||
echo "Gate BLOCKED - Security policy violation"
|
||||
echo "Review the gate decision for details:"
|
||||
echo "${STELLAOPS_BACKEND_URL}/gates/decisions/${DECISION_ID}"
|
||||
exit 2
|
||||
;;
|
||||
*)
|
||||
echo "Gate evaluation error (exit code: $EXIT_CODE)"
|
||||
exit $EXIT_CODE
|
||||
;;
|
||||
esac
|
||||
artifacts:
|
||||
reports:
|
||||
dotenv: gate.env
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
- if: $CI_MERGE_REQUEST_IID
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Gate Override: Manual override for blocked releases
|
||||
# -----------------------------------------------------------------------------
|
||||
stellaops-gate-override:
|
||||
extends: .stellaops-gate-base
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
- job: stellaops-gate
|
||||
artifacts: true
|
||||
script:
|
||||
- |
|
||||
if [ "$GATE_STATUS" != "Fail" ]; then
|
||||
echo "Override not needed - gate status is ${GATE_STATUS}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "============================================"
|
||||
echo "StellaOps Gate Override Request"
|
||||
echo "============================================"
|
||||
echo "Original Decision ID: ${GATE_DECISION_ID}"
|
||||
echo "Override requested by: ${GITLAB_USER_LOGIN}"
|
||||
echo "Justification: ${OVERRIDE_JUSTIFICATION}"
|
||||
echo "============================================"
|
||||
|
||||
if [ -z "$OVERRIDE_JUSTIFICATION" ]; then
|
||||
echo "ERROR: OVERRIDE_JUSTIFICATION variable must be set"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Request override with justification
|
||||
stella gate evaluate \
|
||||
--image "${IMAGE_DIGEST}" \
|
||||
--baseline "last-approved" \
|
||||
--allow-override \
|
||||
--justification "${OVERRIDE_JUSTIFICATION}" \
|
||||
--ci-context "gitlab-ci-override" \
|
||||
--repository "${CI_PROJECT_PATH}" \
|
||||
--tag "${CI_COMMIT_SHORT_SHA}"
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH
|
||||
when: manual
|
||||
allow_failure: true
|
||||
environment:
|
||||
name: security-override
|
||||
action: prepare
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Deploy Stage: Deploy to staging (only if gate passed)
|
||||
# -----------------------------------------------------------------------------
|
||||
deploy-staging:
|
||||
stage: deploy
|
||||
image: alpine:3.19
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
- job: stellaops-gate
|
||||
artifacts: true
|
||||
script:
|
||||
- |
|
||||
echo "Deploying ${IMAGE_REF} to staging..."
|
||||
|
||||
# Verify gate passed
|
||||
if [ "$GATE_STATUS" != "Pass" ] && [ "$GATE_STATUS" != "Warn" ]; then
|
||||
echo "ERROR: Gate did not pass (status: ${GATE_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add your deployment commands here
|
||||
# Example: kubectl set image deployment/app app=${IMAGE_REF}
|
||||
echo "Deployment complete!"
|
||||
environment:
|
||||
name: staging
|
||||
url: https://staging.example.com
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
- if: $CI_COMMIT_BRANCH =~ /^release\//
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Deploy Stage: Deploy to production (requires manual approval)
|
||||
# -----------------------------------------------------------------------------
|
||||
deploy-production:
|
||||
stage: deploy
|
||||
image: alpine:3.19
|
||||
needs:
|
||||
- job: build
|
||||
artifacts: true
|
||||
- job: stellaops-gate
|
||||
artifacts: true
|
||||
script:
|
||||
- |
|
||||
echo "Deploying ${IMAGE_REF} to production..."
|
||||
|
||||
# Verify gate passed (warnings not allowed for production)
|
||||
if [ "$GATE_STATUS" != "Pass" ]; then
|
||||
echo "ERROR: Production deployment requires Pass status (got: ${GATE_STATUS})"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Add your production deployment commands here
|
||||
echo "Production deployment complete!"
|
||||
environment:
|
||||
name: production
|
||||
url: https://example.com
|
||||
rules:
|
||||
- if: $CI_COMMIT_BRANCH == "main"
|
||||
when: manual
|
||||
Reference in New Issue
Block a user