Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 68 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,85 @@
name: Release
name: Signed Release

on:
push:
tags:
- 'v*'
- "v*"

permissions:
contents: write
id-token: write
attestations: write

jobs:
release:
runs-on: ubuntu-latest
env:
TAG: ${{ github.ref_name }}
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- name: Checkout signed tag
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
fetch-depth: 0
- uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2

- name: Verify annotated tag signature
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
tag_object=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq '.object.sha')
object_type=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" --jq '.object.type')
if [ "$object_type" != "tag" ]; then
echo "Release tags must be signed annotated tags; ${TAG} is ${object_type}."
exit 1
fi
verified=$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${tag_object}" --jq '.verification.verified')
if [ "$verified" != "true" ]; then
echo "GitHub could not verify the signature on ${TAG}."
exit 1
fi

- name: Install Syft
env:
SYFT_VERSION: "1.46.0"
SYFT_SHA256: d654f678b709eb53c393d38519d5ed7d2e57205529404018614cfefa0fb2b5ca
run: |
set -euo pipefail
archive="syft_${SYFT_VERSION}_linux_amd64.tar.gz"
curl --fail --silent --show-error --location \
--output "$archive" \
"https://github.com/anchore/syft/releases/download/v${SYFT_VERSION}/${archive}"
echo "${SYFT_SHA256} ${archive}" | sha256sum --check --strict
sudo tar --extract --gzip --file "$archive" --directory /usr/local/bin syft

- name: Build deterministic release artifacts
run: |
set -euo pipefail
mkdir -p dist
syft dir:. --source-name openshield --source-version "$TAG" \
-o "cyclonedx-json=dist/openshield-${TAG}-sbom.cyclonedx.json"
git archive --format=tar --prefix="openshield-${TAG}/" "$TAG" | \
gzip --no-name > "dist/openshield-${TAG}.tar.gz"
cd dist
sha256sum "openshield-${TAG}.tar.gz" "openshield-${TAG}-sbom.cyclonedx.json" > SHA256SUMS

- name: Attest source archive
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: dist/openshield-${{ env.TAG }}.tar.gz

- name: Attest SBOM
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: dist/openshield-${{ env.TAG }}-sbom.cyclonedx.json

- name: Attest checksum manifest
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: dist/SHA256SUMS

- name: Publish release
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
with:
generate_release_notes: true
make_latest: true
files: dist/*
40 changes: 0 additions & 40 deletions .github/workflows/sbom-release.yml

This file was deleted.

3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![CHANGELOG](https://img.shields.io/badge/changelog-here-blue)](CHANGELOG.md)

Release artifacts include SHA-256 checksums, an SBOM, and identity-bound
provenance attestations. See [release verification](docs/release-verification.md).

> **Open source Cloud Security Posture Management (CSPM) for Azure - detect misconfigurations, map to CIS/NIST/ISO27001/SOC2, fix them with one command, and identify cryptographic assets requiring quantum-safe migration.**

[![GitHub Repo stars](https://img.shields.io/github/stars/openshield-org/openshield?style=flat-square)](https://github.com/openshield-org/openshield/stargazers)
Expand Down
2 changes: 1 addition & 1 deletion docs/ci-pipeline.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This document explains each job, how to reproduce every check locally before ope

`.github/workflows/codeql.yml` (separate workflow, PRs to `dev`/`main` + weekly cron): **Analyze (python)** and **Analyze (javascript)** — CodeQL semantic/taint analysis.

`.github/workflows/sbom-release.yml` (triggered `on: release: published`): generates a CycloneDX SBOM from the tagged code and uploads it to the GitHub Release assets.
`.github/workflows/release.yml` (triggered by `v*` tags): requires a verified signed annotated tag, builds a deterministic source archive and CycloneDX SBOM, publishes SHA-256 checksums and identity-bound provenance attestations, then creates the GitHub Release.

The **Container Scan** job is intentionally **not** a required check yet: no `Dockerfile` exists, so it has nothing to scan. It activates automatically once INFRA 1 (#154) adds one.

Expand Down
50 changes: 50 additions & 0 deletions docs/release-verification.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Verifying OpenShield Releases

OpenShield release artifacts are produced only from a GitHub-verified signed
annotated tag. GitHub Actions generates a deterministic source archive, a
CycloneDX SBOM and SHA-256 checksums, then creates identity-bound Sigstore
provenance attestations before uploading the files to the release.

## Verify checksums

Download all release assets into one directory, then run:

```bash
sha256sum --check SHA256SUMS
```

## Verify provenance

Install the GitHub CLI and verify each artifact against this repository:

```bash
gh attestation verify openshield-vX.Y.Z.tar.gz \
--repo openshield-org/openshield \
--signer-workflow openshield-org/openshield/.github/workflows/release.yml

gh attestation verify openshield-vX.Y.Z-sbom.cyclonedx.json \
--repo openshield-org/openshield \
--signer-workflow openshield-org/openshield/.github/workflows/release.yml

gh attestation verify SHA256SUMS \
--repo openshield-org/openshield \
--signer-workflow openshield-org/openshield/.github/workflows/release.yml
```

Successful verification proves that the artifact digest was attested by the
OpenShield release workflow for this public repository. It does not mean that
GitHub or Sigstore audited the source code.

## Maintainer release procedure

1. Confirm the release commit is on the approved `main` history and CI passes.
2. Create a signed annotated tag: `git tag -s vX.Y.Z -m "OpenShield vX.Y.Z"`.
SSH signing may be used when Git is configured with `gpg.format=ssh`.
3. Verify locally with `git tag -v vX.Y.Z` using the project's trusted signer
configuration.
4. Push only the tag: `git push origin vX.Y.Z`.
5. The workflow independently asks GitHub to verify the tag signature. A
lightweight or unverified tag fails before artifacts are produced.
6. After publication, download and verify every asset using the commands above.

Existing historical lightweight tags are not retroactively described as signed.
44 changes: 44 additions & 0 deletions tests/test_signed_release_workflow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Safety checks for the signed release workflow."""

from pathlib import Path

import yaml


WORKFLOW = Path(__file__).parents[1] / ".github" / "workflows" / "release.yml"


def _workflow():
return yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))


def test_release_workflow_has_keyless_attestation_permissions():
assert _workflow()["permissions"] == {
"contents": "write",
"id-token": "write",
"attestations": "write",
}


def test_release_requires_verified_annotated_tag():
source = WORKFLOW.read_text(encoding="utf-8")
assert 'object_type" != "tag"' in source
assert ".verification.verified" in source
assert 'verified" != "true"' in source


def test_release_attests_every_distributed_manifest():
source = WORKFLOW.read_text(encoding="utf-8")
action = "actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373"
assert source.count(action) == 3
assert "openshield-${{ env.TAG }}.tar.gz" in source
assert "openshield-${{ env.TAG }}-sbom.cyclonedx.json" in source
assert "subject-path: dist/SHA256SUMS" in source


def test_release_artifact_is_deterministic_and_checksums_are_published():
source = WORKFLOW.read_text(encoding="utf-8")
assert "git archive --format=tar" in source
assert "gzip --no-name" in source
assert "sha256sum" in source
assert "files: dist/*" in source
Loading