Skip to content

Commit 17ce3df

Browse files
committed
Integrate Docker API functionality by adding endpoints for listing running containers and retrieving code paths. Update Docker Compose configuration to allow socket access. Include new dependencies in pyproject.toml for Docker integration. Remove obsolete migration files to clean up the project structure.
1 parent 9943118 commit 17ce3df

10 files changed

Lines changed: 278 additions & 4 deletions

backend/core/docker_service.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import docker
2+
from docker.errors import DockerException, NotFound
3+
4+
def list_running_containers():
5+
"""
6+
Connects to the Docker daemon and lists running containers.
7+
Returns a list of dictionaries, each containing container info,
8+
or an error message string.
9+
"""
10+
try:
11+
client = docker.from_env()
12+
containers = client.containers.list()
13+
container_list = []
14+
for container in containers:
15+
container_list.append({
16+
"id": container.short_id,
17+
"name": container.name,
18+
"image": container.attrs['Config']['Image'],
19+
"status": container.status
20+
})
21+
if not container_list:
22+
return "No running containers found."
23+
return container_list
24+
except DockerException as e:
25+
return f"Docker API error: {str(e)}"
26+
except Exception as e:
27+
return f"An unexpected error occurred: {str(e)}"
28+
29+
def get_container_code_paths(container_id: str):
30+
"""
31+
Retrieves the host paths for volume mounts of a given container.
32+
33+
Args:
34+
container_id: The ID or name of the Docker container.
35+
36+
Returns:
37+
A list of host source paths for the container's mounts,
38+
or an error message string if an issue occurs.
39+
"""
40+
try:
41+
client = docker.from_env()
42+
container = client.containers.get(container_id)
43+
mounts = container.attrs.get('Mounts', [])
44+
host_paths = []
45+
for mount in mounts:
46+
if mount.get('Type') == 'volume' or mount.get('Type') == 'bind': # Consider both volumes and binds
47+
# For named volumes, 'Source' might be the volume name, not a host path.
48+
# For binds, 'Source' is the host path.
49+
# We are primarily interested in host paths that could contain code.
50+
if mount.get('Source') and mount.get('Source').startswith('/'): # Heuristic: host paths are absolute
51+
host_paths.append(mount['Source'])
52+
53+
if not host_paths and mounts:
54+
return f"Container '{container_id}' has mounts, but no direct host source paths found: {mounts}"
55+
elif not host_paths:
56+
return f"No volume mounts found for container '{container_id}' that appear to be host paths."
57+
58+
return host_paths
59+
except NotFound:
60+
return f"Container '{container_id}' not found."
61+
except DockerException as e:
62+
return f"Docker API error while inspecting container '{container_id}': {str(e)}"
63+
except Exception as e:
64+
return f"An unexpected error occurred while inspecting container '{container_id}': {str(e)}"
65+
66+
if __name__ == '__main__':
67+
print("Attempting to list running containers...")
68+
running_containers = list_running_containers()
69+
if isinstance(running_containers, str): # Error message
70+
print(running_containers)
71+
elif running_containers:
72+
print("Running containers:")
73+
for cont in running_containers:
74+
print(f" ID: {cont['id']}, Name: {cont['name']}, Image: {cont['image']}, Status: {cont['status']}")
75+
print(f" Attempting to get code paths for {cont['id']} ({cont['name']})...")
76+
paths = get_container_code_paths(cont['id'])
77+
if isinstance(paths, str): # Error message
78+
print(f" Error/Info: {paths}")
79+
elif paths:
80+
print(" Found potential host paths:")
81+
for path in paths:
82+
print(f" - {path}")
83+
else: # Empty list but no error string
84+
print(f" No specific host paths identified for {cont['id']}.")
85+
86+
else: # Empty list from list_running_containers, but not an error string.
87+
print("No running containers were found to inspect.")

backend/core/migrations/0007_project_default_tool_settings_json_and_more.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

backend/core/migrations/0008_project_project_main_targets_json_and_more.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

backend/core/migrations/0009_scanjob_target_info_scanjob_tool_settings.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

