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
842 changes: 842 additions & 0 deletions .github/dapper-unfixed.json

Large diffs are not rendered by default.

599 changes: 599 additions & 0 deletions .github/runtime-unfixed.json

Large diffs are not rendered by default.

68 changes: 68 additions & 0 deletions .github/scripts/enforce-runtime-vulnerabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
"""Enforce release findings; only exact, reviewed unfixed low/medium entries may pass."""
import argparse
import json
from pathlib import Path


def violations(report, ledger, repository):
if (not isinstance(report, dict) or report.get("SchemaVersion") != 2
or not isinstance(report.get("Results"), list) or not report["Results"]):
raise ValueError("missing or malformed Trivy scan results")
for result in report["Results"]:
if not isinstance(result, dict) or not isinstance(result.get("Target"), str):
raise ValueError("malformed Trivy result")
for key in ("Vulnerabilities", "Secrets"):
if key in result and result[key] is not None and not isinstance(result[key], list):
raise ValueError("malformed Trivy findings")
if ledger.get("repository") != repository:
raise ValueError("allowlist repository mismatch")
reviewed = {}
for entry in ledger.get("unfixed", []):
key = (entry["id"], entry["package"], entry["version"], entry.get("purl", ""))
if key in reviewed:
raise ValueError("duplicate reviewed finding")
if (entry.get("severity") not in ("LOW", "MEDIUM")
or entry.get("status") != "no_official_fix"
or not entry.get("officialEvidence", "").startswith("https://")
or not entry.get("reason") or not entry.get("checkedAt")):
raise ValueError("incomplete official no-fix evidence")
reviewed[key] = entry
errors = []
for result in report.get("Results") or []:
if result.get("Secrets"):
errors.append("secret findings remain")
for finding in result.get("Vulnerabilities") or []:
severity = finding.get("Severity", "UNKNOWN")
key = (finding.get("VulnerabilityID"), finding.get("PkgName"),
finding.get("InstalledVersion"),
(finding.get("PkgIdentifier") or {}).get("PURL", ""))
entry = reviewed.get(key)
fixed = str(finding.get("FixedVersion") or "").strip()
if (severity not in ("LOW", "MEDIUM") or fixed
or entry is None or entry["severity"] != severity):
errors.append("%s %s@%s severity=%s fixed=%s" % (
key[0], key[1], key[2], severity, fixed or "none"))
return errors


if __name__ == "__main__":
parser = argparse.ArgumentParser()
# Workflows select a known evidence scope, never an arbitrary input path.
reports = {
"source": "evidence/source-security.json",
"product": "evidence/product-security.json",
"runtime": "evidence/runtime-security.json",
"dapper": "evidence/dapper-nonheader-security.json",
"release-runtime": "dist/release/runtime-security.json",
}
parser.add_argument("scope", choices=tuple(reports))
parser.add_argument("--repository", required=True)
args = parser.parse_args()
report_path = Path(reports[args.scope])
ledger_path = Path(".github/dapper-unfixed.json" if args.scope == "dapper"
else ".github/runtime-unfixed.json")
failures = violations(json.loads(report_path.read_text(encoding="utf-8")),
json.loads(ledger_path.read_text(encoding="utf-8")), args.repository)
if failures:
raise SystemExit("Release vulnerability policy failed:\n" + "\n".join(failures))
print("RELEASE_VULNERABILITY_POLICY_OK")
82 changes: 82 additions & 0 deletions .github/scripts/test_runtime_vulnerability_policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import copy
import importlib.util
import pathlib
import subprocess
import sys
import unittest

PATH = pathlib.Path(__file__).with_name('enforce-runtime-vulnerabilities.py')
SPEC = importlib.util.spec_from_file_location('gate', PATH)
GATE = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(GATE)
REPO = 'PastureStack/ipsec-vxlan-overlay-network'


class RuntimePolicy(unittest.TestCase):
def setUp(self):
self.finding = {'VulnerabilityID': 'TEST-1', 'PkgName': 'lib', 'InstalledVersion': '1',
'Severity': 'MEDIUM', 'FixedVersion': '',
'PkgIdentifier': {'PURL': 'pkg:test/lib@1'}}
self.report = {'SchemaVersion': 2, 'Results': [{'Target': 'fixture', 'Vulnerabilities': [self.finding]}]}
self.ledger = {'repository': REPO, 'unfixed': [{'id': 'TEST-1', 'package': 'lib',
'version': '1', 'severity': 'MEDIUM', 'purl': 'pkg:test/lib@1',
'status': 'no_official_fix', 'officialEvidence': 'https://fixture.invalid/advisory',
'reason': 'Test fixture only', 'checkedAt': '2026-10-23'}]}

