Files
pulse/.github/workflows/security-scan.yml
T
pulse-triage[bot] 168f52972a Expose complete stable release continuity failures
Change-source: pulse-maintainer
2026-09-01 12:26:46 +01:00

392 lines
17 KiB
YAML

name: Security Scan
# Build and Test audits dependencies on every push, but a quiet week with no
# pushes means no run — and newly disclosed vulnerabilities or delivery drift
# land against unchanged code. The six-hour schedule cheaply verifies that the
# advertised stable release remains locked and bound to its activation marker.
# The weekly schedule re-scans every dependency and performs the full public
# delivery verification. A failed scheduled run emails the repository owner.
on:
schedule:
- cron: '17 */6 * * *' # six-hour stable release lock and activation watch
- cron: '30 5 * * 1' # weekly, Monday 05:30 UTC, before the 06:00 triage run
workflow_dispatch:
workflow_run:
workflows: [Release Convergence]
types: [completed]
branches: [main]
permissions:
contents: read
jobs:
release-continuity:
name: Latest stable release continuity
# Stable convergence gets an immediate independent read-back. Prerelease
# convergence is excluded because this job intentionally follows the
# advertised stable channel. The six-hour schedule stops after release and
# activation identity resolution; weekly, manual, and convergence runs
# continue through every artifact and customer-facing delivery surface.
if: ${{ github.event_name != 'workflow_run' || !contains(github.event.workflow_run.display_title, '-') }}
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout release verification controls
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
if: ${{ github.event.schedule != '17 */6 * * *' }}
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Set up Helm
if: ${{ github.event.schedule != '17 */6 * * *' }}
uses: azure/setup-helm@dda3372f752e03dde6b3237bc9431cdc2f7a02a2 # v5.0.0
with:
version: v3.15.2
- name: Set up Docker Buildx
if: ${{ github.event.schedule != '17 */6 * * *' }}
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
- name: Resolve the advertised stable release
id: release
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
run: |
set -euo pipefail
mkdir -p release-continuity-evidence
release_json=release-continuity-evidence/release.json
if ! gh api \
-H 'Accept: application/vnd.github+json' \
-H 'X-GitHub-Api-Version: 2026-03-10' \
"repos/${REPOSITORY}/releases/latest" > "${release_json}"; then
python3 scripts/release_control/release_continuity.py release \
--release-json "${release_json}" \
--diagnostic release-continuity-evidence/release-diagnostic.json \
--github-output "${GITHUB_OUTPUT}" || true
exit 1
fi
python3 scripts/release_control/release_continuity.py release \
--release-json "${release_json}" \
--diagnostic release-continuity-evidence/release-diagnostic.json \
--github-output "${GITHUB_OUTPUT}"
- name: Bind the release activation marker
id: activation
# A structurally valid tag/id/SHA remains safe lookup data even when
# release trust fails. Inspect its marker so one defect cannot hide a
# second; all public-surface verification below still requires both
# release and activation admission to succeed.
if: ${{ !cancelled() && steps.release.outputs.referenceable == 'true' }}
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
release_json=release-continuity-evidence/release.json
marker=release-continuity-evidence/release-activation.json
if ! gh release download "${TAG}" \
--repo "${REPOSITORY}" \
--pattern release-activation.json \
--dir release-continuity-evidence; then
python3 scripts/release_control/release_continuity.py activation \
--release-json "${release_json}" \
--activation-json "${marker}" \
--diagnostic release-continuity-evidence/activation-diagnostic.json \
--github-output "${GITHUB_OUTPUT}" || true
exit 1
fi
python3 scripts/release_control/release_continuity.py activation \
--release-json "${release_json}" \
--activation-json "${marker}" \
--diagnostic release-continuity-evidence/activation-diagnostic.json \
--github-output "${GITHUB_OUTPUT}"
- name: Verify immutable release and build provenance
id: packet
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.release.outputs.tag }}
RELEASE_ID: ${{ steps.release.outputs.release_id }}
SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
run: >-
./scripts/verify-github-release-integrity.sh
"${TAG}" "${REPOSITORY}" "${RELEASE_ID}" "${SOURCE_SHA}"
- name: Authenticate every published release asset
id: assets
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
env:
PULSE_UPDATE_SIGNING_PUBLIC_KEY: ${{ vars.PULSE_UPDATE_SIGNING_PUBLIC_KEY }}
REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.release.outputs.tag }}
run: ./scripts/validate-published-release.sh "${TAG}" "${REPOSITORY}"
- name: Verify exact-version container identities
id: containers
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.release.outputs.tag }}
SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
EXPECTED_SERVER_DIGEST: ${{ steps.activation.outputs.server_image_digest }}
EXPECTED_CONTROL_PLANE_DIGEST: ${{ steps.activation.outputs.control_plane_image_digest }}
run: |
set -euo pipefail
proof="$(./scripts/verify-release-container-images.sh "${TAG}" "${SOURCE_SHA}" "${REPOSITORY}")"
printf '%s\n' "${proof}"
server_digest="$(awk -F= '$1 == "server_digest" {print $2}' <<<"${proof}")"
control_plane_digest="$(awk -F= '$1 == "control_plane_digest" {print $2}' <<<"${proof}")"
if [ "${server_digest}" != "${EXPECTED_SERVER_DIGEST}" ] || \
[ "${control_plane_digest}" != "${EXPECTED_CONTROL_PLANE_DIGEST}" ]; then
echo "::error::Exact-version container identities moved from the committed activation marker."
exit 1
fi
- name: Verify stable container discovery aliases
id: aliases
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
env:
TAG: ${{ steps.release.outputs.tag }}
EXPECTED_SERVER_DIGEST: ${{ steps.activation.outputs.server_image_digest }}
EXPECTED_CONTROL_PLANE_DIGEST: ${{ steps.activation.outputs.control_plane_image_digest }}
REGISTRY_OWNER: ${{ github.repository_owner }}
run: >-
./scripts/verify-stable-container-aliases.sh
"${TAG}" "${EXPECTED_SERVER_DIGEST}" "${EXPECTED_CONTROL_PLANE_DIGEST}"
"${REGISTRY_OWNER}"
- name: Verify exact-version Helm identity
id: helm
if: ${{ always() && steps.release.outcome == 'success' && steps.activation.outcome == 'success' && github.event.schedule != '17 */6 * * *' }}
env:
GH_TOKEN: ${{ github.token }}
REPOSITORY: ${{ github.repository }}
TAG: ${{ steps.release.outputs.tag }}
SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
EXPECTED_HELM_DIGEST: ${{ steps.activation.outputs.helm_chart_digest }}
run: >-
./scripts/verify-release-helm-chart.sh
"${TAG}" "${SOURCE_SHA}" "${REPOSITORY}" "${EXPECTED_HELM_DIGEST}"
- name: Record continuity evidence
if: ${{ always() }}
env:
REPOSITORY: ${{ github.repository }}
RUN_ID: ${{ github.run_id }}
RUN_ATTEMPT: ${{ github.run_attempt }}
TRIGGER_EVENT: ${{ github.event_name }}
TRIGGER_SCHEDULE: ${{ github.event.schedule }}
CONVERGENCE_RUN_ID: ${{ github.event.workflow_run.id }}
CONVERGENCE_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
CONVERGENCE_CONCLUSION: ${{ github.event.workflow_run.conclusion }}
CONVERGENCE_DISPLAY_TITLE: ${{ github.event.workflow_run.display_title }}
TAG: ${{ steps.release.outputs.tag }}
RELEASE_ID: ${{ steps.release.outputs.release_id }}
SOURCE_SHA: ${{ steps.release.outputs.source_sha }}
ACTIVATION_SHA256: ${{ steps.activation.outputs.activation_sha256 }}
SERVER_IMAGE_DIGEST: ${{ steps.activation.outputs.server_image_digest }}
CONTROL_PLANE_IMAGE_DIGEST: ${{ steps.activation.outputs.control_plane_image_digest }}
HELM_CHART_DIGEST: ${{ steps.activation.outputs.helm_chart_digest }}
RELEASE_RESULT: ${{ steps.release.outcome }}
ACTIVATION_RESULT: ${{ steps.activation.outcome }}
PACKET_RESULT: ${{ steps.packet.outcome }}
ASSET_RESULT: ${{ steps.assets.outcome }}
CONTAINER_RESULT: ${{ steps.containers.outcome }}
ALIAS_RESULT: ${{ steps.aliases.outcome }}
HELM_RESULT: ${{ steps.helm.outcome }}
run: |
set -euo pipefail
mkdir -p release-continuity-evidence
checked_at="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
mode=full
if [ "${TRIGGER_SCHEDULE}" = '17 */6 * * *' ]; then
mode=release_lock
fi
for diagnostic in release activation; do
path="release-continuity-evidence/${diagnostic}-diagnostic.json"
if [ ! -s "${path}" ]; then
jq -n \
--arg check "${diagnostic}" \
'{schema_version: 1, check: $check, status: "not_run", identity: {}, violations: []}' \
> "${path}"
fi
done
jq -n \
--slurpfile release_diagnostic release-continuity-evidence/release-diagnostic.json \
--slurpfile activation_diagnostic release-continuity-evidence/activation-diagnostic.json \
--arg schema_version "1" \
--arg checked_at "${checked_at}" \
--arg repository "${REPOSITORY}" \
--arg run_id "${RUN_ID}" \
--arg run_attempt "${RUN_ATTEMPT}" \
--arg trigger_event "${TRIGGER_EVENT}" \
--arg trigger_schedule "${TRIGGER_SCHEDULE}" \
--arg mode "${mode}" \
--arg convergence_run_id "${CONVERGENCE_RUN_ID}" \
--arg convergence_run_attempt "${CONVERGENCE_RUN_ATTEMPT}" \
--arg convergence_conclusion "${CONVERGENCE_CONCLUSION}" \
--arg convergence_display_title "${CONVERGENCE_DISPLAY_TITLE}" \
--arg tag "${TAG}" \
--arg release_id "${RELEASE_ID}" \
--arg source_sha "${SOURCE_SHA}" \
--arg activation_sha256 "${ACTIVATION_SHA256}" \
--arg server_image_digest "${SERVER_IMAGE_DIGEST}" \
--arg control_plane_image_digest "${CONTROL_PLANE_IMAGE_DIGEST}" \
--arg helm_chart_digest "${HELM_CHART_DIGEST}" \
--arg release_result "${RELEASE_RESULT}" \
--arg activation_result "${ACTIVATION_RESULT}" \
--arg packet_result "${PACKET_RESULT}" \
--arg asset_result "${ASSET_RESULT}" \
--arg container_result "${CONTAINER_RESULT}" \
--arg alias_result "${ALIAS_RESULT}" \
--arg helm_result "${HELM_RESULT}" \
'{
schema_version: ($schema_version | tonumber),
checked_at: $checked_at,
repository: $repository,
workflow_run: {id: $run_id, attempt: $run_attempt},
trigger: {
event: $trigger_event,
schedule: $trigger_schedule,
mode: $mode,
release_convergence_run: {
id: $convergence_run_id,
attempt: $convergence_run_attempt,
conclusion: $convergence_conclusion,
display_title: $convergence_display_title
}
},
release: {
tag: $tag,
id: $release_id,
source_sha: $source_sha,
activation_sha256: $activation_sha256,
server_image_digest: $server_image_digest,
control_plane_image_digest: $control_plane_image_digest,
helm_chart_digest: $helm_chart_digest
},
diagnostics: {
release_identity: $release_diagnostic[0],
activation_binding: $activation_diagnostic[0]
},
checks: {
release_resolution: $release_result,
activation_binding: $activation_result,
immutable_packet_and_provenance: $packet_result,
authenticated_assets: $asset_result,
container_identities: $container_result,
stable_container_aliases: $alias_result,
helm_identity: $helm_result
}
}' > release-continuity-evidence/continuity-evidence.json
cat release-continuity-evidence/continuity-evidence.json >> "${GITHUB_STEP_SUMMARY}"
- name: Retain continuity evidence
if: ${{ always() }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-continuity-${{ steps.release.outputs.tag || 'unresolved' }}-${{ github.run_id }}-${{ github.run_attempt }}
path: release-continuity-evidence
if-no-files-found: error
retention-days: 90
container-lifecycle:
name: Container trust and support window
if: ${{ github.event_name != 'workflow_run' && github.event.schedule != '17 */6 * * *' }}
runs-on: ubuntu-24.04
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Check governed image pins and lifecycle runway
run: >-
go test ./scripts/installtests
-run '^(TestGovernedContainerBaseSupportWindow|TestEveryDockerfilePinsExternalBases|TestNodeToolchainParity|TestIntegrationContainersUseGovernedImmutableBases|TestDockerBuildUsesCanonicalReleaseLdflags|TestProviderMSPControlPlaneDockerfileBuildsReleaseLicenseBinary)$'
govulncheck:
name: Go Vulnerability Scan (${{ matrix.directory }})
if: ${{ github.event_name != 'workflow_run' && github.event.schedule != '17 */6 * * *' }}
runs-on: ubuntu-24.04
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
directory:
- '.'
- 'tests/integration/mock-github-server'
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Go
uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6.4.0
with:
go-version-file: go.mod
- name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@v1.7.0
- name: Scan Go module for reachable vulnerabilities
working-directory: ${{ matrix.directory }}
run: govulncheck ./...
npm-audit:
name: npm Dependency Audit (${{ matrix.name }})
if: ${{ github.event_name != 'workflow_run' && github.event.schedule != '17 */6 * * *' }}
runs-on: ubuntu-24.04
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- name: root tooling
directory: '.'
- name: product frontend
directory: 'frontend-modern'
- name: account frontend
directory: 'internal/cloudcp/portal/frontend'
- name: integration tests
directory: 'tests/integration'
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Set up Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '24'
- name: Audit complete dependency graph
working-directory: ${{ matrix.directory }}
run: npm audit --package-lock-only
- name: Audit production dependencies
working-directory: ${{ matrix.directory }}
run: npm audit --package-lock-only --omit=dev