Skip to content
Merged
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
50 changes: 48 additions & 2 deletions docker_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import subprocess
import csv
import pandas as pd
import logging
from typing import List, Tuple, Dict, Optional
from datetime import datetime
from fpdf import FPDF
Expand All @@ -12,7 +13,10 @@
from pathlib import Path
from config import RESULTS_DIR
from config import docker_score_prompt
from utils import ScoreResponse, get_llm, print_section
from utils import ScoreResponse, get_llm, print_section, get_custom_logger

# Initialize logger
logger = get_custom_logger(__name__)

class DockerSecurityScanner:
@staticmethod
Expand Down Expand Up @@ -134,7 +138,11 @@ def __init__(self, dockerfile_path: str, image_name: str, results_dir: str = RES
# Verify required tools
missing_tools = self._check_tools()
if missing_tools:
raise ValueError(f"Missing required tools: {', '.join(missing_tools)}")
error_msg = f"Missing required tools: {', '.join(missing_tools)}\n\n"
error_msg += "Installation instructions:\n"
for tool in missing_tools:
error_msg += f"\n{tool.upper()}:\n{self._get_tool_installation_instructions(tool)}\n"
raise ValueError(error_msg)

# Verify Dockerfile exists (after validation)
if self.dockerfile_path and not os.path.exists(self.dockerfile_path):
Expand Down Expand Up @@ -180,6 +188,7 @@ def run_image_only_scan(self, severity: str = "CRITICAL,HIGH") -> Dict:
"""
# Validate severity input
severity = self._validate_severity(severity)
logger.info(f"Starting image-only scan for {self.image_name}")
print(f"\n=== Starting image-only scan for {self.image_name} ===")

results = {
Expand Down Expand Up @@ -237,6 +246,31 @@ def _check_tools(self) -> List[str]:
missing_tools.append(tool)

return missing_tools

def _get_tool_installation_instructions(self, tool: str) -> str:
"""Get installation instructions for a missing tool."""
instructions = {
'docker': (
"Docker is required for image scanning. Please install Docker:\n"
" - Linux: https://docs.docker.com/engine/install/\n"
" - macOS: https://docs.docker.com/desktop/install/mac-install/\n"
" - Windows: https://docs.docker.com/desktop/install/windows-install/"
),
'trivy': (
"Trivy is required for vulnerability scanning. Install it:\n"
" - Linux/Mac: curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh -s -- -b /usr/local/bin\n"
" - Windows: See https://aquasecurity.github.io/trivy/latest/getting-started/installation/\n"
" - Or run: python setup_external_tools.py"
),
'hadolint': (
"Hadolint is required for Dockerfile linting. Install it:\n"
" - Linux: curl -L -o hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64 && chmod +x hadolint && sudo mv hadolint /usr/local/bin/\n"
" - macOS: brew install hadolint\n"
" - Windows: See https://github.com/hadolint/hadolint#install\n"
" - Or run: python setup_external_tools.py"
)
}
return instructions.get(tool, f"Please install {tool} from its official documentation.")

def scan_dockerfile(self) -> Tuple[bool, Optional[str]]:
"""
Expand All @@ -247,6 +281,7 @@ def scan_dockerfile(self) -> Tuple[bool, Optional[str]]:
- bool: True if no issues found, False otherwise
- Optional[str]: Output from the scan or None if successful
"""
logger.info(f"Starting Dockerfile scan with Hadolint: {self.dockerfile_path}")
print("\n=== Starting Dockerfile scan with Hadolint ===")
try:
result = subprocess.run(
Expand All @@ -259,16 +294,25 @@ def scan_dockerfile(self) -> Tuple[bool, Optional[str]]:

if result.returncode != 0:
output = result.stdout if result.stdout else result.stderr
logger.warning(f"Hadolint found issues in {self.dockerfile_path}")
print("Dockerfile linting issues found:")
print(output)
return False, output
else:
logger.info("No Dockerfile linting issues found.")
print("No Dockerfile linting issues found.")
return True, None

except subprocess.CalledProcessError as e:
logger.error(f"Error running Hadolint: {e}", exc_info=True)
print(f"Error running Hadolint: {e}")
return False, str(e)
except subprocess.TimeoutExpired:
logger.error(f"Hadolint scan timed out after 300 seconds for {self.dockerfile_path}")
return False, "Scan timeout"
except Exception as e:
logger.error(f"Unexpected error during Hadolint scan: {e}", exc_info=True)
return False, str(e)

def _filter_scan_results(self, scan_results: Dict) -> List[Dict]:
"""
Expand Down Expand Up @@ -321,6 +365,7 @@ def scan_image_json(self, severity: str = "CRITICAL,HIGH") -> Tuple[bool, Option
"""
# Validate severity input
severity = self._validate_severity(severity)
logger.info(f"Starting Trivy JSON scan for image: {self.image_name}")
print("\n=== Starting vulnerability scan with Trivy for Json Output ===")

try:
Expand Down Expand Up @@ -376,6 +421,7 @@ def scan_image(self, severity: str = "CRITICAL,HIGH") -> Tuple[bool, Optional[st
"""
# Validate severity input
severity = self._validate_severity(severity)
logger.info(f"Starting Trivy scan for image: {self.image_name} with severity: {severity}")
print("\n=== Starting vulnerability scan with Trivy ===")

try:
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"

[tool.black]
line-length = 88
target-version = ['py38']
target-version = ['py312']
include = '\.pyi?$'

[tool.isort]
Expand Down
27 changes: 16 additions & 11 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@
pydantic
langchain-core
langchain
langchain-openai
python-dotenv
pandas
# Core dependencies
pydantic>=2.0.0,<3.0.0
langchain-core>=0.3.0,<2.0.0
langchain>=0.3.0,<2.0.0
langchain-openai>=0.2.0,<1.0.0
python-dotenv>=1.0.0,<2.0.0
pandas>=2.0.0,<3.0.0

tqdm
colorama
rich
# UI and progress
tqdm>=4.65.0,<5.0.0
colorama>=0.4.6,<1.0.0
rich>=13.0.0,<14.0.0

fpdf
setuptools
# PDF generation
fpdf2>=2.7.0,<3.0.0

# Build tools
setuptools>=65.0.0
23 changes: 11 additions & 12 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,17 @@
},
python_requires=">=3.12",
install_requires=[
"pydantic",
"langchain-core",
"langchain",
"langchain-openai",
"python-dotenv",
"pandas",

"tqdm",
"colorama",
"rich",
"fpdf",
"setuptools",
"pydantic>=2.0.0,<3.0.0",
"langchain-core>=0.3.0,<2.0.0",
"langchain>=0.3.0,<2.0.0",
"langchain-openai>=0.2.0,<1.0.0",
"python-dotenv>=1.0.0,<2.0.0",
"pandas>=2.0.0,<3.0.0",
"tqdm>=4.65.0,<5.0.0",
"colorama>=0.4.6,<1.0.0",
"rich>=13.0.0,<14.0.0",
"fpdf2>=2.7.0,<3.0.0",
"setuptools>=65.0.0",
],
classifiers=[
"Programming Language :: Python :: 3",
Expand Down
46 changes: 46 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# DockSec Test Suite

This directory contains unit and integration tests for DockSec.

## Test Structure

- `test_docker_scanner.py`: Unit tests for the DockerSecurityScanner class
- `test_utils.py`: Unit tests for utility functions
- `test_config.py`: Unit tests for configuration management
- `test_integration.py`: Integration tests that test multiple components together
- `conftest.py`: Pytest fixtures and configuration

## Running Tests

### Using unittest
```bash
python -m unittest discover tests
```

### Using pytest (if installed)
```bash
pytest tests/
```

### Running specific test file
```bash
python -m unittest tests.test_docker_scanner
```

## Test Coverage

Currently, tests cover:
- Input validation (image names, file paths, severity levels)
- Tool checking and installation instructions
- Dockerfile loading
- Configuration and API key handling
- LLM initialization
- Basic scanner initialization

## Adding New Tests

When adding new functionality, please add corresponding tests:
1. Unit tests for individual functions/methods
2. Integration tests for workflows
3. Update this README if adding new test categories

2 changes: 2 additions & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
"""Tests for DockSec package."""

23 changes: 23 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
"""Pytest configuration and fixtures."""
import pytest
import os
import tempfile
import shutil


@pytest.fixture
def temp_dir():
"""Create a temporary directory for tests."""
test_dir = tempfile.mkdtemp()
yield test_dir
shutil.rmtree(test_dir)


@pytest.fixture
def sample_dockerfile(temp_dir):
"""Create a sample Dockerfile for testing."""
dockerfile_path = os.path.join(temp_dir, "Dockerfile")
with open(dockerfile_path, 'w') as f:
f.write("FROM ubuntu:latest\nRUN echo 'test'\n")
return dockerfile_path

38 changes: 38 additions & 0 deletions tests/test_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
"""Unit tests for configuration."""
import unittest
import os
from unittest.mock import patch


class TestConfig(unittest.TestCase):
"""Test cases for configuration."""

@patch.dict(os.environ, {}, clear=True)
def test_get_openai_api_key_missing(self):
"""Test API key retrieval when not set."""
from config import get_openai_api_key

with self.assertRaises(EnvironmentError):
get_openai_api_key()

@patch.dict(os.environ, {'OPENAI_API_KEY': 'test-key-123'})
def test_get_openai_api_key_present(self):
"""Test API key retrieval when set."""
from config import get_openai_api_key

api_key = get_openai_api_key()
self.assertEqual(api_key, 'test-key-123')

def test_prompt_templates_exist(self):
"""Test that prompt templates are defined."""
from config import docker_agent_template, docker_score_template

self.assertIsNotNone(docker_agent_template)
self.assertIsNotNone(docker_score_template)
self.assertIn("Dockerfile", docker_agent_template)
self.assertIn("score", docker_score_template.lower())


if __name__ == '__main__':
unittest.main()

Loading