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
75 changes: 74 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,77 @@ eeauditor/processor/outputs/*.html
LOCAL_external_providers.toml
output.json
output_ocsf_v1-4-0_events.json
gcp_cred.json
gcp_cred.json

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

# Claude settings
.claude/*

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

# Build artifacts
build/
dist/
*.egg-info/
*.egg
MANIFEST
.eggs/

# IDE files
.vscode/
.idea/
*.swp
*.swo
*.swn
.DS_Store

# Temporary files
*.tmp
*.temp
*.log
tmp/
temp/

# Python cache
__pycache__/
*.py[cod]
*$py.class

# Unit test / coverage reports
.nox/
.coverage.*
nosetests.xml
test-results/
junit.xml

# mypy
.mypy_cache/
.dmypy.json
dmypy.json

# Pyre type checker
.pyre/

# pytype static type analyzer
.pytype/

# Cython debug symbols
cython_debug/

# Poetry - DO NOT ignore poetry.lock
# poetry.lock is important for reproducible builds
3,617 changes: 3,617 additions & 0 deletions poetry.lock

Large diffs are not rendered by default.

103 changes: 103 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
[tool.poetry]
name = "electriceye"
version = "0.1.0"
description = "ElectricEye is a multi-cloud security auditing tool"
authors = ["ElectricEye Contributors"]
readme = "README.md"
packages = [{include = "eeauditor"}]

[tool.poetry.dependencies]
python = "^3.9"
awscli = ">=1.32.108"
boto3 = ">=1.34.108"
click = "8.1.8"
detect-secrets = "1.5.0"
google-api-python-client = ">=2.88.0"
matplotlib = ">=3.9.0"
oci = ">=2.104.0"
pandas = ">=2.2.0"
pluginbase = "1.0.1"
psycopg2-binary = ">=2.9.9"
pymongo = ">=4.6.1"
pysnow = "<=0.7.17"
python3-nmap = ">=1.6.0"
tomli = ">=2.0.1"
vt-py = ">=0.18.0"
snowflake-connector-python = ">=3.12.1"

[tool.poetry.group.dev.dependencies]
pytest = "^8.0.0"
pytest-cov = "^5.0.0"
pytest-mock = "^3.14.0"
black = "^24.0.0"

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

[tool.pytest.ini_options]
minversion = "8.0"
addopts = [
"-ra",
"--strict-markers",
"--strict-config",
"--cov=eeauditor",
"--cov-branch",
"--cov-report=term-missing:skip-covered",
"--cov-report=html",
"--cov-report=xml",
"--cov-fail-under=80",
]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
markers = [
"unit: marks tests as unit tests (fast, isolated)",
"integration: marks tests as integration tests (may require external resources)",
"slow: marks tests as slow-running tests",
]
filterwarnings = [
"error",
"ignore::UserWarning",
"ignore::DeprecationWarning",
]

[tool.coverage.run]
source = ["eeauditor"]
branch = true
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/site-packages/*",
"*/distutils/*",
"*/venv/*",
"*/.venv/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
exclude_lines = [
"pragma: no cover",
"def __repr__",
"def __str__",
"raise AssertionError",
"raise NotImplementedError",
"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.
196 changes: 196 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
import os
import json
import tempfile
import shutil
from pathlib import Path
from typing import Generator, Dict, Any
from unittest.mock import Mock, MagicMock

import pytest


@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory that is cleaned up after the test."""
temp_path = Path(tempfile.mkdtemp())
try:
yield temp_path
finally:
shutil.rmtree(temp_path, ignore_errors=True)


@pytest.fixture
def mock_config() -> Dict[str, Any]:
"""Provide a mock configuration dictionary for testing."""
return {
"aws": {
"region": "us-east-1",
"account_id": "123456789012",
"access_key_id": "AKIAIOSFODNN7EXAMPLE",
"secret_access_key": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
},
"azure": {
"subscription_id": "00000000-0000-0000-0000-000000000000",
"tenant_id": "00000000-0000-0000-0000-000000000000",
"client_id": "00000000-0000-0000-0000-000000000000",
"client_secret": "example_secret",
},
"gcp": {
"project_id": "example-project-123",
"credentials_path": "/path/to/credentials.json",
},
"output": {
"format": "json",
"path": "/tmp/output",
},
"log_level": "INFO",
"parallel_execution": True,
"max_workers": 4,
}


@pytest.fixture
def mock_aws_client():
"""Create a mock AWS client for testing."""
client = MagicMock()
client.describe_regions.return_value = {
"Regions": [
{"RegionName": "us-east-1"},
{"RegionName": "us-west-2"},
]
}
return client


@pytest.fixture
def mock_aws_session():
"""Create a mock AWS session for testing."""
session = MagicMock()
session.client = MagicMock(return_value=mock_aws_client())
return session


@pytest.fixture
def sample_finding() -> Dict[str, Any]:
"""Provide a sample finding dictionary for testing."""
return {
"SchemaVersion": "2018-10-08",
"Id": "test-finding-id",
"ProductArn": "arn:aws:securityhub:us-east-1:123456789012:product/electriceye/electriceye",
"GeneratorId": "test-generator",
"AwsAccountId": "123456789012",
"Types": ["Software and Configuration Checks/Industry and Regulatory Standards"],
"FirstObservedAt": "2024-01-01T00:00:00Z",
"LastObservedAt": "2024-01-01T00:00:00Z",
"CreatedAt": "2024-01-01T00:00:00Z",
"UpdatedAt": "2024-01-01T00:00:00Z",
"Severity": {
"Label": "MEDIUM",
"Normalized": 50
},
"Title": "Test Finding",
"Description": "This is a test finding for unit tests",
"Resources": [{
"Type": "AwsEc2Instance",
"Id": "arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
"Region": "us-east-1",
}],
"Compliance": {
"Status": "FAILED",
"RelatedRequirements": ["TEST-1.1"],
},
}


@pytest.fixture
def mock_env_vars(monkeypatch):
"""Set up mock environment variables for testing."""
env_vars = {
"AWS_DEFAULT_REGION": "us-east-1",
"AWS_ACCOUNT_ID": "123456789012",
"ELECTRICEYE_OUTPUT_FORMAT": "json",
"ELECTRICEYE_LOG_LEVEL": "INFO",
}
for key, value in env_vars.items():
monkeypatch.setenv(key, value)
return env_vars


@pytest.fixture
def temp_file(temp_dir: Path) -> Generator[Path, None, None]:
"""Create a temporary file within the temp directory."""
file_path = temp_dir / "test_file.txt"
file_path.write_text("test content")
yield file_path


@pytest.fixture
def json_file(temp_dir: Path) -> Generator[Path, None, None]:
"""Create a temporary JSON file with test data."""
file_path = temp_dir / "test_data.json"
test_data = {
"test": "data",
"nested": {
"key": "value"
},
"list": [1, 2, 3]
}
file_path.write_text(json.dumps(test_data, indent=2))
yield file_path


@pytest.fixture
def mock_cloud_provider():
"""Create a mock cloud provider instance."""
provider = Mock()
provider.name = "test_provider"
provider.is_authenticated = Mock(return_value=True)
provider.get_resources = Mock(return_value=[
{"id": "resource-1", "type": "instance"},
{"id": "resource-2", "type": "bucket"},
])
return provider


@pytest.fixture(autouse=True)
def reset_singleton_instances():
"""Reset any singleton instances between tests to ensure test isolation."""
yield


@pytest.fixture
def capture_logs(caplog):
"""Fixture to capture log messages during tests."""
caplog.set_level("DEBUG")
return caplog


@pytest.fixture
def mock_datetime(monkeypatch):
"""Mock datetime for consistent test results."""
import datetime

class MockDatetime:
@staticmethod
def now():
return datetime.datetime(2024, 1, 1, 12, 0, 0)

@staticmethod
def utcnow():
return datetime.datetime(2024, 1, 1, 12, 0, 0)

monkeypatch.setattr("datetime.datetime", MockDatetime)
return MockDatetime


def pytest_configure(config):
"""Configure pytest with custom settings."""
config.addinivalue_line(
"markers", "requires_aws: mark test as requiring AWS credentials"
)
config.addinivalue_line(
"markers", "requires_network: mark test as requiring network access"
)
config.addinivalue_line(
"markers", "destructive: mark test as potentially destructive"
)
Empty file added tests/integration/__init__.py
Empty file.
Loading