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
4 changes: 4 additions & 0 deletions .github/runtime-unfixed.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
{
"repository": "PastureStack/distributed-cache-runtime",
"unfixed": []
}
26 changes: 12 additions & 14 deletions .github/workflows/security-release-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ jobs:
run: |
set -euo pipefail
bash scripts/setup-ci-python-tools
python scripts/test-pasturestack-vulnerability-policy.py -v

- name: Verify candidate identity and supply-chain pins
id: candidate
Expand Down Expand Up @@ -384,22 +385,22 @@ jobs:
-v "$trivy_cache:/root/.cache/trivy" \
"$TRIVY_IMAGE" fs \
--cache-dir /root/.cache/trivy --skip-db-update --offline-scan \
--scanners vuln,secret --severity CRITICAL,HIGH --format json \
--scanners vuln,secret --list-all-pkgs --format json \
--output /evidence/source-security.json /scan
docker run --rm --network none \
-v "$PWD/dist:/artifact:ro" \
-v "$PWD/evidence:/evidence" \
-v "$trivy_cache:/root/.cache/trivy" \
"$TRIVY_IMAGE" rootfs --pkg-types library \
--cache-dir /root/.cache/trivy --skip-db-update --offline-scan \
--scanners vuln --severity CRITICAL,HIGH --format json \
--scanners vuln --list-all-pkgs --format json \
--output /evidence/hazelcast-5.7.6.trivy.json /artifact/hazelcast-5.7.6.jar
docker run --rm --network none \
-v "$PWD/evidence:/evidence" \
-v "$trivy_cache:/root/.cache/trivy" \
"$TRIVY_IMAGE" sbom \
--cache-dir /root/.cache/trivy --skip-db-update --skip-java-db-update --offline-scan \
--scanners vuln --severity CRITICAL,HIGH --format json \
--scanners vuln --list-all-pkgs --format json \
--output /evidence/hazelcast-5.7.6.sbom.trivy.json \
/evidence/hazelcast-5.7.6.cdx.json
sha256sum evidence/maven-effective-pom.xml \
Expand All @@ -409,10 +410,13 @@ jobs:
evidence/native-resources.sha256 evidence/runtime-toolchain-boundary.txt \
> evidence/security-evidence.sha256