backend/core/tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Diese Datei macht das 'tests'-Verzeichnis zu einem Python-Paket
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
from django.urls import reverse
2+
from django.contrib.auth import get_user_model
3+
from rest_framework import status
4+
from rest_framework.test import APITestCase
5+
from unittest.mock import patch
6+
7+
User = get_user_model()
8+
9+
class DockerAPITests(APITestCase):
10+
def setUp(self):
11+
# Create an admin user
12+
self.admin_user = User.objects.create_superuser(
13+
username='admin_test',
14+
email='admin_test@example.com',
15+
password='admin_password123'
16+
)
17+
# Create a regular user
18+
self.regular_user = User.objects.create_user(
19+
username='user_test',
20+
email='user_test@example.com',
21+
password='user_password123'
22+
)
23+
24+
self.list_containers_url = reverse('docker-list-containers')
25+
# URL for GetDockerContainerPathsView (needs a placeholder container_id)
26+
self.container_paths_url_template = reverse('docker-container-paths', kwargs={'container_id': 'PLACEHOLDER_ID'})
27+
28+
def test_list_docker_containers_permissions_and_basic_success(self):
29+
"""
30+
Test permissions for the list_docker_containers endpoint and a basic success scenario.
31+
"""
32+
# 1. Test unauthenticated access
33+
response = self.client.get(self.list_containers_url)
34+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
35+
36+
# 2. Test access with regular user (should be forbidden)
37+
self.client.force_authenticate(user=self.regular_user)
38+
response = self.client.get(self.list_containers_url)
39+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
40+
self.client.force_authenticate(user=None) # Clear authentication
41+
42+
# 3. Test access with admin user (should be successful)
43+
# We mock the actual service call to avoid real Docker interaction
44+
mock_container_data = [
45+
{"id": "abc123xyz", "name": "test_container_1", "image": "test_image:latest", "status": "running"},
46+
{"id": "def456uvw", "name": "test_container_2", "image": "another_image:1.0", "status": "running"}
47+
]
48+
49+
with patch('core.views.list_running_containers') as mock_list_containers:
50+
mock_list_containers.return_value = mock_container_data
51+
52+
self.client.force_authenticate(user=self.admin_user)
53+
response = self.client.get(self.list_containers_url)
54+
55+
self.assertEqual(response.status_code, status.HTTP_200_OK)
56+
self.assertEqual(len(response.data), 2)
57+
self.assertEqual(response.data[0]['name'], 'test_container_1')
58+
mock_list_containers.assert_called_once() # Ensure our service function was called
59+
60+
def test_list_docker_containers_service_error(self):
61+
"""
62+
Test the list_docker_containers endpoint when the service returns an error.
63+
"""
64+
error_message = "Docker API error: Connection refused"
65+
with patch('core.views.list_running_containers') as mock_list_containers:
66+
mock_list_containers.return_value = error_message
67+
68+
self.client.force_authenticate(user=self.admin_user)
69+
response = self.client.get(self.list_containers_url)
70+
71+
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
72+
self.assertIn('error', response.data)
73+
self.assertEqual(response.data['error'], error_message)
74+
mock_list_containers.assert_called_once()
75+
76+
def test_get_container_paths_permissions_and_success(self):
77+
"""
78+
Test permissions and basic success scenario for GetDockerContainerPathsView.
79+
"""
80+
test_container_id = "test_container_123"
81+
specific_container_paths_url = self.container_paths_url_template.replace('PLACEHOLDER_ID', test_container_id)
82+
83+
# 1. Test unauthenticated access
84+
response = self.client.get(specific_container_paths_url)
85+
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
86+
87+
# 2. Test access with regular user (should be forbidden)
88+
self.client.force_authenticate(user=self.regular_user)
89+
response = self.client.get(specific_container_paths_url)
90+
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
91+
self.client.force_authenticate(user=None) # Clear authentication
92+
93+
# 3. Test access with admin user (should be successful)
94+
mock_path_data = ['/mnt/code/project_a', '/var/www/html']
95+
with patch('core.views.get_container_code_paths') as mock_get_paths:
96+
mock_get_paths.return_value = mock_path_data
97+
98+
self.client.force_authenticate(user=self.admin_user)
99+
response = self.client.get(specific_container_paths_url)
100+
101+
self.assertEqual(response.status_code, status.HTTP_200_OK)
102+
self.assertEqual(len(response.data), 2)
103+
self.assertEqual(response.data[0], '/mnt/code/project_a')
104+
# Ensure the service function was called with the correct container_id
105+
mock_get_paths.assert_called_once_with(test_container_id)
106+
107+
def test_get_container_paths_not_found(self):
108+
"""
109+
Test GetDockerContainerPathsView when the container is not found.
110+
"""
111+
test_container_id = "non_existent_container"
112+
specific_container_paths_url = self.container_paths_url_template.replace('PLACEHOLDER_ID', test_container_id)
113+
error_message = f"Container '{test_container_id}' not found."
114+
115+
with patch('core.views.get_container_code_paths') as mock_get_paths:
116+
mock_get_paths.return_value = error_message
117+
118+
self.client.force_authenticate(user=self.admin_user)
119+
response = self.client.get(specific_container_paths_url)
120+
121+
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
122+
self.assertIn('error', response.data)
123+
self.assertEqual(response.data['error'], error_message)
124+
mock_get_paths.assert_called_once_with(test_container_id)
125+
126+
def test_get_container_paths_service_error(self):
127+
"""
128+
Test GetDockerContainerPathsView when the service returns a generic error.
129+
"""
130+
test_container_id = "another_container_id"
131+
specific_container_paths_url = self.container_paths_url_template.replace('PLACEHOLDER_ID', test_container_id)
132+
error_message = "Docker API error: Unexpected issue"
133+
134+
with patch('core.views.get_container_code_paths') as mock_get_paths:
135+
mock_get_paths.return_value = error_message
136+
137+
self.client.force_authenticate(user=self.admin_user)
138+
response = self.client.get(specific_container_paths_url)
139+
140+
self.assertEqual(response.status_code, status.HTTP_500_INTERNAL_SERVER_ERROR)
141+
self.assertIn('error', response.data)
142+
self.assertEqual(response.data['error'], error_message)
143+
mock_get_paths.assert_called_once_with(test_container_id)
144+
145+
# Additional test methods for GetDockerContainerPathsView will be added here
146+
# and for error cases of ListDockerContainersView.

