|
|
|
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
|
|
import json
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
|
|
|
|
def run_command(command: list[str]) -> tuple:
|
|
|
|
proc = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
|
|
|
return proc.communicate()
|
|
|
|
|
|
|
|
|
|
|
|
def run_command_in_image(image: str, commands: list[str]) -> tuple:
|
|
|
|
(id, _) = run_command(["docker", "run", "-d", image, "sh", "-c", "sleep 30"])
|
|
|
|
cid = id.decode().strip()
|
|
|
|
result = run_command(["docker", "exec", cid] + commands)
|
|
|
|
(_,_) = run_command(["docker", "kill", cid])
|
|
|
|
return result
|
|
|
|
|
|
|
|
def cleanup_image(image: str) -> None:
|
|
|
|
check_command = ['docker', 'ps', '--filter', f'ancestor={image}', '--quiet']
|
|
|
|
(stdout, _) = run_command(check_command)
|
|
|
|
if stdout == b'':
|
|
|
|
(_,_) = run_command(["docker", "rmi", image, '--force'])
|
|
|
|
|
|
|
|
def cleanup_all() -> None:
|
|
|
|
(_,_) = run_command(["docker", "system", "prune", "-af", "--volumes"])
|
|
|
|
|
|
|
|
def get_os_hash(image:str) -> dict:
|
|
|
|
(stdout, _) = run_command_in_image(image, ["cat", "/etc/os-release"])
|
|
|
|
hash = dict()
|
|
|
|
for line in stdout.decode().split('\n'):
|
|
|
|
if len(line) != 0:
|
|
|
|
(name, var) = line.partition("=")[::2]
|
|
|
|
hash[name.strip().strip('"')] = var.strip().strip('"')
|
|
|
|
return hash
|
|
|
|
|
|
|
|
def get_os_for_image(image: str, hash: dict) -> str:
|
|
|
|
if "PRETTY_NAME" in hash:
|
|
|
|
if hash["PRETTY_NAME"] == "Distroless":
|
|
|
|
return "distroless"
|
|
|
|
if not "ID" in hash:
|
|
|
|
return "unknown"
|
|
|
|
return hash["ID"]
|
|
|
|
|
|
|
|
|
|
|
|
def parse_packages(provider: str, input: str) -> list[dict]:
|
|
|
|
output = list()
|
|
|
|
object_distroless = dict()
|
|
|
|
for line in input.split('\n'):
|
|
|
|
if len(line) > 0:
|
|
|
|
object = dict()
|
|
|
|
object["provider"] = provider
|
|
|
|
match provider:
|
|
|
|
case "alpine":
|
|
|
|
first_pass = line.split(' ')[0]
|
|
|
|
second_pass = first_pass.split('-')
|
|
|
|
package = second_pass[0]
|
|
|
|
version = "-".join(second_pass[1:])
|
|
|
|
case "centos":
|
|
|
|
first_pass = line.split(' ')[0]
|
|
|
|
second_pass = first_pass.split('-')
|
|
|
|
package = second_pass[0]
|
|
|
|
version = "-".join(second_pass[1:])
|
|
|
|
case "debian":
|
|
|
|
package, version = line.split('\t')
|
|
|
|
case "distroless":
|
|
|
|
key, value = line.split(':')
|
|
|
|
if key.strip() == "Package":
|
|
|
|
del object_distroless
|
|
|
|
object_distroless = dict()
|
|
|
|
object_distroless["provider"] = "debian"
|
|
|
|
object_distroless["package"] = value.strip()
|
|
|
|
else:
|
|
|
|
object_distroless["version"] = value.strip()
|
|
|
|
output.append(object_distroless)
|
|
|
|
case "fedora":
|
|
|
|
first_pass = line.split(' ')[0]
|
|
|
|
second_pass = first_pass.split('-')
|
|
|
|
package = second_pass[0]
|
|
|
|
version = "-".join(second_pass[1:])
|
|
|
|
case "npm":
|
|
|
|
first_pass = line.split(':')
|
|
|
|
package = first_pass[0]
|
|
|
|
version = ':'.join(first_pass[:1])
|
|
|
|
case "pip":
|
|
|
|
package, version = line.split('==')
|
|
|
|
case "ubuntu":
|
|
|
|
package, version = line.split('\t')
|
|
|
|
if provider != "distroless":
|
|
|
|
object["package"] = package
|
|
|
|
object["version"] = version
|
|
|
|
output.append(object)
|
|
|
|
return output
|
|
|
|
|
|
|
|
def get_inspect_data(image: str) -> list[dict]:
|
|
|
|
(output, _) = run_command(["docker", "image", "inspect", image])
|
|
|
|
return json.loads(output.decode())
|
|
|
|
|
|
|
|
def get_packages(image: str, hash: dict) -> list[dict]:
|
|
|
|
os = get_os_for_image(image, hash)
|
|
|
|
command = list()
|
|
|
|
result = list()
|
|
|
|
match os:
|
|
|
|
case "alpine":
|
|
|
|
command = ["apk", "list", "-q"]
|
|
|
|
case "centos":
|
|
|
|
command = ["rpm", "-qa"]
|
|
|
|
case "debian":
|
|
|
|
command = ["dpkg-query", "-W"]
|
|
|
|
case "distroless":
|
|
|
|
command = ["sh", "-c", "cat /var/lib/dpkg/status.d/* | egrep 'Package|Version'"]
|
|
|
|
bboxcommand = ["sh", "-c", "busybox | grep 'BusyBox v' | awk '{print $1,$2}'"]
|
|
|
|
oneline,_ = run_command_in_image(image, bboxcommand)
|
|
|
|
first_pass = oneline.decode().split()
|
|
|
|
result = [ {"provider": os, "package": first_pass[0], "version": first_pass[1]}]
|
|
|
|
case "fedora":
|
|
|
|
command = ["rpm", "-qa"]
|
|
|
|
case "ubuntu":
|
|
|
|
command = ["dpkg-query", "-W"]
|
|
|
|
if not command:
|
|
|
|
return [{"provider": os, "package": None, "version": None}]
|
|
|
|
|
|
|
|
(output, _) = run_command_in_image(image, command)
|
|
|
|
(pip_output, _) = run_command_in_image(image, ["sh", "-c", "python3 -m pip list --format freeze || true"])
|
|
|
|
(npm_output, _) = run_command_in_image(image, ["sh", "-c", "npm ls -p -l || true"])
|
|
|
|
os_result = parse_packages(os, output.decode())
|
|
|
|
result = result + os_result
|
|
|
|
if pip_output:
|
|
|
|
pip_result = parse_packages("pip", pip_output.decode())
|
|
|
|
result += pip_result
|
|
|
|
if npm_output:
|
|
|
|
npm_result = parse_packages("npm", npm_output.decode())
|
|
|
|
result += npm_result
|
|
|
|
return result
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
parser = argparse.ArgumentParser(description='Scan docker images.')
|
|
|
|
parser.add_argument('--images', nargs=argparse.REMAINDER, required=True)
|
|
|
|
args = parser.parse_args()
|
|
|
|
|
|
|
|
result = dict()
|
|
|
|
for image in args.images:
|
|
|
|
os_hash = get_os_hash(image)
|
|
|
|
pkg_list = get_packages(image, os_hash)
|
|
|
|
inspect_data = get_inspect_data(image)
|
|
|
|
if os_hash == {} and pkg_list[0]['package']['version'] == None and inspect_data == []:
|
|
|
|
continue
|
|
|
|
result[image] = { "pkg_list": pkg_list }
|
|
|
|
result[image]["inspect_data"] = inspect_data
|
|
|
|
result[image]["os_hash"] = os_hash
|
|
|
|
cleanup_image(image)
|
|
|
|
cleanup_all()
|
|
|
|
|
|
|
|
if result != {}:
|
|
|
|
print(json.dumps(result))
|