Skip to content

Commit af26c36

Browse files
authored
Merge pull request #56 from advaitpatel/ci/enforce-tests-on-pr
ci: enforce pytest as required gate on all PRs, fix pre-existing test failures
2 parents 88fcb2d + 0068e2e commit af26c36

6 files changed

Lines changed: 98 additions & 172 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 10 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,30 +11,28 @@ jobs:
1111
coverage:
1212
runs-on: ubuntu-latest
1313
name: Test Coverage Report
14-
14+
1515
steps:
1616
- uses: actions/checkout@v4
17-
17+
1818
- name: Set up Python
1919
uses: actions/setup-python@v4
2020
with:
2121
python-version: '3.12'
22-
22+
2323
- name: Install dependencies
2424
run: |
2525
python -m pip install --upgrade pip
2626
pip install pytest pytest-cov
27-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
28-
27+
pip install -r requirements.txt
28+
2929
- name: Install package
30-
run: |
31-
pip install -e .
32-
30+
run: pip install -e .
31+
3332
- name: Run tests with coverage
3433
run: |
35-
pytest tests/ --cov=. --cov-report=xml --cov-report=html --cov-report=term-missing || echo "Tests completed with coverage"
36-
continue-on-error: true
37-
34+
pytest tests/ --cov=. --cov-report=xml --cov-report=html --cov-report=term-missing
35+
3836
- name: Upload coverage to Codecov
3937
uses: codecov/codecov-action@v4
4038
with:
@@ -44,7 +42,7 @@ jobs:
4442
fail_ci_if_error: false
4543
env:
4644
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
47-
45+
4846
- name: Upload coverage reports as artifact
4947
uses: actions/upload-artifact@v4
5048
with:
@@ -53,26 +51,3 @@ jobs:
5351
coverage.xml
5452
htmlcov/
5553
if-no-files-found: ignore
56-
57-
- name: Generate Coverage Summary
58-
run: |
59-
echo "## Coverage Report" >> $GITHUB_STEP_SUMMARY
60-
echo "" >> $GITHUB_STEP_SUMMARY
61-
if [ -f coverage.xml ]; then
62-
echo "Coverage report generated successfully!" >> $GITHUB_STEP_SUMMARY
63-
echo "" >> $GITHUB_STEP_SUMMARY
64-
echo "📊 View detailed HTML report in artifacts" >> $GITHUB_STEP_SUMMARY
65-
echo "" >> $GITHUB_STEP_SUMMARY
66-
# Extract coverage percentage if available
67-
if command -v coverage &> /dev/null; then
68-
echo "### Coverage Details:" >> $GITHUB_STEP_SUMMARY
69-
coverage report --format=markdown >> $GITHUB_STEP_SUMMARY 2>/dev/null || echo "Run completed" >> $GITHUB_STEP_SUMMARY
70-
fi
71-
else
72-
echo "⚠️ No coverage data generated" >> $GITHUB_STEP_SUMMARY
73-
fi
74-
75-
- name: Coverage Badge
76-
run: |
77-
echo "Add this badge to your README.md:"
78-
echo "[![codecov](https://codecov.io/gh/advaitpatel/DockSec/branch/main/graph/badge.svg)](https://codecov.io/gh/advaitpatel/DockSec)"

.github/workflows/python-app.yml