def test_exact_reviewed_unfixed_medium_passes(self):
self.assertEqual(GATE.violations(self.report, self.ledger, REPO), [])

def test_clean_scan_passes(self):
self.assertEqual(GATE.violations({'SchemaVersion': 2, 'Results': [{'Target': 'fixture'}]},
{'repository': REPO, 'unfixed': []}, REPO), [])

def test_missing_or_malformed_scan_fails_closed(self):
for report in ({}, {'Results': []}, {'SchemaVersion': 2, 'Results': []},
{'SchemaVersion': 2, 'Results': [{}]},
{'SchemaVersion': 2, 'Results': [{'Target': 'fixture', 'Vulnerabilities': {}}]}):
with self.assertRaises(ValueError):
GATE.violations(report, self.ledger, REPO)

def test_unregistered_medium_blocks(self):
self.ledger['unfixed'] = []
self.assertTrue(GATE.violations(self.report, self.ledger, REPO))

def test_fixed_medium_blocks(self):
for severity in ('LOW', 'MEDIUM'):
self.finding['Severity'] = severity
self.ledger['unfixed'][0]['severity'] = severity
self.finding['FixedVersion'] = '2'
self.assertTrue(GATE.violations(self.report, self.ledger, REPO))

def test_high_critical_and_unknown_always_block(self):
for severity in ('HIGH', 'CRITICAL', 'UNKNOWN'):
self.finding['Severity'] = severity
self.assertTrue(GATE.violations(self.report, self.ledger, REPO))

def test_version_or_purl_drift_blocks(self):
for key in ('version', 'purl'):
ledger = copy.deepcopy(self.ledger)
ledger['unfixed'][0][key] = 'different'
self.assertTrue(GATE.violations(self.report, ledger, REPO))

def test_secrets_always_block(self):
self.report['Results'][0]['Secrets'] = [{'RuleID': 'test'}]
self.assertTrue(GATE.violations(self.report, self.ledger, REPO))

def test_repository_drift_fails_closed(self):
with self.assertRaises(ValueError):
GATE.violations(self.report, self.ledger, 'another/repo')

def test_incomplete_evidence_fails_closed(self):
del self.ledger['unfixed'][0]['checkedAt']
with self.assertRaises(ValueError):
GATE.violations(self.report, self.ledger, REPO)

def test_cli_rejects_free_form_paths(self):
result = subprocess.run([sys.executable, str(PATH), "../../etc/passwd",
"--repository", REPO], capture_output=True, text=True)
self.assertEqual(result.returncode, 2)
self.assertIn("invalid choice", result.stderr)

if __name__ == '__main__':
unittest.main()
2 changes: 1 addition & 1 deletion .github/workflows/codeql-verification.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ jobs:
cd "$GITHUB_WORKSPACE"
go test -run '^$' ./...
go test -run '^$' -tags=integration ./store
VERSION_OVERRIDE=0.14.39 ./scripts/build
VERSION_OVERRIDE=0.14.40 ./scripts/build

- name: Analyze without publishing temporary alerts
uses: github/codeql-action/analyze@db488ddef3bf6cb639b32c2e9a7c0a7ea8271d28 # v4.37.8
Expand Down
51 changes: 32 additions & 19 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: Release IPsec overlay

# A release is dispatched from main only after an annotated v0.14.39 tag and
# A release is dispatched from main only after an annotated v0.14.40 tag and
# successful, same-commit Security release gate and CodeQL verification runs.
# GHCR, registry attestations, and GitHub Releases are not an atomic transaction.
# All source/product/runtime gates precede the image push. A later failure may
Expand All @@ -9,11 +9,11 @@ on:
workflow_dispatch:
inputs:
release_tag:
description: Existing annotated, numeric v0.14.39 tag at main HEAD
description: Existing annotated, numeric v0.14.40 tag at main HEAD
required: true
type: choice
options:
- v0.14.39
- v0.14.40
security_run_id:
description: Successful same-SHA Security release gate run ID
required: true
Expand All @@ -35,7 +35,7 @@ permissions:
attestations: write

