From 337d8f0d1408254a885f502f5356bb39e8046073 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 10 Mar 2023 13:10:22 -0500 Subject: [PATCH] Update linters (#100) * upgrade linter version + update Makefile targets Signed-off-by: Alex Goodman * remove linter ignores for existing providers Signed-off-by: Alex Goodman * fixes for ruff 0.0.254 update Signed-off-by: Alex Goodman * upgrade black to v23.1.0 Signed-off-by: Alex Goodman * make exception for python 3.9 upgrade rule Signed-off-by: Alex Goodman * enable access to token for the github provider Signed-off-by: Alex Goodman * bump submodule to the latest labels Signed-off-by: Alex Goodman * show year limit and require NVD for github provider changes Signed-off-by: Alex Goodman * bump yardstick to v0.4.4 Signed-off-by: Alex Goodman * bump and unpin syft in quality gate Signed-off-by: Alex Goodman * add submodules to checkout for quality gate (for labels) Signed-off-by: Alex Goodman * allow for using provider cache for the subject under test Signed-off-by: Alex Goodman * remove latest indeterminate label check and cache rhel provider for qg runs Signed-off-by: Alex Goodman * bump to include more el labels Signed-off-by: Alex Goodman * fix end of line for qg check Signed-off-by: Alex Goodman --------- Signed-off-by: Alex Goodman --- .github/workflows/nightly-quality-gate.yaml | 5 ++ .github/workflows/pr-quality-gate.yaml | 7 +- Makefile | 78 +++++++++++------ poetry.lock | 85 +++++++++++-------- pyproject.toml | 34 +++----- src/vunnel/provider.py | 2 - src/vunnel/providers/alpine/__init__.py | 2 - src/vunnel/providers/amazon/parser.py | 12 +-- src/vunnel/providers/centos/__init__.py | 1 - src/vunnel/providers/debian/__init__.py | 1 - src/vunnel/providers/debian/parser.py | 13 ++- src/vunnel/providers/github/__init__.py | 1 - src/vunnel/providers/github/parser.py | 17 ++-- src/vunnel/providers/nvd/api.py | 2 - src/vunnel/providers/oracle/__init__.py | 2 - src/vunnel/providers/oracle/parser.py | 5 +- src/vunnel/providers/rhel/__init__.py | 1 - src/vunnel/providers/rhel/parser.py | 1 - src/vunnel/providers/sles/__init__.py | 1 - src/vunnel/providers/sles/parser.py | 8 +- src/vunnel/providers/ubuntu/__init__.py | 1 - src/vunnel/providers/ubuntu/parser.py | 12 ++- src/vunnel/providers/wolfi/__init__.py | 1 - src/vunnel/utils/fdb.py | 7 +- src/vunnel/utils/vulnerability.py | 2 +- tests/conftest.py | 6 +- tests/quality/Makefile | 2 +- tests/quality/config.yaml | 24 +++++- tests/quality/configure.py | 70 +++++++-------- tests/quality/gate.py | 73 +++++++++------- tests/quality/vulnerability-match-labels | 2 +- tests/unit/cli/test_cli.py | 7 +- tests/unit/providers/alpine/test_alpine.py | 31 ++++--- tests/unit/providers/amazon/test_amazon.py | 5 +- tests/unit/providers/centos/test_centos.py | 13 ++- tests/unit/providers/debian/test_debian.py | 22 ++--- tests/unit/providers/github/test_github.py | 40 ++++----- tests/unit/providers/nvd/test_api.py | 33 ++++--- tests/unit/providers/nvd/test_manager.py | 3 +- tests/unit/providers/nvd/test_nvd.py | 7 +- tests/unit/providers/oracle/test_oracle.py | 53 ++++++------ tests/unit/providers/sles/test_sles.py | 2 - .../unit/providers/ubuntu/test_git_wrapper.py | 19 ++--- tests/unit/providers/ubuntu/test_ubuntu.py | 22 ++--- tests/unit/providers/wolfi/test_wolfi.py | 41 +++++---- tests/unit/test_provider.py | 3 +- tests/unit/utils/test_fdb.py | 5 +- tests/unit/utils/test_oval_v2.py | 41 ++++----- tests/unit/utils/test_rpm.py | 3 +- 49 files changed, 425 insertions(+), 403 deletions(-) diff --git a/.github/workflows/nightly-quality-gate.yaml b/.github/workflows/nightly-quality-gate.yaml index 4a4fc8c1..f8c2e61f 100644 --- a/.github/workflows/nightly-quality-gate.yaml +++ b/.github/workflows/nightly-quality-gate.yaml @@ -47,6 +47,8 @@ jobs: with: # in order to properly resolve the version from git fetch-depth: 0 + # we need submodules for the quality gate to work (requires vulnerability-match-labels repo) + submodules: true - name: Bootstrap environment uses: ./.github/actions/bootstrap @@ -57,6 +59,9 @@ jobs: uses: ./.github/actions/quality-gate with: provider: ${{ matrix.provider }} + env: + # needed as a secret for the github provider + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # note: the name for this check is referenced in release.yaml, do not change here without changing there Nightly-Quality-Gate: diff --git a/.github/workflows/pr-quality-gate.yaml b/.github/workflows/pr-quality-gate.yaml index f76429f4..13771306 100644 --- a/.github/workflows/pr-quality-gate.yaml +++ b/.github/workflows/pr-quality-gate.yaml @@ -57,6 +57,8 @@ jobs: with: # in order to properly resolve the version from git fetch-depth: 0 + # we need submodules for the quality gate to work (requires vulnerability-match-labels repo) + submodules: true - name: Bootstrap environment uses: ./.github/actions/bootstrap @@ -67,6 +69,9 @@ jobs: uses: ./.github/actions/quality-gate with: provider: ${{ matrix.provider }} + env: + # needed as a secret for the github provider + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} evaluate-quality-gate: runs-on: ubuntu-20.04 @@ -88,7 +93,7 @@ jobs: echo echo "This could happen for a couple of reasons:" echo " - A provider test failed, in which case see the logs in previous jobs for more details" - echo " - A required provider test was skipped. You might need to add the 'run-pr-quality-gate' label to your PR to prevent skipping the test. + echo " - A required provider test was skipped. You might need to add the 'run-pr-quality-gate' label to your PR to prevent skipping the test." exit 1 fi echo "🟢 Quality gate passed! (all tests passed)" diff --git a/Makefile b/Makefile index ddcff511..08c2e4a3 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,5 @@ TEMP_DIR = ./.tmp +IMAGE_NAME = ghcr.io/anchore/vunnel BIN_DIR = ./bin ABS_BIN_DIR = $(shell realpath $(BIN_DIR)) @@ -6,12 +7,18 @@ ABS_BIN_DIR = $(shell realpath $(BIN_DIR)) GRYPE_PATH ?= ../grype GRYPE_DB_PATH ?= ../grype-db +# Command templates ################################# + CRANE = $(TEMP_DIR)/crane CHRONICLE = $(TEMP_DIR)/chronicle GLOW = $(TEMP_DIR)/glow -IMAGE_NAME = ghcr.io/anchore/vunnel -# formatting support +# Tool versions ################################# +CHRONICLE_VERSION = v0.6.0 +GLOW_VERSION = v1.4.1 +CRANE_VERSION = v0.12.1 + +# Formatting variables ################################# BOLD := $(shell tput -T linux bold) PURPLE := $(shell tput -T linux setaf 5) GREEN := $(shell tput -T linux setaf 2) @@ -28,10 +35,6 @@ PACKAGE_VERSION = v$(shell poetry run dunamai from git --style semver --dirty -- COMMIT = $(shell git rev-parse HEAD) COMMIT_TAG = git-$(COMMIT) -CHRONICLE_VERSION = v0.6.0 -GLOW_VERSION = v1.4.1 -CRANE_VERSION = v0.12.1 - ifndef PACKAGE_VERSION $(error PACKAGE_VERSION is not set) @@ -42,6 +45,34 @@ endif .PHONY: all all: static-analysis test ## Run all validations +.PHONY: static-analysis +static-analysis: virtual-env-check ## Run all static analyses + pre-commit run -a --hook-stage push + +.PHONY: test +test: unit ## Run all tests + +virtual-env-check: + @ if [ "${VIRTUAL_ENV}" = "" ]; then \ + echo "$(ERROR)Not in a virtual environment. Try running with 'poetry run' or enter a 'poetry shell' session.$(RESET)"; \ + exit 1; \ + fi + + +## Bootstrapping targets ################################# + +.PHONY: bootstrap +bootstrap: $(TEMP_DIR) ## Download and install all tooling dependencies + curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION) + GOBIN="$(abspath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION) + GOBIN="$(abspath $(TEMP_DIR))" go install github.com/google/go-containerregistry/cmd/crane@$(CRANE_VERSION) + +$(TEMP_DIR): + mkdir -p $(TEMP_DIR) + + +## Development targets ################################# + .PHONY: dev dev: ## Get a development shell with locally editable grype, grype-db, and vunnel repos @DEV_VUNNEL_BIN_DIR=$(ABS_BIN_DIR) .github/scripts/dev-shell.sh $(provider) $(providers) @@ -62,21 +93,13 @@ update-db: check-dev-shell ## Build and import a grype database based off of the check-dev-shell: @test -n "$$DEV_VUNNEL_SHELL" || (echo "$(RED)DEV_VUNNEL_SHELL is not set. Run 'make dev provider=\"...\"' first$(RESET)" && exit 1) -$(TEMP_DIR): - mkdir -p $(TEMP_DIR) -.PHONY: bootstrap -bootstrap: $(TEMP_DIR) ## Download and install all tooling dependencies - curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMP_DIR)/ $(CHRONICLE_VERSION) - GOBIN="$(abspath $(TEMP_DIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION) - GOBIN="$(abspath $(TEMP_DIR))" go install github.com/google/go-containerregistry/cmd/crane@$(CRANE_VERSION) -.PHONY: test -test: unit ## Run all tests +## Static analysis targets ################################# -.PHONY: static-analysis -static-analysis: virtual-env-check ## Run all static analyses - pre-commit run -a --hook-stage push +.PHONY: lint +lint: virtual-env-check ## Show linting issues (ruff) + ruff check . .PHONY: lint-fix lint-fix: virtual-env-check ## Fix linting issues (ruff) @@ -90,13 +113,15 @@ format: virtual-env-check ## Format all code (black) check-types: virtual-env-check ## Run type checks (mypy) mypy --config-file ./pyproject.toml src/vunnel + +## Testing targets ################################# + .PHONY: unit unit: virtual-env-check ## Run unit tests pytest --cov-report html --cov vunnel -v tests/unit/ -.PHONY: version -version: - @echo $(PACKAGE_VERSION) + +## Build-related targets ################################# .PHONY: build build: ## Run build assets @@ -107,6 +132,10 @@ build: ## Run build assets -t $(IMAGE_NAME):$(COMMIT_TAG) \ . +.PHONY: version +version: + @echo $(PACKAGE_VERSION) + .PHONY: ci-check ci-check: @.github/scripts/ci-check.sh @@ -129,11 +158,8 @@ changelog: release: @.github/scripts/trigger-release.sh -virtual-env-check: - @ if [ "${VIRTUAL_ENV}" = "" ]; then \ - echo "$(ERROR)Not in a virtual environment. Try running with 'poetry run' or enter a 'poetry shell' session.$(RESET)"; \ - exit 1; \ - fi + +## Halp! ################################# .PHONY: help help: diff --git a/poetry.lock b/poetry.lock index ebb8b190..e31efcac 100644 --- a/poetry.lock +++ b/poetry.lock @@ -21,32 +21,46 @@ tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy [[package]] name = "black" -version = "22.12.0" +version = "23.1.0" description = "The uncompromising code formatter." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "black-22.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eedd20838bd5d75b80c9f5487dbcb06836a43833a37846cf1d8c1cc01cef59d"}, - {file = "black-22.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:159a46a4947f73387b4d83e87ea006dbb2337eab6c879620a3ba52699b1f4351"}, - {file = "black-22.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d30b212bffeb1e252b31dd269dfae69dd17e06d92b87ad26e23890f3efea366f"}, - {file = "black-22.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:7412e75863aa5c5411886804678b7d083c7c28421210180d67dfd8cf1221e1f4"}, - {file = "black-22.12.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c116eed0efb9ff870ded8b62fe9f28dd61ef6e9ddd28d83d7d264a38417dcee2"}, - {file = "black-22.12.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1f58cbe16dfe8c12b7434e50ff889fa479072096d79f0a7f25e4ab8e94cd8350"}, - {file = "black-22.12.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77d86c9f3db9b1bf6761244bc0b3572a546f5fe37917a044e02f3166d5aafa7d"}, - {file = "black-22.12.0-cp38-cp38-win_amd64.whl", hash = "sha256:82d9fe8fee3401e02e79767016b4907820a7dc28d70d137eb397b92ef3cc5bfc"}, - {file = "black-22.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101c69b23df9b44247bd88e1d7e90154336ac4992502d4197bdac35dd7ee3320"}, - {file = "black-22.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:559c7a1ba9a006226f09e4916060982fd27334ae1998e7a38b3f33a37f7a2148"}, - {file = "black-22.12.0-py3-none-any.whl", hash = "sha256:436cc9167dd28040ad90d3b404aec22cedf24a6e4d7de221bec2730ec0c97bcf"}, - {file = "black-22.12.0.tar.gz", hash = "sha256:229351e5a18ca30f447bf724d007f890f97e13af070bb6ad4c0a441cd7596a2f"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, + {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, + {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, + {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, + {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, + {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, + {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, + {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, + {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, + {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, + {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, + {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, + {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, + {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" +packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" -tomli = {version = ">=1.1.0", markers = "python_full_version < \"3.11.0a7\""} +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} [package.extras] @@ -1306,28 +1320,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.0.243" +version = "0.0.254" description = "An extremely fast Python linter, written in Rust." category = "dev" optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.0.243-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:242571d79d3d7a1e441e88b0cf2814b24bfc4e3a073e5d82df81aa52ad829e4c"}, - {file = "ruff-0.0.243-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:4fd0ef0dddd7ccce6457cca556baf51504c11f7deaaa5944a47c5e0c6c3b1425"}, - {file = "ruff-0.0.243-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e2e6632e2d07e6e7257a44592e0fade0d5df23004a3b180efd0d3bbb581a09"}, - {file = "ruff-0.0.243-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a70c7810453f6c5120887fc22fcbcf8a4987e767f45270a9aad5e6e9b0a26ff"}, - {file = "ruff-0.0.243-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0cb8ef4a5cbb219ed344286b07795c0b88f277bc860207e0a6bce0fd8e4c5f8e"}, - {file = "ruff-0.0.243-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:f75c11940cc6b374ba070b5dc154c85c2b8753d03cbb53f182438404bae52d31"}, - {file = "ruff-0.0.243-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919075726724d62b60caedd286317ca0c77cb67ba4291b9067feafdac2506872"}, - {file = "ruff-0.0.243-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a0f5bdb14a2a2f9a63f6f0979fb0501e426e2bd8e6499ade41e1311b379a4d92"}, - {file = "ruff-0.0.243-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ffdbf62d72db5ab5d3b51abe5b4dfb53cf3f330af7f57e0101f36ff7176449"}, - {file = "ruff-0.0.243-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:081b70f1dd2d16d9f60079cf95215d9095ca16032c02118cfc88b0b53e406b9c"}, - {file = "ruff-0.0.243-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d8f1d26c0a3b51a4b5c493c29536112c61ec6ff7a66c5b673b2af37b7859d6f1"}, - {file = "ruff-0.0.243-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f432e745f6b38e2a643ae9c05ae30345196a435a23d844b61a50d1808acba82"}, - {file = "ruff-0.0.243-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f38b6b6470b468271e283ecc7bc169465b3f3cdb6df199abe69aff0c3c17c756"}, - {file = "ruff-0.0.243-py3-none-win32.whl", hash = "sha256:2707e2c32ace855afad3e06bddf2280d1fc15e303dea2de3ccd0e308a5b395ae"}, - {file = "ruff-0.0.243-py3-none-win_amd64.whl", hash = "sha256:be44aff098fd424b9a9218eedef80d7125222ea86c3cd62e15f6f587455c99f3"}, - {file = "ruff-0.0.243.tar.gz", hash = "sha256:d5847e75038b51801f45b31a93c3526114d3aac59acea3493bb06ebc7783b004"}, + {file = "ruff-0.0.254-py3-none-macosx_10_7_x86_64.whl", hash = "sha256:dd58c500d039fb381af8d861ef456c3e94fd6855c3d267d6c6718c9a9fe07be0"}, + {file = "ruff-0.0.254-py3-none-macosx_10_9_x86_64.macosx_11_0_arm64.macosx_10_9_universal2.whl", hash = "sha256:688379050ae05394a6f9f9c8471587fd5dcf22149bd4304a4ede233cc4ef89a1"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac1429be6d8bd3db0bf5becac3a38bd56f8421447790c50599cd90fd53417ec4"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:059a380c08e849b6f312479b18cc63bba2808cff749ad71555f61dd930e3c9a2"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3f15d5d033fd3dcb85d982d6828ddab94134686fac2c02c13a8822aa03e1321"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8deba44fd563361c488dedec90dc330763ee0c01ba54e17df54ef5820079e7e0"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef20bf798ffe634090ad3dc2e8aa6a055f08c448810a2f800ab716cc18b80107"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0deb1d7226ea9da9b18881736d2d96accfa7f328c67b7410478cc064ad1fa6aa"}, + {file = "ruff-0.0.254-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:27d39d697fdd7df1f2a32c1063756ee269ad8d5345c471ee3ca450636d56e8c6"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:2fc21d060a3197ac463596a97d9b5db2d429395938b270ded61dd60f0e57eb21"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f70dc93bc9db15cccf2ed2a831938919e3e630993eeea6aba5c84bc274237885"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_i686.whl", hash = "sha256:09c764bc2bd80c974f7ce1f73a46092c286085355a5711126af351b9ae4bea0c"}, + {file = "ruff-0.0.254-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:d4385cdd30153b7aa1d8f75dfd1ae30d49c918ead7de07e69b7eadf0d5538a1f"}, + {file = "ruff-0.0.254-py3-none-win32.whl", hash = "sha256:c38291bda4c7b40b659e8952167f386e86ec29053ad2f733968ff1d78b4c7e15"}, + {file = "ruff-0.0.254-py3-none-win_amd64.whl", hash = "sha256:e15742df0f9a3615fbdc1ee9a243467e97e75bf88f86d363eee1ed42cedab1ec"}, + {file = "ruff-0.0.254-py3-none-win_arm64.whl", hash = "sha256:b435afc4d65591399eaf4b2af86e441a71563a2091c386cadf33eaa11064dc09"}, + {file = "ruff-0.0.254.tar.gz", hash = "sha256:0eb66c9520151d3bd950ea43b3a088618a8e4e10a5014a72687881e6f3606312"}, ] [[package]] @@ -1715,7 +1730,7 @@ files = [ [[package]] name = "yardstick" -version = "0.3.4.post23.dev0+0ed40aa" +version = "0.3.4.post30.dev0+bd77dfc" description = "Tool for comparing the results from vulnerability scanners" category = "dev" optional = false @@ -1742,8 +1757,8 @@ tabulate = "^0.9.0" [package.source] type = "git" url = "https://github.com/anchore/yardstick" -reference = "v0.4.3" -resolved_reference = "16257bced5cda1033b25c7f877ffdb5b80bad77d" +reference = "v0.4.4" +resolved_reference = "b8ca41acfb6ebef194e994f94ecc065f43b8f26c" [[package]] name = "zipp" @@ -1764,4 +1779,4 @@ testing = ["big-O", "flake8 (<5)", "jaraco.functools", "jaraco.itertools", "more [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "6ce4b7d1a055847b41258f9b619628b3e36d0d5f2a65a7632f7a21c44f462b89" +content-hash = "4e459f423f4f0748882b7bc9d191e84655d6d47e65856c0d529f18f90c037f34" diff --git a/pyproject.toml b/pyproject.toml index 39786541..1f719d7f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ +[project] +name = "vunnel" +requires-python = ">=3.9.0" + [tool.poetry.scripts] vunnel = "vunnel.cli:run" @@ -35,7 +39,7 @@ importlib-metadata = "^6.0.0" [tool.poetry.group.dev.dependencies] pytest = "^7.2.2" pre-commit = "^3.1.1" -black = "^22.10.0" +black = "^23.1.0" jsonschema = "^4.17.3" pytest-unordered = "^0.5.2" pytest-sugar = "^0.9.6" @@ -49,8 +53,8 @@ types-requests = "^2.28.11.7" mypy = "^1.1" radon = "^5.1.0" dunamai = "^1.15.0" -ruff = "^0.0.243" -yardstick = {git = "https://github.com/anchore/yardstick", rev = "v0.4.3"} +ruff = "^0.0.254" +yardstick = {git = "https://github.com/anchore/yardstick", rev = "v0.4.4"} tabulate = "0.9.0" [build-system] @@ -185,28 +189,14 @@ select = [ ignore = [ "ARG001", # unused args are ok, as they communicate intent in interfaces, even if not used in impls. "ARG002", # unused args are ok, as they communicate intent in interfaces, even if not used in impls. - "RUF100", # no blanket "noqa" usage, can be improved over time, but not now + "G004", # it's ok to use formatted strings for logging "PGH004", # no blanked "noqa" usage, can be improved over time, but not now "PLR2004", # a little too agressive, not allowing any magic numbers - "G004", # it's ok to use formatted strings for logging + "PLW2901", # "Outer for loop variable X overwritten by inner assignment target", not useful in most cases + "RUF100", # no blanket "noqa" usage, can be improved over time, but not now + "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` -- not compatible with python 3.9 (even with __future__ import) ] extend-exclude = [ - "**/src/vunnel/providers/alpine/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/amazon/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/centos/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/debian/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/github/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/nvd/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/oracle/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/rhel/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/sles/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/ubuntu/git.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/ubuntu/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/providers/wolfi/parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/utils/oval_v2.py", # ported from enterprise, never had type hints - "**/src/vunnel/utils/oval_parser.py", # ported from enterprise, never had type hints - "**/src/vunnel/utils/fdb.py", # ported from enterprise, never had type hints - "**/src/vunnel/utils/vulnerability.py", # ported from enterprise, never had type hints - "**/tests/**", # any tests + "**/tests/**", ] diff --git a/src/vunnel/provider.py b/src/vunnel/provider.py index d1d83a1b..aa6e39f8 100644 --- a/src/vunnel/provider.py +++ b/src/vunnel/provider.py @@ -51,7 +51,6 @@ class OnErrorConfig: results: ResultStatePolicy = ResultStatePolicy.KEEP def __post_init__(self) -> None: - if not isinstance(self.action, OnErrorAction): self.action = OnErrorAction(self.action) if not isinstance(self.input, InputStatePolicy): @@ -72,7 +71,6 @@ class RuntimeConfig: result_store: result.StoreStrategy = result.StoreStrategy.FLAT_FILE def __post_init__(self) -> None: - if not isinstance(self.existing_input, InputStatePolicy): self.existing_input = InputStatePolicy(self.existing_input) if not isinstance(self.existing_results, ResultStatePolicy): diff --git a/src/vunnel/providers/alpine/__init__.py b/src/vunnel/providers/alpine/__init__.py index 12676839..7bfc52d4 100644 --- a/src/vunnel/providers/alpine/__init__.py +++ b/src/vunnel/providers/alpine/__init__.py @@ -46,11 +46,9 @@ def name(cls) -> str: return "alpine" def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int]: - with self.results_writer() as writer: # TODO: tech debt: on subsequent runs, we should only write new vulns (this currently re-writes all) for namespace, vulns in self.parser.get(): - namespace = namespace.lower() for vuln_id, record in vulns.items(): vuln_id = vuln_id.lower() diff --git a/src/vunnel/providers/amazon/parser.py b/src/vunnel/providers/amazon/parser.py index 97d0edf7..ee5efb4b 100644 --- a/src/vunnel/providers/amazon/parser.py +++ b/src/vunnel/providers/amazon/parser.py @@ -78,10 +78,7 @@ def _parse_rss(self, file_path): sev = found.group(2) elif element.tag == "description": desc_str = element.text.strip() - if desc_str: - cves = re.sub(self._whitespace_pattern_, "", desc_str).split(",") - else: - cves = [] + cves = re.sub(self._whitespace_pattern_, "", desc_str).split(",") if desc_str else [] elif element.tag == "link": url = element.text.strip() elif element.tag == "item": @@ -170,11 +167,10 @@ def json(self): jsonified[k] = [x.json() if hasattr(x, "json") and callable(x.json) else x for x in v] elif isinstance(v, dict): jsonified[k] = {x: y.json() if hasattr(y, "json") and callable(y.json) else y for x, y in v.items()} + elif hasattr(v, "json"): + jsonified[k] = v.json() else: - if hasattr(v, "json"): - jsonified[k] = v.json() - else: - jsonified[k] = v + jsonified[k] = v return jsonified diff --git a/src/vunnel/providers/centos/__init__.py b/src/vunnel/providers/centos/__init__.py index f6ea27e8..1135670f 100644 --- a/src/vunnel/providers/centos/__init__.py +++ b/src/vunnel/providers/centos/__init__.py @@ -57,7 +57,6 @@ def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int with self.results_writer() as writer: for (vuln_id, namespace), (_, record) in vuln_dict.items(): - namespace = namespace.lower() vuln_id = vuln_id.lower() diff --git a/src/vunnel/providers/debian/__init__.py b/src/vunnel/providers/debian/__init__.py index 812c2887..05b4c129 100644 --- a/src/vunnel/providers/debian/__init__.py +++ b/src/vunnel/providers/debian/__init__.py @@ -52,7 +52,6 @@ def name(cls) -> str: return "debian" def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int]: - with self.results_writer() as writer: # TODO: tech debt: on subsequent runs, we should only write new vulns (this currently re-writes all) for relno, vuln_id, record in self.parser.get(): diff --git a/src/vunnel/providers/debian/parser.py b/src/vunnel/providers/debian/parser.py index 1dafe32e..c4e479dc 100644 --- a/src/vunnel/providers/debian/parser.py +++ b/src/vunnel/providers/debian/parser.py @@ -119,7 +119,7 @@ def _get_cve_to_dsalist(self, dsa): distro=fixedin["distro"], pkg=fixedin["pkg"], ver=fixedin["ver"], - ) + ), ) else: self.logger.debug(f"no CVEs found for {dsa['id']}") @@ -169,7 +169,7 @@ def _parse_dsa_record(self, dsa_lines): version = version.strip() if version else None if not version: self.logger.debug( - f"release version not included dsa: {dsa.get('id', None)}, distro: {distro}, pkg: {pkg}" + f"release version not included dsa: {dsa.get('id', None)}, distro: {distro}, pkg: {pkg}", ) dsa["fixed_in"].append({"distro": distro, "pkg": pkg, "ver": version}) continue @@ -180,7 +180,7 @@ def _parse_dsa_record(self, dsa_lines): version = version.strip() if version else None if not version: self.logger.debug( - f"release version not included dsa: {dsa.get('id', None)}, distro: {distro}, pkg: {pkg}" + f"release version not included dsa: {dsa.get('id', None)}, distro: {distro}, pkg: {pkg}", ) dsa["fixed_in"].append({"distro": distro, "pkg": pkg, "ver": version}) continue @@ -257,8 +257,7 @@ def _normalize_dsa_list(self): return ns_cve_dsalist - # noqa - def _normalize_json(self, ns_cve_dsalist=None): + def _normalize_json(self, ns_cve_dsalist=None): # noqa: PLR0912,PLR0915 adv_mets = {} # all_matched_dsas = set() # all_dsas = set() @@ -284,7 +283,6 @@ def _normalize_json(self, ns_cve_dsalist=None): for pkg in data: for vid in data[pkg]: - # skip non CVE vids if not re.match("^CVE.*", vid): continue @@ -320,7 +318,6 @@ def _normalize_json(self, ns_cve_dsalist=None): complete = False if complete: - if vid not in vuln_records[relno]: # create a new record vuln_records[relno][vid] = copy.deepcopy(vulnerability.vulnerability_element) @@ -397,7 +394,7 @@ def _normalize_json(self, ns_cve_dsalist=None): "dsa": {"fixed": 0, "notfixed": 0}, "nodsa": {"fixed": 0, "notfixed": 0}, "neither": {"fixed": 0, "notfixed": 0}, - } + }, } if met_sev not in adv_mets[met_ns]: diff --git a/src/vunnel/providers/github/__init__.py b/src/vunnel/providers/github/__init__.py index 84d6c8df..f7327ebc 100644 --- a/src/vunnel/providers/github/__init__.py +++ b/src/vunnel/providers/github/__init__.py @@ -66,7 +66,6 @@ def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int for advisory in self.parser.get(): all_fixes = copy.deepcopy(advisory.get("FixedIn")) if isinstance(advisory.get("FixedIn"), list) else [] for ecosystem in advisory.ecosystems: - advisory["namespace"] = f"{namespace}:{ecosystem}" # filter the list of fixes for this ecosystem diff --git a/src/vunnel/providers/github/parser.py b/src/vunnel/providers/github/parser.py index c9b57963..1d3930c6 100644 --- a/src/vunnel/providers/github/parser.py +++ b/src/vunnel/providers/github/parser.py @@ -38,7 +38,7 @@ class Parser: - def __init__( + def __init__( # noqa: PLR0913 self, workspace, token, @@ -156,7 +156,7 @@ def get(self): # determine if a run was completed by looking for a timestamp metadata = self.db.get_metadata() self.timestamp = metadata.data.get("timestamp") - current_timestamp = f"{datetime.datetime.utcnow().isoformat()}Z" + current_timestamp = f"{datetime.datetime.now(tz=datetime.timezone.utc).isoformat()}Z" has_cursor = True # Process everything that was persisted first @@ -263,10 +263,7 @@ def get_vulnerabilities(token, ghsaId, timestamp, vuln_cursor, parent_cursor): # pagination using the ghsaId vulnerabilities = advisory.get("vulnerabilities", {}) page_info = vulnerabilities.get("pageInfo", {}) - if page_info.get("hasNextPage"): - vuln_cursor = page_info.get("endCursor") - else: - vuln_cursor = None + vuln_cursor = page_info.get("endCursor") if page_info.get("hasNextPage") else None for vulnerability in vulnerabilities.get("nodes", []): nodes.append(vulnerability) @@ -389,13 +386,13 @@ def graphql_advisories(cursor=None, timestamp=None, vuln_cursor=None): if cursor: after = 'after: "%s", ' % cursor - caller = "{query_func}{after}{updatedSince}first: 100)".format(query_func=query_func, after=after, updatedSince=updatedSince) + caller = f"{query_func}{after}{updatedSince}first: 100)" if vuln_cursor: vuln_after = 'after: "%s", ' % vuln_cursor vulnerabilities = "%sfirst: 100, orderBy: {field: UPDATED_AT, direction: ASC}" % vuln_after - query = """ + return """ {{ {} {{ nodes {{ @@ -439,11 +436,9 @@ def graphql_advisories(cursor=None, timestamp=None, vuln_cursor=None): caller, vulnerabilities, ) - return query class NodeParser(dict): - __parsers__ = ("_severity", "_fixedin", "_summary", "_url", "_cves", "_withdrawn") def __init__(self, data, logger=None): @@ -538,7 +533,7 @@ def _fixedin(self): "ecosystem": ecosystem, "namespace": f"github:{ecosystem}", "range": version_range, - } + }, ) else: # Log vuln skipped for unknown ecosystem diff --git a/src/vunnel/providers/nvd/api.py b/src/vunnel/providers/nvd/api.py index f57a635a..a2cee257 100644 --- a/src/vunnel/providers/nvd/api.py +++ b/src/vunnel/providers/nvd/api.py @@ -36,7 +36,6 @@ def cve_history( | None = None, # note: if you specify a changeStartDate, you must also specify a changeEndDate change_end_date: str | datetime.datetime | None = None, # note: maximum date range is 120 days ) -> Generator[dict[str, Any], Any, None]: - parameters = {} if cve_id: @@ -71,7 +70,6 @@ def cve( | None = None, # note: if you specify a pubStartDate, you must also specify a pubEndDate pub_end_date: str | datetime.datetime | None = None, # note: maximum date range is 120 days ) -> Generator[dict[str, Any], Any, None]: - parameters = {} if cve_id: diff --git a/src/vunnel/providers/oracle/__init__.py b/src/vunnel/providers/oracle/__init__.py index fcd39235..7b3e1e67 100644 --- a/src/vunnel/providers/oracle/__init__.py +++ b/src/vunnel/providers/oracle/__init__.py @@ -48,9 +48,7 @@ def name(cls) -> str: return "oracle" def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int]: - with self.results_writer() as writer: - # TODO: tech debt: on subsequent runs, we should only write new vulns (this currently re-writes all) vuln_dict = self.parser.get() diff --git a/src/vunnel/providers/oracle/parser.py b/src/vunnel/providers/oracle/parser.py index b45791e7..f5c98ab3 100644 --- a/src/vunnel/providers/oracle/parser.py +++ b/src/vunnel/providers/oracle/parser.py @@ -92,8 +92,7 @@ def _parse_oval_data(self, path: str, config: dict): # See:https://github.com/anchore/anchore-engine/issues/1237 for details and links. # This approach is the minimally risk since it only impacts this driver and only ksplice-based packages. filterer = KspliceFilterer(logger=self.logger) - filtered_results = filterer.filter(raw_results) - return filtered_results + return filterer.filter(raw_results) def get(self): # download @@ -120,7 +119,7 @@ def _is_ksplice_version(cls, version) -> bool: epoch, version, release = rpm.split_fullversion(version) # noqa return cls.ksplice_regex.match(release) is not None - def filter(self, vuln_dict: dict) -> dict: + def filter(self, vuln_dict: dict) -> dict: # noqa: A003 """ Filters affected packages and ELSAs that are for ksplice packages since the matching logic for these in Grype isn't diff --git a/src/vunnel/providers/rhel/__init__.py b/src/vunnel/providers/rhel/__init__.py index 7085e12e..264d4796 100644 --- a/src/vunnel/providers/rhel/__init__.py +++ b/src/vunnel/providers/rhel/__init__.py @@ -50,7 +50,6 @@ def name(cls) -> str: return "rhel" def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int]: - with self.results_writer() as writer: for namespace, vuln_id, record in self.parser.get(skip_if_exists=self.config.runtime.skip_if_exists): namespace = namespace.lower() diff --git a/src/vunnel/providers/rhel/parser.py b/src/vunnel/providers/rhel/parser.py index 9cb25e4c..3e1871a9 100644 --- a/src/vunnel/providers/rhel/parser.py +++ b/src/vunnel/providers/rhel/parser.py @@ -294,7 +294,6 @@ def _fetch_rhsa(self, rhsa_id, platform): return p def _init_rhsa_data(self, skip_if_exists=False): - # setup workspace directory if not os.path.exists(self.rhsa_dir_path): self.logger.debug(f"creating workspace for rhsa source data at {self.rhsa_dir_path}") diff --git a/src/vunnel/providers/sles/__init__.py b/src/vunnel/providers/sles/__init__.py index 1f7ee0d6..bee4e1b4 100644 --- a/src/vunnel/providers/sles/__init__.py +++ b/src/vunnel/providers/sles/__init__.py @@ -52,7 +52,6 @@ def name(cls) -> str: return "sles" def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int]: - with self.results_writer() as writer: # TODO: tech debt: on subsequent runs, we should only write new vulns (this currently re-writes all) for namespace, vuln_id, record in self.parser.get(): diff --git a/src/vunnel/providers/sles/parser.py b/src/vunnel/providers/sles/parser.py index 018bef83..f745ddc7 100644 --- a/src/vunnel/providers/sles/parser.py +++ b/src/vunnel/providers/sles/parser.py @@ -215,7 +215,6 @@ def _release_resolver( @classmethod def _transform_oval_vulnerabilities(cls, major_version: str, parsed_dict: dict) -> list[Vulnerability]: - cls.logger.info( "generating normalized vulnerabilities from oval vulnerabilities for %s", major_version, @@ -244,7 +243,10 @@ def _transform_oval_vulnerabilities(cls, major_version: str, parsed_dict: dict) # process impact item, each impact translates to a normalized vulnerability for impact_item in vulnerability_obj.impact: # get the release and version - (release_name, release_version,) = cls._get_name_and_version_from_test( + ( + release_name, + release_version, + ) = cls._get_name_and_version_from_test( impact_item.namespace_test_id, tests_dict, artifacts_dict, @@ -355,7 +357,6 @@ def get(self): @dataclass class SLESOVALVulnerability(Parsed): - name: str severity: str description: str @@ -370,7 +371,6 @@ class SLESVulnerabilityParser(VulnerabilityParser): @classmethod def parse(cls, xml_element, config: OVALParserConfig) -> SLESOVALVulnerability | None: - identity = name = severity = description = link = None impact = cvss = [] try: diff --git a/src/vunnel/providers/ubuntu/__init__.py b/src/vunnel/providers/ubuntu/__init__.py index e1bc6b6a..787faec4 100644 --- a/src/vunnel/providers/ubuntu/__init__.py +++ b/src/vunnel/providers/ubuntu/__init__.py @@ -53,7 +53,6 @@ def name(cls) -> str: return "ubuntu" def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int]: - with self.results_writer() as writer: for namespace, vuln_id, record in self.parser.get(skip_if_exists=self.config.runtime.skip_if_exists): namespace = namespace.lower() diff --git a/src/vunnel/providers/ubuntu/parser.py b/src/vunnel/providers/ubuntu/parser.py index 9b4a18b1..ff6cdab7 100644 --- a/src/vunnel/providers/ubuntu/parser.py +++ b/src/vunnel/providers/ubuntu/parser.py @@ -797,7 +797,11 @@ def _reprocess_merged_cve(self, cve_id: str, cve_rel_path: str): if to_be_merged_map: if self.enable_rev_history: self.logger.debug("attempting to resolve patches using revision history for {}".format(cve_rel_path)) - (resolved_patches, pending_dpt_list, cve_latest_rev,) = self._resolve_patches_using_history( + ( + resolved_patches, + pending_dpt_list, + cve_latest_rev, + ) = self._resolve_patches_using_history( cve_id=cve_id, cve_rel_path=cve_rel_path, to_be_merged_dpt_list=list(to_be_merged_map.keys()), @@ -889,7 +893,11 @@ def _merge_cve(self, cve_id: str, cve_rel_path: str, cve_abs_path: str, repo_cur saved_cve = self._load_merged_cve(cve_id) if self.enable_rev_history: - (resolved_patches, pending_dpt_list, cve_latest_rev,) = self._resolve_patches_using_history( + ( + resolved_patches, + pending_dpt_list, + cve_latest_rev, + ) = self._resolve_patches_using_history( cve_id=cve_id, cve_rel_path=cve_rel_path, to_be_merged_dpt_list=list(to_be_merged_map.keys()), diff --git a/src/vunnel/providers/wolfi/__init__.py b/src/vunnel/providers/wolfi/__init__.py index 0405e503..68f03481 100644 --- a/src/vunnel/providers/wolfi/__init__.py +++ b/src/vunnel/providers/wolfi/__init__.py @@ -47,7 +47,6 @@ def name(cls) -> str: return "wolfi" def update(self, last_updated: datetime.datetime | None) -> tuple[list[str], int]: - with self.results_writer() as writer: # TODO: tech debt: on subsequent runs, we should only write new vulns (this currently re-writes all) for release, vuln_dict in self.parser.get(): diff --git a/src/vunnel/utils/fdb.py b/src/vunnel/utils/fdb.py index 0e50d94a..7c05dc1f 100644 --- a/src/vunnel/utils/fdb.py +++ b/src/vunnel/utils/fdb.py @@ -30,12 +30,13 @@ def get(self, name): how a `.get()` would work in a dictionary. """ if not name.endswith(self.serializer.ext): - name = "{}{}".format(name, self.serializer.ext) + name = f"{name}{self.serializer.ext}" if self.files == []: self._update_file_cache() if name in self.files: path = os.path.join(self.directory_path, name) return self.serializer(path) + return None def create(self, name): """ @@ -44,7 +45,7 @@ def create(self, name): the file exists or not, the serializer should be able to write to it. """ if not name.endswith(self.serializer.ext): - name = "{}{}".format(name, self.serializer.ext) + name = f"{name}{self.serializer.ext}" path = os.path.join(self.directory_path, name) return self.serializer(path) @@ -81,7 +82,6 @@ def get_metadata(self): class JSONSerializer: - ext = ".json" def __init__(self, path): @@ -106,7 +106,6 @@ def commit(self, data=None): class RawSerializer: - ext = ".txt" def __init__(self, path): diff --git a/src/vunnel/utils/vulnerability.py b/src/vunnel/utils/vulnerability.py index 3a14f43e..b957c620 100644 --- a/src/vunnel/utils/vulnerability.py +++ b/src/vunnel/utils/vulnerability.py @@ -24,7 +24,7 @@ "Metadata": {}, "Name": None, "CVSS": [], - } + }, } diff --git a/tests/conftest.py b/tests/conftest.py index 9a1deae4..e00aa38f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -24,7 +24,7 @@ def results_dir(self): def result_files(self): results = [] - for root, dirs, files in os.walk(self.results_dir): + for root, _dirs, files in os.walk(self.results_dir): for filename in files: results.append(os.path.join(root, filename)) return results @@ -96,7 +96,7 @@ def provider_workspace_helper(self, name: str, create=True) -> WorkspaceHelper: return WorkspaceHelper(root, name) -@pytest.fixture +@pytest.fixture() def helpers(request, tmpdir): """ Returns a common set of helper functions for tests. @@ -113,7 +113,7 @@ def git_root() -> str: ) -@pytest.fixture +@pytest.fixture() def dummy_file(): def apply(d: str, name: str = ""): if name == "": diff --git a/tests/quality/Makefile b/tests/quality/Makefile index 62a14954..d4b1b291 100644 --- a/tests/quality/Makefile +++ b/tests/quality/Makefile @@ -77,7 +77,7 @@ $(YARDSTICK_RESULT_DIR): .PHONY: update-labels update-labels: ## Update vulnerability-match-labels submodule to grab the latest labels on the main branch - git submodule update vulnerability-match-labels + git submodule update --remote vulnerability-match-labels ## Cleanup targets ################################# diff --git a/tests/quality/config.yaml b/tests/quality/config.yaml index c25f2280..a63a0250 100644 --- a/tests/quality/config.yaml +++ b/tests/quality/config.yaml @@ -3,9 +3,10 @@ yardstick: tools: - name: syft - # note: don't change this version, this is really controlled by the configuration in anchore/vulnerability-match-labels - version: v0.68.1 + # note: if there is ever a problem with the syft version, it can be pinned explicitly here (instead of "latest") + version: latest produces: SBOM + refresh: false - name: grype label: custom-db @@ -66,6 +67,18 @@ tests: - docker.io/debian:7@sha256:81e88820a7759038ffa61cff59dfcc12d3772c3a2e75b7cfe963c952da2ad264 - provider: github + additional_providers: + # we need to convert GHSAs to CVEs so that we can filter based on date + - name: nvd + use_cache: true + # note: the base images for most of the test images are alpine and we are including the NVD namespace. The alpine + # matcher in grype is unique in the sense that it searches the NVD namespace first for results and filters + # out any fixes found in the alpine namespace. For this reason it is important to keep alpine and alpine-adjacent + # namespaces (e.g. wolfi) when building the grype database. + - name: alpine + use_cache: true + - name: wolfi + use_cache: true images: - docker.io/anchore/test_images:java-56d52bc@sha256:10008791acbc5866de04108746a02a0c4029ce3a4400a9b3dad45d7f2245f9da - docker.io/anchore/test_images:npm-56d52bc@sha256:ba42ded8613fc643d407a050faf5ab48cfb405ad3ef2015bf6feeb5dff44738d @@ -87,7 +100,11 @@ tests: - docker.io/anchore/test_images:appstreams-oraclelinux-8-1a287dd@sha256:c8d664b0e728d52f57eeb98ed1899c16d3b265f02ddfb41303d7a16c31e0b0f1 - provider: rhel + # ideally we would not use cache, however, the ubuntu provider is currently very expensive to run. + # This will still test incremental updates relative to the nightly cache that is populated. + use_cache: true additional-trigger-globs: + # this provider imports and uses the centos provider code - src/vunnel/providers/centos/** images: - registry.access.redhat.com/ubi8@sha256:68fecea0d255ee253acbf0c860eaebb7017ef5ef007c25bee9eeffd29ce85b29 @@ -105,6 +122,9 @@ tests: # - - provider: ubuntu + # ideally we would not use cache, however, the ubuntu provider is currently very expensive to run. + # This will still test incremental updates relative to the nightly cache that is populated. + use_cache: true images: - docker.io/ubuntu:16.10@sha256:8dc9652808dc091400d7d5983949043a9f9c7132b15c14814275d25f94bca18a diff --git a/tests/quality/configure.py b/tests/quality/configure.py index f78b72bc..6766f7b9 100644 --- a/tests/quality/configure.py +++ b/tests/quality/configure.py @@ -1,31 +1,30 @@ -import logging -import shutil -import subprocess -import os +from __future__ import annotations +import dataclasses +import enum +import fnmatch import glob import json -import fnmatch -import enum -import dataclasses -import requests +import logging +import os import shlex +import shutil +import subprocess import sys from dataclasses import dataclass, field -from typing import Optional, Dict, Any +from typing import Any import click import mergedeep +import requests import yaml -from dataclass_wizard import asdict, fromdict, DumpMeta - +from dataclass_wizard import DumpMeta, asdict, fromdict from yardstick.cli.config import ( Application, - Tool, - ScanMatrix, ResultSet, + ScanMatrix, + Tool, ) - BIN_DIR = "./bin" CLONE_DIR = f"{BIN_DIR}/grype-db-src" GRYPE_DB = f"{BIN_DIR}/grype-db" @@ -52,6 +51,7 @@ class AdditionalProvider: @dataclass class Test: provider: str + use_cache: bool = False images: list[str] = field(default_factory=list) additional_providers: list[AdditionalProvider] = field(default_factory=list) additional_trigger_globs: list[str] = field(default_factory=list) @@ -106,11 +106,11 @@ def yardstick_application_config(self, test_configurations: list[Test]) -> Appli images=images, tools=self.yardstick.tools, ), - ) + ), }, ) - def test_configuration_by_provider(self, provider: str) -> Optional[Test]: + def test_configuration_by_provider(self, provider: str) -> Test | None: for test in self.tests: if test.provider == provider: return test @@ -129,7 +129,10 @@ def provider_data_source(self, providers: list[str]) -> tuple[list[str], list[st tests.append(test) - uncached_providers.append(test.provider) + if test.use_cache: + cached_providers.append(test.provider) + else: + uncached_providers.append(test.provider) if test.additional_providers: for additional_provider in test.additional_providers: if additional_provider.use_cache: @@ -185,7 +188,7 @@ def cli(ctx, verbose: bool, config_path: str): "level": log_level, }, }, - } + }, ) @@ -258,7 +261,7 @@ def read_config_state(path: str = ".state.yaml"): logging.info(f"reading config state from {path!r}") try: - with open(path, "r") as f: + with open(path) as f: return fromdict(ConfigurationState, yaml.safe_load(f.read())) except FileNotFoundError: return ConfigurationState() @@ -283,7 +286,7 @@ def write_grype_db_config(providers: set[str], path: str = ".grype-db.yaml"): root: ./data configs: """ - + "\n".join([f" - name: {provider}" for provider in providers]) + + "\n".join([f" - name: {provider}" for provider in providers]), ) @@ -337,7 +340,7 @@ def select_providers(cfg: Config, output_json: bool): selected_providers.add(test.provider) break - sorted_providers = sorted(list(selected_providers)) + sorted_providers = sorted(selected_providers) if output_json: print(json.dumps(sorted_providers)) @@ -350,8 +353,8 @@ def select_providers(cfg: Config, output_json: bool): @click.option("--json", "-j", "output_json", help="output result as json list (useful for CI)", is_flag=True) @click.pass_obj def all_providers(cfg: Config, output_json: bool): - selected_providers = set(test.provider for test in cfg.tests) - sorted_providers = sorted(list(selected_providers)) + selected_providers = {test.provider for test in cfg.tests} + sorted_providers = sorted(selected_providers) if output_json: print(json.dumps(sorted_providers)) @@ -450,16 +453,13 @@ def _install(version: str): logging.info(f"grype-db version derived from git is {version!r}") - if version.startswith("v"): - if os.path.exists(GRYPE_DB): - grype_db_version = ( - subprocess.check_output([f"{BIN_DIR}/grype-db", "--version"]).decode("utf-8").strip().split(" ")[-1] - ) - if grype_db_version == version: - logging.info(f"grype-db already installed at version {version!r}") - return - else: - logging.info(f"updating grype-db from version {grype_db_version!r} to {version!r}") + if version.startswith("v") and os.path.exists(GRYPE_DB): + grype_db_version = subprocess.check_output([f"{BIN_DIR}/grype-db", "--version"]).decode("utf-8").strip().split(" ")[-1] + if grype_db_version == version: + logging.info(f"grype-db already installed at version {version!r}") + return + else: + logging.info(f"updating grype-db from version {grype_db_version!r} to {version!r}") logging.info(f"installing grype-db at version {version!r}") @@ -478,7 +478,7 @@ def build_db(cfg: Config): state = read_config_state() if not state.cached_providers and not state.uncached_providers: - logging.error(f"no providers configured") + logging.error("no providers configured") return logging.info(f"preparing data directory for uncached={state.uncached_providers!r} cached={state.cached_providers!r}") @@ -489,7 +489,7 @@ def build_db(cfg: Config): db_archive = f"{build_dir}/grype-db.tar.gz" # clear data directory - logging.info(f"clearing existing data") + logging.info("clearing existing data") shutil.rmtree(data_dir, ignore_errors=True) shutil.rmtree(build_dir, ignore_errors=True) diff --git a/tests/quality/gate.py b/tests/quality/gate.py index b4e07c14..b9a01e2e 100755 --- a/tests/quality/gate.py +++ b/tests/quality/gate.py @@ -1,16 +1,15 @@ #!/usr/bin/env python3 +from __future__ import annotations import re import sys -from typing import Optional, Any +from dataclasses import InitVar, dataclass, field +from typing import Any import click -from tabulate import tabulate -from dataclasses import dataclass, InitVar, field - import yardstick -from yardstick import store, comparison, artifact, utils -from yardstick.cli import display, config - +from tabulate import tabulate +from yardstick import artifact, comparison, store, utils +from yardstick.cli import config, display # see the .yardstick.yaml configuration for details default_result_set = "pr_vs_latest_via_sbom" @@ -19,15 +18,15 @@ @dataclass class Gate: - label_comparisons: InitVar[Optional[list[comparison.AgainstLabels]]] - label_comparison_stats: InitVar[Optional[comparison.ImageToolLabelStats]] + label_comparisons: InitVar[list[comparison.AgainstLabels] | None] + label_comparison_stats: InitVar[comparison.ImageToolLabelStats | None] reasons: list[str] = field(default_factory=list) def __post_init__( self, - label_comparisons: Optional[list[comparison.AgainstLabels]], - label_comparison_stats: Optional[comparison.ImageToolLabelStats], + label_comparisons: list[comparison.AgainstLabels] | None, + label_comparison_stats: comparison.ImageToolLabelStats | None, ): if not label_comparisons and not label_comparison_stats: return @@ -44,30 +43,33 @@ def __post_init__( } current_comparisons_by_image = {comp.config.image: comp for comp in label_comparisons if comp.config.tool == current_tool} - for image, comp in latest_release_comparisons_by_image.items(): - if comp.summary.indeterminate_percent > 10: - reasons.append( - f"latest indeterminate matches % is greater than 10%: {bcolors.BOLD+bcolors.UNDERLINE}current={comp.summary.indeterminate_percent:0.2f}%{bcolors.RESET} image={image}" - ) + # this doesn't make sense in all cases, especially if we aren't failing any other gates against the current changes + # we might want this in the future to protect against no labels for images in an edge case, but that reason is not + # currently apparent + # for image, comp in latest_release_comparisons_by_image.items(): + # if comp.summary.indeterminate_percent > 10: + # reasons.append( + # f"latest indeterminate matches % is greater than 10%: {bcolors.BOLD+bcolors.UNDERLINE}current={comp.summary.indeterminate_percent:0.2f}%{bcolors.RESET} image={image}", + # ) for image, comp in current_comparisons_by_image.items(): latest_f1_score = latest_release_comparisons_by_image[image].summary.f1_score current_f1_score = comp.summary.f1_score if current_f1_score < latest_f1_score: reasons.append( - f"current F1 score is lower than the latest release F1 score: {bcolors.BOLD+bcolors.UNDERLINE}current={current_f1_score:0.2f} latest={latest_f1_score:0.2f}{bcolors.RESET} image={image}" + f"current F1 score is lower than the latest release F1 score: {bcolors.BOLD+bcolors.UNDERLINE}current={current_f1_score:0.2f} latest={latest_f1_score:0.2f}{bcolors.RESET} image={image}", ) if comp.summary.indeterminate_percent > 10: reasons.append( - f"current indeterminate matches % is greater than 10%: {bcolors.BOLD+bcolors.UNDERLINE}current={comp.summary.indeterminate_percent:0.2f}%{bcolors.RESET} image={image}" + f"current indeterminate matches % is greater than 10%: {bcolors.BOLD+bcolors.UNDERLINE}current={comp.summary.indeterminate_percent:0.2f}%{bcolors.RESET} image={image}", ) latest_fns = latest_release_comparisons_by_image[image].summary.false_negatives current_fns = comp.summary.false_negatives if current_fns > latest_fns: reasons.append( - f"current false negatives is greater than the latest release false negatives: {bcolors.BOLD+bcolors.UNDERLINE}current={current_fns} latest={latest_fns}{bcolors.RESET} image={image}" + f"current false negatives is greater than the latest release false negatives: {bcolors.BOLD+bcolors.UNDERLINE}current={current_fns} latest={latest_fns}{bcolors.RESET} image={image}", ) self.reasons = reasons @@ -110,7 +112,7 @@ class bcolors: def show_results_used(results: list[artifact.ScanResult]): - print(f" Results used:") + print(" Results used:") for idx, result in enumerate(results): branch = "├──" if idx == len(results) - 1: @@ -136,9 +138,9 @@ def validate( images: list[str], always_run_label_comparison: bool, verbosity: int, - label_entries: Optional[list[artifact.LabelEntry]] = None, + label_entries: list[artifact.LabelEntry] | None = None, ): - print(f"{bcolors.HEADER}{bcolors.BOLD}Validating with {result_set!r}", bcolors.RESET) + print(f"{bcolors.HEADER}{bcolors.BOLD}Validating with {result_set!r}", bcolors.RESET, "\n") result_set_obj = store.result_set.load(name=result_set) namespaces = get_namespaces_from_db() @@ -146,6 +148,10 @@ def validate( for namespace in namespaces: print(f" - {namespace}") + print() + print(f"{bcolors.HEADER}{bcolors.BOLD}Configuration:", bcolors.RESET) + print(" max year limit:", cfg.default_max_year) + ret = [] for image, result_states in result_set_obj.result_state_by_image.items(): if images and image not in images: @@ -195,7 +201,7 @@ def validate_image( always_run_label_comparison: bool, verbosity: int, namespaces: list[str], - label_entries: Optional[list[artifact.LabelEntry]] = None, + label_entries: list[artifact.LabelEntry] | None = None, ): def matches_filter(matches): return matches_filter_by_namespaces(matches, namespaces) @@ -220,7 +226,7 @@ def matches_filter(matches): # bail if there are no differences found if not always_run_label_comparison and not sum( - [len(relative_comparison.unique[result.ID]) for result in relative_comparison.results] + [len(relative_comparison.unique[result.ID]) for result in relative_comparison.results], ): print("no differences found between tool results") return Gate(None, None) @@ -290,7 +296,7 @@ def matches_filter(matches): f"{color}{unique_match.vulnerability.id}{bcolors.RESET}", f"{color}{label}{bcolors.RESET}", f"{commentary}", - ] + ], ) def escape_ansi(line): @@ -307,9 +313,10 @@ def escape_ansi(line): print( indent + tabulate( - [["TOOL PARTITION", "PACKAGE", "VULNERABILITY", "LABEL", "COMMENTARY"]] + all_rows, tablefmt="plain" + [["TOOL PARTITION", "PACKAGE", "VULNERABILITY", "LABEL", "COMMENTARY"], *all_rows], + tablefmt="plain", ).replace("\n", "\n" + indent) - + "\n" + + "\n", ) # populate the quality gate with data that can evaluate pass/fail conditions @@ -344,7 +351,7 @@ def main(images: list[str], always_run_label_comparison: bool, breakdown_by_ecos result_set_obj = store.result_set.load(name=result_set) for state in result_set_obj.state: images.add(state.config.image) - images = sorted(list(images)) + images = sorted(images) print("Loading label entries...", end=" ") label_entries = store.labels.load_for_image(images, year_max_limit=cfg.default_max_year) @@ -361,14 +368,16 @@ def main(images: list[str], always_run_label_comparison: bool, breakdown_by_ecos always_run_label_comparison=always_run_label_comparison, verbosity=verbosity, label_entries=label_entries, - ) + ), ) print() if breakdown_by_ecosystem: print(f"{bcolors.HEADER}Breaking down label comparison by ecosystem performance...", bcolors.RESET) results_by_image, label_entries, stats = yardstick.compare_results_against_labels_by_ecosystem( - result_set=result_set, year_max_limit=cfg.default_max_year, label_entries=label_entries + result_set=result_set, + year_max_limit=cfg.default_max_year, + label_entries=label_entries, ) display.labels_by_ecosystem_comparison( results_by_image, @@ -377,7 +386,7 @@ def main(images: list[str], always_run_label_comparison: bool, breakdown_by_ecos ) print() - failure = not all([gate.passed() for gate in gates]) + failure = not all(gate.passed() for gate in gates) if failure: print("Reasons for quality gate failure:") for gate in gates: @@ -427,7 +436,7 @@ def setup_logging(verbosity: int): "level": log_level, }, }, - } + }, ) diff --git a/tests/quality/vulnerability-match-labels b/tests/quality/vulnerability-match-labels index 92e24d05..a240fd62 160000 --- a/tests/quality/vulnerability-match-labels +++ b/tests/quality/vulnerability-match-labels @@ -1 +1 @@ -Subproject commit 92e24d0559915b55c18a4f97013a50d82564a5ac +Subproject commit a240fd62a222f18c98762ca2820838057d4bc25d diff --git a/tests/unit/cli/test_cli.py b/tests/unit/cli/test_cli.py index 01945ed8..8414093c 100644 --- a/tests/unit/cli/test_cli.py +++ b/tests/unit/cli/test_cli.py @@ -5,7 +5,6 @@ import pytest from click.testing import CliRunner - from vunnel import cli, provider, providers, result from vunnel.providers import nvd @@ -79,12 +78,12 @@ def test_run(mocker, monkeypatch) -> None: request_timeout=125, api_key="secret", ), - ) + ), ] @pytest.mark.parametrize( - "args, clear, clear_input, clear_results", + ("args", "clear", "clear_input", "clear_results"), ( (["wolfi"], 1, 0, 0), (["wolfi", "-i"], 0, 1, 0), @@ -97,7 +96,7 @@ def test_clear(mocker, monkeypatch, args, clear, clear_input, clear_results) -> mocker.patch.object(providers, "create", create_mock) runner = CliRunner() - res = runner.invoke(cli.cli, ["clear"] + args) + res = runner.invoke(cli.cli, ["clear", *args]) assert res.exit_code == 0 assert workspace_mock.clear.call_count == clear assert workspace_mock.clear_input.call_count == clear_input diff --git a/tests/unit/providers/alpine/test_alpine.py b/tests/unit/providers/alpine/test_alpine.py index b0555723..c577b223 100644 --- a/tests/unit/providers/alpine/test_alpine.py +++ b/tests/unit/providers/alpine/test_alpine.py @@ -4,14 +4,13 @@ import shutil import pytest - from vunnel import result, workspace from vunnel.providers.alpine import Config, Provider, parser from vunnel.providers.alpine.parser import Parser, SecdbLandingParser class TestAlpineProvider: - @pytest.fixture + @pytest.fixture() def mock_raw_data(self): """ Returns stringified version of the following yaml @@ -53,7 +52,7 @@ def mock_raw_data(self): return "apkurl: '{{urlprefix}}/{{distroversion}}/{{reponame}}/{{arch}}/{{pkg.name}}-{{pkg.ver}}.apk'\narchs:\n- x86_64\n- x86\n- armhf\ndistroversion: v0.0\npackages:\n- pkg:\n name: apache2\n secfixes:\n 2.4.26-r0:\n - CVE-2017-3167\n - CVE-2017-3169\n - CVE-2017-7659\n - CVE-2017-7668\n - CVE-2017-7679\n 2.4.27-r0:\n - CVE-2017-9789\n 2.4.27-r1:\n - CVE-2017-9798\n- pkg:\n name: augeas\n secfixes:\n 1.4.0-r5:\n - CVE-2017-7555\n- pkg:\n name: bash\n secfixes:\n 4.3.42-r5:\n - CVE-2016-9401\nreponame: main\nurlprefix: http://dl-cdn.alpinelinux.org/alpine\n" # noqa: E501 - @pytest.fixture + @pytest.fixture() def mock_parsed_data(self): """ Returns the parsed output generated by AlpineDataProvider._load() for the mock_raw_data @@ -81,29 +80,29 @@ def mock_parsed_data(self): "2.4.27-r0": ["CVE-2017-9789"], "2.4.27-r1": ["CVE-2017-9798"], }, - } + }, }, { "pkg": { "name": "augeas", "secfixes": {"1.4.0-r5": ["CVE-2017-7555"]}, - } + }, }, { "pkg": { "name": "bash", "secfixes": {"4.3.42-r5": ["CVE-2016-9401"]}, - } + }, }, ], "reponame": "main", "urlprefix": "http://dl-cdn.alpinelinux.org/alpine", - } + }, } return release, dbtype_data_dict @pytest.mark.parametrize( - "release,expected", + ("release", "expected"), [ ("v3.3", True), ("3.4", False), @@ -126,7 +125,7 @@ def test_load(self, mock_raw_data, tmpdir): counter = 0 for release, dbtype_data_dict in p._load(): counter += 1 - print("got secdb data for release {}, db types: {}".format(release, list(dbtype_data_dict.keys()))) + print(f"got secdb data for release {release}, db types: {list(dbtype_data_dict.keys())}") assert release == "0.0" assert isinstance(dbtype_data_dict, dict) assert list(dbtype_data_dict.keys()) == ["main"] @@ -141,8 +140,8 @@ def test_normalize(self, mock_parsed_data, tmpdir): vuln_records = p._normalize(release, dbtype_data_dict) assert len(vuln_records) > 0 - assert all(map(lambda x: "Vulnerability" in x, vuln_records.values())) - assert sorted(list(vuln_records.keys())) == sorted( + assert all("Vulnerability" in x for x in vuln_records.values()) + assert sorted(vuln_records.keys()) == sorted( [ "CVE-2017-3167", "CVE-2017-3169", @@ -153,11 +152,11 @@ def test_normalize(self, mock_parsed_data, tmpdir): "CVE-2017-9798", "CVE-2017-7555", "CVE-2016-9401", - ] + ], ) @pytest.mark.parametrize( - "content,expected", + ("content", "expected"), [ pytest.param( '\r\nIndex of /\r\n\r\n

Index of /


../\r\nv3.10/ 11-Jun-2020 20:17 -\r\nv3.11/ 11-Jun-2020 18:12 -\r\n

\r\n\r\n', # noqa: E501 @@ -175,7 +174,7 @@ def test_secdb_landing_parser(self, content, expected): assert sorted(parser.links) == sorted(expected) @pytest.mark.parametrize( - "content,expected", + ("content", "expected"), [ pytest.param( '\r\nIndex of /\r\n\r\n

Index of /


../\r\nv3.10/ 11-Jun-2020 20:17 -\r\nv3.11/ 11-Jun-2020 18:12 -\r\n

\r\n\r\n', # noqa: E501 @@ -191,7 +190,7 @@ def test_link_finder_regex(self, content, expected): assert Parser._link_finder_regex_.findall(content) == expected -@pytest.fixture +@pytest.fixture() def disable_get_requests(monkeypatch): def disabled(*args, **kwargs): raise RuntimeError("requests disabled but HTTP GET attempted") @@ -219,5 +218,5 @@ def mock_download(): p.update(None) - assert 16 == workspace.num_result_entries() + assert workspace.num_result_entries() == 16 assert workspace.result_schemas_valid(require_entries=True) diff --git a/tests/unit/providers/amazon/test_amazon.py b/tests/unit/providers/amazon/test_amazon.py index 4fa93b5a..e5ea7616 100644 --- a/tests/unit/providers/amazon/test_amazon.py +++ b/tests/unit/providers/amazon/test_amazon.py @@ -3,7 +3,6 @@ import shutil import pytest - from vunnel import result, workspace from vunnel.providers.amazon import Config, Provider, parser @@ -21,7 +20,7 @@ def test_rss_parsing(self, tmpdir, helpers): assert isinstance(alas, parser.AlasSummary) assert alas.id is not None assert alas.url is not None - assert alas.sev.lower() in parser.severity_map.keys() + assert alas.sev.lower() in parser.severity_map # TODO: beef up these assertions (should cover the full data shape) @@ -76,7 +75,7 @@ def test_get_pkg_name_version(self): assert a == b -@pytest.fixture +@pytest.fixture() def disable_get_requests(monkeypatch): def disabled(*args, **kwargs): raise RuntimeError("requests disabled but HTTP GET attempted") diff --git a/tests/unit/providers/centos/test_centos.py b/tests/unit/providers/centos/test_centos.py index 981a55de..cf0fc816 100644 --- a/tests/unit/providers/centos/test_centos.py +++ b/tests/unit/providers/centos/test_centos.py @@ -3,14 +3,13 @@ import shutil import pytest - from vunnel import result, workspace from vunnel.providers import centos from vunnel.providers.centos import Parser, parser @pytest.mark.parametrize( - "mock_data_path,full_entry", + ("mock_data_path", "full_entry"), [ ( "test-fixtures/centos-3-entry", @@ -37,7 +36,7 @@ }, "Name": "RHSA-0000-0000", "CVSS": [], - } + }, }, ), ( @@ -53,7 +52,7 @@ "VersionFormat": "rpm", "NamespaceName": "centos:7", "Module": None, - } + }, ], "Link": "https://access.redhat.com/errata/RHSA-0000-0000", "Description": "", @@ -65,7 +64,7 @@ }, "Name": "RHSA-0000-0000", "CVSS": [], - } + }, }, ), ], @@ -83,7 +82,7 @@ def test_parser(tmpdir, helpers, mock_data_path, full_entry): assert vuln == full_entry -@pytest.fixture +@pytest.fixture() def disable_get_requests(monkeypatch): def disabled(*args, **kwargs): raise RuntimeError("requests disabled but HTTP GET attempted") @@ -92,7 +91,7 @@ def disabled(*args, **kwargs): @pytest.mark.parametrize( - "mock_data_path,expected_written_entries", + ("mock_data_path", "expected_written_entries"), [ ("test-fixtures/centos-3-entry", 0), ("test-fixtures/centos-7-entry", 1), diff --git a/tests/unit/providers/debian/test_debian.py b/tests/unit/providers/debian/test_debian.py index b87d7c49..cdbea03e 100644 --- a/tests/unit/providers/debian/test_debian.py +++ b/tests/unit/providers/debian/test_debian.py @@ -3,12 +3,11 @@ import shutil import pytest - from vunnel import result, workspace from vunnel.providers.debian import Config, Provider, parser -@pytest.fixture +@pytest.fixture() def disable_get_requests(monkeypatch): def disabled(*args, **kwargs): raise RuntimeError("requests disabled but HTTP GET attempted") @@ -31,15 +30,10 @@ def test_normalize_dsa_list(self, tmpdir, helpers, disable_get_requests): assert len(ns_cve_dsalist) > 0 - for ns, cve_dsalist in ns_cve_dsalist.items(): + for _ns, cve_dsalist in ns_cve_dsalist.items(): assert isinstance(cve_dsalist, dict) assert len(cve_dsalist) > 0 - assert all( - map( - lambda x: isinstance(x, list) and len(x) > 0, - cve_dsalist.values(), - ) - ) + assert all(isinstance(x, list) and len(x) > 0 for x in cve_dsalist.values()) # print("Number of CVEs in {}: {}".format(ns, len(cve_dsalist))) # more_dsas = {x: y for x, y in cve_dsalist.items() if len(y) > 1} @@ -63,12 +57,12 @@ def test_get_dsa_map(self, tmpdir, helpers, disable_get_requests): weird_dsas = [dsa for dsa in no_cves if not dsa["fixed_in"]] # print("") # print("Number of DSAs with neither fixes nor CVEs: {}".format(len(weird_dsas))) - assert 3 == len(weird_dsas) + assert len(weird_dsas) == 3 no_cve_dsas = [dsa for dsa in no_cves if dsa["fixed_in"]] # print("") # print("Number of DSAs with fixes and no CVEs: {}".format(len(no_cve_dsas))) - assert 1 == len(no_cve_dsas) + assert len(no_cve_dsas) == 1 def test_normalize_json(self, tmpdir, helpers, disable_get_requests): subject = parser.Parser(workspace=workspace.Workspace(tmpdir, "test", create=True)) @@ -84,10 +78,10 @@ def test_normalize_json(self, tmpdir, helpers, disable_get_requests): assert isinstance(vuln_records, dict) assert len(vuln_records) > 0 - for rel, vuln_dict in vuln_records.items(): + for _rel, vuln_dict in vuln_records.items(): assert isinstance(vuln_dict, dict) assert len(vuln_dict) > 0 - assert all(map(lambda x: "Vulnerability" in x, vuln_dict.values())) + assert all("Vulnerability" in x for x in vuln_dict.values()) assert all(x.get("Vulnerability", {}).get("Name") for x in vuln_dict.values()) @@ -115,5 +109,5 @@ def mock_download(): p.update(None) - assert 21 == workspace.num_result_entries() + assert workspace.num_result_entries() == 21 assert workspace.result_schemas_valid(require_entries=True) diff --git a/tests/unit/providers/github/test_github.py b/tests/unit/providers/github/test_github.py index ed60ff29..626371ef 100644 --- a/tests/unit/providers/github/test_github.py +++ b/tests/unit/providers/github/test_github.py @@ -1,13 +1,12 @@ from __future__ import annotations import pytest - -from vunnel import provider, result, workspace +from vunnel import result, workspace from vunnel.providers.github import Config, Provider, parser from vunnel.utils import fdb as db -@pytest.fixture +@pytest.fixture() def advisory(): def apply(has_next_page=False): return { @@ -27,15 +26,15 @@ def apply(has_next_page=False): "vulnerableVersionRange": ">= 1.2.0, < 4.0.0", }, ], - } - } - } + }, + }, + }, } return apply -@pytest.fixture +@pytest.fixture() def advisories(): def apply(has_next_page=False, vuln_has_next_page=False): return { @@ -81,7 +80,7 @@ def apply(has_next_page=False, vuln_has_next_page=False): ], }, "withdrawnAt": None, - } + }, ], "pageInfo": { "endCursor": "Y3Vyc29yOnYyOpK5MjAxOS0wMi0yMFQyMjoyOToxNi0wODowMM0D7w==", @@ -89,14 +88,14 @@ def apply(has_next_page=False, vuln_has_next_page=False): "hasPreviousPage": False, "startCursor": "Y3Vyc29yOnYyOpK5MjAxOS0wMi0yMFQyMjoyOToxNi0wODowMM0D7w==", }, - } - } + }, + }, } return apply -@pytest.fixture +@pytest.fixture() def empty_response(): return { "data": { @@ -108,12 +107,12 @@ def empty_response(): "hasPreviousPage": False, "startCursor": None, }, - } - } + }, + }, } -@pytest.fixture +@pytest.fixture() def node(): return { "ghsaId": "GHSA-73m2-3pwg-5fgc", @@ -152,7 +151,7 @@ def node(): } -@pytest.fixture +@pytest.fixture() def fake_get_query(monkeypatch): def apply(return_values): responses = Capture(return_values=return_values) @@ -289,6 +288,7 @@ def __call__(self, *a, **kw): return self.always_returns if self.return_values: return self.return_values.pop() + return None class TestGetNestedVulnerabilities: @@ -330,14 +330,14 @@ class TestParser: def test_get_with_no_cursor_no_timestamp(self, fake_get_query, tmpdir, empty_response): fake_get_query([empty_response]) p = parser.Parser(workspace.Workspace(root=tmpdir.strpath, name="test", create=True), "secret") - result = [i for i in p.get()] + result = list(p.get()) assert result == [] def test_get_commits_timestamp(self, fake_get_query, tmpdir, empty_response): fake_get_query([empty_response]) ws = workspace.Workspace(root=tmpdir.strpath, name="test", create=True) p = parser.Parser(ws, "secret") - for i in p.get(): + for _i in p.get(): pass database = db.connection(ws.input_path) metadata = database.get_metadata() @@ -349,7 +349,7 @@ def test_get_commits_timestamp_with_cursors(self, advisories, fake_get_query, tm fake_get_query([empty_response, advisories(has_next_page=True)]) ws = workspace.Workspace(root=tmpdir.strpath, name="test", create=True) p = parser.Parser(ws, "secret") - for i in p.get(): + for _i in p.get(): pass database = db.connection(ws.input_path) metadata = database.get_metadata() @@ -360,13 +360,13 @@ def test_get_commits_timestamp_with_cursors(self, advisories, fake_get_query, tm def test_has_next_page(self, advisories, fake_get_query, tmpdir, empty_response): fake_get_query([empty_response, advisories(has_next_page=True)]) p = parser.Parser(workspace.Workspace(root=tmpdir.strpath, name="test", create=True), "secret") - result = [i for i in p.get()] + result = list(p.get()) assert len(result) == 1 def test_has_next_page_with_advisories(self, advisories, fake_get_query, tmpdir): fake_get_query([advisories(), advisories(has_next_page=True)]) p = parser.Parser(workspace.Workspace(root=tmpdir.strpath, name="test", create=True), "secret") - result = [i for i in p.get()] + result = list(p.get()) assert len(result) == 2 diff --git a/tests/unit/providers/nvd/test_api.py b/tests/unit/providers/nvd/test_api.py index 50bcabb6..08834946 100644 --- a/tests/unit/providers/nvd/test_api.py +++ b/tests/unit/providers/nvd/test_api.py @@ -4,11 +4,10 @@ from datetime import datetime import pytest - from vunnel.providers.nvd import api -@pytest.fixture +@pytest.fixture() def simple_mock(mocker): subject = api.NvdAPI(api_key="secret", timeout=1) @@ -23,7 +22,7 @@ def simple_mock(mocker): mocker.Mock( status_code=200, text=json.dumps(first_json_dict).encode("utf-8"), - ) + ), ] return mocker.patch.object(api.requests, "get", side_effect=responses), [first_json_dict], subject @@ -34,7 +33,7 @@ def test_cve_no_api_key(self, simple_mock, mocker): mock, responses, subject = simple_mock subject.api_key = None - vulnerabilities = [v for v in subject.cve("CVE-2020-0000")] + vulnerabilities = list(subject.cve("CVE-2020-0000")) assert vulnerabilities == responses assert api.requests.get.call_args_list == [ @@ -49,7 +48,7 @@ def test_cve_no_api_key(self, simple_mock, mocker): def test_cve_single_cve(self, simple_mock, mocker): mock, responses, subject = simple_mock - vulnerabilities = [v for v in subject.cve("CVE-2020-0000")] + vulnerabilities = list(subject.cve("CVE-2020-0000")) assert vulnerabilities == responses assert api.requests.get.call_args_list == [ @@ -101,12 +100,12 @@ def test_cve_multi_page(self, mocker): mocker.Mock( status_code=200, text=json.dumps(json_response).encode("utf-8"), - ) + ), ) mocker.patch.object(api.requests, "get", side_effect=responses) - vulnerabilities = [v for v in subject.cve()] + vulnerabilities = list(subject.cve()) assert vulnerabilities == json_responses assert api.requests.get.call_args_list == [ @@ -133,13 +132,12 @@ def test_cve_multi_page(self, mocker): def test_cve_pub_date_range(self, simple_mock, mocker): mock, responses, subject = simple_mock - vulnerabilities = [ - v - for v in subject.cve( + vulnerabilities = list( + subject.cve( pub_start_date=datetime.fromisoformat("2019-12-04"), pub_end_date=datetime.fromisoformat("2019-12-05"), ) - ] + ) assert vulnerabilities assert api.requests.get.call_args_list == [ @@ -154,13 +152,12 @@ def test_cve_pub_date_range(self, simple_mock, mocker): def test_cve_last_modified_date_range(self, simple_mock, mocker): mock, responses, subject = simple_mock - vulnerabilities = [ - v - for v in subject.cve( + vulnerabilities = list( + subject.cve( last_mod_start_date=datetime.fromisoformat("2019-12-04"), last_mod_end_date=datetime.fromisoformat("2019-12-05"), ) - ] + ) assert vulnerabilities assert api.requests.get.call_args_list == [ @@ -176,9 +173,9 @@ def test_results_per_page(self, simple_mock, mocker): mock, responses, subject = simple_mock with pytest.raises(RuntimeError): - vulnerabilities = [v for v in subject.cve(results_per_page=2001)] + list(subject.cve(results_per_page=2001)) - vulnerabilities = [v for v in subject.cve(results_per_page=5)] + list(subject.cve(results_per_page=5)) assert api.requests.get.call_args_list == [ mocker.call( @@ -192,7 +189,7 @@ def test_results_per_page(self, simple_mock, mocker): def test_cve_history(self, simple_mock, mocker): mock, responses, subject = simple_mock - changes = [c for c in subject.cve_history("CVE-2020-0000")] + changes = list(subject.cve_history("CVE-2020-0000")) assert changes assert api.requests.get.call_args_list == [ diff --git a/tests/unit/providers/nvd/test_manager.py b/tests/unit/providers/nvd/test_manager.py index 7b665420..eda92458 100644 --- a/tests/unit/providers/nvd/test_manager.py +++ b/tests/unit/providers/nvd/test_manager.py @@ -3,7 +3,6 @@ import json import pytest - from vunnel import workspace from vunnel.providers.nvd import manager @@ -29,6 +28,6 @@ def test_parser(tmpdir, helpers, mock_data_path, mocker): subject = manager.Manager(workspace=workspace.Workspace(tmpdir, "test", create=True)) subject.api.cve = mocker.Mock(return_value=[json_dict]) - actual_vulns = [v for v in subject.get(None)] + actual_vulns = list(subject.get(None)) assert expected_vulns == actual_vulns diff --git a/tests/unit/providers/nvd/test_nvd.py b/tests/unit/providers/nvd/test_nvd.py index e2ec48dd..6776e4c5 100644 --- a/tests/unit/providers/nvd/test_nvd.py +++ b/tests/unit/providers/nvd/test_nvd.py @@ -3,13 +3,12 @@ import json import pytest - -from vunnel import provider, result, workspace +from vunnel import provider, result from vunnel.providers import nvd @pytest.mark.parametrize( - "policy,should_raise", + ("policy", "should_raise"), ( (provider.ResultStatePolicy.KEEP, False), (provider.ResultStatePolicy.DELETE_BEFORE_WRITE, True), @@ -28,7 +27,7 @@ def make(): @pytest.mark.parametrize( - "mock_data_path,expected_written_entries", + ("mock_data_path", "expected_written_entries"), [ ("test-fixtures/single-entry.json", 1), ], diff --git a/tests/unit/providers/oracle/test_oracle.py b/tests/unit/providers/oracle/test_oracle.py index 9f13f6a8..da4d00f3 100644 --- a/tests/unit/providers/oracle/test_oracle.py +++ b/tests/unit/providers/oracle/test_oracle.py @@ -4,13 +4,12 @@ import pytest from pytest_unordered import unordered - from vunnel import result, workspace from vunnel.providers.oracle import Config, Provider, parser @pytest.mark.parametrize( - "input_file,expected", + ("input_file", "expected"), [ # TEST 1 ############################################################ ( @@ -41,9 +40,9 @@ }, "Name": "ELSA-2021-9151", "CVSS": [], - } + }, }, - ) + ), }, ), # TEST 2 ############################################################ @@ -114,7 +113,7 @@ "NamespaceName": "ol:5", "Module": None, }, - ] + ], ), "Link": "http://linux.oracle.com/errata/ELSA-2007-0057.html", "Description": "", @@ -134,7 +133,7 @@ }, "Name": "ELSA-2007-0057", "CVSS": [], - } + }, }, ), ("ELSA-2018-4250", "ol:6"): ( @@ -187,7 +186,7 @@ "NamespaceName": "ol:6", "Module": None, }, - ] + ], ), "Link": "http://linux.oracle.com/errata/ELSA-2018-4250.html", "Description": "", @@ -215,7 +214,7 @@ }, "Name": "ELSA-2018-4250", "CVSS": [], - } + }, }, ), }, @@ -234,7 +233,7 @@ def test_parse(tmpdir, helpers, input_file, expected): class TestKspliceFilterer: @pytest.mark.parametrize( - ["input_vulnerability", "expected_output"], + ("input_vulnerability", "expected_output"), [ ( { @@ -250,11 +249,11 @@ class TestKspliceFilterer: "NamespaceName": "ol:8", "Version": "2:1.1.1g-15.ksplice1.el8_3", "Module": None, - } + }, ], - } + }, }, - ) + ), }, { ("ELSA-123", "ol:8"): ( @@ -263,9 +262,9 @@ class TestKspliceFilterer: "Vulnerability": { "Name": "ELSA-123", "FixedIn": [], - } + }, }, - ) + ), }, ), ( @@ -282,11 +281,11 @@ class TestKspliceFilterer: "NamespaceName": "ol:8", "Version": "2:1.1.1g-15.1.el8_3", "Module": None, - } + }, ], - } + }, }, - ) + ), }, { ("ELSA-123", "ol:8"): ( @@ -301,11 +300,11 @@ class TestKspliceFilterer: "NamespaceName": "ol:8", "Version": "2:1.1.1g-15.1.el8_3", "Module": None, - } + }, ], - } + }, }, - ) + ), }, ), ( @@ -331,9 +330,9 @@ class TestKspliceFilterer: "Module": None, }, ], - } + }, }, - ) + ), }, { ("ELSA-123", "ol:8"): ( @@ -348,11 +347,11 @@ class TestKspliceFilterer: "NamespaceName": "ol:8", "Version": "2:1.1.1g-15.1.el8_3", "Module": None, - } + }, ], - } + }, }, - ) + ), }, ), ], @@ -362,7 +361,7 @@ def test_filterer(self, input_vulnerability, expected_output): assert f.filter(input_vulnerability) == expected_output -@pytest.fixture +@pytest.fixture() def disable_get_requests(monkeypatch): def disabled(*args, **kwargs): raise RuntimeError("requests disabled but HTTP GET attempted") @@ -387,5 +386,5 @@ def mock_download(): p.update(None) - assert 2 == workspace.num_result_entries() + assert workspace.num_result_entries() == 2 assert workspace.result_schemas_valid(require_entries=True) diff --git a/tests/unit/providers/sles/test_sles.py b/tests/unit/providers/sles/test_sles.py index 4a0e428b..287d0d78 100644 --- a/tests/unit/providers/sles/test_sles.py +++ b/tests/unit/providers/sles/test_sles.py @@ -25,12 +25,10 @@ class TestSLESVulnerabilityParser: - # flake8: noqa: E501 @pytest.fixture def valid_element(self): def generate(with_namespace): - content = ( "" + "CVE-2021-29154" diff --git a/tests/unit/providers/ubuntu/test_git_wrapper.py b/tests/unit/providers/ubuntu/test_git_wrapper.py index 464b1437..2e06c84d 100644 --- a/tests/unit/providers/ubuntu/test_git_wrapper.py +++ b/tests/unit/providers/ubuntu/test_git_wrapper.py @@ -5,7 +5,6 @@ import unittest import pytest - from vunnel.providers.ubuntu.git import GitRevision, GitWrapper @@ -137,12 +136,12 @@ def test_parse_log(self): commits = wrapper._parse_log(git_commit_log) - self.assertEqual(len(commits), len(self._commit_changes_)) + assert len(commits) == len(self._commit_changes_) for got, expected in zip(commits, self._commit_changes_): - self.assertTrue(got.sha in expected["sha"]) - self.assertEqual(got.updated, expected["updated"]) - self.assertEqual(got.deleted, expected["deleted"]) + assert got.sha in expected["sha"] + assert got.updated == expected["updated"] + assert got.deleted == expected["deleted"] print(got) def test_compute_change_set(self): @@ -155,14 +154,14 @@ def test_compute_change_set(self): modified, removed = wrapper._compute_change_set(commits) - self.assertEqual(modified, self._overall_changes_["modified"]) - self.assertEqual(removed, self._overall_changes_["removed"]) - print("Modified: {}".format(modified)) - print("Removed: {}".format(removed)) + assert modified == self._overall_changes_["modified"] + assert removed == self._overall_changes_["removed"] + print(f"Modified: {modified}") + print(f"Removed: {removed}") @pytest.mark.parametrize( - "git_log_output,expected", + ("git_log_output", "expected"), [ pytest.param( """ diff --git a/tests/unit/providers/ubuntu/test_ubuntu.py b/tests/unit/providers/ubuntu/test_ubuntu.py index 4c530774..ca950d0c 100644 --- a/tests/unit/providers/ubuntu/test_ubuntu.py +++ b/tests/unit/providers/ubuntu/test_ubuntu.py @@ -9,7 +9,6 @@ from dataclasses import asdict import pytest - from vunnel import result, workspace from vunnel.providers import ubuntu from vunnel.providers.ubuntu.parser import ( @@ -229,7 +228,7 @@ def test_parse_cve(self): "version": "7:3.2.6-1", }, ], - } + }, ) # No longer parsing by default @@ -307,7 +306,7 @@ def test_checkers(self): assert check_patch(data[0]) == data[1] @pytest.mark.parametrize( - "patch,expected", + ("patch", "expected"), [ (Patch(distro="foo", package="bar", status="ignored ftw", version="end-of-life now but something else before"), True), (Patch(distro="foo", package="bar", status="ignored", version="reached end-of-life"), True), @@ -368,16 +367,11 @@ def test_reprocess_merged_cve(self, tmpdir): "Priority": "medium", "Name": "CVE-0000-0000", "ignored_patches": [ - { - "distro": "devel", - "package": "mozjs52", - "status": "DNE", - "version": None, - }, - ] - + new_distro_patches, + {"distro": "devel", "package": "mozjs52", "status": "DNE", "version": None}, + *new_distro_patches, + ], "git_last_processed_rev": "40a85b5b23bd905f8d5c6791d3f61108406ec372", - } + }, ) ws = workspace.Workspace(tmpdir, "test") udp = Parser(workspace=ws, additional_versions={"madeup": "00.00"}, enable_rev_history=False) @@ -394,7 +388,7 @@ def test_reprocess_merged_cve(self, tmpdir): assert result.patches == data.patches + [Patch(**p) for p in new_distro_patches] -@pytest.fixture +@pytest.fixture() def hydrate_git_repo(tmpdir, helpers): def run(cmd, **kwargs): subprocess.run(shlex.split(cmd), **kwargs, stderr=sys.stderr, stdout=sys.stdout) @@ -417,7 +411,7 @@ def apply(export_path): @pytest.mark.parametrize( - "mock_data_path,expected_written_entries", + ("mock_data_path", "expected_written_entries"), [ ("test-fixtures/repo-fast-export", 42), # this is 6 records distributed across multiple distros: diff --git a/tests/unit/providers/wolfi/test_wolfi.py b/tests/unit/providers/wolfi/test_wolfi.py index 69f8de3b..b3364a32 100644 --- a/tests/unit/providers/wolfi/test_wolfi.py +++ b/tests/unit/providers/wolfi/test_wolfi.py @@ -5,14 +5,13 @@ import shutil import pytest - from vunnel import result, workspace from vunnel.providers.wolfi import Config, Provider, parser from vunnel.providers.wolfi.parser import Parser class TestParser: - @pytest.fixture + @pytest.fixture() def mock_raw_data(self, helpers): """ Returns stringified version of the following json @@ -33,19 +32,19 @@ def mock_raw_data(self, helpers): "2.39-r2": ["CVE-2022-38533"], "2.39-r3": ["CVE-2022-38128"], }, - } + }, }, { "pkg": { "name": "brotli", "secfixes": {"1.0.9-r0": ["CVE-2020-8927"]}, - } + }, }, { "pkg": { "name": "busybox", "secfixes": {"1.35.0-r3": ["CVE-2022-28391", "CVE-2022-30065"]}, - } + }, }, {"pkg": {"name": "coreutils", "secfixes": {"0": ["CVE-2016-2781"]}}}, {"pkg": {"name": "cups", "secfixes": {"2.4.2-r0": ["CVE-2022-26691"]}}}, @@ -57,16 +56,16 @@ def mock_raw_data(self, helpers): "CVE-2022-42010", "CVE-2022-42011", "CVE-2022-42012", - ] + ], }, - } + }, }, ], } return json.dumps(data) - @pytest.fixture + @pytest.fixture() def mock_parsed_data(self): """ Returns the parsed output generated by AlpineDataProvider._load() for the mock_raw_data @@ -89,31 +88,31 @@ def mock_parsed_data(self): "2.39-r2": ["CVE-2022-38533"], "2.39-r3": ["CVE-2022-38128"], }, - } + }, }, { "pkg": { "name": "brotli", "secfixes": {"1.0.9-r0": ["CVE-2020-8927"]}, - } + }, }, { "pkg": { "name": "busybox", "secfixes": {"1.35.0-r3": ["CVE-2022-28391", "CVE-2022-30065"]}, - } + }, }, { "pkg": { "name": "coreutils", "secfixes": {"0": ["CVE-2016-2781"]}, - } + }, }, { "pkg": { "name": "cups", "secfixes": {"2.4.2-r0": ["CVE-2022-26691"]}, - } + }, }, { "pkg": { @@ -123,12 +122,12 @@ def mock_parsed_data(self): "CVE-2022-42010", "CVE-2022-42011", "CVE-2022-42012", - ] + ], }, - } + }, }, ], - } + }, } return release, dbtype_data_dict @@ -163,8 +162,8 @@ def test_normalize(self, mock_parsed_data, tmpdir): vuln_records = p._normalize(release, dbtype_data_dict) assert len(vuln_records) > 0 - assert all(map(lambda x: "Vulnerability" in x, vuln_records.values())) - assert sorted(list(vuln_records.keys())) == sorted( + assert all("Vulnerability" in x for x in vuln_records.values()) + assert sorted(vuln_records.keys()) == sorted( [ "CVE-2016-2781", "CVE-2020-8927", @@ -177,11 +176,11 @@ def test_normalize(self, mock_parsed_data, tmpdir): "CVE-2022-42010", "CVE-2022-42011", "CVE-2022-42012", - ] + ], ) -@pytest.fixture +@pytest.fixture() def disable_get_requests(monkeypatch): def disabled(*args, **kwargs): raise RuntimeError("requests disabled but HTTP GET attempted") @@ -201,5 +200,5 @@ def test_provider_schema(helpers, disable_get_requests): p.update(None) - assert 50 == workspace.num_result_entries() + assert workspace.num_result_entries() == 50 assert workspace.result_schemas_valid(require_entries=True) diff --git a/tests/unit/test_provider.py b/tests/unit/test_provider.py index bc85074a..6deac21e 100644 --- a/tests/unit/test_provider.py +++ b/tests/unit/test_provider.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock import pytest - from vunnel import provider, result, schema, workspace @@ -53,7 +52,7 @@ def update(self, *args, **kwargs): return ["http://localhost:8000/dummy-input-1.json"], 1 -@pytest.fixture +@pytest.fixture() def dummy_provider(tmpdir): def apply(populate=True, use_dir=None, **kwargs): if not use_dir: diff --git a/tests/unit/utils/test_fdb.py b/tests/unit/utils/test_fdb.py index 5c2a5002..bf8f6231 100644 --- a/tests/unit/utils/test_fdb.py +++ b/tests/unit/utils/test_fdb.py @@ -1,7 +1,6 @@ from __future__ import annotations import pytest - from vunnel.utils import fdb as db @@ -42,7 +41,7 @@ def test_gets_all_files(self, tmpdir): record = conn.create(str(i)) record.commit({"name": str(i)}) records = conn.get_all() - assert len([i for i in records]) == 9 + assert len(list(records)) == 9 class TestMeta: @@ -58,7 +57,7 @@ def test_doesnt_get_mixed(self, tmpdir): reloaded_meta.load() assert reloaded_meta.data == {"type": "json"} records = conn.get_all() - assert len([i for i in records]) == 9 + assert len(list(records)) == 9 def test_updates(self, tmpdir): conn = db.connection(tmpdir.strpath, serializer="json") diff --git a/tests/unit/utils/test_oval_v2.py b/tests/unit/utils/test_oval_v2.py index ce5a0193..22d4d2cc 100644 --- a/tests/unit/utils/test_oval_v2.py +++ b/tests/unit/utils/test_oval_v2.py @@ -6,7 +6,6 @@ import defusedxml.ElementTree as ET import pytest - from vunnel.utils.oval_v2 import ( Artifact, ArtifactParser, @@ -27,7 +26,7 @@ TestParser.__test__ = False -@pytest.fixture +@pytest.fixture() def dummy_config(): return OVALParserConfig( platform_regex=None, @@ -61,7 +60,8 @@ class TestTestParser: def test_happy_path(self, dummy_config, element): xml_element = ET.fromstring(element) result = TestParser.parse(xml_element, dummy_config) - assert result and isinstance(result, Test) + assert result + assert isinstance(result, Test) assert result.identity == "oval:org.opensuse.security:tst:2009223735" assert result.artifact_id == "oval:org.opensuse.security:obj:2009042619" assert result.version_id == "oval:org.opensuse.security:ste:2009061809" @@ -103,7 +103,8 @@ class TestArtifactParser: def test_happy_path(self, dummy_config, element): xml_element = ET.fromstring(element) result = ArtifactParser.parse(xml_element, dummy_config) - assert result and isinstance(result, Artifact) + assert result + assert isinstance(result, Artifact) assert result.identity == "oval:org.opensuse.security:obj:2009041419" assert result.name == "policycoreutils-python" @@ -115,7 +116,7 @@ def test_happy_path(self, dummy_config, element): pytest.param(ET.fromstring(''), id="not-artifact"), pytest.param( ET.fromstring( - '' # noqa: E501 + '', # noqa: E501 ), id="invalid-artifact", ), @@ -127,7 +128,7 @@ def test_unhappy_paths(self, dummy_config, element): class TestVersionParser: @pytest.mark.parametrize( - "element, identity, op, value", + ("element", "identity", "op", "value"), [ pytest.param( '' # noqa: E501 @@ -171,7 +172,8 @@ class TestVersionParser: def test_happy_path(self, dummy_config, element, identity, op, value): xml_element = ET.fromstring(element) result = VersionParser.parse(xml_element, dummy_config) - assert result and isinstance(result, Version) + assert result + assert isinstance(result, Version) assert result.identity == identity assert result.operation == op assert result.value == value @@ -184,7 +186,7 @@ def test_happy_path(self, dummy_config, element, identity, op, value): pytest.param(ET.fromstring(''), id="not-version"), pytest.param( ET.fromstring( - '' # noqa: E501 + '', # noqa: E501 ), id="invalid-version", ), @@ -195,26 +197,26 @@ def test_unhappy_paths(self, dummy_config, element): class TestParserFactory: - @pytest.fixture + @pytest.fixture() def invalid_parsers_error(self): return "Invalid input for parsers, must be a list of OVALElementParser sub-classes" - @pytest.fixture + @pytest.fixture() def invalid_enum_error(self): return "Invalid input for oval element enumeration, must be a python enum class" - @pytest.fixture + @pytest.fixture() def parser_enum_mismatch_error(self): return "Parsers are not a match for the oval element enumeration" - @pytest.fixture + @pytest.fixture() def example_enum_class(self): class RandomEnum(enum.Enum): FOO = "foo" return RandomEnum - @pytest.fixture + @pytest.fixture() def example_parser_class(self, example_enum_class): class RandomParser(OVALElementParser): oval_element = example_enum_class.FOO @@ -226,7 +228,7 @@ def parse(xml_element, config: OVALParserConfig): return RandomParser @pytest.mark.parametrize( - "parsers,oval_enum", + ("parsers", "oval_enum"), [ pytest.param( [ @@ -343,7 +345,7 @@ def test_get_oval_element_happy_path( pytest.param(ET.fromstring(''), id="invalid-tag"), pytest.param( ET.fromstring( - '' + '', ), id="invalid-tag-with-ns", ), @@ -366,7 +368,7 @@ def test_get_oval_element_invalid_input( class TestOVALElementParser: @pytest.mark.parametrize( - "data, regex, expected", + ("data", "regex", "expected"), [ pytest.param( "{http://oval.mitre.org/XMLSchema/oval-definitions-5#linux}object", @@ -386,7 +388,7 @@ def test_find_with_regex_happy_path(self, data, regex, expected): assert OVALElementParser._find_with_regex(data, regex) == expected @pytest.mark.parametrize( - "data, regex", + ("data", "regex"), [ pytest.param( None, @@ -412,7 +414,7 @@ def test_find_with_regex_invalid_input(self, data, regex): class TestIterParse: @pytest.mark.parametrize( - "content, tag, parser_fn, expected", + ("content", "tag", "parser_fn", "expected"), [ pytest.param( '' # noqa: E501 @@ -460,7 +462,8 @@ class RandomParser(OVALElementParser): parser_factory=OVALParserFactory(parsers=[RandomParser], element_enum=RandomElements), ) - assert results and isinstance(results, dict) + assert results + assert isinstance(results, dict) assert len(results) == 1 assert list(results.keys())[0] == RandomElements.FOO diff --git a/tests/unit/utils/test_rpm.py b/tests/unit/utils/test_rpm.py index b37e40c1..f5d4ce6a 100644 --- a/tests/unit/utils/test_rpm.py +++ b/tests/unit/utils/test_rpm.py @@ -1,12 +1,11 @@ from __future__ import annotations import pytest - from vunnel.utils import rpm @pytest.mark.parametrize( - "version1, version2, expected", + ("version1", "version2", "expected"), [ # no epoch tests... ("1", "1", 0),