Files
CTF-tool/src/ctf/forensics.py
venus 4dc0a24152 coming allong nicely. adding more commands, tests, and
artifacts for testing. basic framework down just adding features now
2026-07-18 02:38:13 -05:00

169 lines
4.4 KiB
Python

# 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
)
# }}}