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 docs/releases/server-1.6.546.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Raw scans and SBOMs remain separate from vulnerability dispositions. An
officially unfixed LOW/MEDIUM finding is tracked against its exact artifact,
package and version; that is not remediation. Available fixes, unregistered
findings, HIGH/CRITICAL issues and secrets still block publication.
The OpenPGP module advisory retains the independently reviewed component
dispositions. Before applying them, the publisher verifies each affected
executable path, module version and exact binary SHA256; a new or changed
consumer cannot inherit the exception just by using the same module version.

## Upgrade and rollback

Expand Down
47 changes: 47 additions & 0 deletions server/security/check_scan_coverage.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Reject a clean-looking scan that did not analyze the active Go payloads."""
import hashlib
import json
from pathlib import Path, PurePosixPath

Expand All @@ -13,6 +14,46 @@
'host-api-linux-amd64', 'node-agent-linux-amd64',
'node-agent-windows-amd64.exe', 'per-host-subnet-windows-amd64.exe',
})
# The module-level VEX must not silently apply to a new OpenPGP consumer.
# These exact shipped executables have separately verified source/binary
# dependency graphs; the image builder pins their producer and payload SHA256.
OPENPGP_VEX_SCOPE = frozenset({
('usr/bin/authentication-service.real', 'pkg:golang/golang.org/x/crypto@v0.57.0'),
('usr/bin/docker-machine', 'pkg:golang/golang.org/x/crypto@v0.57.0'),
('usr/bin/docker-machine-driver-packet', 'pkg:golang/golang.org/x/crypto@v0.57.0'),
('usr/bin/compose-executor.real', 'pkg:golang/golang.org/x/crypto@v0.56.0'),
('usr/bin/host-provisioner.real', 'pkg:golang/golang.org/x/crypto@v0.56.0'),
})
OPENPGP_VEX_BINARY_SHA256 = {
'usr/bin/authentication-service.real': 'e121cee8b7a6bd3c0eb6fe3e15ff304b78119b426642ed76f13e3d92be161192',
'usr/bin/docker-machine': '9d379214de116542cb0e2b1a09c17409fd59fd2b0109428896d81de9cf9c0d31',
'usr/bin/docker-machine-driver-packet': 'b3e5c71928fcebe1bb648f79ebbf38af9ad32d6f6dbb87587e1b92c6c331e100',
'usr/bin/compose-executor.real': '0f0c0ada23564572b569e1c73951ca363c0dc262fe7bba9c2474106650c4eac5',
'usr/bin/host-provisioner.real': '92c6d9ce1222a93802ff2c92b100eeb6c58286f3fca90698f9687b92d6fa9dff',
}


def validate_vex_payloads(report, rootfs):
"""Bind a component-absence claim to the reviewed binary, not just its name."""
rootfs = rootfs.resolve()
checked = set()
for result in report['Results']:
if not any(v.get('VulnerabilityID') == 'GO-2026-5932'
for v in result.get('Vulnerabilities') or []):
continue
target = result['Target']
expected = OPENPGP_VEX_BINARY_SHA256.get(target)
payload = (rootfs / target).resolve()
if not expected or rootfs not in payload.parents or not payload.is_file():
raise ValueError('Unreviewed OpenPGP payload: ' + target)
checksum = hashlib.sha256()
with payload.open('rb') as stream:
for chunk in iter(lambda: stream.read(1024 * 1024), b''):
checksum.update(chunk)
if checksum.hexdigest() != expected:
raise ValueError('Changed OpenPGP VEX payload: ' + target)
checked.add(target)
return len(checked)


def validate_coverage(report, expected):
Expand All @@ -28,6 +69,11 @@ def validate_coverage(report, expected):
packages = result.get('Packages')
if not isinstance(packages, list) or not packages:
raise ValueError('Go binary dependency analysis is missing')
for finding in result.get('Vulnerabilities') or []:
if finding.get('VulnerabilityID') == 'GO-2026-5932':
identity = (result.get('Target'), finding.get('PkgIdentifier', {}).get('PURL'))
if identity not in OPENPGP_VEX_SCOPE:
raise ValueError('Unreviewed OpenPGP VEX scope: ' + str(identity))
found.add(PurePosixPath(result['Target']).name)
missing = expected - found
if missing:
Expand All @@ -41,4 +87,5 @@ def validate_coverage(report, expected):
nested = json.loads(Path('server-nested-scan-raw.json').read_text(encoding='utf-8'))
root_count = validate_coverage(root, ROOT_BINARIES)
nested_count = validate_coverage(nested, NESTED_BINARIES)
validate_vex_payloads(root, Path('../server-rootfs'))
print('SERVER_GO_SCAN_COVERAGE_OK root=%d nested=%d' % (root_count, nested_count))
8 changes: 4 additions & 4 deletions server/security/openvex.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
"@context": "https://openvex.dev/ns/v0.2.0",
"@id": "https://github.com/PastureStack/server/security/openvex/v1.6.546",
"author": "PastureStack Security",
"timestamp": "2026-10-23T17:46:40Z",
"version": 59,
"timestamp": "2026-10-23T18:02:09Z",
"version": 60,
"statements": [
{
"vulnerability": {
Expand Down Expand Up @@ -339,15 +339,15 @@
},
"products": [
{
"@id": "pkg:golang/golang.org/x/crypto@v0.55.0"
"@id": "pkg:golang/golang.org/x/crypto@v0.57.0"
},
{
"@id": "pkg:golang/golang.org/x/crypto@v0.56.0"
}
],
"status": "not_affected",
"justification": "vulnerable_code_not_present",
"impact_statement": "GO-2026-5932 applies only to the unmaintained golang.org/x/crypto/openpgp packages. The committed source graph and govulncheck evidence contain no openpgp import or reachable symbol in authentication-service, compose-executor, or host-provisioner."
"impact_statement": "GO-2026-5932 applies only to the unmaintained golang.org/x/crypto/openpgp packages. The exact authentication-service 0.4.46 and machine-driver-bundle 0.16.5 release dependency graphs and govulncheck evidence contain no OpenPGP import or reachable symbol, including both docker-machine and docker-machine-driver-packet. The existing compose-executor and host-provisioner v0.56.0 paths remain unchanged. Before applying this VEX the publisher rejects any other executable path or module version using the reviewed occurrence allowlist; this does not authorize a new OpenPGP consumer."
},
{
"vulnerability": {
Expand Down
60 changes: 59 additions & 1 deletion server/security/test_scan_coverage.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import copy
import hashlib
from pathlib import Path
import tempfile
import unittest
from unittest.mock import patch

from check_scan_coverage import validate_coverage, ROOT_BINARIES, NESTED_BINARIES
from check_scan_coverage import (validate_coverage, validate_vex_payloads,
ROOT_BINARIES, NESTED_BINARIES, OPENPGP_VEX_SCOPE)


class CoverageTest(unittest.TestCase):
Expand Down Expand Up @@ -53,6 +58,59 @@ def test_scan_is_not_mutated(self):
validate_coverage(report, ROOT_BINARIES)
self.assertEqual(report, original)

def test_reviewed_openpgp_module_occurrences_are_allowed(self):
report = self.fixture(ROOT_BINARIES)
for target, purl in OPENPGP_VEX_SCOPE:
result = next(item for item in report['Results'] if item['Target'] == target)
result['Vulnerabilities'] = [{'VulnerabilityID': 'GO-2026-5932',
'PkgIdentifier': {'PURL': purl}}]
self.assertEqual(validate_coverage(report, ROOT_BINARIES), len(ROOT_BINARIES))

def test_unreviewed_openpgp_target_or_version_blocks_before_vex(self):
for target, purl in (
('usr/bin/govc', 'pkg:golang/golang.org/x/crypto@v0.57.0'),
('usr/bin/docker-machine', 'pkg:golang/golang.org/x/crypto@v0.58.0'),
('tmp/docker-machine', 'pkg:golang/golang.org/x/crypto@v0.57.0'),
('usr/bin/docker-machine', None)):
with self.subTest(target=target, purl=purl):
report = self.fixture(ROOT_BINARIES)
report['Results'].append({'Target': target, 'Type': 'gobinary',
'Packages': [{'Name': 'golang.org/x/crypto', 'Version': 'v0.57.0'}],
'Vulnerabilities': [{'VulnerabilityID': 'GO-2026-5932',
'PkgIdentifier': {'PURL': purl}}]})
with self.assertRaisesRegex(ValueError, 'Unreviewed OpenPGP'):
validate_coverage(report, ROOT_BINARIES)

def test_vex_requires_reviewed_binary_bytes_and_path(self):
target = 'usr/bin/docker-machine'
report = {'Results': [{'Target': target, 'Vulnerabilities': [
{'VulnerabilityID': 'GO-2026-5932'}]}]}
with tempfile.TemporaryDirectory() as folder:
root = Path(folder)
payload = root / target
payload.parent.mkdir(parents=True)
payload.write_bytes(b'reviewed-fixture')
expected = hashlib.sha256(b'reviewed-fixture').hexdigest()
with patch.dict('check_scan_coverage.OPENPGP_VEX_BINARY_SHA256', {target: expected}):
self.assertEqual(validate_vex_payloads(report, root), 1)
payload.write_bytes(b'changed-fixture')
with self.assertRaisesRegex(ValueError, 'Changed OpenPGP'):
validate_vex_payloads(report, root)
payload.unlink()
with self.assertRaisesRegex(ValueError, 'Unreviewed OpenPGP'):
validate_vex_payloads(report, root)
report['Results'][0]['Target'] = '../unreviewed'
with self.assertRaisesRegex(ValueError, 'Unreviewed OpenPGP'):
validate_vex_payloads(report, root)

def test_clean_null_vulnerabilities_need_no_exception(self):
report = self.fixture(ROOT_BINARIES)
for result in report['Results']:
result['Vulnerabilities'] = None
self.assertEqual(validate_coverage(report, ROOT_BINARIES), len(ROOT_BINARIES))
with tempfile.TemporaryDirectory() as folder:
self.assertEqual(validate_vex_payloads(report, Path(folder)), 0)


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