fix: remove builder proxy declarations from exported package environments
This commit is contained in:
@@ -1,5 +1,8 @@
|
|||||||
"""Prepare only exported Gentoo payloads and configure the dedicated guest network."""
|
"""Prepare only exported Gentoo payloads and configure the dedicated guest network."""
|
||||||
import argparse
|
import argparse
|
||||||
|
import bz2
|
||||||
|
import io
|
||||||
|
import runpy
|
||||||
import hashlib
|
import hashlib
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
@@ -50,6 +53,39 @@ def write(root, relative, content, mode=0o644):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
def sanitize_saved_environments(root):
|
||||||
|
"""Use Portage's parser to scrub exported declarations without evaluating them."""
|
||||||
|
database = root_path(root, 'var/db/pkg')
|
||||||
|
paths = sorted(database.glob('*/*/environment.bz2'))
|
||||||
|
result = {'files_scanned': len(paths), 'filtered_files': 0, 'variable_names': []}
|
||||||
|
if not paths:
|
||||||
|
return result
|
||||||
|
scripts = sorted(root_path(root, 'usr/lib/portage').glob('*/filter-bash-environment.py'))
|
||||||
|
if not scripts:
|
||||||
|
raise ImageError('Installed Portage environment filter is required for export')
|
||||||
|
script = root_path(root, scripts[0].relative_to(root))
|
||||||
|
api = runpy.run_path(str(script))
|
||||||
|
pattern = re.compile(rb'(?i:(?:HTTP|HTTPS|FTP|ALL|NO)_PROXY|.*(?:PASSWORD|PASSWD|SECRET|TOKEN|API_KEY|ACCESS_KEY|CREDENTIAL|AUTHORIZATION).*)')
|
||||||
|
declarations = re.compile(rb'^(?:declare|export|readonly)\s+(?:-[^ ]+\s+)*([A-Za-z_][A-Za-z0-9_]*)=', re.M)
|
||||||
|
names = set()
|
||||||
|
for path in paths:
|
||||||
|
path = root_path(root, path.relative_to(root))
|
||||||
|
try:
|
||||||
|
original = bz2.decompress(path.read_bytes())
|
||||||
|
except (OSError, ValueError) as error:
|
||||||
|
raise ImageError('Cannot read an exported package environment') from error
|
||||||
|
filtered = io.BytesIO()
|
||||||
|
api['filter_bash_environment'](pattern, io.BytesIO(original), filtered)
|
||||||
|
clean = filtered.getvalue()
|
||||||
|
if clean != original:
|
||||||
|
names.update(name.decode('ascii') for name in declarations.findall(original)
|
||||||
|
if pattern.fullmatch(name))
|
||||||
|
path.write_bytes(bz2.compress(clean))
|
||||||
|
result['filtered_files'] += 1
|
||||||
|
result['variable_names'] = sorted(names)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def sanitize_rootfs(root):
|
def sanitize_rootfs(root):
|
||||||
"""Erase builder identity/secrets in the exported copy, retaining Gentoo updates."""
|
"""Erase builder identity/secrets in the exported copy, retaining Gentoo updates."""
|
||||||
for relative in ('etc/portage/gnupg', 'etc/portage/gnupg.hydra-untrusted',
|
for relative in ('etc/portage/gnupg', 'etc/portage/gnupg.hydra-untrusted',
|
||||||
@@ -76,6 +112,8 @@ def sanitize_rootfs(root):
|
|||||||
write(root, 'etc/hosts', '127.0.0.1 localhost\n::1 localhost\n127.0.1.1 hydra-os\n')
|
write(root, 'etc/hosts', '127.0.0.1 localhost\n::1 localhost\n127.0.1.1 hydra-os\n')
|
||||||
remove(root, 'etc/resolv.conf')
|
remove(root, 'etc/resolv.conf')
|
||||||
root_path(root, 'etc/resolv.conf', follow_leaf=False).symlink_to('/run/NetworkManager/resolv.conf')
|
root_path(root, 'etc/resolv.conf', follow_leaf=False).symlink_to('/run/NetworkManager/resolv.conf')
|
||||||
|
audit = sanitize_saved_environments(root)
|
||||||
|
write(root, 'usr/share/hydra/environment-sanitization.json', json.dumps(audit, indent=2) + '\n')
|
||||||
|
|
||||||
|
|
||||||
def copy_overlay(root, overlay):
|
def copy_overlay(root, overlay):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
|
import bz2
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import shlex
|
import shlex
|
||||||
@@ -73,6 +74,36 @@ class ImageTests(unittest.TestCase):
|
|||||||
self.assertEqual((self.root / 'etc/hostname').read_text(), 'hydra-os\n')
|
self.assertEqual((self.root / 'etc/hostname').read_text(), 'hydra-os\n')
|
||||||
self.assertTrue((self.root / 'etc/resolv.conf').is_symlink())
|
self.assertTrue((self.root / 'etc/resolv.conf').is_symlink())
|
||||||
|
|
||||||
|
def test_export_filters_saved_environment_proxies_but_retains_package_metadata(self):
|
||||||
|
self.assertTrue(callable(getattr(image, 'sanitize_saved_environments', None)), 'saved environment scrubber is missing')
|
||||||
|
# Portable stand-in for the external Portage filter API, not the scrubber.
|
||||||
|
helper = self.file('usr/lib/portage/python3.14/filter-bash-environment.py',
|
||||||
|
"import re\ndef filter_bash_environment(pattern, source, output):\n"
|
||||||
|
" for line in source:\n"
|
||||||
|
" match = re.match(rb'declare -x ([A-Za-z_][A-Za-z0-9_]*)=', line)\n"
|
||||||
|
" if not match or not pattern.fullmatch(match.group(1)):\n"
|
||||||
|
" output.write(line)\n")
|
||||||
|
path = self.root / 'var/db/pkg/app-emulation/qemu-9.2.4/environment.bz2'
|
||||||
|
path.parent.mkdir(parents=True)
|
||||||
|
package_metadata = b'declare -x LICENSE="GPL-2"\ndeclare -x EAPI="8"\n'
|
||||||
|
path.write_bytes(bz2.compress(package_metadata + b'declare -x HTTPS_PROXY="https://user:secret@builder.invalid"\n'
|
||||||
|
b'declare -x HTTP_PROXY="http://127.0.0.1:1234"\n'
|
||||||
|
b'declare -x NO_PROXY="localhost"\n'))
|
||||||
|
result = image.sanitize_saved_environments(self.root)
|
||||||
|
self.assertEqual(bz2.decompress(path.read_bytes()), package_metadata)
|
||||||
|
self.assertEqual(result['filtered_files'], 1)
|
||||||
|
self.assertEqual(result['variable_names'], ['HTTPS_PROXY', 'HTTP_PROXY', 'NO_PROXY'])
|
||||||
|
|
||||||
|
def test_saved_environment_filter_refuses_missing_portage_helper(self):
|
||||||
|
self.assertTrue(callable(getattr(image, 'sanitize_saved_environments', None)), 'saved environment scrubber is missing')
|
||||||
|
path = self.root / 'var/db/pkg/app-emulation/qemu-9.2.4/environment.bz2'
|
||||||
|
path.parent.mkdir(parents=True)
|
||||||
|
original = bz2.compress(b'declare -x HTTPS_PROXY="private"\n')
|
||||||
|
path.write_bytes(original)
|
||||||
|
with self.assertRaises(image.ImageError):
|
||||||
|
image.sanitize_saved_environments(self.root)
|
||||||
|
self.assertEqual(path.read_bytes(), original)
|
||||||
|
|
||||||
def test_sanitization_cannot_follow_exported_symlink_to_host(self):
|
def test_sanitization_cannot_follow_exported_symlink_to_host(self):
|
||||||
outside = Path(self.tmp.name) / 'host'
|
outside = Path(self.tmp.name) / 'host'
|
||||||
outside.mkdir()
|
outside.mkdir()
|
||||||
|
|||||||
Reference in New Issue
Block a user