Release v1.9.0: Add video-comparer skill and enhance transcript-fixer

## New Skill: video-comparer v1.0.0
- Compare original and compressed videos with interactive HTML reports
- Calculate quality metrics (PSNR, SSIM) for compression analysis
- Generate frame-by-frame visual comparisons (slider, side-by-side, grid)
- Extract video metadata (codec, resolution, bitrate, duration)
- Multi-platform FFmpeg support with security features

## transcript-fixer Enhancements
- Add async AI processor for parallel processing
- Add connection pool management for database operations
- Add concurrency manager and rate limiter
- Add audit log retention and database migrations
- Add health check and metrics monitoring
- Add comprehensive test suite (8 new test files)
- Enhance security with domain and path validators

## Marketplace Updates
- Update marketplace version from 1.8.0 to 1.9.0
- Update skills count from 15 to 16
- Update documentation (README.md, CLAUDE.md, CHANGELOG.md)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
daymade
2025-10-30 00:23:12 +08:00
parent bd0aa12004
commit 9b724f33e3
49 changed files with 15357 additions and 270 deletions

View File

@@ -0,0 +1,436 @@
#!/usr/bin/env python3
"""
Test Suite for Path Validator
CRITICAL FIX VERIFICATION: Tests for Critical-5
Purpose: Verify path traversal and symlink attack prevention
Test Coverage:
1. Path traversal prevention (../)
2. Symlink attack detection
3. Directory whitelist enforcement
4. File extension validation
5. Null byte injection prevention
6. Path canonicalization
Author: Chief Engineer
Priority: P0 - Critical
"""
import pytest
import os
import sys
from pathlib import Path
import tempfile
# Add parent directory to path
sys.path.insert(0, str(Path(__file__).parent.parent))
from utils.path_validator import (
PathValidator,
PathValidationError,
validate_input_path,
validate_output_path,
ALLOWED_READ_EXTENSIONS,
ALLOWED_WRITE_EXTENSIONS,
)
class TestPathTraversalPrevention:
"""Test path traversal attack prevention"""
def test_parent_directory_traversal(self, tmp_path):
"""Test ../ path traversal is blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
# Create a file outside allowed directory
outside_dir = tmp_path.parent / "outside"
outside_dir.mkdir(exist_ok=True)
outside_file = outside_dir / "secret.md"
outside_file.write_text("secret data")
# Try to access it via ../
malicious_path = str(tmp_path / ".." / "outside" / "secret.md")
with pytest.raises(PathValidationError, match="Dangerous pattern"):
validator.validate_input_path(malicious_path)
# Cleanup
outside_file.unlink()
outside_dir.rmdir()
def test_absolute_path_outside_whitelist(self, tmp_path):
"""Test absolute paths outside whitelist are blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
# Try to access /etc/passwd
with pytest.raises(PathValidationError, match="not under allowed directories"):
validator.validate_input_path("/etc/passwd")
def test_multiple_parent_traversals(self, tmp_path):
"""Test ../../ is blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
with pytest.raises(PathValidationError, match="Dangerous pattern"):
validator.validate_input_path("../../etc/passwd")
class TestSymlinkAttacks:
"""Test symlink attack prevention"""
def test_direct_symlink_blocked(self, tmp_path):
"""Test direct symlink is blocked by default"""
validator = PathValidator(allowed_base_dirs={tmp_path})
# Create a real file
real_file = tmp_path / "real.md"
real_file.write_text("data")
# Create symlink to it
symlink = tmp_path / "link.md"
symlink.symlink_to(real_file)
with pytest.raises(PathValidationError, match="Symlink detected"):
validator.validate_input_path(str(symlink))
# Cleanup
symlink.unlink()
real_file.unlink()
def test_symlink_allowed_when_configured(self, tmp_path):
"""Test symlinks can be allowed"""
validator = PathValidator(
allowed_base_dirs={tmp_path},
allow_symlinks=True
)
# Create real file and symlink
real_file = tmp_path / "real.md"
real_file.write_text("data")
symlink = tmp_path / "link.md"
symlink.symlink_to(real_file)
# Should succeed with allow_symlinks=True
result = validator.validate_input_path(str(symlink))
assert result.exists()
# Cleanup
symlink.unlink()
real_file.unlink()
def test_symlink_in_parent_directory(self, tmp_path):
"""Test symlink in parent path is blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
# Create real directory
real_dir = tmp_path / "real_dir"
real_dir.mkdir()
# Create symlink to directory
symlink_dir = tmp_path / "link_dir"
symlink_dir.symlink_to(real_dir)
# Create file inside real directory
real_file = real_dir / "file.md"
real_file.write_text("data")
# Try to access via symlinked directory
malicious_path = symlink_dir / "file.md"
with pytest.raises(PathValidationError, match="Symlink"):
validator.validate_input_path(str(malicious_path))
# Cleanup
real_file.unlink()
symlink_dir.unlink()
real_dir.rmdir()
class TestDirectoryWhitelist:
"""Test directory whitelist enforcement"""
def test_file_in_allowed_directory(self, tmp_path):
"""Test file in allowed directory is accepted"""
validator = PathValidator(allowed_base_dirs={tmp_path})
test_file = tmp_path / "test.md"
test_file.write_text("test data")
result = validator.validate_input_path(str(test_file))
assert result == test_file.resolve()
test_file.unlink()
def test_file_outside_allowed_directory(self, tmp_path):
"""Test file outside allowed directory is rejected"""
allowed_dir = tmp_path / "allowed"
allowed_dir.mkdir()
validator = PathValidator(allowed_base_dirs={allowed_dir})
# File in parent directory (not in whitelist)
outside_file = tmp_path / "outside.md"
outside_file.write_text("data")
with pytest.raises(PathValidationError, match="not under allowed directories"):
validator.validate_input_path(str(outside_file))
outside_file.unlink()
def test_add_allowed_directory(self, tmp_path):
"""Test dynamically adding allowed directories"""
validator = PathValidator(allowed_base_dirs={tmp_path / "initial"})
new_dir = tmp_path / "new"
new_dir.mkdir()
# Should fail initially
test_file = new_dir / "test.md"
test_file.write_text("data")
with pytest.raises(PathValidationError):
validator.validate_input_path(str(test_file))
# Add directory to whitelist
validator.add_allowed_directory(new_dir)
# Should succeed now
result = validator.validate_input_path(str(test_file))
assert result.exists()
test_file.unlink()
class TestFileExtensionValidation:
"""Test file extension validation"""
def test_allowed_read_extension(self, tmp_path):
"""Test allowed read extensions are accepted"""
validator = PathValidator(allowed_base_dirs={tmp_path})
for ext in ['.md', '.txt', '.html', '.json']:
test_file = tmp_path / f"test{ext}"
test_file.write_text("data")
result = validator.validate_input_path(str(test_file))
assert result.exists()
test_file.unlink()
def test_disallowed_read_extension(self, tmp_path):
"""Test disallowed extensions are rejected for reading"""
validator = PathValidator(allowed_base_dirs={tmp_path})
dangerous_files = [
"script.sh",
"executable.exe",
"code.py",
"binary.bin",
]
for filename in dangerous_files:
test_file = tmp_path / filename
test_file.write_text("data")
with pytest.raises(PathValidationError, match="not allowed for reading"):
validator.validate_input_path(str(test_file))
test_file.unlink()
def test_allowed_write_extension(self, tmp_path):
"""Test allowed write extensions are accepted"""
validator = PathValidator(allowed_base_dirs={tmp_path})
for ext in ['.md', '.html', '.db', '.log']:
test_file = tmp_path / f"output{ext}"
result = validator.validate_output_path(str(test_file))
assert result.parent.exists()
def test_disallowed_write_extension(self, tmp_path):
"""Test disallowed extensions are rejected for writing"""
validator = PathValidator(allowed_base_dirs={tmp_path})
with pytest.raises(PathValidationError, match="not allowed for writing"):
validator.validate_output_path(str(tmp_path / "output.exe"))
class TestNullByteInjection:
"""Test null byte injection prevention"""
def test_null_byte_in_path(self, tmp_path):
"""Test null byte injection is blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
malicious_paths = [
"file.md\x00.exe",
"file\x00.md",
"\x00etc/passwd",
]
for path in malicious_paths:
with pytest.raises(PathValidationError, match="Dangerous pattern"):
validator.validate_input_path(path)
class TestNewlineInjection:
"""Test newline injection prevention"""
def test_newline_in_path(self, tmp_path):
"""Test newline injection is blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
malicious_paths = [
"file\n.md",
"file.md\r\n",
"file\r.md",
]
for path in malicious_paths:
with pytest.raises(PathValidationError, match="Dangerous pattern"):
validator.validate_input_path(path)
class TestOutputPathValidation:
"""Test output path validation"""
def test_output_path_creates_parent(self, tmp_path):
"""Test parent directory creation for output paths"""
validator = PathValidator(allowed_base_dirs={tmp_path})
output_path = tmp_path / "subdir" / "output.md"
result = validator.validate_output_path(str(output_path), create_parent=True)
assert result.parent.exists()
assert result == output_path.resolve()
def test_output_path_no_create_parent(self, tmp_path):
"""Test error when parent doesn't exist and create_parent=False"""
validator = PathValidator(allowed_base_dirs={tmp_path})
output_path = tmp_path / "nonexistent" / "output.md"
with pytest.raises(PathValidationError, match="Parent directory does not exist"):
validator.validate_output_path(str(output_path), create_parent=False)
class TestEdgeCases:
"""Test edge cases and corner scenarios"""
def test_empty_path(self):
"""Test empty path is rejected"""
validator = PathValidator()
with pytest.raises(PathValidationError):
validator.validate_input_path("")
def test_directory_instead_of_file(self, tmp_path):
"""Test directory path is rejected (expect file)"""
validator = PathValidator(allowed_base_dirs={tmp_path})
test_dir = tmp_path / "testdir"
test_dir.mkdir()
with pytest.raises(PathValidationError, match="not a file"):
validator.validate_input_path(str(test_dir))
test_dir.rmdir()
def test_nonexistent_file(self, tmp_path):
"""Test nonexistent file is rejected for reading"""
validator = PathValidator(allowed_base_dirs={tmp_path})
with pytest.raises(PathValidationError, match="does not exist"):
validator.validate_input_path(str(tmp_path / "nonexistent.md"))
def test_case_insensitive_extension(self, tmp_path):
"""Test extension matching is case-insensitive"""
validator = PathValidator(allowed_base_dirs={tmp_path})
test_file = tmp_path / "TEST.MD" # Uppercase extension
test_file.write_text("data")
# Should succeed (case-insensitive)
result = validator.validate_input_path(str(test_file))
assert result.exists()
test_file.unlink()
class TestGlobalValidator:
"""Test global validator convenience functions"""
def test_global_validate_input_path(self, tmp_path):
"""Test global validate_input_path function"""
from utils.path_validator import get_validator
# Add tmp_path to global validator
get_validator().add_allowed_directory(tmp_path)
test_file = tmp_path / "test.md"
test_file.write_text("data")
result = validate_input_path(str(test_file))
assert result.exists()
test_file.unlink()
def test_global_validate_output_path(self, tmp_path):
"""Test global validate_output_path function"""
from utils.path_validator import get_validator
get_validator().add_allowed_directory(tmp_path)
output_path = tmp_path / "output.md"
result = validate_output_path(str(output_path))
assert result == output_path.resolve()
class TestSecurityScenarios:
"""Test realistic attack scenarios"""
def test_zipslip_attack(self, tmp_path):
"""Test zipslip-style attack is blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
# Zipslip: ../../../etc/passwd
with pytest.raises(PathValidationError, match="Dangerous pattern"):
validator.validate_input_path("../../../etc/passwd")
def test_windows_path_traversal(self, tmp_path):
"""Test Windows-style path traversal is blocked"""
validator = PathValidator(allowed_base_dirs={tmp_path})
malicious_paths = [
"..\\..\\..\\windows\\system32",
"C:\\..\\..\\etc\\passwd",
]
for path in malicious_paths:
with pytest.raises(PathValidationError):
validator.validate_input_path(path)
def test_home_directory_expansion_safe(self, tmp_path):
"""Test home directory expansion works safely"""
# Create test file in actual home directory
home = Path.home()
test_file = home / "Documents" / "test_path_validator.md"
test_file.parent.mkdir(parents=True, exist_ok=True)
test_file.write_text("test")
validator = PathValidator() # Uses default whitelist including ~/Documents
# Should work with ~ expansion
result = validator.validate_input_path("~/Documents/test_path_validator.md")
assert result.exists()
# Cleanup
test_file.unlink()
# Run tests with: pytest -v test_path_validator.py
if __name__ == "__main__":
pytest.main([__file__, "-v", "--tb=short"])