From 3664208c74f33ac60b276e4eb96f27cae839d80b Mon Sep 17 00:00:00 2001 From: venus Date: Fri, 17 Jul 2026 01:20:57 -0500 Subject: [PATCH] Implement all 6 universal filesystem metadata properties inside inspect command --- src/ctf/forensics.py | 122 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 116 insertions(+), 6 deletions(-) diff --git a/src/ctf/forensics.py b/src/ctf/forensics.py index c516dbc..2b58d5d 100644 --- a/src/ctf/forensics.py +++ b/src/ctf/forensics.py @@ -2,10 +2,19 @@ # Library for forensic analysis import click -from dataclasses import dataclass +from dataclasses import dataclass, field from pathlib import Path -from rich.console import Console -from rich.table import Table +import stat +import os +import sys +from typing import List, Dict + +try: + import pwd + import grp +except ImportError: + pwd = None + grp = None @click.group(name="forensics") def forensics_group(): pass @@ -19,28 +28,118 @@ class FileMetadata: 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) - size = p.stat().st_size + 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(4).hex().upper() except Exception: - magic = "UNKNOWN" + magic = "" + # Task 1: POSIX Permissions + mode = stat_info.st_mode + permissions_octal = oct(stat.S_IMODE(mode)) + permissions_symbolic = stat.filemode(mode) + + # Task 2: 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 + + # Task 3: Allocation Metrics + if hasattr(stat_info, "st_blocks"): + allocated_size = stat_info.st_blocks * 512 + else: + allocated_size = size + + # Task 4: Hard Links + hard_links = stat_info.st_nlink + + # Task 5: Inode & Device Identifiers + inode = stat_info.st_ino + device = stat_info.st_dev + + # Task 6: 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 + 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") @@ -50,6 +149,17 @@ def inspect(path): 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