Merge pull request #339 from anchore/add-file-metadata-indexer-invert-control

Add file metadata cataloger
This commit is contained in:
Alex Goodman 2021-03-23 17:02:10 -04:00 committed by GitHub
commit e256e86888
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
327 changed files with 4919 additions and 3810 deletions

View File

@ -1,142 +0,0 @@
name: "Acceptance"
on:
workflow_dispatch:
push:
# ... only act on pushes to main
branches:
- main
# ... do not act on release tags
tags-ignore:
- v*
env:
GO_VERSION: "1.14.x"
jobs:
Build-Snapshot-Artifacts:
# though the release pipeline is running on mac for the signing step, we are skipping the signing step here and
# require a system with docker installed, which ubuntu-20.04 has by default (and mac does not for licensing reasons).
runs-on: ubuntu-20.04
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: cache
uses: actions/cache@v2.1.3
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Bootstrap project dependencies
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Build snapshot artifacts
run: make snapshot
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: snapshot/**/*
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Linux:
needs: [Build-Snapshot-Artifacts]
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Linux)
run: make acceptance-linux
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Mac:
needs: [Build-Snapshot-Artifacts]
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Mac)
run: make acceptance-mac
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Inline-Compare:
needs: [Build-Snapshot-Artifacts]
# come Nov 30 2020 ubuntu-latest will be ubuntu-20.04, until then it needs to be explicitly referenced due to python 3.7 specific features being used
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v2
- name: Fingerprint inline-compare sources
run: make compare-fingerprint
- name: Restore inline reports cache
id: cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/test/inline-compare/inline-reports
key: inline-reports-${{ hashFiles('**/inline-compare.fingerprint') }}
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Compare Anchore inline-scan results against snapshot build output
run: make compare-snapshot
- uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
fields: repo,workflow,job,commit,message,author
text: The syft acceptance tests have failed tragically!
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_TOOLBOX_WEBHOOK_URL }}
if: ${{ failure() }}

View File

@ -9,7 +9,7 @@ on:
- "v*"
env:
GO_VERSION: "1.14.x"
GO_VERSION: "1.16.x"
jobs:
wait-for-checks:
@ -29,17 +29,26 @@ jobs:
id: static-analysis
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
checkName: "Static-Analysis (1.x, ubuntu-latest)"
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Static-Analysis"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check unit + integration results (latest go version)
- name: Check unit test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: unit-integration
id: unit
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
checkName: "Tests (1.x, ubuntu-latest)"
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Unit-Test"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check integration test results
uses: fountainhead/action-wait-for-check@v1.0.0
id: integration
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Integration-Test"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check acceptance test results (linux)
@ -47,7 +56,7 @@ jobs:
id: acceptance-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Acceptance-Linux"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
@ -56,27 +65,28 @@ jobs:
id: acceptance-mac
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Acceptance-Mac"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Check inline comparison test results
- name: Check cli test results (linux)
uses: fountainhead/action-wait-for-check@v1.0.0
id: inline-compare
id: cli-linux
with:
token: ${{ secrets.GITHUB_TOKEN }}
# This check name is defined as the github action job name (in .github/workflows/acceptance-test.yaml)
checkName: "Inline-Compare"
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
checkName: "Cli-Linux"
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Quality gate
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit-integration.outputs.conclusion != 'success' || steps.inline-compare.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
if: steps.static-analysis.outputs.conclusion != 'success' || steps.unit.outputs.conclusion != 'success' || steps.integration.outputs.conclusion != 'success' || steps.cli-linux.outputs.conclusion != 'success' || steps.acceptance-linux.outputs.conclusion != 'success' || steps.acceptance-mac.outputs.conclusion != 'success'
run: |
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
echo "Unit & Integration Test Status: ${{ steps.unit-integration.outputs.conclusion }}"
echo "Unit Test Status: ${{ steps.unit.outputs.conclusion }}"
echo "Integration Test Status: ${{ steps.integration.outputs.conclusion }}"
echo "Acceptance Test (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.outputs.conclusion }}"
echo "Inline Compare Status: ${{ steps.inline-compare.outputs.conclusion }}"
echo "CLI Test (Linux) Status: ${{ steps.cli-linux.outputs.conclusion }}"
false
release:
@ -93,23 +103,32 @@ jobs:
with:
fetch-depth: 0
# We are expecting this cache to have been created during the "Build-Snapshot-Artifacts" job in the "Acceptance" workflow.
- name: Restore bootstrap cache
id: cache
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}
restore-keys: |
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ env.GO_VERSION }}-
- name: Bootstrap project dependencies
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
- 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: Login to Docker Hub
uses: docker/login-action@v1
with:
username: ${{ secrets.TOOLBOX_DOCKER_USER }}
password: ${{ secrets.TOOLBOX_DOCKER_PASS }}
- name: Import GPG key
id: import_gpg
uses: crazy-max/ghaction-import-gpg@v2

View File

@ -1,101 +0,0 @@
name: "Static Analysis + Unit + Integration"
on:
workflow_dispatch:
push:
pull_request:
jobs:
Static-Analysis:
strategy:
matrix:
go-version: [1.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: bootstrap-cache
uses: actions/cache@v2.1.3
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ matrix.go-version }}-
- name: Bootstrap project dependencies
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI dependencies
run: make ci-bootstrap
- name: Run static analysis
run: make static-analysis
Tests:
strategy:
matrix:
# test the lower bounds of support, and the latest available
go-version: [1.13.x, 1.x]
platform: [ubuntu-latest]
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ matrix.go-version }}
- uses: actions/checkout@v2
- name: Restore bootstrap cache
id: bootstrap-cache
uses: actions/cache@v2.1.3
with:
path: |
~/go/pkg/mod
${{ github.workspace }}/.tmp
key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-${{ hashFiles('Makefile') }}
restore-keys: |
${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }}-
${{ runner.os }}-go-${{ matrix.go-version }}-
- name: Bootstrap project dependencies
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
run: make bootstrap
- name: Bootstrap CI dependencies
run: make ci-bootstrap
- name: Build cache key for java test-fixture blobs (for unit tests)
run: make java-packages-fingerprint
- name: Restore Java test-fixture cache
id: unit-java-cache
uses: actions/cache@v2.1.3
with:
path: syft/cataloger/java/test-fixtures/java-builds/packages
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
- name: Run unit tests
run: make unit
- name: Validate syft output against the CycloneDX schema
run: make validate-cyclonedx-schema
- name: Build key for tar cache
run: make integration-fingerprint
- name: Restore integration test cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/integration/test-fixtures/cache
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('integration/test-fixtures/cache.fingerprint') }}
- name: Run integration tests
run: make integration

354
.github/workflows/validations.yaml vendored Normal file
View File

@ -0,0 +1,354 @@
name: "Validations"
on:
workflow_dispatch:
push:
pull_request:
env:
GO_VERSION: "1.16.x"
jobs:
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Static-Analysis:
name: "Static analysis"
runs-on: ubuntu-20.04
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
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: Run static analysis
run: make static-analysis
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Unit-Test:
name: "Unit tests"
runs-on: ubuntu-20.04
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
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 cache key for java test-fixture blobs (for unit tests)
run: make java-packages-fingerprint
- name: Restore Java test-fixture cache
id: unit-java-cache
uses: actions/cache@v2.1.3
with:
path: syft/pkg/cataloger/java/test-fixtures/java-builds/packages
key: ${{ runner.os }}-unit-java-cache-${{ hashFiles( 'syft/pkg/cataloger/java/test-fixtures/java-builds/packages.fingerprint' ) }}
- name: Run unit tests
run: make unit
- uses: actions/upload-artifact@v2
with:
name: unit-test-results
path: test/results/**/*
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Integration-Test:
name: "Integration tests"
runs-on: ubuntu-20.04
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
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: Validate syft output against the CycloneDX schema
run: make validate-cyclonedx-schema
- name: Build key for tar cache
run: make integration-fingerprint
- name: Restore integration test cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/test/integration/test-fixtures/cache
key: ${{ runner.os }}-integration-test-cache-${{ hashFiles('test/integration/test-fixtures/cache.fingerprint') }}
- name: Run integration tests
run: make integration
Benchmark-Test:
name: "Benchmark tests"
runs-on: ubuntu-20.04
# note: we want benchmarks to run on pull_request events in order to publish results to a sticky comment, and
# we also want to run on push such that merges to main are recorded to the cache. For this reason we don't filter
# the job by event.
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
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: Restore base benchmark result
uses: actions/cache@v2
with:
path: test/results/benchmark-main.txt
# use base sha for PR or new commit hash for main push in benchmark result key
key: ${{ runner.os }}-bench-${{ (github.event.pull_request.base.sha != github.event.after) && github.event.pull_request.base.sha || github.event.after }}
- name: Run benchmark tests
id: benchmark
run: |
REF_NAME=${GITHUB_REF##*/} make benchmark
echo "::set-output name=result::$(make show-benchstat)"
- uses: actions/upload-artifact@v2
with:
name: benchmark-test-results
path: test/results/**/*
- name: Update PR benchmark results comment
uses: marocchino/sticky-pull-request-comment@v2
with:
header: benchmark
message: |
### Benchmark Test Results
<details>
<summary>Benchmark results from the latest changes vs base branch</summary>
```
${{ steps.benchmark.outputs.result }}
```
</details>
Build-Snapshot-Artifacts:
name: "Build snapshot artifacts"
runs-on: ubuntu-20.04
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/setup-go@v2
with:
go-version: ${{ env.GO_VERSION }}
- uses: actions/checkout@v2
- name: Restore tool cache
id: tool-cache
uses: actions/cache@v2.1.3
with:
path: ${{ github.workspace }}/.tmp
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
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 snapshot artifacts
run: make snapshot
- uses: actions/upload-artifact@v2
with:
name: artifacts
path: snapshot/**/*
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Linux:
name: "Acceptance tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-20.04
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Run Acceptance Tests (Linux)
run: make acceptance-linux
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Acceptance-Mac:
name: "Acceptance tests (Mac)"
needs: [Build-Snapshot-Artifacts]
runs-on: macos-latest
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v2
- uses: actions/download-artifact@v2
with:
name: artifacts
path: snapshot
- name: Restore docker image cache
id: go-cache
uses: actions/cache@v2.1.3
with:
path: /tmp/image.tar
key: ${{ runner.os }}-${{ hashFiles('test/acceptance/mac.sh') }}
- name: Run Acceptance Tests (Mac)
run: make acceptance-mac
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
Cli-Linux:
name: "CLI tests (Linux)"
needs: [Build-Snapshot-Artifacts]
runs-on: ubuntu-20.04
# run only on push event (internal PRs) or on a pull_request event that is from a fork (external PR)
# skip if this is a pull_request event on an internal PR (which is already covered by push events)
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name != github.repository
steps:
- uses: actions/checkout@v2
- name: Restore go cache
id: go-cache
uses: actions/cache@v2.1.3
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
- name: Build key for tar cache
run: make cli-fingerprint
- name: Restore CLI test cache
uses: actions/cache@v2.1.3
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@v2
with:
name: artifacts
path: snapshot
- name: Run CLI Tests (Linux)
run: make cli

1
.gitignore vendored
View File

@ -1,4 +1,5 @@
CHANGELOG.md
/test/results
/dist
/snapshot
.server/

View File

@ -92,18 +92,15 @@ brews:
description: *description
dockers:
-
binaries:
- syft
dockerfile: Dockerfile
- dockerfile: Dockerfile
image_templates:
- "anchore/syft:latest"
- "anchore/syft:{{ .Tag }}"
- "anchore/syft:v{{ .Major }}"
- "anchore/syft:v{{ .Major }}.{{ .Minor }}"
build_flag_templates:
- "--build-arg=BUILD_DATE={{.Date}}"
- "--build-arg=BUILD_VERSION={{.Version}}"
- "--build-arg=VCS_REF={{.FullCommit}}"
- "--build-arg=VCS_URL={{.GitURL}}"
use_buildx: true

143
Makefile
View File

@ -1,8 +1,8 @@
BIN = syft
TEMPDIR = ./.tmp
RESULTSDIR = $(TEMPDIR)/results
COVER_REPORT = $(RESULTSDIR)/cover.report
COVER_TOTAL = $(RESULTSDIR)/cover.total
RESULTSDIR = test/results
COVER_REPORT = $(RESULTSDIR)/unit-coverage-details.txt
COVER_TOTAL = $(RESULTSDIR)/unit-coverage-summary.txt
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --config .golangci.yaml
ACC_TEST_IMAGE = centos:8.2.2004
ACC_DIR = ./test/acceptance
@ -15,17 +15,23 @@ RESET := $(shell tput -T linux sgr0)
TITLE := $(BOLD)$(PURPLE)
SUCCESS := $(BOLD)$(GREEN)
# the quality gate lower threshold for unit test total % coverage (by function statements)
COVERAGE_THRESHOLD := 68
COVERAGE_THRESHOLD := 70
# CI cache busting values; change these if you want CI to not use previous stored cache
COMPARE_CACHE_BUSTER="f7e689d76a9"
INTEGRATION_CACHE_BUSTER="789bacdf"
BOOTSTRAP_CACHE="789bacdf"
INTEGRATION_CACHE_BUSTER="88738d2f"
CLI_CACHE_BUSTER="789bacdf"
BOOTSTRAP_CACHE="c7afb99ad"
## Build variables
DISTDIR=./dist
SNAPSHOTDIR=./snapshot
GITTREESTATE=$(if $(shell git status --porcelain),dirty,clean)
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/syft_linux_amd64/syft)
OS := $(shell uname)
ifeq ($(OS),Darwin)
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/$(BIN)-macos_darwin_amd64/$(BIN))
else
SNAPSHOT_CMD=$(shell realpath $(shell pwd)/$(SNAPSHOTDIR)/$(BIN)_linux_amd64/$(BIN))
endif
ifeq "$(strip $(VERSION))" ""
override VERSION = $(shell git describe --always --tags --dirty)
@ -57,6 +63,10 @@ ifndef SNAPSHOTDIR
$(error SNAPSHOTDIR is not set)
endif
ifndef REF_NAME
REF_NAME = $(VERSION)
endif
define title
@printf '$(TITLE)$(1)$(RESET)\n'
endef
@ -68,7 +78,7 @@ all: clean static-analysis test ## Run all linux-based checks (linting, license
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
.PHONY: test
test: unit validate-cyclonedx-schema integration acceptance-linux ## Run all tests (currently unit, integration, and linux acceptance tests)
test: unit validate-cyclonedx-schema integration benchmark acceptance-linux cli ## Run all tests (currently unit, integration, linux acceptance, and cli tests)
.PHONY: help
help:
@ -78,19 +88,30 @@ help:
ci-bootstrap:
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
.PHONY: bootstrap
bootstrap: ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
$(call title,Bootstrapping dependencies)
@pwd
# prep temp dirs
mkdir -p $(TEMPDIR)
.PHONY:
ci-bootstrap-mac:
github_changelog_generator --version || sudo gem install github_changelog_generator
$(RESULTSDIR):
mkdir -p $(RESULTSDIR)
# install go dependencies
$(TEMPDIR):
mkdir -p $(TEMPDIR)
.PHONY: bootstrap-tools
bootstrap-tools: $(TEMPDIR)
GO111MODULE=off GOBIN=$(shell 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)/ v1.26.0
curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.160.0
.PHONY: bootstrap-go
bootstrap-go:
go mod download
# install utilities
[ -f "$(TEMPDIR)/golangci" ] || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(TEMPDIR)/ v1.26.0
[ -f "$(TEMPDIR)/bouncer" ] || curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
[ -f "$(TEMPDIR)/goreleaser" ] || curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.140.0
.PHONY: bootstrap
bootstrap: $(RESULTSDIR) 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-licenses
@ -124,42 +145,50 @@ validate-cyclonedx-schema:
cd schema/cyclonedx && make
.PHONY: unit
unit: fixtures ## Run unit tests (with coverage)
unit: $(RESULTSDIR) fixtures ## Run unit tests (with coverage)
$(call title,Running unit tests)
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/test)
go test -coverprofile $(COVER_REPORT) $(shell go list ./... | grep -v anchore/syft/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
.PHONY: benchmark
benchmark: $(RESULTSDIR) ## Run benchmark tests and compare against the baseline (if available)
$(call title,Running benchmark tests)
go test -p 1 -run=^Benchmark -bench=. -count=5 -benchmem ./... | tee $(RESULTSDIR)/benchmark-$(REF_NAME).txt
(test -s $(RESULTSDIR)/benchmark-main.txt && \
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-main.txt $(RESULTSDIR)/benchmark-$(REF_NAME).txt || \
$(TEMPDIR)/benchstat $(RESULTSDIR)/benchmark-$(REF_NAME).txt) \
| tee $(RESULTSDIR)/benchstat.txt
.PHONY: show-benchstat
show-benchstat:
@cat $(RESULTSDIR)/benchstat.txt
.PHONY: integration
integration: ## Run integration tests
$(call title,Running integration tests)
go test -v ./test/integration
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
integration-fingerprint:
find test/integration/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/integration/test-fixtures/cache.fingerprint && echo "$(INTEGRATION_CACHE_BUSTER)" >> test/integration/test-fixtures/cache.fingerprint
.PHONY: java-packages-fingerprint
java-packages-fingerprint:
@cd syft/cataloger/java/test-fixtures/java-builds && \
@cd syft/pkg/cataloger/java/test-fixtures/java-builds && \
make packages.fingerprint
.PHONY: fixtures
fixtures:
$(call title,Generating test fixtures)
cd syft/cataloger/java/test-fixtures/java-builds && make
cd syft/pkg/cataloger/java/test-fixtures/java-builds && make
.PHONY: generate-json-schema
generate-json-schema: ## Generate a new json schema
cd schema/json && go run generate.go
.PHONY: clear-test-cache
clear-test-cache: ## Delete all test cache (built docker image tars)
find . -type f -wholename "**/test-fixtures/cache/*.tar" -delete
.PHONY: build
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
@ -175,7 +204,7 @@ $(SNAPSHOTDIR): ## Build snapshot release binaries and packages
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
.PHONY: acceptance-mac
acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Mac)
acceptance-mac: $(RESULTSDIR) $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binaries and packages (Mac)
$(call title,Running acceptance test: Run on Mac)
$(ACC_DIR)/mac.sh \
$(SNAPSHOTDIR) \
@ -187,22 +216,8 @@ acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binarie
.PHONY: acceptance-linux
acceptance-linux: acceptance-test-deb-package-install acceptance-test-rpm-package-install ## Run acceptance tests on build snapshot binaries and packages (Linux)
# note: this is used by CI to determine if the inline-scan report cache should be busted for the inline-compare tests
.PHONY: compare-fingerprint
compare-fingerprint:
find test/inline-compare/* -type f -exec md5sum {} + | grep -v '\-reports' | grep -v 'fingerprint' | awk '{print $1}' | sort | md5sum | tee test/inline-compare/inline-compare.fingerprint && echo "$(COMPARE_CACHE_BUSTER)" >> test/inline-compare/inline-compare.fingerprint
.PHONY: compare-snapshot
compare-snapshot: $(SNAPSHOTDIR) ## Compare the reports of a run of a snapshot build of syft against inline-scan
chmod 755 $(SNAPSHOT_CMD)
@cd test/inline-compare && SYFT_CMD=$(SNAPSHOT_CMD) make
.PHONY: compare
compare: ## Compare the reports of a run of a main-branch build of syft against inline-scan
@cd test/inline-compare && make
.PHONY: acceptance-test-deb-package-install
acceptance-test-deb-package-install: $(SNAPSHOTDIR)
acceptance-test-deb-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
$(call title,Running acceptance test: DEB install)
$(ACC_DIR)/deb.sh \
$(SNAPSHOTDIR) \
@ -211,7 +226,7 @@ acceptance-test-deb-package-install: $(SNAPSHOTDIR)
$(RESULTSDIR)
.PHONY: acceptance-test-rpm-package-install
acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
acceptance-test-rpm-package-install: $(RESULTSDIR) $(SNAPSHOTDIR)
$(call title,Running acceptance test: RPM install)
$(ACC_DIR)/rpm.sh \
$(SNAPSHOTDIR) \
@ -219,6 +234,17 @@ acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
$(ACC_TEST_IMAGE) \
$(RESULTSDIR)
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
cli-fingerprint:
find test/cli/test-fixtures/image-* -type f -exec md5sum {} + | awk '{print $1}' | sort | md5sum | tee test/cli/test-fixtures/cache.fingerprint && echo "$(CLI_CACHE_BUSTER)" >> test/cli/test-fixtures/cache.fingerprint
.PHONY: cli
cli: $(SNAPSHOTDIR) ## Run CLI tests
chmod 755 "$(SNAPSHOT_CMD)"
$(SNAPSHOT_CMD) version
SYFT_BINARY_LOCATION='$(SNAPSHOT_CMD)' \
go test -count=1 -v ./test/cli
.PHONY: changlog-release
changelog-release:
@echo "Last tag: $(SECOND_TO_LAST_TAG)"
@ -283,7 +309,7 @@ release: clean-dist changelog-release ## Build and publish final binaries and pa
.PHONY: clean
clean: clean-dist clean-snapshot ## Remove previous builds and result reports
clean: clean-dist clean-snapshot clean-test-image-cache ## Remove previous builds, result reports, and test cache
rm -rf $(RESULTSDIR)/*
.PHONY: clean-snapshot
@ -293,3 +319,26 @@ clean-snapshot:
.PHONY: clean-dist
clean-dist:
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
clean-test-image-cache: clean-test-image-tar-cache clean-test-image-docker-cache
.PHONY: clear-test-image-tar-cache
clean-test-image-tar-cache: ## Delete all test cache (built docker image tars)
find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" -delete
.PHONY: clear-test-image-docker-cache
clean-test-image-docker-cache: ## Purge all test docker images
docker images --format '{{.ID}} {{.Repository}}' | grep stereoscope-fixture- | awk '{print $$1}' | uniq | xargs docker rmi --force
.PHONY: show-test-image-cache
show-test-image-cache: ## Show all docker and image tar cache
$(call title,Docker daemon cache)
@docker images --format '{{.ID}} {{.Repository}}:{{.Tag}}' | grep stereoscope-fixture- | sort
$(call title,Tar cache)
@find . -type f -wholename "**/test-fixtures/cache/stereoscope-fixture-*.tar" | sort
.PHONY: show-test-snapshots
show-test-snapshots: ## Show all test snapshots
$(call title,Test snapshots)
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort

