diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ee31c71..411c7df 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -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. diff --git a/GEMINI.md b/GEMINI.md index 9e18bff..1d5f020 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -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 (`# {{{ ` and `# }}}`). 6. **Matching Test Files:** Every Python implementation file inside `src/` must have a corresponding test file under the `tests/` directory named `test_.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`). diff --git a/config.toml b/config.toml index 1a2ceba..1a059bf 100644 --- a/config.toml +++ b/config.toml @@ -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" diff --git a/project_status.md b/project_status.md index 074173f..80baf2d 100644 --- a/project_status.md +++ b/project_status.md @@ -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. diff --git a/src/ctf/cli_forensics.py b/src/ctf/cli_forensics.py new file mode 100644 index 0000000..eaeacbb --- /dev/null +++ b/src/ctf/cli_forensics.py @@ -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) +# }}} + diff --git a/src/ctf/commands.py b/src/ctf/commands.py index 309136b..d5d9ad1 100644 --- a/src/ctf/commands.py +++ b/src/ctf/commands.py @@ -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}") +# }}} + diff --git a/src/ctf/forensics.py b/src/ctf/forensics.py index bc69f10..d011a3e 100644 --- a/src/ctf/forensics.py +++ b/src/ctf/forensics.py @@ -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]") -# }}} diff --git a/src/ctf/main.py b/src/ctf/main.py index 9cc9968..56c5cc2 100644 --- a/src/ctf/main.py +++ b/src/ctf/main.py @@ -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__": diff --git a/src/ctf/utils.py b/src/ctf/utils.py index 5b1407c..482e9fc 100644 --- a/src/ctf/utils.py +++ b/src/ctf/utils.py @@ -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: diff --git a/tests/artifacts/cat.jpg b/tests/artifacts/cat.jpg new file mode 100644 index 0000000..7351fbc Binary files /dev/null and b/tests/artifacts/cat.jpg differ diff --git a/tests/artifacts/easy-cat.jpg b/tests/artifacts/easy-cat.jpg new file mode 100644 index 0000000..a0404b7 Binary files /dev/null and b/tests/artifacts/easy-cat.jpg differ diff --git a/tests/artifacts/easy-cat.jpg_original b/tests/artifacts/easy-cat.jpg_original new file mode 100644 index 0000000..7351fbc Binary files /dev/null and b/tests/artifacts/easy-cat.jpg_original differ diff --git a/tests/artifacts/hips.jpg b/tests/artifacts/hips.jpg new file mode 100644 index 0000000..6c2b163 Binary files /dev/null and b/tests/artifacts/hips.jpg differ diff --git a/tests/artifacts/red.png b/tests/artifacts/red.png new file mode 100644 index 0000000..6c2b163 Binary files /dev/null and b/tests/artifacts/red.png differ diff --git a/tests/env/pure_flag_data.bin b/tests/env/pure_flag_data.bin new file mode 100644 index 0000000..c0726d3 --- /dev/null +++ b/tests/env/pure_flag_data.bin @@ -0,0 +1 @@ +pre_flag{hello_world_1337}post_stuff \ No newline at end of file