Skip to content
Merged
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
89 changes: 57 additions & 32 deletions .github/workflows/build-baseline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,6 @@ on:
- main
tags:
- "v*"
release:
types:
- published

permissions:
contents: read
Expand Down Expand Up @@ -288,44 +285,72 @@ jobs:
- name: Confirm both macOS architectures built
run: true

attach-windows-release-artifact:
name: release-artifact / windows
if: github.event_name == 'release'
publish-immutable-release:
name: release-artifact / publish
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
needs:
- build-windows-native
- build-windows-arm64
- gate-windows
- gate-macos
permissions:
contents: write
steps:
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
pattern: bandscope-windows-*-${{ github.sha }}
path: artifacts
merge-multiple: true
- name: Attach Windows artifacts to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" artifacts/* --clobber --repo ${{ github.repository }}

attach-macos-release-artifact:
name: release-artifact / macos
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs:
- build-macos-native
- build-macos-arm64
permissions:
contents: write
steps:
persist-credentials: false
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: bandscope-macos-*-${{ github.sha }}
pattern: bandscope-*-${{ github.sha }}
path: artifacts
merge-multiple: true
- name: Attach macOS artifacts to release
- name: Generate release CycloneDX SBOM
uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0.23.1
with:
path: .
format: cyclonedx-json
output-file: bandscope-sbom.cdx.json
upload-artifact: false
upload-release-assets: false
- name: Upload release SBOM artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: bandscope-release-sbom-${{ github.sha }}
path: |
bandscope-sbom.cdx.json
supply-chain/supplemental-component-inventory.json
- name: Validate release asset set
run: |
set -euo pipefail
shopt -s nullglob
test -f bandscope-sbom.cdx.json
test -f supply-chain/supplemental-component-inventory.json
windows_amd64=(artifacts/*windows-amd64*)
windows_arm64=(artifacts/*windows-arm64*)
macos_amd64=(artifacts/*macos-amd64*)
macos_arm64=(artifacts/*macos-arm64*)
checksums=(artifacts/*.sha256)
(( ${#windows_amd64[@]} > 0 ))
(( ${#windows_arm64[@]} > 0 ))
(( ${#macos_amd64[@]} > 0 ))
(( ${#macos_arm64[@]} > 0 ))
(( ${#checksums[@]} > 0 ))
- name: Create draft release with complete assets, then publish
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" artifacts/* --clobber --repo ${{ github.repository }}
RELEASE_TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
if gh release view "$RELEASE_TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
echo "Release $RELEASE_TAG already exists; immutable release assets must be attached before publication."
exit 1
fi
gh release create "$RELEASE_TAG" \
artifacts/* \
bandscope-sbom.cdx.json \
supply-chain/supplemental-component-inventory.json \
--draft \
--generate-notes \
--title "BandScope ${RELEASE_TAG#v}" \
--verify-tag \
--repo "${{ github.repository }}"
gh release edit "$RELEASE_TAG" --draft=false --repo "${{ github.repository }}"
26 changes: 0 additions & 26 deletions .github/workflows/sbom.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,3 @@ jobs:
with:
name: bandscope-supply-chain-inventory
path: supply-chain/supplemental-component-inventory.json

release-sbom:
name: attach-sbom-to-release
if: github.event_name == 'release'
runs-on: ubuntu-latest
needs:
- sbom
permissions:
contents: write
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bandscope-sbom

- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bandscope-supply-chain-inventory
path: supply-chain

- name: Attach SBOM to GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELEASE_TAG: ${{ github.event.release.tag_name }}
run: gh release upload "$RELEASE_TAG" bandscope-sbom.cdx.json supply-chain/supplemental-component-inventory.json --clobber
2 changes: 1 addition & 1 deletion docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,5 @@ GitHub is the source of truth for repository governance, PR review, CI/CD, Code
## CI/CD and release flow

- PRs into `develop` and `main` run CI, dependency review, security audit, secret-scan gate, SBOM generation, and CodeQL
- release flows publish desktop artifacts plus SBOM evidence to GitHub Releases
- release flows publish desktop artifacts plus SBOM evidence to GitHub Releases through a tag-driven draft-before-publish path
- branch protection connects stable required checks after bootstrap workflows exist
1 change: 1 addition & 0 deletions docs/operations/deploy-runbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ BandScope currently relies on GitHub Actions CI/release workflows as deploy-qual
- SBOM artifact generation (`.github/workflows/sbom.yml`)
- Release preflight completion (`.github/workflows/release.yml`)
- Cross-platform build baseline completion (`.github/workflows/build-baseline.yml`)
- For immutable GitHub Releases, release assets are attached by the tag-driven draft release flow before publication, not by post-publication `release` events

## Runtime verification baseline

Expand Down
2 changes: 2 additions & 0 deletions docs/release/release-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,7 @@ BandScope distributes release artifacts through GitHub Releases.
## Release rules

- release merges do not bypass review or required checks
- immutable releases are published from a tag-driven draft release after assets, checksums, SBOM, and supplemental inventory are attached
- release workflows must not attach assets after a GitHub Release is already published
- release artifacts must remain traceable to the GitHub Release record
- missing SBOM or missing supplemental inventory means the release baseline is incomplete
7 changes: 4 additions & 3 deletions docs/security/github-required-checks.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ These controls are expressed by repo workflows and are expected to be connected
## Release evidence baseline

- CycloneDX JSON SBOM must be uploaded as a GitHub Actions artifact
- CycloneDX JSON SBOM must be attached to the GitHub Release when the workflow runs on a Release event
- `supply-chain/supplemental-component-inventory.json` must be uploaded as a GitHub Actions artifact and attached to the GitHub Release on Release events
- packaged desktop artifacts and checksums should remain traceable from the same release record when the release workflow emits them
- CycloneDX JSON SBOM must be attached to the GitHub Release before publication by the tag-driven draft release flow
- `supply-chain/supplemental-component-inventory.json` must be uploaded as a GitHub Actions artifact and attached to the GitHub Release before publication
- packaged desktop artifacts and checksums must remain traceable from the same release record when the release workflow emits them
- release artifacts should include explicit OS/arch naming for Windows amd64, Windows arm64, macOS amd64, and macOS arm64
- workflows must not attach assets in response to `release: published`; immutable releases reject post-publication mutation

## Enforcement note

Expand Down
2 changes: 1 addition & 1 deletion docs/security/sbom-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ BandScope generates machine-readable SBOMs in GitHub Actions as a bootstrap cont
## Retention

- upload the SBOM as a GitHub Actions artifact
- attach the SBOM to the GitHub Release when a release event exists
- attach the SBOM to the GitHub Release before publication through the tag-driven draft release flow
- retain the supplemental component inventory with the SBOM

## Supplemental inventory
Expand Down
28 changes: 25 additions & 3 deletions scripts/checks/verify_supply_chain.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,21 +119,23 @@ def verify_workflow_coverage() -> list[str]:
"main",
"pull_request",
"push",
"release:",
"tags:",
"windows-2025",
"windows-11-arm",
"macos-15-intel",
"macos-15",
"gate / build / windows",
"gate / build / macos",
"release-artifact / macos",
"release-artifact / windows",
"release-artifact / publish",
"ubuntu-latest",
"bandscope-windows-amd64-${{ github.sha }}",
"bandscope-windows-arm64-${{ github.sha }}",
"bandscope-macos-amd64-${{ github.sha }}",
"bandscope-macos-arm64-${{ github.sha }}",
"bandscope-release-sbom-${{ github.sha }}",
"gh release create",
"--draft",
"--verify-tag",
"Get-MpComputerStatus",
]:
if build and token not in build:
Expand All @@ -159,13 +161,33 @@ def verify_workflow_coverage() -> list[str]:
return missing


def verify_immutable_release_upload_policy() -> list[str]:
"""Return workflow violations that mutate immutable releases after publication."""
violations: list[str] = []
workflow_paths = sorted(Path(".github/workflows").glob("*.yml")) + sorted(
Path(".github/workflows").glob("*.yaml")
)
for path in workflow_paths:
content = path.read_text(encoding="utf-8")
if "release:" not in content or "published" not in content:
continue
if "gh release upload" not in content:
continue
violations.append(
f"{path}: release published workflows must not upload GitHub Release assets; "
"immutable releases require draft-before-publish asset attachment"
)
return violations


def main() -> int:
"""Return a failing exit code when supply-chain controls are incomplete."""
violations: list[str] = []
violations.extend(f"missing file: {item}" for item in verify_required_files())
violations.extend(verify_pinned_actions())
violations.extend(verify_dependabot_coverage())
violations.extend(verify_workflow_coverage())
violations.extend(verify_immutable_release_upload_policy())

if violations:
print("Supply-chain verification failed:")
Expand Down
54 changes: 54 additions & 0 deletions services/analysis-engine/tests/test_supply_chain_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,3 +113,57 @@ def test_supply_chain_check_requires_ossf_default_branch_guard(
"ossf scorecard workflow must guard Scorecard execution to the repository default branch"
in violations
)


def test_supply_chain_check_rejects_release_published_asset_upload(
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
) -> None:
"""Ensure immutable releases are not mutated after publication."""
supply_chain = load_module(
"scripts/checks/verify_supply_chain.py", "verify_supply_chain_immutable_release_upload"
)

workflow_dir = tmp_path / ".github" / "workflows"
workflow_dir.mkdir(parents=True)
(workflow_dir / "sbom.yml").write_text(
"""
name: sbom
on:
release:
types:
- published
jobs:
release-sbom:
steps:
- name: Attach SBOM to GitHub Release
run: gh release upload "$RELEASE_TAG" bandscope-sbom.cdx.json --clobber
""".strip(),
encoding="utf-8",
)

monkeypatch.chdir(tmp_path)

assert hasattr(supply_chain, "verify_immutable_release_upload_policy")
violations = supply_chain.verify_immutable_release_upload_policy()

assert (
".github/workflows/sbom.yml: release published workflows must not upload GitHub "
"Release assets; immutable releases require draft-before-publish asset attachment"
) in violations


def test_supply_chain_check_accepts_immutable_release_safe_workflows(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Ensure checked-in workflows avoid release-published asset mutation."""
supply_chain = load_module(
"scripts/checks/verify_supply_chain.py", "verify_supply_chain_immutable_release_repo"
)
repo_root = Path(__file__).resolve().parents[3]

monkeypatch.chdir(repo_root)

assert hasattr(supply_chain, "verify_immutable_release_upload_policy")
violations = supply_chain.verify_immutable_release_upload_policy()

assert not violations
Loading