- name: Enforce complete zero-Critical and zero-High evidence
- name: Enforce complete all-severity vulnerability evidence
shell: bash
run: |
set -euo pipefail
for phase in source artifact dependencies; do
python scripts/pasturestack-enforce-vulnerabilities.py --phase "$phase"
done
python scripts/pasturestack-verify-maven-evidence.py \
--effective-pom evidence/maven-effective-pom.xml \
--sbom evidence/hazelcast-5.7.6.cdx.json \
Expand All @@ -436,7 +440,6 @@ jobs:
item
for result in source.get('Results', [])
for item in result.get('Vulnerabilities') or []
if item.get('Severity') in {'CRITICAL', 'HIGH'}
]
source_secrets = [
item
Expand All @@ -447,13 +450,11 @@ jobs:
item
for result in artifact.get('Results', [])
for item in result.get('Vulnerabilities') or []
if item.get('Severity') in {'CRITICAL', 'HIGH'}
]
dependency_vulnerabilities = [
item
for result in dependency_scan.get('Results', [])
for item in result.get('Vulnerabilities') or []
if item.get('Severity') in {'CRITICAL', 'HIGH'}
]
components = sbom.get('components') or []
component_refs = [item.get('bom-ref') for item in components]
Expand Down Expand Up @@ -490,10 +491,10 @@ jobs:
if not item.get('group') or not item.get('name') or not item.get('version') or not item.get('purl')
]
summary = (
f'source_critical_high={len(source_vulnerabilities)}\n'
f'source_all_severities={len(source_vulnerabilities)}\n'
f'source_secrets={len(source_secrets)}\n'
f'artifact_critical_high={len(artifact_vulnerabilities)}\n'
f'dependency_critical_high={len(dependency_vulnerabilities)}\n'
f'artifact_all_severities={len(artifact_vulnerabilities)}\n'
f'dependency_all_severities={len(dependency_vulnerabilities)}\n'
f'sbom_spec={sbom.get("specVersion", "missing")}\n'
f'sbom_components={len(components)}\n'
f'sbom_dependencies={len(dependencies)}\n'
Expand All @@ -505,10 +506,7 @@ jobs:
pathlib.Path('evidence/security-summary.txt').write_text(summary, encoding='utf-8')
print(summary, end='')
if (
source_vulnerabilities
or source_secrets
or artifact_vulnerabilities
or dependency_vulnerabilities
source_secrets
or sbom.get('specVersion') != '1.6'
or not components
or missing_identity
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ not contain a JRE or Python runtime. The optional OCI build retains Ubuntu
24.04 and installs its matching CA security update before the verified JDK.
The generated Hazelcast cluster version remains `5.7.3`.

The source, released JAR and resolved-runtime SBOM scans retain every severity.
Any available fix, Critical/High/Unknown finding or secret blocks publication;
only exact reviewed Low/Medium findings with official no-fix evidence can pass.
The committed no-fix ledger is currently empty. This CI policy update does not
change or republish the existing `5.7.6` JAR.

Earlier public Maven artifacts remain immutable historical evidence. The
current public GitHub Release is
[`v5.7.6`](https://github.com/PastureStack/distributed-cache-runtime/releases/tag/v5.7.6).
Expand Down
6 changes: 3 additions & 3 deletions SECURITY-MAINTENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,9 +160,9 @@ A release is acceptable only when all of the following are true:
4. The produced file is `hazelcast-5.7.5.jar`; its embedded core Maven metadata reports 5.7.5, its generated cluster runtime metadata reports numeric version 5.7.3, and its embedded Jackson metadata reports Jackson 2.22.3 and Jackson 3.2.3. The full source commit and abbreviated revision embedded in `GeneratedBuildProperties` must match the OCI revision supplied to the build; unresolved placeholders are rejected during packaging.
5. Relevant core, Spring, database, and distribution integration tests pass on Java 25 LTS. The focused source-boundary suite must retain its expected suite and test counts so an accidentally undiscovered test cannot appear successful.
6. Both standalone source-boundary gates complete successfully on the release JDK.
7. `trivy fs --offline-scan --scanners vuln --severity CRITICAL,HIGH` reports zero Critical and zero High findings across the resolved source POMs.
8. `trivy rootfs --scanners vuln --severity CRITICAL,HIGH` reports zero Critical and zero High findings for the produced JAR.
9. Maven Help Plugin `3.5.2` generates the complete active-reactor effective POM, and the gate rejects any active direct dependency, managed dependency, build plugin, or managed plugin without a resolved version. CycloneDX Maven Plugin `2.9.3` then generates a CycloneDX `1.6` SBOM for the actual `hazelcast` runtime module from its resolved compile/runtime dependency graph. The gate compares every required SBOM coordinate with an independently generated Maven Dependency Plugin `3.11.0` runtime tree for the same module, independently validates the SBOM with CycloneDX CLI `0.33.1`, requires unique component identities and a populated dependency graph rooted at the reviewed runtime component, and submits that complete SBOM to Trivy for a separate zero-Critical/High dependency scan. All three Maven evidence-plugin JARs and POMs are fetched from Maven Central and checked against committed SHA-256 values before and after use. The source revision, every Maven input hash, the effective POM, the runtime tree, the CycloneDX SBOM, and their validation summaries are retained together. This intentionally excludes reactor test-support modules from the deployed SBOM and replaces filesystem-only JAR inventory, which cannot reconstruct dependencies relocated into a shaded artifact.
7. `trivy fs --offline-scan --scanners vuln,secret --list-all-pkgs` retains all severity levels across the resolved source POMs. The release policy rejects every available `FixedVersion`, every Critical/High/Unknown finding, and every secret. Only exact vulnerability/package/version/PURL/severity entries with official no-fix evidence in `.github/runtime-unfixed.json` may admit Low/Medium findings; this ledger is currently empty. Missing or malformed reports fail closed.
8. `trivy rootfs --scanners vuln --list-all-pkgs` retains all severity levels for the produced JAR and applies the same exact policy. The policy is regression-tested against fixed Low/Medium findings, Unknown severity, identity drift, malformed evidence, and secrets.
9. Maven Help Plugin `3.5.2` generates the complete active-reactor effective POM, and the gate rejects any active direct dependency, managed dependency, build plugin, or managed plugin without a resolved version. CycloneDX Maven Plugin `2.9.3` then generates a CycloneDX `1.6` SBOM for the actual `hazelcast` runtime module from its resolved compile/runtime dependency graph. The gate compares every required SBOM coordinate with an independently generated Maven Dependency Plugin `3.11.0` runtime tree for the same module, independently validates the SBOM with CycloneDX CLI `0.33.1`, requires unique component identities and a populated dependency graph rooted at the reviewed runtime component, and submits that complete SBOM to Trivy for a separate all-severity dependency scan governed by the same exact no-fix policy. All three Maven evidence-plugin JARs and POMs are fetched from Maven Central and checked against committed SHA-256 values before and after use. The source revision, every Maven input hash, the effective POM, the runtime tree, the CycloneDX SBOM, and their validation summaries are retained together. This intentionally excludes reactor test-support modules from the deployed SBOM and replaces filesystem-only JAR inventory, which cannot reconstruct dependencies relocated into a shaded artifact.
10. The JAR contains the upstream `LICENSE` and `NOTICE` material.
11. GitHub CodeQL reports zero current-source Critical and High findings for the release commit. Historical alerts against removed workflow snapshots are documented separately and do not replace this current-source gate.
12. The published GitHub release JAR is read back over HTTPS and matches the reviewed bytes, embedded source revision and metadata. The Engine pins its release tag, asset ID and SHA-256; download gateways must not change those identities. Any separately published OCI carrier must additionally be public and read back by manifest and layer digest. `v5.7.5` publishes the GitHub assets only, not a new carrier.
Expand Down
65 changes: 65 additions & 0 deletions scripts/pasturestack-enforce-vulnerabilities.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
"""Enforce release findings; only exact, reviewed unfixed low/medium entries may pass."""
import argparse
import json
from pathlib import Path

REPOSITORY = "PastureStack/distributed-cache-runtime"
ROOT = Path(__file__).resolve().parent.parent
REPORTS = {
"source": ROOT / "evidence/source-security.json",
"artifact": ROOT / "evidence/hazelcast-5.7.6.trivy.json",
"dependencies": ROOT / "evidence/hazelcast-5.7.6.sbom.trivy.json",
}
LEDGER = ROOT / ".github/runtime-unfixed.json"


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()
parser.add_argument("--phase", choices=tuple(REPORTS), required=True)
args = parser.parse_args()
failures = violations(json.loads(REPORTS[args.phase].read_text(encoding="utf-8")),
json.loads(LEDGER.read_text(encoding="utf-8")), REPOSITORY)
if failures:
raise SystemExit("Release vulnerability policy failed:\n" + "\n".join(failures))
print("RELEASE_VULNERABILITY_POLICY_OK")
88 changes: 88 additions & 0 deletions scripts/test-pasturestack-vulnerability-policy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import copy
import importlib.util
import pathlib
import subprocess
import sys
import unittest

sys.dont_write_bytecode = True

PATH = pathlib.Path(__file__).with_name('pasturestack-enforce-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/distributed-cache-runtime'


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):
self.finding['FixedVersion'] = '2'
self.assertTrue(GATE.violations(self.report, self.ledger, REPO))

def test_fixed_low_is_not_an_unfixed_exception(self):
self.finding['Severity'] = 'LOW'
self.ledger['unfixed'][0]['severity'] = 'LOW'
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), '--phase', '../../etc/passwd'],
capture_output=True, text=True)
self.assertEqual(result.returncode, 2)
self.assertIn('invalid choice', result.stderr)

if __name__ == '__main__':
unittest.main()
Loading