View File

@ -1,7 +1,6 @@
# syft
[![Static Analysis + Unit + Integration](https://github.com/anchore/syft/workflows/Static%20Analysis%20+%20Unit%20+%20Integration/badge.svg)](https://github.com/anchore/syft/actions?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22)
[![Acceptance](https://github.com/anchore/syft/workflows/Acceptance/badge.svg)](https://github.com/anchore/syft/actions?query=workflow%3AAcceptance)
[![Validations](https://github.com/anchore/syft/workflows/validations.yaml/badge.svg)](https://github.com/anchore/syft/workflows/validations.yaml)
[![Go Report Card](https://goreportcard.com/badge/github.com/anchore/syft)](https://goreportcard.com/report/github.com/anchore/syft)
[![GitHub release](https://img.shields.io/github/release/anchore/syft.svg)](https://github.com/anchore/syft/releases/latest)
[![License: Apache-2.0](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/anchore/syft/blob/main/LICENSE)
@ -25,25 +24,30 @@ To generate an SBOM for a Docker or OCI image:
syft <image>
```
**Note**: This is equivalent to specifying the `packages` subcommand:
```
syft packages <image>
```
The above output includes only software that is visible in the container (i.e., the squashed representation of the image).
To include software from all image layers in the SBOM, regardless of its presence in the final image, provide `--scope all-layers`:
```
syft <image> --scope all-layers
syft packages <image> --scope all-layers
```
Syft can generate a SBOM from a variety of sources:
```
# catalog a container image archive (from the result of `docker image save ...`, `podman save ...`, or `skopeo copy` commands)
syft path/to/image.tar
syft packages path/to/image.tar
# catalog a directory
syft path/to/dir
syft packages path/to/dir
```
The output format for Syft is configurable as well:
```
syft <image> -o <format>
syft packages <image> -o <format>
```
Where the `format`s available are:
@ -85,10 +89,6 @@ Configuration options (example values are the default):
# same as -o ; SYFT_OUTPUT env var
output: "table"
# the search space to look for packages (options: all-layers, squashed)
# same as -s ; SYFT_SCOPE env var
scope: "squashed"
# suppress all output (except for the SBOM report)
# same as -q ; SYFT_QUIET env var
quiet: false
@ -97,6 +97,32 @@ quiet: false
# same as SYFT_CHECK_FOR_APP_UPDATE env var
check-for-app-update: true
# cataloging packages is exposed through the packages and power-user subcommands
package:
cataloger:
# enable/disable cataloging of packages
# SYFT_PACKAGE_CATALOGER_ENABLED env var
enabled: true
# the search space to look for packages (options: all-layers, squashed)
# same as -s ; SYFT_PACKAGE_CATALOGER_SCOPE env var
scope: "squashed"
# cataloging file metadata is exposed through the power-user subcommand
file-metadata:
cataloger:
# enable/disable cataloging of file metadata
# SYFT_FILE_METADATA_CATALOGER_ENABLED env var
enabled: true
# the search space to look for file metadata (options: all-layers, squashed)
# SYFT_FILE_METADATA_CATALOGER_SCOPE env var
scope: "squashed"
# the file digest algorithms to use when cataloging files (options: "sha256", "md5", "sha1")
# SYFT_FILE_METADATA_DIGESTS env var
digests: ["sha256"]
log:
# use structured logging
# same as SYFT_LOG_STRUCTURED env var
@ -110,11 +136,8 @@ log:
# same as SYFT_LOG_FILE env var
file: ""
# uploading package SBOM is exposed through the packages subcommand
anchore:
# (feature-preview) enable uploading of results to Anchore Enterprise automatically (supported on Enterprise 3.0+)
# same as SYFT_ANCHORE_UPLOAD_ENABLED env var
upload-enabled: false
# (feature-preview) the Anchore Enterprise Host or URL to upload results to (supported on Enterprise 3.0+)
# same as -H ; SYFT_ANCHORE_HOST env var
host: ""

View File

@ -0,0 +1,30 @@
package cmd
import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/event"
"github.com/wagoodman/go-partybus"
)
func checkForApplicationUpdate() {
if appConfig.CheckForAppUpdate {
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
// this should never stop the application
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s (current version is %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)
}
}
}

View File

@ -4,28 +4,27 @@ import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/anchore/stereoscope"
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/logger"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"github.com/gookit/color"
"github.com/spf13/cobra"
"github.com/spf13/viper"
"github.com/wagoodman/go-partybus"
)
var appConfig *config.Application
var eventBus *partybus.Bus
var eventSubscription *partybus.Subscription
var cliOpts = config.CliOnlyOptions{}
var (
appConfig *config.Application
eventBus *partybus.Bus
eventSubscription *partybus.Subscription
)
func init() {
setGlobalCliOptions()
cobra.OnInitialize(
initCmdAliasBindings,
initAppConfig,
initLogging,
logAppConfig,
@ -35,109 +34,41 @@ func init() {
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err.Error())
fmt.Fprintln(os.Stderr, color.Red.Sprint(err.Error()))
os.Exit(1)
}
}
func setGlobalCliOptions() {
rootCmd.PersistentFlags().StringVarP(&cliOpts.ConfigPath, "config", "c", "", "application config file")
// scan options
flag := "scope"
rootCmd.Flags().StringP(
"scope", "s", source.SquashedScope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
// we must setup the config-cli bindings first before the application configuration is parsed. However, this cannot
// be done without determining what the primary command that the config options should be bound to since there are
// shared concerns (the root-packages alias).
func initCmdAliasBindings() {
activeCmd, _, err := rootCmd.Find(os.Args[1:])
if err != nil {
panic(err)
}
setGlobalFormatOptions()
setGlobalUploadOptions()
}
func setGlobalFormatOptions() {
// output & formatting options
flag := "output"
rootCmd.Flags().StringP(
flag, "o", string(presenter.TablePresenter),
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "quiet"
rootCmd.Flags().BoolP(
flag, "q", false,
"suppress all logging output",
)
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
}
func setGlobalUploadOptions() {
flag := "host"
rootCmd.Flags().StringP(
flag, "H", "",
"the hostname or URL of the Anchore Enterprise instance to upload to",
)
if err := viper.BindPFlag("anchore.host", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "username"
rootCmd.Flags().StringP(
flag, "u", "",
"the username to authenticate against Anchore Enterprise",
)
if err := viper.BindPFlag("anchore.username", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "password"
rootCmd.Flags().StringP(
flag, "p", "",
"the password to authenticate against Anchore Enterprise",
)
if err := viper.BindPFlag("anchore.password", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
os.Exit(1)
}
flag = "dockerfile"
rootCmd.Flags().StringP(
flag, "d", "",
"include dockerfile for upload to Anchore Enterprise",
)
if err := viper.BindPFlag("anchore.dockerfile", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '#{flag}': #{err}")
os.Exit(1)
}
flag = "overwrite-existing-image"
rootCmd.Flags().Bool(
flag, false,
"overwrite an existing image during the upload to Anchore Enterprise",
)
if err := viper.BindPFlag("anchore.overwrite-existing-image", rootCmd.Flags().Lookup(flag)); err != nil {
fmt.Printf("unable to bind flag '#{flag}': #{err}")
os.Exit(1)
if activeCmd == packagesCmd || activeCmd == rootCmd {
// note: we need to lazily bind config options since they are shared between both the root command
// and the packages command. Otherwise there will be global viper state that is in contention.
// See for more details: https://github.com/spf13/viper/issues/233 . Additionally, the bindings must occur BEFORE
// reading the application configuration, which implies that it must be an initializer (or rewrite the command
// initialization structure against typical patterns used with cobra, which is somewhat extreme for a
// temporary alias)
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
panic(err)
}
} else {
// even though the root command or packages command is NOT being run, we still need default bindings
// such that application config parsing passes.
if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil {
panic(err)
}
}
}
func initAppConfig() {
cfgVehicle := viper.GetViper()
wasHostnameSet := rootCmd.Flags().Changed("host")
cfg, err := config.LoadApplicationConfig(cfgVehicle, cliOpts, wasHostnameSet)
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)
@ -163,7 +94,7 @@ func initLogging() {
}
func logAppConfig() {
log.Debugf("Application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
log.Debugf("application config:\n%+v", color.Magenta.Sprint(appConfig.String()))
}
func initEventBus() {

View File

@ -1,15 +1,22 @@
package cmd
import (
"context"
"os"
"strings"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/spf13/cobra"
)
// completionCmd represents the completion command
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Hidden: true,
Use: "completion [bash|zsh|fish]",
Short: "Generate a shell completion for Syft (listing local docker images)",
Long: `To load completions (docker image list):
Bash:
@ -63,3 +70,45 @@ $ syft completion fish > ~/.config/fish/completions/syft.fish
func init() {
rootCmd.AddCommand(completionCmd)
}
func dockerImageValidArgsFunction(cmd *cobra.Command, args []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
}
func listLocalDockerImages(prefix string) ([]string, error) {
var repoTags = make([]string, 0)
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return repoTags, err
}
// Only want to return tagged images
imageListArgs := filters.NewArgs()
imageListArgs.Add("dangling", "false")
images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
if err != nil {
return repoTags, err
}
for _, image := range images {
// image may have multiple tags
for _, tag := range image.RepoTags {
if strings.HasPrefix(tag, prefix) {
repoTags = append(repoTags, tag)
}
}
}
return repoTags, nil
}

270
cmd/packages.go Normal file
View File

@ -0,0 +1,270 @@
package cmd
import (
"context"
"fmt"
"io/ioutil"
"os"
"github.com/spf13/viper"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/presenter/packages"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"github.com/wagoodman/go-partybus"
)
const (
packagesExample = ` {{.appName}} {{.command}} alpine:latest a summary of discovered packages
{{.appName}} {{.command}} alpine:latest -o json show all possible cataloging details
{{.appName}} {{.command}} alpine:latest -o cyclonedx show a CycloneDX SBOM
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
Supports the following image sources:
{{.appName}} {{.command}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
{{.appName}} {{.command}} path/to/a/file/or/dir a Docker tar, OCI tar, OCI directory, or generic filesystem directory
You can also explicitly specify the scheme to use:
{{.appName}} {{.command}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
{{.appName}} {{.command}} docker-archive:path/to/yourimage.tar use a tarball from disk for archives created from "docker save"
{{.appName}} {{.command}} oci-archive:path/to/yourimage.tar use a tarball from disk for OCI archives (from Skopeo or otherwise)
{{.appName}} {{.command}} oci-dir:path/to/yourimage read directly from a path on disk for OCI layout directories (from Skopeo or otherwise)
{{.appName}} {{.command}} dir:path/to/yourproject read directly from a path on disk (any directory)
`
)
var (
packagesPresenterOpt packages.PresenterOption
packagesArgs = cobra.MinimumNArgs(1)
packagesCmd = &cobra.Command{
Use: "packages [SOURCE]",
Short: "Generate a package SBOM",
Long: "Generate a packaged-based Software Bill Of Materials (SBOM) from container images and filesystems",
Example: internal.Tprintf(packagesExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "packages",
}),
Args: packagesArgs,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
return err
}
// silently exit
return fmt.Errorf("")
}
// set the presenter
presenterOption := packages.ParsePresenterOption(appConfig.Output)
if presenterOption == packages.UnknownPresenterOption {
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
}
packagesPresenterOpt = presenterOption
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if appConfig.Dev.ProfileCPU {
defer profile.Start(profile.CPUProfile).Stop()
} else if appConfig.Dev.ProfileMem {
defer profile.Start(profile.MemProfile).Stop()
}
return packagesExec(cmd, args)
},
ValidArgsFunction: dockerImageValidArgsFunction,
}
)
func init() {
setPackageFlags(packagesCmd.Flags())
rootCmd.AddCommand(packagesCmd)
}
func setPackageFlags(flags *pflag.FlagSet) {
///////// Formatting & Input options //////////////////////////////////////////////
flags.StringP(
"scope", "s", source.SquashedScope.String(),
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
flags.StringP(
"output", "o", string(packages.TablePresenterOption),
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
)
///////// Upload options //////////////////////////////////////////////////////////
flags.StringP(
"host", "H", "",
"the hostname or URL of the Anchore Enterprise instance to upload to",
)
flags.StringP(
"username", "u", "",
"the username to authenticate against Anchore Enterprise",
)
flags.StringP(
"password", "p", "",
"the password to authenticate against Anchore Enterprise",
)
flags.StringP(
"dockerfile", "d", "",
"include dockerfile for upload to Anchore Enterprise",
)
flags.Bool(
"overwrite-existing-image", false,
"overwrite an existing image during the upload to Anchore Enterprise",
)
}
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
///////// Formatting & Input options //////////////////////////////////////////////
if err := viper.BindPFlag("package.cataloger.scope", flags.Lookup("scope")); err != nil {
return err
}
if err := viper.BindPFlag("output", flags.Lookup("output")); err != nil {
return err
}
///////// Upload options //////////////////////////////////////////////////////////
if err := viper.BindPFlag("anchore.host", flags.Lookup("host")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.username", flags.Lookup("username")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.password", flags.Lookup("password")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.dockerfile", flags.Lookup("dockerfile")); err != nil {
return err
}
if err := viper.BindPFlag("anchore.overwrite-existing-image", flags.Lookup("overwrite-existing-image")); err != nil {
return err
}
return nil
}
func packagesExec(_ *cobra.Command, args []string) error {
errs := packagesExecWorker(args[0])
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
return ux(errs, eventSubscription)
}
func packagesExecWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
checkForApplicationUpdate()
src, cleanup, err := source.New(userInput)
if err != nil {
errs <- fmt.Errorf("failed to determine image source: %+v", err)
return
}
defer cleanup()
catalog, d, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %+v", err)
return
}
if appConfig.Anchore.Host != "" {
if err := runPackageSbomUpload(src, src.Metadata, catalog, d, appConfig.Package.Cataloger.ScopeOpt); err != nil {
errs <- err
return
}
}
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: packages.Presenter(packagesPresenterOpt, packages.PresenterConfig{
SourceMetadata: src.Metadata,
Catalog: catalog,
Distro: d,
Scope: appConfig.Package.Cataloger.ScopeOpt,
}),
})
}()
return errs
}
func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) error {
log.Infof("uploading results to %s", appConfig.Anchore.Host)
if src.Metadata.Scheme != source.ImageScheme {
return fmt.Errorf("unable to upload results: only images are supported")
}
var dockerfileContents []byte
if appConfig.Anchore.Dockerfile != "" {
if _, err := os.Stat(appConfig.Anchore.Dockerfile); os.IsNotExist(err) {
return fmt.Errorf("unable dockerfile=%q does not exist: %w", appConfig.Anchore.Dockerfile, err)
}
fh, err := os.Open(appConfig.Anchore.Dockerfile)
if err != nil {
return fmt.Errorf("unable to open dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
dockerfileContents, err = ioutil.ReadAll(fh)
if err != nil {
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
}
c, err := anchore.NewClient(anchore.Configuration{
BaseURL: appConfig.Anchore.Host,
Username: appConfig.Anchore.Username,
Password: appConfig.Anchore.Password,
})
if err != nil {
return fmt.Errorf("failed to create anchore client: %+v", err)
}
importCfg := anchore.ImportConfig{
ImageMetadata: src.Image.Metadata,
SourceMetadata: s,
Catalog: catalog,
Distro: d,
Dockerfile: dockerfileContents,
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
Scope: scope,
}
if err := c.Import(context.Background(), importCfg); err != nil {
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
}
return nil
}

113
cmd/power_user.go Normal file
View File

@ -0,0 +1,113 @@
package cmd
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/source"
"github.com/pkg/profile"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
)
const powerUserExample = ` {{.appName}} {{.command}} <image>
Only image sources are supported (e.g. docker: , docker-archive: , oci: , etc.), the directory source (dir:) is not supported.
All behavior is controlled via application configuration and environment variables (see https://github.com/anchore/syft#configuration)
`
var powerUserOpts = struct {
configPath string
}{}
var powerUserCmd = &cobra.Command{
Use: "power-user [IMAGE]",
Short: "Run bulk operations on container images",
Example: internal.Tprintf(powerUserExample, map[string]interface{}{
"appName": internal.ApplicationName,
"command": "power-user",
}),
Args: cobra.ExactArgs(1),
Hidden: true,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: func(cmd *cobra.Command, args []string) error {
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
return fmt.Errorf("cannot profile CPU and memory simultaneously")
}
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
if appConfig.Dev.ProfileCPU {
defer profile.Start(profile.CPUProfile).Stop()
} else if appConfig.Dev.ProfileMem {
defer profile.Start(profile.MemProfile).Stop()
}
return powerUserExec(cmd, args)
},
ValidArgsFunction: dockerImageValidArgsFunction,
}
func init() {
powerUserCmd.Flags().StringVarP(&powerUserOpts.configPath, "config", "c", "", "config file path with all power-user options")
rootCmd.AddCommand(powerUserCmd)
}
func powerUserExec(_ *cobra.Command, args []string) error {
errs := powerUserExecWorker(args[0])
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
return ux(errs, eventSubscription)
}
func powerUserExecWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
tasks, err := powerUserTasks()
if err != nil {
errs <- err
return
}
checkForApplicationUpdate()
src, cleanup, err := source.New(userInput)
if err != nil {
errs <- err
return
}
defer cleanup()
if src.Metadata.Scheme != source.ImageScheme {
errs <- fmt.Errorf("the power-user subcommand only allows for 'image' schemes, given %q", src.Metadata.Scheme)
return
}
analysisResults := poweruser.JSONDocumentConfig{
SourceMetadata: src.Metadata,
ApplicationConfig: *appConfig,
}
for _, task := range tasks {
if err = task(&analysisResults, src); err != nil {
errs <- err
return
}
}
bus.Publish(partybus.Event{
Type: event.PresenterReady,
Value: poweruser.NewJSONPresenter(analysisResults),
})
}()
return errs
}

