Skip to content

Commit 64e0f08

Browse files
committed
added dockerfile and base image processing
1 parent b4b2d27 commit 64e0f08

File tree

1 file changed

+355
-0
lines changed

1 file changed

+355
-0
lines changed

examples/scan_docker_image_lite.py

Lines changed: 355 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,355 @@
1+
'''
2+
Created on March 25, 2021
3+
4+
@author: kumykov
5+
6+
Alternative version if Docker image layer by layer scan.
7+
8+
This program will download docker image and scan it into Blackduck server layer by layer
9+
Each layer will be scanned as a separate scan with a signature scan.
10+
11+
Layers in the container images could be grouped into groups of contiguous layers.
12+
13+
I.e.
14+
layers 1-5 - Group 1, layers 6-8 - Group 2, etc.
15+
16+
Each group will be scanned as a version within a project
17+
18+
Project naming will follow docker container image specification
19+
20+
repository/image-name:version
21+
22+
Will create project named "repository/image-name" and will have "version" as a version prefix
23+
24+
Project versions corresponding to groups will be named
25+
26+
version_group_name
27+
28+
Scans will be named as
29+
30+
repository/image-name_version_layer_1
31+
repository/image-name_version_layer_2
32+
.........
33+
34+
Layers are numbered in chronological order
35+
36+
Usage:
37+
38+
scan_docker_image_slim.py [-h] imagespec [--grouping=group_end:group_name,group_end:group_name]
39+
40+
positional arguments:
41+
imagespec Container image tag, e.g. repository/imagename:version
42+
43+
optional arguments:
44+
-h, --help show this help message and exit
45+
46+
--grouping=group_end:group_name,group_end:group_name specify layer grouping
47+
48+
'''
49+
50+
from blackduck.HubRestApi import HubInstance
51+
from pprint import pprint
52+
from sys import argv
53+
import json
54+
import os
55+
import requests
56+
import shutil
57+
import subprocess
58+
import sys
59+
from argparse import ArgumentParser
60+
import argparse
61+
62+
#hub = HubInstance()
63+
64+
'''
65+
quick and dirty wrapper to process some docker functionality
66+
'''
67+
class DockerWrapper():
68+
69+
def __init__(self, workdir, scratch = True):
70+
self.workdir = workdir
71+
self.imagedir = self.workdir + "/container"
72+
self.imagefile = self.workdir + "/image.tar"
73+
if scratch:
74+
self.initdir()
75+
self.docker_path = self.locate_docker()
76+
77+
def initdir(self):
78+
if os.path.exists(self.workdir):
79+
if os.path.isdir(self.workdir):
80+
shutil.rmtree(self.workdir)
81+
else:
82+
os.remove(self.workdir)
83+
os.makedirs(self.workdir, 0o755, True)
84+
os.makedirs(self.workdir + "/container", 0o755, True)
85+
86+
87+
def locate_docker(self):
88+
os.environ['PATH'] += os.pathsep + '/usr/local/bin'
89+
args = []
90+
args.append('/usr/bin/which')
91+
args.append('docker')
92+
proc = subprocess.Popen(['which','docker'], stdout=subprocess.PIPE)
93+
out, err = proc.communicate()
94+
lines = out.decode().split('\n')
95+
print(lines)
96+
if 'docker' in lines[0]:
97+
return lines[0]
98+
else:
99+
raise Exception('Can not find docker executable in PATH.')
100+
101+
def pull_container_image(self, image_name):
102+
args = []
103+
args.append(self.docker_path)
104+
args.append('pull')
105+
args.append(image_name)
106+
return subprocess.run(args)
107+
108+
def save_container_image(self, image_name):
109+
args = []
110+
args.append(self.docker_path)
111+
args.append('save')
112+
args.append('-o')
113+
args.append(self.imagefile)
114+
args.append(image_name)
115+
return subprocess.run(args)
116+
117+
def unravel_container(self):
118+
args = []
119+
args.append('tar')
120+
args.append('xvf')
121+
args.append(self.imagefile)
122+
args.append('-C')
123+
args.append(self.imagedir)
124+
return subprocess.run(args)
125+
126+
def read_manifest(self):
127+
filename = self.imagedir + "/manifest.json"
128+
with open(filename) as fp:
129+
data = json.load(fp)
130+
return data
131+
132+
def read_config(self):
133+
manifest = self.read_manifest()
134+
configFile = self.imagedir + "/" + manifest[0]['Config']
135+
with open(configFile) as fp:
136+
data = json.load(fp)
137+
return data
138+
139+
class Detector():
140+
def __init__(self, hub):
141+
# self.detecturl = 'https://blackducksoftware.github.io/hub-detect/hub-detect.sh'
142+
self.detecturl = 'https://detect.synopsys.com/detect.sh'
143+
self.baseurl = hub.config['baseurl']
144+
self.filename = '/tmp/hub-detect.sh'
145+
self.token=hub.config['api_token']
146+
self.baseurl=hub.config['baseurl']
147+
self.download_detect()
148+
149+
def download_detect(self):
150+
with open(self.filename, "wb") as file:
151+
response = requests.get(self.detecturl)
152+
file.write(response.content)
153+
154+
def detect_run(self, options=['--help']):
155+
cmd = ['bash']
156+
cmd.append(self.filename)
157+
cmd.append('--blackduck.url=%s' % self.baseurl)
158+
cmd.append('--blackduck.api.token=' + self.token)
159+
cmd.append('--blackduck.trust.cert=true')
160+
cmd.extend(options)
161+
subprocess.run(cmd)
162+
163+
class ContainerImageScanner():
164+
165+
def __init__(self, hub, container_image_name, workdir='/tmp/workdir', grouping=None, base_image=None, dockerfile=None):
166+
self.hub = hub
167+
self.hub_detect = Detector(hub)
168+
self.docker = DockerWrapper(workdir)
169+
self.container_image_name = container_image_name
170+
cindex = container_image_name.rfind(':')
171+
if cindex == -1:
172+
self.image_name = container_image_name
173+
self.image_version = 'latest'
174+
else:
175+
self.image_name = container_image_name[:cindex]
176+
self.image_version = container_image_name[cindex+1:]
177+
self.grouping = grouping
178+
self.base_image = base_image
179+
self.dockerfile = dockerfile
180+
self.base_layers = None
181+
print ("<--{}-->".format(self.grouping))
182+
183+
def prepare_container_image(self):
184+
self.docker.initdir()
185+
self.docker.pull_container_image(self.container_image_name)
186+
self.docker.save_container_image(self.container_image_name)
187+
self.docker.unravel_container()
188+
189+
def process_container_image_by_user_defined_groups(self):
190+
self.manifest = self.docker.read_manifest()
191+
print(self.manifest)
192+
self.config = self.docker.read_config()
193+
print (json.dumps(self.config, indent=4))
194+
195+
if self.grouping:
196+
self.groups = dict(x.split(":") for x in self.grouping.split(","))
197+
198+
self.layers = []
199+
num = 1
200+
offset = 0
201+
for i in self.manifest[0]['Layers']:
202+
layer = {}
203+
if self.grouping:
204+
intlist = [int(x) for x in sorted(self.groups.keys())]
205+
intlist.sort()
206+
key_number = len(self.groups) - len([i for i in intlist if i >= num])
207+
if key_number >= len(self.groups):
208+
layer['group_name'] = "undefined"
209+
else:
210+
layer['group_name'] = self.groups.get(str(intlist[key_number]))
211+
layer['project_version'] = "{}_{}".format(self.image_version,layer['group_name'])
212+
layer['name'] = "{}_{}_{}_layer_{}".format(self.image_name,self.image_version,layer['group_name'],str(num))
213+
else:
214+
layer['project_version'] = self.image_version
215+
layer['name'] = self.image_name + "_" + self.image_version + "_layer_" + str(num)
216+
layer['project_name'] = self.image_name
217+
layer['path'] = i
218+
while self.config['history'][num + offset -1].get('empty_layer', False):
219+
offset = offset + 1
220+
layer['command'] = self.config['history'][num + offset - 1]
221+
layer['shaid'] = self.config['rootfs']['diff_ids'][num - 1]
222+
self.layers.append(layer)
223+
num = num + 1
224+
print (json.dumps(self.layers, indent=4))
225+
226+
def process_container_image_by_base_image_info(self):
227+
self.manifest = self.docker.read_manifest()
228+
print(self.manifest)
229+
self.config = self.docker.read_config()
230+
print (json.dumps(self.config, indent=4))
231+
232+
self.layers = []
233+
num = 1
234+
offset = 0
235+
for i in self.manifest[0]['Layers']:
236+
layer = {}
237+
layer['project_name'] = self.image_name
238+
layer['path'] = i
239+
while self.config['history'][num + offset -1].get('empty_layer', False):
240+
offset = offset + 1
241+
layer['command'] = self.config['history'][num + offset - 1]
242+
layer['shaid'] = self.config['rootfs']['diff_ids'][num - 1]
243+
244+
if self.base_layers:
245+
pass
246+
if layer['shaid'] in self.base_layers:
247+
layer['project_version'] = "{}_{}".format(self.image_version,'base')
248+
layer['name'] = "{}_{}_{}_layer_{}".format(self.image_name,self.image_version,'base',str(num))
249+
else:
250+
layer['project_version'] = "{}_{}".format(self.image_version,'addon')
251+
layer['name'] = "{}_{}_{}_layer_{}".format(self.image_name,self.image_version,'addon',str(num))
252+
else:
253+
layer['project_version'] = self.image_version
254+
layer['name'] = self.image_name + "_" + self.image_version + "_layer_" + str(num)
255+
self.layers.append(layer)
256+
num = num + 1
257+
print (json.dumps(self.layers, indent=4))
258+
259+
def process_container_image(self):
260+
if self.grouping:
261+
self.process_container_image_by_user_defined_groups()
262+
else:
263+
self.process_container_image_by_base_image_info()
264+
265+
def submit_layer_scans(self):
266+
for layer in self.layers:
267+
options = []
268+
options.append('--detect.project.name={}'.format(layer['project_name']))
269+
options.append('--detect.project.version.name="{}"'.format(layer['project_version']))
270+
# options.append('--detect.blackduck.signature.scanner.disabled=false')
271+
options.append('--detect.code.location.name={}_{}_code_{}'.format(layer['name'],self.image_version,layer['path']))
272+
options.append('--detect.source.path={}/{}'.format(self.docker.imagedir, layer['path'].split('/')[0]))
273+
self.hub_detect.detect_run(options)
274+
275+
def get_base_layers(self):
276+
if (not self.dockerfile)and (not self.base_image):
277+
raise Exception ("No dockerfile or base image specified")
278+
imagelist = []
279+
280+
if self.dockerfile:
281+
from pathlib import Path
282+
dfile = Path(self.dockerfile)
283+
if not dfile.exists():
284+
raise Exception ("Dockerfile {} does not exist",format(self.dockerfile))
285+
if not dfile.is_file():
286+
raise Exception ("{} is not a file".format(self.dockerfile))
287+
with open(dfile) as f:
288+
for line in f:
289+
if 'FROM' in line.upper():
290+
a = line.split()
291+
if a[0].upper() == 'FROM':
292+
imagelist.append(a[1])
293+
if self.base_image:
294+
imagelist.append(self.base_image)
295+
296+
print (imagelist)
297+
base_layers = []
298+
for image in imagelist:
299+
self.docker.initdir()
300+
self.docker.pull_container_image(image)
301+
self.docker.save_container_image(image)
302+
self.docker.unravel_container()
303+
manifest = self.docker.read_manifest()
304+
print(manifest)
305+
config = self.docker.read_config()
306+
print(config)
307+
base_layers.extend(config['rootfs']['diff_ids'])
308+
return base_layers
309+
310+
311+
def scan_container_image(imagespec, grouping=None, base_image=None, dockerfile=None):
312+
313+
hub = HubInstance()
314+
scanner = ContainerImageScanner(hub, imagespec, grouping=grouping, base_image=base_image, dockerfile=dockerfile)
315+
if not grouping:
316+
scanner.base_layers = scanner.get_base_layers()
317+
scanner.prepare_container_image()
318+
scanner.process_container_image()
319+
scanner.submit_layer_scans()
320+
321+
def main(argv=None):
322+
323+
if argv is None:
324+
argv = sys.argv
325+
else:
326+
argv.extend(sys.argv)
327+
328+
parser = ArgumentParser()
329+
parser.add_argument('imagespec', help="Container image tag, e.g. repository/imagename:version")
330+
parser.add_argument('--grouping',default=None, type=str, help="Group layers into user defined provect versions (can't be used with --base-image)")
331+
parser.add_argument('--base-image',default=None, type=str, help="Use base image spec to determine base image/layers (can't be used with --grouping or --dockerfile)")
332+
parser.add_argument('--dockerfile',default=None, type=str, help="Use Dockerfile to determine base image/layers (can't be used with --grouping or ---base-image)")
333+
334+
args = parser.parse_args()
335+
336+
print (args);
337+
338+
if not args.imagespec:
339+
parser.print_help(sys.stdout)
340+
sys.exit(1)
341+
342+
if args.dockerfile and args.base_image:
343+
parser.print_help(sys.stdout)
344+
sys.exit(1)
345+
346+
if args.grouping and (args.dockerfile and args.base_image):
347+
parser.print_help(sys.stdout)
348+
sys.exit(1)
349+
350+
scan_container_image(args.imagespec, args.grouping, args.base_image, args.dockerfile)
351+
352+
353+
if __name__ == "__main__":
354+
sys.exit(main())
355+

0 commit comments

Comments
 (0)