Skip to content

Commit

Permalink
feat: require complete and valid image ref on config parse (#388)
Browse files Browse the repository at this point in the history
* feat: require complete and valid image ref on config parse

Because image references are used to compute paths to state relating to
the scan of the image, it's critical that the image is always completely
specified.

Signed-off-by: Will Murphy <[email protected]>

* fix: path is optional, host is not

Signed-off-by: Will Murphy <[email protected]>

* stop requiring tag

Signed-off-by: Will Murphy <[email protected]>

* test: add invalid sha256 test case

Signed-off-by: Will Murphy <[email protected]>

---------

Signed-off-by: Will Murphy <[email protected]>
  • Loading branch information
willmurphyscode authored Sep 17, 2024
1 parent 83a0dda commit e29c335
Show file tree
Hide file tree
Showing 2 changed files with 178 additions and 0 deletions.
50 changes: 50 additions & 0 deletions src/yardstick/cli/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from typing import Any, Sequence

import mergedeep # type: ignore[import]
import re
import yaml
from dataclass_wizard import asdict, fromdict # type: ignore[import]

Expand Down Expand Up @@ -45,6 +46,8 @@ class ScanMatrix:
images: list[str] = field(default_factory=list)
tools: list[Tool] = field(default_factory=list)

DIGEST_REGEX = re.compile(r"(?P<digest>sha256:[a-fA-F0-9]{64})")

def __post_init__(self):
for idx, tool in enumerate(self.tools):
(
Expand All @@ -63,6 +66,53 @@ def __post_init__(self):
else:
images += [image]
self.images = images
invalid = [
image for image in images if not ScanMatrix.is_valid_oci_reference(image)
]
if invalid:
raise ValueError(
f"all images must be complete OCI references, but {' '.join(invalid)} are not"
)

@staticmethod
def is_valid_oci_reference(image: str) -> bool:
host, _, repository, _, digest = ScanMatrix.parse_oci_reference(image)
return (
all([host, repository, digest])
and bool(ScanMatrix.DIGEST_REGEX.match(digest or ""))
and ("." in host or "localhost" in host)
)

@staticmethod
def parse_oci_reference(image: str) -> tuple[str, str, str, str, str]:
host = ""
path = ""
host_and_path = ""
repository = ""
tag = ""
digest = ""

if "@" in image:
pre_digest, digest = image.rsplit("@", 1)
else:
pre_digest = image

if ":" in pre_digest:
pre_tag, tag = pre_digest.rsplit(":", 1)
else:
pre_tag = pre_digest

if "/" in pre_tag:
host_and_path, repository = pre_tag.rsplit("/", 1)
else:
repository = pre_tag

if host_and_path:
parts = host_and_path.split("/")
host = parts[0]
path = "/".join(parts[1:]) if len(parts) > 1 else ""

return host, path, repository, tag, digest


@dataclass()
Expand Down
128 changes: 128 additions & 0 deletions tests/unit/cli/test_config.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import pytest

from yardstick.cli import config


Expand Down Expand Up @@ -69,3 +71,129 @@ def test_config(tmp_path):
},
},
)


@pytest.mark.parametrize(
"name, image, expected_valid",
[
# valid: everything present
(
"valid",
"registry.example.com/repo/image:latest@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
True,
),
(
"valid: vulhub",
"docker.io/vulhub/cve-2017-1000353:latest@sha256:da2a59314b9ccfb428a313a7f163adcef77a74a393b8ebadeca8223b8cea9797",
True,
),
(
"valid: alpine",
"docker.io/alpine:3.2@sha256:ddac200f3ebc9902fb8cfcd599f41feb2151f1118929da21bcef57dc276975f9",
True,
),
# valid: localhost with port as repo host
(
"valid: localhost with port as repo host",
"localhost:5555/repo/image:latest@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
True,
),
(
"valid: missing tag is allowed but discouraged",
"registry.access.redhat.com/ubi8@sha256:68fecea0d255ee253acbf0c860eaebb7017ef5ef007c25bee9eeffd29ce85b29",
True,
),
(
"invalid: missing host",
"repo/image:latest@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
False,
),
("invalid: missing digest", "registry.example.com/repo/image:latest", False),
("invalid: missing everything", "repo/image", False),
("invalid: empty string", "", False),
(
"invalid: missing repo",
"registry.example.com/:latest@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
False,
),
("invalid: missing repo and tag", "registry.example.com/", False),
("invalid: missing digest", "registry.example.com/repo/image:stable", False),
(
"invalid: digest does not look like sha256",
"registry.example.com/repo/image:latest@sha256:invaliddigest",
False,
),
(
"invalid: bad sha256 (too short)",
"docker.io/alpine:3.2@sha256:ddac200f3ebc9902fb8cfcd599f41feb2151f1118929da21bcef57dc27697",
False,
),
],
)
def test_is_valid_oci_reference(name, image, expected_valid):
result = config.ScanMatrix.is_valid_oci_reference(image)
assert (
result == expected_valid
), f"Test case {name}: Expected {expected_valid} but got {result} for image '{image}'"


@pytest.mark.parametrize(
"image, expected_output",
[
(
"docker.io/anchore/test_images:some-tag@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
(
"docker.io",
"anchore",
"test_images",
"some-tag",
"sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
),
),
# Localhost reference with path, repository, tag, and digest
(
"localhost/anchore/test_images:some-tag@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
(
"localhost",
"anchore",
"test_images",
"some-tag",
"sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
),
),
# Localhost with port, path, repository, tag, and digest
(
"localhost:5000/anchore/test_images:some-tag@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
(
"localhost:5000",
"anchore",
"test_images",
"some-tag",
"sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
),
),
# Missing digest
(
"docker.io/anchore/test_images:some-tag",
("docker.io", "anchore", "test_images", "some-tag", ""),
),
# Missing tag
(
"docker.io/anchore/test_images@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
(
"docker.io",
"anchore",
"test_images",
"",
"sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
),
),
# Only repository
("test_images", ("", "", "test_images", "", "")),
],
)
def test_parse_oci_reference(image, expected_output):
result = config.ScanMatrix.parse_oci_reference(image)
assert (
result == expected_output
), f"Expected {expected_output} but got {result} for image '{image}'"

0 comments on commit e29c335

Please sign in to comment.