Update GEMINI.md, add test cases for universal metadata, and configure rich dependency
This commit is contained in:
@@ -11,6 +11,15 @@ The primary goal is to use the context of CTF challenges (forensics, crypto, web
|
|||||||
3. **Summarization:** When directed, summarize documentation (local or web-based) to aid in understanding CTF tools and Python modules.
|
3. **Summarization:** When directed, summarize documentation (local or web-based) to aid in understanding CTF tools and Python modules.
|
||||||
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.
|
||||||
|
|
||||||
|
## Interaction Workflow
|
||||||
|
When discussing new implementations, features, or additions:
|
||||||
|
1. **Discussion**: Discuss implementation details and potential approaches.
|
||||||
|
2. **The Pitch**: Provide a clear, technical pitch detailing the planned code modifications.
|
||||||
|
3. **Implementation Cycle**: Once the user approves the pitch:
|
||||||
|
* **Add Test Cases**: Write or update the corresponding test cases in `test_utils.py` following the SDET instructions.
|
||||||
|
* **Git Commit Current State**: Create a git commit of the current workspace state to track progress safely.
|
||||||
|
* **Update the Codebase**: Write/update the actual project implementation files as approved.
|
||||||
|
|
||||||
## 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`).
|
||||||
- Analyzing existing scripts to understand control flow, data structures, and error handling.
|
- Analyzing existing scripts to understand control flow, data structures, and error handling.
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import toml
|
import toml
|
||||||
|
import os
|
||||||
|
import stat
|
||||||
|
import sys
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
from ctf.utils import load_config, active_categories
|
from ctf.utils import load_config, active_categories
|
||||||
from ctf.forensics import forensics_group, inspect
|
from ctf.forensics import forensics_group, inspect
|
||||||
@@ -7,10 +10,43 @@ from ctf.forensics import forensics_group, inspect
|
|||||||
# Define the persistent test environment path
|
# Define the persistent test environment path
|
||||||
TEST_ENV = Path("tests/env")
|
TEST_ENV = Path("tests/env")
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 1. Test Strategy Summary
|
||||||
|
# =====================================================================
|
||||||
|
# Core scenarios tested:
|
||||||
|
#
|
||||||
|
# A. Happy Path:
|
||||||
|
# - Verifying metadata attributes (permissions, ownership, size,
|
||||||
|
# hard links, inode, device) map exactly to filesystem ground truth.
|
||||||
|
#
|
||||||
|
# B. Boundary Conditions:
|
||||||
|
# - Testing empty files (0 bytes) for correct apparent size and allocation.
|
||||||
|
# - Testing file permissions changes (e.g. read-only, executable) and
|
||||||
|
# making sure the parsed octal/symbolic codes match.
|
||||||
|
# - Testing multiple hard links (count > 1).
|
||||||
|
#
|
||||||
|
# C. Edge Cases & OS Portability:
|
||||||
|
# - Extended attributes (xattr) support, handling platforms where xattr
|
||||||
|
# is not present or supported on the active file system.
|
||||||
|
# - Graceful system identity fallbacks for UID/GID names when passwd/group
|
||||||
|
# databases are unavailable or running on non-Unix systems.
|
||||||
|
#
|
||||||
|
# D. Error Handling:
|
||||||
|
# - Accessing non-existent paths (CLI validation errors).
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
|
# =====================================================================
|
||||||
|
# 2. Mocking/Setup Requirements
|
||||||
|
# =====================================================================
|
||||||
|
# - Filesystem sandbox: Uses `tests/env/` to dynamically create files
|
||||||
|
# with specific mode bits, content, and links.
|
||||||
|
# - No external network/API mocking is required.
|
||||||
|
# - OS-specific conditional blocks handle platforms (e.g., Windows)
|
||||||
|
# where POSIX ownership resolution or xattr is not native.
|
||||||
|
# =====================================================================
|
||||||
|
|
||||||
def test_load_config_static():
|
def test_load_config_static():
|
||||||
"""
|
"""Verifies load_config using the persistent test file."""
|
||||||
Verifies load_config using the persistent test file.
|
|
||||||
"""
|
|
||||||
config_file = TEST_ENV / "config.toml"
|
config_file = TEST_ENV / "config.toml"
|
||||||
test_data = {
|
test_data = {
|
||||||
"Competition": {"name": "PersistentComp"},
|
"Competition": {"name": "PersistentComp"},
|
||||||
@@ -24,9 +60,7 @@ def test_load_config_static():
|
|||||||
assert loaded_data["Competition"]["name"] == "PersistentComp"
|
assert loaded_data["Competition"]["name"] == "PersistentComp"
|
||||||
|
|
||||||
def test_active_categories_static():
|
def test_active_categories_static():
|
||||||
"""
|
"""Verifies active_categories using the pre-created 'comp1' folder."""
|
||||||
Verifies active_categories using the pre-created 'comp1' folder.
|
|
||||||
"""
|
|
||||||
comp_dir = TEST_ENV / "comp1"
|
comp_dir = TEST_ENV / "comp1"
|
||||||
comp_dir.mkdir(parents=True, exist_ok=True)
|
comp_dir.mkdir(parents=True, exist_ok=True)
|
||||||
(comp_dir / "web").mkdir(exist_ok=True)
|
(comp_dir / "web").mkdir(exist_ok=True)
|
||||||
@@ -38,42 +72,162 @@ def test_active_categories_static():
|
|||||||
assert "web" in cat_names
|
assert "web" in cat_names
|
||||||
assert "pwn" in cat_names
|
assert "pwn" in cat_names
|
||||||
|
|
||||||
# --- Direct Function Callback Tests ---
|
|
||||||
|
|
||||||
def test_inspect_callback_success():
|
# --- Task-Specific Universal Metadata Tests ---
|
||||||
|
|
||||||
|
def test_inspect_permissions():
|
||||||
"""
|
"""
|
||||||
Verifies that calling the inspect command's callback directly returns
|
Task 1: POSIX Permissions.
|
||||||
the correct FileMetadata dataclass with expected values.
|
Verifies that the parsed octal and symbolic representation of permissions
|
||||||
|
matches Python's native os.stat mode decoding.
|
||||||
"""
|
"""
|
||||||
test_file = TEST_ENV / "callback_test.txt"
|
test_file = TEST_ENV / "perm_test.bin"
|
||||||
with open(test_file, "wb") as f:
|
with open(test_file, "wb") as f:
|
||||||
f.write(b"Hello World")
|
f.write(b"data")
|
||||||
|
|
||||||
|
# Set permissions explicitly to 0o755 (rwxr-xr-x)
|
||||||
|
test_file.chmod(0o755)
|
||||||
|
|
||||||
# Using inspect.callback to invoke the underlying undecorated function
|
|
||||||
meta = inspect.callback(str(test_file))
|
meta = inspect.callback(str(test_file))
|
||||||
|
|
||||||
assert meta.filename == "callback_test.txt"
|
# Assert correct octal format
|
||||||
assert meta.size == 11
|
assert meta.permissions_octal == "0o755"
|
||||||
assert meta.magic == "48656C6C" # Hex for "Hell"
|
# Assert correct symbolic format (standard file type prefix '-')
|
||||||
assert meta.extension == ".txt"
|
assert meta.permissions_symbolic == "-rwxr-xr-x"
|
||||||
|
|
||||||
def test_inspect_callback_empty_file():
|
# Change permissions to 0o644 (rw-r--r--)
|
||||||
|
test_file.chmod(0o644)
|
||||||
|
meta_new = inspect.callback(str(test_file))
|
||||||
|
assert meta_new.permissions_octal == "0o644"
|
||||||
|
assert meta_new.permissions_symbolic == "-rw-r--r--"
|
||||||
|
|
||||||
|
|
||||||
|
def test_inspect_ownership():
|
||||||
"""
|
"""
|
||||||
Tests the boundary case of an empty (0-byte) file.
|
Task 2: Ownership Identity.
|
||||||
Verifies that the parser handles it gracefully without raising exceptions.
|
Verifies that numeric UID/GID and resolved username/groupname match.
|
||||||
"""
|
"""
|
||||||
test_file = TEST_ENV / "empty_test.txt"
|
test_file = TEST_ENV / "owner_test.bin"
|
||||||
with open(test_file, "wb") as f:
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"data")
|
||||||
|
|
||||||
|
stat_info = test_file.stat()
|
||||||
|
meta = inspect.callback(str(test_file))
|
||||||
|
|
||||||
|
assert meta.owner_uid == stat_info.st_uid
|
||||||
|
assert meta.owner_gid == stat_info.st_gid
|
||||||
|
|
||||||
|
# On Unix, verify that user and group strings are resolved
|
||||||
|
if sys.platform != "win32":
|
||||||
|
import pwd
|
||||||
|
import grp
|
||||||
|
expected_user = pwd.getpwuid(stat_info.st_uid).pw_name
|
||||||
|
expected_group = grp.getgrgid(stat_info.st_gid).gr_name
|
||||||
|
assert meta.owner_username == expected_user
|
||||||
|
assert meta.owner_groupname == expected_group
|
||||||
|
else:
|
||||||
|
# Fallback for Windows
|
||||||
|
assert isinstance(meta.owner_username, str)
|
||||||
|
assert isinstance(meta.owner_groupname, str)
|
||||||
|
|
||||||
|
|
||||||
|
def test_inspect_allocation():
|
||||||
|
"""
|
||||||
|
Task 3: Allocation Metrics.
|
||||||
|
Verifies apparent file size matches size_bytes, and allocated_size
|
||||||
|
corresponds to st_blocks * 512 bytes.
|
||||||
|
"""
|
||||||
|
test_file = TEST_ENV / "alloc_test.bin"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"A" * 1234)
|
||||||
|
|
||||||
|
stat_info = test_file.stat()
|
||||||
|
meta = inspect.callback(str(test_file))
|
||||||
|
|
||||||
|
assert meta.size == 1234
|
||||||
|
|
||||||
|
# st_blocks is Unix-specific. On other platforms, fallback to size on disk
|
||||||
|
if hasattr(stat_info, "st_blocks"):
|
||||||
|
expected_blocks_size = stat_info.st_blocks * 512
|
||||||
|
assert meta.allocated_size == expected_blocks_size
|
||||||
|
else:
|
||||||
|
assert meta.allocated_size >= 1234
|
||||||
|
|
||||||
|
|
||||||
|
def test_inspect_hard_links():
|
||||||
|
"""
|
||||||
|
Task 4: Hard Link Count.
|
||||||
|
Verifies that the hard link count changes when files are linked.
|
||||||
|
"""
|
||||||
|
test_file = TEST_ENV / "link_test.bin"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"link")
|
||||||
|
|
||||||
|
meta_single = inspect.callback(str(test_file))
|
||||||
|
assert meta_single.hard_links == 1
|
||||||
|
|
||||||
|
# Create a hard link
|
||||||
|
link_file = TEST_ENV / "link_test_hard.bin"
|
||||||
|
if link_file.exists():
|
||||||
|
link_file.unlink()
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.link(str(test_file), str(link_file))
|
||||||
|
meta_linked = inspect.callback(str(test_file))
|
||||||
|
assert meta_linked.hard_links == 2
|
||||||
|
finally:
|
||||||
|
if link_file.exists():
|
||||||
|
link_file.unlink()
|
||||||
|
|
||||||
|
|
||||||
|
def test_inspect_inode_device():
|
||||||
|
"""
|
||||||
|
Task 5: Inode & Device Identifiers.
|
||||||
|
Verifies that the unique Inode number and Device ID match os.stat results.
|
||||||
|
"""
|
||||||
|
test_file = TEST_ENV / "inode_test.bin"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"data")
|
||||||
|
|
||||||
|
stat_info = test_file.stat()
|
||||||
|
meta = inspect.callback(str(test_file))
|
||||||
|
|
||||||
|
assert meta.inode == stat_info.st_ino
|
||||||
|
assert meta.device == stat_info.st_dev
|
||||||
|
|
||||||
|
|
||||||
|
def test_inspect_extended_attributes():
|
||||||
|
"""
|
||||||
|
Task 6: Extended Attributes (xattr).
|
||||||
|
Checks that user-defined extended attributes can be retrieved if supported
|
||||||
|
by the platform and filesystem.
|
||||||
|
"""
|
||||||
|
test_file = TEST_ENV / "xattr_test.bin"
|
||||||
|
with open(test_file, "wb") as f:
|
||||||
|
f.write(b"data")
|
||||||
|
|
||||||
|
# Attempt to set an extended attribute (Linux/macOS specific)
|
||||||
|
has_xattr_support = False
|
||||||
|
if sys.platform in ("linux", "darwin"):
|
||||||
|
try:
|
||||||
|
import xattr
|
||||||
|
# Use user namespace for custom attribute
|
||||||
|
os.setxattr(str(test_file), "user.ctf_flag", b"FLAG{filesystem_metadata_ftw}")
|
||||||
|
has_xattr_support = True
|
||||||
|
except (ImportError, OSError, AttributeError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
meta = inspect.callback(str(test_file))
|
meta = inspect.callback(str(test_file))
|
||||||
|
|
||||||
assert meta.filename == "empty_test.txt"
|
if has_xattr_support:
|
||||||
assert meta.size == 0
|
assert "user.ctf_flag" in meta.extended_attributes
|
||||||
assert meta.magic == "" # Empty string as no bytes could be read
|
assert meta.extended_attributes["user.ctf_flag"] == "FLAG{filesystem_metadata_ftw}"
|
||||||
assert meta.extension == ".txt"
|
else:
|
||||||
|
# Should gracefully return an empty dict if not supported or none defined
|
||||||
|
assert isinstance(meta.extended_attributes, dict)
|
||||||
|
|
||||||
# --- CLI Integration Tests ---
|
|
||||||
|
# --- CLI Validation Tests ---
|
||||||
|
|
||||||
def test_forensics_inspect_cli_success():
|
def test_forensics_inspect_cli_success():
|
||||||
"""
|
"""
|
||||||
@@ -89,7 +243,7 @@ def test_forensics_inspect_cli_success():
|
|||||||
assert result.exit_code == 0
|
assert result.exit_code == 0
|
||||||
assert "Metadata: test_file.png" in result.output
|
assert "Metadata: test_file.png" in result.output
|
||||||
assert "Size" in result.output
|
assert "Size" in result.output
|
||||||
assert "89504E47" in result.output # Hex representation of PNG signature
|
assert "89504E47" in result.output
|
||||||
|
|
||||||
def test_forensics_inspect_cli_missing_file():
|
def test_forensics_inspect_cli_missing_file():
|
||||||
"""
|
"""
|
||||||
|
|||||||
36
uv.lock
generated
36
uv.lock
generated
@@ -30,6 +30,7 @@ source = { editable = "." }
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "platformdirs" },
|
{ name = "platformdirs" },
|
||||||
|
{ name = "rich" },
|
||||||
{ name = "toml" },
|
{ name = "toml" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ dev = [
|
|||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "click" },
|
{ name = "click" },
|
||||||
{ name = "platformdirs", specifier = ">=4.9.4" },
|
{ name = "platformdirs", specifier = ">=4.9.4" },
|
||||||
|
{ name = "rich" },
|
||||||
{ name = "toml", specifier = ">=0.10.2" },
|
{ name = "toml", specifier = ">=0.10.2" },
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -57,6 +59,27 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markdown-it-py"
|
||||||
|
version = "4.2.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "mdurl" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mdurl"
|
||||||
|
version = "0.1.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "packaging"
|
name = "packaging"
|
||||||
version = "26.1"
|
version = "26.1"
|
||||||
@@ -109,6 +132,19 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rich"
|
||||||
|
version = "15.0.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "markdown-it-py" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" },
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.10.2"
|
version = "0.10.2"
|
||||||
|
|||||||
Reference in New Issue
Block a user