Lines changed: 42 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -10,81 +10,61 @@ on:
1010
jobs:
1111
build-and-test:
1212
runs-on: ubuntu-latest
13-
13+
1414
steps:
1515
- uses: actions/checkout@v4
16-
16+
1717
- name: Set up Python
1818
uses: actions/setup-python@v4
1919
with:
2020
python-version: '3.12'
21-
21+
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install -r requirements.txt
26+
27+
- name: Build and install package
28+
run: |
29+
pip install build
30+
python -m build
31+
pip install -e .
32+
2233
- name: Setup Hadolint
2334
run: |
2435
curl -sL -o hadolint https://github.com/hadolint/hadolint/releases/latest/download/hadolint-Linux-x86_64
2536
chmod +x hadolint
2637
sudo mv hadolint /usr/local/bin/
27-
38+
2839
- name: Setup Trivy
2940
run: |
30-
sudo apt-get update
41+
sudo apt-get update -qq
3142
sudo apt-get install -y wget apt-transport-https gnupg lsb-release
3243
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo gpg --dearmor -o /usr/share/keyrings/trivy.gpg
3344
echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/trivy.list
34-
sudo apt-get update
45+
sudo apt-get update -qq
3546
sudo apt-get install -y trivy
36-
37-
- name: Install dependencies
38-
run: |
39-
python -m pip install --upgrade pip
40-
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
41-
42-
- name: Build package
43-
run: |
44-
pip install build
45-
python -m build
46-
47-
- name: Install package in development mode
47+
48+
- name: Run unit tests
4849
run: |
49-
pip install -e .
50-
# Ensure all dependencies are installed (including from setup.py)
51-
pip install -r requirements.txt
52-
53-
- name: Run Hadolint on Dockerfiles
50+
pip install pytest
51+
pytest tests/ -v
52+
53+
- name: Lint Dockerfiles with Hadolint
5454
run: |
5555
find . -name "Dockerfile*" -exec hadolint {} \;
56-
57-
- name: Run Trivy for vulnerability scanning
56+
57+
- name: Scan Dockerfiles with Trivy
5858
run: |
5959
find . -name "Dockerfile*" -exec trivy config {} \;
60-
61-
- name: Debug folder structure
62-
run: |
63-
echo "Current directory: $(pwd)"
64-
ls -R
65-
66-
- name: Run tests
67-
env:
68-
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
60+
61+
- name: Verify CLI installs correctly
6962
run: |
70-
# Test 1: Version check
71-
echo "=========================================="
72-
echo "Test 1: Version check"
73-
echo "=========================================="
7463
docksec --version
75-
76-
# Test 2: Help command
77-
echo ""
78-
echo "=========================================="
79-
echo "Test 2: Help command"
80-
echo "=========================================="
81-
docksec -h
82-
83-
# Test 3: Create a test Dockerfile
84-
echo ""
85-
echo "=========================================="
86-
echo "Test 3: Creating test Dockerfile"
87-
echo "=========================================="
64+
docksec --help
65+
66+
- name: Run scan-only mode (no AI required)
67+
run: |
8868
mkdir -p test_dir
8969
cat > test_dir/Dockerfile << 'EOF'
9070
FROM alpine:latest
@@ -93,68 +73,18 @@ jobs:
9373
COPY . .
9474
CMD ["sh"]
9575
EOF
96-
cat test_dir/Dockerfile
97-
98-
# Test 4: Pull Docker image for testing
99-
echo ""
100-
echo "=========================================="
101-
echo "Test 4: Pull test Docker image"
102-
echo "=========================================="
10376
docker pull alpine:latest
104-
echo "✅ Alpine image pulled successfully"
105-
106-
# Test 5: Scan-only mode (no AI)
107-
echo ""
108-
echo "=========================================="
109-
echo "Test 5: Scan-only mode (no AI)"
110-
echo "=========================================="
111-
docksec test_dir/Dockerfile --scan-only -i alpine:latest || echo "Scan completed"
112-
113-
# Test 6: Image-only scan
114-
echo ""
115-
echo "=========================================="
116-
echo "Test 6: Image-only scan"
117-
echo "=========================================="
118-
docksec --image-only -i alpine:latest || echo "Image scan completed"
119-
120-
# Test 7: Verify results directory was created
121-
echo ""
122-
echo "=========================================="
123-
echo "Test 7: Checking results"
124-
echo "=========================================="
125-
if [ -d "results" ]; then
126-
echo "✅ Results directory created successfully"
127-
echo "Contents:"
128-
ls -lh results/ 2>/dev/null || echo "No files in results (may be expected)"
129-
else
130-
echo "⚠️ Results directory not found"
131-
echo "This may be expected if scans failed due to missing API key"
132-
fi
133-
134-
# Cleanup
135-
echo ""
136-
echo "=========================================="
137-
echo "Cleanup"
138-
echo "=========================================="
77+
docksec test_dir/Dockerfile --scan-only -i alpine:latest
13978
rm -rf test_dir
140-
echo "Test directory cleaned up"
141-
142-
echo ""
143-
echo "=========================================="
144-
echo "✅ All DockSec CLI tests passed!"
145-
echo "=========================================="
146-
echo ""
147-
echo "Summary:"
148-
echo "- Version flag: ✅ Working"
149-
echo "- Help command: ✅ Working"
150-
echo "- CLI installation: ✅ Working"
151-
echo "- Scan modes: ⚠️ Working (API key warnings expected)"
152-
echo ""
153-
echo "Note: Full AI features require OPENAI_API_KEY to be set in GitHub Secrets"
154-
155-
- name: Upload coverage report
79+
80+
- name: Run image-only scan
81+
run: |
82+
docksec --image-only -i alpine:latest
83+
84+
- name: Upload scan results
15685
uses: actions/upload-artifact@v4
86+
if: always()
15787
with:
158-
name: coverage-report
159-
path: coverage.xml
160-
if-no-files-found: ignore
88+
name: scan-results
89+
path: results/
90+
if-no-files-found: ignore