backend/core/urls.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
ProjectViewSet, ScanTargetViewSet, TargetGroupViewSet,
55
SecurityToolViewSet, ScanConfigurationViewSet, UserProfileViewSet,
66
ApiKeyViewSet, ScanTriggerViewSet, ScanJobViewSet,
7-
ProjectMembershipViewSet, UserViewSet, CIScanTriggerViewSet
7+
ProjectMembershipViewSet, UserViewSet, CIScanTriggerViewSet,
8+
ListDockerContainersView, GetDockerContainerPathsView
89
)
910

1011
router = DefaultRouter()
@@ -23,4 +24,6 @@
2324

2425
urlpatterns = [
2526
path('', include(router.urls)),
27+
path('docker/containers/', ListDockerContainersView.as_view(), name='docker-list-containers'),
28+
path('docker/containers/<str:container_id>/paths/', GetDockerContainerPathsView.as_view(), name='docker-container-paths'),
2629
]

backend/core/views.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@
2626
from .tasks import execute_scan_job, simulate_bandit_scan # Ensure both are imported
2727
from django.contrib.auth import get_user_model
2828
from .authentication import ApiKeyAuthentication # Import the custom authentication class
29+
# Docker API Integration Imports
30+
from rest_framework.views import APIView
31+
from .docker_service import list_running_containers, get_container_code_paths # Assuming docker_service.py is in the same app directory
32+
# End Docker API Integration Imports
2933
User = get_user_model()
3034

3135
# Create your views here.
@@ -584,3 +588,37 @@ def create(self, request, *args, **kwargs):
584588
response_serializer = ScanJobSerializer(scan_job)
585589
return Response(response_serializer.data, status=status.HTTP_201_CREATED)
586590
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
591+
592+
# Docker API Views
593+
class ListDockerContainersView(APIView):
594+
"""
595+
Lists all currently running Docker containers.
596+
Requires admin privileges.
597+
"""
598+
permission_classes = [IsAdminUser]
599+
600+
def get(self, request, *args, **kwargs):
601+
containers_or_error = list_running_containers()
602+
if isinstance(containers_or_error, str): # Error message returned
603+
if "No running containers found" in containers_or_error:
604+
return Response({"message": containers_or_error}, status=status.HTTP_200_OK) # Or 404 if preferred
605+
return Response({"error": containers_or_error}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
606+
return Response(containers_or_error, status=status.HTTP_200_OK)
607+
608+
class GetDockerContainerPathsView(APIView):
609+
"""
610+
Retrieves potential host code paths for a given Docker container.
611+
Requires admin privileges.
612+
"""
613+
permission_classes = [IsAdminUser]
614+
615+
def get(self, request, container_id, *args, **kwargs):
616+
paths_or_error = get_container_code_paths(container_id)
617+
if isinstance(paths_or_error, str): # Error message returned
618+
if "not found" in paths_or_error.lower():
619+
return Response({"error": paths_or_error}, status=status.HTTP_404_NOT_FOUND)
620+
# For other errors from the service, consider them server-side issues or specific Docker issues
621+
return Response({"error": paths_or_error}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
622+
if not paths_or_error: # Empty list, but valid response
623+
return Response([], status=status.HTTP_200_OK)
624+
return Response(paths_or_error, status=status.HTTP_200_OK)

backend/pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ dj-rest-auth = ">=6.0.0,<7.0.0"
2323
django-allauth = ">=0.63.0,<0.64.0"
2424
requests = ">=2.30.0,<3.0.0"
2525
django-celery-beat = ">=2.6.0,<3.0.0"
26+
docker = "^7.0.0"
2627

2728
[build-system]
2829
requires = ["poetry-core>=1.0.0"]

docker-compose.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ services:
4747
- ./backend:/app
4848
- backend_static_collected:/app/staticfiles_collected
4949
- backend_media_data:/app/media
50+
- /var/run/docker.sock:/var/run/docker.sock
5051
ports:
5152
- "8000:8000"
5253
env_file:

0 commit comments

Comments
 (0)