# src/ctf/forensics.py # Library for forensic analysis (pure functions only) # vim foldmethod=marker from dataclasses import dataclass, field from pathlib import Path import stat import os import sys from typing import List, Dict try: import pwd import grp except ImportError: pwd = None grp = None COMMON_SIGNATURES = { b"\x89PNG\r\n\x1a\n": ("PNG Image", [".png"]), b"\xff\xd8\xff": ("JPEG Image", [".jpg", ".jpeg"]), b"%PDF": ("PDF Document", [".pdf"]), b"PK\x03\x04": ("ZIP Archive", [".zip"]), b"\x7fELF": ("ELF Executable", [".elf"]), b"MZ": ("PE Executable", [".exe", ".dll"]), b"GIF87a": ("GIF Image", [".gif"]), b"GIF89a": ("GIF Image", [".gif"]), b"7z\xbc\xaf\x27\x1c": ("7-Zip Archive", [".7z"]), b"\x1f\x8b": ("GZIP Archive", [".gz"]), b"Rar!\x1a\x07\x00": ("RAR Archive", [".rar"]), b"Rar!\x1a\x07\x01\x00": ("RAR Archive", [".rar"]), b"BZh": ("BZIP2 Archive", [".bz2"]), b"BM": ("BMP Image", [".bmp"]), b"ID3": ("MP3 Audio", [".mp3"]), } # {{{ FileMetadata @dataclass class FileMetadata: filename: str size: int magic: str extension: str detected_type: str # Task 1: POSIX Permissions permissions_octal: str permissions_symbolic: str # Task 2: Ownership Identity owner_uid: int owner_username: str owner_gid: int owner_groupname: str # Task 3: Allocation Metrics allocated_size: int # Task 4: Hard Links hard_links: int # Task 5: Inode & Device Identifiers inode: int device: int # Task 6: Extended Attributes extended_attributes: Dict[str, str] = field(default_factory=dict) # }}} # {{{ get_metadata def get_metadata(path: Path) -> FileMetadata: """Extracts metadata attributes from a file without any console rendering.""" p = Path(path) if not p.exists(): raise FileNotFoundError(f"File not found: {p}") stat_info = p.stat() # Apparent size & extension size = stat_info.st_size extension = p.suffix # Magic bytes try: with open(p, 'rb') as f: magic = f.read(8).hex().upper() except Exception: magic = "" detected_type = "Unknown" try: magic_bytes = bytes.fromhex(magic) for signature, (type_name, exts) in COMMON_SIGNATURES.items(): if magic_bytes.startswith(signature): detected_type = type_name break except Exception: pass # POSIX Permissions mode = stat_info.st_mode permissions_octal = oct(stat.S_IMODE(mode)) permissions_symbolic = stat.filemode(mode) # Ownership Identity owner_uid = stat_info.st_uid owner_gid = stat_info.st_gid owner_username = str(owner_uid) owner_groupname = str(owner_gid) if pwd is not None: try: owner_username = pwd.getpwuid(owner_uid).pw_name except KeyError: pass if grp is not None: try: owner_groupname = grp.getgrgid(owner_gid).gr_name except KeyError: pass # Allocation Metrics if hasattr(stat_info, "st_blocks"): allocated_size = stat_info.st_blocks * 512 else: allocated_size = size # Hard Links hard_links = stat_info.st_nlink # Inode & Device Identifiers inode = stat_info.st_ino device = stat_info.st_dev # Extended Attributes extended_attributes = {} if hasattr(os, "listxattr") and hasattr(os, "getxattr"): try: attrs = os.listxattr(p) for attr in attrs: try: val = os.getxattr(p, attr) extended_attributes[attr] = val.decode("utf-8", errors="ignore") except OSError: pass except OSError: pass return FileMetadata( filename=p.name, size=size, magic=magic, extension=extension, detected_type=detected_type, permissions_octal=permissions_octal, permissions_symbolic=permissions_symbolic, owner_uid=owner_uid, owner_username=owner_username, owner_gid=owner_gid, owner_groupname=owner_groupname, allocated_size=allocated_size, hard_links=hard_links, inode=inode, device=device, extended_attributes=extended_attributes ) # }}}