Standardize matching test files structure, add test_main.py and test_commands.py, and update GEMINI.md

This commit is contained in:
venus
2026-07-17 01:55:44 -05:00
parent cf21abe0bc
commit c069e51263
9 changed files with 173 additions and 40 deletions

View File

@@ -1,22 +1,47 @@
# functions for commands needed
# src/commands.py
# from pathlib import path
import click
from pathlib import Path
# {{{ basic_group
# This defines a group with name basic which will nest other comands to be imported in the main loop
@click.group(name="basic")
def basic_group(): pass
# }}}
# Adds a simple commmand to be run with `ctf test` per the function name
@click.command()
# {{{ test
# Adds a simple commmand to be run with `ctf basic test`
@basic_group.command(name="test")
def test():
print("hello from test")
# }}}
# {{{ greet
# A simple command with a positional(required) argument name
@click.command()
@basic_group.command(name="greet")
@click.argument('name')
def greet(name):
print(f"hello {name}")
# }}}
# {{{ set_flag_format
# Sets the flag format for the current competition in config.toml
@basic_group.command(name="set-flag-format")
@click.argument("pattern")
def set_flag_format(pattern: str):
"""Set the flag regex format for the active competition."""
from ctf.utils import load_config, write_config
config_path = "/home/venus/code/ctf/config.toml"
config = load_config(config_path)
if "Competition" not in config:
config["Competition"] = {}
config["Competition"]["flag_format"] = pattern
write_config(config, config_path)
click.echo(f"Flag format set to: {pattern}")
# }}}
# def Set_Challenge(comp: str, chal: str, setDirectory: bool):
# # set the current challenge and competition from input
@@ -24,11 +49,9 @@ def greet(name):
# state.current_comp=comp
# state.comp_dir=pathlib
# # TODO archive the old competitions
#
# if state.current_chal != chal:
# state.current_chal=chal
# print("challenge already set")
# # TODO archive the old challenges
# # TODO set the directory to challenge directory, with ignore option

View File

@@ -1,6 +1,7 @@
# src/ctf/forensics.py
# Library for forensic analysis
# vim foldmethod=marker
import click
from dataclasses import dataclass, field
from pathlib import Path
@@ -34,18 +35,24 @@ COMMON_SIGNATURES = {
b"ID3": ("MP3 Audio", [".mp3"]),
}
# {{{ forensics_group
@click.group(name="forensics")
def forensics_group(): pass
def forensics_group():
''' A collection of forensics tools '''
pass
@forensics_group.command()
def tf(): print("hello from forensics")
# }}}
# {{{ FileMetadata
@dataclass
class FileMetadata:
filename: str
size: int
magic: str
extension: str
detected_type: str
# Task 1: POSIX Permissions
permissions_octal: str
@@ -69,10 +76,13 @@ class FileMetadata:
# Task 6: Extended Attributes
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'''
p = Path(path)
stat_info = p.stat()
@@ -87,6 +97,16 @@ def inspect(path):
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))
@@ -141,6 +161,7 @@ def inspect(path):
size=size,
magic=magic,
extension=extension,
detected_type=detected_type,
permissions_octal=permissions_octal,
permissions_symbolic=permissions_symbolic,
owner_uid=owner_uid,
@@ -166,6 +187,7 @@ def inspect(path):
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})")
@@ -181,7 +203,9 @@ def inspect(path):
console.print(table)
return meta
# }}}
# {{{ list_signatures
@forensics_group.command(name="signatures")
def list_signatures():
"""List all supported file magic signatures and expected extensions."""
@@ -200,3 +224,45 @@ def list_signatures():
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]")
# }}}

View File

@@ -1,18 +1,19 @@
# src/main.py
# Parses and calls commands
import ctf.commands as commands
from ctf.commands import basic_group
from ctf.forensics import forensics_group
import click
# {{{ main
def main():
@click.group()
def cli(): pass
cli.add_command(forensics_group())
cli.add_command(forensics_group)
cli.add_command(basic_group)
cli()
# }}}
if __name__ == "__main__":
main()