Skip to content

Commit

Permalink
Add a test to images/dnsmasq/validate.py to verify dnsmasq dynamic deps
Browse files Browse the repository at this point in the history
  • Loading branch information
tstapler committed Jul 13, 2022
1 parent 3032999 commit d4b1472
Show file tree
Hide file tree
Showing 2 changed files with 157 additions and 0 deletions.
6 changes: 6 additions & 0 deletions images/dnsmasq/validate.sh
@@ -1,10 +1,16 @@
#!/bin/bash
set -e

if [ -z "$IMAGE" ]; then
echo "IMAGE needs to be set to the dnsmasq image"
fi

echo "Checking ${IMAGE} ${ARCH}"

# Check that dnsmasq is correctly compiled and has the right
# dynamic libraries to run.
./validate_dynamic_deps.py --image ${IMAGE} --target-bin /usr/sbin/dnsmasq

# Check that dnsmasq is able to start.
exec docker run --rm -- "${IMAGE}" --version >/dev/null

151 changes: 151 additions & 0 deletions images/dnsmasq/validate_dynamic_deps.py
@@ -0,0 +1,151 @@
# /usr/bin/env python3
# This script requires python 3.8+
import os
import shutil
import subprocess
import pathlib
import sys

IMAGE_DIR = '.test-images'


def replace_img_special_chars(image_ref: str):
return image_ref.lower().replace('/', '_').replace(':', '_')


def relative_to_root(path: pathlib.Path) -> pathlib.Path:
return path.relative_to(path.anchor)


def detect_architecture(cwd: pathlib.Path, target_binary: pathlib.Path) -> str:
result = subprocess.run(['file', target_binary],
capture_output=True,
text=True,
cwd=cwd)
return result.stdout.split(",")[1].strip()


def untar_container_image(image_ref: str, output_dir: pathlib.Path):
container_name = replace_img_special_chars(image_ref)
subprocess.run(['docker', 'create', '--name', container_name, image_ref],
capture_output=True)
subprocess.run(f'docker export {container_name} | tar x',
shell=True,
cwd=output_dir,
capture_output=True)
subprocess.run(['docker', 'rm', container_name], capture_output=True)


def detect_dependencies(cwd: pathlib.Path, target_binary: pathlib.Path):
result = subprocess.run(['objdump', '-p', target_binary],
capture_output=True,
text=True,
cwd=cwd)
deps = []
for line in result.stdout.splitlines():
if "NEEDED" in line:
deps.append(line.split()[1])
return deps


def find_dependency_path(root_dir: pathlib.Path, dependency_name: str):
paths = sorted(root_dir.glob(f'**/{dependency_name}'))
return paths[0] if paths else None


def resolve_container_link(root_dir: pathlib.Path, link: pathlib.Path):
"""Resolve the provided link, make absolute paths relative to root_dir"""
resolved = os.readlink(link)

if str(resolved).startswith('/'):
return root_dir.joinpath(relative_to_root(resolved))
return link.resolve().relative_to(root_dir.resolve())


def main(image_ref: str, target_binary: str, clean=True):
print(f"Analyzing the binary {target_binary} from {image_ref}")
container_name = replace_img_special_chars(image_ref)
output_dir = pathlib.Path(IMAGE_DIR, container_name)
target_binary = relative_to_root(pathlib.Path(target_binary))

output_dir.mkdir(parents=True, exist_ok=True)
untar_container_image(image_ref, output_dir)
target_binary_arch = detect_architecture(output_dir, target_binary)
deps = detect_dependencies(output_dir, target_binary)

seen = set()
missing = set()
wrong_arch = {}
while deps:
current = deps.pop()

if current in seen:
continue

current_path = find_dependency_path(output_dir, current)

if not current_path:
missing.add(current)
continue

if current_path.is_symlink():
current_path = resolve_container_link(output_dir, current_path)

current_arch = detect_architecture(output_dir, current_path)
if current_arch != target_binary_arch:
wrong_arch[current_path] = current_arch

current_dependencies = detect_dependencies(output_dir, current_path)

deps.extend(current_dependencies)

seen.add(current)

print("Dependencies:")
for dep in seen:
print(dep)
print()

exit_code = 0
if wrong_arch:
exit_code = 1
print("Wrong Architecture:")
for dep, arch in wrong_arch.items():
print(f"{dep} - {arch}")
print()

if missing:
exit_code = 1
print("Missing Dependencies:")
for miss in missing:
print(miss)
print()

if exit_code == 0:
print(f"All dependencies for {target_binary} are present")

if clean:
shutil.rmtree(output_dir)

sys.exit(exit_code)


if __name__ == '__main__':
import argparse

parser = argparse.ArgumentParser(
description=
"A script to validate check if all of a binary's dynamically linked dependencies are present in the container"
)
parser.add_argument("--image",
help="A docker image to download and analyze",
required=True)
parser.add_argument("--target-bin",
help="The binary to analyze within the IMAGE",
required=True)
parser.add_argument("--skip-clean",
action="store_false",
help="Do not clean up the intermediate artifacts")

args = parser.parse_args()
main(args.image, args.target_bin, clean=args.skip_clean)

0 comments on commit d4b1472

Please sign in to comment.