coming allong nicely. adding more commands, tests, and

artifacts for testing. basic framework down just adding features now
This commit is contained in:
venus
2026-07-18 02:38:13 -05:00
parent 8274d08f3e
commit 4dc0a24152
15 changed files with 362 additions and 209 deletions

View File

@@ -71,16 +71,18 @@ Implements specialized forensic inspection utilities registered as a nested subg
## 5. Planned Architectural Components
### A. File Scraper & Extended Configuration
* A scraping module to fetch details/files for challenges or competitions.
* Integration with an expanded configuration schema in [config.toml](file:///home/venus/code/ctf/config.toml) to store credentials, URLs, and directory preferences.
### A. Forensics Pipeline & Extended Metadata
* **Pipeline Coordination**: A central runner that coordinates file inspections, signature verification, and string searches sequentially on target files.
* **Carving & Extraction Toolkit**: Deep inspection and carving utilities (such as archive extracting, custom chunk/payload carvers, and EXIF/metadata header decoders).
### B. Download Organizer & Challenge Progress Documenter
* Monitoring or organizing downloaded challenge assets (e.g., from the browser's downloads folder) and sorting them into the correct competition/challenge subdirectories.
* An automated mechanism to log commands, notes, and milestones, providing clean progress documentation.
### B. Core Solving Infrastructure & QOL
* **Solving Assistants**: Standard templates and modular helper scripts for repeating CTF patterns (e.g., base conversions, cipher decoding, and request generation).
* **Context & Progress Management**: Full CLI commands to organize active challenges, automate note tracking, and manage workspace folders dynamically.
* **Per-Challenge Progress Logging**: A logging interface that creates separate progress logs for each challenge inside a configurable directory, tracking actions, milestones, attempts, and command history.
### C. Forensics Metadata Expansion
* Extend forensics capabilities inside [forensics.py](file:///home/venus/code/ctf/src/ctf/forensics.py) to extract file-specific metadata (e.g., EXIF header extraction for JPG/PNG files, archive contents listing, and PE section analysis).
### C. Agentic Solving Orchestration
* **Action Space & Sandboxing**: API boundaries and isolated docker/sandbox execution paths to run unknown commands and binaries safely.
* **Autonomous Solving Agent**: Integration with LLM controllers that parse output from the forensics pipeline, suggest plans, iterate on solving steps, and verify flag captures.
---
@@ -96,3 +98,9 @@ To provide a clean, modern, and easily readable console output without building
* **Tables**: Use `rich.table.Table` to align and structure multi-column metadata outputs.
* **Formatting & Alerts**: Utilize `rich.console` or `rich.panel.Panel` to highlight warnings (such as signature/extension mismatches) with distinct styling and colors.
* **JSON Serialization**: Dataclasses should be easily convertible to dictionaries to support raw JSON output options for scripting pipelines.
### C. Function Isolation & Pipeline Composition
To support fully automated or agentic solving, all processing functions must be isolated and highly composable:
* **Pure Functions**: Core analysis and transformation functions (e.g., file carving, cipher decoders, and metadata extraction) must remain pure. They must accept standard Python types (e.g., `Path`, `bytes`, `str`) and return structured dataclasses without depending on global CLI state, click contexts, or interactive user prompts.
* **Pipeline Chaining**: The output of one tool must serve as valid input for another. For example, a list of files or byte arrays carved from a file-carver must be pipeable directly into the string parser or decompression module.
* **Programmatic API**: The entire core logic must be importable as standard Python APIs (`import ctf.utils`, `import ctf.forensics`) separate from CLI bindings. This allows external scripts, test cases, or autonomous agents to programmatically chain functions together to build complex, self-contained solving pipelines.

View File

@@ -12,6 +12,7 @@ The primary goal is to use the context of CTF challenges (forensics, crypto, web
4. **Educational Context:** Use the existing scripts and tools in this repository (like `psk_crack.py`, `exploit.sh`, or the `tools/` directory) as examples when explaining technical concepts.
5. **Vim Folding Markers:** Wrap all classes, command groups, subcommands, and functions inside project implementation files in standard Vim/Neovim folding syntax markers (`# {{{ <name>` and `# }}}`).
6. **Matching Test Files:** Every Python implementation file inside `src/` must have a corresponding test file under the `tests/` directory named `test_<filename>.py`.
7. **Write Only, Do Not Run**: The agent must only write code and tests. Do not attempt to run tests or execute command-line scripts; the user will handle all execution, verification, and testing tasks.
## Interaction Workflow
When discussing new implementations, features, or additions:
@@ -21,6 +22,7 @@ When discussing new implementations, features, or additions:
* **Add Test Cases**: Write or update the corresponding test cases in the test files (e.g. `tests/test_forensics.py`) following the SDET instructions.
* **Git Commit Current State**: Create a git commit of the current workspace state (including the new tests) *before* modifying any implementation files.
* **Update the Codebase**: Write/update the actual project implementation files as approved. Do **NOT** create a git commit after writing this implementation code. This ensures all implementation changes remain unstaged so the user can easily run `git diff` to review them.
* **Hand Over**: Present the changes to the user so they can run the tests. Do not run the tests yourself.
## Python Learning Objectives
- Understanding standard library modules relevant to security (e.g., `os`, `sys`, `base64`, `hashlib`).

View File

@@ -1,8 +1,10 @@
[Competition]
producer = "testProd"
competition = "testComp"
competition = "picoctf"
catagory = "textCat"
challenge = "testChal"
flag_format = "picoCTF\\{.*\\}"
last_flag = "picoCTF{flag}"
[Enviroment]
ctf_dir = "/home/venus/ctf"

View File

@@ -19,13 +19,17 @@ This file tracks the completed progress and upcoming development milestones for
## Upcoming Milestones & Features
### 📅 Phase 1: Configuration Completion & Basic File Scraper
* Extend [config.toml](file:///home/venus/code/ctf/config.toml) to store arbitrary data in the future
* Implement a basic tool to load the latest download files into the active directory
### 📅 Phase 1: Functional Forensics Pipeline & Toolkit
* **Pipeline Automation**: Integrate the existing forensics command-line utilities into a cohesive analysis pipeline where files are automatically checked for magic bytes, file extensions, and flag patterns.
* **Metadata & Extraction Toolkit**: Extend forensics tools to extract specific metadata (e.g., EXIF records, archive tables) and automate extraction/carving of nested data structures (e.g., binwalk-like carving, automated unzipping, extraction of hidden payloads).
* **QOL Utilities**: Add standard format outputs (JSON, Rich logs) and automatic logging of analysis artifacts to speed up user-led inspections.
### 📅 Phase 2: Downloads Organizer & Progress Tracker
* Write a tool to scan specified download directories for newly acquired challenge files and automatically organize them into the active competition's directory structure.
* Implement progress tracking to output current exploration paths, notes, and milestones.
### 📅 Phase 2: Core CTF Solving & Solver QOL
* **Challenge Organization**: Implement automated download management, challenge creation, directory configuration, and context management (`set-challenge`).
* **Solving Assistants**: Build automated helper scripts for common solving needs (e.g., basic cryptography decoders, web request templates, PSK cracking utility integration).
* **Solve Tracker & Note-taking QOL**: Create a command-line interface to capture solver actions, record active notes, log flag attempts, and update challenge statuses.
* **Per-Challenge Progress Logging**: Design a challenge-specific progress logger that creates and maintains isolated log files for each active challenge in a specified workspace directory, tracking attempts, timestamps, solver notes, and command history.
### 📅 Phase 3: Forensics Metadata Expansion
* Extend forensics tools to parse specific file-format metadata (e.g. EXIF data for JPGs or headers for specific archives).
### 📅 Phase 3: Agentic Solving Capabilities
* **Sandbox Environments**: Prepare secure, isolated environments to run untrusted challenge scripts or binaries.
* **Agent Orchestration**: Equip the toolchain with LLM agents capable of viewing the forensics pipeline outputs, reading challenge text, suggesting next steps, executing terminal tools, and recursively working to solve the challenge autonomously.

79
src/ctf/cli_forensics.py Normal file
View File

@@ -0,0 +1,79 @@
# src/ctf/cli_forensics.py
# CLI wrapper and rendering layer for forensics commands
# vim foldmethod=marker
import click
from pathlib import Path
from ctf.forensics import get_metadata, COMMON_SIGNATURES
# {{{ forensics_group
@click.group(name="forensics")
def forensics_group():
""" A collection of forensics tools """
pass
# }}}
# {{{ tf
@forensics_group.command()
def tf():
click.echo("hello from forensics")
# }}}
# {{{ inspect
@forensics_group.command()
@click.argument('path', type=click.Path(exists=True))
def inspect(path):
"""Lists all basic inode metadata about a file"""
meta = get_metadata(Path(path))
# 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)
# }}}

View File

@@ -1,5 +1,7 @@
# functions for commands needed
# src/commands.py
# vim foldmethod=marker
import click
from pathlib import Path
@@ -48,11 +50,16 @@ def set_flag_format(pattern: str, original: str):
if "Competition" not in config:
config["Competition"] = {}
comp_name = config["Competition"].get("competition", "")
if original:
if not original.strip():
raise click.UsageError("Original flag cannot be empty or whitespace only.")
patterns = suggest_patterns(original)
click.echo("Suggested regex patterns:")
if comp_name and comp_name.lower() not in original.lower():
click.echo(f"Warning: Current competition name '{comp_name}' was not found in the example flag.")
patterns = suggest_patterns(original, comp_name=comp_name)
click.echo(f"Suggested regex patterns for competition {comp_name}:")
for i, pat in enumerate(patterns, 1):
click.echo(f" {i}. {pat}")
@@ -64,15 +71,22 @@ def set_flag_format(pattern: str, original: str):
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
# if state.current_comp != comp:
# 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
# {{{ set_competition
# Sets the competition name in config.toml
@basic_group.command(name="set-competition")
@click.argument("name")
def set_competition(name: str):
"""Set the name of 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"]["competition"] = name
write_config(config, config_path)
click.echo(f"Competition name set to: {name}")
# }}}

View File

@@ -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]")
# }}}

View File

@@ -2,8 +2,87 @@
# Parses and calls commands
from ctf.commands import basic_group
from ctf.forensics import forensics_group
from ctf.cli_forensics import forensics_group
import click
import sys
import re
# Globally track the detector instance if it is active
_active_detector = None
# {{{ FlagDetectorStream
class FlagDetectorStream:
"""Intercepts stdout to scan output for matching flag formats."""
def __init__(self, original_stream, pattern_str):
self.original_stream = original_stream
self._flag_buffer = ""
self.detecting = True
# Clean pattern from standard anchors ^ and $ for substring matching
pat = pattern_str
if pat.startswith("^") and pat.endswith("$"):
pat = pat[1:-1]
try:
self.pattern = re.compile(pat) if pat else None
except re.error:
self.pattern = None
def write(self, data):
self.original_stream.write(data)
if self.detecting and self.pattern:
self._flag_buffer += data
matches = []
for match in self.pattern.finditer(self._flag_buffer):
matches.append(match.group())
if matches:
self.detecting = False
self.original_stream.write("\n\033[1;32m[!] Potential flag(s) detected in command output:\033[0m\n")
for match in matches:
self.original_stream.write(f" \033[1;32m✓ {match}\033[0m\n")
try:
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"]["last_flag"] = match
write_config(config, config_path)
except Exception:
pass
self._flag_buffer = ""
self.detecting = True
def flush(self):
self.original_stream.flush()
def __getattr__(self, name):
return getattr(self.original_stream, name)
# }}}
# {{{ flag_cmd
@click.command(name="flag")
@click.option("-p", "--plain", is_flag=True, help="Print raw flag without flavor text.")
def flag_cmd(plain):
"""Retrieve the last detected flag from config."""
global _active_detector
if _active_detector is not None:
_active_detector.detecting = False
try:
from ctf.utils import load_config
config = load_config("/home/venus/code/ctf/config.toml")
last_flag = config.get("Competition", {}).get("last_flag", "")
if not last_flag:
if not plain:
click.echo("No flag has been detected yet.")
return
if plain:
click.echo(last_flag, nl=False)
else:
click.echo(f"Last detected flag: {last_flag}")
finally:
if _active_detector is not None:
_active_detector.detecting = True
# }}}
# {{{ main
def main():
@@ -12,7 +91,24 @@ def main():
cli.add_command(forensics_group)
cli.add_command(basic_group)
cli()
cli.add_command(flag_cmd)
from ctf.utils import load_config
config = load_config("/home/venus/code/ctf/config.toml")
flag_format = config.get("Competition", {}).get("flag_format", "")
global _active_detector
if flag_format:
detector = FlagDetectorStream(sys.stdout, flag_format)
sys.stdout = detector
_active_detector = detector
try:
cli()
finally:
sys.stdout = detector.original_stream
_active_detector = None
else:
cli()
# }}}
if __name__ == "__main__":

View File

@@ -37,17 +37,14 @@ def active_competitions(dir: str) -> dict:
comps[item] = active_categories(item)
print(item.name)
print(comps[item])
return comps
# {{{ suggest_patterns
def suggest_patterns(s: str) -> list[str]:
"""Suggests a list of regex patterns from a sample flag string."""
def suggest_patterns(s: str, comp_name: str = "") -> list[str]:
"""Suggests a list of regex patterns from a sample flag string, optionally incorporating the competition name."""
if not s or not s.strip():
raise ValueError("Input string cannot be empty or whitespace only.")
# Group characters into types
groups = []
current_type = None
current_chars = []
def get_type(c):
if c.isupper():
return 'U'
@@ -57,19 +54,67 @@ def suggest_patterns(s: str) -> list[str]:
return 'D'
else:
return 'S'
for c in s:
ctype = get_type(c)
if ctype != current_type:
groups = []
comp_idx = -1
matched_prefix = ""
if comp_name and comp_name.strip():
comp_idx = s.lower().find(comp_name.lower())
if comp_idx != -1:
matched_prefix = s[comp_idx : comp_idx + len(comp_name)]
# Parse prefix before competition name
prefix_part = s[:comp_idx]
if prefix_part:
current_type = None
current_chars = []
for c in prefix_part:
ctype = get_type(c)
if ctype != current_type:
if current_type is not None:
groups.append((current_type, "".join(current_chars)))
current_type = ctype
current_chars = [c]
else:
current_chars.append(c)
if current_type is not None:
groups.append((current_type, "".join(current_chars)))
current_type = ctype
current_chars = [c]
else:
current_chars.append(c)
if current_type is not None:
groups.append((current_type, "".join(current_chars)))
# Insert competition name group
groups.append(('C', matched_prefix))
# Parse suffix after competition name
suffix_part = s[comp_idx + len(comp_name):]
if suffix_part:
current_type = None
current_chars = []
for c in suffix_part:
ctype = get_type(c)
if ctype != current_type:
if current_type is not None:
groups.append((current_type, "".join(current_chars)))
current_type = ctype
current_chars = [c]
else:
current_chars.append(c)
if current_type is not None:
groups.append((current_type, "".join(current_chars)))
else:
# Normal grouping without competition name
current_type = None
current_chars = []
for c in s:
ctype = get_type(c)
if ctype != current_type:
if current_type is not None:
groups.append((current_type, "".join(current_chars)))
current_type = ctype
current_chars = [c]
else:
current_chars.append(c)
if current_type is not None:
groups.append((current_type, "".join(current_chars)))
def escape_special(chars):
res = []
for c in chars:
@@ -79,55 +124,36 @@ def suggest_patterns(s: str) -> list[str]:
res.append(c)
return "".join(res)
# 1. Exact counts for group types
opt1_parts = []
for gtype, gchars in groups:
if gtype == 'U':
opt1_parts.append(f"[A-Z]{{{len(gchars)}}}")
elif gtype == 'L':
opt1_parts.append(f"[a-z]{{{len(gchars)}}}")
elif gtype == 'D':
opt1_parts.append(f"\\d{{{len(gchars)}}}")
else:
opt1_parts.append(escape_special(gchars))
opt1 = "^" + "".join(opt1_parts) + "$"
# 2. Variable counts for group types
opt2_parts = []
for gtype, gchars in groups:
if gtype == 'U':
opt2_parts.append("[A-Z]+")
elif gtype == 'L':
opt2_parts.append("[a-z]+")
elif gtype == 'D':
opt2_parts.append("\\d+")
else:
opt2_parts.append(escape_special(gchars))
opt2 = "^" + "".join(opt2_parts) + "$"
def build_pat(use_counts=True, merge_case=False):
parts = []
for gtype, gchars in groups:
if gtype == 'C':
parts.append(escape_special(gchars))
elif gtype == 'U':
if merge_case:
parts.append(f"[A-Za-z]{{{len(gchars)}}}" if use_counts else "[A-Za-z]+")
else:
parts.append(f"[A-Z]{{{len(gchars)}}}" if use_counts else "[A-Z]+")
elif gtype == 'L':
if merge_case:
parts.append(f"[A-Za-z]{{{len(gchars)}}}" if use_counts else "[A-Za-z]+")
else:
parts.append(f"[a-z]{{{len(gchars)}}}" if use_counts else "[a-z]+")
elif gtype == 'D':
parts.append(f"\\d{{{len(gchars)}}}" if use_counts else "\\d+")
else:
parts.append(escape_special(gchars))
return "".join(parts)
# 3. Case-insensitive / merged letters with exact counts
opt3_parts = []
for gtype, gchars in groups:
if gtype in ('U', 'L'):
opt3_parts.append(f"[A-Za-z]{{{len(gchars)}}}")
elif gtype == 'D':
opt3_parts.append(f"\\d{{{len(gchars)}}}")
else:
opt3_parts.append(escape_special(gchars))
opt3 = "^" + "".join(opt3_parts) + "$"
candidates = []
# 4. Case-insensitive / merged letters with variable counts
opt4_parts = []
for gtype, gchars in groups:
if gtype in ('U', 'L'):
opt4_parts.append("[A-Za-z]+")
elif gtype == 'D':
opt4_parts.append("\\d+")
else:
opt4_parts.append(escape_special(gchars))
opt4 = "^" + "".join(opt4_parts) + "$"
# 1. Unanchored patterns (ideal for searching/scanning)
candidates.append(build_pat(use_counts=True, merge_case=False))
candidates.append(build_pat(use_counts=False, merge_case=False))
candidates.append(build_pat(use_counts=True, merge_case=True))
candidates.append(build_pat(use_counts=False, merge_case=True))
# 5. General alphanumeric character class plus unique special characters
# Custom character class
unique_specials = set()
has_alpha = False
has_digit = False
@@ -139,7 +165,6 @@ def suggest_patterns(s: str) -> list[str]:
has_digit = True
else:
unique_specials.add(c)
char_class_parts = []
if has_alpha:
char_class_parts.append("A-Za-z")
@@ -150,13 +175,35 @@ def suggest_patterns(s: str) -> list[str]:
char_class_parts.append("\\" + spec)
else:
char_class_parts.append(spec)
opt5 = f"^[{''.join(char_class_parts)}]+$"
candidates.append(f"[{''.join(char_class_parts)}]+")
candidates.append(escape_special(s))
# 6. Literal exact escaped match
opt6 = "^" + escape_special(s) + "$"
# Wildcard patterns with .* and .*? (unanchored)
if comp_idx == 0:
suffix_part = s[len(comp_name):]
esc_prefix = escape_special(matched_prefix)
if suffix_part.startswith("{") and suffix_part.endswith("}"):
candidates.append(f"{esc_prefix}\\{{.*\\}}")
candidates.append(f"{esc_prefix}\\{{.*?\\}}")
candidates.append(f"{esc_prefix}\\{{[^}}]*\\}}")
candidates.append(f"{esc_prefix}\\{{[A-Za-z0-9_\\-]+\\}}")
inner_content = suffix_part[1:-1]
if all(c.islower() or c.isdigit() or c == '_' for c in inner_content):
candidates.append(f"{esc_prefix}\\{{[a-z0-9_]+\\}}")
else:
candidates.append(f"{esc_prefix}.*")
candidates.append(f"{esc_prefix}.*?")
else:
candidates.append(".*")
candidates.append(".*?")
# 2. Anchored versions of the above (ideal for strict validation)
anchored_candidates = []
for cand in candidates:
anchored_candidates.append(f"^{cand}$")
candidates.extend(anchored_candidates)
# Deduplicate while preserving order
candidates = [opt1, opt2, opt3, opt4, opt5, opt6]
seen = set()
result = []
for c in candidates:

BIN
tests/artifacts/cat.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 858 KiB

BIN
tests/artifacts/hips.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

BIN
tests/artifacts/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

1
tests/env/pure_flag_data.bin vendored Normal file
View File

@@ -0,0 +1 @@
pre_flag{hello_world_1337}post_stuff