mirror of
https://github.com/storytold/craft-common-actions.git
synced 2026-10-31 15:57:54 +00:00
- Disallow carriage returns and newlines upfront in resolve-version to prevent GitHub output-file protocol forgery - Strictly validate generated build date against YYYY-MM-DD format - Sanitize APP_TITLE against carriage returns and newlines in step summary - Validate package tokens in install-apt-packages, rejecting options, path traversals, and injection payloads - Terminate options with -- when invoking apt-get install - Add unit tests for install-apt-packages validation and new test suite for resolve-version
99 lines
3.8 KiB
Python
99 lines
3.8 KiB
Python
"""The cached apt install action: pinned actions, and PR runs that never write the cache."""
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import subprocess
|
|
import tempfile
|
|
import unittest
|
|
|
|
import yaml
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
ACTION = ROOT / 'actions/install-apt-packages/action.yml'
|
|
|
|
|
|
def steps():
|
|
return yaml.load(ACTION.read_text(), Loader=yaml.BaseLoader)['runs']['steps']
|
|
|
|
|
|
class InstallAptPackagesTests(unittest.TestCase):
|
|
def test_every_used_action_is_pinned_to_a_commit(self):
|
|
for step in steps():
|
|
if 'uses' in step:
|
|
with self.subTest(step=step.get('name')):
|
|
self.assertRegex(step['uses'], r'@[0-9a-f]{40}$')
|
|
|
|
def test_restore_and_save_use_the_same_key_and_path(self):
|
|
restore = next(s for s in steps() if s.get('id') == 'restore')
|
|
save = next(s for s in steps() if 'cache/save' in s.get('uses', ''))
|
|
self.assertEqual(restore['with']['key'], save['with']['key'])
|
|
self.assertEqual(restore['with']['path'], save['with']['path'])
|
|
|
|
def test_by_default_only_pushes_to_the_default_branch_save(self):
|
|
save = next(s for s in steps() if 'cache/save' in s.get('uses', ''))
|
|
condition = re.sub(r'\s+', ' ', save['if'])
|
|
self.assertIn("github.event_name == 'push'", condition)
|
|
self.assertIn('github.ref_name == github.event.repository.default_branch', condition)
|
|
self.assertIn("cache-hit != 'true'", condition)
|
|
|
|
def test_install_step_uses_option_terminator(self):
|
|
install_step = next(s for s in steps() if s.get('name') == 'Install packages')
|
|
self.assertIn('install -y --no-install-recommends -- $PACKAGE_LIST', install_step['run'])
|
|
|
|
def _run_key_step(self, packages):
|
|
key_step = next(s for s in steps() if s.get('id') == 'key')
|
|
with tempfile.TemporaryDirectory() as tmpdir:
|
|
out_file = Path(tmpdir) / "output"
|
|
res = subprocess.run(
|
|
['bash', '-euo', 'pipefail', '-c', key_step['run']],
|
|
env={**os.environ, 'PACKAGES': packages, 'GITHUB_OUTPUT': str(out_file)},
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
outputs = {}
|
|
if out_file.exists():
|
|
for line in out_file.read_text().splitlines():
|
|
if '=' in line:
|
|
k, v = line.split('=', 1)
|
|
outputs[k] = v
|
|
return res, outputs
|
|
|
|
def test_valid_packages_accepted(self):
|
|
cases = [
|
|
("curl", "curl "),
|
|
("curl git build-essential", "build-essential curl git "),
|
|
("libx11-dev:amd64 pkg-config=1.8.0-1", "libx11-dev:amd64 pkg-config=1.8.0-1 "),
|
|
("curl\ngit\nbuild-essential", "build-essential curl git "),
|
|
]
|
|
for packages, expected_list in cases:
|
|
with self.subTest(packages=packages):
|
|
res, outputs = self._run_key_step(packages)
|
|
self.assertEqual(res.returncode, 0, f"{res.stderr}\n{res.stdout}")
|
|
self.assertEqual(outputs.get('list'), expected_list)
|
|
|
|
def test_invalid_package_names_rejected(self):
|
|
invalid_cases = [
|
|
"-o APT::Update=1",
|
|
"--config",
|
|
"-y",
|
|
"../traversal.deb",
|
|
"/tmp/evil.deb",
|
|
"curl; touch injected",
|
|
"curl && touch injected",
|
|
"curl | touch injected",
|
|
"$(touch injected)",
|
|
"`touch injected`",
|
|
"curl>injected",
|
|
"",
|
|
" \n \t ",
|
|
]
|
|
for bad_pkg in invalid_cases:
|
|
with self.subTest(bad_pkg=bad_pkg):
|
|
res, _ = self._run_key_step(bad_pkg)
|
|
self.assertNotEqual(res.returncode, 0)
|
|
self.assertIn("::error::install-apt-packages:", res.stdout + res.stderr)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|