Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 70 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,71 @@
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
*.pyc

# C extensions
*.so

# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib64/
parts/
sdist/
var/
wheels/
pip-wheel-metadata/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST

# Testing
.pytest_cache/
.coverage
htmlcov/
coverage.xml
*.cover
*.py,cover
.hypothesis/
.tox/
.nox/

# Virtual environments
venv/
ENV/
env/
.venv/
.env

# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
.DS_Store

# Claude settings
.claude/*

# Poetry
poetry.lock

# Logs
*.log

# Local env files
.env.local
.env.*.local

# Temporary files
tmp/
temp/
86 changes: 86 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
[tool.poetry]
name = "redwarden"
version = "0.1.0"
description = "A security proxy tool for red team operations"
authors = ["RedWarden Team"]
readme = "README.md"
packages = [{include = "lib"}, {include = "plugins"}]

[tool.poetry.dependencies]
python = "^3.8"
brotli = "*"
requests = "*"
PyYaml = "*"
sqlitedict = "*"
tornado = "*"

[tool.poetry.group.dev.dependencies]
pytest = "^7.4.0"
pytest-cov = "^4.1.0"
pytest-mock = "^3.11.1"

[tool.poetry.scripts]
test = "pytest:main"
tests = "pytest:main"

[tool.pytest.ini_options]
minversion = "7.0"
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
"--cov=lib",
"--cov=plugins",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html:htmlcov",
"--cov-report=xml:coverage.xml",
"--cov-fail-under=0", # Set to 0 for infrastructure setup, change to 80 when writing actual tests
]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Tests that take a long time to run",
]

[tool.coverage.run]
source = ["lib", "plugins"]
omit = [
"*/tests/*",
"*/__pycache__/*",
"*/migrations/*",
"*/.venv/*",
"*/venv/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self\\.debug:",
"if settings\\.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]

[tool.coverage.html]
directory = "htmlcov"

[tool.coverage.xml]
output = "coverage.xml"

[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
Empty file added tests/__init__.py
Empty file.
139 changes: 139 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Shared pytest fixtures and configuration for all tests."""

import os
import sys
import tempfile
from pathlib import Path
from typing import Generator, Dict, Any
from unittest.mock import Mock, MagicMock

import pytest
import yaml

# Add project root to Python path
PROJECT_ROOT = Path(__file__).parent.parent
sys.path.insert(0, str(PROJECT_ROOT))


@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory for test files."""
with tempfile.TemporaryDirectory() as tmpdir:
yield Path(tmpdir)


@pytest.fixture
def mock_config() -> Dict[str, Any]:
"""Provide a mock configuration dictionary."""
return {
"proxy": {
"host": "127.0.0.1",
"port": 8080,
"ssl": False,
},
"logging": {
"level": "DEBUG",
"file": None,
},
"plugins": {
"enabled": ["malleable_redirector"],
"config": {}
}
}


@pytest.fixture
def mock_yaml_config(temp_dir: Path, mock_config: Dict[str, Any]) -> Path:
"""Create a temporary YAML config file."""
config_file = temp_dir / "test_config.yaml"
with open(config_file, 'w') as f:
yaml.dump(mock_config, f)
return config_file


@pytest.fixture
def mock_logger() -> Mock:
"""Provide a mock logger object."""
logger = Mock()
logger.debug = MagicMock()
logger.info = MagicMock()
logger.warning = MagicMock()
logger.error = MagicMock()
logger.critical = MagicMock()
return logger


@pytest.fixture
def mock_request() -> Mock:
"""Provide a mock HTTP request object."""
request = Mock()
request.method = "GET"
request.path = "/test"
request.headers = {"User-Agent": "TestAgent/1.0"}
request.body = b""
request.remote_addr = ("127.0.0.1", 12345)
return request


@pytest.fixture
def mock_response() -> Mock:
"""Provide a mock HTTP response object."""
response = Mock()
response.status_code = 200
response.headers = {"Content-Type": "text/html"}
response.body = b"<html><body>Test</body></html>"
return response


@pytest.fixture
def mock_plugin() -> Mock:
"""Provide a mock plugin object."""
plugin = Mock()
plugin.name = "test_plugin"
plugin.enabled = True
plugin.process_request = MagicMock(return_value=None)
plugin.process_response = MagicMock(return_value=None)
return plugin


@pytest.fixture(autouse=True)
def reset_environment():
"""Reset environment variables before each test."""
original_env = os.environ.copy()
yield
os.environ.clear()
os.environ.update(original_env)


@pytest.fixture
def sample_malleable_profile(temp_dir: Path) -> Path:
"""Create a sample Malleable C2 profile for testing."""
profile_content = """
set useragent "Mozilla/5.0 Test Browser";

http-get {
set uri "/test.php";

client {
header "Accept" "text/html";
}

server {
header "Server" "TestServer/1.0";
}
}
"""
profile_file = temp_dir / "test.profile"
profile_file.write_text(profile_content)
return profile_file


@pytest.fixture
def isolated_imports():
"""Ensure imports are isolated between tests."""
original_modules = sys.modules.copy()
yield
# Remove any modules imported during the test
for module in list(sys.modules.keys()):
if module not in original_modules:
del sys.modules[module]
Empty file added tests/integration/__init__.py
Empty file.
Loading