Update GEMINI.md, add test cases for universal metadata, and configure rich dependency

This commit is contained in:
venus
2026-07-17 01:19:25 -05:00
parent 70e793c4a3
commit 1b53658a50
3 changed files with 228 additions and 29 deletions

View File

@@ -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.

View File

@@ -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
View File

@@ -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"