103
cmd/power_user_tasks.go Normal file
View File

@ -0,0 +1,103 @@
package cmd
import (
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type powerUserTask func(*poweruser.JSONDocumentConfig, source.Source) error
func powerUserTasks() ([]powerUserTask, error) {
var tasks []powerUserTask
generators := []func() (powerUserTask, error){
catalogPackagesTask,
catalogFileMetadataTask,
catalogFileDigestTask,
}
for _, generator := range generators {
task, err := generator()
if err != nil {
return nil, err
}
if task != nil {
tasks = append(tasks, task)
}
}
return tasks, nil
}
func catalogPackagesTask() (powerUserTask, error) {
if !appConfig.Package.Cataloger.Enabled {
return nil, nil
}
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
packageCatalog, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
if err != nil {
return err
}
results.PackageCatalog = packageCatalog
results.Distro = theDistro
return nil
}
return task, nil
}
func catalogFileMetadataTask() (powerUserTask, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
metadataCataloger := file.NewMetadataCataloger()
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
}
result, err := metadataCataloger.Catalog(resolver)
if err != nil {
return err
}
results.FileMetadata = result
return nil
}
return task, nil
}
func catalogFileDigestTask() (powerUserTask, error) {
if !appConfig.FileMetadata.Cataloger.Enabled {
return nil, nil
}
digestsCataloger, err := file.NewDigestsCataloger(appConfig.FileMetadata.Digests)
if err != nil {
return nil, err
}
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
if err != nil {
return err
}
result, err := digestsCataloger.Catalog(resolver)
if err != nil {
return err
}
results.FileDigests = result
return nil
}
return task, nil
}

View File

