from pathlib import Path import toml import os import stat import sys from click.testing import CliRunner from ctf.utils import load_config, active_categories from ctf.forensics import forensics_group, inspect # Define the persistent test environment path TEST_ENV = Path("tests/env") # ===================================================================== # 1. Test Strategy Summary # ===================================================================== # Core scenarios tested: # # A. Happy Path: # - Verifying metadata attributes (permissions, ownership, size, # hard links, inode, device) map exactly to filesystem ground truth. # # B. Boundary Conditions: # - Testing empty files (0 bytes) for correct apparent size and allocation. # - Testing file permissions changes (e.g. read-only, executable) and # making sure the parsed octal/symbolic codes match. # - Testing multiple hard links (count > 1). # # C. Edge Cases & OS Portability: # - Extended attributes (xattr) support, handling platforms where xattr # is not present or supported on the active file system. # - Graceful system identity fallbacks for UID/GID names when passwd/group # databases are unavailable or running on non-Unix systems. # # D. Error Handling: # - Accessing non-existent paths (CLI validation errors). # ===================================================================== # ===================================================================== # 2. Mocking/Setup Requirements # ===================================================================== # - Filesystem sandbox: Uses `tests/env/` to dynamically create files # with specific mode bits, content, and links. # - No external network/API mocking is required. # - OS-specific conditional blocks handle platforms (e.g., Windows) # where POSIX ownership resolution or xattr is not native. # ===================================================================== def test_load_config_static(): """Verifies load_config using the persistent test file.""" config_file = TEST_ENV / "config.toml" test_data = { "Competition": {"name": "PersistentComp"}, "Enviroment": {"data_dir": str(TEST_ENV.absolute())} } config_file.parent.mkdir(parents=True, exist_ok=True) with open(config_file, "w") as f: toml.dump(test_data, f) loaded_data = load_config(str(config_file)) assert loaded_data["Competition"]["name"] == "PersistentComp" def test_active_categories_static(): """Verifies active_categories using the pre-created 'comp1' folder.""" comp_dir = TEST_ENV / "comp1" comp_dir.mkdir(parents=True, exist_ok=True) (comp_dir / "web").mkdir(exist_ok=True) (comp_dir / "pwn").mkdir(exist_ok=True) cats = active_categories(comp_dir) cat_names = [c.name for c in cats] assert "web" in cat_names assert "pwn" in cat_names # --- Task-Specific Universal Metadata Tests --- def test_inspect_permissions(): """ Task 1: POSIX Permissions. Verifies that the parsed octal and symbolic representation of permissions matches Python's native os.stat mode decoding. """ test_file = TEST_ENV / "perm_test.bin" with open(test_file, "wb") as f: f.write(b"data") # Set permissions explicitly to 0o755 (rwxr-xr-x) test_file.chmod(0o755) meta = inspect.callback(str(test_file)) # Assert correct octal format assert meta.permissions_octal == "0o755" # Assert correct symbolic format (standard file type prefix '-') assert meta.permissions_symbolic == "-rwxr-xr-x" # Change permissions to 0o644 (rw-r--r--) test_file.chmod(0o644) meta_new = inspect.callback(str(test_file)) assert meta_new.permissions_octal == "0o644" assert meta_new.permissions_symbolic == "-rw-r--r--" def test_inspect_ownership(): """ Task 2: Ownership Identity. Verifies that numeric UID/GID and resolved username/groupname match. """ test_file = TEST_ENV / "owner_test.bin" with open(test_file, "wb") as f: f.write(b"data") stat_info = test_file.stat() meta = inspect.callback(str(test_file)) assert meta.owner_uid == stat_info.st_uid assert meta.owner_gid == stat_info.st_gid # On Unix, verify that user and group strings are resolved if sys.platform != "win32": import pwd import grp expected_user = pwd.getpwuid(stat_info.st_uid).pw_name expected_group = grp.getgrgid(stat_info.st_gid).gr_name assert meta.owner_username == expected_user assert meta.owner_groupname == expected_group else: # Fallback for Windows assert isinstance(meta.owner_username, str) assert isinstance(meta.owner_groupname, str) def test_inspect_allocation(): """ Task 3: Allocation Metrics. Verifies apparent file size matches size_bytes, and allocated_size corresponds to st_blocks * 512 bytes. """ test_file = TEST_ENV / "alloc_test.bin" with open(test_file, "wb") as f: f.write(b"A" * 1234) stat_info = test_file.stat() meta = inspect.callback(str(test_file)) assert meta.size == 1234 # st_blocks is Unix-specific. On other platforms, fallback to size on disk if hasattr(stat_info, "st_blocks"): expected_blocks_size = stat_info.st_blocks * 512 assert meta.allocated_size == expected_blocks_size else: assert meta.allocated_size >= 1234 def test_inspect_hard_links(): """ Task 4: Hard Link Count. Verifies that the hard link count changes when files are linked. """ test_file = TEST_ENV / "link_test.bin" with open(test_file, "wb") as f: f.write(b"link") meta_single = inspect.callback(str(test_file)) assert meta_single.hard_links == 1 # Create a hard link link_file = TEST_ENV / "link_test_hard.bin" if link_file.exists(): link_file.unlink() try: os.link(str(test_file), str(link_file)) meta_linked = inspect.callback(str(test_file)) assert meta_linked.hard_links == 2 finally: if link_file.exists(): link_file.unlink() def test_inspect_inode_device(): """ Task 5: Inode & Device Identifiers. Verifies that the unique Inode number and Device ID match os.stat results. """ test_file = TEST_ENV / "inode_test.bin" with open(test_file, "wb") as f: f.write(b"data") stat_info = test_file.stat() meta = inspect.callback(str(test_file)) assert meta.inode == stat_info.st_ino assert meta.device == stat_info.st_dev def test_inspect_extended_attributes(): """ Task 6: Extended Attributes (xattr). Checks that user-defined extended attributes can be retrieved if supported by the platform and filesystem. """ test_file = TEST_ENV / "xattr_test.bin" with open(test_file, "wb") as f: f.write(b"data") # Attempt to set an extended attribute (Linux/macOS specific) has_xattr_support = False if sys.platform in ("linux", "darwin"): try: import xattr # Use user namespace for custom attribute os.setxattr(str(test_file), "user.ctf_flag", b"FLAG{filesystem_metadata_ftw}") has_xattr_support = True except (ImportError, OSError, AttributeError): pass meta = inspect.callback(str(test_file)) if has_xattr_support: assert "user.ctf_flag" in meta.extended_attributes assert meta.extended_attributes["user.ctf_flag"] == "FLAG{filesystem_metadata_ftw}" else: # Should gracefully return an empty dict if not supported or none defined assert isinstance(meta.extended_attributes, dict) # --- CLI Validation Tests --- def test_forensics_inspect_cli_success(): """ Verifies that 'forensics inspect' successfully runs via the CLI and prints the metadata in a formatted table. """ runner = CliRunner() test_file = TEST_ENV / "test_file.png" with open(test_file, "wb") as f: f.write(b"\x89PNG\r\n\x1a\n") result = runner.invoke(forensics_group, ["inspect", str(test_file)]) assert result.exit_code == 0 assert "Metadata: test_file.png" in result.output assert "Size" in result.output assert "89504E47" in result.output assert "Detected Type" in result.output assert "PNG Image" in result.output def test_forensics_inspect_cli_missing_file(): """ Verifies that the CLI fails gracefully when a non-existent file path is specified. """ runner = CliRunner() result = runner.invoke(forensics_group, ["inspect", "non_existent_file.png"]) assert result.exit_code != 0 assert "does not exist" in result.output def test_forensics_signatures_cli(): """ Verifies that 'forensics signatures' runs successfully and displays the list of supported file signatures. """ runner = CliRunner() result = runner.invoke(forensics_group, ["signatures"]) assert result.exit_code == 0 assert "Supported Magic Signatures" in result.output assert "PNG Image" in result.output assert "89504E47" in result.output def test_forensics_flag_detect_cli_found(): """ Verifies that 'forensics flag-detect' successfully finds flag patterns. """ runner = CliRunner() test_file = TEST_ENV / "flag_data.bin" with open(test_file, "wb") as f: f.write(b"random_data_here_flag{found_statically_in_file}more_data") result = runner.invoke(forensics_group, ["flag-detect", str(test_file)]) assert result.exit_code == 0 assert "Potential Flag(s) Found" in result.output assert "flag{found_statically_in_file}" in result.output def test_forensics_flag_detect_cli_not_found(): """ Verifies that 'forensics flag-detect' displays 'No flag patterns found.' when no flag matches the pattern. """ runner = CliRunner() test_file = TEST_ENV / "clean_data.bin" with open(test_file, "wb") as f: f.write(b"this is a completely normal text file without flags.") result = runner.invoke(forensics_group, ["flag-detect", str(test_file)]) assert result.exit_code == 0 assert "No flag patterns found" in result.output def test_set_flag_format_cli(): """ Verifies that 'set-flag-format' CLI command writes the pattern to config.toml. """ from ctf.commands import set_flag_format from ctf.utils import load_config runner = CliRunner() config_file = Path("/home/venus/code/ctf/config.toml") # Save the original config to restore later original_config = load_config(str(config_file)) try: result = runner.invoke(set_flag_format, ["TEST_FLAG{[a-z]+}"]) assert result.exit_code == 0 assert "Flag format set to" in result.output # Verify it was written to config.toml updated_config = load_config(str(config_file)) assert updated_config["Competition"]["flag_format"] == "TEST_FLAG{[a-z]+}" finally: # Restore original config from ctf.utils import write_config write_config(original_config, str(config_file))