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

Add basic namespace check on publishing DBs #192

Merged
merged 2 commits into from
Nov 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions manager/src/grype_db_manager/cli/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,9 +92,22 @@ def show_db(cfg: config.Application, db_uuid: str) -> None:
)
@click.option("--verbose", "-v", "verbosity", count=True, help="show details of all comparisons")
@click.option("--recapture", "-r", is_flag=True, help="recapture grype results (even if not stale)")
@click.option(
"--skip-namespace-check",
"skip_namespace_check",
is_flag=True,
help="do not ensure the minimum expected namespaces are present",
)
@click.argument("db-uuid")
@click.pass_obj
def validate_db(cfg: config.Application, db_uuid: str, images: list[str], verbosity: int, recapture: bool) -> None:
def validate_db(
cfg: config.Application,
db_uuid: str,
images: list[str],
verbosity: int,
recapture: bool,
skip_namespace_check: bool,
) -> None:
logging.info(f"validating DB {db_uuid}")

if not images:
Expand All @@ -107,6 +120,10 @@ def validate_db(cfg: config.Application, db_uuid: str, images: list[str], verbos
click.echo(f"no database found with session id {db_uuid}")
return

if not skip_namespace_check:
# ensure the minimum number of namespaces are present
db_manager.validate_namespaces(db_uuid=db_uuid)

# resolve tool versions and install them
yardstick.store.config.set_values(store_root=cfg.data.yardstick_root)

Expand Down Expand Up @@ -198,6 +215,12 @@ def upload_db(cfg: config.Application, db_uuid: str, ttl_seconds: int) -> None:
@click.option("--schema-version", "-s", required=True, help="the DB schema version to build, validate, and upload")
@click.option("--dry-run", "-d", is_flag=True, help="do not upload the DB to S3")
@click.option("--skip-validate", is_flag=True, help="skip validation of the DB")
@click.option(
"--skip-namespace-check",
"skip_namespace_check",
is_flag=True,
help="do not ensure the minimum expected namespaces are present",
)
@click.option("--verbose", "-v", "verbosity", count=True, help="show details of all comparisons")
@click.pass_obj
@click.pass_context
Expand All @@ -207,6 +230,7 @@ def build_and_upload_db(
cfg: config.Application,
schema_version: str,
skip_validate: bool,
skip_namespace_check: bool,
dry_run: bool,
verbosity: bool,
) -> None:
Expand All @@ -222,7 +246,7 @@ def build_and_upload_db(
click.echo(f"{Format.ITALIC}Skipping validation of DB {db_uuid!r}{Format.RESET}")
else:
click.echo(f"{Format.BOLD}Validating DB {db_uuid!r}{Format.RESET}")
ctx.invoke(validate_db, db_uuid=db_uuid, verbosity=verbosity)
ctx.invoke(validate_db, db_uuid=db_uuid, verbosity=verbosity, skip_namespace_check=skip_namespace_check)

if not dry_run:
click.echo(f"{Format.BOLD}Uploading DB {db_uuid!r}{Format.RESET}")
Expand Down
232 changes: 232 additions & 0 deletions manager/src/grype_db_manager/grypedb.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import shlex
import shutil
import sqlite3
import subprocess
import sys
import uuid
Expand All @@ -25,6 +26,206 @@
# TODO:
# - add tests for GrypeDB.install*

# these are the minimum expected namespaces that should be present in the DB based on the v4+ schema.
# TODO: ideally this would be coupled to the definitions defined in the vunnel quality gate config file
# https://github.com/anchore/vunnel/blob/v0.17.2/tests/quality/config.yaml#L53
# however, its important to use the file for the same version of vunnel used by grype-db to build the DB, which
# isn't always possible to know. Ideally this version info would be captured in the vunnel data directory directly.
# For the meantime this is a snapshot of the expected namespaces for vunnel 0.17.2 in Oct 2023 (boo! 👻).
expected_namespaces = [
"alpine:distro:alpine:3.10",
"alpine:distro:alpine:3.11",
"alpine:distro:alpine:3.12",
"alpine:distro:alpine:3.13",
"alpine:distro:alpine:3.14",
"alpine:distro:alpine:3.15",
"alpine:distro:alpine:3.16",
"alpine:distro:alpine:3.17",
"alpine:distro:alpine:3.18",
"alpine:distro:alpine:3.2",
"alpine:distro:alpine:3.3",
"alpine:distro:alpine:3.4",
"alpine:distro:alpine:3.5",
"alpine:distro:alpine:3.6",
"alpine:distro:alpine:3.7",
"alpine:distro:alpine:3.8",
"alpine:distro:alpine:3.9",
"alpine:distro:alpine:edge",
"amazon:distro:amazonlinux:2",
"amazon:distro:amazonlinux:2022",
"amazon:distro:amazonlinux:2023",
"chainguard:distro:chainguard:rolling",
"debian:distro:debian:10",
"debian:distro:debian:11",
"debian:distro:debian:12",
"debian:distro:debian:13",
"debian:distro:debian:7",
"debian:distro:debian:8",
"debian:distro:debian:9",
"debian:distro:debian:unstable",
"github:language:dart",
"github:language:dotnet",
"github:language:go",
"github:language:java",
"github:language:javascript",
"github:language:php",
"github:language:python",
"github:language:ruby",
"github:language:rust",
"github:language:swift",
"mariner:distro:mariner:1.0",
"mariner:distro:mariner:2.0",
"nvd:cpe",
"oracle:distro:oraclelinux:5",
"oracle:distro:oraclelinux:6",
"oracle:distro:oraclelinux:7",
"oracle:distro:oraclelinux:8",
"oracle:distro:oraclelinux:9",
"redhat:distro:redhat:5",
"redhat:distro:redhat:6",
"redhat:distro:redhat:7",
"redhat:distro:redhat:8",
"redhat:distro:redhat:9",
"sles:distro:sles:11",
"sles:distro:sles:11.1",
"sles:distro:sles:11.2",
"sles:distro:sles:11.3",
"sles:distro:sles:11.4",
"sles:distro:sles:12",
"sles:distro:sles:12.1",
"sles:distro:sles:12.2",
"sles:distro:sles:12.3",
"sles:distro:sles:12.4",
"sles:distro:sles:12.5",
"sles:distro:sles:15",
"sles:distro:sles:15.1",
"sles:distro:sles:15.2",
"sles:distro:sles:15.3",
"sles:distro:sles:15.4",
"sles:distro:sles:15.5",
"ubuntu:distro:ubuntu:12.04",
"ubuntu:distro:ubuntu:12.10",
"ubuntu:distro:ubuntu:13.04",
"ubuntu:distro:ubuntu:14.04",
"ubuntu:distro:ubuntu:14.10",
"ubuntu:distro:ubuntu:15.04",
"ubuntu:distro:ubuntu:15.10",
"ubuntu:distro:ubuntu:16.04",
"ubuntu:distro:ubuntu:16.10",
"ubuntu:distro:ubuntu:17.04",
"ubuntu:distro:ubuntu:17.10",
"ubuntu:distro:ubuntu:18.04",
"ubuntu:distro:ubuntu:18.10",
"ubuntu:distro:ubuntu:19.04",
"ubuntu:distro:ubuntu:19.10",
"ubuntu:distro:ubuntu:20.04",
"ubuntu:distro:ubuntu:20.10",
"ubuntu:distro:ubuntu:21.04",
"ubuntu:distro:ubuntu:21.10",
"ubuntu:distro:ubuntu:22.04",
"ubuntu:distro:ubuntu:22.10",
"ubuntu:distro:ubuntu:23.04",
"ubuntu:distro:ubuntu:23.10",
"wolfi:distro:wolfi:rolling",
]

v3_expected_namespaces = [
"alpine:3.10",
"alpine:3.11",
"alpine:3.12",
"alpine:3.13",
"alpine:3.14",
"alpine:3.15",
"alpine:3.16",
"alpine:3.17",
"alpine:3.18",
"alpine:3.2",
"alpine:3.3",
"alpine:3.4",
"alpine:3.5",
"alpine:3.6",
"alpine:3.7",
"alpine:3.8",
"alpine:3.9",
"alpine:edge",
"amzn:2",
"amzn:2022",
"amzn:2023",
"chainguard:rolling",
"debian:10",
"debian:11",
"debian:12",
"debian:13",
"debian:7",
"debian:8",
"debian:9",
"debian:unstable",
"github:composer",
"github:dart",
"github:gem",
"github:go",
"github:java",
"github:npm",
"github:nuget",
"github:python",
"github:rust",
"github:swift",
"mariner:1.0",
"mariner:2.0",
"nvd",
"ol:5",
"ol:6",
"ol:7",
"ol:8",
"ol:9",
"rhel:5",
"rhel:6",
"rhel:7",
"rhel:8",
"rhel:9",
"sles:11",
"sles:11.1",
"sles:11.2",
"sles:11.3",
"sles:11.4",
"sles:12",
"sles:12.1",
"sles:12.2",
"sles:12.3",
"sles:12.4",
"sles:12.5",
"sles:15",
"sles:15.1",
"sles:15.2",
"sles:15.3",
"sles:15.4",
"sles:15.5",
"ubuntu:12.04",
"ubuntu:12.10",
"ubuntu:13.04",
"ubuntu:14.04",
"ubuntu:14.10",
"ubuntu:15.04",
"ubuntu:15.10",
"ubuntu:16.04",
"ubuntu:16.10",
"ubuntu:17.04",
"ubuntu:17.10",
"ubuntu:18.04",
"ubuntu:18.10",
"ubuntu:19.04",
"ubuntu:19.10",
"ubuntu:20.04",
"ubuntu:20.10",
"ubuntu:21.04",
"ubuntu:21.10",
"ubuntu:22.04",
"ubuntu:22.10",
"ubuntu:23.04",
"ubuntu:23.10",
"wolfi:rolling",
]


@dataclasses.dataclass
class DBInfo:
Expand All @@ -40,6 +241,10 @@ class DBInvalidException(Exception):
pass


class DBNamespaceException(Exception):
pass


class DBManager:
def __init__(self, root_dir: str):
self.db_dir = os.path.join(root_dir, DB_DIR)
Expand All @@ -65,6 +270,33 @@ def new_session(self) -> str:

return db_uuid

def list_namespaces(self, db_uuid: str) -> list[str]:
_, build_dir = self.db_paths(db_uuid=db_uuid)
# a sqlite3 db
db_path = os.path.join(build_dir, "vulnerability.db")

# select distinct values in the "namespace" column of the "vulnerability" table
con = sqlite3.connect(db_path)
crsr = con.cursor()
crsr.execute("SELECT DISTINCT namespace FROM vulnerability")
result = crsr.fetchall()
con.close()

return sorted([r[0] for r in result])

def validate_namespaces(self, db_uuid: str) -> None:
db_info = self.get_db_info(db_uuid)

expected = v3_expected_namespaces if db_info.schema_version <= 3 else expected_namespaces

missing_namespaces = set(expected) - set(self.list_namespaces(db_uuid=db_uuid))

if missing_namespaces:
msg = f"missing namespaces in DB {db_uuid!r}: {sorted(missing_namespaces)!r}"
raise DBNamespaceException(msg)

logging.info(f"minimum expected namespaces present in {db_uuid!r}")

def get_db_info(self, db_uuid: str) -> DBInfo | None:
session_dir = os.path.join(self.db_dir, db_uuid)
if not os.path.exists(session_dir):
Expand Down
19 changes: 16 additions & 3 deletions manager/tests/cli/workflow-2-validate-db.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,33 @@ header "Case 1: fail DB validation (too many unknowns)"

make clean-yardstick-labels

run_expect_fail grype-db-manager db validate $DB_ID -vvv
run_expect_fail grype-db-manager db validate $DB_ID -vvv --skip-namespace-check
assert_contains $(last_stderr_file) "current indeterminate matches % is greater than 10.0%"


#############################################
header "Case 2: pass DB validation"
header "Case 2: fail DB validation (missing namespaces)"

make clean-yardstick-labels
echo "installing labels"
# use the real labels
cp -a ../../../data/vulnerability-match-labels/labels/docker.io+oraclelinux* ./cli-test-data/yardstick/labels/
tree ./cli-test-data/yardstick/labels/

run_expect_fail grype-db-manager db validate $DB_ID -vvv
assert_contains $(last_stderr_file) "missing namespaces in DB"


#############################################
header "Case 3: pass DB validation"

make clean-yardstick-labels
echo "installing labels"
# use the real labels
cp -a ../../../data/vulnerability-match-labels/labels/docker.io+oraclelinux* ./cli-test-data/yardstick/labels/
tree ./cli-test-data/yardstick/labels/

run grype-db-manager db validate $DB_ID -vvv
run grype-db-manager db validate $DB_ID -vvv --skip-namespace-check
assert_contains $(last_stdout_file) "Validation passed"


Expand Down
4 changes: 2 additions & 2 deletions manager/tests/cli/workflow-4-full-publish.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,10 +42,10 @@ header "Case 1: create and publish a DB"

# note: this test is exercising the following commands:
# grype-db-manager db build
# grype-db-manager db validate <uuid>
# grype-db-manager db validate <uuid> --skip-namespace-check
# grype-db-manager db upload <uuid>

run grype-db-manager db build-and-upload --schema-version $SCHEMA_VERSION
run grype-db-manager db build-and-upload --schema-version $SCHEMA_VERSION --skip-namespace-check
assert_contains $(last_stdout_file) "Validation passed"
assert_contains $(last_stdout_file) "' uploaded to s3://testbucket/grype/databases"

Expand Down
Loading