Implement all 6 universal filesystem metadata properties inside inspect command

This commit is contained in:
venus
2026-07-17 01:20:57 -05:00
parent 1b53658a50
commit 3664208c74

View File

@@ -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