Files
PEASS-ng/linPEAS/tests/test_modules_metadata.py
T

115 lines
4.2 KiB
Python

import re
import shutil
import subprocess
import sys
import unittest
from pathlib import Path
class LinpeasModulesMetadataTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
cls.repo_root = Path(__file__).resolve().parents[2]
cls.linpeas_dir = cls.repo_root / "linPEAS"
cls.parts_dir = cls.linpeas_dir / "builder" / "linpeas_parts"
# Ensure `import builder.*` works when tests are run from repo root.
sys.path.insert(0, str(cls.linpeas_dir))
from builder.src.linpeasModule import LinpeasModule # pylint: disable=import-error
cls.LinpeasModule = LinpeasModule
cls.module_files = sorted(cls.parts_dir.rglob("*.sh"))
cls.modules = [cls.LinpeasModule(str(path)) for path in cls.module_files]
def _iter_module_files(self):
return self.module_files
def test_all_modules_parse(self):
self.assertGreater(len(self.modules), 0, "No linPEAS module files were found.")
def test_check_module_id_matches_filename(self):
for module in self.modules:
if not getattr(module, "is_check", False):
continue
# For checks, the filename (without numeric prefix) must match the module ID
# (either full ID or stripping section prefix like `SI_`).
path = Path(module.path)
file_base = re.sub(r"^[0-9]+_", "", path.stem)
module_id = getattr(module, "id", "")
module_id_tail = module_id[3:] if len(module_id) >= 3 else ""
self.assertIn(
file_base,
{module_id, module_id_tail},
f"Module ID mismatch in {path}: id={module_id} expected suffix={file_base}",
)
def test_module_ids_are_unique(self):
ids = []
for module in self.modules:
ids.append(getattr(module, "id", ""))
duplicates = {x for x in ids if x and ids.count(x) > 1}
self.assertEqual(set(), duplicates, f"Duplicate module IDs found: {sorted(duplicates)}")
def test_module_shell_snippets_are_syntactically_valid(self):
shell = shutil.which("sh")
self.assertIsNotNone(shell, "sh is required to syntax-check linPEAS snippets.")
failures = []
for path in self._iter_module_files():
result = subprocess.run([shell, "-n", str(path)], capture_output=True, text=True)
if result.returncode != 0:
failures.append(f"{path}: {result.stderr.strip()}")
self.assertEqual([], failures)
def test_declared_function_dependencies_exist(self):
defined_functions = {
function_name
for module in self.modules
for function_name in getattr(module, "defined_funcs", [])
}
missing = []
for module in self.modules:
for function_name in sorted(set(module.functions_used + module.initial_functions)):
if function_name not in defined_functions:
missing.append(f"{module.path}: {function_name}")
self.assertEqual([], missing)
def test_declared_global_variable_dependencies_exist(self):
# These are shell/runtime values intentionally supplied by the environment
# or CLI parser instead of a linPEAS variable module.
runtime_globals = {
"IP",
"RANDOM",
}
generated_globals = {
variable_name
for module in self.modules
for variable_name in getattr(module, "generated_global_variables", [])
}
missing = []
for module in self.modules:
for variable_name in sorted(set(module.global_variables)):
if variable_name not in generated_globals and variable_name not in runtime_globals:
missing.append(f"{module.path}: ${variable_name}")
self.assertEqual([], missing)
def test_sudo_l_check_is_bounded_for_non_interactive_runs(self):
sudo_l_module = self.parts_dir / "6_users_information" / "7_Sudo_l.sh"
content = sudo_l_module.read_text(encoding="utf-8")
self.assertIn('"$TIMEOUT" 15 sudo -S -l', content)
self.assertIn("sudo -n -l", content)
self.assertNotRegex(content, r"secure_path_line=\$\(sudo\s+-l\b")
if __name__ == "__main__":
unittest.main()