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
+16 -8
View File
@@ -71,16 +71,18 @@ Implements specialized forensic inspection utilities registered as a nested subg
## 5. Planned Architectural Components ## 5. Planned Architectural Components
### A. File Scraper & Extended Configuration ### A. Forensics Pipeline & Extended Metadata
* A scraping module to fetch details/files for challenges or competitions. * **Pipeline Coordination**: A central runner that coordinates file inspections, signature verification, and string searches sequentially on target files.
* Integration with an expanded configuration schema in [config.toml](file:///home/venus/code/ctf/config.toml) to store credentials, URLs, and directory preferences. * **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 ### B. Core Solving Infrastructure & QOL
* Monitoring or organizing downloaded challenge assets (e.g., from the browser's downloads folder) and sorting them into the correct competition/challenge subdirectories. * **Solving Assistants**: Standard templates and modular helper scripts for repeating CTF patterns (e.g., base conversions, cipher decoding, and request generation).
* An automated mechanism to log commands, notes, and milestones, providing clean progress documentation. * **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 ### C. Agentic Solving Orchestration
* 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). * **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. * **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. * **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. * **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.
+2
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. 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 `# }}}`). 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`. 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 ## Interaction Workflow
When discussing new implementations, features, or additions: 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. * **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. * **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. * **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 ## Python Learning Objectives
- Understanding standard library modules relevant to security (e.g., `os`, `sys`, `base64`, `hashlib`). - Understanding standard library modules relevant to security (e.g., `os`, `sys`, `base64`, `hashlib`).
+3 -1
View File
@@ -1,8 +1,10 @@
[Competition] [Competition]
producer = "testProd" producer = "testProd"
competition = "testComp" competition = "picoctf"
catagory = "textCat" catagory = "textCat"
challenge = "testChal" challenge = "testChal"
flag_format = "picoCTF\\{.*\\}"
last_flag = "picoCTF{flag}"
[Enviroment] [Enviroment]
ctf_dir = "/home/venus/ctf" ctf_dir = "/home/venus/ctf"
+12 -8
View File
@@ -19,13 +19,17 @@ This file tracks the completed progress and upcoming development milestones for
## Upcoming Milestones & Features ## Upcoming Milestones & Features
### 📅 Phase 1: Configuration Completion & Basic File Scraper ### 📅 Phase 1: Functional Forensics Pipeline & Toolkit
* Extend [config.toml](file:///home/venus/code/ctf/config.toml) to store arbitrary data in the future * **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.
* Implement a basic tool to load the latest download files into the active directory * **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 ### 📅 Phase 2: Core CTF Solving & Solver QOL
* Write a tool to scan specified download directories for newly acquired challenge files and automatically organize them into the active competition's directory structure. * **Challenge Organization**: Implement automated download management, challenge creation, directory configuration, and context management (`set-challenge`).
* Implement progress tracking to output current exploration paths, notes, and milestones. * **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 ### 📅 Phase 3: Agentic Solving Capabilities
* Extend forensics tools to parse specific file-format metadata (e.g. EXIF data for JPGs or headers for specific archives). * **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
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)
# }}}
+28 -14
View File
@@ -1,5 +1,7 @@
# functions for commands needed # functions for commands needed
# src/commands.py # src/commands.py
# vim foldmethod=marker
import click import click
from pathlib import Path from pathlib import Path
@@ -48,11 +50,16 @@ def set_flag_format(pattern: str, original: str):
if "Competition" not in config: if "Competition" not in config:
config["Competition"] = {} config["Competition"] = {}
comp_name = config["Competition"].get("competition", "")
if original: if original:
if not original.strip(): if not original.strip():
raise click.UsageError("Original flag cannot be empty or whitespace only.") raise click.UsageError("Original flag cannot be empty or whitespace only.")
patterns = suggest_patterns(original) if comp_name and comp_name.lower() not in original.lower():
click.echo("Suggested regex patterns:") 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): for i, pat in enumerate(patterns, 1):
click.echo(f" {i}. {pat}") 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}") click.echo(f"Flag format set to: {pattern}")
# }}} # }}}
# def Set_Challenge(comp: str, chal: str, setDirectory: bool): # {{{ set_competition
# # set the current challenge and competition from input # Sets the competition name in config.toml
# if state.current_comp != comp: @basic_group.command(name="set-competition")
# state.current_comp=comp @click.argument("name")
# state.comp_dir=pathlib def set_competition(name: str):
# # TODO archive the old competitions """Set the name of the active competition."""
# from ctf.utils import load_config, write_config
# if state.current_chal != chal:
# state.current_chal=chal config_path = "/home/venus/code/ctf/config.toml"
# print("challenge already set") config = load_config(config_path)
# # TODO archive the old challenges
# # TODO set the directory to challenge directory, with ignore option if "Competition" not in config:
config["Competition"] = {}
config["Competition"]["competition"] = name
write_config(config, config_path)
click.echo(f"Competition name set to: {name}")
# }}}
+8 -108
View File
@@ -1,8 +1,7 @@
# src/ctf/forensics.py # src/ctf/forensics.py
# Library for forensic analysis # Library for forensic analysis (pure functions only)
# vim foldmethod=marker # vim foldmethod=marker
import click
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
import stat import stat
@@ -35,16 +34,6 @@ COMMON_SIGNATURES = {
b"ID3": ("MP3 Audio", [".mp3"]), 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 # {{{ FileMetadata
@dataclass @dataclass
class FileMetadata: class FileMetadata:
@@ -78,12 +67,13 @@ class FileMetadata:
extended_attributes: Dict[str, str] = field(default_factory=dict) extended_attributes: Dict[str, str] = field(default_factory=dict)
# }}} # }}}
# {{{ inspect # {{{ get_metadata
@forensics_group.command() def get_metadata(path: Path) -> FileMetadata:
@click.argument('path', type=click.Path(exists=True)) """Extracts metadata attributes from a file without any console rendering."""
def inspect(path):
'''Lists all basic inode metadata about a file'''
p = Path(path) p = Path(path)
if not p.exists():
raise FileNotFoundError(f"File not found: {p}")
stat_info = p.stat() stat_info = p.stat()
# Apparent size & extension # Apparent size & extension
@@ -156,7 +146,7 @@ def inspect(path):
except OSError: except OSError:
pass pass
meta = FileMetadata( return FileMetadata(
filename=p.name, filename=p.name,
size=size, size=size,
magic=magic, magic=magic,
@@ -174,95 +164,5 @@ def inspect(path):
device=device, device=device,
extended_attributes=extended_attributes 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]")
# }}}
+97 -1
View File
@@ -2,8 +2,87 @@
# Parses and calls commands # Parses and calls commands
from ctf.commands import basic_group from ctf.commands import basic_group
from ctf.forensics import forensics_group from ctf.cli_forensics import forensics_group
import click 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 # {{{ main
def main(): def main():
@@ -12,6 +91,23 @@ def main():
cli.add_command(forensics_group) cli.add_command(forensics_group)
cli.add_command(basic_group) cli.add_command(basic_group)
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() cli()
# }}} # }}}
+100 -53
View File
@@ -37,17 +37,14 @@ def active_competitions(dir: str) -> dict:
comps[item] = active_categories(item) comps[item] = active_categories(item)
print(item.name) print(item.name)
print(comps[item]) print(comps[item])
return comps
# {{{ suggest_patterns # {{{ suggest_patterns
def suggest_patterns(s: str) -> list[str]: def suggest_patterns(s: str, comp_name: str = "") -> list[str]:
"""Suggests a list of regex patterns from a sample flag string.""" """Suggests a list of regex patterns from a sample flag string, optionally incorporating the competition name."""
if not s or not s.strip(): if not s or not s.strip():
raise ValueError("Input string cannot be empty or whitespace only.") raise ValueError("Input string cannot be empty or whitespace only.")
# Group characters into types
groups = []
current_type = None
current_chars = []
def get_type(c): def get_type(c):
if c.isupper(): if c.isupper():
return 'U' return 'U'
@@ -58,6 +55,54 @@ def suggest_patterns(s: str) -> list[str]:
else: else:
return 'S' return 'S'
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)))
# 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: for c in s:
ctype = get_type(c) ctype = get_type(c)
if ctype != current_type: if ctype != current_type:
@@ -79,55 +124,36 @@ def suggest_patterns(s: str) -> list[str]:
res.append(c) res.append(c)
return "".join(res) return "".join(res)
# 1. Exact counts for group types def build_pat(use_counts=True, merge_case=False):
opt1_parts = [] parts = []
for gtype, gchars in groups: for gtype, gchars in groups:
if gtype == 'U': if gtype == 'C':
opt1_parts.append(f"[A-Z]{{{len(gchars)}}}") 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': elif gtype == 'L':
opt1_parts.append(f"[a-z]{{{len(gchars)}}}") if merge_case:
elif gtype == 'D': parts.append(f"[A-Za-z]{{{len(gchars)}}}" if use_counts else "[A-Za-z]+")
opt1_parts.append(f"\\d{{{len(gchars)}}}")
else: else:
opt1_parts.append(escape_special(gchars)) parts.append(f"[a-z]{{{len(gchars)}}}" if use_counts else "[a-z]+")
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': elif gtype == 'D':
opt2_parts.append("\\d+") parts.append(f"\\d{{{len(gchars)}}}" if use_counts else "\\d+")
else: else:
opt2_parts.append(escape_special(gchars)) parts.append(escape_special(gchars))
opt2 = "^" + "".join(opt2_parts) + "$" return "".join(parts)
# 3. Case-insensitive / merged letters with exact counts candidates = []
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) + "$"
# 4. Case-insensitive / merged letters with variable counts # 1. Unanchored patterns (ideal for searching/scanning)
opt4_parts = [] candidates.append(build_pat(use_counts=True, merge_case=False))
for gtype, gchars in groups: candidates.append(build_pat(use_counts=False, merge_case=False))
if gtype in ('U', 'L'): candidates.append(build_pat(use_counts=True, merge_case=True))
opt4_parts.append("[A-Za-z]+") candidates.append(build_pat(use_counts=False, merge_case=True))
elif gtype == 'D':
opt4_parts.append("\\d+")
else:
opt4_parts.append(escape_special(gchars))
opt4 = "^" + "".join(opt4_parts) + "$"
# 5. General alphanumeric character class plus unique special characters # Custom character class
unique_specials = set() unique_specials = set()
has_alpha = False has_alpha = False
has_digit = False has_digit = False
@@ -139,7 +165,6 @@ def suggest_patterns(s: str) -> list[str]:
has_digit = True has_digit = True
else: else:
unique_specials.add(c) unique_specials.add(c)
char_class_parts = [] char_class_parts = []
if has_alpha: if has_alpha:
char_class_parts.append("A-Za-z") char_class_parts.append("A-Za-z")
@@ -150,13 +175,35 @@ def suggest_patterns(s: str) -> list[str]:
char_class_parts.append("\\" + spec) char_class_parts.append("\\" + spec)
else: else:
char_class_parts.append(spec) 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 # Wildcard patterns with .* and .*? (unanchored)
opt6 = "^" + escape_special(s) + "$" 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 # Deduplicate while preserving order
candidates = [opt1, opt2, opt3, opt4, opt5, opt6]
seen = set() seen = set()
result = [] result = []
for c in candidates: for c in candidates:
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

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