@ -1,217 +1,45 @@
package cmd
import (
"context"
"fmt"
"io/ioutil"
"os"
"strings"
"github.com/pkg/profile"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/anchore"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/internal/ui"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/filters"
"github.com/docker/docker/client"
"github.com/anchore/syft/internal/config"
"github.com/spf13/cobra"
"github.com/wagoodman/go-partybus"
"github.com/spf13/viper"
)
var persistentOpts = config.CliOnlyOptions{}
// rootCmd is currently an alias for the packages command
var rootCmd = &cobra.Command{
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
Short: "A tool for generating a Software Bill Of Materials (PackageSBOM) from container images and filesystems",
Long: internal.Tprintf(`
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, or generic filesystem directory
You can also explicitly specify the scheme to use:
{{.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}} dir:path/to/yourproject read directly from a path on disk (any directory)
`, map[string]interface{}{
"appName": internal.ApplicationName,
}),
Args: cobra.MaximumNArgs(1),
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
err := cmd.Help()
if err != nil {
log.Errorf(err.Error())
os.Exit(1)
}
os.Exit(1)
}
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
log.Errorf("cannot profile CPU and memory simultaneously")
os.Exit(1)
}
if appConfig.Dev.ProfileCPU {
defer profile.Start(profile.CPUProfile).Stop()
} else if appConfig.Dev.ProfileMem {
defer profile.Start(profile.MemProfile).Stop()
}
err := doRunCmd(cmd, args)
if err != nil {
log.Errorf(err.Error())
os.Exit(1)
}
},
ValidArgsFunction: func(cmd *cobra.Command, args []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
},
Short: packagesCmd.Short,
Long: packagesCmd.Long,
Args: packagesCmd.Args,
Example: packagesCmd.Example,
SilenceUsage: true,
SilenceErrors: true,
PreRunE: packagesCmd.PreRunE,
RunE: packagesCmd.RunE,
ValidArgsFunction: packagesCmd.ValidArgsFunction,
}
func startWorker(userInput string) <-chan error {
errs := make(chan error)
go func() {
defer close(errs)
func init() {
// set universal flags
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
if appConfig.CheckForAppUpdate {
isAvailable, newVersion, err := version.IsUpdateAvailable()
if err != nil {
log.Errorf(err.Error())
}
if isAvailable {
log.Infof("new version of %s is available: %s", internal.ApplicationName, newVersion)
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)
}
bus.Publish(partybus.Event{
Type: event.AppUpdateAvailable,
Value: newVersion,
})
} else {
log.Debugf("no new %s update available", internal.ApplicationName)
}
}
rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
src, catalog, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
if err != nil {
errs <- fmt.Errorf("failed to catalog input: %+v", err)
return
}
if appConfig.Anchore.UploadEnabled {
if err := doImport(src, src.Metadata, catalog, distro); err != nil {
errs <- err
return
}
}
bus.Publish(partybus.Event{
Type: event.CatalogerFinished,
Value: presenter.GetPresenter(appConfig.PresenterOpt, src.Metadata, catalog, distro),
})
}()
return errs
}
func doImport(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) error {
// TODO: ETUI element for this
log.Infof("uploading results to %s", appConfig.Anchore.Host)
if src.Metadata.Scheme != source.ImageScheme {
return fmt.Errorf("unable to upload results: only images are supported")
}
var dockerfileContents []byte
if appConfig.Anchore.Dockerfile != "" {
if _, err := os.Stat(appConfig.Anchore.Dockerfile); os.IsNotExist(err) {
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
fh, err := os.Open(appConfig.Anchore.Dockerfile)
if err != nil {
return fmt.Errorf("unable to open dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
dockerfileContents, err = ioutil.ReadAll(fh)
if err != nil {
return fmt.Errorf("unable to read dockerfile=%q: %w", appConfig.Anchore.Dockerfile, err)
}
}
c, err := anchore.NewClient(anchore.Configuration{
BaseURL: appConfig.Anchore.Host,
Username: appConfig.Anchore.Username,
Password: appConfig.Anchore.Password,
})
if err != nil {
return fmt.Errorf("unable to upload results: %w", err)
}
importCfg := anchore.ImportConfig{
ImageMetadata: src.Image.Metadata,
SourceMetadata: s,
Catalog: catalog,
Distro: d,
Dockerfile: dockerfileContents,
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
}
if err := c.Import(context.Background(), importCfg); err != nil {
return fmt.Errorf("failed to upload results to host=%s: %+v", appConfig.Anchore.Host, err)
}
return nil
}
func doRunCmd(_ *cobra.Command, args []string) error {
userInput := args[0]
errs := startWorker(userInput)
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
return ux(errs, eventSubscription)
}
func ListLocalDockerImages(prefix string) ([]string, error) {
var repoTags = make([]string, 0)
ctx := context.Background()
cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation())
if err != nil {
return repoTags, err
}
// Only want to return tagged images
imageListArgs := filters.NewArgs()
imageListArgs.Add("dangling", "false")
images, err := cli.ImageList(ctx, types.ImageListOptions{All: false, Filters: imageListArgs})
if err != nil {
return repoTags, err
}
for _, image := range images {
// image may have multiple tags
for _, tag := range image.RepoTags {
if strings.HasPrefix(tag, prefix) {
repoTags = append(repoTags, tag)
}
}
}
return repoTags, nil
// set common options that are not universal (package subcommand-alias specific)
setPackageFlags(rootCmd.Flags())
}

View File

@ -8,7 +8,6 @@ import (
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/presenter"
"github.com/spf13/cobra"
)
@ -21,7 +20,7 @@ var versionCmd = &cobra.Command{
}
func init() {
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", string(presenter.TextPresenter), "format to show version information (available=[text, json])")
versionCmd.Flags().StringVarP(&outputFormat, "output", "o", "text", "format to show version information (available=[text, json])")
rootCmd.AddCommand(versionCmd)
}

7
go.mod
View File

@ -3,13 +3,14 @@ module github.com/anchore/syft
go 1.14
require (
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/adrg/xdg v0.2.1
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
github.com/anchore/stereoscope v0.0.0-20210201165248-e94c52b4052d
github.com/anchore/stereoscope v0.0.0-20210323182342-47b72675ff65
github.com/antihax/optional v1.0.0
github.com/bmatcuk/doublestar/v2 v2.0.4
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
@ -31,7 +32,9 @@ require (
github.com/sirupsen/logrus v1.6.0
github.com/spf13/afero v1.2.2
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.0
github.com/stretchr/testify v1.6.0
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163

11
go.sum
View File

@ -96,10 +96,12 @@ github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbt
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
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/adrg/xdg v0.2.1 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=
github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921 h1:T3+cD5fYvuH36h7EZq+TDpm+d8a6FSD4pQsbmuGGQ8o=
github.com/alecthomas/jsonschema v0.0.0-20200530073317-71f438968921/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60=
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074 h1:Lw9q+WyJLFOR+AULchS5/2GKfM+6gOh4szzizdfH3MU=
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60=
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
@ -113,8 +115,8 @@ github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0v
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
github.com/anchore/stereoscope v0.0.0-20210201165248-e94c52b4052d h1:2hv5NOZ0fD8tPk1UdGiW9PHxmjBmBLL+sFlhLXjjKgo=
github.com/anchore/stereoscope v0.0.0-20210201165248-e94c52b4052d/go.mod h1:lhSEYyGLXTXMIFHAz7Ls/MNQ5EjYd5ziLxovKZp1xOs=
github.com/anchore/stereoscope v0.0.0-20210323182342-47b72675ff65 h1:r3tiir6UCgj/YeTqy4s2bfhZ9SuJYNlXx1Z9e/eLrbI=
github.com/anchore/stereoscope v0.0.0-20210323182342-47b72675ff65/go.mod h1:G7tFR0iI9r6AvibmXKA9v010pRS1IIJgd0t6fOMDxCw=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
@ -737,7 +739,6 @@ github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyC
github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
github.com/vbatts/tar-split v0.11.1/go.mod h1:LEuURwDEiWjRjwu46yU3KVGuUdVv/dcnpcEPSzR8z6g=
github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c=
github.com/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d h1:KOxOL6qpmqwoPloNwi+CEgc1ayjHNOFNrvoOmeDOjDg=

View File

@ -26,6 +26,7 @@ type ImportConfig struct {
Distro *distro.Distro
Dockerfile []byte
OverwriteExistingUpload bool
Scope source.Scope
}
func importProgress(source string) (*progress.Stage, *progress.Manual) {
@ -71,7 +72,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
prog.N++
sessionID := startOperation.Uuid
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SourceMetadata, cfg.Catalog, cfg.Distro, stage)
packageDigest, err := importPackageSBOM(authedCtx, c.client.ImportsApi, sessionID, cfg.SourceMetadata, cfg.Catalog, cfg.Distro, cfg.Scope, stage)
if err != nil {
return fmt.Errorf("failed to import Package SBOM: %w", err)
}

View File

@ -8,9 +8,9 @@ import (
"fmt"
"net/http"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/internal/presenter/packages"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/source"
@ -24,9 +24,9 @@ type packageSBOMImportAPI interface {
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
}
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro) (*external.ImagePackageManifest, error) {
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) {
var buf bytes.Buffer
pres := jsonPresenter.NewPresenter(catalog, s, d)
pres := packages.NewJSONPresenter(catalog, s, d, scope)
err := pres.Present(&buf)
if err != nil {
return nil, fmt.Errorf("unable to serialize results: %w", err)
@ -41,11 +41,11 @@ func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro)
return &model, nil
}
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, stage *progress.Stage) (string, error) {
func importPackageSBOM(ctx context.Context, api packageSBOMImportAPI, sessionID string, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope, stage *progress.Stage) (string, error) {
log.Debug("importing package SBOM")
stage.Current = "package SBOM"
model, err := packageSbomModel(s, catalog, d)
model, err := packageSbomModel(s, catalog, d, scope)
if err != nil {
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
}

View File

@ -9,9 +9,9 @@ import (
"strings"
"testing"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/internal/presenter/packages"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/distro"
@ -38,7 +38,6 @@ func TestPackageSbomToModel(t *testing.T) {
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Scope: "scope!",
Layers: []source.LayerMetadata{
{
MediaType: "layer-metadata-type!",
@ -76,7 +75,7 @@ func TestPackageSbomToModel(t *testing.T) {
c := pkg.NewCatalog(p)
model, err := packageSbomModel(m, c, &d)
model, err := packageSbomModel(m, c, &d, source.AllLayersScope)
if err != nil {
t.Fatalf("unable to generate model from source material: %+v", err)
}
@ -89,19 +88,19 @@ func TestPackageSbomToModel(t *testing.T) {
}
var buf bytes.Buffer
pres := jsonPresenter.NewPresenter(c, m, &d)
pres := packages.NewJSONPresenter(c, m, &d, source.AllLayersScope)
if err := pres.Present(&buf); err != nil {
t.Fatalf("unable to get expected json: %+v", err)
}
// unmarshal expected result
var expectedDoc jsonPresenter.Document
var expectedDoc packages.JSONDocument
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
// unmarshal actual result
var actualDoc jsonPresenter.Document
var actualDoc packages.JSONDocument
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
t.Fatalf("unable to parse json doc: %+v", err)
}
@ -178,10 +177,9 @@ func TestPackageSbomImport(t *testing.T) {
})
m := source.Metadata{
Scheme: "a-schema",
Scheme: source.ImageScheme,
ImageMetadata: source.ImageMetadata{
UserInput: "user-in",
Scope: "scope!",
Layers: nil,
Size: 10,
ManifestDigest: "sha256:digest!",
@ -192,7 +190,7 @@ func TestPackageSbomImport(t *testing.T) {
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
theModel, err := packageSbomModel(m, catalog, &d)
theModel, err := packageSbomModel(m, catalog, &d, source.AllLayersScope)
if err != nil {
t.Fatalf("could not get sbom model: %+v", err)
}
@ -231,7 +229,7 @@ func TestPackageSbomImport(t *testing.T) {
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, &progress.Stage{})
digest, err := importPackageSBOM(context.TODO(), test.api, sessionID, m, catalog, &d, source.AllLayersScope, &progress.Stage{})
// validate error handling
if err != nil && !test.expectsError {

View File

@ -0,0 +1,13 @@
package config
type anchore struct {
// upload options
Host string `yaml:"host" json:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to (setting this value enables upload)
Path string `yaml:"path" json:"path" mapstructure:"path"` // override the engine/enterprise API upload path
// IMPORTANT: do not show the username in any YAML/JSON output (sensitive information)
Username string `yaml:"-" json:"-" mapstructure:"username"` // -u , username to authenticate upload
// IMPORTANT: do not show the password in any YAML/JSON output (sensitive information)
Password string `yaml:"-" json:"-" mapstructure:"password"` // -p , password to authenticate upload
Dockerfile string `yaml:"dockerfile" json:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
OverwriteExistingImage bool `yaml:"overwrite-existing-image" json:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload
}

View File

@ -0,0 +1,196 @@
package config
import (
"errors"
"fmt"
"path"
"strings"
"github.com/anchore/syft/syft/source"
"github.com/adrg/xdg"
"github.com/anchore/syft/internal"
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
var ErrApplicationConfigNotFound = fmt.Errorf("application config not found")
// Application is the main syft application configuration.
type Application struct {
ConfigPath string `yaml:",omitempty" json:"configPath"` // the location where the application config was read from (either from -c or discovered while loading)
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)
Log logging `yaml:"log" json:"log" mapstructure:"log"` // all logging-related options
CliOptions CliOnlyOptions `yaml:"-" json:"-"` // all options only available through the CLI (not via env vars or config)
Dev Development `yaml:"dev" json:"dev" mapstructure:"dev"`
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
Anchore anchore `yaml:"anchore" json:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
Package Packages `yaml:"package" json:"package" mapstructure:"package"`
FileMetadata FileMetadata `yaml:"file-metadata" json:"file-metadata" mapstructure:"file-metadata"`
}
// LoadApplicationConfig populates the given viper object with application configuration discovered on disk
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
setNonCliDefaultAppConfigValues(v)
if err := readConfig(v, cliOpts.ConfigPath); err != nil && !errors.Is(err, ErrApplicationConfigNotFound) {
return nil, err
}
config := &Application{
CliOptions: cliOpts,
}
if err := v.Unmarshal(config); err != nil {
return nil, fmt.Errorf("unable to parse config: %w", err)
}
config.ConfigPath = v.ConfigFileUsed()
if err := config.build(); err != nil {
return nil, fmt.Errorf("invalid application config: %w", err)
}
return config, nil
}
// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in.
func (cfg *Application) build() error {
if cfg.Quiet {
// TODO: this is bad: quiet option trumps all other logging options
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
} else {
if cfg.Log.Level != "" {
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
}
// set the log level explicitly
cfg.Log.LevelOpt = lvl
} else {
// set the log level implicitly
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.WarnLevel
}
}
}
if cfg.Anchore.Host == "" && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
}
for _, builder := range []func() error{
cfg.Package.build,
cfg.FileMetadata.build,
} {
if err := builder(); err != nil {
return err
}
}
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)
}
// readConfig attempts to read the given config path from disk or discover an alternate store location
// nolint:funlen
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 .<appname>.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 .<appname>/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 ~/.<appname>.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 <appname>/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
}
// setNonCliDefaultAppConfigValues ensures that there are sane defaults for values that do not have CLI equivalent options (where there would already be a default value)
func setNonCliDefaultAppConfigValues(v *viper.Viper) {
v.SetDefault("anchore.path", "")
v.SetDefault("log.structured", false)
v.SetDefault("check-for-app-update", true)
v.SetDefault("dev.profile-cpu", false)
v.SetDefault("dev.profile-mem", false)
v.SetDefault("package.cataloger.enabled", true)
v.SetDefault("file-metadata.cataloger.enabled", true)
v.SetDefault("file-metadata.cataloger.scope", source.SquashedScope)
v.SetDefault("file-metadata.digests", []string{"sha256"})
}

View File

@ -0,0 +1,23 @@
package config
import (
"fmt"
"github.com/anchore/syft/syft/source"
)
type catalogerOptions struct {
Enabled bool `yaml:"enabled" json:"enabled" mapstructure:"enabled"`
Scope string `yaml:"scope" json:"scope" mapstructure:"scope"`
ScopeOpt source.Scope `yaml:"-" json:"-"`
}
func (cfg *catalogerOptions) build() error {
scopeOption := source.ParseScope(cfg.Scope)
if scopeOption == source.UnknownScope {
return fmt.Errorf("bad scope value %q", cfg.Scope)
}
cfg.ScopeOpt = scopeOption
return nil
}

View File

@ -0,0 +1,7 @@
package config
// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file)
type CliOnlyOptions struct {
ConfigPath string // -c. where the read config is on disk
Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be
}

View File

@ -1,228 +0,0 @@
package config
import (
"fmt"
"path"
"strings"
"github.com/adrg/xdg"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"github.com/mitchellh/go-homedir"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
"gopkg.in/yaml.v2"
)
// Application is the main syft application configuration.
type Application struct {
ConfigPath string `yaml:",omitempty"` // the location where the application config was read from (either from -c or discovered while loading)
PresenterOpt presenter.Option `yaml:"-"` // -o, the native Presenter.Option to use for report formatting
Output string `yaml:"output" mapstructure:"output"` // -o, the Presenter hint string to use for report formatting
ScopeOpt source.Scope `yaml:"-"` // -s, the native source.Scope option to use for how to catalog the container image
Scope string `yaml:"scope" mapstructure:"scope"` // -s, the source.Scope string hint for how to catalog the container image
Quiet bool `yaml:"quiet" mapstructure:"quiet"` // -q, indicates to not show any status output to stderr (ETUI or logging UI)
Log logging `yaml:"log" mapstructure:"log"` // all logging-related options
CliOptions CliOnlyOptions `yaml:"-"` // all options only available through the CLI (not via env vars or config)
Dev Development `mapstructure:"dev"`
CheckForAppUpdate bool `yaml:"check-for-app-update" mapstructure:"check-for-app-update"` // whether to check for an application update on start up or not
Anchore anchore `yaml:"anchore" mapstructure:"anchore"` // options for interacting with Anchore Engine/Enterprise
}
// CliOnlyOptions are options that are in the application config in memory, but are only exposed via CLI switches (not from unmarshaling a config file)
type CliOnlyOptions struct {
ConfigPath string // -c. where the read config is on disk
Verbosity int // -v or -vv , controlling which UI (ETUI vs logging) and what the log level should be
}
// logging contains all logging-related configuration options available to the user via the application config.
type logging struct {
Structured bool `yaml:"structured" mapstructure:"structured"` // show all log entries as JSON formatted strings
LevelOpt logrus.Level `yaml:"level"` // the native log level object used by the logger
Level string `yaml:"-" mapstructure:"level"` // the log level string hint
FileLocation string `yaml:"file" mapstructure:"file"` // the file path to write logs to
}
type anchore struct {
// upload options
UploadEnabled bool `yaml:"upload-enabled" mapstructure:"upload-enabled"` // whether to upload results to Anchore Engine/Enterprise (defaults to "false" unless there is the presence of -h CLI option)
Host string `yaml:"host" mapstructure:"host"` // -H , hostname of the engine/enterprise instance to upload to
Path string `yaml:"path" mapstructure:"path"` // override the engine/enterprise API upload path
Username string `yaml:"username" mapstructure:"username"` // -u , username to authenticate upload
Password string `yaml:"password" mapstructure:"password"` // -p , password to authenticate upload
Dockerfile string `yaml:"dockerfile" mapstructure:"dockerfile"` // -d , dockerfile to attach for upload
OverwriteExistingImage bool `yaml:"overwrite-existing-image" mapstructure:"overwrite-existing-image"` // --overwrite-existing-image , if any of the SBOM components have already been uploaded this flag will ensure they are overwritten with the current upload
}
type Development struct {
ProfileCPU bool `mapstructure:"profile-cpu"`
ProfileMem bool `mapstructure:"profile-mem"`
}
// LoadApplicationConfig populates the given viper object with application configuration discovered on disk
func LoadApplicationConfig(v *viper.Viper, cliOpts CliOnlyOptions, wasHostnameSet bool) (*Application, error) {
// the user may not have a config, and this is OK, we can use the default config + default cobra cli values instead
setNonCliDefaultValues(v)
_ = readConfig(v, cliOpts.ConfigPath)
config := &Application{
CliOptions: cliOpts,
}
if err := v.Unmarshal(config); err != nil {
return nil, fmt.Errorf("unable to parse config: %w", err)
}
config.ConfigPath = v.ConfigFileUsed()
if err := config.build(v, wasHostnameSet); err != nil {
return nil, fmt.Errorf("invalid config: %w", err)
}
return config, nil
}
// build inflates simple config values into syft native objects (or other complex objects) after the config is fully read in.
func (cfg *Application) build(v *viper.Viper, wasHostnameSet bool) error {
// set the presenter
presenterOption := presenter.ParseOption(cfg.Output)
if presenterOption == presenter.UnknownPresenter {
return fmt.Errorf("bad --output value '%s'", cfg.Output)
}
cfg.PresenterOpt = presenterOption
// set the source
scopeOption := source.ParseScope(cfg.Scope)
if scopeOption == source.UnknownScope {
return fmt.Errorf("bad --scope value '%s'", cfg.Scope)
}
cfg.ScopeOpt = scopeOption
if cfg.Quiet {
// TODO: this is bad: quiet option trumps all other logging options
// we should be able to quiet the console logging and leave file logging alone...
// ... this will be an enhancement for later
cfg.Log.LevelOpt = logrus.PanicLevel
} else {
if cfg.Log.Level != "" {
if cfg.CliOptions.Verbosity > 0 {
return fmt.Errorf("cannot explicitly set log level (cfg file or env var) and use -v flag together")
}
lvl, err := logrus.ParseLevel(strings.ToLower(cfg.Log.Level))
if err != nil {
return fmt.Errorf("bad log level configured (%q): %w", cfg.Log.Level, err)
}
// set the log level explicitly
cfg.Log.LevelOpt = lvl
} else {
// set the log level implicitly
switch v := cfg.CliOptions.Verbosity; {
case v == 1:
cfg.Log.LevelOpt = logrus.InfoLevel
case v >= 2:
cfg.Log.LevelOpt = logrus.DebugLevel
default:
cfg.Log.LevelOpt = logrus.WarnLevel
}
}
}
// check if upload should be done relative to the CLI and config behavior
if !v.IsSet("anchore.upload-enabled") && wasHostnameSet {
// we know the user didn't specify to upload in the config file and a --hostname option was provided (so set upload)
cfg.Anchore.UploadEnabled = true
}
if !cfg.Anchore.UploadEnabled && cfg.Anchore.Dockerfile != "" {
return fmt.Errorf("cannot provide dockerfile option without enabling upload")
}
return nil
}
func (cfg Application) String() string {
// redact sensitive information
if cfg.Anchore.Username != "" {
cfg.Anchore.Username = "********"
}
if cfg.Anchore.Password != "" {
cfg.Anchore.Password = "********"
}
// 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)
}
// readConfig attempts to read the given config path from disk or discover an alternate store location
func readConfig(v *viper.Viper, configPath string) 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 nil
}
// don't fall through to other options if this fails
return fmt.Errorf("unable to read config: %v", configPath)
}
// start searching for valid configs in order...
// 1. look for .<appname>.yaml (in the current directory)
v.AddConfigPath(".")
v.SetConfigName(internal.ApplicationName)
if err := v.ReadInConfig(); err == nil {
return nil
}
// 2. look for .<appname>/config.yaml (in the current directory)
v.AddConfigPath("." + internal.ApplicationName)
v.SetConfigName("config")
if err := v.ReadInConfig(); err == nil {
return nil
}
// 3. look for ~/.<appname>.yaml
home, err := homedir.Dir()
if err == nil {
v.AddConfigPath(home)
v.SetConfigName("." + internal.ApplicationName)
if err := v.ReadInConfig(); err == nil {
return nil
}
}
// 4. look for <appname>/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
}
return fmt.Errorf("application config not found")
}
// setNonCliDefaultValues ensures that there are sane defaults for values that do not have CLI equivalent options (where there would already be a default value)
func setNonCliDefaultValues(v *viper.Viper) {
v.SetDefault("log.level", "")
v.SetDefault("log.file", "")
v.SetDefault("log.structured", false)
v.SetDefault("check-for-app-update", true)
v.SetDefault("dev.profile-cpu", false)
v.SetDefault("dev.profile-mem", false)
}

View File

@ -0,0 +1,6 @@
package config
type Development struct {
ProfileCPU bool `yaml:"profile-cpu" json:"profile-cpu" mapstructure:"profile-cpu"`
ProfileMem bool `yaml:"profile-mem" json:"profile-mem" mapstructure:"profile-mem"`
}

View File

@ -0,0 +1,10 @@
package config
type FileMetadata struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
Digests []string `yaml:"digests" json:"digests" mapstructure:"digests"`
}
func (cfg *FileMetadata) build() error {
return cfg.Cataloger.build()
}

View File

@ -0,0 +1,11 @@
package config
import "github.com/sirupsen/logrus"
// 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
LevelOpt logrus.Level `yaml:"-" json:"-"` // the native log level object used by the logger
Level string `yaml:"level" json:"level" mapstructure:"level"` // the log level string hint
FileLocation string `yaml:"file" json:"file-location" mapstructure:"file"` // the file path to write logs to
}

View File

@ -0,0 +1,9 @@
package config
type Packages struct {
Cataloger catalogerOptions `yaml:"cataloger" json:"cataloger" mapstructure:"cataloger"`
}
func (cfg *Packages) build() error {
return cfg.Cataloger.build()
}

View File

@ -6,5 +6,5 @@ const (
// JSONSchemaVersion is the current schema version output by the JSON presenter
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
JSONSchemaVersion = "1.0.3"
JSONSchemaVersion = "1.0.4"
)

View File

@ -0,0 +1,69 @@
package packages
import (
"encoding/xml"
"time"
"github.com/anchore/syft/syft/source"
)
// Source: https://cyclonedx.org/ext/bom-descriptor/
// CycloneDxBomDescriptor represents all metadata surrounding the BOM report (such as when the BOM was made, with which tool, and the item being cataloged).
type CycloneDxBomDescriptor struct {
XMLName xml.Name `xml:"metadata"`
Timestamp string `xml:"timestamp,omitempty"` // The date and time (timestamp) when the document was created
Tools []CycloneDxBdTool `xml:"tools>tool"` // The tool used to create the BOM.
Component *CycloneDxBdComponent `xml:"component"` // The component that the BOM describes.
}
// CycloneDxBdTool represents the tool that created the BOM report.
type CycloneDxBdTool struct {
XMLName xml.Name `xml:"tool"`
Vendor string `xml:"vendor,omitempty"` // The vendor of the tool used to create the BOM.
Name string `xml:"name,omitempty"` // The name of the tool used to create the BOM.
Version string `xml:"version,omitempty"` // The version of the tool used to create the BOM.
// TODO: hashes, author, manufacture, supplier
// TODO: add user-defined fields for the remaining build/version parameters
}
// CycloneDxBdComponent represents the software/package being cataloged.
type CycloneDxBdComponent struct {
XMLName xml.Name `xml:"component"`
CycloneDxComponent
}
// NewCycloneDxBomDescriptor returns a new CycloneDxBomDescriptor tailored for the current time and "syft" tool details.
func NewCycloneDxBomDescriptor(name, version string, srcMetadata source.Metadata) *CycloneDxBomDescriptor {
descriptor := CycloneDxBomDescriptor{
XMLName: xml.Name{},
Timestamp: time.Now().Format(time.RFC3339),
Tools: []CycloneDxBdTool{
{
Vendor: "anchore",
Name: name,
Version: version,
},
},
}
switch srcMetadata.Scheme {
case source.ImageScheme:
descriptor.Component = &CycloneDxBdComponent{
CycloneDxComponent: CycloneDxComponent{
Type: "container",
Name: srcMetadata.ImageMetadata.UserInput,
Version: srcMetadata.ImageMetadata.ManifestDigest,
},
}
case source.DirectoryScheme:
descriptor.Component = &CycloneDxBdComponent{
CycloneDxComponent: CycloneDxComponent{
Type: "file",
Name: srcMetadata.Path,
},
}
}
return &descriptor
}

View File

@ -0,0 +1,27 @@
package packages
import "encoding/xml"
// CycloneDxComponent represents a single element in the CycloneDX BOM
type CycloneDxComponent struct {
XMLName xml.Name `xml:"component"`
Type string `xml:"type,attr"` // Required; Describes if the component is a library, framework, application, container, operating system, firmware, hardware device, or file
Supplier string `xml:"supplier,omitempty"` // The organization that supplied the component. The supplier may often be the manufacture, but may also be a distributor or repackager.
Author string `xml:"author,omitempty"` // The person(s) or organization(s) that authored the component
Publisher string `xml:"publisher,omitempty"` // The person(s) or organization(s) that published the component
Group string `xml:"group,omitempty"` // The high-level classification that a project self-describes as. This will often be a shortened, single name of the company or project that produced the component, or the source package or domain name.
Name string `xml:"name"` // Required; The name of the component as defined by the project
Version string `xml:"version"` // Required; The version of the component as defined by the project
Description string `xml:"description,omitempty"` // A description of the component
Licenses *[]CycloneDxLicense `xml:"licenses>license"` // A node describing zero or more license names, SPDX license IDs or expressions
PackageURL string `xml:"purl,omitempty"` // Specifies the package-url (PackageURL). The purl, if specified, must be valid and conform to the specification defined at: https://github.com/package-url/purl-spec
// TODO: source, hashes, copyright, cpe, purl, swid, modified, pedigree, externalReferences
// TODO: add user-defined parameters for syft-specific values (image layer index, cataloger, location path, etc.)
}
// CycloneDxLicense represents a single software license for a CycloneDxComponent
type CycloneDxLicense struct {
XMLName xml.Name `xml:"license"`
ID string `xml:"id,omitempty"` // A valid SPDX license ID
Name string `xml:"name,omitempty"` // If SPDX does not define the license used, this field may be used to provide the license name
}

View File