concurrency:
group: ipsec-vxlan-overlay-release-v0.14.39
group: ipsec-vxlan-overlay-release-v0.14.40
cancel-in-progress: false

jobs:
Expand All @@ -48,8 +48,8 @@ jobs:
CODEQL_RUN_ID: ${{ inputs.codeql_run_id }}
RESUME_DIGEST: ${{ inputs.resume_digest }}
GH_TOKEN: ${{ github.token }}
SERVICE_IMAGE: ghcr.io/pasturestack/ipsec-vxlan-overlay-network:v0.14.39
LOCAL_IMAGE: local/pasturestack/ipsec-vxlan-overlay-network:0.14.39
SERVICE_IMAGE: ghcr.io/pasturestack/ipsec-vxlan-overlay-network:v0.14.40
LOCAL_IMAGE: local/pasturestack/ipsec-vxlan-overlay-network:0.14.40
TRIVY_IMAGE: aquasec/trivy:0.74.0@sha256:62b1e65e8869bc4b4c6aa4fa2b21595256c7c2f6018a9d9ad61caf87187c1969
GOFLAGS: -mod=vendor
GOWORK: off
Expand All @@ -67,7 +67,7 @@ jobs:
run: |
set -euo pipefail
test "$GITHUB_REF" = refs/heads/main
test "$RELEASE_TAG" = v0.14.39
test "$RELEASE_TAG" = v0.14.40
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
[[ "$SECURITY_RUN_ID" =~ ^[0-9]+$ ]]
[[ "$CODEQL_RUN_ID" =~ ^[0-9]+$ ]]
Expand Down Expand Up @@ -115,7 +115,8 @@ jobs:
--name "ipsec-vxlan-overlay-network-security-$SOURCE_SHA" --dir evidence
for file in SHA256SUMS source-tree.txt source-locks.sha256 product.sha256 \
security-summary.txt source-security.json product-security.json \
runtime-security.json source-sbom.cdx.json; do
runtime-security.json source-sbom.cdx.json dapper-security.json \
dapper-nonheader-security.json; do
test -s "evidence/$file"
done
sha256sum --check evidence/SHA256SUMS
Expand All @@ -124,8 +125,17 @@ jobs:
sha256sum --check evidence/source-locks.sha256
jq -e '.serialNumber and ((.components // []) | length > 0)' \
evidence/source-sbom.cdx.json >/dev/null
python3 .github/scripts/enforce-runtime-vulnerabilities.py \
dapper --repository "$GITHUB_REPOSITORY"
for binary in ipsec-vxlan-overlay-network ipsec-vxlan-overlay-topology pasture-cni-resolver; do
jq -e --arg binary "$binary" \
'[.Results[]? | select(.Type == "gobinary" and .Target == $binary) | .Packages[]?] | length > 0' \
evidence/product-security.json >/dev/null
done
for scope in source product runtime; do
report="evidence/${scope}-security.json"
python3 .github/scripts/enforce-runtime-vulnerabilities.py \
"$scope" --repository "$GITHUB_REPOSITORY"
for field in secrets critical high; do
grep -Fx "${scope}_${field}=0" evidence/security-summary.txt >/dev/null
done
Expand All @@ -152,15 +162,15 @@ jobs:
shell: bash
run: |
set -euo pipefail
VERSION_OVERRIDE=0.14.39 ./scripts/build
VERSION_OVERRIDE=0.14.40 ./scripts/build
sha256sum --check evidence/product.sha256
if [ -z "$RESUME_DIGEST" ]; then
VERSION_OVERRIDE=0.14.39 TAG=0.14.39 REPO=local/pasturestack \
VERSION_OVERRIDE=0.14.40 TAG=0.14.40 REPO=local/pasturestack \
IMAGE_REVISION="$SOURCE_SHA" ./scripts/package
test "$(wc -l < dist/images)" -eq 1
grep -Fx "$LOCAL_IMAGE" dist/images
test "$(docker image inspect "$LOCAL_IMAGE" \
--format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = 0.14.39
--format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = 0.14.40
test "$(docker image inspect "$LOCAL_IMAGE" \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$SOURCE_SHA"
docker tag "$LOCAL_IMAGE" "$SERVICE_IMAGE"
Expand All @@ -178,7 +188,7 @@ jobs:
docker pull "$SERVICE_IMAGE@$RESUME_DIGEST"
docker tag "$SERVICE_IMAGE@$RESUME_DIGEST" "$SERVICE_IMAGE"
test "$(docker image inspect "$SERVICE_IMAGE" \
--format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = 0.14.39
--format '{{index .Config.Labels "org.opencontainers.image.version"}}')" = 0.14.40
test "$(docker image inspect "$SERVICE_IMAGE" \
--format '{{index .Config.Labels "org.opencontainers.image.revision"}}')" = "$SOURCE_SHA"
verification_container="$(docker create "$SERVICE_IMAGE")"
Expand Down Expand Up @@ -207,9 +217,12 @@ jobs:
docker run --rm --network none -v /var/run/docker.sock:/var/run/docker.sock \
-v "$cache:/root/.cache/trivy" -v "$release_dir:/release" "$TRIVY_IMAGE" \
image --cache-dir /root/.cache/trivy --skip-db-update --offline-scan \
--scanners vuln,secret --severity CRITICAL,HIGH --format json \
--scanners vuln,secret --severity UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL --format json \
--output /release/runtime-security.json "$SERVICE_IMAGE"
test -s "$release_dir/runtime-security.json"
python3 .github/scripts/test_runtime_vulnerability_policy.py
python3 .github/scripts/enforce-runtime-vulnerabilities.py \
release-runtime --repository "$GITHUB_REPOSITORY"
test "$(jq '[.Results[]?.Secrets[]?] | length' "$release_dir/runtime-security.json")" -eq 0
test "$(jq '[.Results[]?.Vulnerabilities[]? |
select(.Severity == "CRITICAL" or .Severity == "HIGH")] | length' \
Expand Down Expand Up @@ -315,7 +328,7 @@ jobs:
run: |
set -euo pipefail
release_dir="dist/release"
product_dir="$release_dir/ipsec-vxlan-overlay-network-0.14.39-linux-amd64"
product_dir="$release_dir/ipsec-vxlan-overlay-network-0.14.40-linux-amd64"
mkdir -p "$product_dir"
cp bin/ipsec-vxlan-overlay-network bin/ipsec-vxlan-overlay-topology bin/pasture-cni-resolver \
LICENSE ORIGIN.md SECURITY.md THIRD_PARTY_NOTICES.md "$product_dir/"
Expand All @@ -327,11 +340,11 @@ jobs:
find "$product_dir" -exec touch -h -d "@$source_epoch" {} +
tar --sort=name --mtime="@$source_epoch" --owner=0 --group=0 \
--numeric-owner -C "$release_dir" \
-cJf "$release_dir/ipsec-vxlan-overlay-network-0.14.39-linux-amd64.tar.xz" \
ipsec-vxlan-overlay-network-0.14.39-linux-amd64
-cJf "$release_dir/ipsec-vxlan-overlay-network-0.14.40-linux-amd64.tar.xz" \
ipsec-vxlan-overlay-network-0.14.40-linux-amd64
(
cd "$release_dir"
sha256sum ipsec-vxlan-overlay-network-0.14.39-linux-amd64.tar.xz \
sha256sum ipsec-vxlan-overlay-network-0.14.40-linux-amd64.tar.xz \
source-sbom.cdx.json image-sbom.cdx.json product.sha256 \
runtime-security.json gate-security-summary.txt image-digest.txt \
release-identity.txt > SHA256SUMS
Expand All @@ -355,9 +368,9 @@ jobs:
run: |
set -euo pipefail
gh release create "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --verify-tag \
--title 'IPsec Overlay v0.14.39' \
--title 'IPsec Overlay v0.14.40' \
--notes "Source ${SOURCE_SHA}; Security gate run ${SECURITY_RUN_ID}; CodeQL run ${CODEQL_RUN_ID}. GHCR image ${SERVICE_IMAGE}@$(cat dist/release/image-digest.txt | sed 's/^.*@//')." \
dist/release/ipsec-vxlan-overlay-network-0.14.39-linux-amd64.tar.xz \
dist/release/ipsec-vxlan-overlay-network-0.14.40-linux-amd64.tar.xz \
dist/release/source-sbom.cdx.json \
dist/release/image-sbom.cdx.json \
dist/release/product.sha256 \
Expand Down
Loading
Loading