Files
CTF-tool/src/ctf/forensics.py

203 lines
5.8 KiB
Python

# src/ctf/forensics.py
# Library for forensic analysis
import click
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"]),
}
@click.group(name="forensics")
def forensics_group(): pass
@forensics_group.command()
def tf(): print("hello from forensics")
@dataclass
class FileMetadata:
filename: str
size: int
magic: str
extension: 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)
@forensics_group.command()
@click.argument('path', type=click.Path(exists=True))
def inspect(path):
p = Path(path)
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 = ""
# 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
meta = FileMetadata(
filename=p.name,
size=size,
magic=magic,
extension=extension,
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
)
# Present using Rich Table
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title=f"Metadata: {meta.filename}", show_header=False)
table.add_column("Key", style="bold cyan")
table.add_column("Value")
table.add_row("Filename", meta.filename)
table.add_row("Size", f"{meta.size} bytes")
table.add_row("Magic Bytes (Hex)", meta.magic)
table.add_row("Extension", meta.extension)
table.add_row("Permissions", f"{meta.permissions_symbolic} ({meta.permissions_octal})")
table.add_row("Owner", f"{meta.owner_username} (UID: {meta.owner_uid})")
table.add_row("Group", f"{meta.owner_groupname} (GID: {meta.owner_gid})")
table.add_row("Allocated Space", f"{meta.allocated_size} bytes")
table.add_row("Hard Links", str(meta.hard_links))
table.add_row("Inode", str(meta.inode))
table.add_row("Device", str(meta.device))
if meta.extended_attributes:
xattr_str = ", ".join(f"{k}={v}" for k, v in meta.extended_attributes.items())
table.add_row("Extended Attributes", xattr_str)
console.print(table)
return meta
@forensics_group.command(name="signatures")
def list_signatures():
"""List all supported file magic signatures and expected extensions."""
from rich.console import Console
from rich.table import Table
console = Console()
table = Table(title="Supported Magic Signatures", show_header=True)
table.add_column("Magic Bytes (Hex)", style="bold cyan")
table.add_column("File Type", style="bold green")
table.add_column("Expected Exts")
for signature, (type_name, exts) in COMMON_SIGNATURES.items():
hex_str = signature.hex().upper()
exts_str = ", ".join(exts) if exts else "Any / None"
table.add_row(hex_str, type_name, exts_str)
console.print(table)