docker_scanner.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,14 @@ def _validate_file_path(file_path: str) -> Path:
3535
"""
3636
if not file_path:
3737
raise ValueError("File path cannot be empty")
38-
38+
39+
# Check the raw string before resolution — Path.resolve() removes '..'
40+
# so checking the resolved path would silently allow traversal attempts.
41+
if '..' in file_path:
42+
raise ValueError(f"Invalid path: path traversal detected in '{file_path}'")
43+
3944
try:
4045
path = Path(file_path).resolve()
41-
# Check for path traversal (parent directory access)
42-
if '..' in str(path):
43-
raise ValueError(f"Invalid path: path traversal detected in '{file_path}'")
4446
return path
4547
except (OSError, ValueError) as e:
4648
raise ValueError(f"Invalid file path '{file_path}': {str(e)}")
@@ -71,11 +73,10 @@ def _validate_image_name(image_name: str) -> str:
7173
if '..' in image_name or image_name.startswith('/'):
7274
raise ValueError(f"Image name contains path traversal or absolute path: '{image_name}'")
7375

74-
# Check for dangerous characters that could be used in command injection
75-
dangerous_chars = [';', '&', '|', '`', '$', '(', ')', '<', '>', '\n', '\r']
76-
for char in dangerous_chars:
77-
if char in image_name:
78-
raise ValueError(f"Image name contains invalid character: '{char}'")
76+
# Whitelist: Docker image names allow alphanumeric, '/', ':', '-', '_', '.', '@'
77+
# Anything outside this set (spaces, shell metacharacters, etc.) is rejected.
78+
if not re.match(r'^[a-zA-Z0-9/:._\-@]+$', image_name):
79+
raise ValueError(f"Image name contains invalid characters: '{image_name}'")
7980

8081
return image_name.strip()
8182

tests/test_docker_scanner.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,9 @@ def test_init_with_valid_inputs(self, mock_llm, mock_subprocess):
4747
from docker_scanner import DockerSecurityScanner
4848

4949
scanner = DockerSecurityScanner(dockerfile, "test:latest")
50-
self.assertEqual(scanner.dockerfile_path, dockerfile)
50+
# Compare resolved paths — on macOS tempfile returns /var/... but
51+
# _validate_file_path resolves it to /private/var/... via symlink.
52+
self.assertEqual(scanner.dockerfile_path, str(Path(dockerfile).resolve()))
5153
self.assertEqual(scanner.image_name, "test:latest")
5254
self.assertIsNone(scanner.analysis_score)
5355

tests/test_integration.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import os
44
import tempfile
55
import shutil
6+
from pathlib import Path
67
from unittest.mock import patch, Mock
78

89
# Import after mocking external dependencies
@@ -54,7 +55,9 @@ def subprocess_side_effect(*args, **kwargs):
5455

5556
# Verify initialization
5657
self.assertIsNotNone(scanner)
57-
self.assertEqual(scanner.dockerfile_path, self.test_dockerfile)
58+
# Compare resolved paths — on macOS tempfile returns /var/... but
59+
# _validate_file_path resolves it to /private/var/... via symlink.
60+
self.assertEqual(scanner.dockerfile_path, str(Path(self.test_dockerfile).resolve()))
5861
self.assertEqual(scanner.image_name, "test:latest")
5962

6063
@patch('docker_scanner.subprocess.run')

tests/test_utils.py

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,28 +43,43 @@ def test_load_docker_file_not_found(self):
4343
result = load_docker_file("/nonexistent/path/Dockerfile")
4444
self.assertIsNone(result)
4545

46-
@patch('utils.get_openai_api_key')
4746
@patch('utils.ChatOpenAI')
48-
def test_get_llm(self, mock_chatopenai, mock_api_key):
49-
"""Test LLM initialization."""
47+
@patch('config_manager.get_config')
48+
def test_get_llm(self, mock_get_config, mock_chatopenai):
49+
"""Test LLM initialization with a mocked config and mocked ChatOpenAI."""
5050
from utils import get_llm
51-
52-
mock_api_key.return_value = "test-api-key"
51+
52+
mock_config = Mock()
53+
mock_config.llm_provider = "openai"
54+
mock_config.llm_model = "gpt-4o"
55+
mock_config.llm_temperature = 0.0
56+
mock_config.timeout_llm = 60
57+
mock_config.max_retries_llm = 2
58+
mock_config.get_api_key_for_provider.return_value = "test-api-key"
59+
mock_get_config.return_value = mock_config
60+
5361
mock_llm_instance = Mock()
5462
mock_chatopenai.return_value = mock_llm_instance
55-
63+
5664
llm = get_llm()
57-
65+
5866
mock_chatopenai.assert_called_once()
5967
self.assertIsNotNone(llm)
60-
61-
@patch('utils.get_openai_api_key')
62-
def test_get_llm_no_api_key(self, mock_api_key):
63-
"""Test LLM initialization without API key."""
68+
69+
@patch('config_manager.get_config')
70+
def test_get_llm_no_api_key(self, mock_get_config):
71+
"""Test LLM initialization raises EnvironmentError when API key is missing."""
6472
from utils import get_llm
65-
66-
mock_api_key.side_effect = EnvironmentError("API key not found")
67-
73+
74+
mock_config = Mock()
75+
mock_config.llm_provider = "openai"
76+
mock_config.llm_model = "gpt-4o"
77+
mock_config.llm_temperature = 0.0
78+
mock_config.timeout_llm = 60
79+
mock_config.max_retries_llm = 2
80+
mock_config.get_api_key_for_provider.side_effect = EnvironmentError("API key not found")
81+
mock_get_config.return_value = mock_config
82+
6883
with self.assertRaises(EnvironmentError):
6984
get_llm()
7085

0 commit comments

Comments
 (0)