From 3f27ea53c7f389d93ceafb131cfe5f05ae6859a2 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:03:13 +0800 Subject: [PATCH 1/3] fix(ci): enforce every fixable cache security finding --- .github/runtime-unfixed.json | 4 + .github/workflows/security-release-gate.yml | 27 +++---- README.md | 6 ++ SECURITY-MAINTENANCE.md | 6 +- .../pasturestack-enforce-vulnerabilities.py | 58 ++++++++++++++ .../test-pasturestack-vulnerability-policy.py | 80 +++++++++++++++++++ 6 files changed, 164 insertions(+), 17 deletions(-) create mode 100644 .github/runtime-unfixed.json create mode 100644 scripts/pasturestack-enforce-vulnerabilities.py create mode 100644 scripts/test-pasturestack-vulnerability-policy.py diff --git a/.github/runtime-unfixed.json b/.github/runtime-unfixed.json new file mode 100644 index 000000000000..558a12bac35a --- /dev/null +++ b/.github/runtime-unfixed.json @@ -0,0 +1,4 @@ +{ + "repository": "PastureStack/distributed-cache-runtime", + "unfixed": [] +} diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 4ab974a7536d..71edae061235 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -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 @@ -384,7 +385,7 @@ 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" \ @@ -392,14 +393,14 @@ jobs: -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 \ @@ -409,10 +410,14 @@ 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 report in evidence/source-security.json evidence/hazelcast-5.7.6.trivy.json evidence/hazelcast-5.7.6.sbom.trivy.json; do + python scripts/pasturestack-enforce-vulnerabilities.py "$report" \ + .github/runtime-unfixed.json --repository PastureStack/distributed-cache-runtime + done python scripts/pasturestack-verify-maven-evidence.py \ --effective-pom evidence/maven-effective-pom.xml \ --sbom evidence/hazelcast-5.7.6.cdx.json \ @@ -436,7 +441,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 @@ -447,13 +451,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] @@ -490,10 +492,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' @@ -505,10 +507,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 diff --git a/README.md b/README.md index 10aa34cbaff8..1bb63e650572 100755 --- a/README.md +++ b/README.md @@ -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). diff --git a/SECURITY-MAINTENANCE.md b/SECURITY-MAINTENANCE.md index 97596fd5079c..7b085361542b 100644 --- a/SECURITY-MAINTENANCE.md +++ b/SECURITY-MAINTENANCE.md @@ -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. diff --git a/scripts/pasturestack-enforce-vulnerabilities.py b/scripts/pasturestack-enforce-vulnerabilities.py new file mode 100644 index 000000000000..d3e1d32819fd --- /dev/null +++ b/scripts/pasturestack-enforce-vulnerabilities.py @@ -0,0 +1,58 @@ +"""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() + parser.add_argument("report") + parser.add_argument("ledger") + parser.add_argument("--repository", required=True) + args = parser.parse_args() + failures = violations(json.loads(Path(args.report).read_text(encoding="utf-8")), + json.loads(Path(args.ledger).read_text(encoding="utf-8")), args.repository) + if failures: + raise SystemExit("Release vulnerability policy failed:\n" + "\n".join(failures)) + print("RELEASE_VULNERABILITY_POLICY_OK") diff --git a/scripts/test-pasturestack-vulnerability-policy.py b/scripts/test-pasturestack-vulnerability-policy.py new file mode 100644 index 000000000000..3d0de40f773f --- /dev/null +++ b/scripts/test-pasturestack-vulnerability-policy.py @@ -0,0 +1,80 @@ +import copy +import importlib.util +import pathlib +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) + +if __name__ == '__main__': + unittest.main() From d88cd836d78262678043041d42c228cbff157ba0 Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:04:12 +0800 Subject: [PATCH 2/3] fix(ci): constrain security policy inputs to fixed reports --- .github/workflows/security-release-gate.yml | 5 ++--- scripts/pasturestack-enforce-vulnerabilities.py | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/.github/workflows/security-release-gate.yml b/.github/workflows/security-release-gate.yml index 71edae061235..0be4a2c1a9d6 100644 --- a/.github/workflows/security-release-gate.yml +++ b/.github/workflows/security-release-gate.yml @@ -414,9 +414,8 @@ jobs: shell: bash run: | set -euo pipefail - for report in evidence/source-security.json evidence/hazelcast-5.7.6.trivy.json evidence/hazelcast-5.7.6.sbom.trivy.json; do - python scripts/pasturestack-enforce-vulnerabilities.py "$report" \ - .github/runtime-unfixed.json --repository PastureStack/distributed-cache-runtime + 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 \ diff --git a/scripts/pasturestack-enforce-vulnerabilities.py b/scripts/pasturestack-enforce-vulnerabilities.py index d3e1d32819fd..8183c3d7edee 100644 --- a/scripts/pasturestack-enforce-vulnerabilities.py +++ b/scripts/pasturestack-enforce-vulnerabilities.py @@ -3,6 +3,15 @@ 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 @@ -47,12 +56,10 @@ def violations(report, ledger, repository): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("report") - parser.add_argument("ledger") - parser.add_argument("--repository", required=True) + parser.add_argument("--phase", choices=tuple(REPORTS), required=True) args = parser.parse_args() - failures = violations(json.loads(Path(args.report).read_text(encoding="utf-8")), - json.loads(Path(args.ledger).read_text(encoding="utf-8")), args.repository) + 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") From 344438a4205f8dc96917af71c8d66a54ceeaefbd Mon Sep 17 00:00:00 2001 From: chen21019 <19357113+chen21019@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:05:01 +0800 Subject: [PATCH 3/3] test(ci): reject free-form vulnerability report paths --- scripts/test-pasturestack-vulnerability-policy.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scripts/test-pasturestack-vulnerability-policy.py b/scripts/test-pasturestack-vulnerability-policy.py index 3d0de40f773f..76a159268057 100644 --- a/scripts/test-pasturestack-vulnerability-policy.py +++ b/scripts/test-pasturestack-vulnerability-policy.py @@ -1,6 +1,7 @@ import copy import importlib.util import pathlib +import subprocess import sys import unittest @@ -76,5 +77,12 @@ def test_incomplete_evidence_fails_closed(self): 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()