/
helpers.py
185 lines (165 loc) · 6.96 KB
/
helpers.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# -*- coding: utf-8 -*-
#
# Copyright (c) 2017-2019 VMware, Inc. All Rights Reserved.
# SPDX-License-Identifier: BSD-2-Clause
"""
Docker specific functions - used when trying to retrieve packages when
given a Dockerfile
"""
import docker
import logging
import os
import re
from tern.classes.docker_image import DockerImage
from tern.classes.notice import Notice
from tern.analyze.docker import dockerfile
from tern.analyze.docker import container
from tern.utils import constants
from tern.report import errors
from tern.report import formats
from tern.analyze import common
# dockerfile
dockerfile_global = ''
# dockerfile commands
docker_commands = []
# global logger
logger = logging.getLogger(constants.logger_name)
def load_docker_commands(dockerfile_path):
'''Given a dockerfile get a persistent list of docker commands'''
if not os.path.isfile(dockerfile_path):
raise IOError('{} does not exist'.format(dockerfile_path))
global docker_commands
docker_commands = dockerfile.get_directive_list(
dockerfile.get_command_list(dockerfile_path))
global dockerfile_global
dockerfile_global = dockerfile_path
def print_dockerfile_base(base_instructions):
'''For the purpose of tracking the lines in the dockerfile that
produce packages, return a string containing the lines in the dockerfile
that pertain to the base image'''
base_instr = ''
for instr in base_instructions:
base_instr = base_instr + instr[0] + ' ' + instr[1]
return base_instr
def get_dockerfile_base():
'''Get the base image object from the dockerfile base instructions
1. get the instructions around FROM
2. get the base image and tag
3. Make notes based on what the image and tag rules are
4. Return an image object and the base instructions string'''
try:
base_instructions = dockerfile.get_base_instructions(docker_commands)
base_image_tag = dockerfile.get_base_image_tag(base_instructions)
dockerfile_lines = print_dockerfile_base(base_instructions)
# check for scratch
if base_image_tag[0] == 'scratch':
# there is no base image - return no image object
return None
# there should be some image object here
repotag = base_image_tag[0] + dockerfile.tag_separator + \
base_image_tag[1]
from_line = 'FROM ' + repotag
base_image = DockerImage(repotag)
base_image.origins.add_notice_origin(dockerfile_lines)
base_image.name = base_image_tag[0]
# check if there is a tag
if not base_image_tag[1]:
message_string = errors.dockerfile_no_tag.format(
dockerfile_line=from_line)
base_image.origins.add_notice_to_origins(
dockerfile_lines, Notice(message_string, 'warning'))
base_image.tag = 'latest'
else:
base_image.tag = base_image_tag[1]
# check if the tag is 'latest'
if base_image_tag[1] == 'latest':
message_string = errors.dockerfile_using_latest.format(
dockerfile_line=from_line)
base_image.origins.add_notice_to_origins(
dockerfile_lines, Notice(message_string, 'warning'))
return base_image, dockerfile_lines
except ValueError as e:
logger.warning("%s", errors.cannot_parse_base_image.format(
dockerfile=dockerfile_global, error_msg=e))
return None
def get_dockerfile_image_tag():
'''Return the image and tag used to build an image from the dockerfile'''
image_tag_string = constants.image + dockerfile.tag_separator + \
constants.tag
return image_tag_string
def is_build():
'''Attempt to build a given dockerfile
If it does not build return False. Else return True'''
image_tag_string = get_dockerfile_image_tag()
success = False
msg = ''
try:
container.build_container(dockerfile_global, image_tag_string)
except (docker.errors.APIError, docker.errors.BuildError) as error:
success = False
logger.error('Error building image: %s', str(error))
msg = str(error)
else:
logger.debug('Successfully built image')
success = True
return success, msg
def created_to_instruction(created_by):
'''The 'created_by' key in a Docker image config gives the shell
command that was executed unless it is a #(nop) instruction which is
for the other Docker directives. Convert this line into a Dockerfile
instruction'''
instruction = re.sub('/bin/sh -c ', '', created_by).strip()
instruction = re.sub(re.escape('#(nop) '), '', instruction).strip()
first = instruction.split(' ').pop(0)
if first and first not in dockerfile.directives and \
'RUN' not in instruction:
instruction = 'RUN ' + instruction
return instruction
def get_commands_from_history(image_layer):
'''Given the image layer object and the shell, get the list of command
objects that created the layer'''
# set up notice origin for the layer
origin_layer = 'Layer: ' + image_layer.fs_hash[:10]
if image_layer.created_by:
instruction = created_to_instruction(image_layer.created_by)
image_layer.origins.add_notice_to_origins(origin_layer, Notice(
formats.dockerfile_line.format(dockerfile_instruction=instruction),
'info'))
else:
image_layer.origins.add_notice_to_origins(origin_layer, Notice(
formats.no_created_by, 'warning'))
command_line = instruction.split(' ', 1)[1]
# Image layers are created with the directives RUN, ADD and COPY
# For ADD and COPY instructions, there is no information about the
# packages added
if 'ADD' in instruction or 'COPY' in instruction:
image_layer.origins.add_notice_to_origins(origin_layer, Notice(
errors.unknown_content.format(files=command_line), 'warning'))
# return an empty list as we cannot find any commands
return []
# for RUN instructions we can return a list of commands
command_list, msg = common.filter_install_commands(command_line)
if msg:
image_layer.origins.add_notice_to_origins(origin_layer, Notice(
msg, 'warning'))
return command_list
def set_imported_layers(docker_image):
'''Given a Docker image object that was built from a Dockerfile, set the
layers that were imported using the Dockerfile's FROM command or the ones
that came before it'''
dockerfile_lines = dockerfile.get_command_list(dockerfile_global)
index = -1
from_line = ''
for line in dockerfile_lines:
if 'FROM' in line:
from_line = line
break
for layer in docker_image.layers:
instr = created_to_instruction(layer.created_by)
if instr in dockerfile_lines:
index = docker_image.layers.index(layer)
break
if index != -1:
# index was set so all layers before this index has been imported
for i in range(0, index):
docker_image.layers[i].import_str = from_line