Skip to content

Commit 1357582

Browse files
authored
Merge pull request #1415 from DominiqueDevinci/docker
oscap-docker available without atomic
2 parents 5015ade + eeda020 commit 1357582

File tree

5 files changed

+405
-63
lines changed

5 files changed

+405
-63
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
.libs/
66
.*.swp
77
tags
8+
*.pyc
89

910
CMakeLists.txt.user
1011
build/

utils/oscap-docker.in

Lines changed: 48 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!@OSCAP_DOCKER_PYTHON@
22

33
# Copyright (C) 2015 Brent Baude <[email protected]>
4+
# Copyright (C) 2019 Dominique Blaze <[email protected]>
45
#
56
# This library is free software; you can redistribute it and/or
67
# modify it under the terms of the GNU Lesser General Public
@@ -20,8 +21,11 @@
2021
''' oscap docker command '''
2122

2223
import argparse
23-
from oscap_docker_python.oscap_docker_util import OscapScan
24+
from oscap_docker_python.oscap_docker_util import OscapAtomicScan, \
25+
OscapDockerScan, isAtomicLoaded
26+
2427
import docker
28+
import traceback
2529
import sys
2630
from requests import exceptions
2731

@@ -41,40 +45,44 @@ if __name__ == '__main__':
4145
parser = argparse.ArgumentParser(description='oscap docker',
4246
epilog='See `man oscap` to learn \
4347
more about OSCAP-ARGUMENTS')
44-
parser.add_argument('--oscap', dest='oscap_binary', default='', help='Set the oscap binary to use')
48+
parser.add_argument('--oscap', dest='oscap_binary', default='',
49+
help='Set the oscap binary to use')
50+
51+
parser.add_argument('--disable-atomic', dest='noatomic', action='store_true',
52+
help="Force to use native docker API instead of atomic")
4553
subparser = parser.add_subparsers(help="commands")
4654

4755
# Scan CVEs in image
4856
image_cve = subparser.add_parser('image-cve', help='Scan a docker image \
4957
for known vulnerabilities.')
50-
image_cve.set_defaults(func=OscapScan.scan_cve)
58+
image_cve.set_defaults(action="scan_cve", is_image=True)
5159
image_cve.add_argument('scan_target', help='Container or image to scan')
5260

5361
# Scan an Image
5462
image = subparser.add_parser('image', help='Scan a docker image')
5563
image.add_argument('scan_target',
5664
help='Container or image to scan')
5765

58-
image.set_defaults(func=OscapScan.scan)
66+
image.set_defaults(action="scan", is_image=True)
5967
# Scan a container
6068
container = subparser.add_parser('container', help='Scan a running docker\
6169
container of given name.')
6270
container.add_argument('scan_target',
6371
help='Container or image to scan')
64-
container.set_defaults(func=OscapScan.scan)
72+
container.set_defaults(action="scan", is_image=False)
6573

6674
# Scan CVEs in container
6775
container_cve = subparser.add_parser('container-cve', help='Scan a \
6876
running container for known \
6977
vulnerabilities.')
7078

71-
container_cve.set_defaults(func=OscapScan.scan_cve)
79+
container_cve.set_defaults(action="scan_cve", is_image=False)
7280
container_cve.add_argument('scan_target',
7381
help='Container or image to scan')
7482

7583
args, leftover_args = parser.parse_known_args()
7684

77-
if "func" not in args:
85+
if "action" not in args:
7886
parser.print_help()
7987
sys.exit(2)
8088

@@ -86,10 +94,40 @@ if __name__ == '__main__':
8694
sys.exit(1)
8795

8896
try:
89-
OS = OscapScan(oscap_binary=args.oscap_binary)
90-
rc = args.func(OS, args.scan_target, leftover_args)
97+
if isAtomicLoaded and not args.noatomic:
98+
print("Using Atomic API")
99+
OS = OscapAtomicScan(oscap_binary=args.oscap_binary)
100+
if args.action == "scan":
101+
rc = OscapAtomicScan.scan(OS, args.scan_target, leftover_args)
102+
elif args.action == "scan_cve":
103+
rc = OscapAtomicScan.scan_cve(OS, args.scan_target, leftover_args)
104+
else:
105+
parser.print_help()
106+
sys.exit(2)
107+
108+
else: # without atomic
109+
if args.noatomic:
110+
print("Using native Docker API")
111+
112+
ODS = OscapDockerScan(args.scan_target, args.is_image, args.oscap_binary)
113+
if args.action == "scan":
114+
rc = OscapDockerScan.scan(ODS, leftover_args)
115+
elif args.action == "scan_cve":
116+
rc = OscapDockerScan.scan_cve(ODS, leftover_args)
117+
else:
118+
parser.print_help()
119+
sys.exit(2)
120+
121+
except (ValueError, RuntimeError) as e:
122+
raise e
123+
sys.exit(255)
124+
except(FileNotFoundError) as e:
125+
sys.stderr.write("Target {0} not found.\n".format(target))
126+
sys.exit(255)
91127
except Exception as exc:
128+
traceback.print_exc(file=sys.stdout)
129+
sys.stderr.write("!!! WARNING !!! This software has crashed, so you should "
130+
"check that no temporary container is still running\n")
92131
sys.exit(255)
93-
raise exc
94132

95133
sys.exit(rc)
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Copyright (C) 2015 Brent Baude <[email protected]>
2+
# Copyright (C) 2019 Dominique Blaze <[email protected]>
3+
#
4+
# This library is free software; you can redistribute it and/or
5+
# modify it under the terms of the GNU Lesser General Public
6+
# License as published by the Free Software Foundation; either
7+
# version 2 of the License, or (at your option) any later version.
8+
#
9+
# This library is distributed in the hope that it will be useful,
10+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12+
# Lesser General Public License for more details.
13+
#
14+
# You should have received a copy of the GNU Lesser General Public
15+
# License along with this library; if not, write to the
16+
# Free Software Foundation, Inc., 59 Temple Place - Suite 330,
17+
# Boston, MA 02111-1307, USA.
18+
19+
import subprocess
20+
import platform
21+
import os
22+
import collections
23+
24+
25+
class OscapError(Exception):
26+
''' oscap Error'''
27+
pass
28+
29+
OscapResult = collections.namedtuple("OscapResult", ("returncode", "stdout", "stderr"))
30+
31+
32+
def oscap_chroot(chroot_path, oscap_binary, oscap_args, target_name, local_env=[]):
33+
'''
34+
Wrapper running oscap_chroot on an OscapDockerScan OscapAtomicScan object
35+
'''
36+
os.environ["OSCAP_PROBE_ARCHITECTURE"] = platform.processor()
37+
os.environ["OSCAP_PROBE_ROOT"] = os.path.join(chroot_path)
38+
os.environ["OSCAP_PROBE_OS_NAME"] = platform.system()
39+
os.environ["OSCAP_PROBE_OS_VERSION"] = platform.release()
40+
41+
os.environ["OSCAP_EVALUATION_TARGET"] = target_name
42+
43+
for var in local_env:
44+
vname, val = var.split("=", 1)
45+
os.environ["OSCAP_OFFLINE_" + vname] = val
46+
47+
cmd = [oscap_binary] + [x for x in oscap_args]
48+
49+
oscap_process = subprocess.Popen(cmd, stdout=subprocess.PIPE,
50+
stderr=subprocess.PIPE)
51+
oscap_stdout, oscap_stderr = oscap_process.communicate()
52+
return OscapResult(oscap_process.returncode,
53+
oscap_stdout.decode("utf-8"),
54+
oscap_stderr.decode("utf-8"))
55+
56+
# TODO replace by _get_cpe (in order to indentify any containerized system)
57+
58+
59+
def get_dist(mountpoint, oscap_binary, local_env):
60+
CPE_RHEL = 'oval:org.open-scap.cpe.rhel:def:'
61+
DISTS = ["8", "7", "6", "5"]
62+
63+
'''
64+
Test the chroot and determine what RHEL dist it is; returns
65+
an integer representing the dist
66+
'''
67+
68+
cpe_dict = '/usr/share/openscap/cpe/openscap-cpe-oval.xml'
69+
if not os.path.exists(cpe_dict):
70+
# sometime it's installed into /usr/local/share instead of /usr/local
71+
cpe_dict = '/usr/local/share/openscap/cpe/openscap-cpe-oval.xml'
72+
if not os.path.exists(cpe_dict):
73+
raise OscapError()
74+
75+
for dist in DISTS:
76+
result = oscap_chroot(
77+
mountpoint, oscap_binary,
78+
("oval", "eval", "--id", CPE_RHEL + dist, cpe_dict,
79+
mountpoint, "2>&1", ">", "/dev/null"),
80+
'*',
81+
local_env
82+
)
83+
84+
if "{0}{1}: true".format(CPE_RHEL, dist) in result.stdout:
85+
print("This system seems based on RHEL{0}.".format(dist))
86+
return dist

utils/oscap_docker_python/oscap_docker_util.py

Lines changed: 43 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copyright (C) 2015 Brent Baude <[email protected]>
2+
# Copyright (C) 2019 Dominique Blaze <[email protected]>
23
#
34
# This library is free software; you can redistribute it and/or
45
# modify it under the terms of the GNU Lesser General Public
@@ -28,56 +29,69 @@
2829
import sys
2930
import docker
3031
import collections
32+
from oscap_docker_python.oscap_docker_util_noatomic import OscapDockerScan
33+
from oscap_docker_python.oscap_docker_common import oscap_chroot, get_dist, \
34+
OscapResult, OscapError
35+
36+
atomic_loaded = False
37+
38+
39+
class AtomicError(Exception):
40+
"""Exception raised when an error happens in atomic import
41+
"""
42+
def __init__(self, message):
43+
self.message = message
44+
3145

3246
try:
3347
from Atomic.mount import DockerMount
3448
from Atomic.mount import MountError
3549
import inspect
3650

3751
if "mnt_mkdir" not in inspect.getargspec(DockerMount.__init__).args:
38-
sys.stderr.write(
52+
raise AtomicError(
3953
"\"Atomic.mount.DockerMount\" has been successfully imported but "
4054
"it doesn't support the mnt_mkdir argument. Please upgrade your "
4155
"Atomic installation to 1.4 or higher.\n"
4256
)
43-
sys.exit(1)
4457

4558
# we only care about method names
4659
member_methods = [
4760
x[0] for x in
4861
inspect.getmembers(
49-
DockerMount, predicate=lambda member: inspect.isfunction(member) or inspect.ismethod(member)
62+
DockerMount, predicate=lambda member:
63+
inspect.isfunction(member) or inspect.ismethod(member)
5064
)
5165
]
5266

5367
if "_clean_temp_container_by_path" not in member_methods:
54-
sys.stderr.write(
68+
raise AtomicError(
5569
"\"Atomic.mount.DockerMount\" has been successfully imported but "
5670
"it doesn't have the _clean_temp_container_by_path method. Please "
5771
"upgrade your Atomic installation to 1.4 or higher.\n"
5872
)
59-
sys.exit(1)
73+
74+
# if all imports are ok we can use atomic
75+
atomic_loaded = True
6076

6177
except ImportError:
6278
sys.stderr.write(
6379
"Failed to import \"Atomic.mount.DockerMount\". It seems Atomic has "
6480
"not been installed.\n"
6581
)
66-
sys.exit(1)
6782

83+
except AtomicError as err:
84+
sys.stderr.write(err.message)
6885

69-
class OscapError(Exception):
70-
''' oscap Error'''
71-
pass
7286

73-
74-
OscapResult = collections.namedtuple("OscapResult", ("returncode", "stdout", "stderr"))
87+
def isAtomicLoaded():
88+
return atomic_loaded
7589

7690

7791
class OscapHelpers(object):
7892
''' oscap class full of helpers for scanning '''
7993
CPE = 'oval:org.open-scap.cpe.rhel:def:'
80-
DISTS = ["7", "6", "5"]
94+
DISTS = ["8", "7", "6", "5"]
8195

8296
def __init__(self, cve_input_dir, oscap_binary):
8397
self.cve_input_dir = cve_input_dir
@@ -100,21 +114,6 @@ def _rm_tmp_dir(tmp_dir):
100114
'''
101115
shutil.rmtree(tmp_dir)
102116

103-
def _get_dist(self, chroot, target):
104-
'''
105-
Test the chroot and determine what RHEL dist it is; returns
106-
an integer representing the dist
107-
'''
108-
cpe_dict = '/usr/share/openscap/cpe/openscap-cpe-oval.xml'
109-
if not os.path.exists(cpe_dict):
110-
raise OscapError()
111-
for dist in self.DISTS:
112-
result = self.oscap_chroot(chroot, target, 'oval', 'eval',
113-
'--id', self.CPE + dist, cpe_dict,
114-
'2>&1', '>', '/dev/null')
115-
if "{0}{1}: true".format(self.CPE, dist) in result.stdout:
116-
return dist
117-
118117
def _get_target_name_and_config(self, target):
119118
'''
120119
Determines if target is image or container. For images returns full
@@ -143,40 +142,30 @@ def _get_target_name_and_config(self, target):
143142
except docker.errors.NotFound:
144143
return "unknown", {}
145144

146-
def oscap_chroot(self, chroot_path, target, *oscap_args):
147-
'''
148-
Wrapper function for executing oscap in a subprocess
149-
'''
150-
os.environ["OSCAP_PROBE_ARCHITECTURE"] = platform.processor()
151-
os.environ["OSCAP_PROBE_ROOT"] = os.path.join(chroot_path)
152-
os.environ["OSCAP_PROBE_OS_NAME"] = platform.system()
153-
os.environ["OSCAP_PROBE_OS_VERSION"] = platform.release()
154-
name, conf = self._get_target_name_and_config(target)
155-
os.environ["OSCAP_EVALUATION_TARGET"] = name
156-
for var in config.get("Env", []):
157-
vname, val = var.split("=", 1)
158-
os.environ["OSCAP_OFFLINE_"+vname] = val
159-
cmd = [self.oscap_binary] + [x for x in oscap_args]
160-
oscap_process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
161-
oscap_stdout, oscap_stderr = oscap_process.communicate()
162-
return OscapResult(oscap_process.returncode,
163-
oscap_stdout.decode("utf-8"), oscap_stderr.decode("utf-8"))
164-
165145
def _scan_cve(self, chroot, target, dist, scan_args):
166146
'''
167147
Scan a chroot for cves
168148
'''
169149
cve_input = getInputCVE.dist_cve_name.format(dist)
170-
tmp_tuple = ('oval', 'eval') + tuple(scan_args) + \
171-
(os.path.join(self.cve_input_dir, cve_input),)
172-
return self.oscap_chroot(chroot, target, *tmp_tuple)
150+
151+
args = ("oval", "eval")
152+
for a in scan_args:
153+
args += (a,)
154+
args += (os.path.join(self.cve_input_dir, cve_input),)
155+
156+
name, conf = self._get_target_name_and_config(target)
157+
158+
return oscap_chroot(chroot, self.oscap_binary, args, name,
159+
conf.get("Env", []) or [])
173160

174161
def _scan(self, chroot, target, scan_args):
175162
'''
176163
Scan a container or image
177164
'''
178-
tmp_tuple = tuple(scan_args)
179-
return self.oscap_chroot(chroot, target, *tmp_tuple)
165+
166+
name, conf = self._get_target_name_and_config(target)
167+
return oscap_chroot(chroot, self.oscap_binary, scan_args, name,
168+
conf.get("Env", []) or [])
180169

181170
def resolve_image(self, image):
182171
'''
@@ -209,7 +198,7 @@ def mount_image_filesystem():
209198
_tmp_mnt_dir = DM.mount(image)
210199

211200

212-
class OscapScan(object):
201+
class OscapAtomicScan(object):
213202
def __init__(self, tmp_dir=tempfile.gettempdir(), mnt_dir=None,
214203
hours_old=2, oscap_binary=''):
215204
self.tmp_dir = tmp_dir
@@ -264,7 +253,8 @@ def scan_cve(self, image, scan_args):
264253
chroot = self._find_chroot_path(_tmp_mnt_dir)
265254

266255
# Figure out which RHEL dist is in the chroot
267-
dist = self.helper._get_dist(chroot, image)
256+
name, conf = self.helper._get_target_name_and_config(image)
257+
dist = get_dist(chroot, self.helper.oscap_binary, conf.get("Env", []) or [])
268258

269259
if dist is None:
270260
sys.stderr.write("{0} is not based on RHEL\n".format(image))

0 commit comments

Comments
 (0)