Skip to content

Commit bd448ab

Browse files
authored
oscap-docker: don't start images to scan them
Starting images to scan them has downsides, including that the image must be able to be started and stay running, which isn't always easy. For example, not all images contain `bash` or a shell at all. By instead using the approach taken by `oscap-podman` and extracting the image then scanning it (instead of starting the container and scanning the resulting mount), the container never needs to be run. This approach allows `oscap-docker` to scan any image (including those without `bash`).
1 parent aba83f3 commit bd448ab

File tree

1 file changed

+39
-34
lines changed

1 file changed

+39
-34
lines changed

utils/oscap_docker_python/oscap_docker_util.py

Lines changed: 39 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
from __future__ import print_function
2020

2121
import os
22+
import io
23+
from pathlib import Path
24+
from itertools import chain
25+
import tarfile
2226
import tempfile
2327
import shutil
2428
from oscap_docker_python.get_cve_input import getInputCVE
@@ -45,63 +49,51 @@ def __init__(self, target, is_image=False, oscap_binary='oscap'):
4549
# init docker high level API (to deal with start/stop/run containers/image)
4650
self.client_api = docker.from_env()
4751
self.is_image = is_image
48-
self.stop_at_end = False # stop the container after scan if True
4952
self.oscap_binary = oscap_binary or 'oscap'
5053
self.container_name = None
5154
self.image_name = None
55+
self.extracted_container = False
5256

5357
# init docker low level api (useful for deep details like container pid)
5458
self.client = self.client_api.api
5559

5660
if self.is_image:
5761
self.image_name, self.config = self._get_image_name_and_config(target)
5862
if self.image_name:
59-
print("Running given image in a temporary container ...")
63+
print("Creating a temporary container for the image...")
6064
self.container_name = "tmp_oscap_" + str(uuid.uuid1())
6165

6266
try:
6367
tmp_cont = self.client.create_container(
64-
self.image_name, 'bash', name=self.container_name, tty=True)
65-
# tty=True is required in order to keep the container running
66-
self.client.start(container=tmp_cont.get('Id'))
68+
self.image_name, name=self.container_name)
6769

6870
self.config = self.client.inspect_container(self.container_name)
69-
if int(self.config["State"]["Pid"]) == 0:
70-
sys.stderr.write("Cannot run image {0}.\n".format(self.image_name))
71-
else:
72-
self.pid = int(self.config["State"]["Pid"])
7371
except Exception as e:
74-
sys.stderr.write("Cannot run image {0}.\n".format(self.image_name))
72+
sys.stderr.write("Cannot create container for image {0}.\n".format(self.image_name))
7573
raise e
74+
75+
self._extract_container()
7676
else:
7777
raise ValueError("Image {0} not found.\n".format(target))
7878

7979
else:
8080
self.container_name, self.config = \
8181
self._get_container_name_and_config(target)
82+
if not self.container_name:
83+
raise ValueError("Container {0} not found.\n".format(target))
8284

8385
# is the container running ?
8486
if int(self.config["State"]["Pid"]) == 0:
85-
print("Container {0} is stopped, running it temporarily ..."
87+
print("Container {0} is stopped"
8688
.format(self.container_name))
8789

88-
self.client_api.containers.get(self.container_name).start()
89-
self.container_name, self.config = \
90-
self._get_container_name_and_config(target)
91-
92-
if int(self.config["State"]["Pid"]) == 0:
93-
sys.stderr.write(
94-
"Cannot keep running container {0}, skip it.\n \
95-
Please start this container before scan it.\n"
96-
.format(self.container_name))
97-
else:
98-
self.stop_at_end = True
99-
100-
# now we are sure that the container is running, get its PID
101-
self.pid = int(self.config["State"]["Pid"])
90+
self._extract_container()
91+
else:
92+
print("Container {0} is running, using its existing mount..."
93+
.format(self.container_name))
94+
self.mountpoint = "/proc/{0}/root".format(self.config["State"]["Pid"])
10295

10396
if self._check_container_mountpoint():
104-
self.mountpoint = "/proc/{0}/root".format(self.pid)
10597
print("Docker container {0} ready to be scanned."
10698
.format(self.container_name))
10799
else:
@@ -113,14 +105,27 @@ def __init__(self, target, is_image=False, oscap_binary='oscap'):
113105

114106
def _end(self):
115107
if self.is_image:
116-
# stop and remove the temporary container
117-
self.client.stop(self.container_name)
108+
# remove the temporary container
118109
self.client.remove_container(self.container_name)
119110
print("Temporary container {0} cleaned".format(self.container_name))
120-
else:
121-
if self.stop_at_end:
122-
# just stop the container if the tool have started it.
123-
self.client.stop(self.container_name)
111+
if self.extracted_container:
112+
print("Cleaning temporary extracted container...")
113+
shutil.rmtree(self.mountpoint)
114+
115+
def _extract_container(self):
116+
'''
117+
Extracts the container and sets mountpoint to the extracted directory
118+
'''
119+
with tempfile.TemporaryFile() as tar:
120+
for chunk in self.client.export(self.container_name):
121+
tar.write(chunk)
122+
tar.seek(0)
123+
self.mountpoint = tempfile.mkdtemp()
124+
self.extracted_container = True
125+
with tarfile.open(fileobj=tar) as tf:
126+
tf.extractall(path=self.mountpoint)
127+
Path(os.path.join(self.mountpoint, '.dockerenv')).touch()
128+
124129

125130
def _get_image_name_and_config(self, target):
126131
'''
@@ -159,7 +164,7 @@ def _check_container_mountpoint(self):
159164
'''
160165
Ensure that the container fs is well mounted and return its path
161166
'''
162-
return os.access("/proc/{0}/root".format(self.pid), os.R_OK)
167+
return os.access(self.mountpoint, os.R_OK)
163168

164169
def scan_cve(self, scan_args):
165170
'''
@@ -210,7 +215,7 @@ def scan(self, scan_args):
210215
Wrapper function forwarding oscap args for an offline scan
211216
'''
212217
scan_result = oscap_chroot(
213-
"/proc/{0}/root".format(self.pid),
218+
self.mountpoint,
214219
self.oscap_binary, scan_args,
215220
self.image_name or self.container_name,
216221
self.config["Config"].get("Env", []) or [] # because Env can exists but be None

0 commit comments

Comments
 (0)