coming allong nicely. adding more commands, tests, and
artifacts for testing. basic framework down just adding features now
This commit is contained in:
@@ -1,8 +1,7 @@
|
||||
# src/ctf/forensics.py
|
||||
# Library for forensic analysis
|
||||
# Library for forensic analysis (pure functions only)
|
||||
|
||||
# vim foldmethod=marker
|
||||
import click
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
import stat
|
||||
@@ -35,16 +34,6 @@ COMMON_SIGNATURES = {
|
||||
b"ID3": ("MP3 Audio", [".mp3"]),
|
||||
}
|
||||
|
||||
# {{{ forensics_group
|
||||
@click.group(name="forensics")
|
||||
def forensics_group():
|
||||
''' A collection of forensics tools '''
|
||||
pass
|
||||
|
||||
@forensics_group.command()
|
||||
def tf(): print("hello from forensics")
|
||||
# }}}
|
||||
|
||||
# {{{ FileMetadata
|
||||
@dataclass
|
||||
class FileMetadata:
|
||||
@@ -78,12 +67,13 @@ class FileMetadata:
|
||||
extended_attributes: Dict[str, str] = field(default_factory=dict)
|
||||
# }}}
|
||||
|
||||
# {{{ inspect
|
||||
@forensics_group.command()
|
||||
@click.argument('path', type=click.Path(exists=True))
|
||||
def inspect(path):
|
||||
'''Lists all basic inode metadata about a file'''
|
||||
# {{{ 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
|
||||
@@ -156,7 +146,7 @@ def inspect(path):
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
meta = FileMetadata(
|
||||
return FileMetadata(
|
||||
filename=p.name,
|
||||
size=size,
|
||||
magic=magic,
|
||||
@@ -174,95 +164,5 @@ def inspect(path):
|
||||
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("Detected Type", meta.detected_type)
|
||||
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
|
||||
# }}}
|
||||
|
||||
# {{{ list_signatures
|
||||
@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)
|
||||
# }}}
|
||||
|
||||
# {{{ flag_detect
|
||||
@forensics_group.command(name="flag-detect")
|
||||
@click.argument("filepath", type=click.Path(exists=True, dir_okay=False, path_type=Path))
|
||||
@click.option("--pattern", "-p", default=r"[a-zA-Z0-9_\-]+{[^}]+}", help="Regex pattern to search for.")
|
||||
@click.option("--min-len", "-m", default=4, help="Minimum printable string length.")
|
||||
def flag_detect(filepath: Path, pattern: str, min_len: int):
|
||||
"""Extract printable strings and search for flag patterns."""
|
||||
import re
|
||||
from rich.console import Console
|
||||
from rich.panel import Panel
|
||||
|
||||
console = Console()
|
||||
try:
|
||||
with open(filepath, "rb") as f:
|
||||
content = f.read()
|
||||
except Exception as e:
|
||||
console.print(f"[bold red]Error reading file:[/bold red] {e}")
|
||||
return
|
||||
|
||||
# Extract printable strings
|
||||
printable_re = re.compile(rb"[a-zA-Z0-9/\-:.,_$%'\"()[\]<> ]{" + str(min_len).encode() + rb",}")
|
||||
strings = printable_re.findall(content)
|
||||
|
||||
# Search for flags matching pattern
|
||||
flag_re = re.compile(pattern.encode("utf-8"))
|
||||
flags_found = []
|
||||
for s in strings:
|
||||
for match in flag_re.finditer(s):
|
||||
flags_found.append(match.group().decode("utf-8", errors="ignore"))
|
||||
|
||||
if flags_found:
|
||||
output = "\n".join(f" [bold green]✓[/bold green] {flag}" for flag in flags_found)
|
||||
console.print(Panel(
|
||||
output,
|
||||
title=f"[bold green]Potential Flag(s) Found ({len(flags_found)})[/bold green]",
|
||||
border_style="green"
|
||||
))
|
||||
else:
|
||||
console.print("[bold yellow]No flag patterns found.[/bold yellow]")
|
||||
# }}}
|
||||
|
||||
Reference in New Issue
Block a user