mirror of
https://github.com/anchore/syft.git
synced 2026-04-05 22:30:35 +02:00
Merge pull request #339 from anchore/add-file-metadata-indexer-invert-control
Add file metadata cataloger
This commit is contained in:
commit
e256e86888
142
.github/workflows/acceptance-test.yaml
vendored
142
.github/workflows/acceptance-test.yaml
vendored
@ -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() }}
|
|
||||||
71
.github/workflows/release.yaml
vendored
71
.github/workflows/release.yaml
vendored
@ -9,7 +9,7 @@ on:
|
|||||||
- "v*"
|
- "v*"
|
||||||
|
|
||||||
env:
|
env:
|
||||||
GO_VERSION: "1.14.x"
|
GO_VERSION: "1.16.x"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
wait-for-checks:
|
wait-for-checks:
|
||||||
@ -29,17 +29,26 @@ jobs:
|
|||||||
id: static-analysis
|
id: static-analysis
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
|
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||||
checkName: "Static-Analysis (1.x, ubuntu-latest)"
|
checkName: "Static-Analysis"
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
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
|
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||||
id: unit-integration
|
id: unit
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
# This check name is defined as the circle-ci workflow name (in .circleci/config.yaml)
|
# This check name is defined as the github action job name (in .github/workflows/testing.yaml)
|
||||||
checkName: "Tests (1.x, ubuntu-latest)"
|
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 }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Check acceptance test results (linux)
|
- name: Check acceptance test results (linux)
|
||||||
@ -47,7 +56,7 @@ jobs:
|
|||||||
id: acceptance-linux
|
id: acceptance-linux
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
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"
|
checkName: "Acceptance-Linux"
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
@ -56,27 +65,28 @@ jobs:
|
|||||||
id: acceptance-mac
|
id: acceptance-mac
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
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"
|
checkName: "Acceptance-Mac"
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
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
|
uses: fountainhead/action-wait-for-check@v1.0.0
|
||||||
id: inline-compare
|
id: cli-linux
|
||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
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: "Inline-Compare"
|
checkName: "Cli-Linux"
|
||||||
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
||||||
|
|
||||||
- name: Quality gate
|
- 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: |
|
run: |
|
||||||
echo "Static Analysis Status: ${{ steps.static-analysis.conclusion }}"
|
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 (Linux) Status: ${{ steps.acceptance-linux.outputs.conclusion }}"
|
||||||
echo "Acceptance Test (Mac) Status: ${{ steps.acceptance-mac.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
|
false
|
||||||
|
|
||||||
release:
|
release:
|
||||||
@ -93,23 +103,32 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
# We are expecting this cache to have been created during the "Build-Snapshot-Artifacts" job in the "Acceptance" workflow.
|
- name: Restore tool cache
|
||||||
- name: Restore bootstrap cache
|
id: tool-cache
|
||||||
id: cache
|
|
||||||
uses: actions/cache@v2.1.3
|
uses: actions/cache@v2.1.3
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ${{ github.workspace }}/.tmp
|
||||||
~/go/pkg/mod
|
key: ${{ runner.os }}-tool-${{ hashFiles('Makefile') }}
|
||||||
${{ github.workspace }}/.tmp
|
|
||||||
key: ${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-${{ 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: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-${{ hashFiles('**/go.sum') }}-
|
|
||||||
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
${{ runner.os }}-go-${{ env.GO_VERSION }}-
|
||||||
|
|
||||||
- name: Bootstrap project dependencies
|
- name: (cache-miss) Bootstrap all project dependencies
|
||||||
if: steps.bootstrap-cache.outputs.cache-hit != 'true'
|
if: steps.tool-cache.outputs.cache-hit != 'true' || steps.go-cache.outputs.cache-hit != 'true'
|
||||||
run: make bootstrap
|
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
|
- name: Import GPG key
|
||||||
id: import_gpg
|
id: import_gpg
|
||||||
uses: crazy-max/ghaction-import-gpg@v2
|
uses: crazy-max/ghaction-import-gpg@v2
|
||||||
|
|||||||
101
.github/workflows/static-unit-integration.yaml
vendored
101
.github/workflows/static-unit-integration.yaml
vendored
@ -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
354
.github/workflows/validations.yaml
vendored
Normal 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
1
.gitignore
vendored
@ -1,4 +1,5 @@
|
|||||||
CHANGELOG.md
|
CHANGELOG.md
|
||||||
|
/test/results
|
||||||
/dist
|
/dist
|
||||||
/snapshot
|
/snapshot
|
||||||
.server/
|
.server/
|
||||||
|
|||||||
@ -92,18 +92,15 @@ brews:
|
|||||||
description: *description
|
description: *description
|
||||||
|
|
||||||
dockers:
|
dockers:
|
||||||
-
|
- dockerfile: Dockerfile
|
||||||
binaries:
|
|
||||||
- syft
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image_templates:
|
image_templates:
|
||||||
- "anchore/syft:latest"
|
- "anchore/syft:latest"
|
||||||
- "anchore/syft:{{ .Tag }}"
|
- "anchore/syft:{{ .Tag }}"
|
||||||
- "anchore/syft:v{{ .Major }}"
|
- "anchore/syft:v{{ .Major }}"
|
||||||
- "anchore/syft:v{{ .Major }}.{{ .Minor }}"
|
- "anchore/syft:v{{ .Major }}.{{ .Minor }}"
|
||||||
|
|
||||||
build_flag_templates:
|
build_flag_templates:
|
||||||
- "--build-arg=BUILD_DATE={{.Date}}"
|
- "--build-arg=BUILD_DATE={{.Date}}"
|
||||||
- "--build-arg=BUILD_VERSION={{.Version}}"
|
- "--build-arg=BUILD_VERSION={{.Version}}"
|
||||||
- "--build-arg=VCS_REF={{.FullCommit}}"
|
- "--build-arg=VCS_REF={{.FullCommit}}"
|
||||||
- "--build-arg=VCS_URL={{.GitURL}}"
|
- "--build-arg=VCS_URL={{.GitURL}}"
|
||||||
|
use_buildx: true
|
||||||
|
|||||||
143
Makefile
143
Makefile
@ -1,8 +1,8 @@
|
|||||||
BIN = syft
|
BIN = syft
|
||||||
TEMPDIR = ./.tmp
|
TEMPDIR = ./.tmp
|
||||||
RESULTSDIR = $(TEMPDIR)/results
|
RESULTSDIR = test/results
|
||||||
COVER_REPORT = $(RESULTSDIR)/cover.report
|
COVER_REPORT = $(RESULTSDIR)/unit-coverage-details.txt
|
||||||
COVER_TOTAL = $(RESULTSDIR)/cover.total
|
COVER_TOTAL = $(RESULTSDIR)/unit-coverage-summary.txt
|
||||||
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --config .golangci.yaml
|
LINTCMD = $(TEMPDIR)/golangci-lint run --tests=false --config .golangci.yaml
|
||||||
ACC_TEST_IMAGE = centos:8.2.2004
|
ACC_TEST_IMAGE = centos:8.2.2004
|
||||||
ACC_DIR = ./test/acceptance
|
ACC_DIR = ./test/acceptance
|
||||||
@ -15,17 +15,23 @@ RESET := $(shell tput -T linux sgr0)
|
|||||||
TITLE := $(BOLD)$(PURPLE)
|
TITLE := $(BOLD)$(PURPLE)
|
||||||
SUCCESS := $(BOLD)$(GREEN)
|
SUCCESS := $(BOLD)$(GREEN)
|
||||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
# 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
|
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||||
COMPARE_CACHE_BUSTER="f7e689d76a9"
|
INTEGRATION_CACHE_BUSTER="88738d2f"
|
||||||
INTEGRATION_CACHE_BUSTER="789bacdf"
|
CLI_CACHE_BUSTER="789bacdf"
|
||||||
BOOTSTRAP_CACHE="789bacdf"
|
BOOTSTRAP_CACHE="c7afb99ad"
|
||||||
|
|
||||||
## Build variables
|
## Build variables
|
||||||
DISTDIR=./dist
|
DISTDIR=./dist
|
||||||
SNAPSHOTDIR=./snapshot
|
SNAPSHOTDIR=./snapshot
|
||||||
GITTREESTATE=$(if $(shell git status --porcelain),dirty,clean)
|
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))" ""
|
ifeq "$(strip $(VERSION))" ""
|
||||||
override VERSION = $(shell git describe --always --tags --dirty)
|
override VERSION = $(shell git describe --always --tags --dirty)
|
||||||
@ -57,6 +63,10 @@ ifndef SNAPSHOTDIR
|
|||||||
$(error SNAPSHOTDIR is not set)
|
$(error SNAPSHOTDIR is not set)
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
ifndef REF_NAME
|
||||||
|
REF_NAME = $(VERSION)
|
||||||
|
endif
|
||||||
|
|
||||||
define title
|
define title
|
||||||
@printf '$(TITLE)$(1)$(RESET)\n'
|
@printf '$(TITLE)$(1)$(RESET)\n'
|
||||||
endef
|
endef
|
||||||
@ -68,7 +78,7 @@ all: clean static-analysis test ## Run all linux-based checks (linting, license
|
|||||||
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
|
@printf '$(SUCCESS)All checks pass!$(RESET)\n'
|
||||||
|
|
||||||
.PHONY: test
|
.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
|
.PHONY: help
|
||||||
help:
|
help:
|
||||||
@ -78,19 +88,30 @@ help:
|
|||||||
ci-bootstrap:
|
ci-bootstrap:
|
||||||
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
|
DEBIAN_FRONTEND=noninteractive sudo apt update && sudo -E apt install -y bc jq libxml2-utils
|
||||||
|
|
||||||
.PHONY: bootstrap
|
.PHONY:
|
||||||
bootstrap: ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
ci-bootstrap-mac:
|
||||||
$(call title,Bootstrapping dependencies)
|
github_changelog_generator --version || sudo gem install github_changelog_generator
|
||||||
@pwd
|
|
||||||
# prep temp dirs
|
$(RESULTSDIR):
|
||||||
mkdir -p $(TEMPDIR)
|
|
||||||
mkdir -p $(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
|
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
|
.PHONY: bootstrap
|
||||||
[ -f "$(TEMPDIR)/bouncer" ] || curl -sSfL https://raw.githubusercontent.com/wagoodman/go-bouncer/master/bouncer.sh | sh -s -- -b $(TEMPDIR)/ v0.2.0
|
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
||||||
[ -f "$(TEMPDIR)/goreleaser" ] || curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.140.0
|
$(call title,Bootstrapping dependencies)
|
||||||
|
|
||||||
.PHONY: static-analysis
|
.PHONY: static-analysis
|
||||||
static-analysis: lint check-licenses
|
static-analysis: lint check-licenses
|
||||||
@ -124,42 +145,50 @@ validate-cyclonedx-schema:
|
|||||||
cd schema/cyclonedx && make
|
cd schema/cyclonedx && make
|
||||||
|
|
||||||
.PHONY: unit
|
.PHONY: unit
|
||||||
unit: fixtures ## Run unit tests (with coverage)
|
unit: $(RESULTSDIR) fixtures ## Run unit tests (with coverage)
|
||||||
$(call title,Running unit tests)
|
$(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)
|
@go tool cover -func $(COVER_REPORT) | grep total | awk '{print substr($$3, 1, length($$3)-1)}' > $(COVER_TOTAL)
|
||||||
@echo "Coverage: $$(cat $(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
|
@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
|
.PHONY: integration
|
||||||
integration: ## Run integration tests
|
integration: ## Run integration tests
|
||||||
$(call title,Running integration tests)
|
$(call title,Running integration tests)
|
||||||
|
|
||||||
go test -v ./test/integration
|
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
|
# note: this is used by CI to determine if the integration test fixture cache (docker image tars) should be busted
|
||||||
integration-fingerprint:
|
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
|
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
|
.PHONY: java-packages-fingerprint
|
||||||
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
|
make packages.fingerprint
|
||||||
|
|
||||||
.PHONY: fixtures
|
.PHONY: fixtures
|
||||||
fixtures:
|
fixtures:
|
||||||
$(call title,Generating test 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
|
.PHONY: generate-json-schema
|
||||||
generate-json-schema: ## Generate a new json schema
|
generate-json-schema: ## Generate a new json schema
|
||||||
cd schema/json && go run generate.go
|
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
|
.PHONY: build
|
||||||
build: $(SNAPSHOTDIR) ## Build release snapshot binaries and packages
|
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
|
# note: we cannot clean the snapshot directory since the pipeline builds the snapshot separately
|
||||||
.PHONY: acceptance-mac
|
.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)
|
$(call title,Running acceptance test: Run on Mac)
|
||||||
$(ACC_DIR)/mac.sh \
|
$(ACC_DIR)/mac.sh \
|
||||||
$(SNAPSHOTDIR) \
|
$(SNAPSHOTDIR) \
|
||||||
@ -187,22 +216,8 @@ acceptance-mac: $(SNAPSHOTDIR) ## Run acceptance tests on build snapshot binarie
|
|||||||
.PHONY: acceptance-linux
|
.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)
|
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
|
.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)
|
$(call title,Running acceptance test: DEB install)
|
||||||
$(ACC_DIR)/deb.sh \
|
$(ACC_DIR)/deb.sh \
|
||||||
$(SNAPSHOTDIR) \
|
$(SNAPSHOTDIR) \
|
||||||
@ -211,7 +226,7 @@ acceptance-test-deb-package-install: $(SNAPSHOTDIR)
|
|||||||
$(RESULTSDIR)
|
$(RESULTSDIR)
|
||||||
|
|
||||||
.PHONY: acceptance-test-rpm-package-install
|
.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)
|
$(call title,Running acceptance test: RPM install)
|
||||||
$(ACC_DIR)/rpm.sh \
|
$(ACC_DIR)/rpm.sh \
|
||||||
$(SNAPSHOTDIR) \
|
$(SNAPSHOTDIR) \
|
||||||
@ -219,6 +234,17 @@ acceptance-test-rpm-package-install: $(SNAPSHOTDIR)
|
|||||||
$(ACC_TEST_IMAGE) \
|
$(ACC_TEST_IMAGE) \
|
||||||
$(RESULTSDIR)
|
$(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
|
.PHONY: changlog-release
|
||||||
changelog-release:
|
changelog-release:
|
||||||
@echo "Last tag: $(SECOND_TO_LAST_TAG)"
|
@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
|
.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)/*
|
rm -rf $(RESULTSDIR)/*
|
||||||
|
|
||||||
.PHONY: clean-snapshot
|
.PHONY: clean-snapshot
|
||||||
@ -293,3 +319,26 @@ clean-snapshot:
|
|||||||
.PHONY: clean-dist
|
.PHONY: clean-dist
|
||||||
clean-dist:
|
clean-dist:
|
||||||
rm -rf $(DISTDIR) $(TEMPDIR)/goreleaser.yaml
|
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
|
||||||
51
README.md
51
README.md
@ -1,7 +1,6 @@
|
|||||||
# syft
|
# syft
|
||||||
|
|
||||||
[](https://github.com/anchore/syft/actions?query=workflow%3A%22Static+Analysis+%2B+Unit+%2B+Integration%22)
|
[](https://github.com/anchore/syft/workflows/validations.yaml)
|
||||||
[](https://github.com/anchore/syft/actions?query=workflow%3AAcceptance)
|
|
||||||
[](https://goreportcard.com/report/github.com/anchore/syft)
|
[](https://goreportcard.com/report/github.com/anchore/syft)
|
||||||
[](https://github.com/anchore/syft/releases/latest)
|
[](https://github.com/anchore/syft/releases/latest)
|
||||||
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
[](https://github.com/anchore/syft/blob/main/LICENSE)
|
||||||
@ -25,25 +24,30 @@ To generate an SBOM for a Docker or OCI image:
|
|||||||
syft <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).
|
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`:
|
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:
|
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)
|
# 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
|
# catalog a directory
|
||||||
syft path/to/dir
|
syft packages path/to/dir
|
||||||
```
|
```
|
||||||
|
|
||||||
The output format for Syft is configurable as well:
|
The output format for Syft is configurable as well:
|
||||||
```
|
```
|
||||||
syft <image> -o <format>
|
syft packages <image> -o <format>
|
||||||
```
|
```
|
||||||
|
|
||||||
Where the `format`s available are:
|
Where the `format`s available are:
|
||||||
@ -85,10 +89,6 @@ Configuration options (example values are the default):
|
|||||||
# same as -o ; SYFT_OUTPUT env var
|
# same as -o ; SYFT_OUTPUT env var
|
||||||
output: "table"
|
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)
|
# suppress all output (except for the SBOM report)
|
||||||
# same as -q ; SYFT_QUIET env var
|
# same as -q ; SYFT_QUIET env var
|
||||||
quiet: false
|
quiet: false
|
||||||
@ -97,6 +97,32 @@ quiet: false
|
|||||||
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
# same as SYFT_CHECK_FOR_APP_UPDATE env var
|
||||||
check-for-app-update: true
|
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:
|
log:
|
||||||
# use structured logging
|
# use structured logging
|
||||||
# same as SYFT_LOG_STRUCTURED env var
|
# same as SYFT_LOG_STRUCTURED env var
|
||||||
@ -110,11 +136,8 @@ log:
|
|||||||
# same as SYFT_LOG_FILE env var
|
# same as SYFT_LOG_FILE env var
|
||||||
file: ""
|
file: ""
|
||||||
|
|
||||||
|
# uploading package SBOM is exposed through the packages subcommand
|
||||||
anchore:
|
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+)
|
# (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
|
# same as -H ; SYFT_ANCHORE_HOST env var
|
||||||
host: ""
|
host: ""
|
||||||
|
|||||||
30
cmd/check_for_application_update.go
Normal file
30
cmd/check_for_application_update.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
137
cmd/cmd.go
137
cmd/cmd.go
@ -4,28 +4,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope"
|
"github.com/anchore/stereoscope"
|
||||||
"github.com/anchore/syft/internal/config"
|
"github.com/anchore/syft/internal/config"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/internal/logger"
|
"github.com/anchore/syft/internal/logger"
|
||||||
"github.com/anchore/syft/syft"
|
"github.com/anchore/syft/syft"
|
||||||
"github.com/anchore/syft/syft/presenter"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
"github.com/gookit/color"
|
"github.com/gookit/color"
|
||||||
"github.com/spf13/cobra"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
var appConfig *config.Application
|
var (
|
||||||
var eventBus *partybus.Bus
|
appConfig *config.Application
|
||||||
var eventSubscription *partybus.Subscription
|
eventBus *partybus.Bus
|
||||||
var cliOpts = config.CliOnlyOptions{}
|
eventSubscription *partybus.Subscription
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
setGlobalCliOptions()
|
|
||||||
|
|
||||||
cobra.OnInitialize(
|
cobra.OnInitialize(
|
||||||
|
initCmdAliasBindings,
|
||||||
initAppConfig,
|
initAppConfig,
|
||||||
initLogging,
|
initLogging,
|
||||||
logAppConfig,
|
logAppConfig,
|
||||||
@ -35,109 +34,41 @@ func init() {
|
|||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err.Error())
|
fmt.Fprintln(os.Stderr, color.Red.Sprint(err.Error()))
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func setGlobalCliOptions() {
|
// we must setup the config-cli bindings first before the application configuration is parsed. However, this cannot
|
||||||
rootCmd.PersistentFlags().StringVarP(&cliOpts.ConfigPath, "config", "c", "", "application config file")
|
// 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).
|
||||||
// scan options
|
func initCmdAliasBindings() {
|
||||||
flag := "scope"
|
activeCmd, _, err := rootCmd.Find(os.Args[1:])
|
||||||
rootCmd.Flags().StringP(
|
if err != nil {
|
||||||
"scope", "s", source.SquashedScope.String(),
|
panic(err)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setGlobalFormatOptions()
|
if activeCmd == packagesCmd || activeCmd == rootCmd {
|
||||||
setGlobalUploadOptions()
|
// 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
|
||||||
func setGlobalFormatOptions() {
|
// reading the application configuration, which implies that it must be an initializer (or rewrite the command
|
||||||
// output & formatting options
|
// initialization structure against typical patterns used with cobra, which is somewhat extreme for a
|
||||||
flag := "output"
|
// temporary alias)
|
||||||
rootCmd.Flags().StringP(
|
if err = bindPackagesConfigOptions(activeCmd.Flags()); err != nil {
|
||||||
flag, "o", string(presenter.TablePresenter),
|
panic(err)
|
||||||
fmt.Sprintf("report output formatter, options=%v", presenter.Options),
|
}
|
||||||
)
|
} else {
|
||||||
if err := viper.BindPFlag(flag, rootCmd.Flags().Lookup(flag)); err != nil {
|
// even though the root command or packages command is NOT being run, we still need default bindings
|
||||||
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
// such that application config parsing passes.
|
||||||
os.Exit(1)
|
if err = bindPackagesConfigOptions(packagesCmd.Flags()); err != nil {
|
||||||
}
|
panic(err)
|
||||||
|
}
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initAppConfig() {
|
func initAppConfig() {
|
||||||
cfgVehicle := viper.GetViper()
|
cfg, err := config.LoadApplicationConfig(viper.GetViper(), persistentOpts)
|
||||||
wasHostnameSet := rootCmd.Flags().Changed("host")
|
|
||||||
cfg, err := config.LoadApplicationConfig(cfgVehicle, cliOpts, wasHostnameSet)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("failed to load application config: \n\t%+v\n", err)
|
fmt.Printf("failed to load application config: \n\t%+v\n", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@ -163,7 +94,7 @@ func initLogging() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func logAppConfig() {
|
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() {
|
func initEventBus() {
|
||||||
|
|||||||
@ -1,15 +1,22 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/filters"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
// completionCmd represents the completion command
|
// completionCmd represents the completion command
|
||||||
var completionCmd = &cobra.Command{
|
var completionCmd = &cobra.Command{
|
||||||
Use: "completion [bash|zsh|fish]",
|
Hidden: true,
|
||||||
Short: "Generate a shell completion for Syft (listing local docker images)",
|
Use: "completion [bash|zsh|fish]",
|
||||||
|
Short: "Generate a shell completion for Syft (listing local docker images)",
|
||||||
Long: `To load completions (docker image list):
|
Long: `To load completions (docker image list):
|
||||||
|
|
||||||
Bash:
|
Bash:
|
||||||
@ -63,3 +70,45 @@ $ syft completion fish > ~/.config/fish/completions/syft.fish
|
|||||||
func init() {
|
func init() {
|
||||||
rootCmd.AddCommand(completionCmd)
|
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
270
cmd/packages.go
Normal 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
113
cmd/power_user.go
Normal 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
103
cmd/power_user_tasks.go
Normal 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
|
||||||
|
}
|
||||||
230
cmd/root.go
230
cmd/root.go
@ -1,217 +1,45 @@
|
|||||||
package cmd
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/pkg/profile"
|
"github.com/anchore/syft/internal/config"
|
||||||
|
|
||||||
"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/spf13/cobra"
|
"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{
|
var rootCmd = &cobra.Command{
|
||||||
Use: fmt.Sprintf("%s [SOURCE]", internal.ApplicationName),
|
Short: packagesCmd.Short,
|
||||||
Short: "A tool for generating a Software Bill Of Materials (PackageSBOM) from container images and filesystems",
|
Long: packagesCmd.Long,
|
||||||
Long: internal.Tprintf(`
|
Args: packagesCmd.Args,
|
||||||
Supports the following image sources:
|
Example: packagesCmd.Example,
|
||||||
{{.appName}} yourrepo/yourimage:tag defaults to using images from a Docker daemon
|
SilenceUsage: true,
|
||||||
{{.appName}} path/to/yourproject a Docker tar, OCI tar, OCI directory, or generic filesystem directory
|
SilenceErrors: true,
|
||||||
|
PreRunE: packagesCmd.PreRunE,
|
||||||
You can also explicitly specify the scheme to use:
|
RunE: packagesCmd.RunE,
|
||||||
{{.appName}} docker:yourrepo/yourimage:tag explicitly use the Docker daemon
|
ValidArgsFunction: packagesCmd.ValidArgsFunction,
|
||||||
{{.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
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startWorker(userInput string) <-chan error {
|
func init() {
|
||||||
errs := make(chan error)
|
// set universal flags
|
||||||
go func() {
|
rootCmd.PersistentFlags().StringVarP(&persistentOpts.ConfigPath, "config", "c", "", "application config file")
|
||||||
defer close(errs)
|
|
||||||
|
|
||||||
if appConfig.CheckForAppUpdate {
|
flag := "quiet"
|
||||||
isAvailable, newVersion, err := version.IsUpdateAvailable()
|
rootCmd.PersistentFlags().BoolP(
|
||||||
if err != nil {
|
flag, "q", false,
|
||||||
log.Errorf(err.Error())
|
"suppress all logging output",
|
||||||
}
|
)
|
||||||
if isAvailable {
|
if err := viper.BindPFlag(flag, rootCmd.PersistentFlags().Lookup(flag)); err != nil {
|
||||||
log.Infof("new version of %s is available: %s", internal.ApplicationName, newVersion)
|
fmt.Printf("unable to bind flag '%s': %+v", flag, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
bus.Publish(partybus.Event{
|
rootCmd.PersistentFlags().CountVarP(&persistentOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)")
|
||||||
Type: event.AppUpdateAvailable,
|
|
||||||
Value: newVersion,
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
log.Debugf("no new %s update available", internal.ApplicationName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
src, catalog, distro, err := syft.Catalog(userInput, appConfig.ScopeOpt)
|
// set common options that are not universal (package subcommand-alias specific)
|
||||||
if err != nil {
|
setPackageFlags(rootCmd.Flags())
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import (
|
|||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/version"
|
"github.com/anchore/syft/internal/version"
|
||||||
"github.com/anchore/syft/syft/presenter"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -21,7 +20,7 @@ var versionCmd = &cobra.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func init() {
|
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)
|
rootCmd.AddCommand(versionCmd)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
7
go.mod
7
go.mod
@ -3,13 +3,14 @@ module github.com/anchore/syft
|
|||||||
go 1.14
|
go 1.14
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
|
||||||
github.com/adrg/xdg v0.2.1
|
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/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||||
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
|
github.com/anchore/go-rpmdb v0.0.0-20201106153645-0043963c2e12
|
||||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
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/antihax/optional v1.0.0
|
||||||
github.com/bmatcuk/doublestar/v2 v2.0.4
|
github.com/bmatcuk/doublestar/v2 v2.0.4
|
||||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
|
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/sirupsen/logrus v1.6.0
|
||||||
github.com/spf13/afero v1.2.2
|
github.com/spf13/afero v1.2.2
|
||||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
|
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/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-partybus v0.0.0-20200526224238-eb215533f07d
|
||||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
||||||
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
|
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
|
||||||
|
|||||||
11
go.sum
11
go.sum
@ -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-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/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/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 h1:VSVdnH7cQ7V+B33qSJHTCRlNgra1607Q8PzEmnvb2Ic=
|
||||||
github.com/adrg/xdg v0.2.1/go.mod h1:ZuOshBmzV4Ta+s23hdfFZnBsdzmoR3US0d7ErpqSbTQ=
|
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-20210301060011-54c507b6f074 h1:Lw9q+WyJLFOR+AULchS5/2GKfM+6gOh4szzizdfH3MU=
|
||||||
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/go.mod h1:/n6+1/DWPltRLWL/VKyUxg6tzsl5kHUCcraimt4vr60=
|
||||||
github.com/alecthomas/kingpin v2.2.6+incompatible/go.mod h1:59OFYbFVLKQKq+mqrL6Rw5bR0c3ACQaawgXx0QYndlE=
|
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-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/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-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 h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
|
||||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
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-20210323182342-47b72675ff65 h1:r3tiir6UCgj/YeTqy4s2bfhZ9SuJYNlXx1Z9e/eLrbI=
|
||||||
github.com/anchore/stereoscope v0.0.0-20210201165248-e94c52b4052d/go.mod h1:lhSEYyGLXTXMIFHAz7Ls/MNQ5EjYd5ziLxovKZp1xOs=
|
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/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 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
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/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/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/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/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/vmware/govmomi v0.20.3/go.mod h1:URlwyTFZX72RmxtxuaFL2Uj3fD1JTvZdx59bHWk6aFU=
|
||||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d h1:KOxOL6qpmqwoPloNwi+CEgc1ayjHNOFNrvoOmeDOjDg=
|
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d h1:KOxOL6qpmqwoPloNwi+CEgc1ayjHNOFNrvoOmeDOjDg=
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type ImportConfig struct {
|
|||||||
Distro *distro.Distro
|
Distro *distro.Distro
|
||||||
Dockerfile []byte
|
Dockerfile []byte
|
||||||
OverwriteExistingUpload bool
|
OverwriteExistingUpload bool
|
||||||
|
Scope source.Scope
|
||||||
}
|
}
|
||||||
|
|
||||||
func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||||
@ -71,7 +72,7 @@ func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
|||||||
prog.N++
|
prog.N++
|
||||||
sessionID := startOperation.Uuid
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to import Package SBOM: %w", err)
|
return fmt.Errorf("failed to import Package SBOM: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,9 +8,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"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/distro"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
@ -24,9 +24,9 @@ type packageSBOMImportAPI interface {
|
|||||||
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
|
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
|
var buf bytes.Buffer
|
||||||
pres := jsonPresenter.NewPresenter(catalog, s, d)
|
pres := packages.NewJSONPresenter(catalog, s, d, scope)
|
||||||
err := pres.Present(&buf)
|
err := pres.Present(&buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
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
|
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")
|
log.Debug("importing package SBOM")
|
||||||
stage.Current = "package SBOM"
|
stage.Current = "package SBOM"
|
||||||
|
|
||||||
model, err := packageSbomModel(s, catalog, d)
|
model, err := packageSbomModel(s, catalog, d, scope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
|
return "", fmt.Errorf("unable to create PackageSBOM model: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,9 +9,9 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"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"
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
@ -38,7 +38,6 @@ func TestPackageSbomToModel(t *testing.T) {
|
|||||||
Scheme: source.ImageScheme,
|
Scheme: source.ImageScheme,
|
||||||
ImageMetadata: source.ImageMetadata{
|
ImageMetadata: source.ImageMetadata{
|
||||||
UserInput: "user-in",
|
UserInput: "user-in",
|
||||||
Scope: "scope!",
|
|
||||||
Layers: []source.LayerMetadata{
|
Layers: []source.LayerMetadata{
|
||||||
{
|
{
|
||||||
MediaType: "layer-metadata-type!",
|
MediaType: "layer-metadata-type!",
|
||||||
@ -76,7 +75,7 @@ func TestPackageSbomToModel(t *testing.T) {
|
|||||||
|
|
||||||
c := pkg.NewCatalog(p)
|
c := pkg.NewCatalog(p)
|
||||||
|
|
||||||
model, err := packageSbomModel(m, c, &d)
|
model, err := packageSbomModel(m, c, &d, source.AllLayersScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate model from source material: %+v", err)
|
t.Fatalf("unable to generate model from source material: %+v", err)
|
||||||
}
|
}
|
||||||
@ -89,19 +88,19 @@ func TestPackageSbomToModel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
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 {
|
if err := pres.Present(&buf); err != nil {
|
||||||
t.Fatalf("unable to get expected json: %+v", err)
|
t.Fatalf("unable to get expected json: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal expected result
|
// unmarshal expected result
|
||||||
var expectedDoc jsonPresenter.Document
|
var expectedDoc packages.JSONDocument
|
||||||
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
|
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
|
||||||
t.Fatalf("unable to parse json doc: %+v", err)
|
t.Fatalf("unable to parse json doc: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal actual result
|
// unmarshal actual result
|
||||||
var actualDoc jsonPresenter.Document
|
var actualDoc packages.JSONDocument
|
||||||
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
|
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
|
||||||
t.Fatalf("unable to parse json doc: %+v", err)
|
t.Fatalf("unable to parse json doc: %+v", err)
|
||||||
}
|
}
|
||||||
@ -178,10 +177,9 @@ func TestPackageSbomImport(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
m := source.Metadata{
|
m := source.Metadata{
|
||||||
Scheme: "a-schema",
|
Scheme: source.ImageScheme,
|
||||||
ImageMetadata: source.ImageMetadata{
|
ImageMetadata: source.ImageMetadata{
|
||||||
UserInput: "user-in",
|
UserInput: "user-in",
|
||||||
Scope: "scope!",
|
|
||||||
Layers: nil,
|
Layers: nil,
|
||||||
Size: 10,
|
Size: 10,
|
||||||
ManifestDigest: "sha256:digest!",
|
ManifestDigest: "sha256:digest!",
|
||||||
@ -192,7 +190,7 @@ func TestPackageSbomImport(t *testing.T) {
|
|||||||
|
|
||||||
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
|
d, _ := distro.NewDistro(distro.CentOS, "8.0", "")
|
||||||
|
|
||||||
theModel, err := packageSbomModel(m, catalog, &d)
|
theModel, err := packageSbomModel(m, catalog, &d, source.AllLayersScope)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("could not get sbom model: %+v", err)
|
t.Fatalf("could not get sbom model: %+v", err)
|
||||||
}
|
}
|
||||||
@ -231,7 +229,7 @@ func TestPackageSbomImport(t *testing.T) {
|
|||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
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
|
// validate error handling
|
||||||
if err != nil && !test.expectsError {
|
if err != nil && !test.expectsError {
|
||||||
|
|||||||
13
internal/config/anchore.go
Normal file
13
internal/config/anchore.go
Normal 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
|
||||||
|
}
|
||||||
196
internal/config/application.go
Normal file
196
internal/config/application.go
Normal 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"})
|
||||||
|
}
|
||||||
23
internal/config/cataloger_options.go
Normal file
23
internal/config/cataloger_options.go
Normal 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
|
||||||
|
}
|
||||||
7
internal/config/cli_only_options.go
Normal file
7
internal/config/cli_only_options.go
Normal 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
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
6
internal/config/development.go
Normal file
6
internal/config/development.go
Normal 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"`
|
||||||
|
}
|
||||||
10
internal/config/file_metadata.go
Normal file
10
internal/config/file_metadata.go
Normal 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()
|
||||||
|
}
|
||||||
11
internal/config/logging.go
Normal file
11
internal/config/logging.go
Normal 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
|
||||||
|
}
|
||||||
9
internal/config/packages.go
Normal file
9
internal/config/packages.go
Normal 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()
|
||||||
|
}
|
||||||
@ -6,5 +6,5 @@ const (
|
|||||||
|
|
||||||
// JSONSchemaVersion is the current schema version output by the JSON presenter
|
// 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.
|
// 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"
|
||||||
)
|
)
|
||||||
|
|||||||
69
internal/presenter/packages/cyclonedx_bom_descriptor.go
Normal file
69
internal/presenter/packages/cyclonedx_bom_descriptor.go
Normal 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
|
||||||
|
}
|
||||||
27
internal/presenter/packages/cyclonedx_component.go
Normal file
27
internal/presenter/packages/cyclonedx_component.go
Normal 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
|
||||||
|
}
|
||||||
57
internal/presenter/packages/cyclonedx_document.go
Normal file
57
internal/presenter/packages/cyclonedx_document.go
Normal 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
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
/*
|
/*
|
||||||
Package cyclonedx is responsible for generating a CycloneDX XML report for the given container image or file system.
|
Package cyclonedx is responsible for generating a CycloneDX XML report for the given container image or file system.
|
||||||
*/
|
*/
|
||||||
package cyclonedx
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
@ -11,23 +11,23 @@ import (
|
|||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Presenter writes a CycloneDX report from the given Catalog and Locations contents
|
// CycloneDxPresenter writes a CycloneDX report from the given Catalog and Locations contents
|
||||||
type Presenter struct {
|
type CycloneDxPresenter struct {
|
||||||
catalog *pkg.Catalog
|
catalog *pkg.Catalog
|
||||||
srcMetadata source.Metadata
|
srcMetadata source.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
|
// NewCycloneDxPresenter creates a CycloneDX presenter from the given Catalog and Locations objects.
|
||||||
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
|
func NewCycloneDxPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *CycloneDxPresenter {
|
||||||
return &Presenter{
|
return &CycloneDxPresenter{
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
srcMetadata: srcMetadata,
|
srcMetadata: srcMetadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present writes the CycloneDX report to the given io.Writer.
|
// Present writes the CycloneDX report to the given io.Writer.
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
func (pres *CycloneDxPresenter) Present(output io.Writer) error {
|
||||||
bom := NewDocument(pres.catalog, pres.srcMetadata)
|
bom := NewCycloneDxDocument(pres.catalog, pres.srcMetadata)
|
||||||
|
|
||||||
encoder := xml.NewEncoder(output)
|
encoder := xml.NewEncoder(output)
|
||||||
encoder.Indent("", " ")
|
encoder.Indent("", " ")
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package cyclonedx
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -62,7 +62,7 @@ func TestCycloneDxDirsPresenter(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -93,8 +93,7 @@ func TestCycloneDxImgsPresenter(t *testing.T) {
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.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",
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
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"
|
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
|
||||||
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||||
|
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -1,21 +1,21 @@
|
|||||||
package json
|
package packages
|
||||||
|
|
||||||
import "github.com/anchore/syft/syft/distro"
|
import "github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
// Distribution provides information about a detected Linux Distribution.
|
// JSONDistribution provides information about a detected Linux JSONDistribution.
|
||||||
type Distribution struct {
|
type JSONDistribution struct {
|
||||||
Name string `json:"name"` // Name of the Linux distribution
|
Name string `json:"name"` // Name of the Linux distribution
|
||||||
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
|
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
|
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.
|
// NewJSONDistribution creates a struct with the Linux distribution to be represented in JSON.
|
||||||
func NewDistribution(d *distro.Distro) Distribution {
|
func NewJSONDistribution(d *distro.Distro) JSONDistribution {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
return Distribution{}
|
return JSONDistribution{}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Distribution{
|
return JSONDistribution{
|
||||||
Name: d.Name(),
|
Name: d.Name(),
|
||||||
Version: d.FullVersion(),
|
Version: d.FullVersion(),
|
||||||
IDLike: d.IDLike,
|
IDLike: d.IDLike,
|
||||||
62
internal/presenter/packages/json_document.go
Normal file
62
internal/presenter/packages/json_document.go
Normal 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"`
|
||||||
|
}
|
||||||
71
internal/presenter/packages/json_package.go
Normal file
71
internal/presenter/packages/json_package.go
Normal 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
|
||||||
|
}
|
||||||
43
internal/presenter/packages/json_presenter.go
Normal file
43
internal/presenter/packages/json_presenter.go
Normal 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)
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package json
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -15,7 +15,7 @@ import (
|
|||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"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 {
|
func must(c pkg.CPE, e error) pkg.CPE {
|
||||||
if e != nil {
|
if e != nil {
|
||||||
@ -24,7 +24,7 @@ func must(c pkg.CPE, e error) pkg.CPE {
|
|||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestJsonDirsPresenter(t *testing.T) {
|
func TestJSONDirsPresenter(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
@ -75,7 +75,7 @@ func TestJsonDirsPresenter(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pres := NewPresenter(catalog, s.Metadata, d)
|
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -84,7 +84,7 @@ func TestJsonDirsPresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateJSONGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
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
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
|
|
||||||
if *update {
|
if *updateJSONGoldenFiles {
|
||||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
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
|
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
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
|
var d *distro.Distro
|
||||||
pres := NewPresenter(catalog, s.Metadata, d)
|
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -169,7 +169,7 @@ func TestJsonImgsPresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateJSONGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1,18 +1,18 @@
|
|||||||
package json
|
package packages
|
||||||
|
|
||||||
import "github.com/anchore/syft/syft/pkg"
|
import "github.com/anchore/syft/syft/pkg"
|
||||||
|
|
||||||
type Relationship struct {
|
type JSONRelationship struct {
|
||||||
Parent string `json:"parent"`
|
Parent string `json:"parent"`
|
||||||
Child string `json:"child"`
|
Child string `json:"child"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Metadata interface{} `json:"metadata"`
|
Metadata interface{} `json:"metadata"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRelationships(relationships []pkg.Relationship) []Relationship {
|
func newJSONRelationships(relationships []pkg.Relationship) []JSONRelationship {
|
||||||
result := make([]Relationship, len(relationships))
|
result := make([]JSONRelationship, len(relationships))
|
||||||
for i, r := range relationships {
|
for i, r := range relationships {
|
||||||
result[i] = Relationship{
|
result[i] = JSONRelationship{
|
||||||
Parent: string(r.Parent),
|
Parent: string(r.Parent),
|
||||||
Child: string(r.Child),
|
Child: string(r.Child),
|
||||||
Type: string(r.Type),
|
Type: string(r.Type),
|
||||||
39
internal/presenter/packages/json_source.go
Normal file
39
internal/presenter/packages/json_source.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
25
internal/presenter/packages/presenter.go
Normal file
25
internal/presenter/packages/presenter.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
14
internal/presenter/packages/presenter_config.go
Normal file
14
internal/presenter/packages/presenter_config.go
Normal 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
|
||||||
|
}
|
||||||
35
internal/presenter/packages/presenter_option.go
Normal file
35
internal/presenter/packages/presenter_option.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package table
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -11,17 +11,17 @@ import (
|
|||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Presenter struct {
|
type TablePresenter struct {
|
||||||
catalog *pkg.Catalog
|
catalog *pkg.Catalog
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPresenter(catalog *pkg.Catalog) *Presenter {
|
func NewTablePresenter(catalog *pkg.Catalog) *TablePresenter {
|
||||||
return &Presenter{
|
return &TablePresenter{
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pres *Presenter) Present(output io.Writer) error {
|
func (pres *TablePresenter) Present(output io.Writer) error {
|
||||||
rows := make([][]string, 0)
|
rows := make([][]string, 0)
|
||||||
|
|
||||||
columns := []string{"Name", "Version", "Type"}
|
columns := []string{"Name", "Version", "Type"}
|
||||||
@ -42,7 +42,7 @@ func (pres *Presenter) Present(output io.Writer) error {
|
|||||||
// sort by name, version, then type
|
// sort by name, version, then type
|
||||||
sort.SliceStable(rows, func(i, j int) bool {
|
sort.SliceStable(rows, func(i, j int) bool {
|
||||||
for col := 0; col < len(columns); col++ {
|
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]
|
return rows[i][col] < rows[j][col]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -73,7 +73,6 @@ func (pres *Presenter) Present(output io.Writer) error {
|
|||||||
|
|
||||||
func removeDuplicateRows(items [][]string) [][]string {
|
func removeDuplicateRows(items [][]string) [][]string {
|
||||||
seen := map[string][]string{}
|
seen := map[string][]string{}
|
||||||
// nolint:prealloc
|
|
||||||
var result [][]string
|
var result [][]string
|
||||||
|
|
||||||
for _, v := range items {
|
for _, v := range items {
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package table
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -16,7 +16,7 @@ import (
|
|||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"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) {
|
func TestTablePresenter(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
@ -24,8 +24,7 @@ func TestTablePresenter(t *testing.T) {
|
|||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||||
@ -48,7 +47,7 @@ func TestTablePresenter(t *testing.T) {
|
|||||||
Type: pkg.DebPkg,
|
Type: pkg.DebPkg,
|
||||||
})
|
})
|
||||||
|
|
||||||
pres := NewPresenter(catalog)
|
pres := NewTablePresenter(catalog)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err := pres.Present(&buffer)
|
err := pres.Present(&buffer)
|
||||||
@ -57,7 +56,7 @@ func TestTablePresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateTablePresenterGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -60,6 +60,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"artifactRelationships": [],
|
||||||
"source": {
|
"source": {
|
||||||
"type": "directory",
|
"type": "directory",
|
||||||
"target": "/some/path"
|
"target": "/some/path"
|
||||||
@ -74,8 +75,7 @@
|
|||||||
"version": "[not provided]"
|
"version": "[not provided]"
|
||||||
},
|
},
|
||||||
"schema": {
|
"schema": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.4",
|
||||||
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.3.json"
|
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.0.4.json"
|
||||||
},
|
}
|
||||||
"artifactRelationships": []
|
|
||||||
}
|
}
|
||||||
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
@ -1,4 +1,4 @@
|
|||||||
package text
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -10,22 +10,22 @@ import (
|
|||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Presenter is a human-friendly text presenter to represent package and source data.
|
// TextPresenter is a human-friendly text presenter to represent package and source data.
|
||||||
type Presenter struct {
|
type TextPresenter struct {
|
||||||
catalog *pkg.Catalog
|
catalog *pkg.Catalog
|
||||||
srcMetadata source.Metadata
|
srcMetadata source.Metadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPresenter creates a new presenter for the given set of catalog and image data.
|
// NewTextPresenter creates a new presenter for the given set of catalog and image data.
|
||||||
func NewPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *Presenter {
|
func NewTextPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *TextPresenter {
|
||||||
return &Presenter{
|
return &TextPresenter{
|
||||||
catalog: catalog,
|
catalog: catalog,
|
||||||
srcMetadata: srcMetadata,
|
srcMetadata: srcMetadata,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present is a method that is in charge of writing to an output buffer
|
// 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
|
// init the tabular writer
|
||||||
w := new(tabwriter.Writer)
|
w := new(tabwriter.Writer)
|
||||||
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
|
w.Init(output, 0, 8, 0, '\t', tabwriter.AlignRight)
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package text
|
package packages
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -14,7 +14,7 @@ import (
|
|||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"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) {
|
func TestTextDirPresenter(t *testing.T) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
@ -37,7 +37,7 @@ func TestTextDirPresenter(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to create source: %+v", err)
|
t.Fatalf("unable to create source: %+v", err)
|
||||||
}
|
}
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewTextPresenter(catalog, s.Metadata)
|
||||||
|
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
@ -46,7 +46,7 @@ func TestTextDirPresenter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
|
|
||||||
if *update {
|
if *updateTextPresenterGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -69,8 +69,7 @@ func TestTextImgPresenter(t *testing.T) {
|
|||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
img, cleanup := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||||
defer cleanup()
|
|
||||||
|
|
||||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.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"
|
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 {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
pres := NewPresenter(catalog, s.Metadata)
|
pres := NewTextPresenter(catalog, s.Metadata)
|
||||||
// run presenter
|
// run presenter
|
||||||
err = pres.Present(&buffer)
|
err = pres.Present(&buffer)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
actual := buffer.Bytes()
|
actual := buffer.Bytes()
|
||||||
if *update {
|
if *updateTextPresenterGoldenFiles {
|
||||||
testutils.UpdateGoldenFileContents(t, actual)
|
testutils.UpdateGoldenFileContents(t, actual)
|
||||||
}
|
}
|
||||||
|
|
||||||
32
internal/presenter/poweruser/json_document.go
Normal file
32
internal/presenter/poweruser/json_document.go
Normal 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
|
||||||
|
}
|
||||||
18
internal/presenter/poweruser/json_document_config.go
Normal file
18
internal/presenter/poweruser/json_document_config.go
Normal 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
|
||||||
|
}
|
||||||
59
internal/presenter/poweruser/json_file_metadata.go
Normal file
59
internal/presenter/poweruser/json_file_metadata.go
Normal 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
|
||||||
|
}
|
||||||
32
internal/presenter/poweruser/json_presenter.go
Normal file
32
internal/presenter/poweruser/json_presenter.go
Normal 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)
|
||||||
|
}
|
||||||
9
internal/presenter/presenter.go
Normal file
9
internal/presenter/presenter.go
Normal 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
|
||||||
|
}
|
||||||
@ -8,11 +8,11 @@ import (
|
|||||||
"github.com/wagoodman/go-partybus"
|
"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.
|
// via the given presenter to stdout.
|
||||||
func CatalogerFinishedHandler(event partybus.Event) error {
|
func CatalogerPresenterReady(event partybus.Event) error {
|
||||||
// show the report to stdout
|
// show the report to stdout
|
||||||
pres, err := syftEventParsers.ParseCatalogerFinished(event)
|
pres, err := syftEventParsers.ParsePresenterReady(event)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -129,7 +129,7 @@ eventLoop:
|
|||||||
log.Errorf("unable to show %s event: %+v", e.Type, err)
|
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
|
// we may have other background processes still displaying progress, wait for them to
|
||||||
// finish before discontinuing dynamic content and showing the final report
|
// finish before discontinuing dynamic content and showing the final report
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
@ -146,7 +146,7 @@ eventLoop:
|
|||||||
fmt.Fprint(output, logBuffer.String())
|
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)
|
log.Errorf("unable to show %s event: %+v", e.Type, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -24,8 +24,8 @@ eventLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ignore all events except for the final event
|
// ignore all events except for the final event
|
||||||
if e.Type == syftEvent.CatalogerFinished {
|
if e.Type == syftEvent.PresenterReady {
|
||||||
err := common.CatalogerFinishedHandler(e)
|
err := common.CatalogerPresenterReady(e)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("unable to show catalog image finished event: %+v", err)
|
log.Errorf("unable to show catalog image finished event: %+v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ package version
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime"
|
"runtime"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const valueNotProvided = "[not provided]"
|
const valueNotProvided = "[not provided]"
|
||||||
@ -28,6 +29,13 @@ type Version struct {
|
|||||||
Platform string `json:"platform"` // GOOS and GOARCH at build-time
|
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
|
// FromBuild provides all version details
|
||||||
func FromBuild() Version {
|
func FromBuild() Version {
|
||||||
return Version{
|
return Version{
|
||||||
|
|||||||
@ -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.
|
// IsUpdateAvailable indicates if there is a newer application version available, and if so, what the new version is.
|
||||||
func IsUpdateAvailable() (bool, string, error) {
|
func IsUpdateAvailable() (bool, string, error) {
|
||||||
currentVersionStr := FromBuild().Version
|
currentBuildInfo := FromBuild()
|
||||||
currentVersion, err := hashiVersion.NewVersion(currentVersionStr)
|
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 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)
|
return false, "", fmt.Errorf("failed to parse current application version: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -81,6 +81,15 @@ func TestIsUpdateAvailable(t *testing.T) {
|
|||||||
newVersion: "",
|
newVersion: "",
|
||||||
err: false,
|
err: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "SnapshotBuildVersion",
|
||||||
|
buildVersion: "2.0.0-SHAPSHOT-a78bf9c",
|
||||||
|
latestVersion: "1.0.0",
|
||||||
|
code: 200,
|
||||||
|
isAvailable: false,
|
||||||
|
newVersion: "",
|
||||||
|
err: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "BadUpdateValidVersion",
|
name: "BadUpdateValidVersion",
|
||||||
buildVersion: "1.0.0",
|
buildVersion: "1.0.0",
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
# JSON Schema
|
# 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 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 `Document` struct definition within `internal/presenters/poweruser/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 `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.
|
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:
|
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
|
- 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
|
## Generating a New Schema
|
||||||
|
|
||||||
|
|||||||
@ -6,13 +6,14 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"reflect"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/alecthomas/jsonschema"
|
"github.com/alecthomas/jsonschema"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/presenter/poweruser"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"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{}).
|
// 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
|
// 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.
|
// not matter as long as it is exported.
|
||||||
type metadataContainer struct {
|
type artifactMetadataContainer struct {
|
||||||
Apk pkg.ApkMetadata
|
Apk pkg.ApkMetadata
|
||||||
Dpkg pkg.DpkgMetadata
|
Dpkg pkg.DpkgMetadata
|
||||||
Gem pkg.GemMetadata
|
Gem pkg.GemMetadata
|
||||||
@ -36,10 +37,23 @@ type metadataContainer struct {
|
|||||||
Cargo pkg.CargoPackageMetadata
|
Cargo pkg.CargoPackageMetadata
|
||||||
}
|
}
|
||||||
|
|
||||||
// nolint:funlen
|
|
||||||
func main() {
|
func main() {
|
||||||
metadataSchema := jsonschema.Reflect(&metadataContainer{})
|
write(encode(build()))
|
||||||
documentSchema := jsonschema.Reflect(&jsonPresenter.Document{})
|
}
|
||||||
|
|
||||||
|
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
|
// TODO: inject source definitions
|
||||||
|
|
||||||
@ -47,7 +61,7 @@ func main() {
|
|||||||
|
|
||||||
var metadataNames []string
|
var metadataNames []string
|
||||||
for name, definition := range metadataSchema.Definitions {
|
for name, definition := range metadataSchema.Definitions {
|
||||||
if name == "metadataContainer" {
|
if name == "artifactMetadataContainer" {
|
||||||
// ignore the definition for the fake container
|
// ignore the definition for the fake container
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -71,22 +85,30 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the "anyOf" field for Package.Metadata to be a conjunction of several types
|
// 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,
|
"anyOf": metadataTypes,
|
||||||
})
|
})
|
||||||
|
|
||||||
filename := fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion)
|
return documentSchema
|
||||||
|
}
|
||||||
|
|
||||||
|
func encode(schema *jsonschema.Schema) []byte {
|
||||||
var newSchemaBuffer = new(bytes.Buffer)
|
var newSchemaBuffer = new(bytes.Buffer)
|
||||||
enc := json.NewEncoder(newSchemaBuffer)
|
enc := json.NewEncoder(newSchemaBuffer)
|
||||||
// prevent > and < from being escaped in the payload
|
// prevent > and < from being escaped in the payload
|
||||||
enc.SetEscapeHTML(false)
|
enc.SetEscapeHTML(false)
|
||||||
enc.SetIndent("", " ")
|
enc.SetIndent("", " ")
|
||||||
err := enc.Encode(&documentSchema)
|
err := enc.Encode(&schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
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) {
|
if _, err := os.Stat(filename); !os.IsNotExist(err) {
|
||||||
// check if the schema is the same...
|
// check if the schema is the same...
|
||||||
existingFh, err := os.Open(filename)
|
existingFh, err := os.Open(filename)
|
||||||
@ -99,7 +121,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if bytes.Equal(existingSchemaBytes, newSchemaBuffer.Bytes()) {
|
if bytes.Equal(existingSchemaBytes, schema) {
|
||||||
// the generated schema is the same, bail with no error :)
|
// the generated schema is the same, bail with no error :)
|
||||||
fmt.Println("No change to the existing schema!")
|
fmt.Println("No change to the existing schema!")
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@ -115,7 +137,7 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = fh.Write(newSchemaBuffer.Bytes())
|
_, err = fh.Write(schema)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
830
schema/json/schema-1.0.4.json
Normal file
830
schema/json/schema-1.0.4.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -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
|
|
||||||
}
|
|
||||||
@ -36,7 +36,7 @@ var identityFiles = []parseEntry{
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Identify parses distro-specific files to determine distro metadata like version and release.
|
// 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
|
var distro *Distro
|
||||||
|
|
||||||
identifyLoop:
|
identifyLoop:
|
||||||
|
|||||||
@ -99,7 +99,12 @@ func TestIdentifyDistro(t *testing.T) {
|
|||||||
t.Fatalf("unable to produce a new source for testing: %s", test.fixture)
|
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 d == nil {
|
||||||
if test.Type == UnknownDistroType {
|
if test.Type == UnknownDistroType {
|
||||||
return
|
return
|
||||||
|
|||||||
@ -10,11 +10,11 @@ const (
|
|||||||
// AppUpdateAvailable is a partybus event that occurs when an application update is available
|
// AppUpdateAvailable is a partybus event that occurs when an application update is available
|
||||||
AppUpdateAvailable partybus.EventType = "syft-app-update-available"
|
AppUpdateAvailable partybus.EventType = "syft-app-update-available"
|
||||||
|
|
||||||
// CatalogerStarted is a partybus event that occurs when the package cataloging has begun
|
// PackageCatalogerStarted is a partybus event that occurs when the package cataloging has begun
|
||||||
CatalogerStarted partybus.EventType = "syft-cataloger-started-event"
|
PackageCatalogerStarted partybus.EventType = "syft-cataloger-started-event"
|
||||||
|
|
||||||
// CatalogerFinished is a partybus event that occurs when the package cataloging has completed
|
// PresenterReady is a partybus event that occurs when an analysis result is ready for final presentation
|
||||||
CatalogerFinished partybus.EventType = "syft-cataloger-finished-event"
|
PresenterReady partybus.EventType = "syft-presenter-ready-event"
|
||||||
|
|
||||||
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
// ImportStarted is a partybus event that occurs when an SBOM upload process has begun
|
||||||
ImportStarted partybus.EventType = "syft-import-started-event"
|
ImportStarted partybus.EventType = "syft-import-started-event"
|
||||||
|
|||||||
@ -6,11 +6,12 @@ package parsers
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/presenter"
|
||||||
|
|
||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/cataloger"
|
|
||||||
"github.com/anchore/syft/syft/event"
|
"github.com/anchore/syft/syft/event"
|
||||||
"github.com/anchore/syft/syft/presenter"
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ func checkEventType(actual, expected partybus.EventType) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func ParseCatalogerStarted(e partybus.Event) (*cataloger.Monitor, 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
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -52,8 +53,8 @@ func ParseCatalogerStarted(e partybus.Event) (*cataloger.Monitor, error) {
|
|||||||
return &monitor, nil
|
return &monitor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseCatalogerFinished(e partybus.Event) (presenter.Presenter, error) {
|
func ParsePresenterReady(e partybus.Event) (presenter.Presenter, error) {
|
||||||
if err := checkEventType(e.Type, event.CatalogerFinished); err != nil {
|
if err := checkEventType(e.Type, event.PresenterReady); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
6
syft/file/digest.go
Normal file
6
syft/file/digest.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package file
|
||||||
|
|
||||||
|
type Digest struct {
|
||||||
|
Algorithm string `json:"algorithm"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
96
syft/file/digest_cataloger.go
Normal file
96
syft/file/digest_cataloger.go
Normal 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)
|
||||||
|
}
|
||||||
98
syft/file/digest_cataloger_test.go
Normal file
98
syft/file/digest_cataloger_test.go
Normal 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")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
25
syft/file/metadata_cataloger.go
Normal file
25
syft/file/metadata_cataloger.go
Normal 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
|
||||||
|
}
|
||||||
134
syft/file/metadata_cataloger_test.go
Normal file
134
syft/file/metadata_cataloger_test.go
Normal 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")
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
1
syft/file/test-fixtures/a-path.txt
Normal file
1
syft/file/test-fixtures/a-path.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-fixtures/a-path.txt file contents!
|
||||||
1
syft/file/test-fixtures/another-path.txt
Normal file
1
syft/file/test-fixtures/another-path.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-fixtures/another-path.txt file contents!
|
||||||
11
syft/file/test-fixtures/image-file-type-mix/Dockerfile
Normal file
11
syft/file/test-fixtures/image-file-type-mix/Dockerfile
Normal 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
|
||||||
1
syft/file/test-fixtures/image-file-type-mix/file-1.txt
Normal file
1
syft/file/test-fixtures/image-file-type-mix/file-1.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
file 1!
|
||||||
1
syft/file/test-fixtures/last/path.txt
Normal file
1
syft/file/test-fixtures/last/path.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
test-fixtures/last/path.txt file contents!
|
||||||
Binary file not shown.
60
syft/lib.go
60
syft/lib.go
@ -17,32 +17,29 @@ Similar to the cataloging process, Linux distribution identification is also per
|
|||||||
package syft
|
package syft
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/cataloger"
|
|
||||||
"github.com/anchore/syft/syft/distro"
|
"github.com/anchore/syft/syft/distro"
|
||||||
"github.com/anchore/syft/syft/logger"
|
"github.com/anchore/syft/syft/logger"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"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/anchore/syft/syft/source"
|
||||||
"github.com/wagoodman/go-partybus"
|
"github.com/wagoodman/go-partybus"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Catalog the given image from a particular perspective (e.g. squashed source, all-layers source). Returns the discovered
|
// CatalogPackages takes an inventory of packages from the given image from a particular perspective
|
||||||
// set of packages, the identified Linux distribution, and the source object used to wrap the data source.
|
// (e.g. squashed source, all-layers source). Returns the discovered set of packages, the identified Linux
|
||||||
func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog, *distro.Distro, error) {
|
// distribution, and the source object used to wrap the data source.
|
||||||
theSource, cleanup, err := source.New(userInput, scope)
|
func CatalogPackages(src source.Source, scope source.Scope) (*pkg.Catalog, *distro.Distro, error) {
|
||||||
defer cleanup()
|
resolver, err := src.FileResolver(scope)
|
||||||
if err != nil {
|
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
|
// find the distro
|
||||||
theDistro := distro.Identify(theSource.Resolver)
|
theDistro := distro.Identify(resolver)
|
||||||
if theDistro != nil {
|
if theDistro != nil {
|
||||||
log.Infof("identified distro: %s", theDistro.String())
|
log.Infof("identified distro: %s", theDistro.String())
|
||||||
} else {
|
} 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)
|
// conditionally use the correct set of loggers based on the input type (container image or directory)
|
||||||
var catalogers []cataloger.Cataloger
|
var catalogers []cataloger.Cataloger
|
||||||
switch theSource.Metadata.Scheme {
|
switch src.Metadata.Scheme {
|
||||||
case source.ImageScheme:
|
case source.ImageScheme:
|
||||||
log.Info("cataloging image")
|
log.Info("cataloging image")
|
||||||
catalogers = cataloger.ImageCatalogers()
|
catalogers = cataloger.ImageCatalogers()
|
||||||
@ -59,46 +56,15 @@ func Catalog(userInput string, scope source.Scope) (source.Source, *pkg.Catalog,
|
|||||||
log.Info("cataloging directory")
|
log.Info("cataloging directory")
|
||||||
catalogers = cataloger.DirectoryCatalogers()
|
catalogers = cataloger.DirectoryCatalogers()
|
||||||
default:
|
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 {
|
if err != nil {
|
||||||
return source.Source{}, nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return theSource, catalog, theDistro, nil
|
return 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLogger sets the logger object used for all syft logging calls.
|
// SetLogger sets the logger object used for all syft logging calls.
|
||||||
|
|||||||
@ -4,8 +4,8 @@ Package apkdb provides a concrete Cataloger implementation for Alpine DB files.
|
|||||||
package apkdb
|
package apkdb
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/anchore/syft/syft/cataloger/common"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewApkdbCataloger returns a new Alpine DB cataloger object.
|
// NewApkdbCataloger returns a new Alpine DB cataloger object.
|
||||||
@ -9,8 +9,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/log"
|
"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"
|
||||||
|
"github.com/anchore/syft/syft/pkg/cataloger/common"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
)
|
)
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user