Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cyt 814 docker scout #193

Merged
merged 10 commits into from
Jun 18, 2024
4 changes: 2 additions & 2 deletions surfactant/cmd/generate.py
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ def warn_if_hash_collision(soft1: Optional[Software], soft2: Optional[Software])
help="SBOM output format, see --list-output-formats for list of options; default is CyTRICS",
)
@click.option(
"--list-output-formats",
"--list_output_formats",
is_flag=True,
callback=print_output_formats,
expose_value=False,
Expand All @@ -200,7 +200,7 @@ def warn_if_hash_collision(soft1: Optional[Software], soft2: Optional[Software])
help="Input SBOM format, see --list-input-formats for list of options; default is CyTRICS",
)
@click.option(
"--list-input-formats",
"--list_input_formats",
is_flag=True,
callback=print_input_formats,
expose_value=False,
Expand Down
30 changes: 30 additions & 0 deletions surfactant/filetypeid/id_magic.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
# See the top-level LICENSE file for details.
#
# SPDX-License-Identifier: MIT
import json
import pathlib
import tarfile
from enum import Enum, auto
from typing import Optional

Expand All @@ -19,6 +21,30 @@ class ExeType(Enum):
MACHO64 = auto()


def is_docker_archive(filepath: str) -> bool:
# pylint: disable=too-many-return-statements
with tarfile.open(filepath) as tar:
try:
manifest_info = tar.getmember("manifest.json")
if not manifest_info.isfile():
return False
with tar.extractfile(manifest_info) as manifest_file:
manifest = json.load(manifest_file)
# There's one entry in the list for each image
if not isinstance(manifest, list):
return False
for data in manifest:
# Just check if this data member exists
_ = tar.getmember(data["Config"])
# Now check that each of the layers exist
for layer in data["Layers"]:
_ = tar.getmember(layer)
# Everything seems to exist and be in order; this is most likely a Docker archive
return True
except KeyError:
return False


@surfactant.plugin.hookimpl(tryfirst=True)
def identify_file_type(filepath: str) -> Optional[str]:
# pylint: disable=too-many-return-statements
Expand Down Expand Up @@ -76,8 +102,12 @@ def identify_file_type(filepath: str) -> Optional[str]:
".tar.gz",
".cab.gz",
]:
if is_docker_archive(filepath):
return "DOCKER_GZIP"
return "GZIP"
if magic_bytes[257:265] == b"ustar\x0000" or magic_bytes[257:265] == b"ustar \x00":
if is_docker_archive(filepath):
return "DOCKER_TAR"
return "TAR"
if magic_bytes[:4] in [b"PK\x03\x04", b"PK\x05\x06", b"PK\x07\x08"]:
suffix = pathlib.Path(filepath).suffix.lower()
Expand Down
64 changes: 64 additions & 0 deletions surfactant/infoextractors/docker_image.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# Copyright 2023 Lawrence Livermore National Security, LLC
# See the top-level LICENSE file for details.
#
# SPDX-License-Identifier: MIT
import gzip
import json
import subprocess
import tempfile

from loguru import logger

import surfactant.plugin
from surfactant.sbomtypes import SBOM, Software


def is_docker_scout_installed():
# Check that Docker Scout can be run
try:
result = subprocess.run(["docker", "scout"], capture_output=True, check=False)
if result.returncode != 0:
logger.warning("Install Docker Scout to scan containers for additional information")
return False
return True
except FileNotFoundError:
return False


# Check if Docker Scout is installed when this Python module gets loaded
disable_docker_scout = not is_docker_scout_installed()


def supports_file(filetype: str) -> bool:
return filetype in ("DOCKER_TAR", "DOCKER_GZIP")


@surfactant.plugin.hookimpl
def extract_file_info(sbom: SBOM, software: Software, filename: str, filetype: str) -> object:
if disable_docker_scout or not supports_file(filetype):
return None
return extract_docker_info(filetype, filename)


def extract_docker_info(filetype: str, filename: str) -> object:
if filetype == "DOCKER_GZIP":
with open(filename, "rb") as gzip_in:
gzip_data = gzip_in.read()
with tempfile.NamedTemporaryFile() as gzip_out:
gzip_out.write(gzip.decompress(gzip_data))
return run_docker_scout(gzip_out.name)
return run_docker_scout(filename)


# Function that extract_docker_info delegates to to actually run Docker scout
def run_docker_scout(filename: str) -> object:
result = subprocess.run(
["docker", "scout", "sbom", "--format", "spdx", f"fs://{filename}"],
capture_output=True,
check=False,
)
if result.returncode != 0:
logger.warning(f"Running Docker Scout on {filename} failed")
return {}
spdx_out = json.loads(result.stdout)
return {"dockerSPDX": spdx_out}
2 changes: 2 additions & 0 deletions surfactant/plugin/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ def _register_plugins(pm: pluggy.PluginManager) -> None:
from surfactant.infoextractors import (
a_out_file,
coff_file,
docker_image,
elf_file,
java_file,
ole_file,
Expand All @@ -42,6 +43,7 @@ def _register_plugins(pm: pluggy.PluginManager) -> None:
id_extension,
a_out_file,
coff_file,
docker_image,
elf_file,
java_file,
pe_file,
Expand Down
Loading