diff --git a/.github/actions/bootstrap/action.yaml b/.github/actions/bootstrap/action.yaml new file mode 100644 index 00000000..0bf02eb1 --- /dev/null +++ b/.github/actions/bootstrap/action.yaml @@ -0,0 +1,99 @@ +name: "Bootstrap" +description: "Bootstrap all tools and dependencies" +inputs: + go-version: + description: "Go version to install" + required: true + default: "1.21.x" + python-version: + description: "Python version to install" + required: true + default: "3.10" + use-go-cache: + description: "Restore go cache" + required: true + default: "true" + cache-key-prefix: + description: "Prefix all cache keys with this value" + required: true + default: "831180ac25" + build-cache-key-prefix: + description: "Prefix build cache key with this value" + required: true + default: "f8b6d31dea" + bootstrap-apt-packages: + description: "Space delimited list of tools to install via apt" + default: "libxml2-utils" + +runs: + using: "composite" + steps: + - uses: actions/setup-go@v3 + with: + go-version: ${{ inputs.go-version }} + + - uses: actions/setup-python@d27e3f3d7c64b4bbf8e4abfb9b63b83e846e0435 # v4.5.0 + with: + python-version: ${{ inputs.python-version }} + + - name: Restore python cache + id: python-venv-cache + uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v3.2.6 + with: + path: | + test/quality/venv + test/quality/vulnerability-match-labels/venv + key: ${{ runner.os }}-python-${{ inputs.python-version }}-${{ hashFiles('**/test/quality/**/requirements.txt') }} + restore-keys: | + ${{ runner.os }}-python-${{ env.python-version }}- + + - name: Restore tool cache + id: tool-cache + uses: actions/cache@v3 + with: + path: ${{ github.workspace }}/.tmp + key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-tool-${{ hashFiles('Makefile') }} + + # note: we need to keep restoring the go mod cache before bootstrapping tools since `go install` is used in + # some installations of project tools. + - name: Restore go module cache + id: go-mod-cache + if: inputs.use-go-cache == 'true' + uses: actions/cache@v3 + with: + path: | + ~/go/pkg/mod + key: ${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ inputs.cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}- + + - name: (cache-miss) Bootstrap project tools + shell: bash + if: steps.tool-cache.outputs.cache-hit != 'true' + run: make bootstrap-tools + + - name: Restore go build cache + id: go-cache + if: inputs.use-go-cache == 'true' + uses: actions/cache@v3 + with: + path: | + ~/.cache/go-build + key: ${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}-${{ hashFiles('**/go.sum') }} + restore-keys: | + ${{ inputs.cache-key-prefix }}-${{ inputs.build-cache-key-prefix }}-${{ runner.os }}-go-${{ inputs.go-version }}- + + - name: (cache-miss) Bootstrap go dependencies + shell: bash + if: steps.go-mod-cache.outputs.cache-hit != 'true' && inputs.use-go-cache == 'true' + run: make bootstrap-go + + - name: Install apt packages + if: inputs.bootstrap-apt-packages != '' + shell: bash + run: | + DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y ${{ inputs.bootstrap-apt-packages }} + + - name: Create all cache fingerprints + shell: bash + run: make fingerprints diff --git a/.github/scripts/ci-check.sh b/.github/scripts/ci-check.sh new file mode 100755 index 00000000..1a56aad1 --- /dev/null +++ b/.github/scripts/ci-check.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +red=$(tput setaf 1) +bold=$(tput bold) +normal=$(tput sgr0) + +# assert we are running in CI (or die!) +if [[ -z "$CI" ]]; then + echo "${bold}${red}This step should ONLY be run in CI. Exiting...${normal}" + exit 1 +fi + diff --git a/.github/scripts/coverage.py b/.github/scripts/coverage.py new file mode 100755 index 00000000..db14135c --- /dev/null +++ b/.github/scripts/coverage.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +import subprocess +import sys +import shlex + + +class bcolors: + HEADER = '\033[95m' + OKBLUE = '\033[94m' + OKCYAN = '\033[96m' + OKGREEN = '\033[92m' + WARNING = '\033[93m' + FAIL = '\033[91m' + ENDC = '\033[0m' + BOLD = '\033[1m' + UNDERLINE = '\033[4m' + + +if len(sys.argv) < 3: + print("Usage: coverage.py [threshold] [go-coverage-report]") + sys.exit(1) + + +threshold = float(sys.argv[1]) +report = sys.argv[2] + + +args = shlex.split(f"go tool cover -func {report}") +p = subprocess.run(args, capture_output=True, text=True) + +percent_coverage = float(p.stdout.splitlines()[-1].split()[-1].replace("%", "")) +print(f"{bcolors.BOLD}Coverage: {percent_coverage}%{bcolors.ENDC}") + +if percent_coverage < threshold: + print(f"{bcolors.BOLD}{bcolors.FAIL}Coverage below threshold of {threshold}%{bcolors.ENDC}") + sys.exit(1) diff --git a/.github/scripts/trigger-release.sh b/.github/scripts/trigger-release.sh new file mode 100644 index 00000000..c1a5432e --- /dev/null +++ b/.github/scripts/trigger-release.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +set -eu + +bold=$(tput bold) +normal=$(tput sgr0) + +if ! [ -x "$(command -v gh)" ]; then + echo "The GitHub CLI could not be found. To continue follow the instructions at https://github.com/cli/cli#installation" + exit 1 +fi + +gh auth status + +# we need all of the git state to determine the next version. Since tagging is done by +# the release pipeline it is possible to not have all of the tags from previous releases. +git fetch --tags + +# populates the CHANGELOG.md and VERSION files +echo "${bold}Generating changelog...${normal}" +make changelog 2> /dev/null + +NEXT_VERSION=$(cat VERSION) + +if [[ "$NEXT_VERSION" == "" || "${NEXT_VERSION}" == "(Unreleased)" ]]; then + echo "Could not determine the next version to release. Exiting..." + exit 1 +fi + +while true; do + read -p "${bold}Do you want to trigger a release for version '${NEXT_VERSION}'?${normal} [y/n] " yn + case $yn in + [Yy]* ) echo; break;; + [Nn]* ) echo; echo "Cancelling release..."; exit;; + * ) echo "Please answer yes or no.";; + esac +done + +echo "${bold}Kicking off release for ${NEXT_VERSION}${normal}..." +echo +gh workflow run release.yaml -f version=${NEXT_VERSION} + +echo +echo "${bold}Waiting for release to start...${normal}" +sleep 10 + +set +e + +echo "${bold}Head to the release workflow to monitor the release:${normal} $(gh run list --workflow=release.yaml --limit=1 --json url --jq '.[].url')" +id=$(gh run list --workflow=release.yaml --limit=1 --json databaseId --jq '.[].databaseId') +gh run watch $id --exit-status || (echo ; echo "${bold}Logs of failed step:${normal}" && GH_PAGER="" gh run view $id --log-failed) diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 029c3d2d..cf68f2af 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -1,19 +1,13 @@ name: "Release" on: - push: - # take no actions on push to any branch... - branches-ignore: - - "**" - # ... only act on release tags - tags: - - "v*" - -env: - GO_VERSION: "1.20.x" + workflow_dispatch: + inputs: + version: + description: tag the latest commit on main with the given version (prefixed with v) + required: true permissions: contents: read - packages: read jobs: quality-gate: @@ -22,12 +16,11 @@ jobs: steps: - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 - # we don't want to release commits that have been pushed and tagged, but not necessarily merged onto main - - name: Ensure tagged commit is on main + - name: Check if tag already exists + # note: this will fail if the tag already exists run: | - echo "Tag: ${GITHUB_REF##*/}" - git fetch origin main - git merge-base --is-ancestor ${GITHUB_REF##*/} origin/main && echo "${GITHUB_REF##*/} is a commit on main!" + [[ "${{ github.event.inputs.version }}" == v* ]] || (echo "version '${{ github.event.inputs.version }}' does not have a 'v' prefix" && exit 1) + git tag ${{ github.event.inputs.version }} - name: Check static analysis results uses: fountainhead/action-wait-for-check@297be350cf8393728ea4d4b39435c7d7ae167c93 # v1.1.0 @@ -95,62 +88,20 @@ jobs: release: needs: [quality-gate] - # due to our code signing process, it's vital that we run our release steps on macOS - runs-on: macos-latest + runs-on: ubuntu-20.04 + permissions: + contents: write + packages: write steps: - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ env.GO_VERSION }} - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 with: fetch-depth: 0 - - name: Restore tool cache - id: tool-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ${{ github.workspace }}/.tmp - key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - - - name: Restore go cache - id: go-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - - name: (cache-miss) Bootstrap all project dependencies - if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' - run: make bootstrap - - - name: Build & publish release artifacts - run: make release - env: - GITHUB_TOKEN: ${{ secrets.XEOL_GITHUB_TOKEN }} - AWS_ACCESS_KEY_ID: ${{ secrets.DATA_XEOL_IO_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.DATA_XEOL_IO_AWS_SECRET_ACCESS_KEY }} - - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 - with: - name: artifacts - path: dist/**/* - - release-docker-assets: - needs: [release] - # code signing requires we run on mac-os runners. docker does not come installed on the mac-os runner - # a previous release process installed and configured docker on the mac-os runner which lead to blocked releases - # the anchore tools team opted to break this step out to a separate process to remove this work constraint - runs-on: ubuntu-latest - permissions: - packages: write - steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 + - name: Bootstrap environment + uses: ./.github/actions/bootstrap with: - go-version: ${{ env.GO_VERSION }} + # use the same cache we used for building snapshots + build-cache-key-prefix: "snapshot" - name: Login to Docker Hub uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 @@ -165,18 +116,23 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.XEOL_GITHUB_TOKEN }} - - name: Restore go cache - id: go-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ env.GO_VERSION }}- + - name: Tag release + run: | + git tag ${{ github.event.inputs.version }} + git push origin --tags + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: (cache-miss) Bootstrap all project dependencies - if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' - run: make bootstrap + - name: Build & publish release artifacts + run: make ci-release + env: + # for creating the release (requires write access to packages and content) + GITHUB_TOKEN: ${{ secrets.XEOL_GITHUB_TOKEN }} + # for updating the VERSION file in S3... + AWS_ACCESS_KEY_ID: ${{ secrets.DATA_XEOL_IO_AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.DATA_XEOL_IO_AWS_SECRET_ACCESS_KEY }} - - name: Build & Publish docker images - run: make release-docker-assets + - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + with: + name: artifacts + path: dist/**/* diff --git a/.github/workflows/update-bootstrap-tools.yml b/.github/workflows/update-bootstrap-tools.yml index 0cfef01f..03b82ae1 100644 --- a/.github/workflows/update-bootstrap-tools.yml +++ b/.github/workflows/update-bootstrap-tools.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - GO_VERSION: "1.20.x" + GO_VERSION: "1.21.x" permissions: contents: read diff --git a/.github/workflows/update-syft-release.yml b/.github/workflows/update-syft-release.yml index 1c8bc76a..e2442e2f 100644 --- a/.github/workflows/update-syft-release.yml +++ b/.github/workflows/update-syft-release.yml @@ -6,7 +6,7 @@ on: workflow_dispatch: env: - GO_VERSION: "1.20.x" + GO_VERSION: "1.21.x" permissions: contents: read diff --git a/.github/workflows/validations.yaml b/.github/workflows/validations.yaml index fbe76bd3..3a687da5 100644 --- a/.github/workflows/validations.yaml +++ b/.github/workflows/validations.yaml @@ -1,14 +1,10 @@ name: "Validations" on: workflow_dispatch: + pull_request: push: branches: - main - pull_request: - -env: - GO_VERSION: "1.20.x" - PYTHON_VERSION: "3.10" permissions: contents: read @@ -19,178 +15,39 @@ jobs: name: "Static analysis" runs-on: ubuntu-20.04 steps: - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ env.GO_VERSION }} + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 - - - name: Restore tool cache - id: tool-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ${{ github.workspace }}/.tmp - key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - - - name: Restore go cache - id: go-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - - name: (cache-miss) Bootstrap all project dependencies - if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' - run: make bootstrap - - - name: Bootstrap CI environment dependencies - run: make ci-bootstrap + - name: Bootstrap environment + uses: ./.github/actions/bootstrap - name: Run static analysis run: make static-analysis - # allow for PRs to skip validating the syft version to allow for incremental updates of syft before release. - # In this way checks against the main branch (which are required for release) will fail, but PR checks will not - - name: Ensure syft version is a release version - run: | - echo "GitHub reference: ${GITHUB_REF##*/}" - git fetch origin main - git merge-base --is-ancestor ${GITHUB_REF##*/} origin/main && make validate-syft-release-version || echo "skipping syft version check" - Unit-Test: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Unit tests" runs-on: ubuntu-20.04 steps: - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe #v4.1.0 - with: - go-version: ${{ env.GO_VERSION }} - - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 #v2.5.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac - - name: Restore tool cache - id: tool-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ${{ github.workspace }}/.tmp - key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - - - name: Restore go cache - id: go-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - - name: (cache-miss) Bootstrap all project dependencies - if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' - run: make bootstrap - - - name: Bootstrap CI environment dependencies - run: make ci-bootstrap + - name: Bootstrap environment + uses: ./.github/actions/bootstrap - name: Run unit tests run: make unit - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 - with: - name: unit-test-results - path: test/results/**/* - - # Quality-Test: - # # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline - # name: "Quality tests" - # runs-on: ubuntu-20.04 - # steps: - # - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - # with: - # go-version: ${{ env.GO_VERSION }} - - # - uses: actions/setup-python@13ae5bb136fac2878aff31522b9efb785519f984 # v4.3.0 - # with: - # python-version: ${{ env.PYTHON_VERSION }} - - # - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 - # with: - # submodules: true - - # - name: Restore tool cache - # id: tool-cache - # uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - # with: - # path: ${{ github.workspace }}/.tmp - # key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - - # - name: Restore go cache - # id: go-cache - # uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - # with: - # path: ~/go/pkg/mod - # key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - # restore-keys: | - # ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - # - name: Restore python cache - # id: python-cache - # uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - # with: - # path: | - # test/quality/venv - # test/quality/vulnerability-match-labels/venv - # key: ${{ runner.os }}-go-${{ env.PYTHON_VERSION }}-${{ hashFiles('**/test/quality/**/requirements.txt') }} - # restore-keys: | - # ${{ runner.os }}-go-${{ env.PYTHON_VERSION }}- - - # - name: (cache-miss) Bootstrap all project dependencies - # if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' - # run: make bootstrap - - # - name: Run quality tests - # run: make quality - Integration-Test: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline name: "Integration tests" runs-on: ubuntu-20.04 steps: - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ env.GO_VERSION }} - - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - - name: Restore tool cache - id: tool-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ${{ github.workspace }}/.tmp - key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - - - name: Restore go cache - id: go-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - - name: (cache-miss) Bootstrap all project dependencies - if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' - run: make bootstrap - - - name: Bootstrap CI environment dependencies - run: make ci-bootstrap - - - name: Build key for tar cache - run: make integration-fingerprint + - name: Bootstrap environment + uses: ./.github/actions/bootstrap - name: Restore integration test cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: ${{ github.workspace }}/test/integration/test-fixtures/cache key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }} @@ -202,42 +59,29 @@ jobs: name: "Build snapshot artifacts" runs-on: ubuntu-20.04 steps: - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ env.GO_VERSION }} - - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0 - - name: Restore tool cache - id: tool-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + - name: Bootstrap environment + uses: ./.github/actions/bootstrap with: - path: ${{ github.workspace }}/.tmp - key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }} - - - name: Restore go cache - id: go-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - - name: (cache-miss) Bootstrap all project dependencies - if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true' - run: make bootstrap + # why have another build cache key? We don't want unit/integration/etc test build caches to replace + # the snapshot build cache, which includes builds for all OSs and architectures. As long as this key is + # unique from the build-cache-key-prefix in other CI jobs, we should be fine. + # + # note: ideally this value should match what is used in release (just to help with build times). + build-cache-key-prefix: "snapshot" + bootstrap-apt-packages: "" - name: Build snapshot artifacts - run: make snapshot snapshot-docker-assets + run: make snapshot - - uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 + # why not use actions/upload-artifact? It is very slow (3 minutes to upload ~600MB of data, vs 10 seconds with this approach). + # see https://github.com/actions/upload-artifact/issues/199 for more info + - name: Upload snapshot artifacts + uses: actions/cache/save@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: - name: artifacts - path: snapshot/**/* + path: snapshot + key: snapshot-build-${{ github.run_id }} Acceptance-Linux: # Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline @@ -245,19 +89,17 @@ jobs: needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac #v4.0.0 - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + - name: Download snapshot build + uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: - name: artifacts path: snapshot - - - name: Build key for image cache - run: make install-fingerprint + key: snapshot-build-${{ github.run_id }} - name: Restore install.sh test image cache id: install-test-image-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: ${{ github.workspace }}/test/install/cache key: ${{ runner.os }}-install-test-image-cache-${{ hashFiles('test/install/cache.fingerprint') }} @@ -279,12 +121,20 @@ jobs: needs: [Build-Snapshot-Artifacts] runs-on: macos-latest steps: - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac #v4.0.0 - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + - name: Download snapshot build + uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: - name: artifacts path: snapshot + key: snapshot-build-${{ github.run_id }} + + - name: Restore docker image cache for compare testing + id: mac-compare-testing-cache + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 + with: + path: image.tar + key: ${{ runner.os }}-${{ hashFiles('test/compare/mac.sh') }} - name: Run install.sh tests (Mac) run: make install-test-ci-mac @@ -295,38 +145,22 @@ jobs: needs: [Build-Snapshot-Artifacts] runs-on: ubuntu-20.04 steps: - - uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 - with: - go-version: ${{ env.GO_VERSION }} - - - uses: actions/checkout@f43a0e5ff2bd294095638e18286ca9a3d1956744 # v2.5.0 - - - name: Restore go cache - id: go-cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 - with: - path: ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ env.GO_VERSION }}- - - - name: (cache-miss) Bootstrap go dependencies - if: steps.go-cache.outputs.cache-hit != 'true' - run: make bootstrap-go + - uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac #v4.0.0 - - name: Build key for tar cache - run: make cli-fingerprint + - name: Bootstrap environment + uses: ./.github/actions/bootstrap - - name: Restore CLI test cache - uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 # v3.3.2 + - name: Restore CLI test-fixture cache + uses: actions/cache@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: path: ${{ github.workspace }}/test/cli/test-fixtures/cache key: ${{ runner.os }}-cli-test-cache-${{ hashFiles('test/cli/test-fixtures/cache.fingerprint') }} - - uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 + - name: Download snapshot build + uses: actions/cache/restore@704facf57e6136b1bc63b828d79edcd491f0ee84 #v3.3.2 with: - name: artifacts path: snapshot + key: snapshot-build-${{ github.run_id }} - name: Run CLI Tests (Linux) run: make cli diff --git a/.golangci.yaml b/.golangci.yaml index ec32c10a..184f0f8e 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -1,6 +1,11 @@ -linters-settings: - funlen: - lines: 65 +issues: + max-same-issues: 25 + + # TODO: enable this when we have coverage on docstring comments +# # The list of ids of default excludes to include or disable. +# include: +# - EXC0002 # disable excluding of issues about comments from golint + linters: # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint disable-all: true @@ -17,6 +22,7 @@ linters: - gocritic - gocyclo - gofmt + - goimports - goprintffuncname - gosec - gosimple @@ -24,7 +30,6 @@ linters: - ineffassign - misspell - nakedret - - nolintlint - revive - staticcheck - stylecheck @@ -34,13 +39,31 @@ linters: - unused - whitespace +linters-settings: + funlen: + # Checks the number of lines in a function. + # If lower than 0, disable the check. + # Default: 60 + lines: 70 + # Checks the number of statements in a function. + # If lower than 0, disable the check. + # Default: 40 + statements: 50 +output: + uniq-by-line: false +run: + timeout: 10m + # do not enable... -# - gochecknoglobals +# - deadcode # The owner seems to have abandoned the linter. Replaced by "unused". # - depguard # we need to setup a configuration for this +# - goprintffuncname # does not catch all cases and there are exceptions +# - nakedret # does not catch all cases and should not fail a build +# - gochecknoglobals # - gochecknoinits # this is too aggressive +# - rowserrcheck disabled per generics https://github.com/golangci/golangci-lint/issues/2649 # - godot # - godox -# - rowserrcheck # blocked until generics upgrade # - goerr113 # - goimports # we're using gosimports now instead to account for extra whitespaces (see https://github.com/golang/go/issues/20818) # - golint # deprecated @@ -49,7 +72,11 @@ linters: # - lll # without a way to specify per-line exception cases, this is not usable # - maligned # this is an excellent linter, but tricky to optimize and we are not sensitive to memory layout optimizations # - nestif -# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code -# - scopelint # deprecated +# - nolintlint # as of go1.19 this conflicts with the behavior of gofmt, which is a deal-breaker (lint-fix will still fail when running lint) +# - prealloc # following this rule isn't consistently a good idea, as it sometimes forces unnecessary allocations that result in less idiomatic code +# - rowserrcheck # not in a repo with sql, so this is not useful +# - scopelint # deprecated +# - structcheck # The owner seems to have abandoned the linter. Replaced by "unused". # - testpackage -# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) +# - varcheck # The owner seems to have abandoned the linter. Replaced by "unused". +# - wsl # this doens't have an auto-fixer yet and is pretty noisy (https://github.com/bombsimon/wsl/issues/90) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ce81a664..cb2df3ca 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -2,8 +2,14 @@ release: prerelease: auto draft: false +env: + # required to support multi architecture docker builds + - DOCKER_CLI_EXPERIMENTAL=enabled + - CGO_ENABLED=0 + builds: - id: linux-build + dir: ./cmd/xeol binary: xeol goos: - linux @@ -14,19 +20,17 @@ builds: - s390x # set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}' - env: &build-env - - CGO_ENABLED=0 ldflags: &build-ldflags | -w -s -extldflags '-static' -X github.com/xeol-io/xeol/internal/version.version={{.Version}} - -X github.com/xeol-io/xeol/internal/version.syftVersion={{.Env.SYFT_VERSION}} -X github.com/xeol-io/xeol/internal/version.gitCommit={{.Commit}} -X github.com/xeol-io/xeol/internal/version.buildDate={{.Date}} -X github.com/xeol-io/xeol/internal/version.gitDescription={{.Summary}} - id: darwin-build + dir: ./cmd/xeol binary: xeol goos: - darwin @@ -34,17 +38,16 @@ builds: - amd64 - arm64 mod_timestamp: *build-timestamp - env: *build-env ldflags: *build-ldflags - id: windows-build + dir: ./cmd/xeol binary: xeol goos: - windows goarch: - amd64 mod_timestamp: *build-timestamp - env: *build-env ldflags: *build-ldflags archives: @@ -71,7 +74,7 @@ nfpms: - deb brews: - - tap: + - repository: owner: xeol-io name: homebrew-xeol ids: @@ -80,3 +83,127 @@ brews: homepage: *website description: *description license: "Apache License 2.0" + +dockers: + - image_templates: + - noqcks/xeol:debug + - noqcks/xeol:{{.Tag}}-debug + - ghcr.io/noqcks/xeol:debug + - ghcr.io/noqcks/xeol:{{.Tag}}-debug + goarch: amd64 + dockerfile: Dockerfile.debug + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--build-arg=BUILD_DATE={{.Date}}" + - "--build-arg=BUILD_VERSION={{.Version}}" + - "--build-arg=VCS_REF={{.FullCommit}}" + - "--build-arg=VCS_URL={{.GitURL}}" + + - image_templates: + - noqcks/xeol:debug-arm64v8 + - noqcks/xeol:{{.Tag}}-debug-arm64v8 + - ghcr.io/noqcks/xeol:debug-arm64v8 + - ghcr.io/noqcks/xeol:{{.Tag}}-debug-arm64v8 + goarch: arm64 + dockerfile: Dockerfile.debug + use: buildx + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--build-arg=BUILD_DATE={{.Date}}" + - "--build-arg=BUILD_VERSION={{.Version}}" + - "--build-arg=VCS_REF={{.FullCommit}}" + - "--build-arg=VCS_URL={{.GitURL}}" + + - image_templates: + - noqcks/xeol:debug-s390x + - noqcks/xeol:{{.Tag}}-debug-s390x + - ghcr.io/noqcks/xeol:debug-s390x + - ghcr.io/noqcks/xeol:{{.Tag}}-debug-s390x + goarch: s390x + dockerfile: Dockerfile.debug + use: buildx + build_flag_templates: + - "--platform=linux/s390x" + - "--build-arg=BUILD_DATE={{.Date}}" + - "--build-arg=BUILD_VERSION={{.Version}}" + - "--build-arg=VCS_REF={{.FullCommit}}" + - "--build-arg=VCS_URL={{.GitURL}}" + + - image_templates: + - noqcks/xeol:latest + - noqcks/xeol:{{.Tag}} + - ghcr.io/noqcks/xeol:latest + - ghcr.io/noqcks/xeol:{{.Tag}} + goarch: amd64 + dockerfile: Dockerfile + use: buildx + build_flag_templates: + - "--platform=linux/amd64" + - "--build-arg=BUILD_DATE={{.Date}}" + - "--build-arg=BUILD_VERSION={{.Version}}" + - "--build-arg=VCS_REF={{.FullCommit}}" + - "--build-arg=VCS_URL={{.GitURL}}" + + - image_templates: + - noqcks/xeol:{{.Tag}}-arm64v8 + - ghcr.io/noqcks/xeol:{{.Tag}}-arm64v8 + goarch: arm64 + dockerfile: Dockerfile + use: buildx + build_flag_templates: + - "--platform=linux/arm64/v8" + - "--build-arg=BUILD_DATE={{.Date}}" + - "--build-arg=BUILD_VERSION={{.Version}}" + - "--build-arg=VCS_REF={{.FullCommit}}" + - "--build-arg=VCS_URL={{.GitURL}}" + + - image_templates: + - noqcks/xeol:{{.Tag}}-s390x + - ghcr.io/noqcks/xeol:{{.Tag}}-s390x + goarch: s390x + dockerfile: Dockerfile + use: buildx + build_flag_templates: + - "--platform=linux/s390x" + - "--build-arg=BUILD_DATE={{.Date}}" + - "--build-arg=BUILD_VERSION={{.Version}}" + - "--build-arg=VCS_REF={{.FullCommit}}" + - "--build-arg=VCS_URL={{.GitURL}}" + + +docker_manifests: + - name_template: noqcks/xeol:latest + image_templates: + - noqcks/xeol:{{.Tag}} + - noqcks/xeol:{{.Tag}}-arm64v8 + - noqcks/xeol:{{.Tag}}-s390x + + - name_template: noqcks/xeol:debug + - noqcks/xeol:{{.Tag}}-debug + - noqcks/xeol:{{.Tag}}-debug-arm64v8 + - noqcks/xeol:{{.Tag}}-debug-s390x + + - name_template: noqcks/xeol:{{.Tag}} + image_templates: + - noqcks/xeol:{{.Tag}} + - noqcks/xeol:{{.Tag}}-arm64v8 + - noqcks/xeol:{{.Tag}}-s390x + + - name_template: ghcr.io/noqcks/xeol:latest + image_templates: + - ghcr.io/noqcks/xeol:{{.Tag}} + - ghcr.io/noqcks/xeol:{{.Tag}}-arm64v8 + - ghcr.io/noqcks/xeol:{{.Tag}}-s390x + + - name_template: ghcr.io/noqcks/xeol:debug + image_templates: + - ghcr.io/noqcks/xeol:{{.Tag}}-debug + - ghcr.io/noqcks/xeol:{{.Tag}}-debug-arm64v8 + - ghcr.io/noqcks/xeol:{{.Tag}}-debug-s390x + + - name_template: ghcr.io/noqcks/xeol:{{.Tag}} + image_templates: + - ghcr.io/noqcks/xeol:{{.Tag}} + - ghcr.io/noqcks/xeol:{{.Tag}}-arm64v8 + - ghcr.io/noqcks/xeol:{{.Tag}}-s390x diff --git a/.goreleaser_docker.yaml b/.goreleaser_docker.yaml deleted file mode 100644 index c2e23193..00000000 --- a/.goreleaser_docker.yaml +++ /dev/null @@ -1,155 +0,0 @@ -# Separate docker configuration to isolate docker dependency away from -# mac-os runner on github actions. -release: - disable: true - -env: - # required to support multi architecture docker builds - - DOCKER_CLI_EXPERIMENTAL=enabled - -builds: - - id: linux-build - binary: xeol - goos: - - linux - goarch: - - amd64 - - arm64 - - s390x - # set the modified timestamp on the output binary to the git timestamp to ensure a reproducible build - mod_timestamp: &build-timestamp '{{ .CommitTimestamp }}' - env: &build-env - - CGO_ENABLED=0 - ldflags: &build-ldflags | - -w - -s - -extldflags '-static' - -X github.com/xeol-io/xeol/internal/version.version={{.Version}} - -X github.com/xeol-io/xeol/internal/version.syftVersion={{.Env.SYFT_VERSION}} - -X github.com/xeol-io/xeol/internal/version.gitCommit={{.Commit}} - -X github.com/xeol-io/xeol/internal/version.buildDate={{.Date}} - -X github.com/xeol-io/xeol/internal/version.gitDescription={{.Summary}} - -dockers: - - image_templates: - - noqcks/xeol:debug - - noqcks/xeol:{{.Tag}}-debug - - ghcr.io/noqcks/xeol:debug - - ghcr.io/noqcks/xeol:{{.Tag}}-debug - goarch: amd64 - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - noqcks/xeol:debug-arm64v8 - - noqcks/xeol:{{.Tag}}-debug-arm64v8 - - ghcr.io/noqcks/xeol:debug-arm64v8 - - ghcr.io/noqcks/xeol:{{.Tag}}-debug-arm64v8 - goarch: arm64 - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/arm64/v8" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - noqcks/xeol:debug-s390x - - noqcks/xeol:{{.Tag}}-debug-s390x - - ghcr.io/noqcks/xeol:debug-s390x - - ghcr.io/noqcks/xeol:{{.Tag}}-debug-s390x - goarch: s390x - dockerfile: Dockerfile.debug - use: buildx - build_flag_templates: - - "--platform=linux/s390x" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - noqcks/xeol:latest - - noqcks/xeol:{{.Tag}} - - ghcr.io/noqcks/xeol:latest - - ghcr.io/noqcks/xeol:{{.Tag}} - goarch: amd64 - dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/amd64" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - noqcks/xeol:{{.Tag}}-arm64v8 - - ghcr.io/noqcks/xeol:{{.Tag}}-arm64v8 - goarch: arm64 - dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/arm64/v8" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - - image_templates: - - noqcks/xeol:{{.Tag}}-s390x - - ghcr.io/noqcks/xeol:{{.Tag}}-s390x - goarch: s390x - dockerfile: Dockerfile - use: buildx - build_flag_templates: - - "--platform=linux/s390x" - - "--build-arg=BUILD_DATE={{.Date}}" - - "--build-arg=BUILD_VERSION={{.Version}}" - - "--build-arg=VCS_REF={{.FullCommit}}" - - "--build-arg=VCS_URL={{.GitURL}}" - - -docker_manifests: - - name_template: noqcks/xeol:latest - image_templates: - - noqcks/xeol:{{.Tag}} - - noqcks/xeol:{{.Tag}}-arm64v8 - - noqcks/xeol:{{.Tag}}-s390x - - - name_template: noqcks/xeol:debug - - noqcks/xeol:{{.Tag}}-debug - - noqcks/xeol:{{.Tag}}-debug-arm64v8 - - noqcks/xeol:{{.Tag}}-debug-s390x - - - name_template: noqcks/xeol:{{.Tag}} - image_templates: - - noqcks/xeol:{{.Tag}} - - noqcks/xeol:{{.Tag}}-arm64v8 - - noqcks/xeol:{{.Tag}}-s390x - - - name_template: ghcr.io/noqcks/xeol:latest - image_templates: - - ghcr.io/noqcks/xeol:{{.Tag}} - - ghcr.io/noqcks/xeol:{{.Tag}}-arm64v8 - - ghcr.io/noqcks/xeol:{{.Tag}}-s390x - - - name_template: ghcr.io/noqcks/xeol:debug - image_templates: - - ghcr.io/noqcks/xeol:{{.Tag}}-debug - - ghcr.io/noqcks/xeol:{{.Tag}}-debug-arm64v8 - - ghcr.io/noqcks/xeol:{{.Tag}}-debug-s390x - - - name_template: ghcr.io/noqcks/xeol:{{.Tag}} - image_templates: - - ghcr.io/noqcks/xeol:{{.Tag}} - - ghcr.io/noqcks/xeol:{{.Tag}}-arm64v8 - - ghcr.io/noqcks/xeol:{{.Tag}}-s390x diff --git a/Dockerfile.dev b/Dockerfile.dev index 94c46cdc..758c4ad0 100644 --- a/Dockerfile.dev +++ b/Dockerfile.dev @@ -1,6 +1,6 @@ FROM docker@sha256:020562d22f11c27997e00da910ed6b580d93094bc25841cb87aacab4ced4a882 -ENV GO_VERSION=1.20.7 +ENV GO_VERSION=1.21.1 ENV PATH=$PATH:/usr/local/go/bin:/usr/bin/env:/root/go/bin WORKDIR /xeol diff --git a/Makefile b/Makefile index 152c3429..f0a41ad5 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,9 @@ GOIMPORTS_CMD = $(TEMPDIR)/gosimports -local github.com/xeol-io RELEASE_CMD=$(TEMPDIR)/goreleaser release --clean SNAPSHOT_CMD=$(RELEASE_CMD) --skip-publish --snapshot VERSION=$(shell git describe --dirty --always --tags) +CHANGELOG := CHANGELOG.md +CHRONICLE_CMD = $(TEMPDIR)/chronicle + # formatting variables BOLD := $(shell tput -T linux bold) @@ -21,7 +24,7 @@ TITLE := $(BOLD)$(PURPLE) SUCCESS := $(BOLD)$(GREEN) # the quality gate lower threshold for unit test total % coverage (by function statements) -COVERAGE_THRESHOLD := 32 +COVERAGE_THRESHOLD := 34 ## Build variables DISTDIR=./dist @@ -36,6 +39,8 @@ CHRONICLE_VERSION = v0.7.0 GOSIMPORTS_VERSION = v0.3.8 YAJSV_VERSION = v1.4.1 GORELEASER_VERSION = v1.20.0 +GLOW_VERSION := v1.5.1 +SKOPEO_VERSION := v1.12.0 ifndef TEMPDIR $(error TEMPDIR is not set) @@ -76,36 +81,43 @@ test: unit cli ## Run all tests (unit, and CLI tests) help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "$(BOLD)$(CYAN)%-25s$(RESET)%s\n", $$1, $$2}' -.PHONY: ci-bootstrap -ci-bootstrap: - DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils - $(RESULTSDIR): mkdir -p $(RESULTSDIR) $(TEMPDIR): mkdir -p $(TEMPDIR) +.PHONY: format +format: ## Auto-format all source code + $(call title,Running formatters) + gofmt -w -s . + $(GOIMPORTS_CMD) -w . + go mod tidy + .PHONY: bootstrap-tools bootstrap-tools: $(TEMPDIR) + GO111MODULE=off GOBIN=$(realpath $(TEMPDIR)) go get -u golang.org/x/perf/cmd/benchstat curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ $(GOLANGCILINT_VERSION) curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ $(BOUNCER_VERSION) curl -sSfL https://raw.githubusercontent.com/anchore/chronicle/main/install.sh | sh -s -- -b $(TEMPDIR)/ $(CHRONICLE_VERSION) + .github/scripts/goreleaser-install.sh -d -b $(TEMPDIR)/ $(GORELEASER_VERSION) # the only difference between goimports and gosimports is that gosimports removes extra whitespace between import blocks (see https://github.com/golang/go/issues/20818) - GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION) - GOBIN="$(shell realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@$(YAJSV_VERSION) - .github/scripts/goreleaser-install.sh -b $(TEMPDIR)/ $(GORELEASER_VERSION) + GOBIN="$(realpath $(TEMPDIR))" go install github.com/rinchsan/gosimports/cmd/gosimports@$(GOSIMPORTS_VERSION) + GOBIN="$(realpath $(TEMPDIR))" go install github.com/neilpa/yajsv@$(YAJSV_VERSION) + GOBIN="$(realpath $(TEMPDIR))" go install github.com/charmbracelet/glow@$(GLOW_VERSION) + GOBIN="$(realpath $(TEMPDIR))" CGO_ENABLED=0 GO_DYN_FLAGS="" go install -tags "containers_image_openpgp" github.com/containers/skopeo/cmd/skopeo@$(SKOPEO_VERSION) + .PHONY: bootstrap-go bootstrap-go: go mod download .PHONY: bootstrap -bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir) +bootstrap: $(TEMPDIR) bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir) $(call title,Bootstrapping dependencies) .PHONY: static-analysis -static-analysis: lint check-go-mod-tidy check-licenses +static-analysis: check-go-mod-tidy lint check-licenses .PHONY: lint lint: ## Run gofmt + golangci lint checks @@ -132,7 +144,8 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers .PHONY: check-licenses check-licenses: - $(TEMPDIR)/bouncer check + $(call title,Checking for license compliance) + $(TEMPDIR)/bouncer check ./... check-go-mod-tidy: @ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!" @@ -143,13 +156,32 @@ validate-xeol-db-schema: python3 test/validate-xeol-db-schema.py .PHONY: unit -unit: ## Run unit tests (with coverage) +unit: $(TEMPDIR) ## Run unit tests (with coverage) $(call title,Running unit tests) - mkdir -p $(RESULTSDIR) - go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v xeol-io/xeol/test) - @go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL) - @echo "Coverage: $$(cat $(COVER_TOTAL))" - @if [ $$(echo "$$(cat $(COVER_TOTAL)) >= $(COVERAGE_THRESHOLD)" | bc -l) -ne 1 ]; then echo "$(RED)$(BOLD)Failed coverage quality gate (> $(COVERAGE_THRESHOLD)%)$(RESET)" && false; fi + go test -race -coverprofile $(TEMPDIR)/unit-coverage-details.txt $(shell go list ./... | grep -v xeol-io/xeol/test) + @.github/scripts/coverage.py $(COVERAGE_THRESHOLD) $(TEMPDIR)/unit-coverage-details.txt + + +.PHONY: ci-release +ci-release: ci-check clean-dist $(CHANGELOG) + $(call title,Publishing release artifacts) + + # create a config with the dist dir overridden + echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml + cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml + + bash -c "\ + $(RELEASE_CMD) \ + --config $(TEMPDIR)/goreleaser.yaml \ + --release-notes <(cat $(CHANGELOG)) \ + || (cat /tmp/quill-*.log && false)" + + # upload the version file that supports the application version update check (excluding pre-releases) + .github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)" + +.PHONY: ci-check +ci-check: + @.github/scripts/ci-check.sh .PHONY: quality quality: ## Run quality tests @@ -195,13 +227,31 @@ cli-fingerprint: .PHONY: cli cli: $(SNAPSHOTDIR) ## Run CLI tests chmod 755 "$(SNAPSHOT_BIN)" + $(SNAPSHOT_BIN) version XEOL_BINARY_LOCATION='$(SNAPSHOT_BIN)' \ go test -count=1 -v ./test/cli +# note: this is used by CI to determine if various test fixture cache should be restored or recreated +# TODO (cphillips) check for all fixtures and individual makefile +fingerprints: + $(call title,Creating all test cache input fingerprints) + + # for IMAGE integration test fixtures + cd test/integration/test-fixtures && \ + make cache.fingerprint + + # for INSTALL integration test fixtures + cd test/install && \ + make cache.fingerprint + + # for CLI test fixtures + cd test/cli/test-fixtures && \ + make cache.fingerprint + .PHONY: build build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages -$(SNAPSHOTDIR): ## Build snapshot release binaries and packages +$(SNAPSHOTDIR): ## Build snapshot release binaries and packages $(call title,Building snapshot artifacts) # create a config with the dist dir overridden @@ -209,71 +259,23 @@ $(SNAPSHOTDIR): ## Build snapshot release binaries and packages cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml # build release snapshots - bash -c "\ - SKIP_SIGNING=true \ - SYFT_VERSION=$(SYFT_VERSION)\ - $(SNAPSHOT_CMD) --skip-sign --config $(TEMPDIR)/goreleaser.yaml" + $(SNAPSHOT_CMD) --config $(TEMPDIR)/goreleaser.yaml .PHONY: changelog -changelog: clean-changelog CHANGELOG.md - @docker run -it --rm \ - -v $(shell pwd)/CHANGELOG.md:/CHANGELOG.md \ - rawkode/mdv \ - -t 748.5989 \ - /CHANGELOG.md +changelog: clean-changelog $(CHANGELOG) ## Generate and show the changelog for the current unreleased version + $(CHRONICLE_CMD) -vv -n --version-file VERSION > $(CHANGELOG) + @$(GLOW_CMD) $(CHANGELOG) -CHANGELOG.md: - $(TEMPDIR)/chronicle -vv > CHANGELOG.md +$(CHANGELOG): + $(CHRONICLE_CMD) -vvv > $(CHANGELOG) .PHONY: validate-syft-release-version validate-syft-release-version: @./.github/scripts/syft-released-version-check.sh .PHONY: release -release: clean-dist # CHANGELOG.md ## Build and publish final binaries and packages. Intended to be run only on macOS. - $(call title,Publishing release artifacts) - - # create a config with the dist dir overridden - echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml - cat .goreleaser.yaml >> $(TEMPDIR)/goreleaser.yaml - - bash -c "\ - SYFT_VERSION=$(SYFT_VERSION)\ - $(RELEASE_CMD) \ - --config $(TEMPDIR)/goreleaser.yaml \ - --skip-sign \ - # --release-notes <(cat CHANGELOG.md)\ - || false" - - # TODO: turn this into a post-release hook - # upload the version file that supports the application version update check (excluding pre-releases) - .github/scripts/update-version-file.sh "$(DISTDIR)" "$(VERSION)" - -.PHONY: release-docker-assets -release-docker-assets: - $(call title,Publishing docker release assets) - - # create a config with the dist dir overridden - echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml - cat .goreleaser_docker.yaml >> $(TEMPDIR)/goreleaser.yaml - - bash -c "\ - SYFT_VERSION=$(SYFT_VERSION)\ - $(RELEASE_CMD) \ - --config $(TEMPDIR)/goreleaser.yaml \ - --parallelism 1" - -snapshot-docker-assets: # Build snapshot images of docker images that will be published on release - $(call title,Building snapshot docker release assets) - - # create a config with the dist dir overridden - echo "dist: $(DISTDIR)" > $(TEMPDIR)/goreleaser.yaml - cat .goreleaser_docker.yaml >> $(TEMPDIR)/goreleaser.yaml - - bash -c "\ - SYFT_VERSION=$(SYFT_VERSION)\ - $(SNAPSHOT_CMD) \ - --config $(TEMPDIR)/goreleaser.yaml" +release: + @.github/scripts/trigger-release.sh .PHONY: clean clean: clean-dist clean-snapshot ## Remove previous builds and result reports diff --git a/README.md b/README.md index acaa0b4a..bf215a24 100644 --- a/README.md +++ b/README.md @@ -54,7 +54,7 @@ If you're using GitHub Actions, you can simply use the [Xeol GitHub action](http xeol ``` -The above command scans for EOL packages that are visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the vulnerability scan, regardless of its presence in the final image, provide `--scope all-layers`: +The above command scans for EOL packages that are visible in the container (i.e., the squashed representation of the image). To include software from all image layers in the scan, regardless of its presence in the final image, provide `--scope all-layers`: ``` xeol --scope all-layers @@ -174,13 +174,13 @@ You can set the cache directory path using the environment variable `XEOL_DB_CAC #### Data staleness -xeol needs up-to-date vulnerability information to provide accurate matches. By default, it will fail execution if the local database was not built in the last 5 days. The data staleness check is configurable via the environment variable `XEOL_DB_MAX_ALLOWED_BUILT_AGE` and `XEOL_DB_VALIDATE_AGE` or the field `max-allowed-built-age` and `validate-age`, under `db`. It uses [golang's time duration syntax](https://pkg.go.dev/time#ParseDuration). Set `XEOL_DB_VALIDATE_AGE` or `validate-age` to `false` to disable staleness check. +xeol needs up-to-date information to provide accurate EOL matches. By default, it will fail execution if the local database was not built in the last 5 days. The data staleness check is configurable via the environment variable `XEOL_DB_MAX_ALLOWED_BUILT_AGE` and `XEOL_DB_VALIDATE_AGE` or the field `max-allowed-built-age` and `validate-age`, under `db`. It uses [golang's time duration syntax](https://pkg.go.dev/time#ParseDuration). Set `XEOL_DB_VALIDATE_AGE` or `validate-age` to `false` to disable staleness check. #### Offline and air-gapped environments By default, xeol checks for a new database on every run, by making a network call over the Internet. You can tell xeol not to perform this check by setting the environment variable `XEOL_DB_AUTO_UPDATE` to `false`. -As long as you place xeol's `vulnerability.db` and `metadata.json` files in the cache directory for the expected schema version, xeol has no need to access the network. Additionally, you can get a listing of the database archives available for download from the `xeol db list` command in an online environment, download the database archive, transfer it to your offline environment, and use `xeol db import ` to use the given database in an offline capacity. +As long as you place xeol's `xeol.db` and `metadata.json` files in the cache directory for the expected schema version, xeol has no need to access the network. Additionally, you can get a listing of the database archives available for download from the `xeol db list` command in an online environment, download the database archive, transfer it to your offline environment, and use `xeol db import ` to use the given database in an offline capacity. If you would like to distribute your own xeol databases internally without needing to use `db import` manually you can leverage xeol's DB update mechanism. To do this you can craft your own `listing.json` file similar to the one found publically (see `xeol db list -o raw` for an example of our public `listing.json` file) and change the download URL to point to an internal endpoint (e.g. a private S3 bucket, an internal file server, etc). Any internal installation of xeol can receive database updates automatically by configuring the `db.update-url` (same as the `XEOL_DB_UPDATE_URL` environment variable) to point to the hosted `listing.json` file you've crafted. @@ -205,7 +205,7 @@ Find complete information on xeol's database commands by running `xeol db --help xeol supplies shell completion through its CLI implementation ([cobra](https://github.com/spf13/cobra/blob/master/shell_completions.md)). Generate the completion code for your shell by running one of the following commands: - `xeol completion ` -- `go run main.go completion ` +- `go run ./cmd/xeol completion ` This will output a shell script to STDOUT, which can then be used as a completion script for xeol. Running one of the above commands with the `-h` or `--help` flags will provide instructions on how to do that for your chosen shell. diff --git a/cmd/cmd.go b/cmd/cmd.go deleted file mode 100644 index d07974da..00000000 --- a/cmd/cmd.go +++ /dev/null @@ -1,132 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "os" - "sort" - - "github.com/anchore/go-logger/adapter/logrus" - "github.com/anchore/stereoscope" - "github.com/anchore/syft/syft" - "github.com/gookit/color" - logrusUpstream "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/spf13/viper" - "github.com/wagoodman/go-partybus" - - "github.com/xeol-io/xeol/internal/config" - "github.com/xeol-io/xeol/internal/log" - "github.com/xeol-io/xeol/internal/version" - "github.com/xeol-io/xeol/xeol" -) - -var ( - appConfig *config.Application - eventBus *partybus.Bus - eventSubscription *partybus.Subscription -) - -func init() { - cobra.OnInitialize( - initRootCmdConfigOptions, - initAppConfig, - initLogging, - logAppConfig, - logAppVersion, - initEventBus, - ) -} - -func Execute() { - if err := rootCmd.Execute(); err != nil { - _ = stderrPrintLnf(err.Error()) - os.Exit(1) - } -} - -func initRootCmdConfigOptions() { - if err := bindRootConfigOptions(rootCmd.Flags()); err != nil { - panic(err) - } -} - -func initAppConfig() { - cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts) - if err != nil { - fmt.Printf("failed to load application config: \n\t%+v\n", err) - os.Exit(1) - } - appConfig = cfg -} - -func initLogging() { - cfg := logrus.Config{ - EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet, - FileLocation: appConfig.Log.FileLocation, - Level: appConfig.Log.Level, - } - - if appConfig.Log.Structured { - cfg.Formatter = &logrusUpstream.JSONFormatter{ - TimestampFormat: "2006-01-02T15:04:05.000Z", - DisableTimestamp: false, - DisableHTMLEscape: false, - PrettyPrint: false, - } - } - - logWrapper, err := logrus.New(cfg) - if err != nil { - // this is kinda circular, but we can't return an error... ¯\_(ツ)_/¯ - // I'm going to leave this here in case we one day have a different default logger other than the "discard" logger - log.Error("unable to initialize logger: %+v", err) - return - } - xeol.SetLogger(logWrapper) - syft.SetLogger(logWrapper.Nested("form-lib", "syft")) - stereoscope.SetLogger(logWrapper.Nested("form-lib", "stereoscope")) -} - -func logAppConfig() { - log.Debugf("application config:\n%+v", color.Magenta.Sprint(appConfig.String())) -} - -func logAppVersion() { - versionInfo := version.FromBuild() - log.Infof("xeol version: %s", versionInfo.Version) - - var fields map[string]interface{} - bytes, err := json.Marshal(versionInfo) - if err != nil { - return - } - err = json.Unmarshal(bytes, &fields) - if err != nil { - return - } - - keys := make([]string, 0, len(fields)) - for k := range fields { - keys = append(keys, k) - } - sort.Strings(keys) - - for idx, field := range keys { - value := fields[field] - branch := "├──" - if idx == len(fields)-1 { - branch = "└──" - } - log.Debugf(" %s %s: %s", branch, field, value) - } -} - -func initEventBus() { - eventBus = partybus.NewBus() - eventSubscription = eventBus.Subscribe() - - stereoscope.SetBus(eventBus) - syft.SetBus(eventBus) - xeol.SetBus(eventBus) -} diff --git a/cmd/db.go b/cmd/db.go deleted file mode 100644 index 835bcd8a..00000000 --- a/cmd/db.go +++ /dev/null @@ -1,14 +0,0 @@ -package cmd - -import ( - "github.com/spf13/cobra" -) - -var dbCmd = &cobra.Command{ - Use: "db", - Short: "eol database operations", -} - -func init() { - rootCmd.AddCommand(dbCmd) -} diff --git a/cmd/db_delete.go b/cmd/db_delete.go deleted file mode 100644 index ad02f23d..00000000 --- a/cmd/db_delete.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/xeol-io/xeol/xeol/db" -) - -var dbDeleteCmd = &cobra.Command{ - Use: "delete", - Short: "delete the eol database", - Args: cobra.ExactArgs(0), - RunE: runDBDeleteCmd, -} - -func init() { - dbCmd.AddCommand(dbDeleteCmd) -} - -func runDBDeleteCmd(_ *cobra.Command, _ []string) error { - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) - if err != nil { - return err - } - - if err := dbCurator.Delete(); err != nil { - return fmt.Errorf("unable to delete eol database: %+v", err) - } - - return stderrPrintLnf("eol database deleted") -} diff --git a/cmd/db_import.go b/cmd/db_import.go deleted file mode 100644 index a70d826f..00000000 --- a/cmd/db_import.go +++ /dev/null @@ -1,35 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/xeol-io/xeol/internal" - "github.com/xeol-io/xeol/xeol/db" -) - -var dbImportCmd = &cobra.Command{ - Use: "import FILE", - Short: "import an EOL database archive", - Long: fmt.Sprintf("import an EOL database archive from a local FILE.\nDB archives can be obtained from %q.", internal.DBUpdateURL), - Args: cobra.ExactArgs(1), - RunE: runDBImportCmd, -} - -func init() { - dbCmd.AddCommand(dbImportCmd) -} - -func runDBImportCmd(_ *cobra.Command, args []string) error { - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) - if err != nil { - return err - } - - if err := dbCurator.ImportFrom(args[0]); err != nil { - return fmt.Errorf("unable to import EOL database: %+v", err) - } - - return stderrPrintLnf("EOL database imported") -} diff --git a/cmd/db_status.go b/cmd/db_status.go deleted file mode 100644 index b33cafa9..00000000 --- a/cmd/db_status.go +++ /dev/null @@ -1,42 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/spf13/cobra" - - "github.com/xeol-io/xeol/xeol/db" -) - -var statusCmd = &cobra.Command{ - Use: "status", - Short: "display database status", - Args: cobra.ExactArgs(0), - RunE: runDBStatusCmd, -} - -func init() { - dbCmd.AddCommand(statusCmd) -} - -func runDBStatusCmd(_ *cobra.Command, _ []string) error { - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) - if err != nil { - return err - } - - status := dbCurator.Status() - - statusStr := "valid" - if status.Err != nil { - statusStr = "invalid" - } - - fmt.Println("Location: ", status.Location) - fmt.Println("Built: ", status.Built.String()) - fmt.Println("Schema: ", status.SchemaVersion) - fmt.Println("Checksum: ", status.Checksum) - fmt.Println("Status: ", statusStr) - - return status.Err -} diff --git a/cmd/db_update.go b/cmd/db_update.go deleted file mode 100644 index 5bb22a91..00000000 --- a/cmd/db_update.go +++ /dev/null @@ -1,72 +0,0 @@ -package cmd - -import ( - "fmt" - - "github.com/anchore/stereoscope" - "github.com/spf13/cobra" - "github.com/wagoodman/go-partybus" - - "github.com/xeol-io/xeol/internal/bus" - "github.com/xeol-io/xeol/internal/log" - "github.com/xeol-io/xeol/internal/ui" - "github.com/xeol-io/xeol/xeol/db" - "github.com/xeol-io/xeol/xeol/event" -) - -var dbUpdateCmd = &cobra.Command{ - Use: "update", - Short: "download the latest eol database", - Args: cobra.ExactArgs(0), - RunE: runDBUpdateCmd, -} - -func init() { - dbCmd.AddCommand(dbUpdateCmd) -} - -func startDBUpdateCmd() <-chan error { - errs := make(chan error) - go func() { - defer close(errs) - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) - if err != nil { - errs <- err - return - } - updated, err := dbCurator.Update() - if err != nil { - errs <- fmt.Errorf("unable to update eol database: %+v", err) - } - - result := "No eol database update available\n" - if updated { - result = "eol database updated to latest version!\n" - } - - bus.Publish(partybus.Event{ - Type: event.NonRootCommandFinished, - Value: result, - }) - }() - return errs -} - -func runDBUpdateCmd(_ *cobra.Command, _ []string) error { - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - if err != nil { - return err - } - return eventLoop( - startDBUpdateCmd(), - setupSignals(), - eventSubscription, - stereoscope.Cleanup, - ui.Select(isVerbose(), appConfig.Quiet, reporter)..., - ) -} diff --git a/cmd/event_loop.go b/cmd/event_loop.go deleted file mode 100644 index 4436596c..00000000 --- a/cmd/event_loop.go +++ /dev/null @@ -1,98 +0,0 @@ -package cmd - -import ( - "errors" - "fmt" - "os" - - "github.com/hashicorp/go-multierror" - "github.com/wagoodman/go-partybus" - - "github.com/xeol-io/xeol/internal/log" - "github.com/xeol-io/xeol/internal/ui" -) - -// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and -// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until -// an eventual graceful exit. -func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, cleanupFn func(), uxs ...ui.UI) error { - defer cleanupFn() - events := subscription.Events() - var err error - var ux ui.UI - - if ux, err = setupUI(subscription.Unsubscribe, uxs...); err != nil { - return err - } - - var retErr error - var forceTeardown bool - - for { - if workerErrs == nil && events == nil { - break - } - select { - case err, isOpen := <-workerErrs: - if !isOpen { - workerErrs = nil - continue - } - if err != nil { - // capture the error from the worker and unsubscribe to complete a graceful shutdown - retErr = multierror.Append(retErr, err) - _ = subscription.Unsubscribe() - // the worker has exited, we may have been mid-handling events for the UI which should now be - // ignored, in which case forcing a teardown of the UI irregardless of the state is required. - forceTeardown = true - } - case e, isOpen := <-events: - if !isOpen { - events = nil - continue - } - - if err := ux.Handle(e); err != nil { - if errors.Is(err, partybus.ErrUnsubscribe) { - events = nil - } else { - retErr = multierror.Append(retErr, err) - // TODO: should we unsubscribe? should we try to halt execution? or continue? - } - } - case <-signals: - // ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up. - // we ignore further errors since cleaning up the tmp directories will affect running catalogers that are - // reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result. - - // TODO: potential future improvement would be to pass context into workers with a cancel function that is - // to the event loop. In this way we can have a more controlled shutdown even at the most nested levels - // of processing. - events = nil - workerErrs = nil - forceTeardown = true - } - } - - if err := ux.Teardown(forceTeardown); err != nil { - retErr = multierror.Append(retErr, err) - } - - return retErr -} - -// setupUI takes one or more UIs that responds to events and takes a event bus unsubscribe function for use -// during teardown. With the given UIs, the first UI which the ui.Setup() function does not return an error -// will be utilized in execution. Providing a set of UIs allows for the caller to provide graceful fallbacks -// when there are environmental problem (e.g. unable to setup a TUI with the current TTY). -func setupUI(unsubscribe func() error, uis ...ui.UI) (ui.UI, error) { - for _, ux := range uis { - if err := ux.Setup(unsubscribe); err != nil { - log.Warnf("unable to setup given UI, falling back to alternative UI: %+v", err) - continue - } - - return ux, nil - } - return nil, fmt.Errorf("unable to setup any UI") -} diff --git a/cmd/event_loop_test.go b/cmd/event_loop_test.go deleted file mode 100644 index 613a073d..00000000 --- a/cmd/event_loop_test.go +++ /dev/null @@ -1,456 +0,0 @@ -package cmd - -import ( - "fmt" - "os" - "syscall" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/wagoodman/go-partybus" - - "github.com/xeol-io/xeol/internal/ui" - "github.com/xeol-io/xeol/xeol/event" -) - -var _ ui.UI = (*uiMock)(nil) - -type uiMock struct { - t *testing.T - finalEvent partybus.Event - unsubscribe func() error - mock.Mock -} - -func (u *uiMock) Setup(unsubscribe func() error) error { - u.t.Logf("UI Setup called") - u.unsubscribe = unsubscribe - return u.Called(unsubscribe).Error(0) -} - -func (u *uiMock) Handle(event partybus.Event) error { - u.t.Logf("UI Handle called: %+v", event.Type) - if event == u.finalEvent { - assert.NoError(u.t, u.unsubscribe()) - } - return u.Called(event).Error(0) -} - -func (u *uiMock) Teardown(_ bool) error { - u.t.Logf("UI Teardown called") - return u.Called().Error(0) -} - -func Test_eventLoop_gracefulExit(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.EolScanningFinished, - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - // ensure the mock sees at least the final event - ux.On("Handle", finalEvent).Return(nil) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - assert.NoError(t, - eventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_eventLoop_workerError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - workerErr := fmt.Errorf("worker error") - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - ret <- workerErr - t.Log("worker sent error") - close(ret) - t.Log("worker closed") - // note: NO final event is fired - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - } - - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // ensure we see an error returned - assert.ErrorIs(t, - eventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - workerErr, - "should have seen a worker error, but did not", - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_eventLoop_unsubscribeError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.EolScanningFinished, - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - // ensure the mock sees at least the final event... note the unsubscribe error here - ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that - // this case is handled as a controlled shutdown (this test should not timeout) - assert.NoError(t, - eventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_eventLoop_handlerError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.EolScanningFinished, - Error: fmt.Errorf("unable to create presenter"), - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - // ensure the mock sees at least the final event... note the event error is propagated - ux.On("Handle", finalEvent).Return(finalEvent.Error) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // handle errors SHOULD propagate the event loop. We are additionally asserting that this case is - // handled as a controlled shutdown (this test should not timeout) - assert.ErrorIs(t, - eventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - finalEvent.Error, - "should have seen a event error, but did not", - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_eventLoop_signalsStopExecution(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - worker := func() <-chan error { - // the worker will never return work and the event loop will always be waiting... - return make(chan error) - } - - signaler := func() <-chan os.Signal { - ret := make(chan os.Signal) - go func() { - ret <- syscall.SIGINT - // note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit - }() - return ret - } - - ux := &uiMock{ - t: t, - } - - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(nil) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - assert.NoError(t, - eventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func Test_eventLoop_uiTeardownError(t *testing.T) { - test := func(t *testing.T) { - - testBus := partybus.NewBus() - subscription := testBus.Subscribe() - t.Cleanup(testBus.Close) - - finalEvent := partybus.Event{ - Type: event.EolScanningFinished, - } - - worker := func() <-chan error { - ret := make(chan error) - go func() { - t.Log("worker running") - // send an empty item (which is ignored) ensuring we've entered the select statement, - // then close (a partial shutdown). - ret <- nil - t.Log("worker sent nothing") - close(ret) - t.Log("worker closed") - // do the other half of the shutdown - testBus.Publish(finalEvent) - t.Log("worker published final event") - }() - return ret - } - - signaler := func() <-chan os.Signal { - return nil - } - - ux := &uiMock{ - t: t, - finalEvent: finalEvent, - } - - teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down") - - // ensure the mock sees at least the final event... note the event error is propagated - ux.On("Handle", finalEvent).Return(nil) - // ensure the mock sees basic setup/teardown events - ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil) - ux.On("Teardown").Return(teardownError) - - var cleanupCalled bool - cleanupFn := func() { - t.Log("cleanup called") - cleanupCalled = true - } - - // ensure we see an error returned - assert.ErrorIs(t, - eventLoop( - worker(), - signaler(), - subscription, - cleanupFn, - ux, - ), - teardownError, - "should have seen a UI teardown error, but did not", - ) - - assert.True(t, cleanupCalled, "cleanup function not called") - ux.AssertExpectations(t) - } - - // if there is a bug, then there is a risk of the event loop never returning - testWithTimeout(t, 5*time.Second, test) -} - -func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) { - done := make(chan bool) - go func() { - test(t) - done <- true - }() - - select { - case <-time.After(timeout): - t.Fatal("test timed out") - case <-done: - } -} diff --git a/cmd/report_writer.go b/cmd/report_writer.go deleted file mode 100644 index 99cdbfcd..00000000 --- a/cmd/report_writer.go +++ /dev/null @@ -1,33 +0,0 @@ -package cmd - -import ( - "fmt" - "io" - "os" - "strings" -) - -func reportWriter() (io.Writer, func() error, error) { - nop := func() error { return nil } - path := strings.TrimSpace(appConfig.File) - - switch len(path) { - case 0: - return os.Stdout, nop, nil - - default: - reportFile, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) - - if err != nil { - return nil, nop, fmt.Errorf("unable to create report file: %w", err) - } - - return reportFile, func() error { - if !appConfig.Quiet { - fmt.Printf("Report written to %q\n", path) - } - - return reportFile.Close() - }, nil - } -} diff --git a/cmd/root.go b/cmd/root.go deleted file mode 100644 index 22bb6d18..00000000 --- a/cmd/root.go +++ /dev/null @@ -1,448 +0,0 @@ -package cmd - -import ( - "bytes" - "encoding/base64" - "errors" - "fmt" - "os" - "sync" - "time" - - "github.com/CycloneDX/cyclonedx-go" - "github.com/anchore/stereoscope" - "github.com/anchore/syft/syft/formats/common/cyclonedxhelpers" - "github.com/anchore/syft/syft/sbom" - "github.com/anchore/syft/syft/source" - "github.com/spf13/cobra" - "github.com/spf13/pflag" - "github.com/spf13/viper" - "github.com/wagoodman/go-partybus" - - "github.com/xeol-io/xeol/internal" - "github.com/xeol-io/xeol/internal/bus" - "github.com/xeol-io/xeol/internal/config" - "github.com/xeol-io/xeol/internal/format" - "github.com/xeol-io/xeol/internal/log" - "github.com/xeol-io/xeol/internal/ui" - "github.com/xeol-io/xeol/internal/version" - "github.com/xeol-io/xeol/internal/xeolio" - "github.com/xeol-io/xeol/xeol" - "github.com/xeol-io/xeol/xeol/db" - "github.com/xeol-io/xeol/xeol/event" - "github.com/xeol-io/xeol/xeol/matcher" - distroMatcher "github.com/xeol-io/xeol/xeol/matcher/distro" - pkgMatcher "github.com/xeol-io/xeol/xeol/matcher/packages" - "github.com/xeol-io/xeol/xeol/pkg" - "github.com/xeol-io/xeol/xeol/policy" - "github.com/xeol-io/xeol/xeol/policy/types" - "github.com/xeol-io/xeol/xeol/presenter" - "github.com/xeol-io/xeol/xeol/presenter/models" - "github.com/xeol-io/xeol/xeol/report" - "github.com/xeol-io/xeol/xeol/store" - "github.com/xeol-io/xeol/xeol/xeolerr" -) - -var persistentOpts = config.CliOnlyOptions{} - -var rootCmd = &cobra.Command{ - Use: fmt.Sprintf("%s [IMAGE]", internal.ApplicationName), - Short: "A scanner for end-of-life (EOL) software in container images, filesystems, and SBOMs", - Long: format.Tprintf(`A scanner for end-of-life (EOL) software in container images, filesystems, and SBOMs. - - -Supports the following image sources: - {{.appName}} yourrepo/yourimage:latest defaults to using images from a Docker daemon - -You can also explicitly specify the schema to use: - {{.appName}} docker://yourrepo/yourimage:latest explicitly use the Docker daemon -`, map[string]interface{}{"appName": internal.ApplicationName}), - Args: validateRootArgs, - SilenceUsage: true, - SilenceErrors: true, - RunE: rootExec, - ValidArgsFunction: dockerImageValidArgsFunction, -} - -func validateRootArgs(cmd *cobra.Command, args []string) error { - isPipedInput, err := internal.IsPipedInput() - if err != nil { - log.Warnf("unable to determine if there is piped input: %+v", err) - isPipedInput = false - } - - if len(args) == 0 && !isPipedInput { - // in the case that no arguments are given and there is no piped input we want to show the help text and return with a non-0 return code. - if err := cmd.Help(); err != nil { - return fmt.Errorf("unable to display help: %w", err) - } - return fmt.Errorf("an image/directory argument is required") - } - - if appConfig.APIKey != "" && appConfig.ProjectName == "" { - return fmt.Errorf("err: couldn't automatically detect a project name. Please set the project name using --project-name flag when using --api-key flag with xeol.io") - } - - return cobra.MaximumNArgs(1)(cmd, args) -} - -func init() { - setGlobalCliOptions() - setRootFlags(rootCmd.Flags()) -} - -func setGlobalCliOptions() { - // setup global CLI options (available on all CLI commands) - rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application configuration file") - - flag := "quiet" - rootCmd.PersistentFlags().BoolP( - flag, "q", false, - "suppress all logging output", - ) - if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil { - fmt.Printf("unable to bind flag '%s': %+v", flag, err) - os.Exit(1) - } - rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)") -} - -func setRootFlags(flags *pflag.FlagSet) { - flags.StringP( - "scope", "s", source.SquashedScope.String(), - fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes), - ) - - flags.StringP( - "name", "", "", - "set the name of the target being analyzed", - ) - - flags.StringP( - "output", "o", "", - fmt.Sprintf("report output formatter, formats=%v", presenter.AvailableFormats), - ) - - flags.StringP( - "project-name", "", "", - "manually set the name of the project being analyzed for xeol.io. If you are running xeol inside a git repository, this will be automatically detected.", - ) - - flags.StringP( - "image-path", "", "", - "set the path to the image being analyzed for xeol.io (e.g /src/Dockerfile)", - ) - - flags.StringP( - "api-key", "", "", - "set the API key for xeol.io. When this is set, scans will be uploaded to xeol.io.", - ) - - flags.BoolP( - "fail-on-eol-found", "f", false, - "set the return code to 1 if an EOL package is found", - ) - - flags.StringP( - "file", "", "", - "file to write the report output to (default is STDOUT)", - ) - - flags.StringP( - "lookahead", "l", "30d", - "an optional lookahead specifier when matching EOL dates (e.g. 'none', '1d', '1w', '1m', '1y'). Packages are matched when their EOL date < today+lookahead", - ) - - flags.StringP( - "platform", "", "", - "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')", - ) -} - -func bindRootConfigOptions(flags *pflag.FlagSet) error { - if err := viper.BindPFlag("search.scope", flags.Lookup("scope")); err != nil { - return err - } - - if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil { - return err - } - - if err := viper.BindPFlag("fail-on-eol-found", flags.Lookup("fail-on-eol-found")); err != nil { - return err - } - - if err := viper.BindPFlag("project-name", flags.Lookup("project-name")); err != nil { - return err - } - - if err := viper.BindPFlag("image-path", flags.Lookup("image-path")); err != nil { - return err - } - - if err := viper.BindPFlag("api-key", flags.Lookup("api-key")); err != nil { - return err - } - - if err := viper.BindPFlag("lookahead", flags.Lookup("lookahead")); err != nil { - return err - } - - if err := viper.BindPFlag("file", flags.Lookup("file")); err != nil { - return err - } - - if err := viper.BindPFlag("name", flags.Lookup("name")); err != nil { - return err - } - - err := viper.BindPFlag("platform", flags.Lookup("platform")) - return err -} - -func rootExec(_ *cobra.Command, args []string) error { - // we may not be provided an image if the user is piping in SBOM input - var userInput string - if len(args) > 0 { - userInput = args[0] - } - - reporter, closer, err := reportWriter() - defer func() { - if err := closer(); err != nil { - log.Warnf("unable to write to report destination: %+v", err) - } - }() - if err != nil { - return err - } - - return eventLoop( - startWorker(userInput, appConfig.FailOnEolFound, appConfig.EolMatchDate), - setupSignals(), - eventSubscription, - stereoscope.Cleanup, - ui.Select(isVerbose(), appConfig.Quiet, reporter)..., - ) -} - -func isVerbose() (result bool) { - isPipedInput, err := internal.IsPipedInput() - if err != nil { - // since we can't tell if there was piped input we assume that there could be to disable the ETUI - log.Warnf("unable to determine if there is piped input: %+v", err) - return true - } - // verbosity should consider if there is piped input (in which case we should not show the ETUI) - return appConfig.CliOptions.Verbosity > 0 || isPipedInput -} - -//nolint:funlen,gocognit -func startWorker(userInput string, failOnEolFound bool, eolMatchDate time.Time) <-chan error { - errs := make(chan error) - go func() { - defer close(errs) - - presenterConfig, err := presenter.ValidatedConfig(appConfig.Output) - if err != nil { - errs <- err - return - } - - checkForAppUpdate() - - var store *store.Store - var status *db.Status - var dbCloser *db.Closer - var packages []pkg.Package - var sbom *sbom.SBOM - var pkgContext pkg.Context - var wg = &sync.WaitGroup{} - var loadedDB, gatheredPackages bool - var policies []policy.Policy - var certificates string - var eventSourceScheme source.Scheme - x := xeolio.NewXeolClient(appConfig.APIKey) - - wg.Add(3) - go func() { - defer wg.Done() - log.Debug("Fetching organization policies") - if appConfig.APIKey != "" { - policies, err = x.FetchPolicies() - if err != nil { - errs <- fmt.Errorf("failed to fetch policy: %w", err) - return - } - certificates, err = x.FetchCertificates() - if err != nil { - errs <- fmt.Errorf("failed to fetch certificate: %w", err) - return - } - } - }() - - go func() { - defer wg.Done() - log.Debug("loading DB") - store, status, dbCloser, err = xeol.LoadEolDB(appConfig.DB.ToCuratorConfig(), appConfig.DB.AutoUpdate) - if err = validateDBLoad(err, status); err != nil { - errs <- err - return - } - loadedDB = true - }() - - go func() { - defer wg.Done() - log.Debugf("gathering packages") - packages, pkgContext, sbom, err = pkg.Provide(userInput, getProviderConfig()) - if err != nil { - errs <- fmt.Errorf("failed to catalog: %w", err) - return - } - eventSourceScheme = xeolio.EventSourceScheme(sbom.Source) - gatheredPackages = true - }() - wg.Wait() - if !loadedDB || !gatheredPackages { - return - } - - if dbCloser != nil { - defer dbCloser.Close() - } - - matchers := matcher.NewDefaultMatchers(matcher.Config{ - Packages: pkgMatcher.MatcherConfig(appConfig.Match.Packages), - Distro: distroMatcher.MatcherConfig(appConfig.Match.Distro), - }) - - allMatches, err := xeol.FindEol(*store, pkgContext.Distro, matchers, packages, failOnEolFound, eolMatchDate) - if err != nil { - errs <- err - if !errors.Is(err, xeolerr.ErrEolFound) { - return - } - } - - pb := models.PresenterConfig{ - Matches: allMatches, - Packages: packages, - SBOM: sbom, - Context: pkgContext, - AppConfig: appConfig, - DBStatus: status, - } - - var failScan bool - var imageVerified bool - for _, p := range policies { - switch p.GetPolicyType() { - case types.PolicyTypeNotary: - // Notary policy is only applicable to images - if eventSourceScheme != source.ImageScheme { - continue - } - shouldFailScan, res := p.Evaluate(allMatches, appConfig.ProjectName, userInput, certificates) - imageVerified = res.GetVerified() - if shouldFailScan { - failScan = true - } - - case types.PolicyTypeEol: - shouldFailScan, _ := p.Evaluate(allMatches, appConfig.ProjectName, "", "") - if shouldFailScan { - failScan = true - } - } - } - - if appConfig.APIKey != "" { - buf := new(bytes.Buffer) - bom := cyclonedxhelpers.ToFormatModel(*sbom) - enc := cyclonedx.NewBOMEncoder(buf, cyclonedx.BOMFileFormatJSON) - if err := enc.Encode(bom); err != nil { - errs <- fmt.Errorf("failed to encode sbom: %w", err) - return - } - - eventSource, err := xeolio.NewEventSource(sbom.Source) - if err != nil { - errs <- fmt.Errorf("failed to create event source: %w", err) - return - } - - if err := x.SendEvent(report.XeolEventPayload{ - Matches: allMatches.Sorted(), - Packages: packages, - Context: pkgContext, - AppConfig: appConfig, - EventSource: eventSource, - ImageVerified: imageVerified, - Sbom: base64.StdEncoding.EncodeToString(buf.Bytes()), - }); err != nil { - errs <- fmt.Errorf("failed to send eol event: %w", err) - return - } - } - - bus.Publish(partybus.Event{ - Type: event.EolScanningFinished, - Value: presenter.GetPresenter(presenterConfig, pb), - }) - - if failScan { - errs <- xeolerr.ErrPolicyViolation - return - } - }() - return errs -} - -func getProviderConfig() pkg.ProviderConfig { - return pkg.ProviderConfig{ - SyftProviderConfig: pkg.SyftProviderConfig{ - RegistryOptions: appConfig.Registry.ToOptions(), - Exclusions: nil, - CatalogingOptions: appConfig.Search.ToConfig(), - Platform: appConfig.Platform, - Name: appConfig.Name, - DefaultImagePullSource: appConfig.DefaultImagePullSource, - }, - } -} - -func validateDBLoad(loadErr error, status *db.Status) error { - if loadErr != nil { - return fmt.Errorf("failed to load eol db: %w", loadErr) - } - if status == nil { - return fmt.Errorf("unable to determine the status of the eol db") - } - if status.Err != nil { - return fmt.Errorf("db could not be loaded: %w", status.Err) - } - return nil -} - -func checkForAppUpdate() { - if !appConfig.CheckForAppUpdate { - return - } - - isAvailable, newVersion, err := version.IsUpdateAvailable() - if err != nil { - log.Errorf(err.Error()) - } - if isAvailable { - log.Infof("new version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version) - - bus.Publish(partybus.Event{ - Type: event.AppUpdateAvailable, - Value: newVersion, - }) - } else { - log.Debugf("no new %s update available", internal.ApplicationName) - } -} diff --git a/cmd/signals.go b/cmd/signals.go deleted file mode 100644 index f20379d1..00000000 --- a/cmd/signals.go +++ /dev/null @@ -1,20 +0,0 @@ -package cmd - -import ( - "os" - "os/signal" - "syscall" -) - -func setupSignals() <-chan os.Signal { - c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify - - interruptions := []os.Signal{ - syscall.SIGINT, - syscall.SIGTERM, - } - - signal.Notify(c, interruptions...) - - return c -} diff --git a/cmd/version.go b/cmd/version.go deleted file mode 100644 index 96e02121..00000000 --- a/cmd/version.go +++ /dev/null @@ -1,58 +0,0 @@ -package cmd - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/spf13/cobra" - - "github.com/xeol-io/xeol/internal" - "github.com/xeol-io/xeol/internal/version" -) - -var versionOutputFormat string - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "show the version", - RunE: printVersion, -} - -func init() { - versionCmd.Flags().StringVarP(&versionOutputFormat, "output", "o", "text", "format to display results (available=[text, json])") - - rootCmd.AddCommand(versionCmd) -} - -func printVersion(_ *cobra.Command, _ []string) error { - versionInfo := version.FromBuild() - switch versionOutputFormat { - case "text": - fmt.Println("Application: ", internal.ApplicationName) - fmt.Println("Version: ", versionInfo.Version) - fmt.Println("Syft Version: ", versionInfo.SyftVersion) - fmt.Println("GitCommit: ", versionInfo.GitCommit) - fmt.Println("Platform: ", versionInfo.Platform) - fmt.Println("GoVersion: ", versionInfo.GoVersion) - fmt.Println("Compiler: ", versionInfo.Compiler) - case "json": - - enc := json.NewEncoder(os.Stdout) - enc.SetEscapeHTML(false) - enc.SetIndent("", " ") - err := enc.Encode(&struct { - version.Version - Application string `json:"application"` - }{ - Version: versionInfo, - Application: internal.ApplicationName, - }) - if err != nil { - return fmt.Errorf("failed to show version information: %+v", err) - } - default: - return fmt.Errorf("unsupported output format: %s", versionOutputFormat) - } - return nil -} diff --git a/cmd/xeol/cli/cli.go b/cmd/xeol/cli/cli.go new file mode 100644 index 00000000..0978a6f1 --- /dev/null +++ b/cmd/xeol/cli/cli.go @@ -0,0 +1,103 @@ +package cli + +import ( + "os" + "runtime/debug" + + "github.com/anchore/clio" + "github.com/anchore/stereoscope" + "github.com/anchore/syft/syft" + "github.com/spf13/cobra" + + "github.com/xeol-io/xeol/cmd/xeol/cli/commands" + handler "github.com/xeol-io/xeol/cmd/xeol/cli/ui" + "github.com/xeol-io/xeol/cmd/xeol/internal/ui" + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/internal/redact" + "github.com/xeol-io/xeol/xeol/eol" +) + +func Application(id clio.Identification) clio.Application { + app, _ := create(id) + return app +} + +func Command(id clio.Identification) *cobra.Command { + _, cmd := create(id) + return cmd +} + +func create(id clio.Identification) (clio.Application, *cobra.Command) { + clioCfg := clio.NewSetupConfig(id). + WithGlobalConfigFlag(). // add persistent -c for reading an application config from + WithGlobalLoggingFlags(). // add persistent -v and -q flags tied to the logging config + WithConfigInRootHelp(). // --help on the root command renders the full application config in the help text + WithUIConstructor( + // select a UI based on the logging configuration and state of stdin (if stdin is a tty) + func(cfg clio.Config) ([]clio.UI, error) { + noUI := ui.None(cfg.Log.Quiet) + if !cfg.Log.AllowUI(os.Stdin) || cfg.Log.Quiet { + return []clio.UI{noUI}, nil + } + + h := handler.New(handler.DefaultHandlerConfig()) + + return []clio.UI{ + ui.New(cfg.Log.Quiet, h), + noUI, + }, nil + }, + ). + WithInitializers( + func(state *clio.State) error { + // clio is setting up and providing the bus, redact store, and logger to the application. Once loaded, + // we can hoist them into the internal packages for global use. + stereoscope.SetBus(state.Bus) + syft.SetBus(state.Bus) + bus.Set(state.Bus) + + redact.Set(state.RedactStore) + + stereoscope.SetLogger(state.Logger) + syft.SetLogger(state.Logger) + log.Set(state.Logger) + + return nil + }, + ) + + app := clio.New(*clioCfg) + + rootCmd := commands.Root(app) + + // add sub-commands + rootCmd.AddCommand( + commands.DB(app), + commands.Completion(), + clio.VersionCommand(id, syftVersion, dbVersion), + ) + + return app, rootCmd +} + +func syftVersion() (string, any) { + buildInfo, ok := debug.ReadBuildInfo() + if !ok { + log.Debug("unable to find the buildinfo section of the binary (syft version is unknown)") + return "", "" + } + + for _, d := range buildInfo.Deps { + if d.Path == "github.com/anchore/syft" { + return "Syft Version", d.Version + } + } + + log.Debug("unable to find 'github.com/anchore/syft' from the buildinfo section of the binary") + return "", "" +} + +func dbVersion() (string, any) { + return "Supported DB Schema", eol.SchemaVersion +} diff --git a/cmd/completion.go b/cmd/xeol/cli/commands/completion.go similarity index 75% rename from cmd/completion.go rename to cmd/xeol/cli/commands/completion.go index c9eebed5..8b7ac9c2 100644 --- a/cmd/completion.go +++ b/cmd/xeol/cli/commands/completion.go @@ -1,4 +1,4 @@ -package cmd +package commands import ( "context" @@ -11,11 +11,12 @@ import ( "github.com/spf13/cobra" ) -// completionCmd represents the completion command -var completionCmd = &cobra.Command{ - Use: "completion [bash|zsh|fish]", - Short: "Generate a shell completion for xeol (listing local docker images)", - Long: `To load completions (docker image list): +// Completion returns a command to provide completion to various terminal shells +func Completion() *cobra.Command { + return &cobra.Command{ + Use: "completion [bash|zsh|fish]", + Short: "Generate a shell completion for xeol (listing local docker images)", + Long: `To load completions (docker image list): Bash: @@ -46,40 +47,22 @@ $ xeol completion fish | source # To load completions for each session, execute once: $ xeol completion fish > ~/.config/fish/completions/xeol.fish `, - DisableFlagsInUseLine: true, - ValidArgs: []string{"bash", "fish", "zsh"}, - Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), - RunE: func(cmd *cobra.Command, args []string) error { - var err error - switch args[0] { - case "zsh": - err = cmd.Root().GenZshCompletion(os.Stdout) - case "bash": - err = cmd.Root().GenBashCompletion(os.Stdout) - case "fish": - err = cmd.Root().GenFishCompletion(os.Stdout, true) - } - return err - }, -} - -func init() { - rootCmd.AddCommand(completionCmd) -} - -func dockerImageValidArgsFunction(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { - // Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided - dockerImageRepoTags, err := listLocalDockerImages(toComplete) - if err != nil { - // Indicates that an error occurred and completions should be ignored - return []string{"completion failed"}, cobra.ShellCompDirectiveError - } - if len(dockerImageRepoTags) == 0 { - return []string{"no docker images found"}, cobra.ShellCompDirectiveError + DisableFlagsInUseLine: true, + ValidArgs: []string{"bash", "fish", "zsh"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + RunE: func(cmd *cobra.Command, args []string) error { + var err error + switch args[0] { + case "zsh": + err = cmd.Root().GenZshCompletion(os.Stdout) + case "bash": + err = cmd.Root().GenBashCompletion(os.Stdout) + case "fish": + err = cmd.Root().GenFishCompletion(os.Stdout, true) + } + return err + }, } - // ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have - // been provided (without implying other possible directives) - return dockerImageRepoTags, cobra.ShellCompDirectiveDefault } func listLocalDockerImages(prefix string) ([]string, error) { @@ -108,3 +91,18 @@ func listLocalDockerImages(prefix string) ([]string, error) { } return repoTags, nil } + +func dockerImageValidArgsFunction(_ *cobra.Command, _ []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // Since we use ValidArgsFunction, Cobra will call this AFTER having parsed all flags and arguments provided + dockerImageRepoTags, err := listLocalDockerImages(toComplete) + if err != nil { + // Indicates that an error occurred and completions should be ignored + return []string{"completion failed"}, cobra.ShellCompDirectiveError + } + if len(dockerImageRepoTags) == 0 { + return []string{"no docker images found"}, cobra.ShellCompDirectiveError + } + // ShellCompDirectiveDefault indicates that the shell will perform its default behavior after completions have + // been provided (without implying other possible directives) + return dockerImageRepoTags, cobra.ShellCompDirectiveDefault +} diff --git a/cmd/xeol/cli/commands/db.go b/cmd/xeol/cli/commands/db.go new file mode 100644 index 00000000..a716b566 --- /dev/null +++ b/cmd/xeol/cli/commands/db.go @@ -0,0 +1,36 @@ +package commands + +import ( + "github.com/anchore/clio" + "github.com/spf13/cobra" + + "github.com/xeol-io/xeol/cmd/xeol/cli/options" +) + +type DBOptions struct { + DB options.Database `yaml:"db" json:"db" mapstructure:"db"` +} + +func dbOptionsDefault(id clio.Identification) *DBOptions { + return &DBOptions{ + DB: options.DefaultDatabase(id), + } +} + +func DB(app clio.Application) *cobra.Command { + db := &cobra.Command{ + Use: "db", + Short: "EOL database operations", + } + + db.AddCommand( + DBCheck(app), + DBDelete(app), + DBImport(app), + DBList(app), + DBStatus(app), + DBUpdate(app), + ) + + return db +} diff --git a/cmd/db_check.go b/cmd/xeol/cli/commands/db_check.go similarity index 53% rename from cmd/db_check.go rename to cmd/xeol/cli/commands/db_check.go index 8160253e..fcd49b3d 100644 --- a/cmd/db_check.go +++ b/cmd/xeol/cli/commands/db_check.go @@ -1,26 +1,38 @@ -package cmd +package commands import ( "fmt" + "os" + "github.com/anchore/clio" "github.com/spf13/cobra" + "github.com/xeol-io/xeol/cmd/xeol/cli/options" + "github.com/xeol-io/xeol/internal/bus" "github.com/xeol-io/xeol/xeol/db" ) -var dbCheckCmd = &cobra.Command{ - Use: "check", - Short: "check to see if there is a database update available", - Args: cobra.ExactArgs(0), - RunE: runDBCheckCmd, -} +const ( + exitCodeOnDBUpgradeAvailable = 100 +) -func init() { - dbCmd.AddCommand(dbCheckCmd) +func DBCheck(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "check", + Short: "check to see if there is a database update available", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBCheck(opts.DB) + }, + }, opts) } -func runDBCheckCmd(_ *cobra.Command, _ []string) error { - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) +func runDBCheck(opts options.Database) error { + defer bus.Exit() + + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) if err != nil { return err } @@ -44,5 +56,7 @@ func runDBCheckCmd(_ *cobra.Command, _ []string) error { fmt.Printf("Updated DB URL: %s\n", updateDBEntry.URL.String()) fmt.Println("You can run 'xeol db update' to update to the latest db") + os.Exit(exitCodeOnDBUpgradeAvailable) //nolint:gocritic + return nil } diff --git a/cmd/xeol/cli/commands/db_delete.go b/cmd/xeol/cli/commands/db_delete.go new file mode 100644 index 00000000..ff970b16 --- /dev/null +++ b/cmd/xeol/cli/commands/db_delete.go @@ -0,0 +1,40 @@ +package commands + +import ( + "fmt" + + "github.com/anchore/clio" + "github.com/spf13/cobra" + + "github.com/xeol-io/xeol/cmd/xeol/cli/options" + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/xeol/db" +) + +func DBDelete(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "delete", + Short: "delete the EOL database", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBDelete(opts.DB) + }, + }, opts) +} + +func runDBDelete(opts options.Database) error { + defer bus.Exit() + + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) + if err != nil { + return err + } + + if err := dbCurator.Delete(); err != nil { + return fmt.Errorf("unable to delete EOL database: %+v", err) + } + + return stderrPrintLnf("EOL database deleted") +} diff --git a/cmd/xeol/cli/commands/db_import.go b/cmd/xeol/cli/commands/db_import.go new file mode 100644 index 00000000..d1976d02 --- /dev/null +++ b/cmd/xeol/cli/commands/db_import.go @@ -0,0 +1,42 @@ +package commands + +import ( + "fmt" + + "github.com/anchore/clio" + "github.com/spf13/cobra" + + "github.com/xeol-io/xeol/cmd/xeol/cli/options" + "github.com/xeol-io/xeol/internal" + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/xeol/db" +) + +func DBImport(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "import FILE", + Short: "import a EOL database archive", + Long: fmt.Sprintf("import a EOL database archive from a local FILE.\nDB archives can be obtained from %q.", internal.DBUpdateURL), + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBImport(opts.DB, args[0]) + }, + }, opts) +} + +func runDBImport(opts options.Database, dbArchivePath string) error { + defer bus.Exit() + + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) + if err != nil { + return err + } + + if err := dbCurator.ImportFrom(dbArchivePath); err != nil { + return fmt.Errorf("unable to import EOL database: %+v", err) + } + + return stderrPrintLnf("EOL database imported") +} diff --git a/cmd/db_list.go b/cmd/xeol/cli/commands/db_list.go similarity index 56% rename from cmd/db_list.go rename to cmd/xeol/cli/commands/db_list.go index 81996cfa..12fe4f58 100644 --- a/cmd/db_list.go +++ b/cmd/xeol/cli/commands/db_list.go @@ -1,32 +1,48 @@ -package cmd +package commands import ( "encoding/json" "fmt" "os" + "github.com/anchore/clio" "github.com/spf13/cobra" + "github.com/xeol-io/xeol/internal/bus" "github.com/xeol-io/xeol/xeol/db" ) -var dbListOutputFormat string +type dbListOptions struct { + Output string `yaml:"output" json:"output" mapstructure:"output"` + DBOptions `yaml:",inline" mapstructure:",squash"` +} + +var _ clio.FlagAdder = (*dbListOptions)(nil) -var dbListCmd = &cobra.Command{ - Use: "list", - Short: "list all DBs available according to the listing URL", - Args: cobra.ExactArgs(0), - RunE: runDBListCmd, +func (d *dbListOptions) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&d.Output, "output", "o", "format to display results (available=[text, raw, json])") } -func init() { - dbListCmd.Flags().StringVarP(&dbListOutputFormat, "output", "o", "text", "format to display results (available=[text, raw, json])") +func DBList(app clio.Application) *cobra.Command { + opts := &dbListOptions{ + Output: "text", + DBOptions: *dbOptionsDefault(app.ID()), + } - dbCmd.AddCommand(dbListCmd) + return app.SetupCommand(&cobra.Command{ + Use: "list", + Short: "list all DBs available according to the listing URL", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBList(opts) + }, + }, opts) } -func runDBListCmd(_ *cobra.Command, _ []string) error { - dbCurator, err := db.NewCurator(appConfig.DB.ToCuratorConfig()) +func runDBList(opts *dbListOptions) error { + defer bus.Exit() + + dbCurator, err := db.NewCurator(opts.DB.ToCuratorConfig()) if err != nil { return err } @@ -43,7 +59,7 @@ func runDBListCmd(_ *cobra.Command, _ []string) error { return stderrPrintLnf("No databases available for the current schema (%d)", supportedSchema) } - switch dbListOutputFormat { + switch opts.Output { case "text": // summarize each listing entry for the current DB schema for _, l := range available { @@ -70,7 +86,7 @@ func runDBListCmd(_ *cobra.Command, _ []string) error { return fmt.Errorf("failed to db listing information: %+v", err) } default: - return fmt.Errorf("unsupported output format: %s", dbListOutputFormat) + return fmt.Errorf("unsupported output format: %s", opts.Output) } return nil diff --git a/cmd/xeol/cli/commands/db_status.go b/cmd/xeol/cli/commands/db_status.go new file mode 100644 index 00000000..be724f66 --- /dev/null +++ b/cmd/xeol/cli/commands/db_status.go @@ -0,0 +1,49 @@ +package commands + +import ( + "fmt" + + "github.com/anchore/clio" + "github.com/spf13/cobra" + + "github.com/xeol-io/xeol/cmd/xeol/cli/options" + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/xeol/db" +) + +func DBStatus(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "status", + Short: "display database status", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBStatus(opts.DB) + }, + }, opts) +} + +func runDBStatus(opts options.Database) error { + defer bus.Exit() + + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) + if err != nil { + return err + } + + status := dbCurator.Status() + + statusStr := "valid" + if status.Err != nil { + statusStr = "invalid" + } + + fmt.Println("Location: ", status.Location) + fmt.Println("Built: ", status.Built.String()) + fmt.Println("Schema: ", status.SchemaVersion) + fmt.Println("Checksum: ", status.Checksum) + fmt.Println("Status: ", statusStr) + + return status.Err +} diff --git a/cmd/xeol/cli/commands/db_update.go b/cmd/xeol/cli/commands/db_update.go new file mode 100644 index 00000000..1354a43a --- /dev/null +++ b/cmd/xeol/cli/commands/db_update.go @@ -0,0 +1,50 @@ +package commands + +import ( + "fmt" + + "github.com/anchore/clio" + "github.com/spf13/cobra" + + "github.com/xeol-io/xeol/cmd/xeol/cli/options" + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/xeol/db" +) + +func DBUpdate(app clio.Application) *cobra.Command { + opts := dbOptionsDefault(app.ID()) + + return app.SetupCommand(&cobra.Command{ + Use: "update", + Short: "download the latest EOL database", + Args: cobra.ExactArgs(0), + RunE: func(cmd *cobra.Command, args []string) error { + return runDBUpdate(opts.DB) + }, + }, opts) +} + +func runDBUpdate(opts options.Database) error { + defer bus.Exit() + + dbCurator, err := db.NewCurator(opts.ToCuratorConfig()) + if err != nil { + return err + } + updated, err := dbCurator.Update() + if err != nil { + return fmt.Errorf("unable to update EOL database: %+v", err) + } + + result := "No EOL database update available\n" + if updated { + result = "EOL database updated to latest version!\n" + } + + log.Debugf("completed db update check with result: %s", result) + + bus.Report(result) + + return nil +} diff --git a/cmd/xeol/cli/commands/root.go b/cmd/xeol/cli/commands/root.go new file mode 100644 index 00000000..98fa6be0 --- /dev/null +++ b/cmd/xeol/cli/commands/root.go @@ -0,0 +1,391 @@ +package commands + +import ( + "bytes" + "encoding/base64" + "errors" + "fmt" + "strings" + "sync" + + "github.com/CycloneDX/cyclonedx-go" + "github.com/anchore/clio" + "github.com/anchore/syft/syft/formats/common/cyclonedxhelpers" + "github.com/anchore/syft/syft/linux" + syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + "github.com/anchore/syft/syft/source" + "github.com/hashicorp/go-multierror" + "github.com/spf13/cobra" + "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/cmd/xeol/cli/options" + "github.com/xeol-io/xeol/internal" + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/internal/format" + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/internal/stringutil" + "github.com/xeol-io/xeol/internal/xeolio" + "github.com/xeol-io/xeol/xeol" + "github.com/xeol-io/xeol/xeol/db" + "github.com/xeol-io/xeol/xeol/event" + "github.com/xeol-io/xeol/xeol/event/parsers" + "github.com/xeol-io/xeol/xeol/matcher" + distroMatcher "github.com/xeol-io/xeol/xeol/matcher/distro" + pkgMatcher "github.com/xeol-io/xeol/xeol/matcher/packages" + "github.com/xeol-io/xeol/xeol/pkg" + "github.com/xeol-io/xeol/xeol/policy" + "github.com/xeol-io/xeol/xeol/policy/types" + "github.com/xeol-io/xeol/xeol/presenter/models" + "github.com/xeol-io/xeol/xeol/report" + "github.com/xeol-io/xeol/xeol/store" + "github.com/xeol-io/xeol/xeol/xeolerr" +) + +func Root(app clio.Application) *cobra.Command { + opts := options.DefaultXeol(app.ID()) + + return app.SetupRootCommand(&cobra.Command{ + Use: fmt.Sprintf("%s [IMAGE]", app.ID().Name), + Short: "A scanner for end-of-life (EOL) software in container images, filesystems, and SBOMs", + Long: stringutil.Tprintf(`A scanner for end-of-life (EOL) software in container images, filesystems, and SBOMs. + +Supports the following image sources: + {{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon + {{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, SIF container, or generic filesystem directory + +You can also explicitly specify the scheme to use: + {{.appName}} podman:yourrepo/yourimage:tag explicitly use the Podman daemon + {{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon + {{.appName}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save" + {{.appName}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Podman or otherwise) + {{.appName}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise) + {{.appName}} singularity:path/to/yourimage.sif read directly from a Singularity Image Format (SIF) container on disk + {{.appName}} dir:path/to/yourproject read directly from a path on disk (any directory) + {{.appName}} sbom:path/to/syft.json read Syft JSON from path on disk + {{.appName}} registry:yourrepo/yourimage:tag pull image directly from a registry (no container runtime required) + {{.appName}} purl:path/to/purl/file read a newline separated file of purls from a path on disk + +You can also pipe in Syft JSON directly: + syft yourimage:tag -o json | {{.appName}} + +`, map[string]interface{}{ + "appName": app.ID().Name, + }), + Args: validateRootArgs, + SilenceUsage: true, + SilenceErrors: true, + RunE: func(cmd *cobra.Command, args []string) error { + userInput := "" + if len(args) > 0 { + userInput = args[0] + } + return runXeol(app, opts, userInput) + }, + ValidArgsFunction: dockerImageValidArgsFunction, + }, opts) +} + +//nolint:funlen,gocognit +func runXeol(app clio.Application, opts *options.Xeol, userInput string) error { + errs := make(chan error) + go func() { + defer close(errs) + defer bus.Exit() + + writer, err := format.MakeScanResultWriter(opts.Outputs, opts.File) + if err != nil { + errs <- err + return + } + + checkForAppUpdate(app.ID(), opts) + + var str *store.Store + var status *db.Status + var dbCloser *db.Closer + var packages []pkg.Package + var s *sbom.SBOM + var pkgContext pkg.Context + var wg = &sync.WaitGroup{} + var loadedDB, gatheredPackages bool + var policies []policy.Policy + var certificates string + x := xeolio.NewXeolClient(opts.APIKey) + + wg.Add(3) + go func() { + defer wg.Done() + log.Debug("Fetching organization policies") + if opts.APIKey != "" { + policies, err = x.FetchPolicies() + if err != nil { + errs <- fmt.Errorf("failed to fetch policy: %w", err) + return + } + certificates, err = x.FetchCertificates() + if err != nil { + errs <- fmt.Errorf("failed to fetch certificate: %w", err) + return + } + } + }() + + go func() { + defer wg.Done() + log.Debug("loading DB") + str, status, dbCloser, err = xeol.LoadEolDB(opts.DB.ToCuratorConfig(), opts.DB.AutoUpdate) + if err = validateDBLoad(err, status); err != nil { + errs <- err + return + } + loadedDB = true + }() + + go func() { + defer wg.Done() + log.Debugf("gathering packages") + // packages are xeol.Package, not syft.Package + // the SBOM is returned for downstream formatting concerns + // xeol uses the SBOM in combination with syft formatters to produce cycloneDX + // with vulnerability information appended + packages, pkgContext, s, err = pkg.Provide(userInput, getProviderConfig(opts)) + if err != nil { + errs <- fmt.Errorf("failed to catalog: %w", err) + return + } + gatheredPackages = true + }() + + wg.Wait() + if !loadedDB || !gatheredPackages { + return + } + + if dbCloser != nil { + defer dbCloser.Close() + } + + applyDistroHint(packages, &pkgContext, opts) + + eolMatcher := xeol.EolMatcher{ + Store: *str, + Matchers: getMatchers(opts), + FailOnEolFound: opts.FailOnEolFound, + EolMatchDate: opts.EolMatchDate, + LinuxRelease: pkgContext.Distro, + } + + allMatches, err := eolMatcher.FindEol(packages) + if err != nil { + errs <- err + if !errors.Is(err, xeolerr.ErrEolFound) { + return + } + } + + var failScan bool + var imageVerified bool + var sourceIsImageType bool + if _, ok := s.Source.Metadata.(source.StereoscopeImageSourceMetadata); ok { + sourceIsImageType = true + } + + for _, p := range policies { + switch p.GetPolicyType() { + case types.PolicyTypeNotary: + // Notary policy is only applicable to images + if !sourceIsImageType { + continue + } + shouldFailScan, res := p.Evaluate(allMatches, opts.ProjectName, userInput, certificates) + imageVerified = res.GetVerified() + if shouldFailScan { + failScan = true + } + + case types.PolicyTypeEol: + shouldFailScan, _ := p.Evaluate(allMatches, opts.ProjectName, "", "") + if shouldFailScan { + failScan = true + } + } + } + + if opts.APIKey != "" { + buf := new(bytes.Buffer) + bom := cyclonedxhelpers.ToFormatModel(*s) + enc := cyclonedx.NewBOMEncoder(buf, cyclonedx.BOMFileFormatJSON) + if err := enc.Encode(bom); err != nil { + errs <- fmt.Errorf("failed to encode sbom: %w", err) + return + } + + eventSource, err := xeolio.NewEventSource(s.Source) + if err != nil { + errs <- fmt.Errorf("failed to create event source: %w", err) + return + } + + if err := x.SendEvent(report.XeolEventPayload{ + Matches: allMatches.Sorted(), + Packages: packages, + Context: pkgContext, + AppConfig: opts, + EventSource: eventSource, + ImageVerified: imageVerified, + Sbom: base64.StdEncoding.EncodeToString(buf.Bytes()), + }); err != nil { + errs <- fmt.Errorf("failed to send eol event: %w", err) + return + } + } + + if err := writer.Write(models.PresenterConfig{ + Matches: allMatches, + Packages: packages, + Context: pkgContext, + SBOM: s, + AppConfig: opts, + DBStatus: status, + }); err != nil { + errs <- err + } + + if failScan { + errs <- xeolerr.ErrPolicyViolation + return + } + }() + + return readAllErrors(errs) +} + +func readAllErrors(errs <-chan error) (out error) { + for { + if errs == nil { + break + } + err, isOpen := <-errs + if !isOpen { + errs = nil + continue + } + if err != nil { + out = multierror.Append(out, err) + } + } + return out +} + +func applyDistroHint(pkgs []pkg.Package, context *pkg.Context, opts *options.Xeol) { + if opts.Distro != "" { + log.Infof("using distro: %s", opts.Distro) + + split := strings.Split(opts.Distro, ":") + d := split[0] + v := "" + if len(split) > 1 { + v = split[1] + } + context.Distro = &linux.Release{ + PrettyName: d, + Name: d, + ID: d, + IDLike: []string{ + d, + }, + Version: v, + VersionID: v, + } + } + + hasOSPackage := false + for _, p := range pkgs { + switch p.Type { + case syftPkg.AlpmPkg, syftPkg.DebPkg, syftPkg.RpmPkg, syftPkg.KbPkg: + hasOSPackage = true + } + } + + if context.Distro == nil && hasOSPackage { + log.Warnf("Unable to determine the OS distribution " + + "You may specify a distro using: --distro :") + } +} + +func checkForAppUpdate(id clio.Identification, opts *options.Xeol) { + if !opts.CheckForAppUpdate { + return + } + + version := id.Version + isAvailable, newVersion, err := isUpdateAvailable(version) + if err != nil { + log.Errorf(err.Error()) + } + if isAvailable { + log.Infof("new version of %s is available: %s (currently running: %s)", id.Name, newVersion, version) + + bus.Publish(partybus.Event{ + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: newVersion, + Current: id.Version, + }, + }) + } else { + log.Debugf("no new %s update available", id.Name) + } +} + +func getMatchers(opts *options.Xeol) []matcher.Matcher { + return matcher.NewDefaultMatchers(matcher.Config{ + Packages: pkgMatcher.MatcherConfig(opts.Match.Packages), + Distro: distroMatcher.MatcherConfig(opts.Match.Distro), + }) +} + +func getProviderConfig(opts *options.Xeol) pkg.ProviderConfig { + return pkg.ProviderConfig{ + SyftProviderConfig: pkg.SyftProviderConfig{ + RegistryOptions: opts.Registry.ToOptions(), + CatalogingOptions: opts.Search.ToConfig(), + Platform: opts.Platform, + Name: opts.Name, + DefaultImagePullSource: opts.DefaultImagePullSource, + }, + SynthesisConfig: pkg.SynthesisConfig{}, + } +} + +func validateDBLoad(loadErr error, status *db.Status) error { + if loadErr != nil { + return fmt.Errorf("failed to load EOL db: %w", loadErr) + } + if status == nil { + return fmt.Errorf("unable to determine the status of the EOL db") + } + if status.Err != nil { + return fmt.Errorf("db could not be loaded: %w", status.Err) + } + return nil +} + +func validateRootArgs(cmd *cobra.Command, args []string) error { + isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() + if err != nil { + log.Warnf("unable to determine if there is piped input: %+v", err) + isStdinPipeOrRedirect = false + } + + if len(args) == 0 && !isStdinPipeOrRedirect { + // in the case that no arguments are given and there is no piped input we want to show the help text and return with a non-0 return code. + if err := cmd.Help(); err != nil { + return fmt.Errorf("unable to display help: %w", err) + } + return fmt.Errorf("an image/directory argument is required") + } + + return cobra.MaximumNArgs(1)(cmd, args) +} diff --git a/cmd/xeol/cli/commands/update.go b/cmd/xeol/cli/commands/update.go new file mode 100644 index 00000000..aef5b22f --- /dev/null +++ b/cmd/xeol/cli/commands/update.go @@ -0,0 +1,79 @@ +package commands + +import ( + "fmt" + "io" + "net/http" + "strings" + + hashiVersion "github.com/anchore/go-version" + + "github.com/xeol-io/xeol/cmd/xeol/internal" +) + +var latestAppVersionURL = struct { + host string + path string +}{ + host: "https://data.xeol.io", + path: fmt.Sprintf("/%s/releases/latest/VERSION", internal.Xeol), +} + +func isProductionBuild(version string) bool { + if strings.Contains(version, "SNAPSHOT") || strings.Contains(version, internal.NotProvided) { + return false + } + return true +} + +func isUpdateAvailable(version string) (bool, string, error) { + if !isProductionBuild(version) { + // don't allow for non-production builds to check for a version. + return false, "", nil + } + currentVersion, err := hashiVersion.NewVersion(version) + if err != nil { + return false, "", fmt.Errorf("failed to parse current application version: %w", err) + } + + latestVersion, err := fetchLatestApplicationVersion() + if err != nil { + return false, "", err + } + + if latestVersion.GreaterThan(currentVersion) { + return true, latestVersion.String(), nil + } + + return false, "", nil +} + +func fetchLatestApplicationVersion() (*hashiVersion.Version, error) { + req, err := http.NewRequest(http.MethodGet, latestAppVersionURL.host+latestAppVersionURL.path, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request for latest version: %w", err) + } + + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("failed to fetch latest version: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP %d on fetching latest version: %s", resp.StatusCode, resp.Status) + } + + versionBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read latest version: %w", err) + } + + versionStr := strings.TrimSuffix(string(versionBytes), "\n") + if len(versionStr) > 50 { + return nil, fmt.Errorf("version too long: %q", versionStr[:50]) + } + + return hashiVersion.NewVersion(versionStr) +} diff --git a/cmd/util.go b/cmd/xeol/cli/commands/util.go similarity index 93% rename from cmd/util.go rename to cmd/xeol/cli/commands/util.go index e9fa6bc3..500938ac 100644 --- a/cmd/util.go +++ b/cmd/xeol/cli/commands/util.go @@ -1,4 +1,4 @@ -package cmd +package commands import ( "fmt" diff --git a/internal/config/database.go b/cmd/xeol/cli/options/database.go similarity index 66% rename from internal/config/database.go rename to cmd/xeol/cli/options/database.go index 225a3443..b592e7da 100644 --- a/internal/config/database.go +++ b/cmd/xeol/cli/options/database.go @@ -1,17 +1,17 @@ -package config +package options import ( "path" "time" "github.com/adrg/xdg" - "github.com/spf13/viper" + "github.com/anchore/clio" "github.com/xeol-io/xeol/internal" "github.com/xeol-io/xeol/xeol/db" ) -type database struct { +type Database struct { Dir string `yaml:"cache-dir" json:"cache-dir" mapstructure:"cache-dir"` UpdateURL string `yaml:"update-url" json:"update-url" mapstructure:"update-url"` CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` @@ -21,18 +21,18 @@ type database struct { MaxAllowedBuiltAge time.Duration `yaml:"max-allowed-built-age" json:"max-allowed-built-age" mapstructure:"max-allowed-built-age"` } -func (cfg database) loadDefaultValues(v *viper.Viper) { - v.SetDefault("db.cache-dir", path.Join(xdg.CacheHome, internal.ApplicationName, "db")) - v.SetDefault("db.update-url", internal.DBUpdateURL) - v.SetDefault("db.ca-cert", "") - v.SetDefault("db.auto-update", true) - v.SetDefault("db.validate-by-hash-on-start", false) - v.SetDefault("db.validate-age", true) - // After this period (5 days) the db data is considered stale - v.SetDefault("db.max-allowed-built-age", time.Hour*24*5) +func DefaultDatabase(id clio.Identification) Database { + return Database{ + Dir: path.Join(xdg.CacheHome, id.Name, "db"), + UpdateURL: internal.DBUpdateURL, + AutoUpdate: true, + ValidateAge: true, + // After this period (5 days) the db data is considered stale + MaxAllowedBuiltAge: time.Hour * 24 * 5, + } } -func (cfg database) ToCuratorConfig() db.Config { +func (cfg Database) ToCuratorConfig() db.Config { return db.Config{ DBRootDir: cfg.Dir, ListingURL: cfg.UpdateURL, diff --git a/cmd/xeol/cli/options/match.go b/cmd/xeol/cli/options/match.go new file mode 100644 index 00000000..a235e4bf --- /dev/null +++ b/cmd/xeol/cli/options/match.go @@ -0,0 +1,24 @@ +package options + +// matchConfig contains all matching-related configuration options available to the user via the application config. +type matchConfig struct { + Packages pkgMatcherConfig `mapstructure:"packages"` // settings for the packages matcher + Distro distroMatcherConfig `mapstructure:"distro"` // settings for the distro matcher +} + +type pkgMatcherConfig struct { + UsePURLs bool `yaml:"using-purls" json:"using-purls" mapstructure:"using-purls"` // if Purls should be used during matching +} + +type distroMatcherConfig struct { + UseCPEs bool `yaml:"using-cpes" json:"using-cpes" mapstructure:"using-cpes"` // if CPEs should be used during matching +} + +func defaultMatchConfig() matchConfig { + useCpe := distroMatcherConfig{UseCPEs: true} + usePurl := pkgMatcherConfig{UsePURLs: true} + return matchConfig{ + Packages: usePurl, + Distro: useCpe, + } +} diff --git a/internal/config/project_name.go b/cmd/xeol/cli/options/project_name.go similarity index 99% rename from internal/config/project_name.go rename to cmd/xeol/cli/options/project_name.go index dfbbad32..4afc930b 100644 --- a/internal/config/project_name.go +++ b/cmd/xeol/cli/options/project_name.go @@ -1,4 +1,4 @@ -package config +package options import ( "fmt" diff --git a/internal/config/project_name_test.go b/cmd/xeol/cli/options/project_name_test.go similarity index 98% rename from internal/config/project_name_test.go rename to cmd/xeol/cli/options/project_name_test.go index 17ec400c..0bfc973b 100644 --- a/internal/config/project_name_test.go +++ b/cmd/xeol/cli/options/project_name_test.go @@ -1,4 +1,4 @@ -package config +package options import ( "testing" diff --git a/cmd/xeol/cli/options/registry.go b/cmd/xeol/cli/options/registry.go new file mode 100644 index 00000000..d1ace661 --- /dev/null +++ b/cmd/xeol/cli/options/registry.go @@ -0,0 +1,82 @@ +package options + +import ( + "os" + + "github.com/anchore/clio" + "github.com/anchore/stereoscope/pkg/image" +) + +type RegistryCredentials struct { + Authority string `yaml:"authority" json:"authority" mapstructure:"authority"` + // IMPORTANT: do not show the username, password, or token in any output (sensitive information) + Username secret `yaml:"username" json:"username" mapstructure:"username"` + Password secret `yaml:"password" json:"password" mapstructure:"password"` + Token secret `yaml:"token" json:"token" mapstructure:"token"` + + TLSCert string `yaml:"tls-cert,omitempty" json:"tls-cert,omitempty" mapstructure:"tls-cert"` + TLSKey string `yaml:"tls-key,omitempty" json:"tls-key,omitempty" mapstructure:"tls-key"` +} + +type registry struct { + InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"` + InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"` + Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"` + CACert string `yaml:"ca-cert" json:"ca-cert" mapstructure:"ca-cert"` +} + +var _ clio.PostLoader = (*registry)(nil) + +func (cfg *registry) PostLoad() error { + // there may be additional credentials provided by env var that should be appended to the set of credentials + authority, username, password, token, tlsCert, tlsKey := + os.Getenv("XEOL_REGISTRY_AUTH_AUTHORITY"), + os.Getenv("XEOL_REGISTRY_AUTH_USERNAME"), + os.Getenv("XEOL_REGISTRY_AUTH_PASSWORD"), + os.Getenv("XEOL_REGISTRY_AUTH_TOKEN"), + os.Getenv("XEOL_REGISTRY_AUTH_TLS_CERT"), + os.Getenv("XEOL_REGISTRY_AUTH_TLS_KEY") + + if hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey) { + // note: we prepend the credentials such that the environment variables take precedence over on-disk configuration. + cfg.Auth = append([]RegistryCredentials{ + { + Authority: authority, + Username: secret(username), + Password: secret(password), + Token: secret(token), + TLSCert: tlsCert, + TLSKey: tlsKey, + }, + }, cfg.Auth...) + } + return nil +} + +func hasNonEmptyCredentials(username, password, token, tlsCert, tlsKey string) bool { + hasUserPass := username != "" && password != "" + hasToken := token != "" + hasTLSMaterial := tlsCert != "" && tlsKey != "" + return hasUserPass || hasToken || hasTLSMaterial +} + +func (cfg *registry) ToOptions() *image.RegistryOptions { + var auth = make([]image.RegistryCredentials, len(cfg.Auth)) + for i, a := range cfg.Auth { + auth[i] = image.RegistryCredentials{ + Authority: a.Authority, + Username: a.Username.String(), + Password: a.Password.String(), + Token: a.Token.String(), + ClientCert: a.TLSCert, + ClientKey: a.TLSKey, + } + } + + return &image.RegistryOptions{ + InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify, + InsecureUseHTTP: cfg.InsecureUseHTTP, + Credentials: auth, + CAFileOrDir: cfg.CACert, + } +} diff --git a/internal/config/registry_test.go b/cmd/xeol/cli/options/registry_test.go similarity index 62% rename from internal/config/registry_test.go rename to cmd/xeol/cli/options/registry_test.go index 74fc0760..e7ba596c 100644 --- a/internal/config/registry_test.go +++ b/cmd/xeol/cli/options/registry_test.go @@ -1,4 +1,4 @@ -package config +package options import ( "fmt" @@ -10,47 +10,60 @@ import ( func TestHasNonEmptyCredentials(t *testing.T) { tests := []struct { - username, password, token string - expected bool + username, password, token, cert, key string + expected bool }{ + { - "", "", "", + "", "", "", "", "", false, }, { - "user", "", "", + "user", "", "", "", "", false, }, { - "", "pass", "", + "", "pass", "", "", "", false, }, { - "", "pass", "tok", + "", "pass", "tok", "", "", true, }, { - "user", "", "tok", + "user", "", "tok", "", "", true, }, { - "", "", "tok", + "", "", "tok", "", "", true, }, { - "user", "pass", "tok", + "user", "pass", "tok", "", "", true, }, { - "user", "pass", "", + "user", "pass", "", "", "", true, }, + { + "", "", "", "cert", "key", + true, + }, + { + "", "", "", "cert", "", + false, + }, + { + "", "", "", "", "key", + false, + }, } for _, test := range tests { t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) { - assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token)) + assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token, test.cert, test.key)) }) } } @@ -100,6 +113,29 @@ func Test_registry_ToOptions(t *testing.T) { Credentials: []image.RegistryCredentials{}, }, }, + { + name: "provide all tls configuration", + input: registry{ + CACert: "ca.crt", + InsecureSkipTLSVerify: true, + Auth: []RegistryCredentials{ + { + TLSCert: "client.crt", + TLSKey: "client.key", + }, + }, + }, + expected: image.RegistryOptions{ + CAFileOrDir: "ca.crt", + InsecureSkipTLSVerify: true, + Credentials: []image.RegistryCredentials{ + { + ClientCert: "client.crt", + ClientKey: "client.key", + }, + }, + }, + }, } for _, test := range tests { diff --git a/cmd/xeol/cli/options/search.go b/cmd/xeol/cli/options/search.go new file mode 100644 index 00000000..c79190af --- /dev/null +++ b/cmd/xeol/cli/options/search.go @@ -0,0 +1,49 @@ +package options + +import ( + "fmt" + + "github.com/anchore/clio" + "github.com/anchore/syft/syft/pkg/cataloger" + "github.com/anchore/syft/syft/source" +) + +type search struct { + Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` + IncludeUnindexedArchives bool `yaml:"unindexed-archives" json:"unindexed-archives" mapstructure:"unindexed-archives"` + IncludeIndexedArchives bool `yaml:"indexed-archives" json:"indexed-archives" mapstructure:"indexed-archives"` +} + +var _ clio.PostLoader = (*search)(nil) + +func defaultSearch(scope source.Scope) search { + c := cataloger.DefaultSearchConfig() + return search{ + Scope: scope.String(), + IncludeUnindexedArchives: c.IncludeUnindexedArchives, + IncludeIndexedArchives: c.IncludeIndexedArchives, + } +} + +func (cfg *search) PostLoad() error { + scopeOption := cfg.GetScope() + if scopeOption == source.UnknownScope { + return fmt.Errorf("bad scope value %q", cfg.Scope) + } + return nil +} + +func (cfg search) GetScope() source.Scope { + return source.ParseScope(cfg.Scope) +} + +func (cfg search) ToConfig() cataloger.Config { + return cataloger.Config{ + Search: cataloger.SearchConfig{ + IncludeIndexedArchives: cfg.IncludeIndexedArchives, + IncludeUnindexedArchives: cfg.IncludeUnindexedArchives, + Scope: cfg.GetScope(), + }, + ExcludeBinaryOverlapByOwnership: true, + } +} diff --git a/cmd/xeol/cli/options/secret.go b/cmd/xeol/cli/options/secret.go new file mode 100644 index 00000000..87ea7635 --- /dev/null +++ b/cmd/xeol/cli/options/secret.go @@ -0,0 +1,26 @@ +package options + +import ( + "fmt" + + "github.com/anchore/clio" + + "github.com/xeol-io/xeol/internal/redact" +) + +type secret string + +var _ interface { + fmt.Stringer + clio.PostLoader +} = (*secret)(nil) + +// PostLoad needs to use a pointer receiver, even if it's not modifying the value +func (r *secret) PostLoad() error { + redact.Add(string(*r)) + return nil +} + +func (r secret) String() string { + return string(r) +} diff --git a/cmd/xeol/cli/options/xeol.go b/cmd/xeol/cli/options/xeol.go new file mode 100644 index 00000000..6046710a --- /dev/null +++ b/cmd/xeol/cli/options/xeol.go @@ -0,0 +1,157 @@ +package options + +import ( + "fmt" + "time" + + "github.com/anchore/clio" + "github.com/anchore/syft/syft/source" + git "github.com/go-git/go-git/v5" + "github.com/karrick/tparse" + + "github.com/xeol-io/xeol/internal/format" +) + +const DefaultProLookahead = "now+3y" + +type Xeol struct { + Outputs []string `yaml:"output" json:"output" mapstructure:"output"` // -o, = the Presenter hint string to use for report formatting and the output file + File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to + Distro string `yaml:"distro" json:"distro" mapstructure:"distro"` // --distro, specify a distro to explicitly use + CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not + Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image + Search search `yaml:"search" json:"search" mapstructure:"search"` + DB Database `yaml:"db" json:"db" mapstructure:"db"` + Lookahead string `yaml:"lookahead" json:"lookahead" mapstructure:"lookahead"` + EolMatchDate time.Time `yaml:"-" json:"-"` + FailOnEolFound bool `yaml:"fail-on-eol-found" json:"fail-on-eol-found" mapstructure:"fail-on-eol-found"` // whether to exit with a non-zero exit code if any EOLs are found + APIKey string `yaml:"api-key" json:"api-key" mapstructure:"api-key"` + ProjectName string `yaml:"project-name" json:"project-name" mapstructure:"project-name"` + ImagePath string `yaml:"image-path" json:"image-path" mapstructure:"image-path"` + CommitHash string `yaml:"commit-hash" json:"commit-hash" mapstructure:"commit-hash"` + Match matchConfig `yaml:"match" json:"match" mapstructure:"match"` + Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` + Name string `yaml:"name" json:"name" mapstructure:"name"` + DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` +} + +var _ interface { + clio.FlagAdder + clio.PostLoader +} = (*Xeol)(nil) + +func DefaultXeol(id clio.Identification) *Xeol { + config := &Xeol{ + Search: defaultSearch(source.SquashedScope), + DB: DefaultDatabase(id), + Match: defaultMatchConfig(), + CheckForAppUpdate: true, + } + return config +} + +func getDefaultProjectNameAndCommit() (string, string) { + repo, err := git.PlainOpen(".") + if err != nil { + return "", "" + } + + h, err := repo.Head() + if err != nil { + return "", "" + } + + p := NewProject(repo) + return p.Name, h.Hash().String() +} + +// nolint:funlen +func (o *Xeol) AddFlags(flags clio.FlagSet) { + flags.StringVarP(&o.Search.Scope, + "scope", "s", + fmt.Sprintf("selection of layers to analyze, options=%v", source.AllScopes), + ) + + flags.StringArrayVarP(&o.Outputs, + "output", "o", + fmt.Sprintf("report output formatter, formats=%v", format.AvailableFormats), + ) + + flags.StringVarP(&o.File, + "file", "", + "file to write the default report output to (default is STDOUT)", + ) + + flags.StringVarP(&o.Name, + "name", "", + "set the name of the target being analyzed", + ) + + flags.StringVarP(&o.ProjectName, + "project-name", "", + "manually set the name of the project being analyzed for xeol.io. If you are running xeol inside a git repository, this will be automatically detected.", + ) + + flags.StringVarP(&o.APIKey, + "api-key", "", + "set the API key for xeol.io. When this is set, scans will be uploaded to xeol.io.", + ) + + flags.BoolVarP(&o.FailOnEolFound, + "fail-on-eol-found", "f", + "set the return code to 1 if an EOL package is found", + ) + + flags.StringVarP(&o.Lookahead, + "lookahead", "l", + "an optional lookahead specifier when matching EOL dates (e.g. 'none', '1d', '1w', '1m', '1y'). Packages are matched when their EOL date < today+lookahead", + ) + + flags.StringVarP(&o.Distro, + "distro", "", + "distro to match against in the format: :", + ) + + flags.StringVarP(&o.Platform, + "platform", "", + "an optional platform specifier for container image sources (e.g. 'linux/arm64', 'linux/arm64/v8', 'arm64', 'linux')", + ) +} + +func (o *Xeol) parseLookaheadOption() (err error) { + // if the user has specified an API key and is posting results to xeol.io, then we + // set a default lookahead value to 3 years from now + if o.APIKey != "" { + o.EolMatchDate, err = tparse.ParseNow(time.RFC3339, DefaultProLookahead) + if err != nil { + return fmt.Errorf("bad --lookahead value: '%s'", o.Lookahead) + } + return nil + } + + if o.Lookahead == "" { + o.EolMatchDate = time.Now() + return nil + } + + o.EolMatchDate, err = tparse.ParseNow(time.RFC3339, fmt.Sprintf("now+%s", o.Lookahead)) + if err != nil { + return fmt.Errorf("bad --lookahead value: '%s'", o.Lookahead) + } + + return nil +} + +func (o *Xeol) loadDefaltValues() { + project, commit := getDefaultProjectNameAndCommit() + o.FailOnEolFound = false + o.Lookahead = "30d" + o.ProjectName = project + o.CommitHash = commit + o.ImagePath = "Dockerfile" +} + +func (o *Xeol) PostLoad() error { + o.loadDefaltValues() + return o.parseLookaheadOption() +} diff --git a/cmd/xeol/cli/ui/__snapshots__/handle_eol_scanning_started_test.snap b/cmd/xeol/cli/ui/__snapshots__/handle_eol_scanning_started_test.snap new file mode 100755 index 00000000..a0f7c2dc --- /dev/null +++ b/cmd/xeol/cli/ui/__snapshots__/handle_eol_scanning_started_test.snap @@ -0,0 +1,16 @@ + +[TestHandler_handleEolScanningStarted/eol_scanning_in_progress/task_line - 1] + ⠋ Scanning for EOL [40 eol matches] +--- + +[TestHandler_handleEolScanningStarted/eol_scanning_in_progress/tree - 1] + +--- + +[TestHandler_handleEolScanningStarted/eol_scanning_complete/task_line - 1] + ✔ Scanned for EOL [45 eol matches] +--- + +[TestHandler_handleEolScanningStarted/eol_scanning_complete/tree - 1] + +--- diff --git a/cmd/xeol/cli/ui/__snapshots__/handle_update_eol_database_test.snap b/cmd/xeol/cli/ui/__snapshots__/handle_update_eol_database_test.snap new file mode 100755 index 00000000..4a6c7f9d --- /dev/null +++ b/cmd/xeol/cli/ui/__snapshots__/handle_update_eol_database_test.snap @@ -0,0 +1,8 @@ + +[TestHandler_handleUpdateEolDatabase/downloading_DB - 1] + ⠋ EOL DB ━━━━━━━━━━━━━━━━━━━━ [current] +--- + +[TestHandler_handleUpdateEolDatabase/DB_download_complete - 1] + ✔ EOL DB [current] +--- diff --git a/cmd/xeol/cli/ui/handle_eol_scanning_started.go b/cmd/xeol/cli/ui/handle_eol_scanning_started.go new file mode 100644 index 00000000..6701d09c --- /dev/null +++ b/cmd/xeol/cli/ui/handle_eol_scanning_started.go @@ -0,0 +1,161 @@ +package ui + +import ( + "fmt" + "strings" + "time" + + "github.com/anchore/bubbly/bubbles/taskprogress" + tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/xeol/event/monitor" + "github.com/xeol-io/xeol/xeol/event/parsers" +) + +var _ progress.StagedProgressable = (*eolScanningAdapter)(nil) + +type eolProgressTree struct { + mon *monitor.Matching + windowSize tea.WindowSizeMsg + totalCount int64 + + id uint32 + sequence int + + updateDuration time.Duration + textStyle lipgloss.Style +} + +func neweolProgressTree(monitor *monitor.Matching, textStyle lipgloss.Style) eolProgressTree { + return eolProgressTree{ + mon: monitor, + textStyle: textStyle, + } +} + +// eolProgressTreeTickMsg indicates that the timer has ticked and we should render a frame. +type eolProgressTreeTickMsg struct { + Time time.Time + Sequence int + ID uint32 +} + +type eolScanningAdapter struct { + mon *monitor.Matching +} + +func (p eolScanningAdapter) Current() int64 { + return p.mon.PackagesProcessed.Current() +} + +func (p eolScanningAdapter) Error() error { + return p.mon.MatchesDiscovered.Error() +} + +func (p eolScanningAdapter) Size() int64 { + return p.mon.PackagesProcessed.Size() +} + +func (p eolScanningAdapter) Stage() string { + return fmt.Sprintf("%d eol matches", p.mon.MatchesDiscovered.Current()) +} + +func (m *Handler) handleEolScanningStarted(e partybus.Event) []tea.Model { + mon, err := parsers.ParseEolScanningStarted(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "Scan for EOL", + Running: "Scanning for EOL", + Success: "Scanned for EOL", + }, + taskprogress.WithStagedProgressable(eolScanningAdapter{mon: mon}), + ) + + tsk.HideStageOnSuccess = false + + textStyle := tsk.HintStyle + + return []tea.Model{ + tsk, + neweolProgressTree(mon, textStyle), + } +} + +func (l eolProgressTree) Init() tea.Cmd { + // this is the periodic update of state information + return func() tea.Msg { + return eolProgressTreeTickMsg{ + // The time at which the tick occurred. + Time: time.Now(), + + // The ID of the log frame that this message belongs to. This can be + // helpful when routing messages, however bear in mind that log frames + // will ignore messages that don't contain ID by default. + ID: l.id, + + Sequence: l.sequence, + } + } +} + +func (l eolProgressTree) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + switch msg := msg.(type) { + case tea.WindowSizeMsg: + l.windowSize = msg + return l, nil + + case eolProgressTreeTickMsg: + // update the model + l.totalCount = l.mon.MatchesDiscovered.Current() + + // kick off the next tick + tickCmd := l.handleTick(msg) + + return l, tickCmd + } + + return l, nil +} + +func (l eolProgressTree) View() string { + sb := strings.Builder{} + return sb.String() +} + +func (l eolProgressTree) queueNextTick() tea.Cmd { + return tea.Tick(l.updateDuration, func(t time.Time) tea.Msg { + return eolProgressTreeTickMsg{ + Time: t, + ID: l.id, + Sequence: l.sequence, + } + }) +} + +func (l *eolProgressTree) handleTick(msg eolProgressTreeTickMsg) tea.Cmd { + // If an ID is set, and the ID doesn't belong to this log frame, reject the message. + if msg.ID > 0 && msg.ID != l.id { + return nil + } + + // If a sequence is set, and it's not the one we expect, reject the message. + // This prevents the log frame from receiving too many messages and + // thus updating too frequently. + if msg.Sequence > 0 && msg.Sequence != l.sequence { + return nil + } + + l.sequence++ + + // note: even if the log is completed we should still respond to stage changes and window size events + return l.queueNextTick() +} diff --git a/cmd/xeol/cli/ui/handle_eol_scanning_started_test.go b/cmd/xeol/cli/ui/handle_eol_scanning_started_test.go new file mode 100644 index 00000000..4080d80b --- /dev/null +++ b/cmd/xeol/cli/ui/handle_eol_scanning_started_test.go @@ -0,0 +1,134 @@ +package ui + +import ( + "testing" + "time" + + "github.com/anchore/bubbly/bubbles/taskprogress" + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/xeol-io/xeol/xeol/event" + "github.com/xeol-io/xeol/xeol/event/monitor" +) + +func TestHandler_handleEolScanningStarted(t *testing.T) { + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "eol scanning in progress", + eventFn: func(t *testing.T) partybus.Event { + return partybus.Event{ + Type: event.EolScanningStarted, + Value: getMatchMonitor(false), + } + }, + }, + { + name: "eol scanning complete", + eventFn: func(t *testing.T) partybus.Event { + return partybus.Event{ + Type: event.EolScanningStarted, + Value: getMatchMonitor(true), + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 2) + + t.Run("task line", func(t *testing.T) { + tsk, ok := models[0].(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + + t.Run("tree", func(t *testing.T) { + log, ok := models[1].(eolProgressTree) + require.True(t, ok) + got := runModel(t, log, tt.iterations, eolProgressTreeTickMsg{ + Time: time.Now(), + Sequence: log.sequence, + ID: log.id, + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + + }) + } +} + +func getMatchMonitor(completed bool) monitor.Matching { + pkgs := &progress.Manual{} + pkgs.SetTotal(-1) + if completed { + pkgs.Set(2000) + pkgs.SetCompleted() + } else { + pkgs.Set(300) + } + + eol := &progress.Manual{} + eol.SetTotal(-1) + if completed { + eol.Set(45) + eol.SetCompleted() + } else { + eol.Set(40) + } + + fixed := &progress.Manual{} + fixed.SetTotal(-1) + if completed { + fixed.Set(35) + fixed.SetCompleted() + } else { + fixed.Set(30) + } + + ignored := &progress.Manual{} + ignored.SetTotal(-1) + if completed { + ignored.Set(5) + ignored.SetCompleted() + } else { + ignored.Set(4) + } + + dropped := &progress.Manual{} + dropped.SetTotal(-1) + if completed { + dropped.Set(3) + dropped.SetCompleted() + } else { + dropped.Set(2) + } + + return monitor.Matching{ + PackagesProcessed: pkgs, + MatchesDiscovered: eol, + } +} diff --git a/cmd/xeol/cli/ui/handle_update_eol_database.go b/cmd/xeol/cli/ui/handle_update_eol_database.go new file mode 100644 index 00000000..5b7ca4c2 --- /dev/null +++ b/cmd/xeol/cli/ui/handle_update_eol_database.go @@ -0,0 +1,53 @@ +package ui + +import ( + "fmt" + + "github.com/anchore/bubbly/bubbles/taskprogress" + tea "github.com/charmbracelet/bubbletea" + "github.com/dustin/go-humanize" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/xeol/event/parsers" +) + +type dbDownloadProgressStager struct { + prog progress.StagedProgressable +} + +func (s dbDownloadProgressStager) Stage() string { + stage := s.prog.Stage() + if stage == "downloading" { + // note: since validation is baked into the download progress there is no visibility into this stage. + // for that reason we report "validating" on the last byte being downloaded (which tends to be the longest + // since go-downloader is doing this work). + if s.prog.Current() >= s.prog.Size()-1 { + return "validating" + } + // show intermediate progress of the download + return fmt.Sprintf("%s / %s", humanize.Bytes(uint64(s.prog.Current())), humanize.Bytes(uint64(s.prog.Size()))) + } + return stage +} + +func (m *Handler) handleUpdateEolDatabase(e partybus.Event) []tea.Model { + prog, err := parsers.ParseUpdateEolDatabase(e) + if err != nil { + log.WithFields("error", err).Warn("unable to parse event") + return nil + } + + tsk := m.newTaskProgress( + taskprogress.Title{ + Default: "EOL DB", + }, + taskprogress.WithStagedProgressable(prog), // ignore the static stage provided by the event + taskprogress.WithStager(dbDownloadProgressStager{prog: prog}), + ) + + tsk.HideStageOnSuccess = false + + return []tea.Model{tsk} +} diff --git a/cmd/xeol/cli/ui/handle_update_eol_database_test.go b/cmd/xeol/cli/ui/handle_update_eol_database_test.go new file mode 100644 index 00000000..63db7d65 --- /dev/null +++ b/cmd/xeol/cli/ui/handle_update_eol_database_test.go @@ -0,0 +1,97 @@ +package ui + +import ( + "testing" + "time" + + "github.com/anchore/bubbly/bubbles/taskprogress" + tea "github.com/charmbracelet/bubbletea" + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + "github.com/wagoodman/go-progress" + + "github.com/xeol-io/xeol/xeol/event" +) + +func TestHandler_handleUpdateEolDatabase(t *testing.T) { + + tests := []struct { + name string + eventFn func(*testing.T) partybus.Event + iterations int + }{ + { + name: "downloading DB", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(50) + + mon := struct { + progress.Progressable + progress.Stager + }{ + Progressable: prog, + Stager: &progress.Stage{ + Current: "current", + }, + } + + return partybus.Event{ + Type: event.UpdateEolDatabase, + Value: mon, + } + }, + }, + { + name: "DB download complete", + eventFn: func(t *testing.T) partybus.Event { + prog := &progress.Manual{} + prog.SetTotal(100) + prog.Set(100) + prog.SetCompleted() + + mon := struct { + progress.Progressable + progress.Stager + }{ + Progressable: prog, + Stager: &progress.Stage{ + Current: "current", + }, + } + + return partybus.Event{ + Type: event.UpdateEolDatabase, + Value: mon, + } + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + e := tt.eventFn(t) + handler := New(DefaultHandlerConfig()) + handler.WindowSize = tea.WindowSizeMsg{ + Width: 100, + Height: 80, + } + + models := handler.Handle(e) + require.Len(t, models, 1) + model := models[0] + + tsk, ok := model.(taskprogress.Model) + require.True(t, ok) + + got := runModel(t, tsk, tt.iterations, taskprogress.TickMsg{ + Time: time.Now(), + Sequence: tsk.Sequence(), + ID: tsk.ID(), + }) + t.Log(got) + snaps.MatchSnapshot(t, got) + }) + } +} diff --git a/cmd/xeol/cli/ui/handler.go b/cmd/xeol/cli/ui/handler.go new file mode 100644 index 00000000..ba4b35c3 --- /dev/null +++ b/cmd/xeol/cli/ui/handler.go @@ -0,0 +1,65 @@ +package ui + +import ( + "sync" + + "github.com/anchore/bubbly" + "github.com/anchore/bubbly/bubbles/taskprogress" + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/xeol/event" +) + +var _ interface { + bubbly.EventHandler + bubbly.MessageListener + bubbly.HandleWaiter +} = (*Handler)(nil) + +type HandlerConfig struct { + TitleWidth int + AdjustDefaultTask func(taskprogress.Model) taskprogress.Model +} + +type Handler struct { + WindowSize tea.WindowSizeMsg + Running *sync.WaitGroup + Config HandlerConfig + + bubbly.EventHandler +} + +func DefaultHandlerConfig() HandlerConfig { + return HandlerConfig{ + TitleWidth: 30, + } +} + +func New(cfg HandlerConfig) *Handler { + d := bubbly.NewEventDispatcher() + + h := &Handler{ + EventHandler: d, + Running: &sync.WaitGroup{}, + Config: cfg, + } + + // register all supported event types with the respective handler functions + d.AddHandlers(map[partybus.EventType]bubbly.EventHandlerFn{ + event.UpdateEolDatabase: h.handleUpdateEolDatabase, + event.EolScanningStarted: h.handleEolScanningStarted, + }) + + return h +} + +func (m *Handler) OnMessage(msg tea.Msg) { + if msg, ok := msg.(tea.WindowSizeMsg); ok { + m.WindowSize = msg + } +} + +func (m *Handler) Wait() { + m.Running.Wait() +} diff --git a/cmd/xeol/cli/ui/new_task_progress.go b/cmd/xeol/cli/ui/new_task_progress.go new file mode 100644 index 00000000..036f7b37 --- /dev/null +++ b/cmd/xeol/cli/ui/new_task_progress.go @@ -0,0 +1,19 @@ +package ui + +import "github.com/anchore/bubbly/bubbles/taskprogress" + +func (m Handler) newTaskProgress(title taskprogress.Title, opts ...taskprogress.Option) taskprogress.Model { + tsk := taskprogress.New(m.Running, opts...) + + tsk.HideProgressOnSuccess = true + tsk.HideStageOnSuccess = true + tsk.WindowSize = m.WindowSize + tsk.TitleWidth = m.Config.TitleWidth + tsk.TitleOptions = title + + if m.Config.AdjustDefaultTask != nil { + tsk = m.Config.AdjustDefaultTask(tsk) + } + + return tsk +} diff --git a/cmd/xeol/cli/ui/util_test.go b/cmd/xeol/cli/ui/util_test.go new file mode 100644 index 00000000..81c8525d --- /dev/null +++ b/cmd/xeol/cli/ui/util_test.go @@ -0,0 +1,69 @@ +package ui + +import ( + "reflect" + "sync" + "testing" + "unsafe" + + tea "github.com/charmbracelet/bubbletea" +) + +func runModel(t testing.TB, m tea.Model, iterations int, message tea.Msg, wgs ...*sync.WaitGroup) string { + t.Helper() + if iterations == 0 { + iterations = 1 + } + m.Init() + var cmd tea.Cmd = func() tea.Msg { + return message + } + + for _, wg := range wgs { + if wg != nil { + wg.Wait() + } + } + + for i := 0; cmd != nil && i < iterations; i++ { + msgs := flatten(cmd()) + var nextCmds []tea.Cmd + var next tea.Cmd + for _, msg := range msgs { + t.Logf("Message: %+v %+v\n", reflect.TypeOf(msg), msg) + m, next = m.Update(msg) + nextCmds = append(nextCmds, next) + } + cmd = tea.Batch(nextCmds...) + } + return m.View() +} + +func flatten(p tea.Msg) (msgs []tea.Msg) { + if reflect.TypeOf(p).Name() == "batchMsg" { + partials := extractBatchMessages(p) + for _, m := range partials { + msgs = append(msgs, flatten(m)...) + } + } else { + msgs = []tea.Msg{p} + } + return msgs +} + +func extractBatchMessages(m tea.Msg) (ret []tea.Msg) { + sliceMsgType := reflect.SliceOf(reflect.TypeOf(tea.Cmd(nil))) + value := reflect.ValueOf(m) // note: this is technically unaddressable + + // make our own instance that is addressable + valueCopy := reflect.New(value.Type()).Elem() + valueCopy.Set(value) + + cmds := reflect.NewAt(sliceMsgType, unsafe.Pointer(valueCopy.UnsafeAddr())).Elem() + for i := 0; i < cmds.Len(); i++ { + item := cmds.Index(i) + r := item.Call(nil) + ret = append(ret, r[0].Interface().(tea.Msg)) + } + return ret +} diff --git a/cmd/xeol/internal/constants.go b/cmd/xeol/internal/constants.go new file mode 100644 index 00000000..d9124097 --- /dev/null +++ b/cmd/xeol/internal/constants.go @@ -0,0 +1,6 @@ +package internal + +const ( + Xeol = "xeol" + NotProvided = "[not provided]" +) diff --git a/cmd/xeol/internal/ui/__snapshots__/post_ui_event_writer_test.snap b/cmd/xeol/internal/ui/__snapshots__/post_ui_event_writer_test.snap new file mode 100755 index 00000000..1a5bb919 --- /dev/null +++ b/cmd/xeol/internal/ui/__snapshots__/post_ui_event_writer_test.snap @@ -0,0 +1,41 @@ + +[Test_postUIEventWriter_write/no_events/stdout - 1] + +--- + +[Test_postUIEventWriter_write/no_events/stderr - 1] + +--- + +[Test_postUIEventWriter_write/all_events/stdout - 1] + + + + + +--- + +[Test_postUIEventWriter_write/all_events/stderr - 1] + + + + + + + +A newer version of xeol is available for download: v0.33.0 (installed version is [not provided]) + +--- + +[Test_postUIEventWriter_write/quiet_only_shows_report/stdout - 1] + + +--- + +[Test_postUIEventWriter_write/quiet_only_shows_report/stderr - 1] + +--- diff --git a/cmd/xeol/internal/ui/no_ui.go b/cmd/xeol/internal/ui/no_ui.go new file mode 100644 index 00000000..f6455b87 --- /dev/null +++ b/cmd/xeol/internal/ui/no_ui.go @@ -0,0 +1,44 @@ +package ui + +import ( + "os" + + "github.com/anchore/clio" + "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/xeol/event" +) + +var _ clio.UI = (*NoUI)(nil) + +type NoUI struct { + finalizeEvents []partybus.Event + subscription partybus.Unsubscribable + quiet bool +} + +func None(quiet bool) *NoUI { + return &NoUI{ + quiet: quiet, + } +} + +func (n *NoUI) Setup(subscription partybus.Unsubscribable) error { + n.subscription = subscription + return nil +} + +func (n *NoUI) Handle(e partybus.Event) error { + switch e.Type { + case event.CLIReport, event.CLINotification, event.EolPolicyEvaluationMessage, event.NotaryPolicyEvaluationMessage: + // keep these for when the UI is terminated to show to the screen (or perform other events) + n.finalizeEvents = append(n.finalizeEvents, e) + case event.CLIExit: + return n.subscription.Unsubscribe() + } + return nil +} + +func (n NoUI) Teardown(_ bool) error { + return newPostUIEventWriter(os.Stdout, os.Stderr).write(n.quiet, n.finalizeEvents...) +} diff --git a/cmd/xeol/internal/ui/post_ui_event_writer.go b/cmd/xeol/internal/ui/post_ui_event_writer.go new file mode 100644 index 00000000..6f0e6dd1 --- /dev/null +++ b/cmd/xeol/internal/ui/post_ui_event_writer.go @@ -0,0 +1,212 @@ +package ui + +import ( + "fmt" + "io" + "strings" + + "github.com/charmbracelet/lipgloss" + "github.com/hashicorp/go-multierror" + "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/xeol/event" + "github.com/xeol-io/xeol/xeol/event/parsers" + policyTypes "github.com/xeol-io/xeol/xeol/policy/types" +) + +var ( + terminalRed = lipgloss.Color("196") + terminalYellow = lipgloss.Color("214") +) + +type postUIEventWriter struct { + handles []postUIHandle +} + +type postUIHandle struct { + respectQuiet bool + event partybus.EventType + writer io.Writer + dispatch eventWriter +} + +type eventWriter func(io.Writer, ...partybus.Event) error + +func newPostUIEventWriter(stdout, stderr io.Writer) *postUIEventWriter { + return &postUIEventWriter{ + handles: []postUIHandle{ + { + event: event.EolPolicyEvaluationMessage, + respectQuiet: false, + writer: stdout, + dispatch: writeEolPolicyEvaluationMessage, + }, + { + event: event.NotaryPolicyEvaluationMessage, + respectQuiet: false, + writer: stdout, + dispatch: writeNotaryPolicyEvaluationMessage, + }, + { + event: event.CLIReport, + respectQuiet: false, + writer: stdout, + dispatch: writeReports, + }, + { + event: event.CLINotification, + respectQuiet: true, + writer: stderr, + dispatch: writeNotifications, + }, + { + event: event.CLIAppUpdateAvailable, + respectQuiet: true, + writer: stderr, + dispatch: writeAppUpdate, + }, + }, + } +} + +func (w postUIEventWriter) write(quiet bool, events ...partybus.Event) error { + var errs error + for _, h := range w.handles { + if quiet && h.respectQuiet { + continue + } + + for _, e := range events { + if e.Type != h.event { + continue + } + + if err := h.dispatch(h.writer, e); err != nil { + errs = multierror.Append(errs, err) + } + } + } + return errs +} + +func writeReports(writer io.Writer, events ...partybus.Event) error { + var reports []string + for _, e := range events { + _, report, err := parsers.ParseCLIReport(e) + if err != nil { + log.WithFields("error", err).Warn("failed to gather final report") + continue + } + + // remove all whitespace padding from the end of the report + reports = append(reports, strings.TrimRight(report, "\n ")+"\n") + } + + // prevent the double new-line at the end of the report + report := strings.Join(reports, "\n") + + if _, err := fmt.Fprint(writer, report); err != nil { + return fmt.Errorf("failed to write final report to stdout: %w", err) + } + return nil +} + +func writeNotifications(writer io.Writer, events ...partybus.Event) error { + // 13 = high intensity magenta (ANSI 16 bit code) + style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")) + + for _, e := range events { + _, notification, err := parsers.ParseCLINotification(e) + if err != nil { + log.WithFields("error", err).Warn("failed to parse notification") + continue + } + + if _, err := fmt.Fprintln(writer, style.Render(notification)); err != nil { + // don't let this be fatal + log.WithFields("error", err).Warn("failed to write final notifications") + } + } + return nil +} + +func writeNotaryPolicyEvaluationMessage(writer io.Writer, events ...partybus.Event) error { + for _, e := range events { + // show the report to stdout + nt, err := parsers.ParseNotaryPolicyEvaluationMessage(e) + if err != nil { + return fmt.Errorf("bad %s event: %w", e.Type, err) + } + + var notice string + if nt.Action == policyTypes.PolicyActionDeny { + notice = lipgloss.NewStyle().Foreground(terminalRed).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party.\n", + nt.Action, nt.Type, nt.ImageReference)) + } else { + if nt.FailDate != "" { + notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party. This policy will fail builds starting on %s.\n", + nt.Action, nt.Type, nt.ImageReference, nt.FailDate)) + } else { + notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party.\n", + nt.Action, nt.Type, nt.ImageReference)) + } + } + if _, err := fmt.Fprint(writer, strings.TrimSpace(notice)); err != nil { + // don't let this be fatal + log.WithFields("error", err).Warn("failed to write app update notification") + } + } + return nil +} + +func writeEolPolicyEvaluationMessage(writer io.Writer, events ...partybus.Event) error { + for _, e := range events { + // show the report to stdout + pt, err := parsers.ParseEolPolicyEvaluationMessage(e) + if err != nil { + return fmt.Errorf("bad %s event: %w", e.Type, err) + } + + var notice string + if pt.Action == policyTypes.PolicyActionDeny { + notice = lipgloss.NewStyle().Foreground(terminalRed).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle)) + } else { + if pt.FailDate != "" { + notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version. This policy will fail builds starting on %s.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle, pt.FailDate)) + } else { + notice = lipgloss.NewStyle().Foreground(terminalYellow).Italic(true).Render(fmt.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle)) + } + } + if _, err := fmt.Fprint(writer, strings.TrimSpace(notice)); err != nil { + // don't let this be fatal + log.WithFields("error", err).Warn("failed to write app update notification") + } + } + return nil +} + +func writeAppUpdate(writer io.Writer, events ...partybus.Event) error { + // 13 = high intensity magenta (ANSI 16 bit code) + italics + style := lipgloss.NewStyle().Foreground(lipgloss.Color("13")).Italic(true) + + for _, e := range events { + version, err := parsers.ParseCLIAppUpdateAvailable(e) + if err != nil { + log.WithFields("error", err).Warn("failed to parse app update notification") + continue + } + + if version.New == "" { + continue + } + + notice := fmt.Sprintf("A newer version of xeol is available for download: %s (installed version is %s)", version.New, version.Current) + + if _, err := fmt.Fprintln(writer, style.Render(notice)); err != nil { + // don't let this be fatal + log.WithFields("error", err).Warn("failed to write app update notification") + } + } + return nil +} diff --git a/cmd/xeol/internal/ui/post_ui_event_writer_test.go b/cmd/xeol/internal/ui/post_ui_event_writer_test.go new file mode 100644 index 00000000..363acbb0 --- /dev/null +++ b/cmd/xeol/internal/ui/post_ui_event_writer_test.go @@ -0,0 +1,102 @@ +package ui + +import ( + "bytes" + "testing" + + "github.com/gkampitakis/go-snaps/snaps" + "github.com/stretchr/testify/require" + "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/xeol/event" + "github.com/xeol-io/xeol/xeol/event/parsers" +) + +func Test_postUIEventWriter_write(t *testing.T) { + + tests := []struct { + name string + quiet bool + events []partybus.Event + wantErr require.ErrorAssertionFunc + }{ + { + name: "no events", + }, + { + name: "all events", + events: []partybus.Event{ + { + Type: event.CLINotification, + Value: "\n\n\n\n", + }, + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: "v0.33.0", + Current: "[not provided]", + }, + }, + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIReport, + Value: "\n\n\n\n", + }, + { + Type: event.CLIReport, + Value: "", + }, + }, + }, + { + name: "quiet only shows report", + quiet: true, + events: []partybus.Event{ + + { + Type: event.CLINotification, + Value: "", + }, + { + Type: event.CLIAppUpdateAvailable, + Value: parsers.UpdateCheck{ + New: "", + Current: "", + }, + }, + { + Type: event.CLIReport, + Value: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.wantErr == nil { + tt.wantErr = require.NoError + } + + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + w := newPostUIEventWriter(stdout, stderr) + + tt.wantErr(t, w.write(tt.quiet, tt.events...)) + + t.Run("stdout", func(t *testing.T) { + snaps.MatchSnapshot(t, stdout.String()) + }) + + t.Run("stderr", func(t *testing.T) { + snaps.MatchSnapshot(t, stderr.String()) + }) + }) + } +} diff --git a/cmd/xeol/internal/ui/ui.go b/cmd/xeol/internal/ui/ui.go new file mode 100644 index 00000000..cc3f31a7 --- /dev/null +++ b/cmd/xeol/internal/ui/ui.go @@ -0,0 +1,166 @@ +package ui + +import ( + "os" + "sync" + + "github.com/anchore/bubbly" + "github.com/anchore/bubbly/bubbles/frame" + "github.com/anchore/clio" + "github.com/anchore/go-logger" + tea "github.com/charmbracelet/bubbletea" + "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/xeol/event" +) + +var _ interface { + tea.Model + partybus.Responder + clio.UI +} = (*UI)(nil) + +type UI struct { + program *tea.Program + running *sync.WaitGroup + quiet bool + subscription partybus.Unsubscribable + finalizeEvents []partybus.Event + + handler *bubbly.HandlerCollection + frame tea.Model +} + +func New(quiet bool, hs ...bubbly.EventHandler) *UI { + return &UI{ + handler: bubbly.NewHandlerCollection(hs...), + frame: frame.New(), + running: &sync.WaitGroup{}, + quiet: quiet, + } +} + +func (m *UI) Setup(subscription partybus.Unsubscribable) error { + // we still want to collect log messages, however, we also the logger shouldn't write to the screen directly + if logWrapper, ok := log.Get().(logger.Controller); ok { + logWrapper.SetOutput(m.frame.(*frame.Frame).Footer()) + } + + m.subscription = subscription + m.program = tea.NewProgram(m, tea.WithOutput(os.Stderr), tea.WithInput(os.Stdin)) + m.running.Add(1) + + go func() { + defer m.running.Done() + if _, err := m.program.Run(); err != nil { + log.Errorf("unable to start UI: %+v", err) + m.exit() + } + }() + + return nil +} + +func (m *UI) exit() { + // stop the event loop + bus.Exit() +} + +func (m *UI) Handle(e partybus.Event) error { + if m.program != nil { + m.program.Send(e) + if e.Type == event.CLIExit { + return m.subscription.Unsubscribe() + } + } + return nil +} + +func (m *UI) Teardown(force bool) error { + if !force { + m.handler.Wait() + m.program.Quit() + // typically in all cases we would want to wait for the UI to finish. However there are still error cases + // that are not accounted for, resulting in hangs. For now, we'll just wait for the UI to finish in the + // happy path only. There will always be an indication of the problem to the user via reporting the error + // string from the worker (outside of the UI after teardown). + m.running.Wait() + } else { + m.program.Kill() + } + + // TODO: allow for writing out the full log output to the screen (only a partial log is shown currently) + // this needs coordination to know what the last frame event is to change the state accordingly (which isn't possible now) + + return newPostUIEventWriter(os.Stdout, os.Stderr).write(m.quiet, m.finalizeEvents...) +} + +// bubbletea.Model functions + +func (m UI) Init() tea.Cmd { + return m.frame.Init() +} + +func (m UI) RespondsTo() []partybus.EventType { + return append([]partybus.EventType{ + event.CLIReport, + event.CLINotification, + event.CLIExit, + event.CLIAppUpdateAvailable, + }, m.handler.RespondsTo()...) +} + +func (m *UI) Update(msg tea.Msg) (tea.Model, tea.Cmd) { + // note: we need a pointer receiver such that the same instance of UI used in Teardown is referenced (to keep finalize events) + + var cmds []tea.Cmd + + // allow for non-partybus UI updates (such as window size events). Note: these must not affect existing models, + // that is the responsibility of the frame object on this UI object. The handler is a factory of models + // which the frame is responsible for the lifecycle of. This update allows for injecting the initial state + // of the world when creating those models. + m.handler.OnMessage(msg) + + switch msg := msg.(type) { + case tea.KeyMsg: + switch msg.String() { + case "esc", "ctrl+c": + m.exit() + return m, tea.Quit + } + + case partybus.Event: + log.WithFields("component", "ui").Tracef("event: %q", msg.Type) + + switch msg.Type { + case event.CLIReport, event.CLINotification, event.CLIExit, event.CLIAppUpdateAvailable, event.EolPolicyEvaluationMessage, event.NotaryPolicyEvaluationMessage: + // keep these for when the UI is terminated to show to the screen (or perform other events) + m.finalizeEvents = append(m.finalizeEvents, msg) + + // why not return tea.Quit here for exit events? because there may be UI components that still need the update-render loop. + // for this reason we'll let the syft event loop call Teardown() which will explicitly wait for these components + return m, nil + } + + for _, newModel := range m.handler.Handle(msg) { + if newModel == nil { + continue + } + cmds = append(cmds, newModel.Init()) + m.frame.(*frame.Frame).AppendModel(newModel) + } + // intentionally fallthrough to update the frame model + } + + frameModel, cmd := m.frame.Update(msg) + m.frame = frameModel + cmds = append(cmds, cmd) + + return m, tea.Batch(cmds...) +} + +func (m UI) View() string { + return m.frame.View() +} diff --git a/cmd/xeol/main.go b/cmd/xeol/main.go new file mode 100644 index 00000000..8a95cc1d --- /dev/null +++ b/cmd/xeol/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "github.com/anchore/clio" + _ "github.com/glebarez/sqlite" + + "github.com/xeol-io/xeol/cmd/xeol/cli" + "github.com/xeol-io/xeol/cmd/xeol/internal" +) + +// applicationName is the non-capitalized name of the application (do not change this) +const applicationName = internal.Xeol + +// all variables here are provided as build-time arguments, with clear default values +var ( + version = internal.NotProvided + buildDate = internal.NotProvided + gitCommit = internal.NotProvided + gitDescription = internal.NotProvided +) + +func main() { + app := cli.Application( + clio.Identification{ + Name: applicationName, + Version: version, + BuildDate: buildDate, + GitCommit: gitCommit, + GitDescription: gitDescription, + }, + ) + + app.Run() +} diff --git a/go.mod b/go.mod index 009d5fcd..e97ef774 100644 --- a/go.mod +++ b/go.mod @@ -1,26 +1,30 @@ module github.com/xeol-io/xeol -go 1.20 +go 1.21.1 require ( github.com/CycloneDX/cyclonedx-go v0.7.2 github.com/Masterminds/semver v1.5.0 github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d github.com/adrg/xdg v0.4.0 - github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 - github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 + github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 + github.com/anchore/clio v0.0.0-20230915181724-f1acbce87918 + github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 - github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 - github.com/anchore/stereoscope v0.0.0-20230609190519-5b5049bf4d3a - github.com/anchore/syft v0.83.1 + github.com/anchore/stereoscope v0.0.0-20230919183137-5841b53a0375 + github.com/anchore/syft v0.91.0 github.com/bmatcuk/doublestar/v2 v2.0.4 + github.com/charmbracelet/bubbletea v0.24.2 + github.com/charmbracelet/lipgloss v0.8.0 github.com/docker/distribution v2.8.2+incompatible - github.com/docker/docker v24.0.5+incompatible + github.com/docker/docker v24.0.6+incompatible github.com/dustin/go-humanize v1.0.1 github.com/facebookincubator/nvdtools v0.1.5 github.com/gabriel-vasile/mimetype v1.4.2 - github.com/go-git/go-git/v5 v5.7.0 + github.com/gkampitakis/go-snaps v0.4.10 + github.com/glebarez/sqlite v1.9.0 + github.com/go-git/go-git/v5 v5.9.0 github.com/go-test/deep v1.1.0 github.com/google/go-cmp v0.5.9 github.com/google/uuid v1.3.1 @@ -43,29 +47,28 @@ require ( github.com/oras-project/oras-credentials-go v0.3.0 github.com/pkg/errors v0.9.1 github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e - github.com/sergi/go-diff v1.3.1 github.com/sigstore/sigstore v1.7.3 - github.com/sirupsen/logrus v1.9.3 github.com/spf13/afero v1.9.5 github.com/spf13/cobra v1.7.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.4 - github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 - github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 - github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb - golang.org/x/term v0.12.0 - gopkg.in/yaml.v2 v2.4.0 - gorm.io/gorm v1.23.5 + github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 + github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b + github.com/wagoodman/go-progress v0.0.0-20230911172108-cf810b7e365c + gorm.io/gorm v1.25.2 oras.land/oras-go/v2 v2.2.1 ) +replace github.com/anchore/syft => github.com/noqcks/syft v0.0.0-20230920223517-21656aa2830c + require ( cloud.google.com/go v0.110.2 // indirect cloud.google.com/go/compute v1.20.1 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.0.1 // indirect + cloud.google.com/go/iam v1.1.0 // indirect cloud.google.com/go/storage v1.29.0 // indirect + dario.cat/mergo v1.0.0 // indirect + github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 // indirect + github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/DataDog/zstd v1.4.5 // indirect @@ -73,35 +76,58 @@ require ( github.com/Masterminds/semver/v3 v3.2.1 // indirect github.com/Masterminds/sprig/v3 v3.2.3 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec // indirect + github.com/Microsoft/hcsshim v0.10.0-rc.7 // indirect + github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect github.com/acobaugh/osrelease v0.1.0 // indirect github.com/acomagu/bufpipe v1.0.4 // indirect + github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe // indirect github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb // indirect github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 // indirect + github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 // indirect github.com/andybalholm/brotli v1.0.4 // indirect - github.com/aws/aws-sdk-go v1.44.268 // indirect + github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 // indirect + github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 // indirect + github.com/aws/aws-sdk-go v1.44.288 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/becheran/wildmatch-go v1.0.0 // indirect github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect github.com/bmatcuk/doublestar/v4 v4.6.0 // indirect + github.com/charmbracelet/bubbles v0.16.1 // indirect + github.com/charmbracelet/harmonica v0.2.0 // indirect github.com/cloudflare/circl v1.3.3 // indirect + github.com/containerd/cgroups v1.1.0 // indirect + github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 // indirect github.com/containerd/containerd v1.7.0 // indirect + github.com/containerd/continuity v0.3.0 // indirect + github.com/containerd/fifo v1.1.0 // indirect github.com/containerd/stargz-snapshotter/estargz v0.14.3 // indirect + github.com/containerd/ttrpc v1.2.1 // indirect + github.com/containerd/typeurl/v2 v2.1.0 // indirect + github.com/cyphar/filepath-securejoin v0.2.4 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/deitch/magic v0.0.0-20230404182410-1ff89d7342da // indirect github.com/docker/cli v24.0.0+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go-connections v0.4.0 // indirect + github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 // indirect + github.com/edsrzf/mmap-go v1.1.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318 // indirect + github.com/felixge/fgprof v0.9.3 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/fxamacker/cbor/v2 v2.4.0 // indirect - github.com/github/go-spdx/v2 v2.1.2 // indirect + github.com/github/go-spdx/v2 v2.2.0 // indirect + github.com/gkampitakis/ciinfo v0.2.5 // indirect + github.com/gkampitakis/go-diff v1.3.2 // indirect + github.com/glebarez/go-sqlite v1.21.2 // indirect github.com/go-asn1-ber/asn1-ber v1.5.4 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.4.1 // indirect + github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-ldap/ldap/v3 v3.4.5 // indirect + github.com/go-logr/logr v1.2.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect github.com/go-restruct/restruct v1.2.0-alpha // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect @@ -110,101 +136,128 @@ require ( github.com/golang/snappy v0.0.4 // indirect github.com/google/go-containerregistry v0.16.1 // indirect github.com/google/licensecheck v0.3.1 // indirect + github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 // indirect github.com/google/s2a-go v0.1.4 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.2.4 // indirect github.com/googleapis/gax-go/v2 v2.11.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect github.com/hashicorp/hcl v1.0.1-vault-5 // indirect github.com/huandu/xstrings v1.4.0 // indirect + github.com/iancoleman/strcase v0.3.0 // indirect github.com/imdario/mergo v0.3.15 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jinzhu/copier v0.3.5 // indirect + github.com/jinzhu/copier v0.4.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.4 // indirect + github.com/jinzhu/now v1.1.5 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect + github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/klauspost/pgzip v1.2.5 // indirect github.com/knqyf263/go-rpmdb v0.0.0-20230301153543-ba94b245509b // indirect + github.com/kr/pretty v0.3.1 // indirect + github.com/kr/text v0.2.0 // indirect github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf // indirect github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 // indirect + github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect + github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d // indirect - github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 // indirect + github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/moby/locker v1.0.1 // indirect + github.com/moby/sys/mountinfo v0.6.2 // indirect + github.com/moby/sys/sequential v0.5.0 // indirect + github.com/moby/sys/signal v0.7.0 // indirect + github.com/moby/term v0.5.0 // indirect + github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 // indirect + github.com/muesli/cancelreader v0.2.2 // indirect + github.com/muesli/reflow v0.3.0 // indirect + github.com/muesli/termenv v0.15.2 // indirect github.com/notaryproject/notation-core-go v1.0.0 // indirect github.com/nwaples/rardecode v1.1.2 // indirect + github.com/opencontainers/runc v1.1.5 // indirect + github.com/opencontainers/runtime-spec v1.1.0-rc.1 // indirect + github.com/opencontainers/selinux v1.11.0 // indirect + github.com/pborman/indent v1.2.1 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pjbgf/sha1cd v0.3.0 // indirect + github.com/pkg/profile v1.7.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/rivo/uniseg v0.2.0 // indirect - github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/rogpeppe/go-internal v1.11.0 // indirect + github.com/saferwall/pe v1.4.5 // indirect + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect github.com/sassoftware/go-rpmutils v0.2.0 // indirect + github.com/sergi/go-diff v1.3.1 // indirect github.com/shopspring/decimal v1.3.1 // indirect - github.com/skeema/knownhosts v1.1.1 // indirect - github.com/spdx/tools-golang v0.5.2 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/skeema/knownhosts v1.2.0 // indirect + github.com/spdx/tools-golang v0.5.3 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/stretchr/objx v0.5.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.16.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect - github.com/sylabs/sif/v2 v2.8.1 // indirect + github.com/sylabs/sif/v2 v2.11.5 // indirect github.com/sylabs/squashfs v0.6.1 // indirect github.com/therootcompany/xz v1.0.1 // indirect github.com/theupdateframework/go-tuf v0.5.2 // indirect + github.com/tidwall/gjson v1.16.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.1 // indirect + github.com/tidwall/sjson v1.2.5 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect github.com/ulikunitz/xz v0.5.10 // indirect github.com/vbatts/go-mtree v0.5.3 // indirect github.com/vbatts/tar-split v0.11.3 // indirect github.com/veraison/go-cose v1.1.0 // indirect - github.com/vifraa/gopom v0.2.1 // indirect + github.com/vifraa/gopom v1.0.0 // indirect github.com/x448/float16 v0.8.4 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect + go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 // indirect go.opencensus.io v0.24.0 // indirect + go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel/trace v1.14.0 // indirect go.uber.org/goleak v1.2.1 // indirect - golang.org/x/crypto v0.12.0 // indirect - golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 // indirect + golang.org/x/crypto v0.13.0 // indirect + golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 // indirect golang.org/x/mod v0.12.0 // indirect - golang.org/x/net v0.14.0 // indirect + golang.org/x/net v0.15.0 // indirect golang.org/x/oauth2 v0.11.0 // indirect golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.12.0 // indirect - golang.org/x/text v0.12.0 // indirect + golang.org/x/term v0.12.0 // indirect + golang.org/x/text v0.13.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.9.1 // indirect + golang.org/x/tools v0.13.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/api v0.126.0 // indirect + google.golang.org/api v0.128.0 // indirect google.golang.org/appengine v1.6.7 // indirect google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/grpc v1.55.0 // indirect + google.golang.org/grpc v1.56.0 // indirect google.golang.org/protobuf v1.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/square/go-jose.v2 v2.6.0 // indirect gopkg.in/warnings.v0 v0.1.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.4.0 // indirect - lukechampine.com/uint128 v1.2.0 // indirect - modernc.org/cc/v3 v3.40.0 // indirect - modernc.org/ccgo/v3 v3.16.13 // indirect - modernc.org/libc v1.22.5 // indirect + modernc.org/libc v1.24.1 // indirect modernc.org/mathutil v1.5.0 // indirect - modernc.org/memory v1.5.0 // indirect - modernc.org/opt v0.1.3 // indirect - modernc.org/sqlite v1.23.1 // indirect - modernc.org/strutil v1.1.3 // indirect - modernc.org/token v1.0.1 // indirect + modernc.org/memory v1.6.0 // indirect + modernc.org/sqlite v1.25.0 // indirect ) diff --git a/go.sum b/go.sum index 45e4e83c..6b803365 100644 --- a/go.sum +++ b/go.sum @@ -113,8 +113,8 @@ cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y97 cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v1.0.1 h1:lyeCAU6jpnVNrE9zGQkTl3WgNgK/X+uWwaw0kynZJMU= -cloud.google.com/go/iam v1.0.1/go.mod h1:yR3tmSL8BcZB4bxByRv2jkSIahVmCtfKZwLYGBalRE8= +cloud.google.com/go/iam v1.1.0 h1:67gSqaPukx7O8WLLHMa0PNs3EBGd2eE4d+psbO/CO94= +cloud.google.com/go/iam v1.1.0/go.mod h1:nxdHjaKfCr7fNYx/HJMM8LgiMugmveWlkatear5gVyk= cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= @@ -189,7 +189,13 @@ cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xX cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1 h1:EKPd1INOIyr5hWOWhvpmQpY6tKjeG0hT1s3AMC/9fic= +github.com/AdaLogics/go-fuzz-headers v0.0.0-20230106234847-43070de90fa1/go.mod h1:VzwV+t+dZ9j/H867F1M2ziD+yLHtB46oM35FxxMJ4d0= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652 h1:+vTEFqeoeur6XSq06bs+roX3YiT49gUniJK7Zky7Xjg= +github.com/AdamKorcz/go-118-fuzz-build v0.0.0-20221215162035-5330a85ea652/go.mod h1:OahwfttHWG6eJ0clwcfBAHoDI6X/LV/15hx/wlMZSrU= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= @@ -215,9 +221,11 @@ github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBa github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= +github.com/Microsoft/hcsshim v0.10.0-rc.7 h1:HBytQPxcv8Oy4244zbQbe6hnOnx544eL5QPUqhJldz8= +github.com/Microsoft/hcsshim v0.10.0-rc.7/go.mod h1:ILuwjA+kNW+MrN/w5un7n3mTqkwsFu4Bp05/okFUZlE= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec h1:vV3RryLxt42+ZIVOFbYJCH1jsZNTNmj2NYru5zfx+4E= -github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 h1:kkhsdkhsCvIsutKu5zLMgWtgh9YxGCNAw8Ad8hjwfYg= +github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8= github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo= github.com/acobaugh/osrelease v0.1.0 h1:Yb59HQDGGNhCj4suHaFQQfBps5wyoKLSSX/J/+UifRE= @@ -232,8 +240,14 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 h1:Kk6a4nehpJ3UuJRqlA3JxYxBZEqCeOmATOvrbT4p9RA= github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= -github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8 h1:imgMA0gN0TZx7PSa/pdWqXadBvrz8WsN6zySzCe4XX0= -github.com/anchore/go-logger v0.0.0-20220728155337-03b66a5207d8/go.mod h1:+gPap4jha079qzRTUaehv+UZ6sSdaNwkH0D3b6zhTuk= +github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461 h1:xGu4/uMWucwWV0YV3fpFIQZ6KVfS/Wfhmma8t0s0vRo= +github.com/anchore/bubbly v0.0.0-20230801194016-acdb4981b461/go.mod h1:Ger02eh5NpPm2IqkPAy396HU1KlK3BhOeCljDYXySSk= +github.com/anchore/clio v0.0.0-20230915181724-f1acbce87918 h1:6OAq04XkelroCdjhjv54sMGNTaeNRf78LSSqAQKxId0= +github.com/anchore/clio v0.0.0-20230915181724-f1acbce87918/go.mod h1:XryJ3CIF1T7SbacQV+OPykfKKIbfXnBssYfpjy2peUg= +github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe h1:pVpLCGWdNeskAw7vGNdCAcGMezrNljHIqOc9HaOja5M= +github.com/anchore/fangs v0.0.0-20230818131516-2186b10924fe/go.mod h1:82EGoxZTfBXSW0/zollEP+Qs3wkiKmip5yBT5j+eZpY= +github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a h1:nJ2G8zWKASyVClGVgG7sfM5mwoZlZ2zYpIzN2OhjWkw= +github.com/anchore/go-logger v0.0.0-20230725134548-c21dafa1ec5a/go.mod h1:ubLFmlsv8/DFUQrZwY5syT5/8Er3ugSr4rDFwHsE3hg= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb h1:iDMnx6LIjtjZ46C0akqveX83WFzhpTD3eqOthawb5vU= github.com/anchore/go-macholibre v0.0.0-20220308212642-53e6d0aaf6fb/go.mod h1:DmTY2Mfcv38hsHbG78xMiTDdxFtkHpgYNVDPsF2TgHk= github.com/anchore/go-struct-converter v0.0.0-20221118182256-c68fdcfa2092 h1:aM1rlcoLz8y5B2r4tTLMiVTrMtpfY0O8EScKJxaSaEc= @@ -244,27 +258,31 @@ github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4 h1:rmZG77uXgE github.com/anchore/go-version v1.2.2-0.20210903204242-51efa5b487c4/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E= github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501 h1:AV7qjwMcM4r8wFhJq3jLRztew3ywIyPTRapl2T1s9o8= github.com/anchore/packageurl-go v0.1.1-0.20230104203445-02e0a6721501/go.mod h1:Blo6OgJNiYF41ufcgHKkbCKF2MDOMlrqhXv/ij6ocR4= -github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963 h1:vrf2PYH77vqVJoNR15ZuFJ63qwBMqrmGIt/7VsBhLF8= -github.com/anchore/sqlite v1.4.6-0.20220607210448-bcc6ee5c4963/go.mod h1:AVRyXOUP0hTz9Cb8OlD1XnwA8t4lBPfTuwPHmEUuiLc= -github.com/anchore/stereoscope v0.0.0-20230609190519-5b5049bf4d3a h1:+Vu04kYx8/4W5WcLeg+HjTTYNNvVMEsXmW9lydK0Rz4= -github.com/anchore/stereoscope v0.0.0-20230609190519-5b5049bf4d3a/go.mod h1:0LsgHgXO4QFnk2hsYwtqd3fR18PIZXlFLIl2qb9tu3g= -github.com/anchore/syft v0.83.1 h1:HWHuCHTfvcvlQaIJwHefNtDekjCbMpWLOLKmkxdoJcw= -github.com/anchore/syft v0.83.1/go.mod h1:7KRcwblqUS1pau/ZjSblPOJHM8bFGeQmsbuGutGMATE= +github.com/anchore/stereoscope v0.0.0-20230919183137-5841b53a0375 h1:lpeR4D5PMFXg80QVJgwlJs0QyS8FTBVuFW45Y+3Tat0= +github.com/anchore/stereoscope v0.0.0-20230919183137-5841b53a0375/go.mod h1:75uRHom5WV5ENe3iSK2B37BQvB+47o9HYjpCYaSwlPQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andybalholm/brotli v1.0.1/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= +github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 h1:vmXNl+HDfqqXgr0uY1UgK1GAhps8nbAAtqHNBcgyf+4= +github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go.mod h1:olhPNdiiAAMiSujemd1O/sc6GcyePr23f/6uGKtthNg= +github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 h1:rcEG5HI490FF0a7zuvxOxen52ddygCfNVjP0XOCMl+M= +github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= +github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.44.268 h1:WoK20tlAvsvQzTcE6TajoprbXmTbcud6MjhErL4P/38= -github.com/aws/aws-sdk-go v1.44.268/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aws/aws-sdk-go v1.44.288 h1:Ln7fIao/nl0ACtelgR1I4AiEw/GLNkKcXfCaHupUW5Q= +github.com/aws/aws-sdk-go v1.44.288/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/becheran/wildmatch-go v1.0.0 h1:mE3dGGkTmpKtT4Z+88t8RStG40yN9T+kFEGj2PZFSzA= github.com/becheran/wildmatch-go v1.0.0/go.mod h1:gbMvj0NtVdJ15Mg/mH9uxk2R1QCistMyU7d9KFzroX4= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -279,6 +297,7 @@ github.com/bmatcuk/doublestar/v2 v2.0.4/go.mod h1:QMmcs3H2AUQICWhfzLXz+IYln8lRQm github.com/bmatcuk/doublestar/v4 v4.6.0 h1:HTuxyug8GyFbRkrffIpzNCSK4luc0TY3wzXvzIZhEXc= github.com/bmatcuk/doublestar/v4 v4.6.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bradleyjkemp/cupaloy/v2 v2.8.0 h1:any4BmKE+jGIaMpnU8YgH/I2LPiLBufr6oMMlVBbn9M= +github.com/bradleyjkemp/cupaloy/v2 v2.8.0/go.mod h1:bm7JXdkRd4BHJk9HpwqAI8BoAY1lps46Enkdqw6aRX0= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= @@ -287,10 +306,21 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/charmbracelet/bubbles v0.16.1 h1:6uzpAAaT9ZqKssntbvZMlksWHruQLNxg49H5WdeuYSY= +github.com/charmbracelet/bubbles v0.16.1/go.mod h1:2QCp9LFlEsBQMvIYERr7Ww2H2bA7xen1idUDIzm/+Xc= +github.com/charmbracelet/bubbletea v0.24.2 h1:uaQIKx9Ai6Gdh5zpTbGiWpytMU+CfsPp06RaW2cx/SY= +github.com/charmbracelet/bubbletea v0.24.2/go.mod h1:XdrNrV4J8GiyshTtx3DNuYkR1FDaJmO3l2nejekbsgg= +github.com/charmbracelet/harmonica v0.2.0 h1:8NxJWRWg/bzKqqEaaeFNipOu77YR5t8aSwG4pgaUBiQ= +github.com/charmbracelet/harmonica v0.2.0/go.mod h1:KSri/1RMQOZLbw7AHqgcBycp8pgJnQMYYT8QZRqZ1Ao= +github.com/charmbracelet/lipgloss v0.8.0 h1:IS00fk4XAHcf8uZKc3eHeMUTCxUH6NkaTrdyCQk84RU= +github.com/charmbracelet/lipgloss v0.8.0/go.mod h1:p4eYUZZJ/0oXTuCQKFF8mqyKCz0ja6y+7DniDDw5KKU= +github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= +github.com/cilium/ebpf v0.7.0/go.mod h1:/oI2+1shJiTGAMgl6/RgJr36Eo1jzrRcAWbcXO2usCA= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= @@ -306,15 +336,32 @@ github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211130200136-a8f946100490/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= +github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw= +github.com/containerd/console v1.0.3/go.mod h1:7LqA/THxQ86k76b8c/EMSiaJ3h1eZkMkXar0TQ1gf3U= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81 h1:q2hJAaP1k2wIvVRd/hEHD7lacgqrCPS+k8g1MndzfWY= +github.com/containerd/console v1.0.4-0.20230313162750-1ae8d489ac81/go.mod h1:YynlIjWYF8myEu6sdkwKIvGQq+cOckRm6So2avqoYAk= github.com/containerd/containerd v1.7.0 h1:G/ZQr3gMZs6ZT0qPUZ15znx5QSdQdASW11nXTLTM2Pg= github.com/containerd/containerd v1.7.0/go.mod h1:QfR7Efgb/6X2BDpTPJRvPTYDE9rsF0FsXX9J8sIs/sc= +github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= +github.com/containerd/continuity v0.3.0/go.mod h1:wJEAIwKOm/pBZuBd0JmeTvnLquTB1Ag8espWhkykbPM= +github.com/containerd/fifo v1.1.0 h1:4I2mbh5stb1u6ycIABlBw9zgtlK8viPI9QkQNRQEEmY= +github.com/containerd/fifo v1.1.0/go.mod h1:bmC4NWMbXlt2EZ0Hc7Fx7QzTFxgPID13eH0Qu+MAb2o= github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= +github.com/containerd/ttrpc v1.2.1 h1:VWv/Rzx023TBLv4WQ+9WPXlBG/s3rsRjY3i9AJ2BJdE= +github.com/containerd/ttrpc v1.2.1/go.mod h1:sIT6l32Ph/H9cvnJsfXM5drIVzTr5A2flTf1G5tYZak= +github.com/containerd/typeurl/v2 v2.1.0 h1:yNAhJvbNEANt7ck48IlEGOxP7YAp6LLpGn5jZACDNIE= +github.com/containerd/typeurl/v2 v2.1.0/go.mod h1:IDp2JFvbwZ31H8dQbEIY7sDl2L3o3HZj1hsSQlywkQ0= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/cyphar/filepath-securejoin v0.2.3/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -326,21 +373,26 @@ github.com/docker/cli v24.0.0+incompatible h1:0+1VshNwBQzQAx9lOl+OYCTCEAD8fKs/qe github.com/docker/cli v24.0.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/docker v24.0.5+incompatible h1:WmgcE4fxyI6EEXxBRxsHnZXrO1pQ3smi0k/jho4HLeY= -github.com/docker/docker v24.0.5+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/docker v24.0.6+incompatible h1:hceabKCtUgDqPu+qm0NgsaXf28Ljf4/pWFL7xjWWDgE= +github.com/docker/docker v24.0.6+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker-credential-helpers v0.7.0 h1:xtCHsjxogADNZcdv1pKUHXryefjlVRqWqIhk/uXJp0A= github.com/docker/docker-credential-helpers v0.7.0/go.mod h1:rETQfLdHNT3foU5kuNkFR1R1V12OJRRO5lzt2D1b5X0= github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ= github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8= +github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= +github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5 h1:iFaUwBSo5Svw6L7HYpRu/0lE3e0BaElwnNO1qkNQxBY= github.com/dsnet/compress v0.0.2-0.20210315054119-f66993602bf5/go.mod h1:qssHWj60/X5sZFNxpG4HBPDHVqxNm4DfnCKgrbZOT+s= github.com/dsnet/golib v0.0.0-20171103203638-1ea166775780/go.mod h1:Lj+Z9rebOhdfkVLjJ8T6VcRQv3SXugXy999NBtR9aFY= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/edsrzf/mmap-go v1.1.0 h1:6EUwBLQ/Mcr1EYLE4Tn1VdW1A4ckqCQWZBw8Hr0kjpQ= +github.com/edsrzf/mmap-go v1.1.0/go.mod h1:19H/e8pUPLicwkyNgOykDXkJ9F0MHE+Z52B8EIth78Q= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= +github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -358,8 +410,11 @@ github.com/envoyproxy/protoc-gen-validate v0.6.2/go.mod h1:2t7qjJNvHPx8IjnBOzl9E github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318 h1:iguwbR+9xsizl84VMHU47I4OOWYSex1HZRotEoqziWQ= github.com/etdub/goparsetime v0.0.0-20160315173935-ea17b0ac3318/go.mod h1:O/QFFckzvu1KpS1AOuQGgi6ErznEF8nZZVNDDMXlDP4= github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a h1:yDWHCSQ40h88yih2JAcL6Ls/kVkSE8GFACTGVnMPruw= +github.com/facebookgo/clock v0.0.0-20150410010913-600d898af40a/go.mod h1:7Ga40egUymuWXxAe151lTNnCv97MddSOVsjpPPkityA= github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01 h1:IeaD1VDVBPlx3viJT9Md8if8IxxJnO+x0JCGb054heg= +github.com/facebookgo/limitgroup v0.0.0-20150612190941-6abd8d71ec01/go.mod h1:ypD5nozFk9vcGw1ATYefw6jHe/jZP++Z15/+VTMcWhc= github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52 h1:a4DFiKFJiDRGFD1qIcqGLX/WlUMD9dyLSLDt+9QZgt8= +github.com/facebookgo/muster v0.0.0-20150708232844-fd3d7953fd52/go.mod h1:yIquW87NGRw1FU5p5lEkpnt/QxoH5uPAOUlOVkAUuMg= github.com/facebookincubator/flog v0.0.0-20190930132826-d2511d0ce33c/go.mod h1:QGzNH9ujQ2ZUr/CjDGZGWeDAVStrWNjHeEcjJL96Nuk= github.com/facebookincubator/nvdtools v0.1.5 h1:jbmDT1nd6+k+rlvKhnkgMokrCAzHoASWE5LtHbX2qFQ= github.com/facebookincubator/nvdtools v0.1.5/go.mod h1:Kh55SAWnjckS96TBSrXI99KrEKH4iB0OJby3N8GRJO4= @@ -368,7 +423,11 @@ github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/set v0.2.1 h1:nn2CaJyknWE/6txyUDGwysr3G5QC6xWB/PtVjPBbeaA= github.com/fatih/set v0.2.1/go.mod h1:+RKtMCH+favT2+3YecHGxcc0b4KyVWA1QWWJUs4E0CI= +github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g= +github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw= +github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= +github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= @@ -377,19 +436,30 @@ github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrt github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/github/go-spdx/v2 v2.1.2 h1:p+Tv0yMgcuO0/vnMe9Qh4tmUgYhI6AsLVlakZ/Sx+DM= -github.com/github/go-spdx/v2 v2.1.2/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w= -github.com/glebarez/go-sqlite v1.20.3 h1:89BkqGOXR9oRmG58ZrzgoY/Fhy5x0M+/WV48U5zVrZ4= +github.com/github/go-spdx/v2 v2.2.0 h1:yBBLMasHA70Ujd35OpL/OjJOWWVNXcJGbars0GinGRI= +github.com/github/go-spdx/v2 v2.2.0/go.mod h1:hMCrsFgT0QnCwn7G8gxy/MxMpy67WgZrwFeISTn0o6w= +github.com/gkampitakis/ciinfo v0.2.5 h1:K0mac90lGguc1conc46l0YEsB7/nioWCqSnJp/6z8Eo= +github.com/gkampitakis/ciinfo v0.2.5/go.mod h1:1NIwaOcFChN4fa/B0hEBdAb6npDlFL8Bwx4dfRLRqAo= +github.com/gkampitakis/go-diff v1.3.2 h1:Qyn0J9XJSDTgnsgHRdz9Zp24RaJeKMUHg2+PDZZdC4M= +github.com/gkampitakis/go-diff v1.3.2/go.mod h1:LLgOrpqleQe26cte8s36HTWcTmMEur6OPYerdAAS9tk= +github.com/gkampitakis/go-snaps v0.4.10 h1:rUcTH4k6+rzw6ylDALMifzw2c/f9cG3NZe/n+7Ygdr4= +github.com/gkampitakis/go-snaps v0.4.10/go.mod h1:N4TpqxI4CqKUfHzDFqrqZ5UP0I0ESz2g2NMslh7MiJw= +github.com/glebarez/go-sqlite v1.21.2 h1:3a6LFC4sKahUunAmynQKLZceZCOzUthkRkEAl9gAXWo= +github.com/glebarez/go-sqlite v1.21.2/go.mod h1:sfxdZyhQjTM2Wry3gVYWaW072Ri1WMdWJi0k6+3382k= +github.com/glebarez/sqlite v1.9.0 h1:Aj6bPA12ZEx5GbSF6XADmCkYXlljPNUY+Zf1EQxynXs= +github.com/glebarez/sqlite v1.9.0/go.mod h1:YBYCoyupOao60lzp1MVBLEjZfgkq0tdB1voAQ09K9zw= github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= +github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= github.com/go-asn1-ber/asn1-ber v1.5.4 h1:vXT6d/FNDiELJnLb6hGNa309LMsrCoYFvpwHDF0+Y1A= github.com/go-asn1-ber/asn1-ber v1.5.4/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= -github.com/go-git/go-git/v5 v5.7.0 h1:t9AudWVLmqzlo+4bqdf7GY+46SUuRsx59SboFxkq2aE= -github.com/go-git/go-git/v5 v5.7.0/go.mod h1:coJHKEOk5kUClpsNlXrUvPrDxY3w3gjHvhcZd8Fodw8= +github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= +github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= +github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -399,6 +469,11 @@ github.com/go-ldap/ldap/v3 v3.4.5 h1:ekEKmaDrpvR2yf5Nc/DClsGG9lAmdDixe44mLzlW5r8 github.com/go-ldap/ldap/v3 v3.4.5/go.mod h1:bMGIq3AGbytbaMwf8wdv5Phdxz0FWHTIYMSzyrYgnQs= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.2.3 h1:2DntVwHkVopvECVRSlL5PSo9eG+cAkDCuckLubN+rq0= +github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-restruct/restruct v1.2.0-alpha h1:2Lp474S/9660+SJjpVxoKuWX09JsXHSrdV7Nv3/gkvc= github.com/go-restruct/restruct v1.2.0-alpha/go.mod h1:KqrpKpn4M8OLznErihXTGLlsXFGeLxHUrLRRI/1YjGk= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= @@ -406,6 +481,7 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -478,6 +554,7 @@ github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIG github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= +github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -493,7 +570,9 @@ github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ= +github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/s2a-go v0.1.4 h1:1kZ/sQM3srePvKs3tXAvQzo66XfcReoqFpIpIccE7Oc= github.com/google/s2a-go v0.1.4/go.mod h1:Ej+mSEMGRnqRzjc7VtF+jdBwYG5fuJfiZ8ELkjEwM0A= @@ -505,8 +584,8 @@ github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.3 h1:yk9/cqRKtT9wXZSsRH9aurXEpJX+U6FLtpYTdC3R06k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= +github.com/googleapis/enterprise-certificate-proxy v0.2.4 h1:uGy6JWR/uMIILU8wbf+OkstIrNiMjGpEIyhx8f6W7s4= +github.com/googleapis/enterprise-certificate-proxy v0.2.4/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= @@ -570,13 +649,18 @@ github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOn github.com/hashicorp/serf v0.9.5/go.mod h1:UWDWwZeL5cuWDJdl0C6wrvrUwEqtQ4ZKBKKENpqIUyk= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/honeycombio/beeline-go v1.10.0 h1:cUDe555oqvw8oD76BQJ8alk7FP0JZ/M/zXpNvOEDLDc= +github.com/honeycombio/beeline-go v1.10.0/go.mod h1:Zz5WMeQCJzFt2Mvf8t6HC1X8RLskLVR/e8rvcmXB1G8= github.com/honeycombio/libhoney-go v1.16.0 h1:kPpqoz6vbOzgp7jC6SR7SkNj7rua7rgxvznI6M3KdHc= +github.com/honeycombio/libhoney-go v1.16.0/go.mod h1:izP4fbREuZ3vqC4HlCAmPrcPT9gxyxejRjGtCYpmBn0= github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= +github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= +github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20210905161508-09a460cdf81d/go.mod h1:aYm2/VgdVmcIU8iMfdMvDMsRAQjcfZSKFby6HOFvi/w= github.com/imdario/mergo v0.3.11/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.15 h1:M8XP7IuFNsqUx6VPK2P9OSmsYsI/YFaGil0uD21V3dM= github.com/imdario/mergo v0.3.15/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= @@ -585,17 +669,18 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg= -github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.4 h1:tHnRBy1i5F2Dh8BAFxqFzxKqqvezXrL2OW1TnX+Mlas= -github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= +github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548 h1:dYTbLf4m0a5u0KLmPfB6mgxbcV7588bOCx79hxa5Sr4= +github.com/jmhodges/clock v0.0.0-20160418191101-880ee4c33548/go.mod h1:hGT6jSUVzF6no3QaDSMLGLEHtHSBSefs+MgcDWnmhmo= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= @@ -603,11 +688,10 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/karrick/tparse v2.4.2+incompatible h1:+cW306qKAzrASC5XieHkgN7/vPaGKIuK62Q7nI7DIRc= github.com/karrick/tparse v2.4.2+incompatible/go.mod h1:ASPA+vrIcN1uEW6BZg8vfWbzm69ODPSYZPU6qJyfdK0= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= +github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953 h1:WdAeg/imY2JFPc/9CST4bZ80nNJbiBFCAdSZCSgrS5Y= +github.com/kastenhq/goversion v0.0.0-20230811215019-93b2f8823953/go.mod h1:6o+UrvuZWc4UTyBhQf0LGjW9Ld7qJxLz/OqvSOWWlEc= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= @@ -630,6 +714,7 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -638,6 +723,8 @@ github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf h1:ndns1qx/5dL github.com/letsencrypt/boulder v0.0.0-20221109233200-85aa52084eaf/go.mod h1:aGkAgvWY/IUcVFfuly53REpfv5edu25oij+qHRFaraA= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381 h1:bqDmpDG49ZRnB5PcgP0RXtQvnMSgIF14M7CBd2shtXs= github.com/logrusorgru/aurora v0.0.0-20200102142835-e9ef32dff381/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= +github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= +github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lyft/protoc-gen-star v0.5.3/go.mod h1:V0xaHgaf5oCCqmcxYcWiDfTiKsZsRc87/1qhoTACD8w= github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= @@ -653,7 +740,6 @@ github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxec github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= -github.com/mattn/go-isatty v0.0.6/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= @@ -662,20 +748,22 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75 h1:P8UmIzZMYDR+NGImiFvErt6VWfIRPuGM+vyjiEdkmIw= +github.com/mattn/go-localereader v0.0.2-0.20220822084749-2491eb6c1c75/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.12/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mholt/archiver/v3 v3.5.1 h1:rDjOBX9JSF5BvoJGvjqK479aL70qh9DIpZCl+k7Clwo= github.com/mholt/archiver/v3 v3.5.1/go.mod h1:e3dqJ7H78uzsRSEACH1joayhuSyhnonssnDhppzS1L4= -github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5 h1:tQRHcLQwnwrPq2j2Qra/NnyjyESBGwdeBeVdAE9kXYg= -github.com/microsoft/go-rustaudit v0.0.0-20220730194248-4b17361d90a5/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= +github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032 h1:TLygBUBxikNJJfLwgm+Qwdgq1FtfV8Uh7bcxRyTzK8s= +github.com/microsoft/go-rustaudit v0.0.0-20220808201409-204dfee52032/go.mod h1:vYT9HE7WCvL64iVeZylKmCsWKfE+JZ8105iuh2Trk8g= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= @@ -698,15 +786,36 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= -github.com/moby/term v0.0.0-20221205130635-1aeaba878587 h1:HfkjXDfhgVaN5rmueG8cL8KKeFNecRCXFhaJ2qZ5SKA= +github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= +github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= +github.com/moby/sys/mountinfo v0.5.0/go.mod h1:3bMD3Rg+zkqx8MRYPi7Pyb0Ie97QEBmdxbhnCLlSvSU= +github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= +github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= +github.com/moby/sys/sequential v0.5.0 h1:OPvI35Lzn9K04PBbCLW0g4LcFAJgHsvXsRyewg5lXtc= +github.com/moby/sys/sequential v0.5.0/go.mod h1:tH2cOOs5V9MlPiXcQzRC+eEyab644PWKGRYaaV5ZZlo= +github.com/moby/sys/signal v0.7.0 h1:25RW3d5TnQEoKvRbEKUGay6DCQ46IxAVTT9CUMgmsSI= +github.com/moby/sys/signal v0.7.0/go.mod h1:GQ6ObYZfqacOwTtlXvcmh9A26dVRul/hbOZn88Kg8Tg= +github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/mrunalp/fileutils v0.5.0/go.mod h1:M1WthSahJixYnrXQl/DFQuteStB1weuxD2QJNHXfbSQ= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70 h1:kMlmsLSbjkikxQJ1IPwaM+7LJ9ltFu/fi8CRzvSnQmA= +github.com/muesli/ansi v0.0.0-20211031195517-c9f0611b6c70/go.mod h1:fQuZ0gauxyBcmsdE3ZT4NasjaRdxmbCS0jRHsrWu3Ho= +github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= +github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo= +github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= +github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= +github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo= +github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/noqcks/syft v0.0.0-20230920223517-21656aa2830c h1:DULb+p2k3BL9VARyikj7uPvLv8Gl6xwmhbEDif9lYTg= +github.com/noqcks/syft v0.0.0-20230920223517-21656aa2830c/go.mod h1:ttY15BEeLIrGINA9acIOjmzsxB/5AYb1Iq11y3eCxSE= github.com/notaryproject/notation v1.0.0 h1:CJ17obUMRJOljApN8NCgYNmxfJQ878UrN0VbKACvDW0= github.com/notaryproject/notation v1.0.0/go.mod h1:BrPDyD390EOKkzLDS/3tEbg79wLIkNc0vCLfKHTd0Hw= github.com/notaryproject/notation-core-go v1.0.0 h1:FgOAihtFW4XU9JYyTzItg1xW3OaN4eCasw5Bp00Ydu4= @@ -718,14 +827,26 @@ github.com/nwaples/rardecode v1.1.2 h1:Cj0yZY6T1Zx1R7AhTbyGSALm44/Mmq+BAPc4B/p/d github.com/nwaples/rardecode v1.1.2/go.mod h1:5DzqNKiOdpKKBH87u8VlvAnPZMXcGRhxWkRpHbbfGS0= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= +github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.0-rc4 h1:oOxKUJWnFC4YGHCCMNql1x4YaDfYBTS5Y4x/Cgeo1E0= github.com/opencontainers/image-spec v1.1.0-rc4/go.mod h1:X4pATf0uXsnn3g5aiGIsVnJBR4mxhKzfwmvK/B2NTm8= +github.com/opencontainers/runc v1.1.5 h1:L44KXEpKmfWDcS02aeGm8QNTFXTo2D+8MYGDIJ/GDEs= +github.com/opencontainers/runc v1.1.5/go.mod h1:1J5XiS+vdZ3wCyZybsuxXZWGrgSr8fFJHLXuG2PsnNg= +github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/runtime-spec v1.1.0-rc.1 h1:wHa9jroFfKGQqFHj0I1fMRKLl0pfj+ynAqBxo3v6u9w= +github.com/opencontainers/runtime-spec v1.1.0-rc.1/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0= +github.com/opencontainers/selinux v1.10.0/go.mod h1:2i0OySw99QjzBBQByd1Gr9gSjvuho1lHsJxIJ3gGbJI= +github.com/opencontainers/selinux v1.11.0 h1:+5Zbo97w3Lbmb3PeqQtpmTkMwsW5nRI3YaLpt7tQ7oU= +github.com/opencontainers/selinux v1.11.0/go.mod h1:E5dMC3VPuVvVHDYmi78qvhJp8+M586T4DlDRYpFkyec= github.com/oras-project/oras-credentials-go v0.3.0 h1:Bg1d9iAmgo50RlaIy2XI5MQs7qL00DB3R9Q4JRP1VWs= github.com/oras-project/oras-credentials-go v0.3.0/go.mod h1:fFCebDQo0Do+gnM96uV9YUnRay0pwuRQupypvofsp4s= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/indent v1.2.1 h1:lFiviAbISHv3Rf0jcuh489bi06hj98JsVMtIDZQb9yM= +github.com/pborman/indent v1.2.1/go.mod h1:FitS+t35kIYtB5xWTZAPhnmrxcciEEOdbyrrpz5K6Vw= github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= @@ -736,10 +857,13 @@ github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA= +github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo= github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -751,36 +875,50 @@ github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= +github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.3.0 h1:UBgGFHqYdG/TPFD1B1ogZywDqEkwp3fBMvqdiQ7Xew4= +github.com/prometheus/client_model v0.3.0/go.mod h1:LDGWKZIo7rky3hgvBe+caln+Dr3dPggB5dvjtD7w9+w= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.37.0 h1:ccBbHCgIiT9uSoFY0vX8H3zsNR5eLt17/RQLUvn8pXE= +github.com/prometheus/common v0.37.0/go.mod h1:phzohg0JFMnBEFGxTDbfu3QyL5GI8gTQJFhYO5B3mfA= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= +github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= +github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= -github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/saferwall/pe v1.4.5 h1:ACIe9QnLTdiRIbuN3BbEUI8SqCQmNrPBb7O2lJTmsK4= +github.com/saferwall/pe v1.4.5/go.mod h1:SNzv3cdgk8SBI0UwHfyTcdjawfdnN+nbydnEL7GZ25s= github.com/sagikazarmark/crypt v0.3.0/go.mod h1:uD/D+6UF4SrIR1uGEv7bBNkNqLGqUr43MRiaGWX1Nig= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/sassoftware/go-rpmutils v0.2.0 h1:pKW0HDYMFWQ5b4JQPiI3WI12hGsVoW0V8+GMoZiI/JE= github.com/sassoftware/go-rpmutils v0.2.0/go.mod h1:TJJQYtLe/BeEmEjelI3b7xNZjzAukEkeWKmoakvaOoI= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e h1:7q6NSFZDeGfvvtIRwBrU/aegEYJYmvev0cHAwo17zZQ= github.com/scylladb/go-set v1.0.3-0.20200225121959-cc7b2070d91e/go.mod h1:DkpGd78rljTxKAnTDPFqXSGxvETQnJyuSOQwsHycqfs= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= +github.com/sebdah/goldie/v2 v2.5.3/go.mod h1:oZ9fp0+se1eapSRjfYbsV/0Hqhbuu3bJVvKI/NNtssI= +github.com/seccomp/libseccomp-golang v0.9.2-0.20220502022130-f33da4d89646/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= @@ -788,20 +926,22 @@ github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NF github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sigstore/sigstore v1.7.3 h1:HVVTfrMezJeLyl2xhJ8edzkrEGBa4KxjQZB4FlQ4JLU= github.com/sigstore/sigstore v1.7.3/go.mod h1:cl0c7Dtg3MM3c13L8pqqrfrmBa0eM3POcdtBepjylmw= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.1.1 h1:MTk78x9FPgDFVFkDLTrsnnfCJl7g1C/nnKvePgrIngE= -github.com/skeema/knownhosts v1.1.1/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= +github.com/skeema/knownhosts v1.2.0 h1:h9r9cf0+u7wSE+M183ZtMGgOJKiL96brpaz5ekfJCpM= +github.com/skeema/knownhosts v1.2.0/go.mod h1:g4fPeYpque7P0xefxtGzV81ihjC8sX2IqpAoNkjxbMo= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= -github.com/spdx/tools-golang v0.5.2 h1:dtMNjJreWPe37584ajk7m/rQtfJaLpRMk7pUGgvekOg= -github.com/spdx/tools-golang v0.5.2/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI= +github.com/spdx/tools-golang v0.5.3 h1:ialnHeEYUC4+hkm5vJm4qz2x+oEJbS0mAMFrNXdQraY= +github.com/spdx/tools-golang v0.5.3/go.mod h1:/ETOahiAo96Ob0/RAIBmFZw6XN0yTnyr/uFZm2NTMhI= github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= @@ -840,15 +980,27 @@ github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXl github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/sylabs/sif/v2 v2.8.1 h1:whr4Vz12RXfLnYyVGHoD/rD/hbF2g9OW7BJHa+WIqW8= -github.com/sylabs/sif/v2 v2.8.1/go.mod h1:LQOdYXC9a8i7BleTKRw9lohi0rTbXkJOeS9u0ebvgyM= +github.com/sylabs/sif/v2 v2.11.5 h1:7ssPH3epSonsTrzbS1YxeJ9KuqAN7ISlSM61a7j/mQM= +github.com/sylabs/sif/v2 v2.11.5/go.mod h1:GBoZs9LU3e4yJH1dcZ3Akf/jsqYgy5SeguJQC+zd75Y= github.com/sylabs/squashfs v0.6.1 h1:4hgvHnD9JGlYWwT0bPYNt9zaz23mAV3Js+VEgQoRGYQ= github.com/sylabs/squashfs v0.6.1/go.mod h1:ZwpbPCj0ocIvMy2br6KZmix6Gzh6fsGQcCnydMF+Kx8= +github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635/go.mod h1:hkRG7XYTFWNJGYcbNJQlaLq0fg1yr4J4t/NcTQtrfww= github.com/terminalstatic/go-xsd-validate v0.1.5 h1:RqpJnf6HGE2CB/lZB1A8BYguk8uRtcvYAPLCF15qguo= +github.com/terminalstatic/go-xsd-validate v0.1.5/go.mod h1:18lsvYFofBflqCrvo1umpABZ99+GneNTw2kEEc8UPJw= github.com/therootcompany/xz v1.0.1 h1:CmOtsn1CbtmyYiusbfmhmkpAAETj0wBIH6kCYaX+xzw= github.com/therootcompany/xz v1.0.1/go.mod h1:3K3UH1yCKgBneZYhuQUvJ9HPD19UEXEI0BWbMn8qNMY= github.com/theupdateframework/go-tuf v0.5.2 h1:habfDzTmpbzBLIFGWa2ZpVhYvFBoK0C1onC3a4zuPRA= github.com/theupdateframework/go-tuf v0.5.2/go.mod h1:SyMV5kg5n4uEclsyxXJZI2UxPFJNDc4Y+r7wv+MlvTA= +github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/gjson v1.16.0 h1:SyXa+dsSPpUlcwEDuKuEBJEz5vzTvOea+9rjyYodQFg= +github.com/tidwall/gjson v1.16.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4= +github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= +github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= +github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 h1:e/5i7d4oYZ+C1wj2THlRK+oAhjeS/TRQwMfkIuet3w0= github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399/go.mod h1:LdwHTNJT99C5fTAzDz0ud328OgXz+gierycbcIx2fRs= github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= @@ -856,30 +1008,39 @@ github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oW github.com/ulikunitz/xz v0.5.9/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.10 h1:t92gobL9l3HE202wg3rlk19F6X+JOxl9BBrCCMYEYd8= github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= github.com/vbatts/go-mtree v0.5.3 h1:S/jYlfG8rZ+a0bhZd+RANXejy7M4Js8fq9U+XoWTd5w= github.com/vbatts/go-mtree v0.5.3/go.mod h1:eXsdoPMdL2jcJx6HweWi9lYQxBsTp4lNhqqAjgkZUg8= github.com/vbatts/tar-split v0.11.3 h1:hLFqsOLQ1SsppQNTMpkpPXClLDfC2A3Zgy9OUU+RVck= github.com/vbatts/tar-split v0.11.3/go.mod h1:9QlHN18E+fEH7RdG+QAJJcuya3rqT7eXSTY7wGrAokY= github.com/veraison/go-cose v1.1.0 h1:AalPS4VGiKavpAzIlBjrn7bhqXiXi4jbMYY/2+UC+4o= github.com/veraison/go-cose v1.1.0/go.mod h1:7ziE85vSq4ScFTg6wyoMXjucIGOf4JkFEZi/an96Ct4= -github.com/vifraa/gopom v0.2.1 h1:MYVMAMyiGzXPPy10EwojzKIL670kl5Zbae+o3fFvQEM= -github.com/vifraa/gopom v0.2.1/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= +github.com/vifraa/gopom v1.0.0 h1:L9XlKbyvid8PAIK8nr0lihMApJQg/12OBvMA28BcWh0= +github.com/vifraa/gopom v1.0.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o= +github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE= +github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU= github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= -github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8= -github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw= -github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5 h1:lwgTsTy18nYqASnH58qyfRW/ldj7Gt2zzBvgYPzdA4s= -github.com/wagoodman/go-progress v0.0.0-20230301185719-21920a456ad5/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= -github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb h1:Yz6VVOcLuWLAHYlJzTw7JKnWxdV/WXpug2X0quEzRnY= -github.com/wagoodman/jotframe v0.0.0-20211129225309-56b0d0a4aebb/go.mod h1:nDi3BAC5nEbVbg+WSJDHLbjHv0ZToq8nMPA97XMxF3E= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651 h1:jIVmlAFIqV3d+DOxazTR9v+zgj8+VYuQBzPgBZvWBHA= +github.com/wagoodman/go-partybus v0.0.0-20230516145632-8ccac152c651/go.mod h1:b26F2tHLqaoRQf8DywqzVaV1MQ9yvjb0OMcNl7Nxu20= +github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b h1:uWNQ0khA6RdFzODOMwKo9XXu7fuewnnkHykUtuKru8s= +github.com/wagoodman/go-presenter v0.0.0-20211015174752-f9c01afc824b/go.mod h1:ewlIKbKV8l+jCj8rkdXIs361ocR5x3qGyoCSca47Gx8= +github.com/wagoodman/go-progress v0.0.0-20230911172108-cf810b7e365c h1:mM8T8YhiD19d2wYv3vqZn8xpe1ZFJrUJCGlK4IV05xM= +github.com/wagoodman/go-progress v0.0.0-20230911172108-cf810b7e365c/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= +github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 h1:nIPpBwaJSVYIxUFsDv3M8ofmx9yWTog9BfvIu0q41lo= github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8/go.mod h1:HUYIGzjTL3rfEspMxjDjgmT5uz5wzYJKVo23qUhYTos= github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHgvgickp1Yw510KJOqX7H24mg8= @@ -893,6 +1054,8 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t go.etcd.io/etcd/api/v3 v3.5.1/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.1/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.1/go.mod h1:pMEacxZW7o8pg4CrFE7pquyCJJzZvkvdD2RibOCCCGs= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= +go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -902,6 +1065,10 @@ go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= +go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= +go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= @@ -911,7 +1078,6 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -929,8 +1095,8 @@ golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0 golang.org/x/crypto v0.3.0/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.13.0 h1:mvySKfSWJ+UKUii46M40LOvyWfN0s2U+46/jDd0e6Ck= +golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -941,8 +1107,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1 h1:k/i9J1pBpvlfR+9QsetwPyERsqu1GIbi967PQMq3Ivc= -golang.org/x/exp v0.0.0-20230522175609-2e198f4a06a1/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63 h1:m64FZMko/V45gv0bNmrNYoDEq8U5YUhetc9cBWKS1TQ= +golang.org/x/exp v0.0.0-20230817173708-d852ddb80c63/go.mod h1:0v4NqG35kSWCMzLaMeX+IQrlSnVE/bqGSyC2cz/9Le8= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1031,8 +1197,8 @@ golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.14.0 h1:BONx9s002vGdD9umnlX1Po8vOZmrgH34qlHcD1MfK14= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= +golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1091,6 +1257,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1098,6 +1265,7 @@ golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191115151921-52ab43148777/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1141,10 +1309,12 @@ golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211205182925-97ca703d548d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1153,7 +1323,6 @@ golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220405052023-b1e9470b6e64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -1176,7 +1345,6 @@ golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210615171337-6886f2dfbf5b/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -1197,8 +1365,8 @@ golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k= +golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -1249,7 +1417,6 @@ golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= @@ -1263,8 +1430,8 @@ golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= -golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -1324,8 +1491,8 @@ google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.126.0 h1:q4GJq+cAdMAC7XP7njvQ4tvohGLiSlytuL4BQxbIZ+o= -google.golang.org/api v0.126.0/go.mod h1:mBwVAtz+87bEN6CbA1GtZPDOqY2R5ONPqJeIlvyo4Aw= +google.golang.org/api v0.128.0 h1:RjPESny5CnQRn9V6siglged+DZCgfu9l6mO9dkX9VOg= +google.golang.org/api v0.128.0/go.mod h1:Y611qgqaE92On/7g65MQgxYul3c0rEB894kniWLY750= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1483,8 +1650,8 @@ google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACu google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.55.0 h1:3Oj82/tFSCeUrRTg/5E/7d/W5A1tj6Ky1ABAuZuv5ag= -google.golang.org/grpc v1.55.0/go.mod h1:iYEXKGkEBhg1PjZQvoYEVPTDkHo1/bjTnfwTeGONTY8= +google.golang.org/grpc v1.56.0 h1:+y7Bs8rtMd07LeXmL3NxcTLn7mUkbKZqEpPhMNkwJEE= +google.golang.org/grpc v1.56.0/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -1505,6 +1672,7 @@ google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/alexcesaro/statsd.v2 v2.0.0 h1:FXkZSCZIH17vLCO5sO2UucTHsH9pc+17F6pl3JVCwMc= +gopkg.in/alexcesaro/statsd.v2 v2.0.0/go.mod h1:i0ubccKGzBVNBpdGV5MocxyA/XlLUJzA7SLonnE4drU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -1532,8 +1700,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/gorm v1.23.5 h1:TnlF26wScKSvknUC/Rn8t0NLLM22fypYBlvj1+aH6dM= -gorm.io/gorm v1.23.5/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk= +gorm.io/gorm v1.25.2 h1:gs1o6Vsa+oVKG/a9ElL3XgyGfghFfkKA2SInQaCyMho= +gorm.io/gorm v1.25.2/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= gotest.tools/v3 v3.4.0 h1:ZazjZUfuVeZGLAmlKKuyv3IKP5orXcwtOwDQH6YVr6o= gotest.tools/v3 v3.4.0/go.mod h1:CtbdzLSsqVhDgMtKsx03ird5YTGB3ar27v0u/yKBW5g= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -1543,52 +1711,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -lukechampine.com/uint128 v1.1.1/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI= -lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw= -modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw= -modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY= -modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= -modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.7/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.8/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.22.5 h1:91BNch/e5B0uPbJFgqbxXuOnxBQjlS//icfQEGmvyjE= -modernc.org/libc v1.22.5/go.mod h1:jj+Z7dTNX8fBScMVNRAYZ/jF91K8fdT2hYMThc3YjBY= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= +modernc.org/libc v1.24.1 h1:uvJSeCKL/AgzBo2yYIPPTy82v21KgGnizcGYfBHaNuM= +modernc.org/libc v1.24.1/go.mod h1:FmfO1RLrU3MHJfyi9eYYmZBfi/R+tqZ6+hQ3yQQUkak= modernc.org/mathutil v1.5.0 h1:rV0Ko/6SfM+8G+yKiyI830l3Wuz1zRutdslNoQ0kfiQ= modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.5.0 h1:N+/8c5rE6EqugZwHii4IFsaJ7MUhoWX07J5tC/iI5Ds= -modernc.org/memory v1.5.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= -modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= -modernc.org/sqlite v1.17.3/go.mod h1:10hPVYar9C0kfXuTWGz8s0XtB8uAGymUy51ZzStYe3k= -modernc.org/sqlite v1.23.1 h1:nrSBg4aRQQwq59JpvGEQ15tNxoO5pX/kUjcRNwSAGQM= -modernc.org/sqlite v1.23.1/go.mod h1:OrDj17Mggn6MhE+iPbBNf7RGKODDE9NFT0f3EwDzJqk= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg= -modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= -modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY= +modernc.org/memory v1.6.0 h1:i6mzavxrE9a30whzMfwf7XWVODx2r5OYXvU46cirX7o= +modernc.org/memory v1.6.0/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= +modernc.org/sqlite v1.25.0 h1:AFweiwPNd/b3BoKnBOfFm+Y260guGMF+0UFk0savqeA= +modernc.org/sqlite v1.25.0/go.mod h1:FL3pVXie73rg3Rii6V/u5BoHlSoyeZeIgKZEgHARyCU= oras.land/oras-go/v2 v2.2.1 h1:3VJTYqy5KfelEF9c2jo1MLSpr+TM3mX8K42wzZcd6qE= oras.land/oras-go/v2 v2.2.1/go.mod h1:GeAwLuC4G/JpNwkd+bSZ6SkDMGaaYglt6YK2WvZP7uQ= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= diff --git a/internal/bus/bus.go b/internal/bus/bus.go index fdbca506..2ba893dc 100644 --- a/internal/bus/bus.go +++ b/internal/bus/bus.go @@ -3,17 +3,13 @@ package bus import "github.com/wagoodman/go-partybus" var publisher partybus.Publisher -var active bool -func SetPublisher(p partybus.Publisher) { +func Set(p partybus.Publisher) { publisher = p - if p != nil { - active = true - } } func Publish(event partybus.Event) { - if active { + if publisher != nil { publisher.Publish(event) } } diff --git a/internal/bus/helpers.go b/internal/bus/helpers.go new file mode 100644 index 00000000..945bdf58 --- /dev/null +++ b/internal/bus/helpers.go @@ -0,0 +1,27 @@ +package bus + +import ( + partybus "github.com/wagoodman/go-partybus" + + "github.com/xeol-io/xeol/xeol/event" +) + +func Exit() { + Publish(partybus.Event{ + Type: event.CLIExit, + }) +} + +func Report(report string) { + Publish(partybus.Event{ + Type: event.CLIReport, + Value: report, + }) +} + +func Notify(message string) { + Publish(partybus.Event{ + Type: event.CLINotification, + Value: message, + }) +} diff --git a/internal/config/application.go b/internal/config/application.go deleted file mode 100644 index a762cb53..00000000 --- a/internal/config/application.go +++ /dev/null @@ -1,281 +0,0 @@ -package config - -import ( - "errors" - "fmt" - "path" - "reflect" - "strings" - "time" - - "github.com/adrg/xdg" - "github.com/anchore/go-logger" - git "github.com/go-git/go-git/v5" - "github.com/karrick/tparse" - "github.com/mitchellh/go-homedir" - "github.com/spf13/viper" - "gopkg.in/yaml.v2" - - "github.com/xeol-io/xeol/internal" -) - -var ErrApplicationConfigNotFound = fmt.Errorf("application config not found") - -type defaultValueLoader interface { - loadDefaultValues(*viper.Viper) -} - -const DefaultProLookahead = "now+3y" - -type parser interface { - parseConfigValues() error -} - -type Application struct { - Verbosity uint `yaml:"verbosity,omitempty" json:"verbosity" mapstructure:"verbosity"` - ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading) - File string `yaml:"file" json:"file" mapstructure:"file"` // --file, the file to write report output to - Output string `yaml:"output" json:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting - Quiet bool `yaml:"quiet" json:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)// -o, the Presenter hint string to use for report formatting - CheckForAppUpdate bool `yaml:"check-for-app-update" json:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not - Log logging `yaml:"log" json:"log" mapstructure:"log"` - DB database `yaml:"db" json:"db" mapstructure:"db"` - CliOptions CliOnlyOptions `yaml:"-" json:"-"` - Match matchConfig `yaml:"match" json:"match" mapstructure:"match"` - Lookahead string `yaml:"lookahead" json:"lookahead" mapstructure:"lookahead"` - EolMatchDate time.Time `yaml:"-" json:"-"` - FailOnEolFound bool `yaml:"fail-on-eol-found" json:"fail-on-eol-found" mapstructure:"fail-on-eol-found"` // whether to exit with a non-zero exit code if any EOLs are found - APIKey string `yaml:"api-key" json:"api-key" mapstructure:"api-key"` - ProjectName string `yaml:"project-name" json:"project-name" mapstructure:"project-name"` - CommitHash string `yaml:"commit-hash" json:"commit-hash" mapstructure:"commit-hash"` - ImagePath string `yaml:"image-path" json:"image-path" mapstructure:"image-path"` - Registry registry `yaml:"registry" json:"registry" mapstructure:"registry"` - Platform string `yaml:"platform" json:"platform" mapstructure:"platform"` // --platform, override the target platform for a container image - Name string `yaml:"name" json:"name" mapstructure:"name"` - DefaultImagePullSource string `yaml:"default-image-pull-source" json:"default-image-pull-source" mapstructure:"default-image-pull-source"` - Search search `yaml:"search" json:"search" mapstructure:"search"` -} - -func NewApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) *Application { - config := &Application{ - CliOptions: cliOpts, - } - config.loadDefaultValues(v) - - return config -} - -func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions) (*Application, error) { - // the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead - config := NewApplicationConfig(v, cliOpts) - - if err := readConfig(v, cliOpts.ConfigPath); err != nil && !errors.Is(err, ErrApplicationConfigNotFound) { - return nil, err - } - - if err := v.Unmarshal(config); err != nil { - return nil, fmt.Errorf("unable to parse config: %w", err) - } - config.ConfigPath = v.ConfigFileUsed() - - if err := config.parseConfigValues(); err != nil { - return nil, fmt.Errorf("invalid application config: %w", err) - } - - return config, nil -} - -// init loads the default configuration values into the viper instance (before the config values are read and parsed). -func (cfg Application) loadDefaultValues(v *viper.Viper) { - // set the default values for primitive fields in this struct - v.SetDefault("check-for-app-update", true) - v.SetDefault("fail-on-eol-found", false) - project, commit := getDefaultProjectNameAndCommit() - v.SetDefault("project-name", project) - v.SetDefault("commit-hash", commit) - v.SetDefault("image-path", "Dockerfile") - v.SetDefault("default-image-pull-source", "") - - // for each field in the configuration struct, see if the field implements the defaultValueLoader interface and invoke it if it does - value := reflect.ValueOf(cfg) - for i := 0; i < value.NumField(); i++ { - // note: the defaultValueLoader method receiver is NOT a pointer receiver. - if loadable, ok := value.Field(i).Interface().(defaultValueLoader); ok { - // the field implements defaultValueLoader, call it - loadable.loadDefaultValues(v) - } - } -} - -func getDefaultProjectNameAndCommit() (string, string) { - repo, err := git.PlainOpen(".") - if err != nil { - return "", "" - } - - h, err := repo.Head() - if err != nil { - return "", "" - } - - p := NewProject(repo) - return p.Name, h.Hash().String() -} - -// readConfig attempts to read the given config path from disk or discover an alternate store location -func readConfig(v *viper.Viper, configPath string) error { - var err error - v.AutomaticEnv() - v.SetEnvPrefix(internal.ApplicationName) - // allow for nested options to be specified via environment variables - // e.g. pod.context = APPNAME_POD_CONTEXT - v.SetEnvKeyReplacer(strings.NewReplacer(".", "_", "-", "_")) - - // use explicitly the given user config - if configPath != "" { - v.SetConfigFile(configPath) - if err := v.ReadInConfig(); err != nil { - return fmt.Errorf("unable to read application config=%q : %w", configPath, err) - } - // don't fall through to other options if the config path was explicitly provided - return nil - } - - // start searching for valid configs in order... - - // 1. look for ..yaml (in the current directory) - v.AddConfigPath(".") - v.SetConfigName("." + internal.ApplicationName) - if err = v.ReadInConfig(); err == nil { - return nil - } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { - return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) - } - - // 2. look for ./config.yaml (in the current directory) - v.AddConfigPath("." + internal.ApplicationName) - v.SetConfigName("config") - if err = v.ReadInConfig(); err == nil { - return nil - } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { - return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) - } - - // 3. look for ~/..yaml - home, err := homedir.Dir() - if err == nil { - v.AddConfigPath(home) - v.SetConfigName("." + internal.ApplicationName) - if err = v.ReadInConfig(); err == nil { - return nil - } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { - return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) - } - } - - // 4. look for /config.yaml in xdg locations (starting with xdg home config dir, then moving upwards) - v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName)) - for _, dir := range xdg.ConfigDirs { - v.AddConfigPath(path.Join(dir, internal.ApplicationName)) - } - v.SetConfigName("config") - if err = v.ReadInConfig(); err == nil { - return nil - } else if !errors.As(err, &viper.ConfigFileNotFoundError{}) { - return fmt.Errorf("unable to parse config=%q: %w", v.ConfigFileUsed(), err) - } - - return ErrApplicationConfigNotFound -} - -func (cfg *Application) parseConfigValues() error { - // parse application config options - for _, optionFn := range []func() error{ - cfg.parseLogLevelOption, - cfg.parseLookaheadOption, - } { - if err := optionFn(); err != nil { - return err - } - } - - // parse nested config options - // for each field in the configuration struct, see if the field implements the parser interface - // note: the app config is a pointer, so we need to grab the elements explicitly (to traverse the address) - value := reflect.ValueOf(cfg).Elem() - for i := 0; i < value.NumField(); i++ { - // note: since the interface method of parser is a pointer receiver we need to get the value of the field as a pointer. - if parsable, ok := value.Field(i).Addr().Interface().(parser); ok { - // the field implements parser, call it - if err := parsable.parseConfigValues(); err != nil { - return err - } - } - } - return nil -} - -func (cfg *Application) parseLookaheadOption() error { - var err error - // if the user has specified an API key and is posting results to xeol.io, then we - // set a default lookahead value to 3 years from now - if cfg.APIKey != "" { - cfg.EolMatchDate, err = tparse.ParseNow(time.RFC3339, DefaultProLookahead) - if err != nil { - return fmt.Errorf("bad --lookahead value: '%s'", cfg.Lookahead) - } - return nil - } - - if cfg.Lookahead == "none" { - cfg.EolMatchDate = time.Now() - return nil - } - - cfg.EolMatchDate, err = tparse.ParseNow(time.RFC3339, fmt.Sprintf("now+%s", cfg.Lookahead)) - if err != nil { - return fmt.Errorf("bad --lookahead value: '%s'", cfg.Lookahead) - } - - return nil -} - -func (cfg *Application) parseLogLevelOption() error { - switch { - case cfg.Quiet: - // TODO: this is bad: quiet option trumps all other logging options (such as to a file on disk) - // we should be able to quiet the console logging and leave file logging alone... - // ... this will be an enhancement for later - cfg.Log.Level = logger.DisabledLevel - - case cfg.CliOptions.Verbosity > 0: - verb := cfg.CliOptions.Verbosity - cfg.Log.Level = logger.LevelFromVerbosity(verb, logger.WarnLevel, logger.InfoLevel, logger.DebugLevel, logger.TraceLevel) - - case cfg.Log.Level != "": - var err error - cfg.Log.Level, err = logger.LevelFromString(string(cfg.Log.Level)) - if err != nil { - return err - } - - if logger.IsVerbose(cfg.Log.Level) { - cfg.Verbosity = 1 - } - default: - cfg.Log.Level = logger.WarnLevel - } - - return nil -} - -func (cfg Application) String() string { - // yaml is pretty human friendly (at least when compared to json) - appCfgStr, err := yaml.Marshal(&cfg) - - if err != nil { - return err.Error() - } - - return string(appCfgStr) -} diff --git a/internal/config/cli_only_options.go b/internal/config/cli_only_options.go deleted file mode 100644 index 5bb0e496..00000000 --- a/internal/config/cli_only_options.go +++ /dev/null @@ -1,6 +0,0 @@ -package config - -type CliOnlyOptions struct { - ConfigPath string - Verbosity int -} diff --git a/internal/config/logging.go b/internal/config/logging.go deleted file mode 100644 index 4f149b4e..00000000 --- a/internal/config/logging.go +++ /dev/null @@ -1,19 +0,0 @@ -package config - -import ( - "github.com/anchore/go-logger" - "github.com/spf13/viper" -) - -// logging contains all logging-related configuration options available to the user via the application config. -type logging struct { - Structured bool `yaml:"structured" json:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings - Level logger.Level `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint - FileLocation string `yaml:"file" json:"file" mapstructure:"file"` // the file path to write logs to -} - -func (cfg logging) loadDefaultValues(v *viper.Viper) { - v.SetDefault("log.structured", false) - v.SetDefault("log.file", "") - v.SetDefault("log.level", string(logger.WarnLevel)) -} diff --git a/internal/config/match.go b/internal/config/match.go deleted file mode 100644 index 5196a95c..00000000 --- a/internal/config/match.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -import "github.com/spf13/viper" - -// matchConfig contains all matching-related configuration options available to the user via the application config. -type matchConfig struct { - Packages pkgMatcherConfig `mapstructure:"packages"` - Distro distroMatcherConfig `mapstructure:"distro"` -} - -type pkgMatcherConfig struct { - UsePurls bool `yaml:"using-purls" json:"using-purls" mapstructure:"using-purls"` // if Purls should be used during matching -} - -type distroMatcherConfig struct { - UseCpes bool `yaml:"using-cpes" json:"using-cpes" mapstructure:"using-cpes"` // if CPEs should be used during matching -} - -func (cfg matchConfig) loadDefaultValues(v *viper.Viper) { - v.SetDefault("match.packages.using-purls", true) - v.SetDefault("match.distro.using-cpes", true) -} diff --git a/internal/config/registry.go b/internal/config/registry.go deleted file mode 100644 index 233ea37c..00000000 --- a/internal/config/registry.go +++ /dev/null @@ -1,74 +0,0 @@ -package config - -import ( - "os" - - "github.com/anchore/stereoscope/pkg/image" - "github.com/spf13/viper" -) - -type RegistryCredentials struct { - Authority string `yaml:"authority" json:"authority" mapstructure:"authority"` - // IMPORTANT: do not show the username in any YAML/JSON output (sensitive information) - Username string `yaml:"-" json:"-" mapstructure:"username"` - // IMPORTANT: do not show the password in any YAML/JSON output (sensitive information) - Password string `yaml:"-" json:"-" mapstructure:"password"` - // IMPORTANT: do not show the token in any YAML/JSON output (sensitive information) - Token string `yaml:"-" json:"-" mapstructure:"token"` -} - -type registry struct { - InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"` - InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"` - Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"` -} - -func (cfg registry) loadDefaultValues(v *viper.Viper) { - v.SetDefault("registry.insecure-skip-tls-verify", false) - v.SetDefault("registry.insecure-use-http", false) - v.SetDefault("registry.auth", []RegistryCredentials{}) -} - -//nolint:unparam -func (cfg *registry) parseConfigValues() error { - // there may be additional credentials provided by env var that should be appended to the set of credentials - authority, username, password, token := - os.Getenv("XEOL_REGISTRY_AUTH_AUTHORITY"), - os.Getenv("XEOL_REGISTRY_AUTH_USERNAME"), - os.Getenv("XEOL_REGISTRY_AUTH_PASSWORD"), - os.Getenv("XEOL_REGISTRY_AUTH_TOKEN") - - if hasNonEmptyCredentials(username, password, token) { - // note: we prepend the credentials such that the environment variables take precedence over on-disk configuration. - cfg.Auth = append([]RegistryCredentials{ - { - Authority: authority, - Username: username, - Password: password, - Token: token, - }, - }, cfg.Auth...) - } - return nil -} - -func hasNonEmptyCredentials(username, password, token string) bool { - return password != "" && username != "" || token != "" -} - -func (cfg *registry) ToOptions() *image.RegistryOptions { - var auth = make([]image.RegistryCredentials, len(cfg.Auth)) - for i, a := range cfg.Auth { - auth[i] = image.RegistryCredentials{ - Authority: a.Authority, - Username: a.Username, - Password: a.Password, - Token: a.Token, - } - } - return &image.RegistryOptions{ - InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify, - InsecureUseHTTP: cfg.InsecureUseHTTP, - Credentials: auth, - } -} diff --git a/internal/config/search.go b/internal/config/search.go deleted file mode 100644 index d2d11fbb..00000000 --- a/internal/config/search.go +++ /dev/null @@ -1,42 +0,0 @@ -package config - -import ( - "fmt" - - "github.com/anchore/syft/syft/pkg/cataloger" - "github.com/anchore/syft/syft/source" - "github.com/spf13/viper" -) - -type search struct { - ScopeOpt source.Scope `yaml:"-" json:"-"` - Scope string `yaml:"scope" json:"scope" mapstructure:"scope"` - IncludeUnindexedArchives bool `yaml:"unindexed-archives" json:"unindexed-archives" mapstructure:"unindexed-archives"` - IncludeIndexedArchives bool `yaml:"indexed-archives" json:"indexed-archives" mapstructure:"indexed-archives"` -} - -func (cfg *search) parseConfigValues() error { - scopeOption := source.ParseScope(cfg.Scope) - if scopeOption == source.UnknownScope { - return fmt.Errorf("bad scope value %q", cfg.Scope) - } - cfg.ScopeOpt = scopeOption - - return nil -} - -func (cfg search) loadDefaultValues(v *viper.Viper) { - c := cataloger.DefaultSearchConfig() - v.SetDefault("search.unindexed-archives", c.IncludeUnindexedArchives) - v.SetDefault("search.indexed-archives", c.IncludeIndexedArchives) -} - -func (cfg search) ToConfig() cataloger.Config { - return cataloger.Config{ - Search: cataloger.SearchConfig{ - IncludeIndexedArchives: cfg.IncludeIndexedArchives, - IncludeUnindexedArchives: cfg.IncludeUnindexedArchives, - Scope: cfg.ScopeOpt, - }, - } -} diff --git a/internal/format/format.go b/internal/format/format.go new file mode 100644 index 00000000..a5d62cb3 --- /dev/null +++ b/internal/format/format.go @@ -0,0 +1,38 @@ +package format + +import ( + "strings" +) + +const ( + UnknownFormat Format = "unknown" + JSONFormat Format = "json" + TableFormat Format = "table" +) + +// Format is a dedicated type to represent a specific kind of presenter output format. +type Format string + +func (f Format) String() string { + return string(f) +} + +// Parse returns the presenter.format specified by the given user input. +func Parse(userInput string) Format { + switch strings.ToLower(userInput) { + case "": + return TableFormat + case strings.ToLower(JSONFormat.String()): + return JSONFormat + case strings.ToLower(TableFormat.String()): + return TableFormat + default: + return UnknownFormat + } +} + +// AvailableFormats is a list of presenter format options available to users. +var AvailableFormats = []Format{ + JSONFormat, + TableFormat, +} diff --git a/internal/format/presenter.go b/internal/format/presenter.go new file mode 100644 index 00000000..ba3bfee3 --- /dev/null +++ b/internal/format/presenter.go @@ -0,0 +1,21 @@ +package format + +import ( + "github.com/wagoodman/go-presenter" + + "github.com/xeol-io/xeol/xeol/presenter/json" + "github.com/xeol-io/xeol/xeol/presenter/models" + "github.com/xeol-io/xeol/xeol/presenter/table" +) + +// GetPresenter retrieves a Presenter that matches a CLI option +func GetPresenter(format Format, pb models.PresenterConfig) presenter.Presenter { + switch format { + case JSONFormat: + return json.NewPresenter(pb) + case TableFormat: + return table.NewPresenter(pb) + default: + return nil + } +} diff --git a/internal/format/writer.go b/internal/format/writer.go new file mode 100644 index 00000000..a1e157bf --- /dev/null +++ b/internal/format/writer.go @@ -0,0 +1,213 @@ +package format + +import ( + "bytes" + "fmt" + "io" + "os" + "path" + "strings" + + "github.com/hashicorp/go-multierror" + "github.com/mitchellh/go-homedir" + + "github.com/xeol-io/xeol/internal/bus" + "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/xeol/presenter/models" +) + +type ScanResultWriter interface { + Write(result models.PresenterConfig) error +} + +var _ ScanResultWriter = (*scanResultMultiWriter)(nil) + +var _ interface { + io.Closer + ScanResultWriter +} = (*scanResultStreamWriter)(nil) + +// MakeScanResultWriter creates a ScanResultWriter for output or returns an error. this will either return a valid writer +// or an error but neither both and if there is no error, ScanResultWriter.Close() should be called +func MakeScanResultWriter(outputs []string, defaultFile string) (ScanResultWriter, error) { + outputOptions, err := parseOutputFlags(outputs, defaultFile) + if err != nil { + return nil, err + } + + writer, err := newMultiWriter(outputOptions...) + if err != nil { + return nil, err + } + + return writer, nil +} + +// MakeScanResultWriterForFormat creates a ScanResultWriter for the given format or returns an error. +func MakeScanResultWriterForFormat(f string, path string) (ScanResultWriter, error) { + format := Parse(f) + + if format == UnknownFormat { + return nil, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, f, AvailableFormats) + } + + writer, err := newMultiWriter(newWriterDescription(format, path)) + if err != nil { + return nil, err + } + + return writer, nil +} + +// parseOutputFlags utility to parse command-line option strings and retain the existing behavior of default format and file +func parseOutputFlags(outputs []string, defaultFile string) (out []scanResultWriterDescription, errs error) { + // always should have one option -- we generally get the default of "table", but just make sure + if len(outputs) == 0 { + outputs = append(outputs, TableFormat.String()) + } + + for _, name := range outputs { + name = strings.TrimSpace(name) + + // split to at most two parts for = + parts := strings.SplitN(name, "=", 2) + + // the format name is the first part + name = parts[0] + + // default to the --file or empty string if not specified + file := defaultFile + + // If a file is specified as part of the output formatName, use that + if len(parts) > 1 { + file = parts[1] + } + + format := Parse(name) + + if format == UnknownFormat { + errs = multierror.Append(errs, fmt.Errorf(`unsupported output format "%s", supported formats are: %+v`, name, AvailableFormats)) + continue + } + + out = append(out, newWriterDescription(format, file)) + } + return out, errs +} + +// scanResultWriterDescription Format and path strings used to create ScanResultWriter +type scanResultWriterDescription struct { + Format Format + Path string +} + +func newWriterDescription(f Format, p string) scanResultWriterDescription { + expandedPath, err := homedir.Expand(p) + if err != nil { + log.Warnf("could not expand given writer output path=%q: %w", p, err) + // ignore errors + expandedPath = p + } + return scanResultWriterDescription{ + Format: f, + Path: expandedPath, + } +} + +// scanResultMultiWriter holds a list of child ScanResultWriters to apply all Write and Close operations to +type scanResultMultiWriter struct { + writers []ScanResultWriter +} + +// newMultiWriter create all report writers from input options; if a file is not specified the given defaultWriter is used +func newMultiWriter(options ...scanResultWriterDescription) (_ *scanResultMultiWriter, err error) { + if len(options) == 0 { + return nil, fmt.Errorf("no output options provided") + } + + out := &scanResultMultiWriter{} + + for _, option := range options { + switch len(option.Path) { + case 0: + out.writers = append(out.writers, &scanResultPublisher{ + format: option.Format, + }) + default: + // create any missing subdirectories + dir := path.Dir(option.Path) + if dir != "" { + s, err := os.Stat(dir) + if err != nil { + err = os.MkdirAll(dir, 0755) // maybe should be os.ModePerm ? + if err != nil { + return nil, err + } + } else if !s.IsDir() { + return nil, fmt.Errorf("output path does not contain a valid directory: %s", option.Path) + } + } + fileOut, err := os.OpenFile(option.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644) + if err != nil { + return nil, fmt.Errorf("unable to create report file: %w", err) + } + out.writers = append(out.writers, &scanResultStreamWriter{ + format: option.Format, + out: fileOut, + }) + } + } + + return out, nil +} + +// Write writes the result to all writers +func (m *scanResultMultiWriter) Write(s models.PresenterConfig) (errs error) { + for _, w := range m.writers { + err := w.Write(s) + if err != nil { + errs = multierror.Append(errs, fmt.Errorf("unable to write result: %w", err)) + } + } + return errs +} + +// scanResultStreamWriter implements ScanResultWriter for a given format and io.Writer, also providing a close function for cleanup +type scanResultStreamWriter struct { + format Format + out io.Writer +} + +// Write the provided result to the data stream +func (w *scanResultStreamWriter) Write(s models.PresenterConfig) error { + pres := GetPresenter(w.format, s) + if err := pres.Present(w.out); err != nil { + return fmt.Errorf("unable to encode result: %w", err) + } + return nil +} + +// Close any resources, such as open files +func (w *scanResultStreamWriter) Close() error { + if closer, ok := w.out.(io.Closer); ok { + return closer.Close() + } + return nil +} + +// scanResultPublisher implements ScanResultWriter that publishes results to the event bus +type scanResultPublisher struct { + format Format +} + +// Write the provided result to the data stream +func (w *scanResultPublisher) Write(s models.PresenterConfig) error { + pres := GetPresenter(w.format, s) + buf := &bytes.Buffer{} + if err := pres.Present(buf); err != nil { + return fmt.Errorf("unable to encode result: %w", err) + } + + bus.Report(buf.String()) + return nil +} diff --git a/internal/input.go b/internal/input.go index 42e262cc..208ad166 100644 --- a/internal/input.go +++ b/internal/input.go @@ -5,8 +5,8 @@ import ( "os" ) -// IsPipedInput returns true if there is no input device, which means the user **may** be providing input via a pipe. -func IsPipedInput() (bool, error) { +// IsStdinPipeOrRedirect returns true if stdin is provided via pipe or redirect +func IsStdinPipeOrRedirect() (bool, error) { fi, err := os.Stdin.Stat() if err != nil { return false, fmt.Errorf("unable to determine if there is piped input: %w", err) @@ -16,5 +16,5 @@ func IsPipedInput() (bool, error) { // on stdin, as running xeol as a subprocess you would expect no character device to be present but input can // be from either stdin or indicated by the CLI. Checking if stdin is a pipe is the most direct way to determine // if there *may* be bytes that will show up on stdin that should be used for the analysis source. - return fi.Mode()&os.ModeNamedPipe != 0, nil + return fi.Mode()&os.ModeNamedPipe != 0 || fi.Size() > 0, nil } diff --git a/internal/log/log.go b/internal/log/log.go index 42ef116e..d94ce96b 100644 --- a/internal/log/log.go +++ b/internal/log/log.go @@ -1,69 +1,91 @@ +/* +Package log contains the singleton object and helper functions for facilitating logging within the syft library. +*/ package log import ( "github.com/anchore/go-logger" "github.com/anchore/go-logger/adapter/discard" + "github.com/anchore/go-logger/adapter/redact" + + red "github.com/xeol-io/xeol/internal/redact" ) -// Log is the singleton used to facilitate logging internally within syft -var Log logger.Logger = discard.New() +// log is the singleton used to facilitate logging internally within +var log = discard.New() + +func Set(l logger.Logger) { + // though the application will automatically have a redaction logger, library consumers may not be doing this. + // for this reason we additionally ensure there is a redaction logger configured for any logger passed. The + // source of truth for redaction values is still in the internal redact package. If the passed logger is already + // redacted, then this is a no-op. + store := red.Get() + if store != nil { + l = redact.New(l, store) + } + log = l +} + +func Get() logger.Logger { + return log +} // Errorf takes a formatted template string and template arguments for the error logging level. func Errorf(format string, args ...interface{}) { - Log.Errorf(format, args...) + log.Errorf(format, args...) } // Error logs the given arguments at the error logging level. func Error(args ...interface{}) { - Log.Error(args...) + log.Error(args...) } // Warnf takes a formatted template string and template arguments for the warning logging level. func Warnf(format string, args ...interface{}) { - Log.Warnf(format, args...) + log.Warnf(format, args...) } // Warn logs the given arguments at the warning logging level. func Warn(args ...interface{}) { - Log.Warn(args...) + log.Warn(args...) } // Infof takes a formatted template string and template arguments for the info logging level. func Infof(format string, args ...interface{}) { - Log.Infof(format, args...) + log.Infof(format, args...) } // Info logs the given arguments at the info logging level. func Info(args ...interface{}) { - Log.Info(args...) + log.Info(args...) } // Debugf takes a formatted template string and template arguments for the debug logging level. func Debugf(format string, args ...interface{}) { - Log.Debugf(format, args...) + log.Debugf(format, args...) } // Debug logs the given arguments at the debug logging level. func Debug(args ...interface{}) { - Log.Debug(args...) + log.Debug(args...) } // Tracef takes a formatted template string and template arguments for the trace logging level. func Tracef(format string, args ...interface{}) { - Log.Tracef(format, args...) + log.Tracef(format, args...) } // Trace logs the given arguments at the trace logging level. func Trace(args ...interface{}) { - Log.Trace(args...) + log.Trace(args...) } // WithFields returns a message logger with multiple key-value fields. func WithFields(fields ...interface{}) logger.MessageLogger { - return Log.WithFields(fields...) + return log.WithFields(fields...) } // Nested returns a new logger with hard coded key-value pairs func Nested(fields ...interface{}) logger.Logger { - return Log.Nested(fields...) + return log.Nested(fields...) } diff --git a/internal/redact/redact.go b/internal/redact/redact.go new file mode 100644 index 00000000..3bb76e26 --- /dev/null +++ b/internal/redact/redact.go @@ -0,0 +1,36 @@ +package redact + +import "github.com/anchore/go-logger/adapter/redact" + +var store redact.Store + +func Set(s redact.Store) { + if store != nil { + // if someone is trying to set a redaction store and we already have one then something is wrong. The store + // that we're replacing might already have values in it, so we should never replace it. + panic("replace existing redaction store (probably unintentional)") + } + store = s +} + +func Get() redact.Store { + return store +} + +func Add(vs ...string) { + if store == nil { + // if someone is trying to add values that should never be output and we don't have a store then something is wrong. + // we should never accidentally output values that should be redacted, thus we panic here. + panic("cannot add redactions without a store") + } + store.Add(vs...) +} + +func Apply(value string) string { + if store == nil { + // if someone is trying to add values that should never be output and we don't have a store then something is wrong. + // we should never accidentally output values that should be redacted, thus we panic here. + panic("cannot apply redactions without a store") + } + return store.RedactString(value) +} diff --git a/internal/stringutil/color.go b/internal/stringutil/color.go new file mode 100644 index 00000000..373b98e2 --- /dev/null +++ b/internal/stringutil/color.go @@ -0,0 +1,21 @@ +package stringutil + +import "fmt" + +const ( + DefaultColor Color = iota + 30 + Red + Green + Yellow + Blue + Magenta + Cyan + White +) + +type Color uint8 + +// TODO: not cross platform (windows...) +func (c Color) Format(s string) string { + return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s) +} diff --git a/internal/stringutil/parse.go b/internal/stringutil/parse.go new file mode 100644 index 00000000..6b33c718 --- /dev/null +++ b/internal/stringutil/parse.go @@ -0,0 +1,15 @@ +package stringutil + +import "regexp" + +// MatchCaptureGroups takes a regular expression and string and returns all of the named capture group results in a map. +func MatchCaptureGroups(regEx *regexp.Regexp, str string) map[string]string { + match := regEx.FindStringSubmatch(str) + results := make(map[string]string) + for i, name := range regEx.SubexpNames() { + if i > 0 && i <= len(match) { + results[name] = match[i] + } + } + return results +} diff --git a/internal/stringutil/string_helpers.go b/internal/stringutil/string_helpers.go new file mode 100644 index 00000000..1ff56e35 --- /dev/null +++ b/internal/stringutil/string_helpers.go @@ -0,0 +1,25 @@ +package stringutil + +import "strings" + +// HasAnyOfSuffixes returns an indication if the given string has any of the given suffixes. +func HasAnyOfSuffixes(input string, suffixes ...string) bool { + for _, suffix := range suffixes { + if strings.HasSuffix(input, suffix) { + return true + } + } + + return false +} + +// HasAnyOfPrefixes returns an indication if the given string has any of the given prefixes. +func HasAnyOfPrefixes(input string, prefixes ...string) bool { + for _, prefix := range prefixes { + if strings.HasPrefix(input, prefix) { + return true + } + } + + return false +} diff --git a/internal/stringutil/string_helpers_test.go b/internal/stringutil/string_helpers_test.go new file mode 100644 index 00000000..b5171686 --- /dev/null +++ b/internal/stringutil/string_helpers_test.go @@ -0,0 +1,122 @@ +package stringutil + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHasAnyOfSuffixes(t *testing.T) { + tests := []struct { + name string + input string + suffixes []string + expected bool + }{ + { + name: "go case", + input: "this has something", + suffixes: []string{ + "has something", + "has NOT something", + }, + expected: true, + }, + { + name: "no match", + input: "this has something", + suffixes: []string{ + "has NOT something", + }, + expected: false, + }, + { + name: "empty", + input: "this has something", + suffixes: []string{}, + expected: false, + }, + { + name: "positive match last", + input: "this has something", + suffixes: []string{ + "that does not have", + "something", + }, + expected: true, + }, + { + name: "empty input", + input: "", + suffixes: []string{ + "that does not have", + "this has", + }, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, HasAnyOfSuffixes(test.input, test.suffixes...)) + }) + } +} + +func TestHasAnyOfPrefixes(t *testing.T) { + tests := []struct { + name string + input string + prefixes []string + expected bool + }{ + { + name: "go case", + input: "this has something", + prefixes: []string{ + "this has", + "that does not have", + }, + expected: true, + }, + { + name: "no match", + input: "this has something", + prefixes: []string{ + "this DOES NOT has", + "that does not have", + }, + expected: false, + }, + { + name: "empty", + input: "this has something", + prefixes: []string{}, + expected: false, + }, + { + name: "positive match last", + input: "this has something", + prefixes: []string{ + "that does not have", + "this has", + }, + expected: true, + }, + { + name: "empty input", + input: "", + prefixes: []string{ + "that does not have", + "this has", + }, + expected: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + assert.Equal(t, test.expected, HasAnyOfPrefixes(test.input, test.prefixes...)) + }) + } +} diff --git a/internal/stringutil/stringset.go b/internal/stringutil/stringset.go new file mode 100644 index 00000000..49a73daa --- /dev/null +++ b/internal/stringutil/stringset.go @@ -0,0 +1,38 @@ +package stringutil + +type StringSet map[string]struct{} + +func NewStringSet() StringSet { + return make(StringSet) +} + +func NewStringSetFromSlice(start []string) StringSet { + ret := make(StringSet) + for _, s := range start { + ret.Add(s) + } + return ret +} + +func (s StringSet) Add(i string) { + s[i] = struct{}{} +} + +func (s StringSet) Remove(i string) { + delete(s, i) +} + +func (s StringSet) Contains(i string) bool { + _, ok := s[i] + return ok +} + +func (s StringSet) ToSlice() []string { + ret := make([]string, len(s)) + idx := 0 + for v := range s { + ret[idx] = v + idx++ + } + return ret +} diff --git a/internal/stringutil/tprint.go b/internal/stringutil/tprint.go new file mode 100644 index 00000000..8d874f29 --- /dev/null +++ b/internal/stringutil/tprint.go @@ -0,0 +1,16 @@ +package stringutil + +import ( + "bytes" + "text/template" +) + +// Tprintf renders a string from a given template string and field values +func Tprintf(tmpl string, data map[string]interface{}) string { + t := template.Must(template.New("").Parse(tmpl)) + buf := &bytes.Buffer{} + if err := t.Execute(buf, data); err != nil { + return "" + } + return buf.String() +} diff --git a/internal/ui/common_event_handlers.go b/internal/ui/common_event_handlers.go deleted file mode 100644 index 9a89f973..00000000 --- a/internal/ui/common_event_handlers.go +++ /dev/null @@ -1,84 +0,0 @@ -package ui - -import ( - "fmt" - "io" - - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - - xeolEventParsers "github.com/xeol-io/xeol/xeol/event/parsers" - policyTypes "github.com/xeol-io/xeol/xeol/policy/types" -) - -func handleNotaryPolicyEvaluationMessage(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - nt, err := xeolEventParsers.ParseNotaryPolicyEvaluationMessage(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - var message string - if nt.Action == policyTypes.PolicyActionDeny { - message = color.Red.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party.\n", nt.Action, nt.Type, nt.ImageReference) - } else { - if nt.FailDate != "" { - message = color.Yellow.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party. This policy will fail builds starting on %s.\n", nt.Action, nt.Type, nt.ImageReference, nt.FailDate) - } else { - message = color.Yellow.Sprintf("[%s][%s] Policy Violation: image '%s' is not signed by a trusted party.\n", nt.Action, nt.Type, nt.ImageReference) - } - } - if _, err := reportOutput.Write([]byte(message)); err != nil { - return fmt.Errorf("unable to show policy evaluation message: %w", err) - } - return nil -} - -func handleEolPolicyEvaluationMessage(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - pt, err := xeolEventParsers.ParseEolPolicyEvaluationMessage(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - var message string - if pt.Action == policyTypes.PolicyActionDeny { - message = color.Red.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle) - } else { - if pt.FailDate != "" { - message = color.Yellow.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version. This policy will fail builds starting on %s.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle, pt.FailDate) - } else { - message = color.Yellow.Sprintf("[%s][%s] Policy Violation: %s (v%s) needs to be upgraded to a newer version.\n", pt.Action, pt.Type, pt.ProductName, pt.Cycle) - } - } - if _, err := reportOutput.Write([]byte(message)); err != nil { - return fmt.Errorf("unable to show policy evaluation message: %w", err) - } - return nil -} - -func handleEolScanningFinished(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - pres, err := xeolEventParsers.ParseEolScanningFinished(event) - if err != nil { - return fmt.Errorf("bad CatalogerFinished event: %w", err) - } - - if err := pres.Present(reportOutput); err != nil { - return fmt.Errorf("unable to show eol report: %w", err) - } - return nil -} - -func handleNonRootCommandFinished(event partybus.Event, reportOutput io.Writer) error { - // show the report to stdout - result, err := xeolEventParsers.ParseNonRootCommandFinished(event) - if err != nil { - return fmt.Errorf("bad NonRootCommandFinished event: %w", err) - } - - if _, err := reportOutput.Write([]byte(*result)); err != nil { - return fmt.Errorf("unable to show eol report: %w", err) - } - return nil -} diff --git a/internal/ui/components/spinner.go b/internal/ui/components/spinner.go deleted file mode 100644 index 9c936876..00000000 --- a/internal/ui/components/spinner.go +++ /dev/null @@ -1,40 +0,0 @@ -package components - -import ( - "strings" - "sync" -) - -// TODO: move me to a common module (used in multiple repos) - -const SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" - -type Spinner struct { - index int - charset []string - lock sync.Mutex -} - -func NewSpinner(charset string) Spinner { - return Spinner{ - charset: strings.Split(charset, ""), - } -} - -func (s *Spinner) Current() string { - s.lock.Lock() - defer s.lock.Unlock() - - return s.charset[s.index] -} - -func (s *Spinner) Next() string { - s.lock.Lock() - defer s.lock.Unlock() - c := s.charset[s.index] - s.index++ - if s.index >= len(s.charset) { - s.index = 0 - } - return c -} diff --git a/internal/ui/ephemeral_terminal_ui.go b/internal/ui/ephemeral_terminal_ui.go deleted file mode 100644 index fcfe684f..00000000 --- a/internal/ui/ephemeral_terminal_ui.go +++ /dev/null @@ -1,184 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "bytes" - "context" - "fmt" - "io" - "os" - "sync" - - "github.com/anchore/go-logger" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - "github.com/xeol-io/xeol/internal/log" - xeolEvent "github.com/xeol-io/xeol/xeol/event" - "github.com/xeol-io/xeol/xeol/ui" -) - -// ephemeralTerminalUI provides an "ephemeral" terminal user interface to display the application state dynamically. -// The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line -// UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen -// must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make -// a shared state, bytes coming from elsewhere to the screen will disrupt this state. -// -// This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a -// published element on the event bus, typically polling the element for the latest state. This allows for the UI to -// control update frequency to the screen, provide "liveness" indications that are interpolated between bus events, -// and overall loosely couple the bus events from screen interactions. -// -// By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should -// attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by -// convention, each new event that the UI should respond to should be added either in this package as a handler function, -// or in the shared ui package as a function on the main handler object. All handler functions should be completed -// processing an event before the ETUI exits (coordinated with a sync.WaitGroup) -type ephemeralTerminalUI struct { - unsubscribe func() error - handler *ui.Handler - waitGroup *sync.WaitGroup - frame *frame.Frame - logBuffer *bytes.Buffer - uiOutput *os.File - reportOutput io.Writer -} - -// NewEphemeralTerminalUI writes all events to a TUI and writes the final report to the given writer. -func NewEphemeralTerminalUI(reportWriter io.Writer) UI { - return &ephemeralTerminalUI{ - handler: ui.NewHandler(), - waitGroup: &sync.WaitGroup{}, - uiOutput: os.Stderr, - reportOutput: reportWriter, - } -} - -func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error { - h.unsubscribe = unsubscribe - hideCursor(h.uiOutput) - - // prep the logger to not clobber the screen from now on (logrus only) - h.logBuffer = bytes.NewBufferString("") - logController, ok := log.Log.(logger.Controller) - if ok { - logController.SetOutput(h.logBuffer) - } - - return h.openScreen() -} - -func (h *ephemeralTerminalUI) Handle(event partybus.Event) error { - ctx := context.Background() - switch { - case h.handler.RespondsTo(event): - if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == xeolEvent.NotaryPolicyEvaluationMessage: - // we need to close the screen now since signaling the the presenter is ready means that we - // are about to write bytes to stdout, so we should reset the terminal state first - h.closeScreen(false) - - if err := handleNotaryPolicyEvaluationMessage(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == xeolEvent.EolPolicyEvaluationMessage: - // we need to close the screen now since signaling the the presenter is ready means that we - // are about to write bytes to stdout, so we should reset the terminal state first - h.closeScreen(false) - - if err := handleEolPolicyEvaluationMessage(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == xeolEvent.AppUpdateAvailable: - if err := handleAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - case event.Type == xeolEvent.EolScanningFinished: - // we need to close the screen now since signaling the the presenter is ready means that we - // are about to write bytes to stdout, so we should reset the terminal state first - h.closeScreen(false) - - if err := handleEolScanningFinished(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - // this is the last expected event, stop listening to events - return h.unsubscribe() - - case event.Type == xeolEvent.NonRootCommandFinished: - h.closeScreen(false) - - if err := handleNonRootCommandFinished(event, h.reportOutput); err != nil { - log.Errorf("unable to show %s event: %+v", event.Type, err) - } - - // this is the last expected event, stop listening to events - return h.unsubscribe() - } - return nil -} - -func (h *ephemeralTerminalUI) openScreen() error { - config := frame.Config{ - PositionPolicy: frame.PolicyFloatForward, - // only report output to stderr, reserve report output for stdout - Output: h.uiOutput, - } - - fr, err := frame.New(config) - if err != nil { - return fmt.Errorf("failed to create the screen object: %w", err) - } - h.frame = fr - - return nil -} - -func (h *ephemeralTerminalUI) closeScreen(force bool) { - // we may have other background processes still displaying progress, wait for them to - // finish before discontinuing dynamic content and showing the final report - if !h.frame.IsClosed() { - if !force { - h.waitGroup.Wait() - } - h.frame.Close() - // TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output - frame.Close() - - // only flush the log on close - h.flushLog() - } -} - -func (h *ephemeralTerminalUI) flushLog() { - // flush any errors to the screen before the report - logController, ok := log.Log.(logger.Controller) - if ok { - fmt.Fprint(logController.GetOutput(), h.logBuffer.String()) - logController.SetOutput(h.uiOutput) - } else { - fmt.Fprint(h.uiOutput, h.logBuffer.String()) - } -} - -func (h *ephemeralTerminalUI) Teardown(force bool) error { - h.closeScreen(force) - showCursor(h.uiOutput) - return nil -} - -func hideCursor(output io.Writer) { - fmt.Fprint(output, "\x1b[?25l") -} - -func showCursor(output io.Writer) { - fmt.Fprint(output, "\x1b[?25h") -} diff --git a/internal/ui/etui_event_handlers.go b/internal/ui/etui_event_handlers.go deleted file mode 100644 index 63a1f4dd..00000000 --- a/internal/ui/etui_event_handlers.go +++ /dev/null @@ -1,36 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "context" - "fmt" - "io" - "sync" - - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - "github.com/xeol-io/xeol/internal" - "github.com/xeol-io/xeol/internal/version" - xeolEventParsers "github.com/xeol-io/xeol/xeol/event/parsers" -) - -func handleAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error { - newVersion, err := xeolEventParsers.ParseAppUpdateAvailable(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - line, err := fr.Prepend() - if err != nil { - return err - } - - message := color.Magenta.Sprintf("New version of %s is available: %s (currently running: %s)", internal.ApplicationName, newVersion, version.FromBuild().Version) - _, _ = io.WriteString(line, message) - - return nil -} diff --git a/internal/ui/logger_ui.go b/internal/ui/logger_ui.go deleted file mode 100644 index fb44a853..00000000 --- a/internal/ui/logger_ui.go +++ /dev/null @@ -1,58 +0,0 @@ -package ui - -import ( - "io" - - "github.com/wagoodman/go-partybus" - - "github.com/xeol-io/xeol/internal/log" - xeolEvent "github.com/xeol-io/xeol/xeol/event" -) - -type loggerUI struct { - unsubscribe func() error - reportOutput io.Writer -} - -// NewLoggerUI writes all events to the common application logger and writes the final report to the given writer. -func NewLoggerUI(reportWriter io.Writer) UI { - return &loggerUI{ - reportOutput: reportWriter, - } -} - -func (l *loggerUI) Setup(unsubscribe func() error) error { - l.unsubscribe = unsubscribe - return nil -} - -func (l loggerUI) Handle(event partybus.Event) error { - switch event.Type { - case xeolEvent.NotaryPolicyEvaluationMessage: - if err := handleNotaryPolicyEvaluationMessage(event, l.reportOutput); err != nil { - log.Warnf("unable to show policy evaluation message event: %+v", err) - } - case xeolEvent.EolPolicyEvaluationMessage: - if err := handleEolPolicyEvaluationMessage(event, l.reportOutput); err != nil { - log.Warnf("unable to show policy evaluation message event: %+v", err) - } - case xeolEvent.EolScanningFinished: - if err := handleEolScanningFinished(event, l.reportOutput); err != nil { - log.Warnf("unable to show catalog image finished event: %+v", err) - } - case xeolEvent.NonRootCommandFinished: - if err := handleNonRootCommandFinished(event, l.reportOutput); err != nil { - log.Warnf("unable to show command finished event: %+v", err) - } - // ignore all events except for the final events - default: - return nil - } - - // this is the last expected event, stop listening to events - return l.unsubscribe() -} - -func (l loggerUI) Teardown(_ bool) error { - return nil -} diff --git a/internal/ui/select.go b/internal/ui/select.go deleted file mode 100644 index 2ecd90cf..00000000 --- a/internal/ui/select.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build linux || darwin -// +build linux darwin - -package ui - -import ( - "io" - "os" - - "golang.org/x/term" -) - -// TODO: build tags to exclude options from windows - -// Select is responsible for determining the specific UI function given select user option, the current platform -// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs -// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there -// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of -// the final SBOM report. -func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) { - isStdoutATty := term.IsTerminal(int(os.Stdout.Fd())) - isStderrATty := term.IsTerminal(int(os.Stderr.Fd())) - notATerminal := !isStderrATty && !isStdoutATty - - switch { - case verbose || quiet || notATerminal || !isStderrATty: - uis = append(uis, NewLoggerUI(reportWriter)) - default: - uis = append(uis, NewEphemeralTerminalUI(reportWriter), NewLoggerUI(reportWriter)) - } - - return uis -} diff --git a/internal/ui/select_windows.go b/internal/ui/select_windows.go deleted file mode 100644 index 20e7e58f..00000000 --- a/internal/ui/select_windows.go +++ /dev/null @@ -1,17 +0,0 @@ -//go:build windows -// +build windows - -package ui - -import ( - "io" -) - -// Select is responsible for determining the specific UI function given select user option, the current platform -// config values, and environment status (such as a TTY being present). The first UI in the returned slice of UIs -// is intended to be used and the UIs that follow are meant to be attempted only in a fallback posture when there -// are environmental problems (e.g. cannot write to the terminal). A writer is provided to capture the output of -// the final SBOM report. -func Select(verbose, quiet bool, reportWriter io.Writer) (uis []UI) { - return append(uis, NewLoggerUI(reportWriter)) -} diff --git a/internal/ui/ui.go b/internal/ui/ui.go deleted file mode 100644 index cb551f1c..00000000 --- a/internal/ui/ui.go +++ /dev/null @@ -1,11 +0,0 @@ -package ui - -import ( - "github.com/wagoodman/go-partybus" -) - -type UI interface { - Setup(unsubscribe func() error) error - partybus.Handler - Teardown(force bool) error -} diff --git a/internal/xeolio/request.go b/internal/xeolio/request.go index 282863ad..d41d4812 100644 --- a/internal/xeolio/request.go +++ b/internal/xeolio/request.go @@ -64,17 +64,19 @@ func (x *XeolClient) FetchCertificates() (string, error) { var raw json.RawMessage statusCode, err := x.makeRequest("GET", XeolAPIURL, "certificate", nil, &raw) if err != nil { - return "", err + log.Warnf("failed to fetch certificates, continuing without notary policy evaluation") + return "", nil } if statusCode == http.StatusNotFound { - log.Debugf("no certificates found in xeol.io API response") + log.Warnf("no certificates found in xeol.io API response") return "", nil } var resp CertificateResponse if err := json.Unmarshal(raw, &resp); err != nil { - return "", err + log.Warnf("failed to unmarshal certificates, continuing without notary policy evaluation") + return "", nil } return resp.Certificate, nil @@ -84,15 +86,21 @@ func (x *XeolClient) FetchPolicies() ([]policy.Policy, error) { var raw json.RawMessage statusCode, err := x.makeRequest("GET", XeolAPIURL, "v2/policy", nil, &raw) if err != nil { - return nil, err + log.Warnf("failed to fetch policies, continuing without policy evaluation") + return nil, nil } if statusCode == http.StatusNotFound { - log.Debugf("no policies found in xeol.io API response") + log.Warnf("no policies found in xeol.io API response") return nil, nil } - return policy.UnmarshalPolicies(raw) + policies, err := policy.UnmarshalPolicies(raw) + if err != nil { + log.Warnf("failed to unmarshal policies, continuing without policy evaluation") + return nil, nil + } + return policies, nil } func (x *XeolClient) SendEvent(payload report.XeolEventPayload) error { diff --git a/internal/xeolio/source.go b/internal/xeolio/source.go index 5fff5e60..00f08fca 100644 --- a/internal/xeolio/source.go +++ b/internal/xeolio/source.go @@ -11,66 +11,53 @@ type EventSource interface { } type DirectorySource struct { - ID string Type string Target string } func (s *DirectorySource) Serialize() map[string]interface{} { return map[string]interface{}{ - "ID": s.ID, "Type": s.Type, "Target": s.Target, } } -func NewDirectorySource(sbomSource source.Metadata) *DirectorySource { +func NewDirectorySource(dirSource source.DirectorySourceMetadata) *DirectorySource { return &DirectorySource{ - ID: sbomSource.ID, - Type: string(sbomSource.Scheme), - Target: sbomSource.Path, + Type: "DirectoryScheme", + Target: dirSource.Path, } } type ImageSource struct { - ID string - Type string - ImageName string - ImageDigest string - ManifestDigest string + Type string + ImageName string + ImageDigest string } -func NewImageSource(sbomSource source.Metadata) *ImageSource { +func NewImageSource(imageSource source.StereoscopeImageSourceMetadata) *ImageSource { return &ImageSource{ - ID: sbomSource.ID, - Type: string(sbomSource.Scheme), - ImageName: sbomSource.ImageMetadata.UserInput, - ImageDigest: sbomSource.ImageMetadata.ID, - ManifestDigest: sbomSource.ImageMetadata.ManifestDigest, + Type: "ImageScheme", + ImageName: imageSource.UserInput, + ImageDigest: imageSource.ManifestDigest, } } func (s *ImageSource) Serialize() map[string]interface{} { return map[string]interface{}{ - "ID": s.ID, - "Type": s.Type, - "ImageName": s.ImageName, - "ImageDigest": s.ImageDigest, - "ManifestDigest": s.ManifestDigest, + "Type": s.Type, + "ImageName": s.ImageName, + "ImageDigest": s.ImageDigest, } } -func EventSourceScheme(sbomSource source.Metadata) source.Scheme { - return sbomSource.Scheme -} - -func NewEventSource(sbomSource source.Metadata) (map[string]interface{}, error) { - if sbomSource.Scheme == source.DirectoryScheme { - return NewDirectorySource(sbomSource).Serialize(), nil +func NewEventSource(sbomSource source.Description) (map[string]interface{}, error) { + switch v := sbomSource.Metadata.(type) { + case source.DirectorySourceMetadata: + return NewDirectorySource(v).Serialize(), nil + case source.StereoscopeImageSourceMetadata: + return NewImageSource(v).Serialize(), nil + default: + return nil, fmt.Errorf("unsupported source type: %s", v) } - if sbomSource.Scheme == source.ImageScheme { - return NewImageSource(sbomSource).Serialize(), nil - } - - return nil, fmt.Errorf("unsupported source type: %s", sbomSource.Scheme) } diff --git a/main.go b/main.go deleted file mode 100644 index 4e68a08f..00000000 --- a/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "github.com/xeol-io/xeol/cmd" - -func main() { - cmd.Execute() -} diff --git a/test/cli/cmd_test.go b/test/cli/cmd_test.go index 2f57c56b..55bd4fbb 100644 --- a/test/cli/cmd_test.go +++ b/test/cli/cmd_test.go @@ -16,8 +16,8 @@ func TestCmd(t *testing.T) { name: "no-args-shows-help", args: []string{}, assertions: []traitAssertion{ - assertInOutput("an image/directory argument is required"), // specific error that should be shown - assertInOutput("A scanner for end-of-life (EOL) software in container images, filesystems, and SBOMs"), // excerpt from help description + assertInOutput("an image/directory argument is required"), // specific error that should be shown + assertInOutput("A scanner for end-of-life (EOL) software in container images, filesystems, and SBOMs."), // excerpt from help description assertFailingReturnCode, }, }, @@ -39,7 +39,7 @@ func TestCmd(t *testing.T) { }, { name: "responds-to-search-options", - args: []string{"-vv"}, + args: []string{"--help"}, env: map[string]string{ "XEOL_SEARCH_UNINDEXED_ARCHIVES": "true", "XEOL_SEARCH_INDEXED_ARCHIVES": "false", @@ -51,7 +51,7 @@ func TestCmd(t *testing.T) { // package-cataloger-level options. assertInOutput("unindexed-archives: true"), assertInOutput("indexed-archives: false"), - assertInOutput("scope: all-layers"), + assertInOutput("scope: 'all-layers'"), }, }, } diff --git a/test/cli/db_validations_test.go b/test/cli/db_validations_test.go index da917204..5ddf5bea 100644 --- a/test/cli/db_validations_test.go +++ b/test/cli/db_validations_test.go @@ -21,7 +21,7 @@ func TestDBValidations(t *testing.T) { "XEOL_DB_CA_CERT": "./does-not-exist.crt", }, assertions: []traitAssertion{ - assertInOutput("failed to load eol db"), + assertInOutput("failed to load EOL db"), assertFailingReturnCode, }, }, diff --git a/test/cli/registry_auth_test.go b/test/cli/registry_auth_test.go index 34a17490..5a7ffadf 100644 --- a/test/cli/registry_auth_test.go +++ b/test/cli/registry_auth_test.go @@ -18,7 +18,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), assertInOutput("localhost:5000/something:latest"), - assertInOutput("no registry credentials configured, using the default keychain"), + assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`), }, }, { @@ -40,7 +40,7 @@ func TestRegistryAuth(t *testing.T) { args: []string{"-vv", "registry:localhost:5000/something:latest"}, env: map[string]string{ "XEOL_REGISTRY_AUTH_AUTHORITY": "localhost:5000", - "XEOL_REGISTRY_AUTH_TOKEN": "token", + "XEOL_REGISTRY_AUTH_TOKEN": "my-token", }, assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), @@ -57,7 +57,7 @@ func TestRegistryAuth(t *testing.T) { assertions: []traitAssertion{ assertInOutput("source=OciRegistry"), assertInOutput("localhost:5000/something:latest"), - assertInOutput(`no registry credentials configured, using the default keychain`), + assertInOutput(`no registry credentials configured for "localhost:5000", using the default keychain`), }, }, { @@ -70,6 +70,17 @@ func TestRegistryAuth(t *testing.T) { assertInOutput("insecure-use-http: true"), }, }, + { + name: "use tls configuration", + args: []string{"-vvv", "registry:localhost:5000/something:latest"}, + env: map[string]string{ + "XEOL_REGISTRY_AUTH_TLS_CERT": "place.crt", + "XEOL_REGISTRY_AUTH_TLS_KEY": "place.key", + }, + assertions: []traitAssertion{ + assertInOutput("using custom TLS credentials from"), + }, + }, } for _, test := range tests { diff --git a/test/cli/sbom_input_test.go b/test/cli/sbom_input_test.go index b6f1886f..d3d21f47 100644 --- a/test/cli/sbom_input_test.go +++ b/test/cli/sbom_input_test.go @@ -2,8 +2,12 @@ package cli import ( "os" + "os/exec" "path" + "runtime" "testing" + + "github.com/stretchr/testify/require" ) func TestSBOMInput_AsArgument(t *testing.T) { @@ -52,3 +56,47 @@ func TestSBOMInput_AsArgument(t *testing.T) { } }) } + +func TestSBOMInput_FromStdin(t *testing.T) { + tests := []struct { + name string + input string + args []string + wantErr require.ErrorAssertionFunc + wantOutput string + }{ + { + name: "empty file", + input: "./test-fixtures/empty.json", + args: []string{"-c", "../xeol-test-config.yaml"}, + wantErr: require.Error, + wantOutput: "unable to decode sbom: unable to identify format", + }, + { + name: "sbom", + input: "./test-fixtures/sbom-ubuntu-20.04--pruned.json", + args: []string{"-c", "../xeol-test-config.yaml"}, + wantErr: require.NoError, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := exec.Command(getXeolSnapshotLocation(t, runtime.GOOS), tt.args...) + + input, err := os.Open(tt.input) + require.NoError(t, err) + + attachFileToCommandStdin(t, input, cmd) + err = input.Close() + require.NoError(t, err) + + output, err := cmd.CombinedOutput() + tt.wantErr(t, err, "output: %s", output) + if tt.wantOutput != "" { + require.Contains(t, string(output), tt.wantOutput) + } + + }) + } +} diff --git a/test/cli/test-fixtures/Makefile b/test/cli/test-fixtures/Makefile new file mode 100644 index 00000000..5042a5aa --- /dev/null +++ b/test/cli/test-fixtures/Makefile @@ -0,0 +1,6 @@ +# change these if you want CI to not use previous stored cache +CLI_CACHE_BUSTER := "e5cdfd8" + +.PHONY: cache.fingerprint +cache.fingerprint: + find image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> cache.fingerprint diff --git a/test/install/Makefile b/test/install/Makefile index 0a2012a4..e7c76794 100644 --- a/test/install/Makefile +++ b/test/install/Makefile @@ -125,3 +125,4 @@ busybox-1.35: cache.fingerprint: $(call title,Install test fixture fingerprint) @find ./environments/* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo "$(INSTALL_TEST_CACHE_BUSTER)" >> cache.fingerprint + diff --git a/test/integration/match_by_image_test.go b/test/integration/match_by_image_test.go index 2408362c..eeadb4bf 100644 --- a/test/integration/match_by_image_test.go +++ b/test/integration/match_by_image_test.go @@ -62,21 +62,6 @@ func addPython34Matches(t *testing.T, theResult *match.Matches) { Package: pkg.Package{ Name: "python", ID: "10ab199091f52dbc", - Version: "3.5.3", - Type: syftPkg.BinaryPkg, - Language: "", - PURL: "pkg:generic/python@3.5.3", - }, - Cycle: eol.Cycle{ - ProductName: "Python", - ReleaseCycle: "3.5", - Eol: "2020-09-13", - }, - }) - theResult.Add(match.Match{ - Package: pkg.Package{ - Name: "python", - ID: "5f9c938f5ff241bf", Version: "3.4.10", Type: syftPkg.BinaryPkg, Language: "", @@ -88,39 +73,56 @@ func addPython34Matches(t *testing.T, theResult *match.Matches) { Eol: "2019-03-18", }, }) - theResult.Add(match.Match{ - Package: pkg.Package{ - Name: "python", - ID: "2ba17cf1680ce4f2", - Version: "2.7.13", - Type: syftPkg.BinaryPkg, - Language: "", - PURL: "pkg:generic/python@2.7.13", - }, - Cycle: eol.Cycle{ - ProductName: "Python", - ReleaseCycle: "2.7", - Eol: "2020-01-01", - }, - }) + // TODO: tracking issue https://github.com/anchore/syft/issues/2153 + // theResult.Add(match.Match{ + // Package: pkg.Package{ + // Name: "python", + // ID: "5f9c938f5ff241bf", + // Version: "3.4.10", + // Type: syftPkg.BinaryPkg, + // Language: "", + // PURL: "pkg:generic/python@3.4.10", + // }, + // Cycle: eol.Cycle{ + // ProductName: "Python", + // ReleaseCycle: "3.4", + // Eol: "2019-03-18", + // }, + // }) + // theResult.Add(match.Match{ + // Package: pkg.Package{ + // Name: "python", + // ID: "2ba17cf1680ce4f2", + // Version: "2.7.13", + // Type: syftPkg.BinaryPkg, + // Language: "", + // PURL: "pkg:generic/python@2.7.13", + // }, + // Cycle: eol.Cycle{ + // ProductName: "Python", + // ReleaseCycle: "2.7", + // Eol: "2020-01-01", + // }, + // }) } func addGolang115Matches(t *testing.T, theResult *match.Matches) { - theResult.Add(match.Match{ - Package: pkg.Package{ - Name: "python", - ID: "2ba17cf1680ce4f2", - Version: "2.7.16", - Type: syftPkg.BinaryPkg, - Language: "", - PURL: "pkg:generic/python@2.7.16", - }, - Cycle: eol.Cycle{ - ProductName: "Python", - ReleaseCycle: "2.7", - Eol: "2020-01-01", - }, - }) + // TODO: tracking issue https://github.com/anchore/syft/issues/2153 + // theResult.Add(match.Match{ + // Package: pkg.Package{ + // Name: "python", + // ID: "2ba17cf1680ce4f2", + // Version: "2.7.16", + // Type: syftPkg.BinaryPkg, + // Language: "", + // PURL: "pkg:generic/python@2.7.16", + // }, + // Cycle: eol.Cycle{ + // ProductName: "Python", + // ReleaseCycle: "2.7", + // Eol: "2020-01-01", + // }, + // }) theResult.Add(match.Match{ Package: pkg.Package{ Name: "go", @@ -148,8 +150,7 @@ func addPostgres9Matches(t *testing.T, theResult *match.Matches) { PURL: "pkg:deb/debian/postgresql-9.6@9.6.24-1.pgdg90+1?arch=amd64&distro=debian-9", }, Cycle: eol.Cycle{ - ProductName: "PostgreSQL", - + ProductName: "PostgreSQL", ReleaseCycle: "9.6", Eol: "2021-11-11", }, @@ -157,21 +158,22 @@ func addPostgres9Matches(t *testing.T, theResult *match.Matches) { } func addElaticsearch6Matches(t *testing.T, theResult *match.Matches) { - theResult.Add(match.Match{ - Package: pkg.Package{ - Name: "python", - ID: "2ba17cf1680ce4f2", - Version: "2.7.5", - Type: syftPkg.BinaryPkg, - Language: "", - PURL: "pkg:generic/python@2.7.5", - }, - Cycle: eol.Cycle{ - ProductName: "Python", - ReleaseCycle: "2.7", - Eol: "2020-01-01", - }, - }) + // TODO: tracking issue https://github.com/anchore/syft/issues/2153 + // theResult.Add(match.Match{ + // Package: pkg.Package{ + // Name: "python", + // ID: "2ba17cf1680ce4f2", + // Version: "2.7.5", + // Type: syftPkg.BinaryPkg, + // Language: "", + // PURL: "pkg:generic/python@2.7.5", + // }, + // Cycle: eol.Cycle{ + // ProductName: "Python", + // ReleaseCycle: "2.7", + // Eol: "2020-01-01", + // }, + // }) theResult.Add(match.Match{ Package: pkg.Package{ Name: "elasticsearch", @@ -238,21 +240,22 @@ func addFedora29Matches(t *testing.T, theResult *match.Matches) { Eol: "2019-11-26", }, }) - theResult.Add(match.Match{ - Package: pkg.Package{ - Name: "python", - ID: "2ba17cf1680ce4f2", - Version: "3.7.2", - Type: syftPkg.BinaryPkg, - Language: "", - PURL: "pkg:generic/python@3.7.2", - }, - Cycle: eol.Cycle{ - ProductName: "Python", - ReleaseCycle: "3.7", - Eol: "2023-06-27", - }, - }) + // requires this PR to be merged first https://github.com/endoflife-date/endoflife.date/pull/3570 + // theResult.Add(match.Match{ + // Package: pkg.Package{ + // Name: "python", + // ID: "2ba17cf1680ce4f2", + // Version: "3.7.2", + // Type: syftPkg.BinaryPkg, + // Language: "", + // PURL: "pkg:generic/python@3.7.2", + // }, + // Cycle: eol.Cycle{ + // ProductName: "Python", + // ReleaseCycle: "3.7", + // Eol: "2023-06-27", + // }, + // }) } func TestMatchByImage(t *testing.T) { @@ -343,13 +346,15 @@ func TestMatchByImage(t *testing.T) { userImage := "docker-archive:" + tarPath - sourceInput, err := source.ParseInput(userImage, "") + detection, err := source.Detect(userImage, source.DetectConfig{}) require.NoError(t, err) // this is purely done to help setup mocks - theSource, cleanup, err := source.New(*sourceInput, nil, nil) + theSource, err := detection.NewSource(source.DetectionSourceConfig{}) require.NoError(t, err) - defer cleanup() + t.Cleanup(func() { + require.NoError(t, theSource.Close()) + }) // TODO: relationships are not verified at this time config := cataloger.DefaultConfig() diff --git a/test/integration/test-fixtures/Makefile b/test/integration/test-fixtures/Makefile new file mode 100644 index 00000000..2a75aa43 --- /dev/null +++ b/test/integration/test-fixtures/Makefile @@ -0,0 +1,6 @@ +# change these if you want CI to not use previous stored cache +INTEGRATION_CACHE_BUSTER := "894d8ca" + +.PHONY: cache.fingerprint +cache.fingerprint: + find image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | tee /dev/stderr | md5sum | tee cache.fingerprint && echo "$(INTEGRATION_CACHE_BUSTER)" >> cache.fingerprint diff --git a/xeol/db/eol_provider.go b/xeol/db/eol_provider.go index 0119d03c..69c10186 100644 --- a/xeol/db/eol_provider.go +++ b/xeol/db/eol_provider.go @@ -24,40 +24,40 @@ func NewEolProvider(reader xeolDB.EolStoreReader) (*EolProvider, error) { }, nil } -func (pr *EolProvider) GetByDistroCpe(r *linux.Release) (string, []eol.Cycle, error) { +func (pr *EolProvider) GetByDistroCpe(r *linux.Release) (string, []eol.Cycle, string, error) { cycles := make([]eol.Cycle, 0) if r == nil { - return "", []eol.Cycle{}, errors.New("empty distro release") + return "", []eol.Cycle{}, "", errors.New("empty distro release") } d, err := distro.NewFromRelease(*r) if err != nil { - return "", []eol.Cycle{}, err + return "", []eol.Cycle{}, "", err } if d == nil || d.CPEName.String() == "" { - return "", []eol.Cycle{}, errors.New("empty distro CPEName") + return "", []eol.Cycle{}, "", errors.New("empty distro CPEName") } shortCPE, version := d.CPEName.Destructured() if version == "" || shortCPE == "" { - return "", []eol.Cycle{}, errors.New("invalid distro CPEName") + return "", []eol.Cycle{}, "", errors.New("invalid distro CPEName") } allCycles, err := pr.reader.GetCyclesByCpe(shortCPE) if err != nil { - return "", []eol.Cycle{}, err + return "", []eol.Cycle{}, "", err } for _, cycle := range allCycles { cycleObj, err := eol.NewCycle(cycle) if err != nil { - return "", []eol.Cycle{}, err + return "", []eol.Cycle{}, "", err } cycles = append(cycles, *cycleObj) } - return version, cycles, nil + return version, cycles, shortCPE, nil } func (pr *EolProvider) GetByPackagePurl(p pkg.Package) ([]eol.Cycle, error) { diff --git a/xeol/db/internal/gormadapter/open.go b/xeol/db/internal/gormadapter/open.go index a25032be..6448dada 100644 --- a/xeol/db/internal/gormadapter/open.go +++ b/xeol/db/internal/gormadapter/open.go @@ -4,7 +4,7 @@ import ( "fmt" "os" - "github.com/anchore/sqlite" + "github.com/glebarez/sqlite" "gorm.io/gorm" ) diff --git a/xeol/db/v1/store/store.go b/xeol/db/v1/store/store.go index fe4da57c..d62dbcfd 100644 --- a/xeol/db/v1/store/store.go +++ b/xeol/db/v1/store/store.go @@ -3,7 +3,7 @@ package store import ( "fmt" - _ "github.com/anchore/sqlite" // provide the sqlite dialect to gorm via import + _ "github.com/glebarez/sqlite" // provide the sqlite dialect to gorm via import "gorm.io/gorm" "github.com/xeol-io/xeol/xeol/db/internal/gormadapter" diff --git a/xeol/db/v1/store/store_test.go b/xeol/db/v1/store/store_test.go index 5e086906..b28bcd4d 100644 --- a/xeol/db/v1/store/store_test.go +++ b/xeol/db/v1/store/store_test.go @@ -1,7 +1,6 @@ package store import ( - "io/ioutil" "os" "testing" "time" @@ -26,7 +25,7 @@ func assertIDReader(t *testing.T, reader v1.IDReader, expected v1.ID) { } func TestStore_GetID_SetID(t *testing.T) { - dbTempFile, err := ioutil.TempFile("", "grype-db-test-store") + dbTempFile, err := os.CreateTemp("", "xeol-db-test-store") if err != nil { t.Fatalf("could not create temp file: %+v", err) } diff --git a/xeol/distro/distro_test.go b/xeol/distro/distro_test.go index 0deab795..92f1ff5a 100644 --- a/xeol/distro/distro_test.go +++ b/xeol/distro/distro_test.go @@ -224,7 +224,7 @@ func Test_NewDistroFromRelease_Coverage(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - s, err := source.NewFromDirectory(test.fixture) + s, err := source.NewFromDirectory(source.DirectoryConfig{Path: test.fixture}) require.NoError(t, err) resolver, err := s.FileResolver(source.SquashedScope) @@ -408,7 +408,7 @@ func TestDistro_CpeName(t *testing.T) { for _, test := range tests { t.Run(test.fixture, func(t *testing.T) { - s, err := source.NewFromDirectory(test.fixture) + s, err := source.NewFromDirectory(source.DirectoryConfig{Path: test.fixture}) require.NoError(t, err) resolver, err := s.FileResolver(source.SquashedScope) diff --git a/xeol/eol/provider.go b/xeol/eol/provider.go index 341ea64f..b7e5093c 100644 --- a/xeol/eol/provider.go +++ b/xeol/eol/provider.go @@ -16,5 +16,5 @@ type ProviderByPackagePurl interface { } type ProviderByDistroCpe interface { - GetByDistroCpe(distro *linux.Release) (string, []Cycle, error) + GetByDistroCpe(distro *linux.Release) (string, []Cycle, string, error) } diff --git a/xeol/eol_matcher.go b/xeol/eol_matcher.go new file mode 100644 index 00000000..088f1417 --- /dev/null +++ b/xeol/eol_matcher.go @@ -0,0 +1,30 @@ +package xeol + +import ( + "time" + + "github.com/anchore/syft/syft/linux" + + "github.com/xeol-io/xeol/xeol/match" + "github.com/xeol-io/xeol/xeol/matcher" + "github.com/xeol-io/xeol/xeol/pkg" + "github.com/xeol-io/xeol/xeol/store" + "github.com/xeol-io/xeol/xeol/xeolerr" +) + +type EolMatcher struct { + Store store.Store + Matchers []matcher.Matcher + FailOnEolFound bool + EolMatchDate time.Time + LinuxRelease *linux.Release +} + +func (e *EolMatcher) FindEol(packages []pkg.Package) (match.Matches, error) { + matches := matcher.FindMatches(e.Store, e.LinuxRelease, e.Matchers, packages, e.FailOnEolFound, e.EolMatchDate) + var err error + if e.FailOnEolFound && matches.Count() > 0 { + err = xeolerr.ErrEolFound + } + return matches, err +} diff --git a/xeol/event/event.go b/xeol/event/event.go index 964feacd..3c068108 100644 --- a/xeol/event/event.go +++ b/xeol/event/event.go @@ -1,15 +1,33 @@ package event -import "github.com/wagoodman/go-partybus" +import ( + "github.com/wagoodman/go-partybus" +) const ( - AppUpdateAvailable partybus.EventType = "xeol-app-update-available" - UpdateEolDatabase partybus.EventType = "xeol-update-eol-database" - EolScanningStarted partybus.EventType = "xeol-eol-scanning-started" - EolScanningFinished partybus.EventType = "xeol-eol-scanning-finished" - EolPolicyEvaluationMessage partybus.EventType = "xeol-eol-policy-evaluation-message" - NotaryPolicyEvaluationMessage partybus.EventType = "xeol-notary-policy-evaluation-message" - AttestationVerified partybus.EventType = "xeol-attestation-signature-passed" - AttestationVerificationSkipped partybus.EventType = "xeol-attestation-verification-skipped" - NonRootCommandFinished partybus.EventType = "xeol-non-root-command-finished" + typePrefix = "xeol" + cliTypePrefix = typePrefix + "-cli" + + // Events from the xeol library + + UpdateEolDatabase partybus.EventType = typePrefix + "-update-eol-database" + EolScanningStarted partybus.EventType = typePrefix + "-eol-scanning-started" + EolScanningFinished partybus.EventType = typePrefix + "-eol-scanning-finished" + EolPolicyEvaluationMessage partybus.EventType = typePrefix + "-eol-policy-evaluation-message" + NotaryPolicyEvaluationMessage partybus.EventType = typePrefix + "-notary-policy-evaluation-message" + DatabaseDiffingStarted partybus.EventType = typePrefix + "-database-diffing-started" + + // Events exclusively for the CLI + + // CLIAppUpdateAvailable is a partybus event that occurs when an application update is available + CLIAppUpdateAvailable partybus.EventType = cliTypePrefix + "-app-update-available" + + // CLIReport is a partybus event that occurs when an analysis result is ready for final presentation to stdout + CLIReport partybus.EventType = cliTypePrefix + "-report" + + // CLINotification is a partybus event that occurs when auxiliary information is ready for presentation to stderr + CLINotification partybus.EventType = cliTypePrefix + "-notification" + + // CLIExit is a partybus event that occurs when an analysis result is ready for final presentation + CLIExit partybus.EventType = cliTypePrefix + "-exit-event" ) diff --git a/xeol/event/monitor/matching.go b/xeol/event/monitor/matching.go new file mode 100644 index 00000000..d744611b --- /dev/null +++ b/xeol/event/monitor/matching.go @@ -0,0 +1,10 @@ +package monitor + +import ( + "github.com/wagoodman/go-progress" +) + +type Matching struct { + PackagesProcessed progress.Progressable + MatchesDiscovered progress.Monitorable +} diff --git a/xeol/event/parsers/parsers.go b/xeol/event/parsers/parsers.go index faed0f19..c69cb493 100644 --- a/xeol/event/parsers/parsers.go +++ b/xeol/event/parsers/parsers.go @@ -7,7 +7,7 @@ import ( "github.com/wagoodman/go-progress" "github.com/xeol-io/xeol/xeol/event" - "github.com/xeol-io/xeol/xeol/matcher" + "github.com/xeol-io/xeol/xeol/event/monitor" policyTypes "github.com/xeol-io/xeol/xeol/policy/types" "github.com/xeol-io/xeol/xeol/presenter" ) @@ -22,6 +22,24 @@ func (e *ErrBadPayload) Error() string { return fmt.Sprintf("event='%s' has bad event payload field='%v': '%+v'", string(e.Type), e.Field, e.Value) } +type UpdateCheck struct { + New string + Current string +} + +func ParseCLIAppUpdateAvailable(e partybus.Event) (*UpdateCheck, error) { + if err := checkEventType(e.Type, event.CLIAppUpdateAvailable); err != nil { + return nil, err + } + + updateCheck, ok := e.Value.(UpdateCheck) + if !ok { + return nil, newPayloadErr(e.Type, "Value", e.Value) + } + + return &updateCheck, nil +} + func newPayloadErr(t partybus.EventType, field string, value interface{}) error { return &ErrBadPayload{ Type: t, @@ -60,31 +78,36 @@ func ParseEolPolicyEvaluationMessage(e partybus.Event) (*policyTypes.EolEvaluati } return &pt, nil } - -func ParseAppUpdateAvailable(e partybus.Event) (string, error) { - if err := checkEventType(e.Type, event.AppUpdateAvailable); err != nil { - return "", err +func ParseEolScanningStarted(e partybus.Event) (*monitor.Matching, error) { + if err := checkEventType(e.Type, event.EolScanningStarted); err != nil { + return nil, err } - newVersion, ok := e.Value.(string) + monitor, ok := e.Value.(monitor.Matching) if !ok { - return "", newPayloadErr(e.Type, "Value", e.Value) + return nil, newPayloadErr(e.Type, "Value", e.Value) } - return newVersion, nil + return &monitor, nil } -func ParseEolScanningStarted(e partybus.Event) (*matcher.Monitor, error) { - if err := checkEventType(e.Type, event.EolScanningStarted); err != nil { - return nil, err +func ParseCLIReport(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.CLIReport); err != nil { + return "", "", err } - monitor, ok := e.Value.(matcher.Monitor) + context, ok := e.Source.(string) if !ok { - return nil, newPayloadErr(e.Type, "Value", e.Value) + // this is optional + context = "" } - return &monitor, nil + report, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) + } + + return context, report, nil } func ParseEolScanningFinished(e partybus.Event) (presenter.Presenter, error) { @@ -100,17 +123,23 @@ func ParseEolScanningFinished(e partybus.Event) (presenter.Presenter, error) { return pres, nil } -func ParseNonRootCommandFinished(e partybus.Event) (*string, error) { - if err := checkEventType(e.Type, event.NonRootCommandFinished); err != nil { - return nil, err +func ParseCLINotification(e partybus.Event) (string, string, error) { + if err := checkEventType(e.Type, event.CLINotification); err != nil { + return "", "", err } - result, ok := e.Value.(string) + context, ok := e.Source.(string) if !ok { - return nil, newPayloadErr(e.Type, "Value", e.Value) + // this is optional + context = "" + } + + notification, ok := e.Value.(string) + if !ok { + return "", "", newPayloadErr(e.Type, "Value", e.Value) } - return &result, nil + return context, notification, nil } func ParseUpdateEolDatabase(e partybus.Event) (progress.StagedProgressable, error) { diff --git a/xeol/lib.go b/xeol/lib.go index 424023fb..a0b58e11 100644 --- a/xeol/lib.go +++ b/xeol/lib.go @@ -3,11 +3,8 @@ package xeol import ( "time" - "github.com/anchore/go-logger" "github.com/anchore/syft/syft/linux" - "github.com/wagoodman/go-partybus" - "github.com/xeol-io/xeol/internal/bus" "github.com/xeol-io/xeol/internal/log" "github.com/xeol-io/xeol/xeol/db" "github.com/xeol-io/xeol/xeol/match" @@ -17,10 +14,6 @@ import ( "github.com/xeol-io/xeol/xeol/xeolerr" ) -func SetLogger(logger logger.Logger) { - log.Log = logger -} - func FindEol(store store.Store, d *linux.Release, matchers []matcher.Matcher, packages []pkg.Package, failOnEolFound bool, eolMatchDate time.Time) (match.Matches, error) { matches := matcher.FindMatches(store, d, matchers, packages, failOnEolFound, eolMatchDate) var err error @@ -30,10 +23,6 @@ func FindEol(store store.Store, d *linux.Release, matchers []matcher.Matcher, pa return matches, err } -func SetBus(b *partybus.Bus) { - bus.SetPublisher(b) -} - func LoadEolDB(cfg db.Config, update bool) (*store.Store, *db.Status, *db.Closer, error) { dbCurator, err := db.NewCurator(cfg) if err != nil { diff --git a/xeol/matcher/distro/matcher.go b/xeol/matcher/distro/matcher.go index dba09bb7..a50def41 100644 --- a/xeol/matcher/distro/matcher.go +++ b/xeol/matcher/distro/matcher.go @@ -11,16 +11,16 @@ import ( ) type Matcher struct { - UseCpes bool + UseCPEs bool } type MatcherConfig struct { - UseCpes bool + UseCPEs bool } func NewPackageMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ - UseCpes: cfg.UseCpes, + UseCPEs: cfg.UseCPEs, } } @@ -28,6 +28,6 @@ func (m *Matcher) Type() match.MatcherType { return match.PackageMatcher } -func (m *Matcher) Match(store eol.Provider, d *linux.Release, eolMatchDate time.Time) (match.Match, error) { +func (m *Matcher) Match(store eol.Provider, d *linux.Release, eolMatchDate time.Time) (match.Match, string, error) { return search.ByDistroCpe(store, d, eolMatchDate) } diff --git a/xeol/matcher/distro/matcher_test.go b/xeol/matcher/distro/matcher_test.go index 0a374e87..e891f9fa 100644 --- a/xeol/matcher/distro/matcher_test.go +++ b/xeol/matcher/distro/matcher_test.go @@ -71,7 +71,7 @@ func TestMatch(t *testing.T) { Cycle: *cycleFound, Package: p, } - actual, err := m.Match(provider, d, time.Now()) + actual, _, err := m.Match(provider, d, time.Now()) assert.NoError(t, err) assertMatches(t, expected, actual) } @@ -101,7 +101,7 @@ func TestMatchCpeMismatch(t *testing.T) { CPEName: "cpe:/o:fedoraproject:fedora:29", } - actual, err := m.Match(provider, d, time.Now()) + actual, _, err := m.Match(provider, d, time.Now()) assert.NoError(t, err) assertMatches(t, match.Match{}, actual) } @@ -133,7 +133,7 @@ func TestMatchNoMatchingVersion(t *testing.T) { CPEName: "cpe:/o:fedoraproject:fedora:29", } - actual, err := m.Match(provider, d, time.Now()) + actual, _, err := m.Match(provider, d, time.Now()) assert.NoError(t, err) assertMatches(t, match.Match{}, actual) } @@ -167,7 +167,7 @@ func TestMatchTimeChange(t *testing.T) { eolMatchTime, err := time.Parse("2006-01-02", "2018-01-01") assert.NoError(t, err) - actual, err := m.Match(provider, d, eolMatchTime) + actual, _, err := m.Match(provider, d, eolMatchTime) assert.NoError(t, err) assertMatches(t, match.Match{}, actual) } diff --git a/xeol/matcher/matchers.go b/xeol/matcher/matchers.go index ecee58c9..700175a1 100644 --- a/xeol/matcher/matchers.go +++ b/xeol/matcher/matchers.go @@ -11,6 +11,7 @@ import ( "github.com/xeol-io/xeol/internal/log" "github.com/xeol-io/xeol/xeol/eol" "github.com/xeol-io/xeol/xeol/event" + "github.com/xeol-io/xeol/xeol/event/monitor" "github.com/xeol-io/xeol/xeol/match" distroMatcher "github.com/xeol-io/xeol/xeol/matcher/distro" pkgMatcher "github.com/xeol-io/xeol/xeol/matcher/packages" @@ -19,7 +20,6 @@ import ( type Monitor struct { PackagesProcessed progress.Monitorable - EolDiscovered progress.Monitorable } // Config contains values used by individual matcher structs for advanced configuration @@ -35,45 +35,64 @@ func NewDefaultMatchers(_ Config) []Matcher { } } -func trackMatcher() (*progress.Manual, *progress.Manual) { - packagesProcessed := progress.Manual{} - eolDiscovered := progress.Manual{} +type monitorWriter struct { + PackagesProcessed *progress.Manual + MatchesDiscovered *progress.Manual +} + +func newMonitor(pkgCount int) (monitorWriter, monitor.Matching) { + m := monitorWriter{ + PackagesProcessed: progress.NewManual(int64(pkgCount)), + MatchesDiscovered: progress.NewManual(-1), + } + + return m, monitor.Matching{ + PackagesProcessed: m.PackagesProcessed, + MatchesDiscovered: m.MatchesDiscovered, + } +} + +func trackMatcher(pkgCount int) *monitorWriter { + writer, reader := newMonitor(pkgCount) bus.Publish(partybus.Event{ - Type: event.EolScanningStarted, - Value: Monitor{ - PackagesProcessed: progress.Monitorable(&packagesProcessed), - EolDiscovered: progress.Monitorable(&eolDiscovered), - }, + Type: event.EolScanningStarted, + Value: reader, }) - return &packagesProcessed, &eolDiscovered + return &writer +} + +func (m *monitorWriter) SetCompleted() { + m.PackagesProcessed.SetCompleted() + m.MatchesDiscovered.SetCompleted() } func FindMatches(store interface { eol.Provider }, distro *linux.Release, _ []Matcher, packages []pkg.Package, _ bool, eolMatchDate time.Time) match.Matches { - // var err error res := match.NewMatches() defaultMatcher := &pkgMatcher.Matcher{ - UsePurls: true, + UsePURLs: true, } distroMatcher := &distroMatcher.Matcher{ - UseCpes: true, + UseCPEs: true, } - distroMatch, err := distroMatcher.Match(store, distro, eolMatchDate) + progressMonitor := trackMatcher(len(packages)) + defer progressMonitor.SetCompleted() + + distroMatch, distroCPE, err := distroMatcher.Match(store, distro, eolMatchDate) if err != nil { log.Warnf("matcher failed for distro=%s: %+v", distro, err) } if (distroMatch.Cycle != eol.Cycle{}) { - logDistroMatch(distro) + logDistroMatch(distroCPE) res.Add(distroMatch) + progressMonitor.MatchesDiscovered.Increment() } - packagesProcessed, eolDiscovered := trackMatcher() - for _, p := range packages { - packagesProcessed.Increment() + progressMonitor.PackagesProcessed.Increment() log.Debugf("searching for eol matches for pkg=%s", p) pkgMatch, err := defaultMatcher.Match(store, p, eolMatchDate) @@ -83,18 +102,15 @@ func FindMatches(store interface { if (pkgMatch.Cycle != eol.Cycle{}) { logPkgMatch(p) res.Add(pkgMatch) - eolDiscovered.Increment() + progressMonitor.MatchesDiscovered.Increment() } } - packagesProcessed.SetCompleted() - eolDiscovered.SetCompleted() - return res } -func logDistroMatch(d *linux.Release) { - log.Debugf("found eol match for distro cpe=%s \n", d.CPEName) +func logDistroMatch(distroCPE string) { + log.Debugf("found eol match for distro cpe=%s \n", distroCPE) } func logPkgMatch(p pkg.Package) { diff --git a/xeol/matcher/packages/matcher.go b/xeol/matcher/packages/matcher.go index 9a2cb9b3..9451fdc3 100644 --- a/xeol/matcher/packages/matcher.go +++ b/xeol/matcher/packages/matcher.go @@ -12,16 +12,16 @@ import ( ) type Matcher struct { - UsePurls bool + UsePURLs bool } type MatcherConfig struct { - UsePurls bool + UsePURLs bool } func NewPackageMatcher(cfg MatcherConfig) *Matcher { return &Matcher{ - UsePurls: cfg.UsePurls, + UsePURLs: cfg.UsePURLs, } } diff --git a/xeol/pkg/context.go b/xeol/pkg/context.go index 4a2e65b5..5f46a6f9 100644 --- a/xeol/pkg/context.go +++ b/xeol/pkg/context.go @@ -6,6 +6,6 @@ import ( ) type Context struct { - Source *source.Metadata + Source *source.Description Distro *linux.Release } diff --git a/xeol/pkg/package.go b/xeol/pkg/package.go index 1ecfdf64..7542c5bb 100644 --- a/xeol/pkg/package.go +++ b/xeol/pkg/package.go @@ -11,8 +11,8 @@ import ( "github.com/anchore/syft/syft/pkg" cpes "github.com/anchore/syft/syft/pkg/cataloger/common/cpe" - "github.com/xeol-io/xeol/internal" "github.com/xeol-io/xeol/internal/log" + "github.com/xeol-io/xeol/internal/stringutil" ) // the source-rpm field has something akin to "util-linux-ng-2.17.2-12.28.el6_9.2.src.rpm" @@ -78,7 +78,6 @@ func FromCollection(catalog *pkg.Collection, config SynthesisConfig) []Package { func FromPackages(syftpkgs []pkg.Package, config SynthesisConfig) []Package { var pkgs []Package - var missingCPEs bool for _, p := range syftpkgs { if len(p.CPEs) == 0 { // For SPDX (or any format, really) we may have no CPEs @@ -86,14 +85,10 @@ func FromPackages(syftpkgs []pkg.Package, config SynthesisConfig) []Package { p.CPEs = cpes.Generate(p) } else { log.Debugf("no CPEs for package: %s", p) - missingCPEs = true } } pkgs = append(pkgs, New(p)) } - if missingCPEs { - log.Warnf("some package(s) are missing CPEs. This may result in missing vulnerabilities. You may autogenerate these using: --add-cpes-if-none") - } return pkgs } @@ -111,7 +106,6 @@ func removePackagesByOverlap(catalog *pkg.Collection, relationships []artifact.R } out := pkg.NewCollection() - for p := range catalog.Enumerate() { r, ok := byOverlap[p.ID()] if ok { @@ -223,7 +217,7 @@ func rpmDataFromPkg(p pkg.Package) (metadata *RpmMetadata, upstreams []UpstreamP } func getNameAndELVersion(sourceRpm string) (string, string) { - groupMatches := internal.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm) + groupMatches := stringutil.MatchCaptureGroups(rpmPackageNamePattern, sourceRpm) version := groupMatches["version"] + "-" + groupMatches["release"] return groupMatches["name"], version } diff --git a/xeol/pkg/package_test.go b/xeol/pkg/package_test.go index a2ecd241..a5b9f583 100644 --- a/xeol/pkg/package_test.go +++ b/xeol/pkg/package_test.go @@ -49,7 +49,7 @@ func TestNew(t *testing.T) { Files: []syftPkg.DpkgFileRecord{ { Path: "path-info", - Digest: &syftFile.Digest{ + Digest: &file.Digest{ Algorithm: "algo-info", Value: "digest-info", }, @@ -83,7 +83,7 @@ func TestNew(t *testing.T) { Path: "path-info", Mode: 20, Size: 10, - Digest: syftFile.Digest{ + Digest: file.Digest{ Algorithm: "algo-info", Value: "digest-info", }, @@ -324,7 +324,6 @@ func TestNew(t *testing.T) { }, }, }, - {}, { name: "cpp conan lock metadata", syftPkg: syftPkg.Package{ @@ -335,7 +334,7 @@ func TestNew(t *testing.T) { "fPIC": "True", "shared": "False", }, - Path: "all/conansyftFile.py", + Path: "all/conanfile.py", Context: "host", }, }, @@ -420,7 +419,7 @@ func TestNew(t *testing.T) { Extras: []string{"a"}, VersionConstraint: "a", URL: "a", - Markers: map[string]string{"a": "a"}, + Markers: "a", }, }, }, @@ -437,15 +436,6 @@ func TestNew(t *testing.T) { }, }, }, - { - name: "nix-store-metadata", - syftPkg: syftPkg.Package{ - MetadataType: syftPkg.NixStoreMetadataType, - Metadata: syftPkg.NixStoreMetadata{ - Files: []string{}, - }, - }, - }, { name: "nix-store-metadata", syftPkg: syftPkg.Package{ @@ -520,6 +510,30 @@ func TestNew(t *testing.T) { }, }, }, + { + name: "dotnet-portable-executable-metadata", + syftPkg: syftPkg.Package{ + MetadataType: syftPkg.DotnetPortableExecutableMetadataType, + Metadata: syftPkg.DotnetPortableExecutableMetadata{ + AssemblyVersion: "a", + LegalCopyright: "a", + Comments: "a", + InternalName: "a", + CompanyName: "a", + ProductName: "a", + ProductVersion: "a", + }, + }, + }, + { + name: "dotnet-portable-executable-metadata", + syftPkg: syftPkg.Package{ + MetadataType: syftPkg.SwiftPackageManagerMetadataType, + Metadata: syftPkg.SwiftPackageManagerMetadata{ + Revision: "a", + }, + }, + }, } // capture each observed metadata type, we should see all of them relate to what syft provides by the end of testing diff --git a/xeol/pkg/syft_provider.go b/xeol/pkg/syft_provider.go index 8b541ba2..5f028b6e 100644 --- a/xeol/pkg/syft_provider.go +++ b/xeol/pkg/syft_provider.go @@ -1,26 +1,27 @@ package pkg import ( + "github.com/anchore/stereoscope/pkg/image" "github.com/anchore/syft/syft" "github.com/anchore/syft/syft/sbom" "github.com/anchore/syft/syft/source" + + "github.com/xeol-io/xeol/internal/log" ) func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, *sbom.SBOM, error) { - if config.CatalogingOptions.Search.Scope == "" { - return nil, Context{}, nil, errDoesNotProvide - } - - sourceInput, err := source.ParseInputWithName(userInput, config.Platform, config.Name, config.DefaultImagePullSource) + src, err := getSource(userInput, config) if err != nil { return nil, Context{}, nil, err } - src, cleanup, err := source.New(*sourceInput, config.RegistryOptions, config.Exclusions) - if err != nil { - return nil, Context{}, nil, err - } - defer cleanup() + defer func() { + if src != nil { + if err := src.Close(); err != nil { + log.Tracef("unable to close source: %+v", err) + } + } + }() catalog, relationships, theDistro, err := syft.CatalogPackages(src, config.CatalogingOptions) if err != nil { @@ -29,14 +30,16 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, catalog = removePackagesByOverlap(catalog, relationships) + srcDescription := src.Describe() + packages := FromCollection(catalog, config.SynthesisConfig) context := Context{ - Source: &src.Metadata, + Source: &srcDescription, Distro: theDistro, } sbom := &sbom.SBOM{ - Source: src.Metadata, + Source: srcDescription, Relationships: relationships, Artifacts: sbom.Artifacts{ Packages: catalog, @@ -45,3 +48,35 @@ func syftProvider(userInput string, config ProviderConfig) ([]Package, Context, return packages, context, sbom, nil } + +func getSource(userInput string, config ProviderConfig) (source.Source, error) { + if config.CatalogingOptions.Search.Scope == "" { + return nil, errDoesNotProvide + } + + detection, err := source.Detect(userInput, source.DetectConfig{ + DefaultImageSource: config.DefaultImagePullSource, + }) + if err != nil { + return nil, err + } + + var platform *image.Platform + if config.Platform != "" { + platform, err = image.NewPlatform(config.Platform) + if err != nil { + return nil, err + } + } + + return detection.NewSource(source.DetectionSourceConfig{ + Alias: source.Alias{ + Name: config.Name, + }, + RegistryOptions: config.RegistryOptions, + Platform: platform, + Exclude: source.ExcludeConfig{ + Paths: config.Exclusions, + }, + }) +} diff --git a/xeol/pkg/syft_sbom_provider.go b/xeol/pkg/syft_sbom_provider.go index a75ed073..14903183 100644 --- a/xeol/pkg/syft_sbom_provider.go +++ b/xeol/pkg/syft_sbom_provider.go @@ -30,8 +30,7 @@ func syftSBOMProvider(userInput string, config ProviderConfig) ([]Package, Conte return nil, Context{}, nil, err } - catalog := s.Artifacts.Packages - catalog = removePackagesByOverlap(catalog, s.Relationships) + catalog := removePackagesByOverlap(s.Artifacts.Packages, s.Relationships) return FromCollection(catalog, config.SynthesisConfig), Context{ Source: &s.Source, @@ -142,13 +141,13 @@ func fileHasContent(f *os.File) bool { } func stdinReader() io.Reader { - isPipedInput, err := internal.IsPipedInput() + isStdinPipeOrRedirect, err := internal.IsStdinPipeOrRedirect() if err != nil { log.Warnf("unable to determine if there is piped input: %+v", err) return nil } - if !isPipedInput { + if !isStdinPipeOrRedirect { return nil } diff --git a/xeol/pkg/syft_sbom_provider_test.go b/xeol/pkg/syft_sbom_provider_test.go index e3da41d1..91cc080d 100644 --- a/xeol/pkg/syft_sbom_provider_test.go +++ b/xeol/pkg/syft_sbom_provider_test.go @@ -1,12 +1,12 @@ package pkg import ( - "errors" "os" "strings" "testing" "github.com/anchore/syft/syft/cpe" + "github.com/anchore/syft/syft/file" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" "github.com/go-test/deep" @@ -14,12 +14,6 @@ import ( "github.com/stretchr/testify/require" ) -func assertAs(expected string) assert.ErrorAssertionFunc { - return func(t assert.TestingT, err error, i ...interface{}) bool { - return assert.ErrorContains(t, errors.New(expected), err.Error()) - } -} - func TestParseSyftJSON(t *testing.T) { tests := []struct { Fixture string @@ -32,8 +26,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "alpine-baselayout", Version: "3.2.0-r6", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:8d3ac3489996423f53d6087c81180006263b79f206d3fdec9e66f0e27ceb8759", }), @@ -56,8 +50,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "fake", Version: "1.2.0", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), @@ -82,8 +76,8 @@ func TestParseSyftJSON(t *testing.T) { { Name: "gmp", Version: "6.2.0-r0", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/lib/apk/db/installed", FileSystemID: "sha256:93cf4cfb673c7e16a9e74f731d6767b70b92a0b7c9f59d06efd72fbff535371c", }), @@ -107,11 +101,10 @@ func TestParseSyftJSON(t *testing.T) { }, }, Context: Context{ - Source: &source.Metadata{ - Scheme: source.ImageScheme, - ImageMetadata: source.ImageMetadata{ + Source: &source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ UserInput: "alpine:fake", - Layers: []source.LayerMetadata{ + Layers: []source.StereoscopeLayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:50644c29ef5a27c9a40c393a73ece2479de78325cae7d762ef3cdc19bf42dd0a", @@ -126,7 +119,6 @@ func TestParseSyftJSON(t *testing.T) { "alpine:fake", }, }, - Path: "", }, Distro: &linux.Release{ Name: "alpine", @@ -144,8 +136,12 @@ func TestParseSyftJSON(t *testing.T) { t.Fatalf("unable to parse: %+v", err) } - context.Source.ImageMetadata.RawConfig = nil - context.Source.ImageMetadata.RawManifest = nil + if m, ok := context.Source.Metadata.(source.StereoscopeImageSourceMetadata); ok { + m.RawConfig = nil + m.RawManifest = nil + + context.Source.Metadata = m + } for _, d := range deep.Equal(test.Packages, pkgs) { if strings.Contains(d, ".ID: ") { @@ -185,8 +181,8 @@ var springImageTestCase = struct { { Name: "charsets", Version: "", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/usr/lib/jvm/java-8-openjdk-amd64/jre/lib/charsets.jar", FileSystemID: "sha256:a1a6ceadb701ab4e6c93b243dc2a0daedc8cee23a24203845ecccd5784cd1393", }), @@ -205,8 +201,8 @@ var springImageTestCase = struct { { Name: "tomcat-embed-el", Version: "9.0.27", - Locations: source.NewLocationSet( - source.NewLocationFromCoordinates(source.Coordinates{ + Locations: file.NewLocationSet( + file.NewLocationFromCoordinates(file.Coordinates{ RealPath: "/app/libs/tomcat-embed-el-9.0.27.jar", FileSystemID: "sha256:89504f083d3f15322f97ae240df44650203f24427860db1b3d32e66dd05940e4", }), @@ -224,11 +220,10 @@ var springImageTestCase = struct { }, }, Context: Context{ - Source: &source.Metadata{ - Scheme: source.ImageScheme, - ImageMetadata: source.ImageMetadata{ + Source: &source.Description{ + Metadata: source.StereoscopeImageSourceMetadata{ UserInput: "springio/gs-spring-boot-docker:latest", - Layers: []source.LayerMetadata{ + Layers: []source.StereoscopeLayerMetadata{ { MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", Digest: "sha256:42a3027eaac150d2b8f516100921f4bd83b3dbc20bfe64124f686c072b49c602", @@ -244,7 +239,6 @@ var springImageTestCase = struct { }, RepoDigests: []string{"springio/gs-spring-boot-docker@sha256:39c2ffc784f5f34862e22c1f2ccdbcb62430736114c13f60111eabdb79decb08"}, }, - Path: "", }, Distro: &linux.Release{ Name: "debian", diff --git a/xeol/presenter/internal/test_helpers.go b/xeol/presenter/internal/test_helpers.go new file mode 100644 index 00000000..b517f1b5 --- /dev/null +++ b/xeol/presenter/internal/test_helpers.go @@ -0,0 +1,245 @@ +package internal + +import ( + "regexp" + "testing" + + "github.com/anchore/stereoscope/pkg/image" + "github.com/anchore/syft/syft/file" + "github.com/anchore/syft/syft/linux" + syftPkg "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/sbom" + syftSource "github.com/anchore/syft/syft/source" + "github.com/google/uuid" + + "github.com/xeol-io/xeol/xeol/eol" + "github.com/xeol-io/xeol/xeol/match" + "github.com/xeol-io/xeol/xeol/pkg" +) + +const ( + DirectorySource SyftSource = "directory" + ImageSource SyftSource = "image" + FileSource SyftSource = "file" +) + +type SyftSource string + +func GenerateAnalysis(t *testing.T, scheme SyftSource) (match.Matches, []pkg.Package, pkg.Context, interface{}, interface{}) { + t.Helper() + + packages := generatePackages(t) + matches := generateMatches(t, packages[0], packages[1]) + context := generateContext(t, scheme) + + return matches, packages, context, nil, nil +} + +func SBOMFromPackages(t *testing.T, packages []pkg.Package) *sbom.SBOM { + t.Helper() + + sbom := &sbom.SBOM{ + Artifacts: sbom.Artifacts{ + Packages: syftPkg.NewCollection(), + }, + } + + for _, p := range packages { + sbom.Artifacts.Packages.Add(toSyftPkg(p)) + } + + return sbom +} + +func toSyftPkg(p pkg.Package) syftPkg.Package { + return syftPkg.Package{ + Name: p.Name, + Version: p.Version, + Type: p.Type, + Metadata: p.Metadata, + Locations: p.Locations, + CPEs: p.CPEs, + } +} + +func Redact(s []byte) []byte { + serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`) + uuidPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) + refPattern := regexp.MustCompile(`ref="[a-zA-Z0-9\-:]+"`) + rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`) + cycloneDxBomRefPattern := regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) + + for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern} { + s = pattern.ReplaceAll(s, []byte("redacted")) + } + return s +} + +func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches { + t.Helper() + + matches := []match.Match{ + { + Cycle: eol.Cycle{ + ProductName: "MongoDB Server", + ReleaseDate: "2018-07-31", + ReleaseCycle: "3.2", + Eol: "2018-07-31", + EolBool: false, + LatestReleaseDate: "2018-07-31", + }, + Package: p, + }, + { + Cycle: eol.Cycle{ + ProductName: "MongoDB Server", + ReleaseDate: "2016-07-31", + ReleaseCycle: "2.8", + Eol: "0001-01-01", + EolBool: true, + LatestReleaseDate: "2016-07-31", + }, + Package: p2, + }, + { + Cycle: eol.Cycle{ + ProductName: "Ubuntu", + ReleaseDate: "2016-07-31", + ReleaseCycle: "16.04", + Eol: "2021-04-02", + EolBool: false, + LatestReleaseDate: "2016-07-31", + }, + Package: pkg.Package{ + ID: pkg.ID(uuid.NewString()), + Name: "ubuntu", + Version: "16.04", + Type: "os", + }, + }, + } + + collection := match.NewMatches(matches...) + + return collection +} + +func generatePackages(t *testing.T) []pkg.Package { + t.Helper() + epoch := 2 + return []pkg.Package{ + { + ID: pkg.ID(uuid.NewString()), + Name: "package-1", + Version: "1.1.1", + Type: syftPkg.RpmPkg, + Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-1.txt", "somefile-1.txt")), + Upstreams: []pkg.UpstreamPackage{ + { + Name: "nothing", + Version: "3.2", + }, + }, + MetadataType: pkg.RpmMetadataType, + Metadata: pkg.RpmMetadata{ + Epoch: &epoch, + }, + }, + { + ID: pkg.ID(uuid.NewString()), + Name: "package-2", + Version: "2.2.2", + Type: syftPkg.DebPkg, + Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-2.txt", "somefile-2.txt")), + Licenses: []string{"MIT", "Apache-2.0"}, + }, + } +} + +//nolint:funlen +func generateContext(t *testing.T, scheme SyftSource) pkg.Context { + var ( + src syftSource.Source + desc syftSource.Description + ) + + switch scheme { + case FileSource: + var err error + src, err = syftSource.NewFromFile(syftSource.FileConfig{ + Path: "user-input", + }) + if err != nil { + t.Fatalf("failed to generate mock file source from mock image: %+v", err) + } + desc = src.Describe() + case ImageSource: + img := image.Image{ + Metadata: image.Metadata{ + ID: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", + ManifestDigest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", + MediaType: "application/vnd.docker.distribution.manifest.v2+json", + Size: 65, + }, + Layers: []*image.Layer{ + { + Metadata: image.LayerMetadata{ + Digest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 22, + }, + }, + { + Metadata: image.LayerMetadata{ + Digest: "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 16, + }, + }, + { + Metadata: image.LayerMetadata{ + Digest: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", + MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", + Size: 27, + }, + }, + }, + } + + var err error + src, err = syftSource.NewFromStereoscopeImageObject(&img, "user-input", nil) + if err != nil { + t.Fatalf("failed to generate mock image source from mock image: %+v", err) + } + desc = src.Describe() + case DirectorySource: + // note: the dir must exist for the source to be created + d := t.TempDir() + var err error + src, err = syftSource.NewFromDirectory(syftSource.DirectoryConfig{ + Path: d, + }) + + if err != nil { + t.Fatalf("failed to generate mock directory source from mock dir: %+v", err) + } + desc = src.Describe() + if m, ok := desc.Metadata.(syftSource.DirectorySourceMetadata); ok { + m.Path = "/some/path" + desc.Metadata = m + } + default: + t.Fatalf("unknown scheme: %s", scheme) + } + + return pkg.Context{ + Source: &desc, + Distro: &linux.Release{ + Name: "centos", + IDLike: []string{ + "centos", + }, + Version: "8.0", + }, + } +} diff --git a/xeol/presenter/json/__snapshots__/presenter_test.snap b/xeol/presenter/json/__snapshots__/presenter_test.snap new file mode 100755 index 00000000..8ce55396 --- /dev/null +++ b/xeol/presenter/json/__snapshots__/presenter_test.snap @@ -0,0 +1,12 @@ + +[TestJsonImgsPresenter - 1] +[]uint8{0x7b, 0xa, 0x20, 0x22, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x4d, 0x6f, 0x6e, 0x67, 0x6f, 0x44, 0x42, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x2e, 0x38, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x45, 0x6f, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x74, 0x72, 0x75, 0x65, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x49, 0x44, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x43, 0x50, 0x45, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x55, 0x52, 0x4c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2d, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x64, 0x65, 0x62, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x3a, 0x20, 0x22, 0x2f, 0x66, 0x6f, 0x6f, 0x2f, 0x62, 0x61, 0x72, 0x2f, 0x73, 0x6f, 0x6d, 0x65, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x32, 0x2e, 0x74, 0x78, 0x74, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x49, 0x54, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, 0x30, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0xa, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x4d, 0x6f, 0x6e, 0x67, 0x6f, 0x44, 0x42, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x33, 0x2e, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x45, 0x6f, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x49, 0x44, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x43, 0x50, 0x45, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x55, 0x52, 0x4c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2d, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x2e, 0x31, 0x2e, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x72, 0x70, 0x6d, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x3a, 0x20, 0x22, 0x2f, 0x66, 0x6f, 0x6f, 0x2f, 0x62, 0x61, 0x72, 0x2f, 0x73, 0x6f, 0x6d, 0x65, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x31, 0x2e, 0x74, 0x78, 0x74, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x33, 0x2e, 0x32, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x52, 0x70, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x22, 0x3a, 0x20, 0x32, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x55, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x36, 0x2e, 0x30, 0x34, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x45, 0x6f, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x32, 0x31, 0x2d, 0x30, 0x34, 0x2d, 0x30, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x49, 0x44, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x43, 0x50, 0x45, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x55, 0x52, 0x4c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x36, 0x2e, 0x30, 0x34, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x73, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0xa, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x22, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x75, 0x73, 0x65, 0x72, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x75, 0x73, 0x65, 0x72, 0x2d, 0x69, 0x6e, 0x70, 0x75, 0x74, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x49, 0x44, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x3a, 0x61, 0x62, 0x35, 0x36, 0x30, 0x38, 0x64, 0x36, 0x33, 0x34, 0x64, 0x62, 0x32, 0x37, 0x31, 0x36, 0x61, 0x32, 0x39, 0x37, 0x61, 0x64, 0x62, 0x66, 0x61, 0x36, 0x61, 0x35, 0x64, 0x64, 0x35, 0x64, 0x38, 0x66, 0x38, 0x66, 0x35, 0x61, 0x37, 0x64, 0x30, 0x63, 0x61, 0x62, 0x37, 0x33, 0x36, 0x34, 0x39, 0x65, 0x61, 0x37, 0x66, 0x62, 0x62, 0x38, 0x63, 0x38, 0x64, 0x61, 0x35, 0x34, 0x34, 0x66, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x3a, 0x63, 0x61, 0x37, 0x33, 0x38, 0x61, 0x62, 0x62, 0x38, 0x37, 0x61, 0x38, 0x64, 0x35, 0x38, 0x66, 0x31, 0x31, 0x32, 0x64, 0x33, 0x34, 0x30, 0x30, 0x65, 0x62, 0x62, 0x30, 0x37, 0x39, 0x62, 0x36, 0x31, 0x63, 0x65, 0x61, 0x65, 0x37, 0x64, 0x63, 0x32, 0x39, 0x30, 0x62, 0x65, 0x62, 0x33, 0x34, 0x62, 0x64, 0x61, 0x37, 0x33, 0x35, 0x62, 0x65, 0x34, 0x62, 0x31, 0x39, 0x34, 0x31, 0x64, 0x35, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64, 0x2e, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x2e, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x2e, 0x76, 0x32, 0x2b, 0x6a, 0x73, 0x6f, 0x6e, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x74, 0x61, 0x67, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x22, 0x3a, 0x20, 0x36, 0x35, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x61, 0x79, 0x65, 0x72, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64, 0x2e, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x2e, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x66, 0x73, 0x2e, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x74, 0x61, 0x72, 0x2e, 0x67, 0x7a, 0x69, 0x70, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x3a, 0x63, 0x61, 0x37, 0x33, 0x38, 0x61, 0x62, 0x62, 0x38, 0x37, 0x61, 0x38, 0x64, 0x35, 0x38, 0x66, 0x31, 0x31, 0x32, 0x64, 0x33, 0x34, 0x30, 0x30, 0x65, 0x62, 0x62, 0x30, 0x37, 0x39, 0x62, 0x36, 0x31, 0x63, 0x65, 0x61, 0x65, 0x37, 0x64, 0x63, 0x32, 0x39, 0x30, 0x62, 0x65, 0x62, 0x33, 0x34, 0x62, 0x64, 0x61, 0x37, 0x33, 0x35, 0x62, 0x65, 0x34, 0x62, 0x31, 0x39, 0x34, 0x31, 0x64, 0x35, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x3a, 0x20, 0x32, 0x32, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64, 0x2e, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x2e, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x66, 0x73, 0x2e, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x74, 0x61, 0x72, 0x2e, 0x67, 0x7a, 0x69, 0x70, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x3a, 0x61, 0x30, 0x35, 0x63, 0x64, 0x39, 0x65, 0x62, 0x66, 0x38, 0x38, 0x61, 0x66, 0x39, 0x36, 0x34, 0x35, 0x30, 0x66, 0x31, 0x65, 0x32, 0x35, 0x33, 0x36, 0x37, 0x32, 0x38, 0x31, 0x61, 0x62, 0x32, 0x33, 0x32, 0x61, 0x63, 0x30, 0x36, 0x34, 0x35, 0x66, 0x33, 0x31, 0x34, 0x31, 0x32, 0x34, 0x66, 0x65, 0x30, 0x31, 0x61, 0x66, 0x37, 0x35, 0x39, 0x62, 0x39, 0x33, 0x66, 0x33, 0x30, 0x30, 0x36, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x3a, 0x20, 0x31, 0x36, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x64, 0x69, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x76, 0x6e, 0x64, 0x2e, 0x64, 0x6f, 0x63, 0x6b, 0x65, 0x72, 0x2e, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2e, 0x72, 0x6f, 0x6f, 0x74, 0x66, 0x73, 0x2e, 0x64, 0x69, 0x66, 0x66, 0x2e, 0x74, 0x61, 0x72, 0x2e, 0x67, 0x7a, 0x69, 0x70, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x3a, 0x61, 0x62, 0x35, 0x36, 0x30, 0x38, 0x64, 0x36, 0x33, 0x34, 0x64, 0x62, 0x32, 0x37, 0x31, 0x36, 0x61, 0x32, 0x39, 0x37, 0x61, 0x64, 0x62, 0x66, 0x61, 0x36, 0x61, 0x35, 0x64, 0x64, 0x35, 0x64, 0x38, 0x66, 0x38, 0x66, 0x35, 0x61, 0x37, 0x64, 0x30, 0x63, 0x61, 0x62, 0x37, 0x33, 0x36, 0x34, 0x39, 0x65, 0x61, 0x37, 0x66, 0x62, 0x62, 0x38, 0x63, 0x38, 0x64, 0x61, 0x35, 0x34, 0x34, 0x66, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x73, 0x69, 0x7a, 0x65, 0x22, 0x3a, 0x20, 0x32, 0x37, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x61, 0x6e, 0x69, 0x66, 0x65, 0x73, 0x74, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x63, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x72, 0x65, 0x70, 0x6f, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x63, 0x68, 0x69, 0x74, 0x65, 0x63, 0x74, 0x75, 0x72, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x6f, 0x73, 0x22, 0x3a, 0x20, 0x22, 0x22, 0xa, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x22, 0x64, 0x69, 0x73, 0x74, 0x72, 0x6f, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x63, 0x65, 0x6e, 0x74, 0x6f, 0x73, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x38, 0x2e, 0x30, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x69, 0x64, 0x4c, 0x69, 0x6b, 0x65, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x63, 0x65, 0x6e, 0x74, 0x6f, 0x73, 0x22, 0xa, 0x20, 0x20, 0x5d, 0xa, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x78, 0x65, 0x6f, 0x6c, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x5b, 0x6e, 0x6f, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x5d, 0x22, 0xa, 0x20, 0x7d, 0xa, 0x7d, 0xa} +--- + +[TestJsonDirsPresenter - 1] +[]uint8{0x7b, 0xa, 0x20, 0x22, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x4d, 0x6f, 0x6e, 0x67, 0x6f, 0x44, 0x42, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x2e, 0x38, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x45, 0x6f, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x74, 0x72, 0x75, 0x65, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x49, 0x44, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x43, 0x50, 0x45, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x55, 0x52, 0x4c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2d, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x64, 0x65, 0x62, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x3a, 0x20, 0x22, 0x2f, 0x66, 0x6f, 0x6f, 0x2f, 0x62, 0x61, 0x72, 0x2f, 0x73, 0x6f, 0x6d, 0x65, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x32, 0x2e, 0x74, 0x78, 0x74, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x49, 0x54, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, 0x30, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0xa, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x4d, 0x6f, 0x6e, 0x67, 0x6f, 0x44, 0x42, 0x20, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x33, 0x2e, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x45, 0x6f, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x49, 0x44, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x43, 0x50, 0x45, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x55, 0x52, 0x4c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2d, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x2e, 0x31, 0x2e, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x72, 0x70, 0x6d, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x61, 0x74, 0x68, 0x22, 0x3a, 0x20, 0x22, 0x2f, 0x66, 0x6f, 0x6f, 0x2f, 0x62, 0x61, 0x72, 0x2f, 0x73, 0x6f, 0x6d, 0x65, 0x66, 0x69, 0x6c, 0x65, 0x2d, 0x31, 0x2e, 0x74, 0x78, 0x74, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6e, 0x6f, 0x74, 0x68, 0x69, 0x6e, 0x67, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x33, 0x2e, 0x32, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x52, 0x70, 0x6d, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x65, 0x70, 0x6f, 0x63, 0x68, 0x22, 0x3a, 0x20, 0x32, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6d, 0x6f, 0x64, 0x75, 0x6c, 0x61, 0x72, 0x69, 0x74, 0x79, 0x4c, 0x61, 0x62, 0x65, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0xa, 0x20, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x55, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x50, 0x65, 0x72, 0x6d, 0x61, 0x6c, 0x69, 0x6e, 0x6b, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x43, 0x79, 0x63, 0x6c, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x36, 0x2e, 0x30, 0x34, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x45, 0x6f, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x32, 0x31, 0x2d, 0x30, 0x34, 0x2d, 0x30, 0x32, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x74, 0x65, 0x73, 0x74, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x52, 0x65, 0x6c, 0x65, 0x61, 0x73, 0x65, 0x44, 0x61, 0x74, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x32, 0x30, 0x31, 0x36, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x22, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x50, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x49, 0x44, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x7b, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x43, 0x50, 0x45, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x50, 0x55, 0x52, 0x4c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x55, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x3a, 0x20, 0x6e, 0x75, 0x6c, 0x6c, 0xa, 0x20, 0x20, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x22, 0x61, 0x72, 0x74, 0x69, 0x66, 0x61, 0x63, 0x74, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x31, 0x36, 0x2e, 0x30, 0x34, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x6f, 0x73, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x6f, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x6c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x63, 0x70, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x70, 0x75, 0x72, 0x6c, 0x22, 0x3a, 0x20, 0x22, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x20, 0x20, 0x22, 0x75, 0x70, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0xa, 0x20, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x20, 0x7d, 0xa, 0x20, 0x5d, 0x2c, 0xa, 0x20, 0x22, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x79, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x2f, 0x73, 0x6f, 0x6d, 0x65, 0x2f, 0x70, 0x61, 0x74, 0x68, 0x22, 0xa, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x22, 0x64, 0x69, 0x73, 0x74, 0x72, 0x6f, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x63, 0x65, 0x6e, 0x74, 0x6f, 0x73, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x38, 0x2e, 0x30, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x69, 0x64, 0x4c, 0x69, 0x6b, 0x65, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x63, 0x65, 0x6e, 0x74, 0x6f, 0x73, 0x22, 0xa, 0x20, 0x20, 0x5d, 0xa, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x78, 0x65, 0x6f, 0x6c, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x5b, 0x6e, 0x6f, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x5d, 0x22, 0xa, 0x20, 0x7d, 0xa, 0x7d, 0xa} +--- + +[TestEmptyJsonPresenter - 1] +[]uint8{0x7b, 0xa, 0x20, 0x22, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x22, 0x3a, 0x20, 0x5b, 0x5d, 0x2c, 0xa, 0x20, 0x22, 0x73, 0x6f, 0x75, 0x72, 0x63, 0x65, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x74, 0x79, 0x70, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x74, 0x61, 0x72, 0x67, 0x65, 0x74, 0x22, 0x3a, 0x20, 0x22, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x22, 0xa, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x22, 0x64, 0x69, 0x73, 0x74, 0x72, 0x6f, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x63, 0x65, 0x6e, 0x74, 0x6f, 0x73, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x38, 0x2e, 0x30, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x69, 0x64, 0x4c, 0x69, 0x6b, 0x65, 0x22, 0x3a, 0x20, 0x5b, 0xa, 0x20, 0x20, 0x20, 0x22, 0x72, 0x68, 0x65, 0x6c, 0x22, 0xa, 0x20, 0x20, 0x5d, 0xa, 0x20, 0x7d, 0x2c, 0xa, 0x20, 0x22, 0x64, 0x65, 0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x6f, 0x72, 0x22, 0x3a, 0x20, 0x7b, 0xa, 0x20, 0x20, 0x22, 0x6e, 0x61, 0x6d, 0x65, 0x22, 0x3a, 0x20, 0x22, 0x78, 0x65, 0x6f, 0x6c, 0x22, 0x2c, 0xa, 0x20, 0x20, 0x22, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x22, 0x3a, 0x20, 0x22, 0x5b, 0x6e, 0x6f, 0x74, 0x20, 0x70, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x64, 0x5d, 0x22, 0xa, 0x20, 0x7d, 0xa, 0x7d, 0xa} +--- diff --git a/xeol/presenter/json/presenter_test.go b/xeol/presenter/json/presenter_test.go index e3639a2b..8254dc21 100644 --- a/xeol/presenter/json/presenter_test.go +++ b/xeol/presenter/json/presenter_test.go @@ -2,24 +2,24 @@ package json import ( "bytes" - "flag" + "regexp" "testing" - "github.com/anchore/go-testutils" "github.com/anchore/syft/syft/linux" "github.com/anchore/syft/syft/source" - "github.com/stretchr/testify/assert" + "github.com/gkampitakis/go-snaps/snaps" "github.com/xeol-io/xeol/xeol/match" "github.com/xeol-io/xeol/xeol/pkg" + "github.com/xeol-io/xeol/xeol/presenter/internal" "github.com/xeol-io/xeol/xeol/presenter/models" ) -var update = flag.Bool("update", false, "update the *.golden files for json presenters") +var timestampRegexp = regexp.MustCompile(`"timestamp":\s*"[^"]+"`) func TestJsonImgsPresenter(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, _, _ := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, context, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) pb := models.PresenterConfig{ Matches: matches, @@ -34,13 +34,9 @@ func TestJsonImgsPresenter(t *testing.T) { t.Fatal(err) } actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } - - var expected = testutils.GetGoldenFileContents(t) + actual = redact(actual) - assert.JSONEq(t, string(expected), string(actual)) + snaps.MatchSnapshot(t, actual) // TODO: add me back in when there is a JSON schema // validateAgainstDbSchema(t, string(actual)) @@ -49,7 +45,7 @@ func TestJsonImgsPresenter(t *testing.T) { func TestJsonDirsPresenter(t *testing.T) { var buffer bytes.Buffer - matches, packages, context, _, _ := models.GenerateAnalysis(t, source.DirectoryScheme) + matches, packages, context, _, _ := internal.GenerateAnalysis(t, internal.DirectorySource) pb := models.PresenterConfig{ Matches: matches, @@ -64,15 +60,9 @@ func TestJsonDirsPresenter(t *testing.T) { t.Fatal(err) } actual := buffer.Bytes() + actual = redact(actual) - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } - - var expected = testutils.GetGoldenFileContents(t) - - assert.JSONEq(t, string(expected), string(actual)) - + snaps.MatchSnapshot(t, actual) // TODO: add me back in when there is a JSON schema // validateAgainstDbSchema(t, string(actual)) } @@ -84,7 +74,7 @@ func TestEmptyJsonPresenter(t *testing.T) { matches := match.NewMatches() ctx := pkg.Context{ - Source: &source.Metadata{}, + Source: &source.Description{}, Distro: &linux.Release{ ID: "centos", IDLike: []string{"rhel"}, @@ -105,11 +95,11 @@ func TestEmptyJsonPresenter(t *testing.T) { t.Fatal(err) } actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } + actual = redact(actual) - var expected = testutils.GetGoldenFileContents(t) + snaps.MatchSnapshot(t, actual) +} - assert.JSONEq(t, string(expected), string(actual)) +func redact(content []byte) []byte { + return timestampRegexp.ReplaceAll(content, []byte(`"timestamp":""`)) } diff --git a/xeol/presenter/models/document_test.go b/xeol/presenter/models/document_test.go index 75f9a345..8c3da741 100644 --- a/xeol/presenter/models/document_test.go +++ b/xeol/presenter/models/document_test.go @@ -68,9 +68,8 @@ func TestPackagesAreSorted(t *testing.T) { packages := []pkg.Package{pkg1, pkg2} ctx := pkg.Context{ - Source: &syftSource.Metadata{ - Scheme: syftSource.DirectoryScheme, - ImageMetadata: syftSource.ImageMetadata{}, + Source: &syftSource.Description{ + Metadata: syftSource.DirectorySourceMetadata{}, }, Distro: &linux.Release{ ID: "centos", diff --git a/xeol/presenter/models/match.go b/xeol/presenter/models/match.go index 5e0ebee1..2c1423a3 100644 --- a/xeol/presenter/models/match.go +++ b/xeol/presenter/models/match.go @@ -29,17 +29,17 @@ func newMatch(m match.Match, p pkg.Package) *Match { } } -var _ sort.Interface = (*ByName)(nil) +var _ sort.Interface = (*MatchSort)(nil) -type ByName []Match +type MatchSort []Match // Len is the number of elements in the collection. -func (m ByName) Len() int { +func (m MatchSort) Len() int { return len(m) } // Less reports whether the element with index i should sort before the element with index j. -func (m ByName) Less(i, j int) bool { +func (m MatchSort) Less(i, j int) bool { if m[i].Artifact.Name == m[j].Artifact.Name { if m[i].Cycle.ReleaseCycle == m[j].Cycle.ReleaseCycle { if m[i].Cycle.ProductName == m[j].Cycle.ProductName { @@ -56,6 +56,6 @@ func (m ByName) Less(i, j int) bool { } // Swap swaps the elements with indexes i and j. -func (m ByName) Swap(i, j int) { +func (m MatchSort) Swap(i, j int) { m[i], m[j] = m[j], m[i] } diff --git a/xeol/presenter/models/models_helpers.go b/xeol/presenter/models/models_helpers.go index 184289f4..2640e7f9 100644 --- a/xeol/presenter/models/models_helpers.go +++ b/xeol/presenter/models/models_helpers.go @@ -1,212 +1 @@ package models - -import ( - "regexp" - "testing" - - "github.com/anchore/stereoscope/pkg/image" - "github.com/anchore/syft/syft/file" - "github.com/anchore/syft/syft/linux" - syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/sbom" - syftSource "github.com/anchore/syft/syft/source" - "github.com/google/uuid" - - "github.com/xeol-io/xeol/xeol/eol" - "github.com/xeol-io/xeol/xeol/match" - "github.com/xeol-io/xeol/xeol/pkg" -) - -func GenerateAnalysis(t *testing.T, scheme syftSource.Scheme) (match.Matches, []pkg.Package, pkg.Context, interface{}, interface{}) { - t.Helper() - - packages := generatePackages(t) - matches := generateMatches(t, packages[0], packages[1]) - context := generateContext(t, scheme) - - return matches, packages, context, nil, nil -} - -func SBOMFromPackages(t *testing.T, packages []pkg.Package) *sbom.SBOM { - t.Helper() - - sbom := &sbom.SBOM{ - Artifacts: sbom.Artifacts{ - Packages: syftPkg.NewCollection(), - }, - } - - for _, p := range packages { - sbom.Artifacts.Packages.Add(toSyftPkg(p)) - } - - return sbom -} - -func toSyftPkg(p pkg.Package) syftPkg.Package { - return syftPkg.Package{ - Name: p.Name, - Version: p.Version, - Type: p.Type, - Metadata: p.Metadata, - Locations: p.Locations, - CPEs: p.CPEs, - } -} - -func Redact(s []byte) []byte { - serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`) - uuidPattern := regexp.MustCompile(`urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) - refPattern := regexp.MustCompile(`ref="[a-zA-Z0-9\-:]+"`) - rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`) - cycloneDxBomRefPattern := regexp.MustCompile(`[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}`) - - for _, pattern := range []*regexp.Regexp{serialPattern, rfc3339Pattern, refPattern, uuidPattern, cycloneDxBomRefPattern} { - s = pattern.ReplaceAll(s, []byte("redacted")) - } - return s -} - -func generateMatches(t *testing.T, p, p2 pkg.Package) match.Matches { - t.Helper() - - matches := []match.Match{ - { - Cycle: eol.Cycle{ - ProductName: "MongoDB Server", - ReleaseDate: "2018-07-31", - ReleaseCycle: "3.2", - Eol: "2018-07-31", - EolBool: false, - LatestReleaseDate: "2018-07-31", - }, - Package: p, - }, - { - Cycle: eol.Cycle{ - ProductName: "MongoDB Server", - ReleaseDate: "2016-07-31", - ReleaseCycle: "2.8", - Eol: "0001-01-01", - EolBool: true, - LatestReleaseDate: "2016-07-31", - }, - Package: p2, - }, - { - Cycle: eol.Cycle{ - ProductName: "Ubuntu", - ReleaseDate: "2016-07-31", - ReleaseCycle: "16.04", - Eol: "2021-04-02", - EolBool: false, - LatestReleaseDate: "2016-07-31", - }, - Package: pkg.Package{ - ID: pkg.ID(uuid.NewString()), - Name: "ubuntu", - Version: "16.04", - Type: "os", - }, - }, - } - - collection := match.NewMatches(matches...) - - return collection -} - -func generatePackages(t *testing.T) []pkg.Package { - t.Helper() - epoch := 2 - return []pkg.Package{ - { - ID: pkg.ID(uuid.NewString()), - Name: "package-1", - Version: "1.1.1", - Type: syftPkg.RpmPkg, - Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-1.txt", "somefile-1.txt")), - Upstreams: []pkg.UpstreamPackage{ - { - Name: "nothing", - Version: "3.2", - }, - }, - MetadataType: pkg.RpmMetadataType, - Metadata: pkg.RpmMetadata{ - Epoch: &epoch, - }, - }, - { - ID: pkg.ID(uuid.NewString()), - Name: "package-2", - Version: "2.2.2", - Type: syftPkg.DebPkg, - Locations: file.NewLocationSet(file.NewVirtualLocation("/foo/bar/somefile-2.txt", "somefile-2.txt")), - Licenses: []string{"MIT", "Apache-2.0"}, - }, - } -} - -func generateContext(t *testing.T, scheme syftSource.Scheme) pkg.Context { - var src syftSource.Source - img := image.Image{ - Metadata: image.Metadata{ - ID: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", - ManifestDigest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", - MediaType: "application/vnd.docker.distribution.manifest.v2+json", - Size: 65, - }, - Layers: []*image.Layer{ - { - Metadata: image.LayerMetadata{ - Digest: "sha256:ca738abb87a8d58f112d3400ebb079b61ceae7dc290beb34bda735be4b1941d5", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 22, - }, - }, - { - Metadata: image.LayerMetadata{ - Digest: "sha256:a05cd9ebf88af96450f1e25367281ab232ac0645f314124fe01af759b93f3006", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 16, - }, - }, - { - Metadata: image.LayerMetadata{ - Digest: "sha256:ab5608d634db2716a297adbfa6a5dd5d8f8f5a7d0cab73649ea7fbb8c8da544f", - MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", - Size: 27, - }, - }, - }, - } - - switch scheme { - case syftSource.ImageScheme: - var err error - src, err = syftSource.NewFromImage(&img, "user-input") - if err != nil { - t.Fatalf("failed to generate mock image source from mock image: %+v", err) - } - case syftSource.DirectoryScheme: - var err error - src, err = syftSource.NewFromDirectory("/some/path") - if err != nil { - t.Fatalf("failed to generate mock directory source from mock dir: %+v", err) - } - default: - t.Fatalf("unknown scheme: %s", scheme) - } - - return pkg.Context{ - Source: &src.Metadata, - Distro: &linux.Release{ - Name: "centos", - IDLike: []string{ - "centos", - }, - Version: "8.0", - }, - } -} diff --git a/xeol/presenter/models/source.go b/xeol/presenter/models/source.go index 8648770c..bdfecbe3 100644 --- a/xeol/presenter/models/source.go +++ b/xeol/presenter/models/source.go @@ -12,39 +12,38 @@ type source struct { } // newSource creates a new source object to be represented into JSON. -func newSource(src syftSource.Metadata) (source, error) { - switch src.Scheme { - case syftSource.ImageScheme: - metadata := src.ImageMetadata +func newSource(src syftSource.Description) (source, error) { + switch m := src.Metadata.(type) { + case syftSource.StereoscopeImageSourceMetadata: // ensure that empty collections are not shown as null - if metadata.RepoDigests == nil { - metadata.RepoDigests = []string{} + if m.RepoDigests == nil { + m.RepoDigests = []string{} } - if metadata.Tags == nil { - metadata.Tags = []string{} + if m.Tags == nil { + m.Tags = []string{} } return source{ Type: "image", - Target: metadata, + Target: m, }, nil - case syftSource.DirectoryScheme: + case syftSource.DirectorySourceMetadata: return source{ Type: "directory", - Target: src.Path, + Target: m.Path, }, nil - case syftSource.FileScheme: + case syftSource.FileSourceMetadata: return source{ Type: "file", - Target: src.Path, + Target: m.Path, }, nil - case "": + case nil: // we may be showing results from a input source that does not support source information return source{ Type: "unknown", Target: "unknown", }, nil default: - return source{}, fmt.Errorf("unsupported source: %q", src.Scheme) + return source{}, fmt.Errorf("unsupported source: %T", src.Metadata) } } diff --git a/xeol/presenter/table/__snapshots__/presenter_test.snap b/xeol/presenter/table/__snapshots__/presenter_test.snap new file mode 100755 index 00000000..ce888983 --- /dev/null +++ b/xeol/presenter/table/__snapshots__/presenter_test.snap @@ -0,0 +1,8 @@ + +[TestTablePresenter - 1] +[]uint8{0x4e, 0x41, 0x4d, 0x45, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x56, 0x45, 0x52, 0x53, 0x49, 0x4f, 0x4e, 0x20, 0x20, 0x45, 0x4f, 0x4c, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x44, 0x41, 0x59, 0x53, 0x20, 0x45, 0x4f, 0x4c, 0x20, 0x20, 0x54, 0x59, 0x50, 0x45, 0x20, 0xa, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2d, 0x31, 0x20, 0x20, 0x31, 0x2e, 0x31, 0x2e, 0x31, 0x20, 0x20, 0x20, 0x20, 0x32, 0x30, 0x31, 0x38, 0x2d, 0x30, 0x37, 0x2d, 0x33, 0x31, 0x20, 0x20, 0x31, 0x36, 0x31, 0x34, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x72, 0x70, 0x6d, 0x20, 0x20, 0x20, 0xa, 0x70, 0x61, 0x63, 0x6b, 0x61, 0x67, 0x65, 0x2d, 0x32, 0x20, 0x20, 0x32, 0x2e, 0x32, 0x2e, 0x32, 0x20, 0x20, 0x20, 0x20, 0x59, 0x45, 0x53, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x2d, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x64, 0x65, 0x62, 0x20, 0x20, 0x20, 0xa, 0x75, 0x62, 0x75, 0x6e, 0x74, 0x75, 0x20, 0x20, 0x20, 0x20, 0x20, 0x31, 0x36, 0x2e, 0x30, 0x34, 0x20, 0x20, 0x20, 0x20, 0x32, 0x30, 0x32, 0x31, 0x2d, 0x30, 0x34, 0x2d, 0x30, 0x32, 0x20, 0x20, 0x36, 0x33, 0x38, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x6f, 0x73, 0x20, 0x20, 0x20, 0x20, 0xa} +--- + +[TestEmptyTablePresenter - 1] +[]uint8{0xe2, 0x9c, 0x85, 0x20, 0x6e, 0x6f, 0x20, 0x45, 0x4f, 0x4c, 0x20, 0x73, 0x6f, 0x66, 0x74, 0x77, 0x61, 0x72, 0x65, 0x20, 0x68, 0x61, 0x73, 0x20, 0x62, 0x65, 0x65, 0x6e, 0x20, 0x66, 0x6f, 0x75, 0x6e, 0x64, 0xa} +--- diff --git a/xeol/presenter/table/presenter_test.go b/xeol/presenter/table/presenter_test.go index 51b4689d..20d2b5a2 100644 --- a/xeol/presenter/table/presenter_test.go +++ b/xeol/presenter/table/presenter_test.go @@ -2,25 +2,20 @@ package table import ( "bytes" - "flag" - "strings" "testing" "time" - "github.com/anchore/go-testutils" syftPkg "github.com/anchore/syft/syft/pkg" - "github.com/anchore/syft/syft/source" - "github.com/sergi/go-diff/diffmatchpatch" + "github.com/gkampitakis/go-snaps/snaps" "github.com/stretchr/testify/assert" "github.com/xeol-io/xeol/xeol/eol" "github.com/xeol-io/xeol/xeol/match" "github.com/xeol-io/xeol/xeol/pkg" + "github.com/xeol-io/xeol/xeol/presenter/internal" "github.com/xeol-io/xeol/xeol/presenter/models" ) -var update = flag.Bool("update", false, "update the *.golden files for table presenters") - func TestCreateRow(t *testing.T) { pkg := pkg.Package{ ID: "package-1-id", @@ -83,9 +78,8 @@ func TestCreateRow(t *testing.T) { } func TestTablePresenter(t *testing.T) { - var buffer bytes.Buffer - matches, packages, _, _, _ := models.GenerateAnalysis(t, source.ImageScheme) + matches, packages, _, _, _ := internal.GenerateAnalysis(t, internal.ImageSource) pb := models.PresenterConfig{ Matches: matches, @@ -100,18 +94,8 @@ func TestTablePresenter(t *testing.T) { t.Fatal(err) } actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } - - var expected = testutils.GetGoldenFileContents(t) - - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(strings.TrimSuffix(strings.TrimSpace(string(expected)), "\n"), strings.TrimSuffix(strings.TrimSpace(string(actual)), "\n"), true) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) - } + snaps.MatchSnapshot(t, actual) // TODO: add me back in when there is a JSON schema // validateAgainstDbSchema(t, string(actual)) } @@ -136,16 +120,5 @@ func TestEmptyTablePresenter(t *testing.T) { t.Fatal(err) } actual := buffer.Bytes() - if *update { - testutils.UpdateGoldenFileContents(t, actual) - } - - var expected = testutils.GetGoldenFileContents(t) - - if !bytes.Equal(expected, actual) { - dmp := diffmatchpatch.New() - diffs := dmp.DiffMain(string(expected), string(actual), true) - t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs)) - } - + snaps.MatchSnapshot(t, actual) } diff --git a/xeol/search/purl.go b/xeol/search/purl.go index 9450958e..03777913 100644 --- a/xeol/search/purl.go +++ b/xeol/search/purl.go @@ -38,20 +38,20 @@ func ByPackagePURL(store eol.Provider, p pkg.Package, _ match.MatcherType, eolMa return match.Match{}, nil } -func ByDistroCpe(store eol.Provider, distro *linux.Release, eolMatchDate time.Time) (match.Match, error) { - version, cycles, err := store.GetByDistroCpe(distro) +func ByDistroCpe(store eol.Provider, distro *linux.Release, eolMatchDate time.Time) (match.Match, string, error) { + version, cycles, distroCPE, err := store.GetByDistroCpe(distro) if err != nil { - return match.Match{}, err + return match.Match{}, "", err } if len(cycles) < 1 { - return match.Match{}, nil + return match.Match{}, "", nil } log.Debugf("matching distro %s with version %s", distro.Name, version) cycle, err := cycleMatch(version, cycles, eolMatchDate) if err != nil { log.Warnf("failed to match cycle for distro %s: %v", distro.Name, err) - return match.Match{}, nil + return match.Match{}, "", nil } if (cycle != eol.Cycle{}) { @@ -62,11 +62,11 @@ func ByDistroCpe(store eol.Provider, distro *linux.Release, eolMatchDate time.Ti Version: version, Type: "os", }, - }, nil + }, distroCPE, nil } log.Warnf("failed to match cycle for distro %s: %v", distro.Name, err) - return match.Match{}, nil + return match.Match{}, "", nil } // normalizeSemver returns the major.minor.patch portion of a semver string diff --git a/xeol/ui/event_handlers.go b/xeol/ui/event_handlers.go deleted file mode 100644 index a483c4cb..00000000 --- a/xeol/ui/event_handlers.go +++ /dev/null @@ -1,133 +0,0 @@ -package ui - -import ( - "context" - "fmt" - "io" - "sync" - "time" - - syftUI "github.com/anchore/syft/ui" - "github.com/dustin/go-humanize" - "github.com/gookit/color" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/go-progress" - "github.com/wagoodman/go-progress/format" - "github.com/wagoodman/jotframe/pkg/frame" - - "github.com/xeol-io/xeol/internal/ui/components" - xeolEventParsers "github.com/xeol-io/xeol/xeol/event/parsers" -) - -const maxBarWidth = 50 -const statusSet = components.SpinnerDotSet // SpinnerCircleOutlineSet -const completedStatus = "✔" // "●" -const tileFormat = color.Bold - -var auxInfoFormat = color.HEX("#777777") -var statusTitleTemplate = fmt.Sprintf(" %%s %%-%ds ", syftUI.StatusTitleColumn) - -func startProcess() (format.Simple, *components.Spinner) { - width, _ := frame.GetTerminalSize() - barWidth := int(0.25 * float64(width)) - if barWidth > maxBarWidth { - barWidth = maxBarWidth - } - formatter := format.NewSimpleWithTheme(barWidth, format.HeavyNoBarTheme, format.ColorCompleted, format.ColorTodo) - spinner := components.NewSpinner(statusSet) - - return formatter, &spinner -} - -func (r *Handler) EolScanningStartedHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - monitor, err := xeolEventParsers.ParseEolScanningStarted(event) - if err != nil { - return fmt.Errorf("bad %s event: %w", event.Type, err) - } - - line, err := fr.Append() - if err != nil { - return err - } - - wg.Add(1) - - _, spinner := startProcess() - stream := progress.StreamMonitors(ctx, []progress.Monitorable{monitor.PackagesProcessed, monitor.EolDiscovered}, 50*time.Millisecond) - title := tileFormat.Sprint("Scanning image...") - - formatFn := func(val int64) { - spin := color.Magenta.Sprint(spinner.Next()) - auxInfo := auxInfoFormat.Sprintf("[%d eol packages]", val) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - } - - go func() { - defer wg.Done() - - formatFn(0) - for p := range stream { - formatFn(p[1]) - } - - spin := color.Green.Sprint(completedStatus) - title = tileFormat.Sprint("Scanned image") - auxInfo := auxInfoFormat.Sprintf("[%d eol]", monitor.EolDiscovered.Current()) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - }() - - return nil -} - -func (r *Handler) UpdateEolDatabaseHandler(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - prog, err := xeolEventParsers.ParseUpdateEolDatabase(event) - if err != nil { - return fmt.Errorf("bad FetchImage event: %w", err) - } - - line, err := fr.Prepend() - if err != nil { - return err - } - - wg.Add(1) - - formatter, spinner := startProcess() - stream := progress.Stream(ctx, prog, 150*time.Millisecond) - title := tileFormat.Sprint("EOL DB") - - formatFn := func(p progress.Progress) { - progStr, err := formatter.Format(p) - spin := color.Magenta.Sprint(spinner.Next()) - if err != nil { - _, _ = io.WriteString(line, fmt.Sprintf("Error: %+v", err)) - } else { - var auxInfo string - switch prog.Stage() { - case "downloading": - progStr += " " - auxInfo = auxInfoFormat.Sprintf(" [%s / %s]", humanize.Bytes(uint64(prog.Current())), humanize.Bytes(uint64(prog.Size()))) - default: - progStr = "" - auxInfo = auxInfoFormat.Sprintf("[%s]", prog.Stage()) - } - - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s%s", spin, title, progStr, auxInfo)) - } - } - - go func() { - defer wg.Done() - - formatFn(progress.Progress{}) - for p := range stream { - formatFn(p) - } - - spin := color.Green.Sprint(completedStatus) - title = tileFormat.Sprint("EOL DB") - auxInfo := auxInfoFormat.Sprintf("[%s]", prog.Stage()) - _, _ = io.WriteString(line, fmt.Sprintf(statusTitleTemplate+"%s", spin, title, auxInfo)) - }() - return err -} diff --git a/xeol/ui/handler.go b/xeol/ui/handler.go deleted file mode 100644 index 06d70b3f..00000000 --- a/xeol/ui/handler.go +++ /dev/null @@ -1,42 +0,0 @@ -package ui - -import ( - "context" - "sync" - - syftUI "github.com/anchore/syft/ui" - "github.com/wagoodman/go-partybus" - "github.com/wagoodman/jotframe/pkg/frame" - - xeolEvent "github.com/xeol-io/xeol/xeol/event" -) - -type Handler struct { - syftHandler *syftUI.Handler -} - -func NewHandler() *Handler { - return &Handler{ - syftHandler: syftUI.NewHandler(), - } -} - -func (r *Handler) RespondsTo(event partybus.Event) bool { - switch event.Type { - case xeolEvent.EolScanningStarted, xeolEvent.UpdateEolDatabase: - return true - default: - return r.syftHandler.RespondsTo(event) - } -} - -func (r *Handler) Handle(ctx context.Context, fr *frame.Frame, event partybus.Event, wg *sync.WaitGroup) error { - switch event.Type { - case xeolEvent.UpdateEolDatabase: - return r.UpdateEolDatabaseHandler(ctx, fr, event, wg) - case xeolEvent.EolScanningStarted: - return r.EolScanningStartedHandler(ctx, fr, event, wg) - default: - return r.syftHandler.Handle(ctx, fr, event, wg) - } -} diff --git a/xeol/xeolerr/expected_error.go b/xeol/xeolerr/expected_error.go index 4efde3a4..2c49c42e 100644 --- a/xeol/xeolerr/expected_error.go +++ b/xeol/xeolerr/expected_error.go @@ -4,7 +4,7 @@ import ( "fmt" ) -// ExpectedErr represents a class of expected errors that grype may produce. +// ExpectedErr represents a class of expected errors that xeol may produce. type ExpectedErr struct { Err error }