169 lines
4.4 KiB
Python
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
|
|
)
|
|
# }}}
|
|
|