@ -0,0 +1,57 @@
package packages
import (
"encoding/xml"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/google/uuid"
)
// Source: https://github.com/CycloneDX/specification
// CycloneDxDocument represents a CycloneDX BOM CycloneDxDocument.
type CycloneDxDocument struct {
XMLName xml.Name `xml:"bom"`
XMLNs string `xml:"xmlns,attr"`
Version int `xml:"version,attr"`
SerialNumber string `xml:"serialNumber,attr"`
BomDescriptor *CycloneDxBomDescriptor `xml:"metadata"` // The BOM descriptor extension
Components []CycloneDxComponent `xml:"components>component"` // The BOM contents
}
// NewCycloneDxDocument returns a CycloneDX CycloneDxDocument object populated with the catalog contents.
func NewCycloneDxDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) CycloneDxDocument {
versionInfo := version.FromBuild()
doc := CycloneDxDocument{
XMLNs: "http://cyclonedx.org/schema/bom/1.2",
Version: 1,
SerialNumber: uuid.New().URN(),
BomDescriptor: NewCycloneDxBomDescriptor(internal.ApplicationName, versionInfo.Version, srcMetadata),
}
// attach components
for p := range catalog.Enumerate() {
component := CycloneDxComponent{
Type: "library", // TODO: this is not accurate
Name: p.Name,
Version: p.Version,
PackageURL: p.PURL,
}
var licenses []CycloneDxLicense
for _, licenseName := range p.Licenses {
licenses = append(licenses, CycloneDxLicense{
Name: licenseName,
})
}
if len(licenses) > 0 {
component.Licenses = &licenses
}
doc.Components = append(doc.Components, component)
}
return doc
}

View File

