|
| 1 | +"""Tests for AppArmor configuration completeness.""" |
| 2 | + |
| 3 | +import os |
| 4 | +import re |
| 5 | +from pathlib import Path |
| 6 | + |
| 7 | + |
| 8 | +def test_apparmor_config(): |
| 9 | + """ |
| 10 | + Verify that the set of Python files in securedrop/ exactly matches the set |
| 11 | + of Python files listed in the AppArmor configuration file. This ensures: |
| 12 | + 1. All application code has proper AppArmor permissions when running under Apache |
| 13 | + 2. The AppArmor config doesn't have stale entries for deleted files |
| 14 | + """ |
| 15 | + # Files/directories that don't run under Apache and should be exempted |
| 16 | + EXEMPTIONS = { |
| 17 | + # Database migrations run via management commands, not Apache |
| 18 | + "alembic/", |
| 19 | + # Management commands run separately, not via Apache |
| 20 | + "management/", |
| 21 | + # Developer/test scripts that don't run under Apache |
| 22 | + "loaddata.py", |
| 23 | + "loadfixeddata.py", |
| 24 | + "manage.py", |
| 25 | + "setup.py", |
| 26 | + "specialstrings.py", |
| 27 | + "upload-screenshots.py", |
| 28 | + } |
| 29 | + |
| 30 | + # Find all Python files in securedrop/ directory (excluding tests and debian) |
| 31 | + securedrop_dir = Path(__file__).parent.parent |
| 32 | + expected_python_files = set() |
| 33 | + |
| 34 | + for root, dirs, files in os.walk(securedrop_dir): |
| 35 | + # Skip hidden directories, __pycache__, and debian directory |
| 36 | + dirs[:] = [d for d in dirs if not d.startswith(".") and d not in ("__pycache__", "debian")] |
| 37 | + |
| 38 | + # Also skip the tests directory itself - tests don't run under Apache |
| 39 | + relative_root = Path(root).relative_to(securedrop_dir) |
| 40 | + if relative_root.parts and relative_root.parts[0] == "tests": |
| 41 | + continue |
| 42 | + |
| 43 | + for file in files: |
| 44 | + if file.endswith(".py"): |
| 45 | + # Get the path relative to securedrop/ |
| 46 | + full_path = Path(root) / file |
| 47 | + rel_path = full_path.relative_to(securedrop_dir) |
| 48 | + rel_path_str = str(rel_path) |
| 49 | + |
| 50 | + # Check if this file matches any exemption |
| 51 | + is_exempted = False |
| 52 | + for exemption in EXEMPTIONS: |
| 53 | + if exemption.endswith("/"): |
| 54 | + # Directory exemption |
| 55 | + if rel_path_str.startswith(exemption): |
| 56 | + is_exempted = True |
| 57 | + break |
| 58 | + # File exemption |
| 59 | + elif rel_path_str == exemption: |
| 60 | + is_exempted = True |
| 61 | + break |
| 62 | + |
| 63 | + if not is_exempted: |
| 64 | + expected_python_files.add(rel_path_str) |
| 65 | + |
| 66 | + # Extract Python file paths from AppArmor config |
| 67 | + # Look for lines like: /var/www/securedrop/path/to/file.py r |
| 68 | + actual_python_files = set() |
| 69 | + python_file_pattern = re.compile(r"^\s*/var/www/securedrop/(.*\.py)\s+r\s*,?\s*$") |
| 70 | + |
| 71 | + apparmor_config = ( |
| 72 | + Path(__file__).parent.parent / "debian/app-code/etc/apparmor.d/usr.sbin.apache2" |
| 73 | + ).read_text() |
| 74 | + |
| 75 | + for line in apparmor_config.splitlines(): |
| 76 | + match = python_file_pattern.match(line) |
| 77 | + if match: |
| 78 | + py_file = match.group(1) |
| 79 | + actual_python_files.add(py_file) |
| 80 | + |
| 81 | + assert expected_python_files == actual_python_files |
0 commit comments