@ -1,7 +1,7 @@
/*
Package cyclonedx is responsible for generating a CycloneDX XML report for the given container image or file system.
*/
package cyclonedx
package packages
import (
"encoding/xml"
@ -11,23 +11,23 @@ import (
"github.com/anchore/syft/syft/source"
)
// Presenter writes a CycloneDX report from the given Catalog and Locations contents
type Presenter struct {
// CycloneDxPresenter writes a CycloneDX report from the given Catalog and Locations contents
type CycloneDxPresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
}
// NewPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
return &Presenter{
// NewCycloneDxPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
func NewCycloneDxPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *CycloneDxPresenter {
return &CycloneDxPresenter{
catalog: catalog,
srcMetadata: srcMetadata,
}
}
// Present writes the CycloneDX report to the given io.Writer.
func (pres *Presenter) Present(output io.Writer) error {
bom := NewDocument(pres.catalog, pres.srcMetadata)
func (pres *CycloneDxPresenter) Present(output io.Writer) error {
bom := NewCycloneDxDocument(pres.catalog, pres.srcMetadata)
encoder := xml.NewEncoder(output)
encoder.Indent("", " ")

View File

@ -1,4 +1,4 @@
package cyclonedx
package packages
import (
"bytes"
@ -62,7 +62,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
t.Fatal(err)
}
pres := NewPresenter(catalog, s.Metadata)
pres := NewCycloneDxPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
@ -93,8 +93,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
defer cleanup()
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
@ -125,7 +124,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
PURL: "the-purl-2",
})
s, err := source.NewFromImage(img, source.SquashedScope, "user-image-input")
s, err := source.NewFromImage(img, "user-image-input")
if err != nil {
t.Fatal(err)
}
@ -138,7 +137,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
pres := NewPresenter(catalog, s.Metadata)
pres := NewCycloneDxPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)

View File

@ -1,21 +1,21 @@
package json
package packages
import "github.com/anchore/syft/syft/distro"
// Distribution provides information about a detected Linux Distribution.
type Distribution struct {
// JSONDistribution provides information about a detected Linux JSONDistribution.
type JSONDistribution struct {
Name string `json:"name"` // Name of the Linux distribution
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
}
// NewDistribution creates a struct with the Linux distribution to be represented in JSON.
func NewDistribution(d *distro.Distro) Distribution {
// NewJSONDistribution creates a struct with the Linux distribution to be represented in JSON.
func NewJSONDistribution(d *distro.Distro) JSONDistribution {
if d == nil {
return Distribution{}
return JSONDistribution{}
}
return Distribution{
return JSONDistribution{
Name: d.Name(),
Version: d.FullVersion(),
IDLike: d.IDLike,

View File

@ -0,0 +1,62 @@
package packages
import (
"fmt"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONDocument represents the syft cataloging findings as a JSON document
type JSONDocument struct {
Artifacts []JSONPackage `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
ArtifactRelationships []JSONRelationship `json:"artifactRelationships"`
Source JSONSource `json:"source"` // Source represents the original object that was cataloged
Distro JSONDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
Descriptor JSONDescriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
Schema JSONSchema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
}
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (JSONDocument, error) {
src, err := NewJSONSource(srcMetadata, scope)
if err != nil {
return JSONDocument{}, err
}
artifacts, err := NewJSONPackages(catalog)
if err != nil {
return JSONDocument{}, err
}
return JSONDocument{
Artifacts: artifacts,
ArtifactRelationships: newJSONRelationships(pkg.NewRelationships(catalog)),
Source: src,
Distro: NewJSONDistribution(d),
Descriptor: JSONDescriptor{
Name: internal.ApplicationName,
Version: version.FromBuild().Version,
Configuration: configuration,
},
Schema: JSONSchema{
Version: internal.JSONSchemaVersion,
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
},
}, nil
}
// JSONDescriptor describes what created the document as well as surrounding metadata
type JSONDescriptor struct {
Name string `json:"name"`
Version string `json:"version"`
Configuration interface{} `json:"configuration,omitempty"`
}
type JSONSchema struct {
Version string `json:"version"`
URL string `json:"url"`
}

View File

@ -0,0 +1,71 @@
package packages
import (
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONPackage represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
type JSONPackage struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Type string `json:"type"`
FoundBy string `json:"foundBy"`
Locations []source.Location `json:"locations"`
Licenses []string `json:"licenses"`
Language string `json:"language"`
CPEs []string `json:"cpes"`
PURL string `json:"purl"`
MetadataType string `json:"metadataType"`
Metadata interface{} `json:"metadata"`
}
func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) {
artifacts := make([]JSONPackage, 0)
if catalog == nil {
return artifacts, nil
}
for _, p := range catalog.Sorted() {
art, err := NewJSONPackage(p)
if err != nil {
return nil, err
}
artifacts = append(artifacts, art)
}
return artifacts, nil
}
// NewJSONPackage crates a new JSONPackage from the given pkg.Package.
func NewJSONPackage(p *pkg.Package) (JSONPackage, error) {
var cpes = make([]string, len(p.CPEs))
for i, c := range p.CPEs {
cpes[i] = c.BindToFmtString()
}
// ensure collections are never nil for presentation reasons
var locations = make([]source.Location, 0)
if p.Locations != nil {
locations = p.Locations
}
var licenses = make([]string, 0)
if p.Licenses != nil {
licenses = p.Licenses
}
return JSONPackage{
ID: string(p.ID),
Name: p.Name,
Version: p.Version,
Type: string(p.Type),
FoundBy: p.FoundBy,
Locations: locations,
Licenses: licenses,
Language: string(p.Language),
CPEs: cpes,
PURL: p.PURL,
MetadataType: string(p.MetadataType),
Metadata: p.Metadata,
}, nil
}

View File

@ -0,0 +1,43 @@
package packages
import (
"encoding/json"
"io"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// JSONPresenter is a JSON presentation object for the syft results
type JSONPresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
distro *distro.Distro
scope source.Scope
}
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewJSONPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro, scope source.Scope) *JSONPresenter {
return &JSONPresenter{
catalog: catalog,
srcMetadata: s,
distro: d,
scope: scope,
}
}
// Present the catalog results to the given writer.
func (pres *JSONPresenter) Present(output io.Writer) error {
// we do not pass in configuration for backwards compatibility
doc, err := NewJSONDocument(pres.catalog, pres.srcMetadata, pres.distro, pres.scope, nil)
if err != nil {
return err
}
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}

View File

@ -1,4 +1,4 @@
package json
package packages
import (
"bytes"
@ -15,7 +15,7 @@ import (
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
func must(c pkg.CPE, e error) pkg.CPE {
if e != nil {
@ -24,7 +24,7 @@ func must(c pkg.CPE, e error) pkg.CPE {
return c
}
func TestJsonDirsPresenter(t *testing.T) {
func TestJSONDirsPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
@ -75,7 +75,7 @@ func TestJsonDirsPresenter(t *testing.T) {
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s.Metadata, d)
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
// run presenter
err = pres.Present(&buffer)
@ -84,7 +84,7 @@ func TestJsonDirsPresenter(t *testing.T) {
}
actual := buffer.Bytes()
if *update {
if *updateJSONGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
@ -98,12 +98,12 @@ func TestJsonDirsPresenter(t *testing.T) {
}
func TestJsonImgsPresenter(t *testing.T) {
func TestJSONImgsPresenter(t *testing.T) {
var buffer bytes.Buffer
testImage := "image-simple"
if *update {
if *updateJSONGoldenFiles {
imagetest.UpdateGoldenFixtureImage(t, testImage)
}
@ -158,9 +158,9 @@ func TestJsonImgsPresenter(t *testing.T) {
// this is a hard coded value that is not given by the fixture helper and must be provided manually
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
s, err := source.NewFromImage(img, source.SquashedScope, "user-image-input")
s, err := source.NewFromImage(img, "user-image-input")
var d *distro.Distro
pres := NewPresenter(catalog, s.Metadata, d)
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
// run presenter
err = pres.Present(&buffer)
@ -169,7 +169,7 @@ func TestJsonImgsPresenter(t *testing.T) {
}
actual := buffer.Bytes()
if *update {
if *updateJSONGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}

View File

@ -1,18 +1,18 @@
package json
package packages
import "github.com/anchore/syft/syft/pkg"
type Relationship struct {
type JSONRelationship struct {
Parent string `json:"parent"`
Child string `json:"child"`
Type string `json:"type"`
Metadata interface{} `json:"metadata"`
}
func newRelationships(relationships []pkg.Relationship) []Relationship {
result := make([]Relationship, len(relationships))
func newJSONRelationships(relationships []pkg.Relationship) []JSONRelationship {
result := make([]JSONRelationship, len(relationships))
for i, r := range relationships {
result[i] = Relationship{
result[i] = JSONRelationship{
Parent: string(r.Parent),
Child: string(r.Child),
Type: string(r.Type),

View File

@ -0,0 +1,39 @@
package packages
import (
"fmt"
"github.com/anchore/syft/syft/source"
)
// JSONSource object represents the thing that was cataloged
type JSONSource struct {
Type string `json:"type"`
Target interface{} `json:"target"`
}
type JSONImageSource struct {
source.ImageMetadata
Scope source.Scope `json:"scope"`
}
// NewJSONSource creates a new source object to be represented into JSON.
func NewJSONSource(src source.Metadata, scope source.Scope) (JSONSource, error) {
switch src.Scheme {
case source.ImageScheme:
return JSONSource{
Type: "image",
Target: JSONImageSource{
Scope: scope,
ImageMetadata: src.ImageMetadata,
},
}, nil
case source.DirectoryScheme:
return JSONSource{
Type: "directory",
Target: src.Path,
}, nil
default:
return JSONSource{}, fmt.Errorf("unsupported source: %q", src.Scheme)
}
}

View File

@ -0,0 +1,25 @@
/*
Defines a Presenter interface for displaying catalog results to an io.Writer as well as a helper utility to obtain
a specific Presenter implementation given user configuration.
*/
package packages
import (
"github.com/anchore/syft/internal/presenter"
)
// Presenter returns a presenter for images or directories
func Presenter(option PresenterOption, config PresenterConfig) presenter.Presenter {
switch option {
case JSONPresenterOption:
return NewJSONPresenter(config.Catalog, config.SourceMetadata, config.Distro, config.Scope)
case TextPresenterOption:
return NewTextPresenter(config.Catalog, config.SourceMetadata)
case TablePresenterOption:
return NewTablePresenter(config.Catalog)
case CycloneDxPresenterOption:
return NewCycloneDxPresenter(config.Catalog, config.SourceMetadata)
default:
return nil
}
}

View File

@ -0,0 +1,14 @@
package packages
import (
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
type PresenterConfig struct {
SourceMetadata source.Metadata
Catalog *pkg.Catalog
Distro *distro.Distro
Scope source.Scope
}

View File

@ -0,0 +1,35 @@
package packages
import "strings"
const (
UnknownPresenterOption PresenterOption = "UnknownPresenterOption"
JSONPresenterOption PresenterOption = "json"
TextPresenterOption PresenterOption = "text"
TablePresenterOption PresenterOption = "table"
CycloneDxPresenterOption PresenterOption = "cyclonedx"
)
var AllPresenters = []PresenterOption{
JSONPresenterOption,
TextPresenterOption,
TablePresenterOption,
CycloneDxPresenterOption,
}
type PresenterOption string
func ParsePresenterOption(userStr string) PresenterOption {
switch strings.ToLower(userStr) {
case string(JSONPresenterOption):
return JSONPresenterOption
case string(TextPresenterOption):
return TextPresenterOption
case string(TablePresenterOption):
return TablePresenterOption
case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx":
return CycloneDxPresenterOption
default:
return UnknownPresenterOption
}
}

View File

@ -1,4 +1,4 @@
package table
package packages
import (
"fmt"
@ -11,17 +11,17 @@ import (
"github.com/anchore/syft/syft/pkg"
)
type Presenter struct {
type TablePresenter struct {
catalog *pkg.Catalog
}
func NewPresenter(catalog *pkg.Catalog) *Presenter {
return &Presenter{
func NewTablePresenter(catalog *pkg.Catalog) *TablePresenter {
return &TablePresenter{
catalog: catalog,
}
}
func (pres *Presenter) Present(output io.Writer) error {
func (pres *TablePresenter) Present(output io.Writer) error {
rows := make([][]string, 0)
columns := []string{"Name", "Version", "Type"}
@ -42,7 +42,7 @@ func (pres *Presenter) Present(output io.Writer) error {
// sort by name, version, then type
sort.SliceStable(rows, func(i, j int) bool {
for col := 0; col < len(columns); col++ {
if rows[i][0] != rows[j][0] {
if rows[i][col] != rows[j][col] {
return rows[i][col] < rows[j][col]
}
}
@ -73,7 +73,6 @@ func (pres *Presenter) Present(output io.Writer) error {
func removeDuplicateRows(items [][]string) [][]string {
seen := map[string][]string{}
// nolint:prealloc
var result [][]string
for _, v := range items {

View File

@ -1,4 +1,4 @@
package table
package packages
import (
"bytes"
@ -16,7 +16,7 @@ import (
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for table presenters")
var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table presenters")
func TestTablePresenter(t *testing.T) {
var buffer bytes.Buffer
@ -24,8 +24,7 @@ func TestTablePresenter(t *testing.T) {
testImage := "image-simple"
catalog := pkg.NewCatalog()
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", testImage)
defer cleanup()
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
@ -48,7 +47,7 @@ func TestTablePresenter(t *testing.T) {
Type: pkg.DebPkg,
})
pres := NewPresenter(catalog)
pres := NewTablePresenter(catalog)
// run presenter
err := pres.Present(&buffer)
@ -57,7 +56,7 @@ func TestTablePresenter(t *testing.T) {
}
actual := buffer.Bytes()
if *update {
if *updateTablePresenterGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}

View File

@ -60,6 +60,7 @@
}
}
],
"artifactRelationships": [],
"source": {
"type": "directory",
"target": "/some/path"
@ -74,8 +75,7 @@
"version": "[not provided]"
},
"schema": {
"version": "1.0.3",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.3.json"
},
"artifactRelationships": []
"version": "1.0.4",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.4.json"
}
}

View File

@ -0,0 +1,107 @@
{
"artifacts": [
{
"id": "package-1-id",
"name": "package-1",
"version": "1.0.1",
"type": "python",
"foundBy": "the-cataloger-1",
"locations": [
{
"path": "/somefile-1.txt",
"layerID": "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b"
}
],
"licenses": [
"MIT"
],
"language": "python",
"cpes": [
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
],
"purl": "a-purl-1",
"metadataType": "PythonPackageMetadata",
"metadata": {
"name": "package-1",
"version": "1.0.1",
"license": "",
"author": "",
"authorEmail": "",
"platform": "",
"sitePackagesRootPath": ""
}
},
{
"id": "package-2-id",
"name": "package-2",
"version": "2.0.1",
"type": "deb",
"foundBy": "the-cataloger-2",
"locations": [
{
"path": "/somefile-2.txt",
"layerID": "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703"
}
],
"licenses": [],
"language": "",
"cpes": [
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
],
"purl": "a-purl-2",
"metadataType": "DpkgMetadata",
"metadata": {
"package": "package-2",
"source": "",
"version": "2.0.1",
"sourceVersion": "",
"architecture": "",
"maintainer": "",
"installedSize": 0,
"files": null
}
}
],
"artifactRelationships": [],
"source": {
"type": "image",
"target": {
"userInput": "user-image-input",
"imageID": "sha256:c2b46b4eb06296933b7cf0722683964e9ecbd93265b9ef6ae9642e3952afbba0",
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
"tags": [
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b"
],
"imageSize": 38,
"layers": [
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:3de16c5b8659a2e8d888b8ded8427be7a5686a3c8c4e4dd30de20f362827285b",
"size": 22
},
{
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
"digest": "sha256:366a3f5653e34673b875891b021647440d0127c2ef041e3b1a22da2a7d4f3703",
"size": 16
}
],
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjoxNTg2LCJkaWdlc3QiOiJzaGEyNTY6YzJiNDZiNGViMDYyOTY5MzNiN2NmMDcyMjY4Mzk2NGU5ZWNiZDkzMjY1YjllZjZhZTk2NDJlMzk1MmFmYmJhMCJ9LCJsYXllcnMiOlt7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuaW1hZ2Uucm9vdGZzLmRpZmYudGFyLmd6aXAiLCJzaXplIjoyMDQ4LCJkaWdlc3QiOiJzaGEyNTY6M2RlMTZjNWI4NjU5YTJlOGQ4ODhiOGRlZDg0MjdiZTdhNTY4NmEzYzhjNGU0ZGQzMGRlMjBmMzYyODI3Mjg1YiJ9LHsibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjozNjZhM2Y1NjUzZTM0NjczYjg3NTg5MWIwMjE2NDc0NDBkMDEyN2MyZWYwNDFlM2IxYTIyZGEyYTdkNGYzNzAzIn1dfQ==",
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJIb3N0bmFtZSI6IiIsIkRvbWFpbm5hbWUiOiIiLCJVc2VyIjoiIiwiQXR0YWNoU3RkaW4iOmZhbHNlLCJBdHRhY2hTdGRvdXQiOmZhbHNlLCJBdHRhY2hTdGRlcnIiOmZhbHNlLCJUdHkiOmZhbHNlLCJPcGVuU3RkaW4iOmZhbHNlLCJTdGRpbk9uY2UiOmZhbHNlLCJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiQ21kIjpudWxsLCJJbWFnZSI6InNoYTI1NjpkYWMyMTUwMzhjMDUwZTM1NzMwNTVlZmU4YTkwM2NkMWY5YmJkZmU0ZjlhZTlkODk5OTFjNTljY2M2OTA1MmU1IiwiVm9sdW1lcyI6bnVsbCwiV29ya2luZ0RpciI6IiIsIkVudHJ5cG9pbnQiOm51bGwsIk9uQnVpbGQiOm51bGwsIkxhYmVscyI6bnVsbH0sImNvbnRhaW5lcl9jb25maWciOnsiSG9zdG5hbWUiOiIiLCJEb21haW5uYW1lIjoiIiwiVXNlciI6IiIsIkF0dGFjaFN0ZGluIjpmYWxzZSwiQXR0YWNoU3Rkb3V0IjpmYWxzZSwiQXR0YWNoU3RkZXJyIjpmYWxzZSwiVHR5IjpmYWxzZSwiT3BlblN0ZGluIjpmYWxzZSwiU3RkaW5PbmNlIjpmYWxzZSwiRW52IjpbIlBBVEg9L3Vzci9sb2NhbC9zYmluOi91c3IvbG9jYWwvYmluOi91c3Ivc2JpbjovdXNyL2Jpbjovc2JpbjovYmluIl0sIkNtZCI6WyIvYmluL3NoIiwiLWMiLCIjKG5vcCkgQUREIGZpbGU6ZGYzYjc0NGY1NGE5YjE2YjliOWFlZDQwZTNlOThkOWNhMmI0OWY1YTc3ZDlmYThhOTc2OTBkN2JhZjU4ODgyMCBpbiAvc29tZWZpbGUtMi50eHQgIl0sIkltYWdlIjoic2hhMjU2OmRhYzIxNTAzOGMwNTBlMzU3MzA1NWVmZThhOTAzY2QxZjliYmRmZTRmOWFlOWQ4OTk5MWM1OWNjYzY5MDUyZTUiLCJWb2x1bWVzIjpudWxsLCJXb3JraW5nRGlyIjoiIiwiRW50cnlwb2ludCI6bnVsbCwiT25CdWlsZCI6bnVsbCwiTGFiZWxzIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDMtMjNUMTg6MTU6NTguODcyMjg5OFoiLCJkb2NrZXJfdmVyc2lvbiI6IjIwLjEwLjIiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0wMy0yM1QxODoxNTo1OC42MTc3OTU2WiIsImNyZWF0ZWRfYnkiOiIvYmluL3NoIC1jICMobm9wKSBBREQgZmlsZTphYzMyZGEyM2Q1MWU4MDFmMDJmOTI0MTIzZWQzMDk5MGViM2YwZmVjMWI5ZWQ0ZjBiMDZjMjRlODhiOWMzNjk1IGluIC9zb21lZmlsZS0xLnR4dCAifSx7ImNyZWF0ZWQiOiIyMDIxLTAzLTIzVDE4OjE1OjU4Ljg3MjI4OThaIiwiY3JlYXRlZF9ieSI6Ii9iaW4vc2ggLWMgIyhub3ApIEFERCBmaWxlOmRmM2I3NDRmNTRhOWIxNmI5YjlhZWQ0MGUzZTk4ZDljYTJiNDlmNWE3N2Q5ZmE4YTk3NjkwZDdiYWY1ODg4MjAgaW4gL3NvbWVmaWxlLTIudHh0ICJ9XSwib3MiOiJsaW51eCIsInJvb3RmcyI6eyJ0eXBlIjoibGF5ZXJzIiwiZGlmZl9pZHMiOlsic2hhMjU2OjNkZTE2YzViODY1OWEyZThkODg4YjhkZWQ4NDI3YmU3YTU2ODZhM2M4YzRlNGRkMzBkZTIwZjM2MjgyNzI4NWIiLCJzaGEyNTY6MzY2YTNmNTY1M2UzNDY3M2I4NzU4OTFiMDIxNjQ3NDQwZDAxMjdjMmVmMDQxZTNiMWEyMmRhMmE3ZDRmMzcwMyJdfX0=",
"scope": "Squashed"
}
},
"distro": {
"name": "",
"version": "",
"idLike": ""
},
"descriptor": {
"name": "syft",
"version": "[not provided]"
},
"schema": {
"version": "1.0.4",
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.4.json"
}
}

View File

@ -1,4 +1,4 @@
package text
package packages
import (
"fmt"
@ -10,22 +10,22 @@ import (
"github.com/anchore/syft/syft/source"
)
// Presenter is a human-friendly text presenter to represent package and source data.
type Presenter struct {
// TextPresenter is a human-friendly text presenter to represent package and source data.
type TextPresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
}
// NewPresenter creates a new presenter for the given set of catalog and image data.
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
return &Presenter{
// NewTextPresenter creates a new presenter for the given set of catalog and image data.
func NewTextPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *TextPresenter {
return &TextPresenter{
catalog: catalog,
srcMetadata: srcMetadata,
}
}
// Present is a method that is in charge of writing to an output buffer
func (pres *Presenter) Present(output io.Writer) error {
func (pres *TextPresenter) Present(output io.Writer) error {
// init the tabular writer
w := new(tabwriter.Writer)
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)

View File

@ -1,4 +1,4 @@
package text
package packages
import (
"bytes"
@ -14,7 +14,7 @@ import (
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for text presenters")
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
func TestTextDirPresenter(t *testing.T) {
var buffer bytes.Buffer
@ -37,7 +37,7 @@ func TestTextDirPresenter(t *testing.T) {
if err != nil {
t.Fatalf("unable to create source: %+v", err)
}
pres := NewPresenter(catalog, s.Metadata)
pres := NewTextPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
@ -46,7 +46,7 @@ func TestTextDirPresenter(t *testing.T) {
}
actual := buffer.Bytes()
if *update {
if *updateTextPresenterGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
@ -69,8 +69,7 @@ func TestTextImgPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
defer cleanup()
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
@ -102,18 +101,18 @@ func TestTextImgPresenter(t *testing.T) {
l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53"
}
s, err := source.NewFromImage(img, source.SquashedScope, "user-image-input")
s, err := source.NewFromImage(img, "user-image-input")
if err != nil {
t.Fatal(err)
}
pres := NewPresenter(catalog, s.Metadata)
pres := NewTextPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
if *updateTextPresenterGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}

View File

@ -0,0 +1,32 @@
package poweruser
import (
"github.com/anchore/syft/internal/presenter/packages"
)
type JSONDocument struct {
// note: poweruser.JSONDocument is meant to always be a superset of packages.JSONDocument, any additional fields
// here should be optional by supplying "omitempty" on these fields hint to the jsonschema generator to not
// require these fields. As an accepted rule in this repo all collections should still be initialized in the
// context of being used in a JSON document.
FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"`
packages.JSONDocument
}
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
pkgsDoc, err := packages.NewJSONDocument(config.PackageCatalog, config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig)
if err != nil {
return JSONDocument{}, err
}
fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests)
if err != nil {
return JSONDocument{}, err
}
return JSONDocument{
FileMetadata: fileMetadata,
JSONDocument: pkgsDoc,
}, nil
}

View File

@ -0,0 +1,18 @@
package poweruser
import (
"github.com/anchore/syft/internal/config"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
type JSONDocumentConfig struct {
ApplicationConfig config.Application
PackageCatalog *pkg.Catalog
FileMetadata map[source.Location]source.FileMetadata
FileDigests map[source.Location][]file.Digest
Distro *distro.Distro
SourceMetadata source.Metadata
}

View File

@ -0,0 +1,59 @@
package poweruser
import (
"fmt"
"sort"
"strconv"
"github.com/anchore/syft/syft/file"
"github.com/anchore/syft/syft/source"
)
type JSONFileMetadata struct {
Location source.Location `json:"location"`
Metadata JSONFileMetadataEntry `json:"metadata"`
}
type JSONFileMetadataEntry struct {
Mode int `json:"mode"`
Type source.FileType `json:"type"`
UserID int `json:"userID"`
GroupID int `json:"groupID"`
Digests []file.Digest `json:"digests"`
}
func NewJSONFileMetadata(data map[source.Location]source.FileMetadata, digests map[source.Location][]file.Digest) ([]JSONFileMetadata, error) {
results := make([]JSONFileMetadata, 0)
for location, metadata := range data {
mode, err := strconv.Atoi(fmt.Sprintf("%o", metadata.Mode))
if err != nil {
return nil, fmt.Errorf("invalid mode found in file catalog @ location=%+v mode=%q: %w", location, metadata.Mode, err)
}
digestResults := make([]file.Digest, 0)
if digestsForLocation, exists := digests[location]; exists {
digestResults = digestsForLocation
}
results = append(results, JSONFileMetadata{
Location: location,
Metadata: JSONFileMetadataEntry{
Mode: mode,
Type: metadata.Type,
UserID: metadata.UserID,
GroupID: metadata.GroupID,
Digests: digestResults,
},
})
}
// sort by real path then virtual path to ensure the result is stable across multiple runs
sort.SliceStable(results, func(i, j int) bool {
if results[i].Location.RealPath != results[j].Location.RealPath {
return results[i].Location.VirtualPath < results[j].Location.VirtualPath
}
return false
})
return results, nil
}

View File

@ -0,0 +1,32 @@
package poweruser
import (
"encoding/json"
"io"
)
// JSONPresenter is a JSON presentation object for the syft results
type JSONPresenter struct {
config JSONDocumentConfig
}
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewJSONPresenter(config JSONDocumentConfig) *JSONPresenter {
return &JSONPresenter{
config: config,
}
}
// Present the PackageCatalog results to the given writer.
func (p *JSONPresenter) Present(output io.Writer) error {
doc, err := NewJSONDocument(p.config)
if err != nil {
return err
}
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}

View File

@ -0,0 +1,9 @@
package presenter
import "io"
// Presenter defines the expected behavior for an object responsible for displaying arbitrary input and processed data
// to a given io.Writer.
type Presenter interface {
Present(io.Writer) error
}

View File

@ -8,11 +8,11 @@ import (
"github.com/wagoodman/go-partybus"
)
// CatalogerFinishedHandler is a UI function for processing the CatalogerFinished bus event, displaying the catalog
// CatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
// via the given presenter to stdout.
func CatalogerFinishedHandler(event partybus.Event) error {
func CatalogerPresenterReady(event partybus.Event) error {
// show the report to stdout
pres, err := syftEventParsers.ParseCatalogerFinished(event)
pres, err := syftEventParsers.ParsePresenterReady(event)
if err != nil {
return fmt.Errorf("bad CatalogerFinished event: %w", err)
}

View File

@ -129,7 +129,7 @@ eventLoop:
log.Errorf("unable to show %s event: %+v", e.Type, err)
}
case e.Type == syftEvent.CatalogerFinished:
case e.Type == syftEvent.PresenterReady:
// we may have other background processes still displaying progress, wait for them to
// finish before discontinuing dynamic content and showing the final report
wg.Wait()
@ -146,7 +146,7 @@ eventLoop:
fmt.Fprint(output, logBuffer.String())
}
if err := common.CatalogerFinishedHandler(e); err != nil {
if err := common.CatalogerPresenterReady(e); err != nil {
log.Errorf("unable to show %s event: %+v", e.Type, err)
}

View File

@ -24,8 +24,8 @@ eventLoop:
}
// ignore all events except for the final event
if e.Type == syftEvent.CatalogerFinished {
err := common.CatalogerFinishedHandler(e)
if e.Type == syftEvent.PresenterReady {
err := common.CatalogerPresenterReady(e)
if err != nil {
log.Errorf("unable to show catalog image finished event: %+v", err)
}

View File

@ -6,6 +6,7 @@ package version
import (
"fmt"
"runtime"
"strings"
)
const valueNotProvided = "[not provided]"
@ -28,6 +29,13 @@ type Version struct {
Platform string `json:"platform"` // GOOS and GOARCH at build-time
}
func (v Version) IsProductionBuild() bool {
if strings.Contains(v.Version, "SNAPSHOT") || strings.Contains(v.Version, valueNotProvided) {
return false
}
return true
}
// FromBuild provides all version details
func FromBuild() Version {
return Version{

View File

@ -20,13 +20,13 @@ var latestAppVersionURL = struct {
// IsUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
func IsUpdateAvailable() (bool, string, error) {
currentVersionStr := FromBuild().Version
currentVersion, err := hashiVersion.NewVersion(currentVersionStr)
currentBuildInfo := FromBuild()
if !currentBuildInfo.IsProductionBuild() {
// don't allow for non-production builds to check for a version.
return false, "", nil
}
currentVersion, err := hashiVersion.NewVersion(currentBuildInfo.Version)
if err != nil {
if currentVersionStr == valueNotProvided {
// this is the default build arg and should be ignored (this is not an error case)
return false, "", nil
}
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
}

View File

@ -81,6 +81,15 @@ func TestIsUpdateAvailable(t *testing.T) {
newVersion: "",
err: false,
},
{
name: "SnapshotBuildVersion",
buildVersion: "2.0.0-SHAPSHOT-a78bf9c",
latestVersion: "1.0.0",
code: 200,
isAvailable: false,
newVersion: "",
err: false,
},
{
name: "BadUpdateValidVersion",
buildVersion: "1.0.0",

View File

@ -1,10 +1,10 @@
# JSON Schema
This is the JSON schema for output from the JSON presenter (`syft <img> -o json`). The required inputs for defining the JSON schema are as follows:
This is the JSON schema for output from the JSON presenters (`syft packages <img> -o json` and `syft power-user <img>`). The required inputs for defining the JSON schema are as follows:
- the value of `internal.JSONSchemaVersion` that governs the schema filename
- the `Document` struct definition within `syft/presenters/json/document.go` that governs the overall document shape
- the `metadataContainer` struct definition within `schema/json/generate.go` that governs the allowable shapes of `pkg.Package.Metadata`
- the `Document` struct definition within `internal/presenters/poweruser/json_document.go` that governs the overall document shape
- the `artifactMetadataContainer` struct definition within `schema/json/generate.go` that governs the allowable shapes of `pkg.Package.Metadata`
With regard to testing the JSON schema, integration test cases provided by the developer are used as examples to validate that JSON output from Syft is always valid relative to the `schema/json/schema-$VERSION.json` file.
@ -26,7 +26,7 @@ When adding a new `pkg.*Metadata` that is assigned to the `pkg.Package.Metadata`
are done:
- a new integration test case is added to `test/integration/pkg_cases_test.go` that exercises the new package type with the new metadata
- the new metadata struct is added to the `metadataContainer` struct within `schema/json/generate.go`
- the new metadata struct is added to the `artifactMetadataContainer` struct within `schema/json/generate.go`
## Generating a New Schema

View File

@ -6,13 +6,14 @@ import (
"fmt"
"io/ioutil"
"os"
"reflect"
"sort"
"strings"
"github.com/alecthomas/jsonschema"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/presenter/poweruser"
"github.com/anchore/syft/syft/pkg"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
)
/*
@ -25,7 +26,7 @@ can be extended to include specific package metadata struct shapes in the future
// This should represent all possible metadatas represented in the pkg.Package.Metadata field (an interface{}).
// When a new package metadata definition is created it will need to be manually added here. The variable name does
// not matter as long as it is exported.
type metadataContainer struct {
type artifactMetadataContainer struct {
Apk pkg.ApkMetadata
Dpkg pkg.DpkgMetadata
Gem pkg.GemMetadata
@ -36,10 +37,23 @@ type metadataContainer struct {
Cargo pkg.CargoPackageMetadata
}
// nolint:funlen
func main() {
metadataSchema := jsonschema.Reflect(&metadataContainer{})
documentSchema := jsonschema.Reflect(&jsonPresenter.Document{})
write(encode(build()))
}
func build() *jsonschema.Schema {
reflector := &jsonschema.Reflector{
AllowAdditionalProperties: true,
TypeNamer: func(r reflect.Type) string {
name := r.Name()
if strings.HasPrefix(name, "JSON") {
name = strings.TrimPrefix(name, "JSON")
}
return name
},
}
documentSchema := reflector.ReflectFromType(reflect.TypeOf(&poweruser.JSONDocument{}))
metadataSchema := reflector.ReflectFromType(reflect.TypeOf(&artifactMetadataContainer{}))
// TODO: inject source definitions
@ -47,7 +61,7 @@ func main() {
var metadataNames []string
for name, definition := range metadataSchema.Definitions {
if name == "metadataContainer" {
if name == "artifactMetadataContainer" {
// ignore the definition for the fake container
continue
}
@ -71,22 +85,30 @@ func main() {
}
// set the "anyOf" field for Package.Metadata to be a conjunction of several types
documentSchema.Definitions["Package"].Properties.Set("metadata", map[string][]map[string]string{
documentSchema.Definitions["Document"].Properties.Set("artifacts.metadata", map[string][]map[string]string{
"anyOf": metadataTypes,
})
filename := fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion)
return documentSchema
}
func encode(schema *jsonschema.Schema) []byte {
var newSchemaBuffer = new(bytes.Buffer)
enc := json.NewEncoder(newSchemaBuffer)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
err := enc.Encode(&documentSchema)
err := enc.Encode(&schema)
if err != nil {
panic(err)
}
return newSchemaBuffer.Bytes()
}
func write(schema []byte) {
filename := fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion)
if _, err := os.Stat(filename); !os.IsNotExist(err) {
// check if the schema is the same...
existingFh, err := os.Open(filename)
@ -99,7 +121,7 @@ func main() {
panic(err)
}
if bytes.Equal(existingSchemaBytes, newSchemaBuffer.Bytes()) {
if bytes.Equal(existingSchemaBytes, schema) {
// the generated schema is the same, bail with no error :)
fmt.Println("No change to the existing schema!")
os.Exit(0)
@ -115,7 +137,7 @@ func main() {
panic(err)
}
_, err = fh.Write(newSchemaBuffer.Bytes())
_, err = fh.Write(schema)
if err != nil {
panic(err)
}

View File

@ -749,4 +749,4 @@
"type": "object"
}
}
}
}

View File

@ -0,0 +1,830 @@
{
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Document",
"definitions": {
"ApkFileRecord": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
},
"ownerUid": {
"type": "string"
},
"ownerGid": {
"type": "string"
},
"permissions": {
"type": "string"
},
"checksum": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"ApkMetadata": {
"required": [
"package",
"originPackage",
"maintainer",
"version",
"license",
"architecture",
"url",
"description",
"size",
"installedSize",
"pullDependencies",
"pullChecksum",
"gitCommitOfApkPort",
"files"
],
"properties": {
"package": {
"type": "string"
},
"originPackage": {
"type": "string"
},
"maintainer": {
"type": "string"
},
"version": {
"type": "string"
},
"license": {
"type": "string"
},
"architecture": {
"type": "string"
},
"url": {
"type": "string"
},
"description": {
"type": "string"
},
"size": {
"type": "integer"
},
"installedSize": {
"type": "integer"
},
"pullDependencies": {
"type": "string"
},
"pullChecksum": {
"type": "string"
},
"gitCommitOfApkPort": {
"type": "string"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/ApkFileRecord"
},
"type": "array"
}
},
"additionalProperties": true,
"type": "object"
},
"CargoPackageMetadata": {
"required": [
"name",
"version",
"source",
"checksum",
"dependencies"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"source": {
"type": "string"
},
"checksum": {
"type": "string"
},
"dependencies": {
"items": {
"type": "string"
},
"type": "array"
}
},
"additionalProperties": true,
"type": "object"
},
"Descriptor": {
"required": [
"name",
"version"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"configuration": {
"additionalProperties": true
}
},
"additionalProperties": true,
"type": "object"
},
"Digest": {
"required": [
"algorithm",
"value"
],
"properties": {
"algorithm": {
"type": "string"
},
"value": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"Distribution": {
"required": [
"name",
"version",
"idLike"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"idLike": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"Document": {
"required": [
"artifacts",
"artifactRelationships",
"source",
"distro",
"descriptor",
"schema"
],
"properties": {
"fileMetadata": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/FileMetadata"
},
"type": "array"
},
"artifacts": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Package"
},
"type": "array"
},
"artifactRelationships": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Relationship"
},
"type": "array"
},
"source": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Source"
},
"distro": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Distribution"
},
"descriptor": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Descriptor"
},
"schema": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Schema"
},
"artifacts.metadata": {
"anyOf": [
{
"type": "null"
},
{
"$ref": "#/definitions/ApkMetadata"
},
{
"$ref": "#/definitions/CargoPackageMetadata"
},
{
"$ref": "#/definitions/DpkgMetadata"
},
{
"$ref": "#/definitions/GemMetadata"
},
{
"$ref": "#/definitions/JavaMetadata"
},
{
"$ref": "#/definitions/NpmPackageJSONMetadata"
},
{
"$ref": "#/definitions/PythonPackageMetadata"
},
{
"$ref": "#/definitions/RpmdbMetadata"
}
]
}
},
"additionalProperties": true,
"type": "object"
},
"DpkgFileRecord": {
"required": [
"path",
"md5"
],
"properties": {
"path": {
"type": "string"
},
"md5": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"DpkgMetadata": {
"required": [
"package",
"source",
"version",
"sourceVersion",
"architecture",
"maintainer",
"installedSize",
"files"
],
"properties": {
"package": {
"type": "string"
},
"source": {
"type": "string"
},
"version": {
"type": "string"
},
"sourceVersion": {
"type": "string"
},
"architecture": {
"type": "string"
},
"maintainer": {
"type": "string"
},
"installedSize": {
"type": "integer"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/DpkgFileRecord"
},
"type": "array"
}
},
"additionalProperties": true,
"type": "object"
},
"FileMetadata": {
"required": [
"location",
"metadata"
],
"properties": {
"location": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Location"
},
"metadata": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/FileMetadataEntry"
}
},
"additionalProperties": true,
"type": "object"
},
"FileMetadataEntry": {
"required": [
"mode",
"type",
"userID",
"groupID",
"digests"
],
"properties": {
"mode": {
"type": "integer"
},
"type": {
"type": "string"
},
"userID": {
"type": "integer"
},
"groupID": {
"type": "integer"
},
"digests": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/Digest"
},
"type": "array"
}
},
"additionalProperties": true,
"type": "object"
},
"GemMetadata": {
"required": [
"name",
"version"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"files": {
"items": {
"type": "string"
},
"type": "array"
},
"authors": {
"items": {
"type": "string"
},
"type": "array"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"homepage": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"JavaManifest": {
"properties": {
"main": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
},
"namedSections": {
"patternProperties": {
".*": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
}
},
"type": "object"
}
},
"additionalProperties": true,
"type": "object"
},
"JavaMetadata": {
"required": [
"virtualPath"
],
"properties": {
"virtualPath": {
"type": "string"
},
"manifest": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/JavaManifest"
},
"pomProperties": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PomProperties"
}
},
"additionalProperties": true,
"type": "object"
},
"Location": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
},
"layerID": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"NpmPackageJSONMetadata": {
"required": [
"author",
"licenses",
"homepage",
"description",
"url"
],
"properties": {
"files": {
"items": {
"type": "string"
},
"type": "array"
},
"author": {
"type": "string"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"homepage": {
"type": "string"
},
"description": {
"type": "string"
},
"url": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"Package": {
"required": [
"id",
"name",
"version",
"type",
"foundBy",
"locations",
"licenses",
"language",
"cpes",
"purl",
"metadataType",
"metadata"
],
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"type": {
"type": "string"
},
"foundBy": {
"type": "string"
},
"locations": {
"items": {
"$ref": "#/definitions/Location"
},
"type": "array"
},
"licenses": {
"items": {
"type": "string"
},
"type": "array"
},
"language": {
"type": "string"
},
"cpes": {
"items": {
"type": "string"
},
"type": "array"
},
"purl": {
"type": "string"
},
"metadataType": {
"type": "string"
},
"metadata": {
"additionalProperties": true
}
},
"additionalProperties": true,
"type": "object"
},
"PomProperties": {
"required": [
"path",
"name",
"groupId",
"artifactId",
"version",
"extraFields"
],
"properties": {
"path": {
"type": "string"
},
"name": {
"type": "string"
},
"groupId": {
"type": "string"
},
"artifactId": {
"type": "string"
},
"version": {
"type": "string"
},
"extraFields": {
"patternProperties": {
".*": {
"type": "string"
}
},
"type": "object"
}
},
"additionalProperties": true,
"type": "object"
},
"PythonFileDigest": {
"required": [
"algorithm",
"value"
],
"properties": {
"algorithm": {
"type": "string"
},
"value": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"PythonFileRecord": {
"required": [
"path"
],
"properties": {
"path": {
"type": "string"
},
"digest": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PythonFileDigest"
},
"size": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"PythonPackageMetadata": {
"required": [
"name",
"version",
"license",
"author",
"authorEmail",
"platform",
"sitePackagesRootPath"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"license": {
"type": "string"
},
"author": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"platform": {
"type": "string"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/PythonFileRecord"
},
"type": "array"
},
"sitePackagesRootPath": {
"type": "string"
},
"topLevelPackages": {
"items": {
"type": "string"
},
"type": "array"
}
},
"additionalProperties": true,
"type": "object"
},
"Relationship": {
"required": [
"parent",
"child",
"type",
"metadata"
],
"properties": {
"parent": {
"type": "string"
},
"child": {
"type": "string"
},
"type": {
"type": "string"
},
"metadata": {
"additionalProperties": true
}
},
"additionalProperties": true,
"type": "object"
},
"RpmdbFileRecord": {
"required": [
"path",
"mode",
"size",
"sha256"
],
"properties": {
"path": {
"type": "string"
},
"mode": {
"type": "integer"
},
"size": {
"type": "integer"
},
"sha256": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"RpmdbMetadata": {
"required": [
"name",
"version",
"epoch",
"architecture",
"release",
"sourceRpm",
"size",
"license",
"vendor",
"files"
],
"properties": {
"name": {
"type": "string"
},
"version": {
"type": "string"
},
"epoch": {
"type": "integer"
},
"architecture": {
"type": "string"
},
"release": {
"type": "string"
},
"sourceRpm": {
"type": "string"
},
"size": {
"type": "integer"
},
"license": {
"type": "string"
},
"vendor": {
"type": "string"
},
"files": {
"items": {
"$schema": "http://json-schema.org/draft-04/schema#",
"$ref": "#/definitions/RpmdbFileRecord"
},
"type": "array"
}
},
"additionalProperties": true,
"type": "object"
},
"Schema": {
"required": [
"version",
"url"
],
"properties": {
"version": {
"type": "string"
},
"url": {
"type": "string"
}
},
"additionalProperties": true,
"type": "object"
},
"Source": {
"required": [
"type",
"target"
],
"properties": {
"type": {
"type": "string"
},
"target": {
"additionalProperties": true
}
},
"additionalProperties": true,
"type": "object"
}
}
}

View File

@ -1,118 +0,0 @@
package common
import (
"fmt"
"io"
"io/ioutil"
"strings"
"testing"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
type testResolverMock struct {
contents map[source.Location]io.ReadCloser
}
func newTestResolver() *testResolverMock {
return &testResolverMock{
contents: make(map[source.Location]io.ReadCloser),
}
}
func (r testResolverMock) HasPath(path string) bool {
panic("not implemented")
}
func (r *testResolverMock) FileContentsByLocation(_ source.Location) (io.ReadCloser, error) {
return nil, fmt.Errorf("not implemented")
}
func (r *testResolverMock) MultipleFileContentsByLocation([]source.Location) (map[source.Location]io.ReadCloser, error) {
return r.contents, nil
}
func (r *testResolverMock) FilesByPath(paths ...string) ([]source.Location, error) {
results := make([]source.Location, len(paths))
for idx, p := range paths {
results[idx] = source.NewLocation(p)
r.contents[results[idx]] = ioutil.NopCloser(strings.NewReader(fmt.Sprintf("%s file contents!", p)))
}
return results, nil
}
func (r *testResolverMock) FilesByGlob(_ ...string) ([]source.Location, error) {
path := "/a-path.txt"
location := source.NewLocation(path)
r.contents[location] = ioutil.NopCloser(strings.NewReader(fmt.Sprintf("%s file contents!", path)))
return []source.Location{location}, nil
}
func (r *testResolverMock) RelativeFileByPath(_ source.Location, _ string) *source.Location {
panic(fmt.Errorf("not implemented"))
return nil
}
func parser(_ string, reader io.Reader) ([]pkg.Package, error) {
contents, err := ioutil.ReadAll(reader)
if err != nil {
panic(err)
}
return []pkg.Package{
{
Name: string(contents),
},
}, nil
}
func TestGenericCataloger(t *testing.T) {
globParsers := map[string]ParserFn{
"**a-path.txt": parser,
}
pathParsers := map[string]ParserFn{
"/another-path.txt": parser,
"/last/path.txt": parser,
}
upstream := "some-other-cataloger"
resolver := newTestResolver()
cataloger := NewGenericCataloger(pathParsers, globParsers, upstream)
expectedSelection := []string{"/last/path.txt", "/another-path.txt", "/a-path.txt"}
expectedPkgs := make(map[string]pkg.Package)
for _, path := range expectedSelection {
expectedPkgs[path] = pkg.Package{
FoundBy: upstream,
Name: fmt.Sprintf("%s file contents!", path),
}
}
actualPkgs, err := cataloger.Catalog(resolver)
if err != nil {
t.Fatalf("cataloger catalog action failed: %+v", err)
}
if len(actualPkgs) != len(expectedPkgs) {
t.Fatalf("unexpected packages len: %d", len(actualPkgs))
}
for _, p := range actualPkgs {
ref := p.Locations[0]
exP, ok := expectedPkgs[ref.RealPath]
if !ok {
t.Errorf("missing expected pkg: ref=%+v", ref)
continue
}
if p.FoundBy != exP.FoundBy {
t.Errorf("bad upstream: %s", p.FoundBy)
}
if exP.Name != p.Name {
t.Errorf("bad contents mapping: %+v", p.Locations)
}
}
}

View File

@ -1,190 +0,0 @@
/*
Package dpkg provides a concrete Cataloger implementation for Debian package DB status files.
*/
package deb
import (
"fmt"
"io"
"path"
"path/filepath"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
const (
md5sumsExt = ".md5sums"
docsPath = "/usr/share/doc"
)
type Cataloger struct{}
// NewDpkgdbCataloger returns a new Deb package cataloger object.
func NewDpkgdbCataloger() *Cataloger {
return &Cataloger{}
}
// Name returns a string that uniquely describes a cataloger
func (c *Cataloger) Name() string {
return "dpkgdb-cataloger"
}
// Catalog is given an object to resolve file references and content, this function returns any discovered Packages after analyzing dpkg support files.
// nolint:funlen
func (c *Cataloger) Catalog(resolver source.Resolver) ([]pkg.Package, error) {
dbFileMatches, err := resolver.FilesByGlob(pkg.DpkgDbGlob)
if err != nil {
return nil, fmt.Errorf("failed to find dpkg status files's by glob: %w", err)
}
var results []pkg.Package
var pkgs []pkg.Package
for _, dbLocation := range dbFileMatches {
dbContents, err := resolver.FileContentsByLocation(dbLocation)
if err != nil {
return nil, err
}
pkgs, err = parseDpkgStatus(dbContents)
if err != nil {
return nil, fmt.Errorf("unable to catalog dpkg package=%+v: %w", dbLocation.RealPath, err)
}
md5ContentsByName, md5RefsByName, err := fetchMd5Contents(resolver, dbLocation, pkgs)
if err != nil {
return nil, fmt.Errorf("unable to find dpkg md5 contents: %w", err)
}
copyrightContentsByName, copyrightLocationByName, err := fetchCopyrightContents(resolver, dbLocation, pkgs)
if err != nil {
return nil, fmt.Errorf("unable to find dpkg copyright contents: %w", err)
}
for i := range pkgs {
p := &pkgs[i]
p.FoundBy = c.Name()
p.Locations = []source.Location{dbLocation}
metadata := p.Metadata.(pkg.DpkgMetadata)
if md5Reader, ok := md5ContentsByName[md5Key(*p)]; ok {
// attach the file list
metadata.Files = parseDpkgMD5Info(md5Reader)
// keep a record of the file where this was discovered
if ref, ok := md5RefsByName[md5Key(*p)]; ok {
p.Locations = append(p.Locations, ref)
}
} else {
// ensure the file list is an empty collection (not nil)
metadata.Files = make([]pkg.DpkgFileRecord, 0)
}
// persist alterations
p.Metadata = metadata
copyrightReader, ok := copyrightContentsByName[p.Name]
if ok {
// attach the licenses
p.Licenses = parseLicensesFromCopyright(copyrightReader)
// keep a record of the file where this was discovered
if ref, ok := copyrightLocationByName[p.Name]; ok {
p.Locations = append(p.Locations, ref)
}
}
}
results = append(results, pkgs...)
}
return results, nil
}
func fetchMd5Contents(resolver source.Resolver, dbLocation source.Location, pkgs []pkg.Package) (map[string]io.Reader, map[string]source.Location, error) {
// fetch all MD5 file contents. This approach is more efficient than fetching each MD5 file one at a time
var md5FileMatches []source.Location
var nameByRef = make(map[source.Location]string)
parentPath := filepath.Dir(dbLocation.RealPath)
for _, p := range pkgs {
// look for /var/lib/dpkg/info/NAME:ARCH.md5sums
name := md5Key(p)
md5SumLocation := resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", name+md5sumsExt))
if md5SumLocation == nil {
// the most specific key did not work, fallback to just the name
// look for /var/lib/dpkg/info/NAME.md5sums
md5SumLocation = resolver.RelativeFileByPath(dbLocation, path.Join(parentPath, "info", p.Name+md5sumsExt))
}
// we should have at least one reference
if md5SumLocation != nil {
md5FileMatches = append(md5FileMatches, *md5SumLocation)
nameByRef[*md5SumLocation] = name
}
}
// fetch the md5 contents
md5ContentsByLocation, err := resolver.MultipleFileContentsByLocation(md5FileMatches)
if err != nil {
return nil, nil, err
}
// organize content results and refs by a combination of name and architecture
var contentsByName = make(map[string]io.Reader)
var locationByName = make(map[string]source.Location)
for location, contents := range md5ContentsByLocation {
name := nameByRef[location]
contentsByName[name] = contents
locationByName[name] = location
}
return contentsByName, locationByName, nil
}
func fetchCopyrightContents(resolver source.Resolver, dbLocation source.Location, pkgs []pkg.Package) (map[string]io.Reader, map[string]source.Location, error) {
// fetch all copyright file contents. This approach is more efficient than fetching each copyright file one at a time
var copyrightFileMatches []source.Location
var nameByLocation = make(map[source.Location]string)
for _, p := range pkgs {
// look for /usr/share/docs/NAME/copyright files
name := p.Name
copyrightPath := path.Join(docsPath, name, "copyright")
copyrightLocation := resolver.RelativeFileByPath(dbLocation, copyrightPath)
// we may not have a copyright file for each package, ignore missing files
if copyrightLocation != nil {
copyrightFileMatches = append(copyrightFileMatches, *copyrightLocation)
nameByLocation[*copyrightLocation] = name
}
}
// fetch the copyright contents
copyrightContentsByLocation, err := resolver.MultipleFileContentsByLocation(copyrightFileMatches)
if err != nil {
return nil, nil, err
}
// organize content results and refs by package name
var contentsByName = make(map[string]io.Reader)
var refsByName = make(map[string]source.Location)
for location, contents := range copyrightContentsByLocation {
name := nameByLocation[location]
contentsByName[name] = contents
refsByName[name] = location
}
return contentsByName, refsByName, nil
}
func md5Key(p pkg.Package) string {
metadata := p.Metadata.(pkg.DpkgMetadata)
contentKey := p.Name
if metadata.Architecture != "" && metadata.Architecture != "all" {
contentKey = contentKey + ":" + metadata.Architecture
}
return contentKey
}

View File

@ -1,49 +0,0 @@
package python
import (
"path/filepath"
"github.com/anchore/syft/syft/source"
)
type packageEntry struct {
Metadata source.FileData
FileRecord *source.FileData
TopPackage *source.FileData
}
// newPackageEntry returns a new packageEntry to be processed relative to what information is available in the given FileResolver.
func newPackageEntry(resolver source.FileResolver, metadataLocation source.Location) *packageEntry {
// we've been given a file reference to a specific wheel METADATA file. note: this may be for a directory
// or for an image... for an image the METADATA file may be present within multiple layers, so it is important
// to reconcile the RECORD path to the same layer (or a lower layer). The same is true with the top_level.txt file.
// lets find the RECORD file relative to the directory where the METADATA file resides (in path AND layer structure)
recordPath := filepath.Join(filepath.Dir(metadataLocation.RealPath), "RECORD")
recordLocation := resolver.RelativeFileByPath(metadataLocation, recordPath)
// a top_level.txt file specifies the python top-level packages (provided by this python package) installed into site-packages
parentDir := filepath.Dir(metadataLocation.RealPath)
topLevelPath := filepath.Join(parentDir, "top_level.txt")
topLevelLocation := resolver.RelativeFileByPath(metadataLocation, topLevelPath)
// build an entry that will later be populated with contents when the request is executed
entry := &packageEntry{
Metadata: source.FileData{
Location: metadataLocation,
},
}
if recordLocation != nil {
entry.FileRecord = &source.FileData{
Location: *recordLocation,
}
}
if topLevelLocation != nil {
entry.TopPackage = &source.FileData{
Location: *topLevelLocation,
}
}
return entry
}

View File

@ -36,7 +36,7 @@ var identityFiles = []parseEntry{
}
// Identify parses distro-specific files to determine distro metadata like version and release.
func Identify(resolver source.Resolver) *Distro {
func Identify(resolver source.FileResolver) *Distro {
var distro *Distro
identifyLoop:

View File

@ -99,7 +99,12 @@ func TestIdentifyDistro(t *testing.T) {
t.Fatalf("unable to produce a new source for testing: %s", test.fixture)
}
d := Identify(s.Resolver)
resolver, err := s.FileResolver(source.SquashedScope)
if err != nil {
t.Fatalf("unable to get resolver: %+v", err)
}
d := Identify(resolver)
if d == nil {
if test.Type == UnknownDistroType {
return

View File

@ -10,11 +10,11 @@ const (
// AppUpdateAvailable is a partybus event that occurs when an application update is available
AppUpdateAvailable partybus.EventType = "syft-app-update-available"
// CatalogerStarted is a partybus event that occurs when the package cataloging has begun
CatalogerStarted partybus.EventType = "syft-cataloger-started-event"
// PackageCatalogerStarted is a partybus event that occurs when the package cataloging has begun
PackageCatalogerStarted partybus.EventType = "syft-cataloger-started-event"
// CatalogerFinished is a partybus event that occurs when the package cataloging has completed
CatalogerFinished partybus.EventType = "syft-cataloger-finished-event"
// PresenterReady is a partybus event that occurs when an analysis result is ready for final presentation
PresenterReady partybus.EventType = "syft-presenter-ready-event"
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
ImportStarted partybus.EventType = "syft-import-started-event"

View File

@ -6,11 +6,12 @@ package parsers
import (
"fmt"
"github.com/anchore/syft/internal/presenter"
"github.com/wagoodman/go-progress"
"github.com/anchore/syft/syft/cataloger"
"github.com/anchore/syft/syft/event"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/wagoodman/go-partybus"
)
@ -40,7 +41,7 @@ func checkEventType(actual, expected partybus.EventType) error {
}
func ParseCatalogerStarted(e partybus.Event) (*cataloger.Monitor, error) {
if err := checkEventType(e.Type, event.CatalogerStarted); err != nil {
if err := checkEventType(e.Type, event.PackageCatalogerStarted); err != nil {
return nil, err
}
@ -52,8 +53,8 @@ func ParseCatalogerStarted(e partybus.Event) (*cataloger.Monitor, error) {
return &monitor, nil
}
func ParseCatalogerFinished(e partybus.Event) (presenter.Presenter, error) {
if err := checkEventType(e.Type, event.CatalogerFinished); err != nil {
func ParsePresenterReady(e partybus.Event) (presenter.Presenter, error) {
if err := checkEventType(e.Type, event.PresenterReady); err != nil {
return nil, err
}

6
syft/file/digest.go Normal file
View File

@ -0,0 +1,6 @@
package file
type Digest struct {
Algorithm string `json:"algorithm"`
Value string `json:"value"`
}

View File

@ -0,0 +1,96 @@
package file
import (
"crypto"
"fmt"
"hash"
"io"
"strings"
"github.com/anchore/syft/syft/source"
)
var supportedHashAlgorithms = make(map[string]crypto.Hash)
type DigestsCataloger struct {
hashes []crypto.Hash
}
func init() {
for _, h := range []crypto.Hash{
crypto.MD5,
crypto.SHA1,
crypto.SHA256,
} {
supportedHashAlgorithms[cleanAlgorithmName(h.String())] = h
}
}
func NewDigestsCataloger(hashAlgorithms []string) (*DigestsCataloger, error) {
var hashes []crypto.Hash
for _, hashStr := range hashAlgorithms {
name := cleanAlgorithmName(hashStr)
hashObj, ok := supportedHashAlgorithms[name]
if !ok {
return nil, fmt.Errorf("unsupported hash algorithm: %s", hashStr)
}
hashes = append(hashes, hashObj)
}
return &DigestsCataloger{
hashes: hashes,
}, nil
}
func (i *DigestsCataloger) Catalog(resolver source.FileResolver) (map[source.Location][]Digest, error) {
results := make(map[source.Location][]Digest)
for location := range resolver.AllLocations() {
result, err := i.catalogLocation(resolver, location)
if err != nil {
return nil, err
}
results[location] = result
}
return results, nil
}
func (i *DigestsCataloger) catalogLocation(resolver source.FileResolver, location source.Location) ([]Digest, error) {
contentReader, err := resolver.FileContentsByLocation(location)
if err != nil {
return nil, err
}
defer contentReader.Close()
// create a set of hasher objects tied together with a single writer to feed content into
hashers := make([]hash.Hash, len(i.hashes))
writers := make([]io.Writer, len(i.hashes))
for idx, hashObj := range i.hashes {
hashers[idx] = hashObj.New()
writers[idx] = hashers[idx]
}
size, err := io.Copy(io.MultiWriter(writers...), contentReader)
if err != nil {
return nil, fmt.Errorf("unable to observe contents of %+v: %+v", location.RealPath, err)
}
result := make([]Digest, len(i.hashes))
if size > 0 {
// only capture digests when there is content. It is important to do this based on SIZE and not
// FILE TYPE. The reasoning is that it is possible for a tar to be crafted with a header-only
// file type but a body is still allowed.
for idx, hasher := range hashers {
result[idx] = Digest{
Algorithm: cleanAlgorithmName(i.hashes[idx].String()),
Value: fmt.Sprintf("%+x", hasher.Sum(nil)),
}
}
}
return result, nil
}
func cleanAlgorithmName(name string) string {
lower := strings.ToLower(name)
return strings.Replace(lower, "-", "", -1)
}

View File

@ -0,0 +1,98 @@
package file
import (
"crypto"
"fmt"
"io/ioutil"
"os"
"testing"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/syft/source"
)
func testDigests(t testing.TB, files []string, hashes ...crypto.Hash) map[source.Location][]Digest {
digests := make(map[source.Location][]Digest)
for _, f := range files {
fh, err := os.Open(f)
if err != nil {
t.Fatalf("could not open %q : %+v", f, err)
}
b, err := ioutil.ReadAll(fh)
if err != nil {
t.Fatalf("could not read %q : %+v", f, err)
}
for _, hash := range hashes {
h := hash.New()
h.Write(b)
digests[source.NewLocation(f)] = append(digests[source.NewLocation(f)], Digest{
Algorithm: cleanAlgorithmName(hash.String()),
Value: fmt.Sprintf("%x", h.Sum(nil)),
})
}
}
return digests
}
func TestDigestsCataloger(t *testing.T) {
files := []string{"test-fixtures/last/path.txt", "test-fixtures/another-path.txt", "test-fixtures/a-path.txt"}
tests := []struct {
name string
algorithms []string
expected map[source.Location][]Digest
constructorErr bool
catalogErr bool
}{
{
name: "bad algorithm",
algorithms: []string{"sha-nothing"},
constructorErr: true,
},
{
name: "unsupported algorithm",
algorithms: []string{"sha512"},
constructorErr: true,
},
{
name: "md5-sha1-sha256",
algorithms: []string{"md5"},
expected: testDigests(t, files, crypto.MD5),
},
{
name: "md5-sha1-sha256",
algorithms: []string{"md5", "sha1", "sha256"},
expected: testDigests(t, files, crypto.MD5, crypto.SHA1, crypto.SHA256),
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
c, err := NewDigestsCataloger(test.algorithms)
if err != nil && !test.constructorErr {
t.Fatalf("could not create cataloger (but should have been able to): %+v", err)
} else if err == nil && test.constructorErr {
t.Fatalf("expected constructor error but did not get one")
} else if test.constructorErr && err != nil {
return
}
resolver := source.NewMockResolverForPaths(files...)
actual, err := c.Catalog(resolver)
if err != nil && !test.catalogErr {
t.Fatalf("could not catalog (but should have been able to): %+v", err)
} else if err == nil && test.catalogErr {
t.Fatalf("expected catalog error but did not get one")
} else if test.catalogErr && err != nil {
return
}
assert.Equal(t, actual, test.expected, "mismatched digests")
})
}
}

View File

@ -0,0 +1,25 @@
package file
import (
"github.com/anchore/syft/syft/source"
)
type MetadataCataloger struct {
}
func NewMetadataCataloger() *MetadataCataloger {
return &MetadataCataloger{}
}
func (i *MetadataCataloger) Catalog(resolver source.FileResolver) (map[source.Location]source.FileMetadata, error) {
results := make(map[source.Location]source.FileMetadata)
for location := range resolver.AllLocations() {
metadata, err := resolver.FileMetadataByLocation(location)
if err != nil {
return nil, err
}
results[location] = metadata
}
return results, nil
}

View File

@ -0,0 +1,134 @@
package file
import (
"flag"
"os"
"testing"
"github.com/anchore/stereoscope/pkg/file"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
)
var updateImageGoldenFiles = flag.Bool("update-image", false, "update the golden fixture images used for testing")
func TestFileMetadataCataloger(t *testing.T) {
testImage := "image-file-type-mix"
if *updateImageGoldenFiles {
imagetest.UpdateGoldenFixtureImage(t, testImage)
}
img := imagetest.GetGoldenFixtureImage(t, testImage)
c := NewMetadataCataloger()
src, err := source.NewFromImage(img, "---")
if err != nil {
t.Fatalf("could not create source: %+v", err)
}
resolver, err := src.FileResolver(source.SquashedScope)
if err != nil {
t.Fatalf("could not create resolver: %+v", err)
}
actual, err := c.Catalog(resolver)
if err != nil {
t.Fatalf("could not catalog: %+v", err)
}
tests := []struct {
path string
exists bool
expected source.FileMetadata
err bool
}{
{
path: "/file-1.txt",
exists: true,
expected: source.FileMetadata{
Mode: 0644,
Type: "regularFile",
UserID: 1,
GroupID: 2,
},
},
{
path: "/hardlink-1",
exists: true,
expected: source.FileMetadata{
Mode: 0644,
Type: "hardLink",
UserID: 1,
GroupID: 2,
},
},
{
path: "/symlink-1",
exists: true,
expected: source.FileMetadata{
Mode: 0777 | os.ModeSymlink,
Type: "symbolicLink",
UserID: 0,
GroupID: 0,
},
},
{
path: "/char-device-1",
exists: true,
expected: source.FileMetadata{
Mode: 0644 | os.ModeDevice | os.ModeCharDevice,
Type: "characterDevice",
UserID: 0,
GroupID: 0,
},
},
{
path: "/block-device-1",
exists: true,
expected: source.FileMetadata{
Mode: 0644 | os.ModeDevice,
Type: "blockDevice",
UserID: 0,
GroupID: 0,
},
},
{
path: "/fifo-1",
exists: true,
expected: source.FileMetadata{
Mode: 0644 | os.ModeNamedPipe,
Type: "fifoNode",
UserID: 0,
GroupID: 0,
},
},
{
path: "/bin",
exists: true,
expected: source.FileMetadata{
Mode: 0755 | os.ModeDir,
Type: "directory",
UserID: 0,
GroupID: 0,
},
},
}
for _, test := range tests {
t.Run(test.path, func(t *testing.T) {
_, ref, err := img.SquashedTree().File(file.Path(test.path))
if err != nil {
t.Fatalf("unable to get file: %+v", err)
}
l := source.NewLocationFromImage(test.path, *ref, img)
assert.Equal(t, actual[l], test.expected, "mismatched metadata")
})
}
}

View File

@ -0,0 +1 @@
test-fixtures/a-path.txt file contents!

View File

@ -0,0 +1 @@
test-fixtures/another-path.txt file contents!

View File

@ -0,0 +1,11 @@
FROM busybox:latest
ADD file-1.txt .
RUN chmod 644 file-1.txt
RUN chown 1:2 file-1.txt
RUN ln -s file-1.txt symlink-1
# note: hard links may behave inconsistently, this should be a golden image
RUN ln file-1.txt hardlink-1
RUN mknod char-device-1 c 89 1
RUN mknod block-device-1 b 0 1
RUN mknod fifo-1 p

View File

@ -0,0 +1 @@
file 1!

View File

@ -0,0 +1 @@
test-fixtures/last/path.txt file contents!

View File

@ -17,32 +17,29 @@ Similar to the cataloging process, Linux distribution identification is also per
package syft
import (
"encoding/json"
"fmt"
"io"
"github.com/anchore/syft/internal/bus"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/logger"
"github.com/anchore/syft/syft/pkg"
jsonPresenter "github.com/anchore/syft/syft/presenter/json"
"github.com/anchore/syft/syft/pkg/cataloger"
"github.com/anchore/syft/syft/source"
"github.com/wagoodman/go-partybus"
)
// Catalog the given image from a particular perspective (e.g. squashed source, all-layers source). Returns the discovered
// set of packages, the identified Linux distribution, and the source object used to wrap the data source.
func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog, *distro.Distro, error) {
theSource, cleanup, err := source.New(userInput, scope)
defer cleanup()
// CatalogPackages takes an inventory of packages from the given image from a particular perspective
// (e.g. squashed source, all-layers source). Returns the discovered set of packages, the identified Linux
// distribution, and the source object used to wrap the data source.
func CatalogPackages(src source.Source, scope source.Scope) (*pkg.Catalog, *distro.Distro, error) {
resolver, err := src.FileResolver(scope)
if err != nil {
return source.Source{}, nil, nil, err
return nil, nil, fmt.Errorf("unable to determine FileResolver while cataloging packages: %w", err)
}
// find the distro
theDistro := distro.Identify(theSource.Resolver)
theDistro := distro.Identify(resolver)
if theDistro != nil {
log.Infof("identified distro: %s", theDistro.String())
} else {
@ -51,7 +48,7 @@ func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog,
// conditionally use the correct set of loggers based on the input type (container image or directory)
var catalogers []cataloger.Cataloger
switch theSource.Metadata.Scheme {
switch src.Metadata.Scheme {
case source.ImageScheme:
log.Info("cataloging image")
catalogers = cataloger.ImageCatalogers()
@ -59,46 +56,15 @@ func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog,
log.Info("cataloging directory")
catalogers = cataloger.DirectoryCatalogers()
default:
return source.Source{}, nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", theSource.Metadata.Scheme)
return nil, nil, fmt.Errorf("unable to determine cataloger set from scheme=%+v", src.Metadata.Scheme)
}
catalog, err := cataloger.Catalog(theSource.Resolver, theDistro, catalogers...)
catalog, err := cataloger.Catalog(resolver, theDistro, catalogers...)
if err != nil {
return source.Source{}, nil, nil, err
return nil, nil, err
}
return theSource, catalog, theDistro, nil
}
// CatalogFromJSON takes an existing syft report and generates native syft objects.
func CatalogFromJSON(reader io.Reader) (source.Metadata, *pkg.Catalog, *distro.Distro, error) {
var doc jsonPresenter.Document
var err error
decoder := json.NewDecoder(reader)
if err := decoder.Decode(&doc); err != nil {
return source.Metadata{}, nil, nil, err
}
var pkgs = make([]pkg.Package, len(doc.Artifacts))
for i, a := range doc.Artifacts {
pkgs[i], err = a.ToPackage()
if err != nil {
return source.Metadata{}, nil, nil, err
}
}
catalog := pkg.NewCatalog(pkgs...)
var theDistro *distro.Distro
if doc.Distro.Name != "" {
d, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike)
if err != nil {
return source.Metadata{}, nil, nil, err
}
theDistro = &d
}
return doc.Source.ToSourceMetadata(), catalog, theDistro, nil
return catalog, theDistro, nil
}
// SetLogger sets the logger object used for all syft logging calls.

View File

@ -4,8 +4,8 @@ Package apkdb provides a concrete Cataloger implementation for Alpine DB files.
package apkdb
import (
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
)
// NewApkdbCataloger returns a new Alpine DB cataloger object.

View File

@ -9,8 +9,8 @@ import (
"strings"
"github.com/anchore/syft/internal/log"
"github.com/anchore/syft/syft/cataloger/common"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/pkg/cataloger/common"
"github.com/mitchellh/mapstructure"
)

Some files were not shown because too many files have changed in this diff Show More