mirror of
https://github.com/anchore/syft.git
synced 2026-10-20 10:02:24 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ddfc8e20c0 | ||
|
|
3f305aa6cd | ||
|
|
2f99a35f51 | ||
|
|
06dcd3261d | ||
|
|
abbba3fc19 | ||
|
|
0799fd9d46 | ||
|
|
5de1a0a236 | ||
|
|
035ef03b0c | ||
|
|
9acabb8307 | ||
|
|
0f0fe59bf2 | ||
|
|
ff828fbac2 | ||
|
|
cba5b5723b | ||
|
|
b0e9b92a25 | ||
|
|
d25d3b1fb3 | ||
|
|
93b4bf9379 | ||
|
|
58f2be95fd | ||
|
|
6f038e9208 | ||
|
|
32a54b94be | ||
|
|
98d4749f86 | ||
|
|
3a5168917e | ||
|
|
58b21c03c3 | ||
|
|
2093d2a1dc | ||
|
|
dd56450f92 | ||
|
|
6f2b52971d | ||
|
|
fee74d57b8 | ||
|
|
fee35dd175 | ||
|
|
ecf4e5546a | ||
|
|
2de56c0749 | ||
|
|
fb0857ff93 | ||
|
|
962e82297c | ||
|
|
c833472b6e | ||
|
|
c825ae2404 | ||
|
|
706322f826 | ||
|
|
66e71c39e2 | ||
|
|
6dd698d89e | ||
|
|
50928ebd05 | ||
|
|
6e70d8af98 | ||
|
|
67b7d63875 | ||
|
|
5a2e2eb679 | ||
|
|
801e662633 | ||
|
|
6a398f9771 | ||
|
|
bf1bf057ab | ||
|
|
36b44b1d8e | ||
|
|
8f85c8affc | ||
|
|
776164a078 | ||
|
|
5ea1d78464 | ||
|
|
afbd8f8ea0 | ||
|
|
2f81a2548c | ||
|
|
2754c889eb | ||
|
|
8be0d988e7 | ||
|
|
80e23bdb50 | ||
|
|
17bbf840cf | ||
|
|
2ca2f03501 | ||
|
|
1201ad6df0 |
+9
-1
@@ -6,4 +6,12 @@ permit:
|
||||
- ISC
|
||||
ignore-packages:
|
||||
# packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE
|
||||
- github.com/package-url/packageurl-go
|
||||
- github.com/package-url/packageurl-go
|
||||
|
||||
# from: https://github.com/spdx/tools-golang/blob/main/LICENSE.code
|
||||
# The tools-golang source code is provided and may be used, at your option,
|
||||
# under either:
|
||||
# * Apache License, version 2.0 (Apache-2.0), OR
|
||||
# * GNU General Public License, version 2.0 or later (GPL-2.0-or-later).
|
||||
# (we choose Apache-2.0)
|
||||
- github.com/spdx/tools-golang
|
||||
@@ -1,11 +1,6 @@
|
||||
contact_links:
|
||||
|
||||
- name: Help & Support Slack Channel ⛑️
|
||||
# link to toolbox-help channel
|
||||
url: https://anchorecommunity.slack.com/archives/C019BUXV7R6
|
||||
about: Ask a questions and get answers
|
||||
|
||||
- name: Developer Slack Channel 💬
|
||||
# link to toolbox-dev channel
|
||||
url: https://anchorecommunity.slack.com/archives/C0190NF9TSM
|
||||
about: Participate in design discussions and feature development
|
||||
- name: Join the Slack community 💬
|
||||
# link to our community Slack registration page
|
||||
url: https://anchore.com/slack
|
||||
about: 'Be sure to check out the channels #syft-help (ask us questions) and #syft-dev (collaborate with us on design and development)!'
|
||||
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env bash
|
||||
set -eu
|
||||
|
||||
ORIGINAL_STATE_DIR=$(mktemp -d "TEMP-original-state-XXXXXXXXX")
|
||||
TIDY_STATE_DIR=$(mktemp -d "TEMP-tidy-state-XXXXXXXXX")
|
||||
|
||||
trap "cp -v ${ORIGINAL_STATE_DIR}/* ./ && rm -fR ${ORIGINAL_STATE_DIR} ${TIDY_STATE_DIR}" EXIT
|
||||
|
||||
echo "Capturing original state of files..."
|
||||
cp -v go.mod go.sum "${ORIGINAL_STATE_DIR}"
|
||||
|
||||
echo "Capturing state of go.mod and go.sum after running go mod tidy..."
|
||||
go mod tidy
|
||||
cp -v go.mod go.sum "${TIDY_STATE_DIR}"
|
||||
echo ""
|
||||
|
||||
set +e
|
||||
|
||||
# Detect difference between the git HEAD state and the go mod tidy state
|
||||
DIFF_MOD=$(diff -u "${ORIGINAL_STATE_DIR}/go.mod" "${TIDY_STATE_DIR}/go.mod")
|
||||
DIFF_SUM=$(diff -u "${ORIGINAL_STATE_DIR}/go.sum" "${TIDY_STATE_DIR}/go.sum")
|
||||
|
||||
if [[ -n "${DIFF_MOD}" || -n "${DIFF_SUM}" ]]; then
|
||||
echo "go.mod diff:"
|
||||
echo "${DIFF_MOD}"
|
||||
echo "go.sum diff:"
|
||||
echo "${DIFF_SUM}"
|
||||
echo ""
|
||||
printf "FAILED! go.mod and/or go.sum are NOT tidy; please run 'go mod tidy'.\n\n"
|
||||
exit 1
|
||||
fi
|
||||
@@ -13,9 +13,6 @@ jobs:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
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:
|
||||
@@ -53,9 +50,6 @@ jobs:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Unit tests"
|
||||
runs-on: ubuntu-20.04
|
||||
# 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:
|
||||
@@ -108,9 +102,6 @@ jobs:
|
||||
# Note: changing this job name requires making the same update in the .github/workflows/release.yaml pipeline
|
||||
name: "Integration tests"
|
||||
runs-on: ubuntu-20.04
|
||||
# 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:
|
||||
@@ -234,9 +225,6 @@ jobs:
|
||||
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:
|
||||
@@ -277,9 +265,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -296,9 +281,6 @@ jobs:
|
||||
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
|
||||
|
||||
@@ -322,9 +304,6 @@ jobs:
|
||||
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
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ RESET := $(shell tput -T linux sgr0)
|
||||
TITLE := $(BOLD)$(PURPLE)
|
||||
SUCCESS := $(BOLD)$(GREEN)
|
||||
# the quality gate lower threshold for unit test total % coverage (by function statements)
|
||||
COVERAGE_THRESHOLD := 70
|
||||
COVERAGE_THRESHOLD := 65
|
||||
# CI cache busting values; change these if you want CI to not use previous stored cache
|
||||
INTEGRATION_CACHE_BUSTER="88738d2f"
|
||||
CLI_CACHE_BUSTER="789bacdf"
|
||||
@@ -103,19 +103,18 @@ 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
|
||||
curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh -s -- -b $(TEMPDIR)/ v0.177.0
|
||||
|
||||
.PHONY: bootstrap-go
|
||||
bootstrap-go:
|
||||
go mod download
|
||||
go mod tidy # note: it is important that the go.sum is kept in a consistent state at all times (especially during release)
|
||||
|
||||
.PHONY: bootstrap
|
||||
bootstrap: $(RESULTSDIR) bootstrap-go bootstrap-tools ## Download and install all go dependencies (+ prep tooling in the ./tmp dir)
|
||||
$(call title,Bootstrapping dependencies)
|
||||
|
||||
.PHONY: static-analysis
|
||||
static-analysis: lint check-licenses
|
||||
static-analysis: lint check-go-mod-tidy check-licenses
|
||||
|
||||
.PHONY: lint
|
||||
lint: ## Run gofmt + golangci lint checks
|
||||
@@ -136,11 +135,15 @@ lint-fix: ## Auto-format all source code + run golangci lint fixers
|
||||
$(call title,Running lint fixers)
|
||||
gofmt -w -s .
|
||||
$(LINTCMD) --fix
|
||||
go mod tidy
|
||||
|
||||
.PHONY: check-licenses
|
||||
check-licenses:
|
||||
$(TEMPDIR)/bouncer check
|
||||
|
||||
check-go-mod-tidy:
|
||||
@ .github/scripts/go-mod-tidy-check.sh && echo "go.mod and go.sum are tidy!"
|
||||
|
||||
.PHONY: validate-cyclonedx-schema
|
||||
validate-cyclonedx-schema:
|
||||
cd schema/cyclonedx && make
|
||||
@@ -346,4 +349,4 @@ show-test-image-cache: ## Show all docker and image tar cache
|
||||
.PHONY: show-test-snapshots
|
||||
show-test-snapshots: ## Show all test snapshots
|
||||
$(call title,Test snapshots)
|
||||
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
|
||||
@find . -type f -wholename "**/test-fixtures/snapshot/*" | sort
|
||||
|
||||
@@ -66,7 +66,9 @@ syft packages <image> -o <format>
|
||||
Where the `format`s available are:
|
||||
- `json`: Use this to get as much information out of Syft as possible!
|
||||
- `text`: A row-oriented, human-and-machine-friendly output.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2](https://cyclonedx.org/) specification.
|
||||
- `cyclonedx`: A XML report conforming to the [CycloneDX 1.2 specification](https://cyclonedx.org/specification/overview/).
|
||||
- `spdx`: A tag-value formatted report conforming to the [SPDX 2.2 specification](https://spdx.github.io/spdx-spec/).
|
||||
- `spdx-json`: A JSON report conforming to the [SPDX 2.2 JSON Schema](https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json).
|
||||
- `table`: A columnar summary (default).
|
||||
|
||||
## Installation
|
||||
@@ -202,6 +204,9 @@ registry:
|
||||
# skip TLS verification when communicating with the registry
|
||||
# SYFT_REGISTRY_INSECURE_SKIP_TLS_VERIFY env var
|
||||
insecure-skip-tls-verify: false
|
||||
# use http instead of https when connecting to the registry
|
||||
# SYFT_REGISTRY_INSECURE_USE_HTTP env var
|
||||
insecure-use-http: false
|
||||
|
||||
# credentials for specific registries
|
||||
auth:
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// eventLoop listens to worker errors (from execution path), worker events (from a partybus subscription), and
|
||||
// signal interrupts. Is responsible for handling each event relative to a given UI an to coordinate eventing until
|
||||
// an eventual graceful exit.
|
||||
// nolint:gocognit,funlen
|
||||
func eventLoop(workerErrs <-chan error, signals <-chan os.Signal, subscription *partybus.Subscription, ux ui.UI, cleanupFn func()) error {
|
||||
defer cleanupFn()
|
||||
events := subscription.Events()
|
||||
var err error
|
||||
if ux, err = setupUI(subscription.Unsubscribe, ux); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var retErr error
|
||||
var forceTeardown bool
|
||||
|
||||
for {
|
||||
if workerErrs == nil && events == nil {
|
||||
break
|
||||
}
|
||||
select {
|
||||
case err, isOpen := <-workerErrs:
|
||||
if !isOpen {
|
||||
workerErrs = nil
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
// capture the error from the worker and unsubscribe to complete a graceful shutdown
|
||||
retErr = multierror.Append(retErr, err)
|
||||
if err := subscription.Unsubscribe(); err != nil {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
}
|
||||
}
|
||||
case e, isOpen := <-events:
|
||||
if !isOpen {
|
||||
events = nil
|
||||
continue
|
||||
}
|
||||
|
||||
if err := ux.Handle(e); err != nil {
|
||||
if errors.Is(err, partybus.ErrUnsubscribe) {
|
||||
log.Warnf("unable to unsubscribe from the event bus")
|
||||
events = nil
|
||||
} else {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
// TODO: should we unsubscribe? should we try to halt execution? or continue?
|
||||
}
|
||||
}
|
||||
case <-signals:
|
||||
// ignore further results from any event source and exit ASAP, but ensure that all cache is cleaned up.
|
||||
// we ignore further errors since cleaning up the tmp directories will affect running catalogers that are
|
||||
// reading/writing from/to their nested temp dirs. This is acceptable since we are bailing without result.
|
||||
|
||||
// TODO: potential future improvement would be to pass context into workers with a cancel function that is
|
||||
// to the event loop. In this way we can have a more controlled shutdown even at the most nested levels
|
||||
// of processing.
|
||||
events = nil
|
||||
workerErrs = nil
|
||||
forceTeardown = true
|
||||
}
|
||||
}
|
||||
|
||||
if err := ux.Teardown(forceTeardown); err != nil {
|
||||
retErr = multierror.Append(retErr, err)
|
||||
}
|
||||
|
||||
return retErr
|
||||
}
|
||||
|
||||
func setupUI(unsubscribe func() error, ux ui.UI) (ui.UI, error) {
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
// replace the existing UI with a (simpler) logger UI
|
||||
ux = ui.NewLoggerUI()
|
||||
if err := ux.Setup(unsubscribe); err != nil {
|
||||
// something is very wrong, bail.
|
||||
return ux, err
|
||||
}
|
||||
log.Errorf("unable to setup given UI, falling back to logger: %+v", err)
|
||||
}
|
||||
return ux, nil
|
||||
}
|
||||
@@ -0,0 +1,455 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
"github.com/anchore/syft/syft/event"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
var _ ui.UI = (*uiMock)(nil)
|
||||
|
||||
type uiMock struct {
|
||||
t *testing.T
|
||||
finalEvent partybus.Event
|
||||
unsubscribe func() error
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
func (u *uiMock) Setup(unsubscribe func() error) error {
|
||||
u.t.Logf("UI Setup called")
|
||||
u.unsubscribe = unsubscribe
|
||||
return u.Called(unsubscribe).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Handle(event partybus.Event) error {
|
||||
u.t.Logf("UI Handle called: %+v", event.Type)
|
||||
if event == u.finalEvent {
|
||||
assert.NoError(u.t, u.unsubscribe())
|
||||
}
|
||||
return u.Called(event).Error(0)
|
||||
}
|
||||
|
||||
func (u *uiMock) Teardown(_ bool) error {
|
||||
u.t.Logf("UI Teardown called")
|
||||
return u.Called().Error(0)
|
||||
}
|
||||
|
||||
func Test_eventLoop_gracefulExit(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event
|
||||
ux.On("Handle", finalEvent).Return(nil)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_workerError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
workerErr := fmt.Errorf("worker error")
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
ret <- workerErr
|
||||
t.Log("worker sent error")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// note: NO final event is fired
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
}
|
||||
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// ensure we see an error returned
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
),
|
||||
workerErr,
|
||||
"should have seen a worker error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_unsubscribeError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event... note the unsubscribe error here
|
||||
ux.On("Handle", finalEvent).Return(partybus.ErrUnsubscribe)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// unsubscribe errors should be handled and ignored, not propagated. We are additionally asserting that
|
||||
// this case is handled as a controlled shutdown (this test should not timeout)
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_handlerError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
Error: fmt.Errorf("unable to create presenter"),
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
// ensure the mock sees at least the final event... note the event error is propagated
|
||||
ux.On("Handle", finalEvent).Return(finalEvent.Error)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// handle errors SHOULD propagate the event loop. We are additionally asserting that this case is
|
||||
// handled as a controlled shutdown (this test should not timeout)
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
),
|
||||
finalEvent.Error,
|
||||
"should have seen a event error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_signalsStopExecution(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
worker := func() <-chan error {
|
||||
// the worker will never return work and the event loop will always be waiting...
|
||||
return make(chan error)
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
ret := make(chan os.Signal)
|
||||
go func() {
|
||||
ret <- syscall.SIGINT
|
||||
// note: we do NOT close the channel to ensure the event loop does not depend on that behavior to exit
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
}
|
||||
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(nil)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
assert.NoError(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
),
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func Test_eventLoop_uiTeardownError(t *testing.T) {
|
||||
test := func(t *testing.T) {
|
||||
|
||||
testBus := partybus.NewBus()
|
||||
subscription := testBus.Subscribe()
|
||||
t.Cleanup(testBus.Close)
|
||||
|
||||
finalEvent := partybus.Event{
|
||||
Type: event.PresenterReady,
|
||||
}
|
||||
|
||||
worker := func() <-chan error {
|
||||
ret := make(chan error)
|
||||
go func() {
|
||||
t.Log("worker running")
|
||||
// send an empty item (which is ignored) ensuring we've entered the select statement,
|
||||
// then close (a partial shutdown).
|
||||
ret <- nil
|
||||
t.Log("worker sent nothing")
|
||||
close(ret)
|
||||
t.Log("worker closed")
|
||||
// do the other half of the shutdown
|
||||
testBus.Publish(finalEvent)
|
||||
t.Log("worker published final event")
|
||||
}()
|
||||
return ret
|
||||
}
|
||||
|
||||
signaler := func() <-chan os.Signal {
|
||||
return nil
|
||||
}
|
||||
|
||||
ux := &uiMock{
|
||||
t: t,
|
||||
finalEvent: finalEvent,
|
||||
}
|
||||
|
||||
teardownError := fmt.Errorf("sorry, dave, the UI doesn't want to be torn down")
|
||||
|
||||
// ensure the mock sees at least the final event... note the event error is propagated
|
||||
ux.On("Handle", finalEvent).Return(nil)
|
||||
// ensure the mock sees basic setup/teardown events
|
||||
ux.On("Setup", mock.AnythingOfType("func() error")).Return(nil)
|
||||
ux.On("Teardown").Return(teardownError)
|
||||
|
||||
var cleanupCalled bool
|
||||
cleanupFn := func() {
|
||||
t.Log("cleanup called")
|
||||
cleanupCalled = true
|
||||
}
|
||||
|
||||
// ensure we see an error returned
|
||||
assert.ErrorIs(t,
|
||||
eventLoop(
|
||||
worker(),
|
||||
signaler(),
|
||||
subscription,
|
||||
ux,
|
||||
cleanupFn,
|
||||
),
|
||||
teardownError,
|
||||
"should have seen a UI teardown error, but did not",
|
||||
)
|
||||
|
||||
assert.True(t, cleanupCalled, "cleanup function not called")
|
||||
ux.AssertExpectations(t)
|
||||
}
|
||||
|
||||
// if there is a bug, then there is a risk of the event loop never returning
|
||||
testWithTimeout(t, 5*time.Second, test)
|
||||
}
|
||||
|
||||
func testWithTimeout(t *testing.T, timeout time.Duration, test func(*testing.T)) {
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
test(t)
|
||||
done <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-time.After(timeout):
|
||||
t.Fatal("test timed out")
|
||||
case <-done:
|
||||
}
|
||||
}
|
||||
+31
-14
@@ -6,29 +6,31 @@ import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"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/presenter/packages"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/pkg/profile"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"github.com/spf13/viper"
|
||||
"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 -o cyclonedx show a CycloneDX formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx show a SPDX 2.2 tag-value formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -o spdx-json show a SPDX 2.2 JSON formatted SBOM
|
||||
{{.appName}} {{.command}} alpine:latest -vv show verbose debug information
|
||||
|
||||
Supports the following image sources:
|
||||
@@ -47,7 +49,7 @@ const (
|
||||
|
||||
var (
|
||||
packagesPresenterOpt packages.PresenterOption
|
||||
packagesArgs = cobra.MinimumNArgs(1)
|
||||
packagesArgs = cobra.MaximumNArgs(1)
|
||||
packagesCmd = &cobra.Command{
|
||||
Use: "packages [SOURCE]",
|
||||
Short: "Generate a package SBOM",
|
||||
@@ -65,8 +67,7 @@ var (
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// silently exit
|
||||
return fmt.Errorf("")
|
||||
return fmt.Errorf("an image/directory argument is required")
|
||||
}
|
||||
|
||||
// set the presenter
|
||||
@@ -137,6 +138,11 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
||||
"overwrite-existing-image", false,
|
||||
"overwrite an existing image during the upload to Anchore Enterprise",
|
||||
)
|
||||
|
||||
flags.Uint(
|
||||
"import-timeout", 30,
|
||||
"set a timeout duration (in seconds) for the upload to Anchore Enterprise",
|
||||
)
|
||||
}
|
||||
|
||||
func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
@@ -172,13 +178,23 @@ func bindPackagesConfigOptions(flags *pflag.FlagSet) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := viper.BindPFlag("anchore.import-timeout", flags.Lookup("import-timeout")); 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)
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
return eventLoop(
|
||||
packagesExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||
stereoscope.Cleanup,
|
||||
)
|
||||
}
|
||||
|
||||
func packagesExecWorker(userInput string) <-chan error {
|
||||
@@ -190,14 +206,14 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
|
||||
src, cleanup, err := source.New(userInput, appConfig.Registry.ToOptions())
|
||||
if err != nil {
|
||||
errs <- fmt.Errorf("failed to determine image source: %+v", err)
|
||||
errs <- fmt.Errorf("failed to determine image source: %w", 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)
|
||||
errs <- fmt.Errorf("failed to catalog input: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -221,7 +237,7 @@ func packagesExecWorker(userInput string) <-chan error {
|
||||
return errs
|
||||
}
|
||||
|
||||
func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) error {
|
||||
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 {
|
||||
@@ -251,7 +267,7 @@ func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Cat
|
||||
Password: appConfig.Anchore.Password,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create anchore client: %+v", err)
|
||||
return fmt.Errorf("failed to create anchore client: %w", err)
|
||||
}
|
||||
|
||||
importCfg := anchore.ImportConfig{
|
||||
@@ -262,6 +278,7 @@ func runPackageSbomUpload(src source.Source, s source.Metadata, catalog *pkg.Cat
|
||||
Dockerfile: dockerfileContents,
|
||||
OverwriteExistingUpload: appConfig.Anchore.OverwriteExistingImage,
|
||||
Scope: scope,
|
||||
Timeout: appConfig.Anchore.ImportTimeout,
|
||||
}
|
||||
|
||||
if err := c.Import(context.Background(), importCfg); err != nil {
|
||||
|
||||
+19
-10
@@ -4,8 +4,8 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/stereoscope"
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/bus"
|
||||
"github.com/anchore/syft/internal/presenter/poweruser"
|
||||
"github.com/anchore/syft/internal/ui"
|
||||
@@ -34,11 +34,19 @@ var powerUserCmd = &cobra.Command{
|
||||
"appName": internal.ApplicationName,
|
||||
"command": "power-user",
|
||||
}),
|
||||
Args: cobra.ExactArgs(1),
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Hidden: true,
|
||||
SilenceUsage: true,
|
||||
SilenceErrors: true,
|
||||
PreRunE: func(cmd *cobra.Command, args []string) error {
|
||||
if len(args) == 0 {
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("an image/directory argument is required")
|
||||
}
|
||||
|
||||
if appConfig.Dev.ProfileCPU && appConfig.Dev.ProfileMem {
|
||||
return fmt.Errorf("cannot profile CPU and memory simultaneously")
|
||||
}
|
||||
@@ -63,9 +71,15 @@ func init() {
|
||||
}
|
||||
|
||||
func powerUserExec(_ *cobra.Command, args []string) error {
|
||||
errs := powerUserExecWorker(args[0])
|
||||
ux := ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet)
|
||||
return ux(errs, eventSubscription)
|
||||
// could be an image or a directory, with or without a scheme
|
||||
userInput := args[0]
|
||||
return eventLoop(
|
||||
powerUserExecWorker(userInput),
|
||||
setupSignals(),
|
||||
eventSubscription,
|
||||
ui.Select(appConfig.CliOptions.Verbosity > 0, appConfig.Quiet),
|
||||
stereoscope.Cleanup,
|
||||
)
|
||||
}
|
||||
|
||||
func powerUserExecWorker(userInput string) <-chan error {
|
||||
@@ -88,11 +102,6 @@ func powerUserExecWorker(userInput string) <-chan error {
|
||||
}
|
||||
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,
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
type powerUserTask func(*poweruser.JSONDocumentConfig, source.Source) error
|
||||
type powerUserTask func(*poweruser.JSONDocumentConfig, *source.Source) error
|
||||
|
||||
func powerUserTasks() ([]powerUserTask, error) {
|
||||
var tasks []powerUserTask
|
||||
@@ -42,7 +42,7 @@ func catalogPackagesTask() (powerUserTask, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
packageCatalog, theDistro, err := syft.CatalogPackages(src, appConfig.Package.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -64,7 +64,7 @@ func catalogFileMetadataTask() (powerUserTask, error) {
|
||||
|
||||
metadataCataloger := file.NewMetadataCataloger()
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -110,7 +110,7 @@ func catalogFileDigestsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileMetadata.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -142,7 +142,7 @@ func catalogSecretsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.Secrets.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -170,7 +170,7 @@ func catalogFileClassificationsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileClassification.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -197,7 +197,7 @@ func catalogContentsTask() (powerUserTask, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
task := func(results *poweruser.JSONDocumentConfig, src source.Source) error {
|
||||
task := func(results *poweruser.JSONDocumentConfig, src *source.Source) error {
|
||||
resolver, err := src.FileResolver(appConfig.FileContents.Cataloger.ScopeOpt)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func setupSignals() <-chan os.Signal {
|
||||
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
|
||||
|
||||
interruptions := []os.Signal{
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
|
||||
signal.Notify(c, interruptions...)
|
||||
|
||||
return c
|
||||
}
|
||||
@@ -7,10 +7,10 @@ require (
|
||||
github.com/adrg/xdg v0.2.1
|
||||
github.com/alecthomas/jsonschema v0.0.0-20210301060011-54c507b6f074
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210602151223-1f0f707a2894
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/stereoscope v0.0.0-20210413221244-d577f30b19e6
|
||||
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a
|
||||
github.com/antihax/optional v1.0.0
|
||||
github.com/bmatcuk/doublestar/v2 v2.0.4
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20200309214505-aa6a9891b09c+incompatible
|
||||
@@ -30,17 +30,22 @@ require (
|
||||
github.com/scylladb/go-set v1.0.2
|
||||
github.com/sergi/go-diff v1.1.0
|
||||
github.com/sirupsen/logrus v1.6.0
|
||||
github.com/spdx/tools-golang v0.1.0
|
||||
github.com/spf13/afero v1.2.2
|
||||
github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905
|
||||
github.com/spf13/pflag v1.0.5
|
||||
github.com/spf13/viper v1.7.0
|
||||
github.com/stretchr/testify v1.6.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20200526224238-eb215533f07d
|
||||
github.com/stretchr/testify v1.7.0
|
||||
github.com/vifraa/gopom v0.1.0
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240
|
||||
github.com/wagoodman/jotframe v0.0.0-20200730190914-3517092dd163
|
||||
github.com/x-cray/logrus-prefixed-formatter v0.5.2
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9
|
||||
golang.org/x/mod v0.3.0
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d
|
||||
gopkg.in/yaml.v2 v2.3.0
|
||||
)
|
||||
|
||||
replace github.com/package-url/packageurl-go v0.1.0 => github.com/anchore/packageurl-go v0.1.0-fixed
|
||||
|
||||
@@ -109,19 +109,20 @@ github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRF
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf h1:DYssiUV1pBmKqzKsm4mqXx8artqC0Q8HgZsVI3lMsAg=
|
||||
github.com/anchore/client-go v0.0.0-20210222170800-9c70f9b80bcf/go.mod h1:FaODhIA06mxO1E6R32JE0TL1JWZZkmjRIAd4ULvHUKk=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6 h1:wEN3HXc3VuC4wo7Cz27YCpeQ4gaB5ASKwMwM5GdFsew=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210415132930-2460011e83c6/go.mod h1:8jNYOxCJC5kyD/Ct4MbzsDN2hOhRoCAzQcb/7KdYYGw=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210602151223-1f0f707a2894 h1:VvCq7fFNU8dwWkCTGUykm4p64nVaDCYkKrj87x350Sk=
|
||||
github.com/anchore/go-rpmdb v0.0.0-20210602151223-1f0f707a2894/go.mod h1:8jNYOxCJC5kyD/Ct4MbzsDN2hOhRoCAzQcb/7KdYYGw=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04 h1:VzprUTpc0vW0nnNKJfJieyH/TZ9UYAnTZs5/gHTdAe8=
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04/go.mod h1:6dK64g27Qi1qGQZ67gFmBFvEHScy0/C8qhQhNe5B5pQ=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZVsCYMrIZBpFxwV26CbsuoEh5muXD5I1Ods=
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/stereoscope v0.0.0-20210413221244-d577f30b19e6 h1:g9ZS2V/T0wxseccI4t1hQTqWBek5DVOQZOzzdWBjwnU=
|
||||
github.com/anchore/stereoscope v0.0.0-20210413221244-d577f30b19e6/go.mod h1:vhh1M99rfWx5ejMvz1lkQiFZUrC5wu32V12R4JXH+ZI=
|
||||
github.com/anchore/packageurl-go v0.1.0-fixed h1:2QJUTALDF7m2WHPOS1NyHguSX4ciG0xD3idaBQwHJS8=
|
||||
github.com/anchore/packageurl-go v0.1.0-fixed/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
|
||||
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a h1:RQb+Gft1MKxjDfJCnHP/f1mwfy0Jz50Kp9QGgSWKQiY=
|
||||
github.com/anchore/stereoscope v0.0.0-20210817160504-0f4abc2a5a5a/go.mod h1:165DfE5jApgEkHTWwu7Bijeml9fofudrgcpuWaD9+tk=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/antihax/optional v1.0.0 h1:xK2lYat7ZLaVVcIuj82J8kIro4V6kDe0AUDFboUCwcg=
|
||||
github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
|
||||
github.com/apex/log v1.1.4/go.mod h1:AlpoD9aScyQfJDVHmLMEcx4oU6LqzkWp4Mg9GdAcEvQ=
|
||||
github.com/apex/log v1.3.0 h1:1fyfbPvUwD10nMoh3hY6MXzvZShJQn9/ck7ATgAt5pA=
|
||||
github.com/apex/log v1.3.0/go.mod h1:jd8Vpsr46WAe3EZSQ/IUMs2qQD/GOycT5rPWCO1yGcs=
|
||||
github.com/apex/logs v0.0.4/go.mod h1:XzxuLZ5myVHDy9SAmYpamKKRNApGj54PfYLcFrXqDwo=
|
||||
github.com/aphistic/golf v0.0.0-20180712155816-02c07f170c5a/go.mod h1:3NqKYiepwy8kCu4PNA+aP7WUV72eXWJeP9/r3/K9aLE=
|
||||
@@ -589,8 +590,6 @@ github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5X
|
||||
github.com/opencontainers/runc v0.1.1 h1:GlxAyO6x8rfZYN9Tt0Kti5a/cP41iuiO2yYT0IJGY8Y=
|
||||
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
|
||||
github.com/opencontainers/runtime-spec v1.0.2/go.mod h1:jwyrGlmzljRJv/Fgzds9SsS/C5hL+LL3ko9hs6T5lQ0=
|
||||
github.com/package-url/packageurl-go v0.1.0 h1:efWBc98O/dBZRg1pw2xiDzovnlMjCa9NPnfaiBduh8I=
|
||||
github.com/package-url/packageurl-go v0.1.0/go.mod h1:C/ApiuWpmbpni4DIOECf6WCjFUZV7O1Fx7VAzrZHgBw=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
@@ -672,6 +671,9 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
|
||||
github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE=
|
||||
github.com/sourcegraph/go-diff v0.5.3/go.mod h1:v9JDtjCE4HHHCZGId75rg8gkKKa98RVjBcBGsVmMmak=
|
||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
|
||||
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
|
||||
github.com/spdx/tools-golang v0.1.0 h1:iDMNEPqQk6CdiDj6eWDIDw85j0wQ3IR3pH9p0X05TSQ=
|
||||
github.com/spdx/tools-golang v0.1.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
@@ -700,6 +702,7 @@ github.com/spf13/viper v1.7.0 h1:xVKxvI7ouOI5I+U9s2eeiUfMaWBVoXA3AWskkrqK0VM=
|
||||
github.com/spf13/viper v1.7.0/go.mod h1:8WkrPz2fc9jxqZNCJI/76HCieCp4Q8HaLFoCha5qpdg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
@@ -707,8 +710,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.3.1-0.20190311161405-34c6fa2dc709/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.6.0 h1:jlIyCplCJFULU/01vCkhKuTyc3OorI3bJFuw6obfgho=
|
||||
github.com/stretchr/testify v1.6.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/subosito/gotenv v1.2.0 h1:Slr1R9HxAlEKefgq5jn9U+DnETlIUa6HfgEzj0g5d7s=
|
||||
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
|
||||
github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
|
||||
@@ -717,7 +722,6 @@ github.com/tetafro/godot v0.3.7/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQx
|
||||
github.com/tetafro/godot v0.4.2/go.mod h1:/7NLHhv08H1+8DNj0MElpAACw1ajsCuf3TKNQxA5S+0=
|
||||
github.com/timakin/bodyclose v0.0.0-20190930140734-f7f2e9bca95e/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/timakin/bodyclose v0.0.0-20200424151742-cb6215831a94/go.mod h1:Qimiffbc6q9tBWlVV6x0P9sat/ao1xEkREYPPj9hphk=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0 h1:Rw8kxzWo1mr6FSaYXjQELRe88y2KdfynXdnK72rdjtA=
|
||||
github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0=
|
||||
github.com/tj/go-elastic v0.0.0-20171221160941-36157cbbebc2/go.mod h1:WjeM0Oo1eNAjXGDx2yma7uG2XoyRZTq1uv3M/o7imD0=
|
||||
github.com/tj/go-kinesis v0.0.0-20171128231115-08b17f58cb1b/go.mod h1:/yhzCV0xPfx6jb1bBgRFjl5lytqVqZXEaeqWP8lTEao=
|
||||
@@ -740,9 +744,12 @@ github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk
|
||||
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/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c=
|
||||
github.com/vifraa/gopom v0.1.0 h1:v897eVxf6lflkEXzPmKbo4YhX2oS/LGjz7cqjWnSmCU=
|
||||
github.com/vifraa/gopom v0.1.0/go.mod h1:oPa1dcrGrtlO37WPDBm5SqHAT+wTgF8An1Q71Z6Vv4o=
|
||||
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/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5 h1:phTLPgMRDYTizrBSKsNSOa2zthoC2KsJsaY/8sg3rD8=
|
||||
github.com/wagoodman/go-partybus v0.0.0-20210627031916-db1f5573bbc5/go.mod h1:JPirS5jde/CF5qIjcK4WX+eQmKXdPc6vcZkJ/P0hfPw=
|
||||
github.com/wagoodman/go-progress v0.0.0-20200621122631-1a2120f0695a/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240 h1:r6BlIP7CVZtMlxUQhT40h1IE1TzEgKVqwmsVGuscvdk=
|
||||
github.com/wagoodman/go-progress v0.0.0-20200731105512-1020f39e6240/go.mod h1:jLXFoL31zFaHKAAyZUh+sxiTDFe1L1ZHrcK2T1itVKA=
|
||||
@@ -865,8 +872,9 @@ golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/
|
||||
golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9 h1:pNX+40auqi2JqRfOP1akLGtYcn15TUbkhwuCO3foqqM=
|
||||
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d h1:LO7XpTYMwTqxjLcGWPijK3vRXg1aWdlNOVOHRq45d7c=
|
||||
golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -931,14 +939,18 @@ golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980 h1:OjiUf46hAmXblsZdnoSXsEUSKU8r1UEzcL5RVZ4gO9Y=
|
||||
golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da h1:b3NXsE2LusjYGGjL5bxEVZZORm/YEFFrWFjR8eFrw/c=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
|
||||
@@ -27,6 +27,7 @@ type ImportConfig struct {
|
||||
Dockerfile []byte
|
||||
OverwriteExistingUpload bool
|
||||
Scope source.Scope
|
||||
Timeout uint
|
||||
}
|
||||
|
||||
func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||
@@ -54,7 +55,8 @@ func importProgress(source string) (*progress.Stage, *progress.Manual) {
|
||||
func (c *Client) Import(ctx context.Context, cfg ImportConfig) error {
|
||||
stage, prog := importProgress(c.config.BaseURL)
|
||||
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, time.Second*30)
|
||||
timeout := time.Duration(cfg.Timeout) * time.Second
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout)
|
||||
defer cancel()
|
||||
|
||||
authedCtx := c.newRequestContext(ctxWithTimeout)
|
||||
|
||||
@@ -12,6 +12,8 @@ type anchore struct {
|
||||
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
|
||||
ImportTimeout uint `yaml:"import-timeout" json:"import-timeout" mapstructure:"import-timeout"` // --import-timeout
|
||||
// , customize the number of seconds within which the SBOM import must be completed or canceled
|
||||
}
|
||||
|
||||
func (cfg anchore) loadDefaultValues(v *viper.Viper) {
|
||||
|
||||
@@ -20,11 +20,13 @@ type RegistryCredentials struct {
|
||||
|
||||
type registry struct {
|
||||
InsecureSkipTLSVerify bool `yaml:"insecure-skip-tls-verify" json:"insecure-skip-tls-verify" mapstructure:"insecure-skip-tls-verify"`
|
||||
InsecureUseHTTP bool `yaml:"insecure-use-http" json:"insecure-use-http" mapstructure:"insecure-use-http"`
|
||||
Auth []RegistryCredentials `yaml:"auth" json:"auth" mapstructure:"auth"`
|
||||
}
|
||||
|
||||
func (cfg registry) loadDefaultValues(v *viper.Viper) {
|
||||
v.SetDefault("registry.insecure-skip-tls-verify", false)
|
||||
v.SetDefault("registry.insecure-use-http", false)
|
||||
v.SetDefault("registry.auth", []RegistryCredentials{})
|
||||
}
|
||||
|
||||
@@ -37,7 +39,7 @@ func (cfg *registry) parseConfigValues() error {
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_PASSWORD"),
|
||||
os.Getenv("SYFT_REGISTRY_AUTH_TOKEN")
|
||||
|
||||
if hasNonEmptyCredentials(authority, username, password, token) {
|
||||
if hasNonEmptyCredentials(username, password, token) {
|
||||
// note: we prepend the credentials such that the environment variables take precedence over on-disk configuration.
|
||||
cfg.Auth = append([]RegistryCredentials{
|
||||
{
|
||||
@@ -51,8 +53,8 @@ func (cfg *registry) parseConfigValues() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasNonEmptyCredentials(authority, username, password, token string) bool {
|
||||
return authority != "" && password != "" && username != "" || authority != "" && token != ""
|
||||
func hasNonEmptyCredentials(username, password, token string) bool {
|
||||
return password != "" && username != "" || token != ""
|
||||
}
|
||||
|
||||
func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
@@ -67,6 +69,7 @@ func (cfg *registry) ToOptions() *image.RegistryOptions {
|
||||
}
|
||||
return &image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: cfg.InsecureSkipTLSVerify,
|
||||
InsecureUseHTTP: cfg.InsecureUseHTTP,
|
||||
Credentials: auth,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/anchore/stereoscope/pkg/image"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -9,51 +10,102 @@ import (
|
||||
|
||||
func TestHasNonEmptyCredentials(t *testing.T) {
|
||||
tests := []struct {
|
||||
auth, username, password, token string
|
||||
expected bool
|
||||
username, password, token string
|
||||
expected bool
|
||||
}{
|
||||
|
||||
{
|
||||
"", "", "", "",
|
||||
"", "", "",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"auth", "", "", "",
|
||||
"user", "", "",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"auth", "user", "", "",
|
||||
"", "pass", "",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"auth", "", "pass", "",
|
||||
false,
|
||||
},
|
||||
{
|
||||
"auth", "", "pass", "tok",
|
||||
"", "pass", "tok",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"auth", "user", "", "tok",
|
||||
"user", "", "tok",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"auth", "", "", "tok",
|
||||
"", "", "tok",
|
||||
true,
|
||||
},
|
||||
{
|
||||
"auth", "user", "pass", "tok",
|
||||
"user", "pass", "tok",
|
||||
true,
|
||||
},
|
||||
|
||||
{
|
||||
"auth", "user", "pass", "",
|
||||
"user", "pass", "",
|
||||
true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(fmt.Sprintf("%+v", test), func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.auth, test.username, test.password, test.token))
|
||||
assert.Equal(t, test.expected, hasNonEmptyCredentials(test.username, test.password, test.token))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_registry_ToOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input registry
|
||||
expected image.RegistryOptions
|
||||
}{
|
||||
{
|
||||
name: "no registry options",
|
||||
input: registry{},
|
||||
expected: image.RegistryOptions{
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureSkipTLSVerify",
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set InsecureUseHTTP",
|
||||
input: registry{
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureUseHTTP: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "set all bool options",
|
||||
input: registry{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
},
|
||||
expected: image.RegistryOptions{
|
||||
InsecureSkipTLSVerify: true,
|
||||
InsecureUseHTTP: true,
|
||||
Credentials: []image.RegistryCredentials{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, &test.expected, test.input.ToOptions())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
@@ -12,3 +14,25 @@ func CloseAndLogError(closer io.Closer, location string) {
|
||||
log.Warnf("unable to close file for location=%q: %+v", location, err)
|
||||
}
|
||||
}
|
||||
|
||||
type ErrPath struct {
|
||||
Path string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e ErrPath) Error() string {
|
||||
return fmt.Sprintf("unable to observe contents of %+v: %v", e.Path, e.Err)
|
||||
}
|
||||
|
||||
func IsErrPath(err error) bool {
|
||||
_, ok := err.(ErrPath)
|
||||
return ok
|
||||
}
|
||||
|
||||
func IsErrPathPermission(err error) bool {
|
||||
pathErr, ok := err.(ErrPath)
|
||||
if ok {
|
||||
return os.IsPermission(pathErr.Err)
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -9,6 +8,8 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var expectedZipArchiveEntries = []string{
|
||||
@@ -18,35 +19,21 @@ var expectedZipArchiveEntries = []string{
|
||||
"nested.zip",
|
||||
}
|
||||
|
||||
// fatalIfError calls the supplied function. If the function returns a non-nil error, t.Fatal(err) is called.
|
||||
func fatalIfError(t *testing.T, fn func() error) {
|
||||
t.Helper()
|
||||
|
||||
if fn == nil {
|
||||
return
|
||||
}
|
||||
|
||||
err := fn()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// createZipArchive creates a new ZIP archive file at destinationArchivePath based on the directory found at
|
||||
// sourceDirPath.
|
||||
func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string) error {
|
||||
func createZipArchive(t testing.TB, sourceDirPath, destinationArchivePath string) {
|
||||
t.Helper()
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to get cwd: %+v", err)
|
||||
t.Fatalf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command("./generate-zip-fixture.sh", destinationArchivePath, path.Base(sourceDirPath))
|
||||
cmd := exec.Command("./generate-zip-fixture-from-source-dir.sh", destinationArchivePath, path.Base(sourceDirPath))
|
||||
cmd.Dir = filepath.Join(cwd, "test-fixtures")
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
return fmt.Errorf("unable to start generate zip fixture script: %+v", err)
|
||||
t.Fatalf("unable to start generate zip fixture script: %+v", err)
|
||||
}
|
||||
|
||||
if err := cmd.Wait(); err != nil {
|
||||
@@ -59,61 +46,62 @@ func createZipArchive(t *testing.T, sourceDirPath, destinationArchivePath string
|
||||
// an ExitStatus() method with the same signature.
|
||||
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
|
||||
if status.ExitStatus() != 0 {
|
||||
return fmt.Errorf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
t.Fatalf("failed to generate fixture: rc=%d", status.ExitStatus())
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("unable to get generate fixture script result: %+v", err)
|
||||
t.Fatalf("unable to get generate fixture script result: %+v", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func assertNoError(t testing.TB, fn func() error) func() {
|
||||
return func() {
|
||||
assert.NoError(t, fn())
|
||||
}
|
||||
}
|
||||
|
||||
// setupZipFileTest encapsulates common test setup work for zip file tests. It returns a cleanup function,
|
||||
// which should be called (typically deferred) by the caller, the path of the created zip archive, and an error,
|
||||
// which should trigger a fatal test failure in the consuming test. The returned cleanup function will never be nil
|
||||
// (even if there's an error), and it should always be called.
|
||||
func setupZipFileTest(t *testing.T, sourceDirPath string) (func() error, string, error) {
|
||||
func setupZipFileTest(t testing.TB, sourceDirPath string) string {
|
||||
t.Helper()
|
||||
|
||||
// Keep track of any needed cleanup work as we go
|
||||
var cleanupFns []func() error
|
||||
cleanup := func(fns []func() error) func() error {
|
||||
return func() error {
|
||||
for _, fn := range fns {
|
||||
err := fn()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to create tempfile: %+v", err)
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(archivePrefix.Name()) })
|
||||
|
||||
t.Cleanup(
|
||||
assertNoError(t,
|
||||
func() error {
|
||||
return os.Remove(archivePrefix.Name())
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
destinationArchiveFilePath := archivePrefix.Name() + ".zip"
|
||||
t.Logf("archive path: %s", destinationArchiveFilePath)
|
||||
err = createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
|
||||
cleanupFns = append(cleanupFns, func() error { return os.Remove(destinationArchiveFilePath) })
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", err
|
||||
}
|
||||
createZipArchive(t, sourceDirPath, destinationArchiveFilePath)
|
||||
|
||||
t.Cleanup(
|
||||
assertNoError(t,
|
||||
func() error {
|
||||
return os.Remove(destinationArchiveFilePath)
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return cleanup(cleanupFns), "", fmt.Errorf("unable to get cwd: %+v", err)
|
||||
t.Fatalf("unable to get cwd: %+v", err)
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
return cleanup(cleanupFns), destinationArchiveFilePath, nil
|
||||
return destinationArchiveFilePath
|
||||
}
|
||||
|
||||
// TODO: Consider moving any non-git asset generation to a task (e.g. make) that's run ahead of running go tests.
|
||||
@@ -121,11 +109,7 @@ func ensureNestedZipExists(t *testing.T, sourceDirPath string) error {
|
||||
t.Helper()
|
||||
|
||||
nestedArchiveFilePath := path.Join(sourceDirPath, "nested.zip")
|
||||
err := createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create nested archive for test fixture: %+v", err)
|
||||
}
|
||||
createZipArchive(t, sourceDirPath, nestedArchiveFilePath)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,23 +1,36 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
)
|
||||
|
||||
// ZipFileManifest is a collection of paths and their file metadata.
|
||||
type ZipFileManifest map[string]os.FileInfo
|
||||
|
||||
// newZipManifest creates an empty ZipFileManifest.
|
||||
func newZipManifest() ZipFileManifest {
|
||||
return make(ZipFileManifest)
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
manifest := make(ZipFileManifest)
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Warnf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// Add a new path and it's file metadata to the collection.
|
||||
@@ -47,26 +60,6 @@ func (z ZipFileManifest) GlobMatch(patterns ...string) []string {
|
||||
return results
|
||||
}
|
||||
|
||||
// NewZipFileManifest creates and returns a new ZipFileManifest populated with path and metadata from the given zip archive path.
|
||||
func NewZipFileManifest(archivePath string) (ZipFileManifest, error) {
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
manifest := newZipManifest()
|
||||
if err != nil {
|
||||
return manifest, fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
defer func() {
|
||||
err = zipReader.Close()
|
||||
if err != nil {
|
||||
log.Errorf("unable to close zip archive (%s): %+v", archivePath, err)
|
||||
}
|
||||
}()
|
||||
|
||||
for _, file := range zipReader.Reader.File {
|
||||
manifest.Add(file.Name, file.FileInfo())
|
||||
}
|
||||
return manifest, nil
|
||||
}
|
||||
|
||||
// normalizeZipEntryName takes the given path entry and ensures it is prefixed with "/".
|
||||
func normalizeZipEntryName(entry string) string {
|
||||
if !strings.HasPrefix(entry, "/") {
|
||||
|
||||
@@ -19,11 +19,7 @@ func TestNewZipFileManifest(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
actual, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
@@ -63,12 +59,7 @@ func TestZipFileManifest_GlobMatch(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
//goland:noinspection GoNilness
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
z, err := NewZipFileManifest(archiveFilePath)
|
||||
if err != nil {
|
||||
|
||||
@@ -47,7 +47,7 @@ func newZipTraverseRequest(paths ...string) zipTraversalRequest {
|
||||
func TraverseFilesInZip(archivePath string, visitor func(*zip.File) error, paths ...string) error {
|
||||
request := newZipTraverseRequest(paths...)
|
||||
|
||||
zipReader, err := zip.OpenReader(archivePath)
|
||||
zipReader, err := OpenZip(archivePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to open zip archive (%s): %w", archivePath, err)
|
||||
}
|
||||
|
||||
@@ -44,14 +44,12 @@ func TestUnzipToDir(t *testing.T) {
|
||||
|
||||
goldenRootDir := filepath.Join(cwd, "test-fixtures")
|
||||
sourceDirPath := path.Join(goldenRootDir, "zip-source")
|
||||
cleanup, archiveFilePath, err := setupZipFileTest(t, sourceDirPath)
|
||||
defer fatalIfError(t, cleanup)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
archiveFilePath := setupZipFileTest(t, sourceDirPath)
|
||||
|
||||
unzipDestinationDir, err := ioutil.TempDir("", "syft-ziputil-contents-TEST-")
|
||||
defer os.RemoveAll(unzipDestinationDir)
|
||||
t.Cleanup(assertNoError(t, func() error {
|
||||
return os.RemoveAll(unzipDestinationDir)
|
||||
}))
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempdir: %+v", err)
|
||||
}
|
||||
@@ -127,41 +125,119 @@ func TestUnzipToDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestContentsFromZip(t *testing.T) {
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-archive-TEST-")
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePrep func(tb testing.TB) string
|
||||
}{
|
||||
{
|
||||
name: "standard, non-nested zip",
|
||||
archivePrep: prepZipSourceFixture,
|
||||
},
|
||||
{
|
||||
name: "zip with prepended bytes",
|
||||
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := test.archivePrep(t)
|
||||
expected := zipSourceFixtureExpectedContents()
|
||||
|
||||
var paths []string
|
||||
for p := range expected {
|
||||
paths = append(paths, p)
|
||||
}
|
||||
|
||||
actual, err := ContentsFromZip(archivePath, paths...)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
}
|
||||
|
||||
assertZipSourceFixtureContents(t, actual, expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func prependZipSourceFixtureWithString(tb testing.TB, value string) func(tb testing.TB) string {
|
||||
if len(value) == 0 {
|
||||
tb.Fatalf("no bytes given to prefix")
|
||||
}
|
||||
return func(t testing.TB) string {
|
||||
archivePath := prepZipSourceFixture(t)
|
||||
|
||||
// create a temp file
|
||||
tmpFile, err := ioutil.TempFile("", "syft-ziputil-prependZipSourceFixtureWithString-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer tmpFile.Close()
|
||||
|
||||
// write value to the temp file
|
||||
if _, err := tmpFile.WriteString(value); err != nil {
|
||||
t.Fatalf("unable to write to tempfile: %+v", err)
|
||||
}
|
||||
|
||||
// open the original archive
|
||||
sourceFile, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read source file: %+v", err)
|
||||
}
|
||||
|
||||
// copy all contents from the archive to the temp file
|
||||
if _, err := io.Copy(tmpFile, sourceFile); err != nil {
|
||||
t.Fatalf("unable to copy source to dest: %+v", err)
|
||||
}
|
||||
|
||||
sourceFile.Close()
|
||||
|
||||
// remove the original archive and replace it with the temp file
|
||||
if err := os.Remove(archivePath); err != nil {
|
||||
t.Fatalf("unable to remove original source archive (%q): %+v", archivePath, err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpFile.Name(), archivePath); err != nil {
|
||||
t.Fatalf("unable to move new archive to old path (%q): %+v", tmpFile.Name(), err)
|
||||
}
|
||||
|
||||
return archivePath
|
||||
}
|
||||
}
|
||||
|
||||
func prepZipSourceFixture(t testing.TB) string {
|
||||
t.Helper()
|
||||
archivePrefix, err := ioutil.TempFile("", "syft-ziputil-prepZipSourceFixture-")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create tempfile: %+v", err)
|
||||
}
|
||||
defer os.Remove(archivePrefix.Name())
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove(archivePrefix.Name()))
|
||||
})
|
||||
|
||||
// the zip utility will add ".zip" to the end of the given name
|
||||
archivePath := archivePrefix.Name() + ".zip"
|
||||
defer os.Remove(archivePath)
|
||||
|
||||
t.Cleanup(func() {
|
||||
assert.NoError(t, os.Remove(archivePath))
|
||||
})
|
||||
|
||||
t.Logf("archive path: %s", archivePath)
|
||||
|
||||
err = createZipArchive(t, "zip-source", archivePrefix.Name())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
createZipArchive(t, "zip-source", archivePrefix.Name())
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
t.Errorf("unable to get cwd: %+v", err)
|
||||
}
|
||||
return archivePath
|
||||
}
|
||||
|
||||
t.Logf("running from: %s", cwd)
|
||||
|
||||
aFilePath := filepath.Join("some-dir", "a-file.txt")
|
||||
bFilePath := filepath.Join("b-file.txt")
|
||||
|
||||
expected := map[string]string{
|
||||
aFilePath: "A file! nice!",
|
||||
bFilePath: "B file...",
|
||||
}
|
||||
|
||||
actual, err := ContentsFromZip(archivePath, aFilePath, bFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to extract from unzip archive: %+v", err)
|
||||
func zipSourceFixtureExpectedContents() map[string]string {
|
||||
return map[string]string{
|
||||
filepath.Join("some-dir", "a-file.txt"): "A file! nice!",
|
||||
filepath.Join("b-file.txt"): "B file...",
|
||||
}
|
||||
}
|
||||
|
||||
func assertZipSourceFixtureContents(t testing.TB, actual map[string]string, expected map[string]string) {
|
||||
t.Helper()
|
||||
diffs := deep.Equal(actual, expected)
|
||||
if len(diffs) > 0 {
|
||||
for _, d := range diffs {
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
// directoryEndLen, readByf, directoryEnd, and findSignatureInBlock were copied from the golang stdlib, specifically:
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/struct.go
|
||||
// - https://github.com/golang/go/blob/go1.16.4/src/archive/zip/reader.go
|
||||
// findArchiveStartOffset is derived from the same stdlib utils, specifically the readDirectoryEnd function.
|
||||
|
||||
const directoryEndLen = 22
|
||||
|
||||
// ZipReadCloser is a drop-in replacement for zip.ReadCloser (from zip.OpenReader) that additionally considers zips
|
||||
// that have bytes prefixed to the front of the archive (common with self-extracting jars).
|
||||
type ZipReadCloser struct {
|
||||
*zip.Reader
|
||||
io.Closer
|
||||
}
|
||||
|
||||
// OpenZip provides a ZipReadCloser for the given filepath.
|
||||
func OpenZip(filepath string) (*ZipReadCloser, error) {
|
||||
f, err := os.Open(filepath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fi, err := f.Stat()
|
||||
if err != nil {
|
||||
f.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// some archives may have bytes prepended to the front of the archive, such as with self executing JARs. We first
|
||||
// need to find the start of the archive and keep track of this offset.
|
||||
offset, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot find beginning of zip archive=%q : %w", filepath, err)
|
||||
}
|
||||
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("unable to seek to beginning of archive: %w", err)
|
||||
}
|
||||
|
||||
size := fi.Size() - int64(offset)
|
||||
|
||||
r, err := zip.NewReader(io.NewSectionReader(f, int64(offset), size), size)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to open ZipReadCloser @ %q: %w", filepath, err)
|
||||
}
|
||||
|
||||
return &ZipReadCloser{
|
||||
Reader: r,
|
||||
Closer: f,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type readBuf []byte
|
||||
|
||||
func (b *readBuf) uint16() uint16 {
|
||||
v := binary.LittleEndian.Uint16(*b)
|
||||
*b = (*b)[2:]
|
||||
return v
|
||||
}
|
||||
|
||||
func (b *readBuf) uint32() uint32 {
|
||||
v := binary.LittleEndian.Uint32(*b)
|
||||
*b = (*b)[4:]
|
||||
return v
|
||||
}
|
||||
|
||||
type directoryEnd struct {
|
||||
diskNbr uint32 // unused
|
||||
dirDiskNbr uint32 // unused
|
||||
dirRecordsThisDisk uint64 // unused
|
||||
directoryRecords uint64
|
||||
directorySize uint64
|
||||
directoryOffset uint64 // relative to file
|
||||
}
|
||||
|
||||
// note: this is derived from readDirectoryEnd within the archive/zip package
|
||||
func findArchiveStartOffset(r io.ReaderAt, size int64) (startOfArchive uint64, err error) {
|
||||
// look for directoryEndSignature in the last 1k, then in the last 65k
|
||||
var buf []byte
|
||||
var directoryEndOffset int64
|
||||
for i, bLen := range []int64{1024, 65 * 1024} {
|
||||
if bLen > size {
|
||||
bLen = size
|
||||
}
|
||||
buf = make([]byte, int(bLen))
|
||||
if _, err := r.ReadAt(buf, size-bLen); err != nil && err != io.EOF {
|
||||
return 0, err
|
||||
}
|
||||
if p := findSignatureInBlock(buf); p >= 0 {
|
||||
buf = buf[p:]
|
||||
directoryEndOffset = size - bLen + int64(p)
|
||||
break
|
||||
}
|
||||
if i == 1 || bLen == size {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
}
|
||||
|
||||
if buf == nil {
|
||||
// we were unable to find the directoryEndSignature block
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
|
||||
// read header into struct
|
||||
b := readBuf(buf[4:]) // skip signature
|
||||
d := &directoryEnd{
|
||||
diskNbr: uint32(b.uint16()),
|
||||
dirDiskNbr: uint32(b.uint16()),
|
||||
dirRecordsThisDisk: uint64(b.uint16()),
|
||||
directoryRecords: uint64(b.uint16()),
|
||||
directorySize: uint64(b.uint32()),
|
||||
directoryOffset: uint64(b.uint32()),
|
||||
}
|
||||
// Calculate where the zip data actually begins
|
||||
startOfArchive = uint64(directoryEndOffset) - d.directorySize - d.directoryOffset
|
||||
|
||||
// These values mean that the file can be a zip64 file
|
||||
if d.directoryRecords == 0xffff || d.directorySize == 0xffff || d.directoryOffset == 0xffffffff {
|
||||
startOfArchive = 0 // Prefixed data not supported
|
||||
}
|
||||
|
||||
// Make sure directoryOffset points to somewhere in our file.
|
||||
if o := int64(d.directoryOffset); o < 0 || o >= size {
|
||||
return 0, zip.ErrFormat
|
||||
}
|
||||
return startOfArchive, nil
|
||||
}
|
||||
|
||||
func findSignatureInBlock(b []byte) int {
|
||||
for i := len(b) - directoryEndLen; i >= 0; i-- {
|
||||
// defined from directoryEndSignature
|
||||
if b[i] == 'P' && b[i+1] == 'K' && b[i+2] == 0x05 && b[i+3] == 0x06 {
|
||||
// n is length of comment
|
||||
n := int(b[i+directoryEndLen-2]) | int(b[i+directoryEndLen-1])<<8
|
||||
if n+directoryEndLen+i <= len(b) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestFindArchiveStartOffset(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
archivePrep func(tb testing.TB) string
|
||||
expected uint64
|
||||
}{
|
||||
{
|
||||
name: "standard, non-nested zip",
|
||||
archivePrep: prepZipSourceFixture,
|
||||
expected: 0,
|
||||
},
|
||||
{
|
||||
name: "zip with prepended bytes",
|
||||
archivePrep: prependZipSourceFixtureWithString(t, "junk at the beginning of the file..."),
|
||||
expected: 36,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
archivePath := test.archivePrep(t)
|
||||
f, err := os.Open(archivePath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not open archive %q: %+v", archivePath, err)
|
||||
}
|
||||
fi, err := os.Stat(f.Name())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to stat archive: %+v", err)
|
||||
}
|
||||
|
||||
actual, err := findArchiveStartOffset(f, fi.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("unable to find offset: %+v", err)
|
||||
}
|
||||
assert.Equal(t, test.expected, actual)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,169 +1,34 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
|
||||
|
||||
func TestCycloneDxDirsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package1",
|
||||
Version: "1.0.1",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"Apache-v2",
|
||||
},
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package2",
|
||||
Version: "1.0.2",
|
||||
Architecture: "amd64",
|
||||
},
|
||||
})
|
||||
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *update {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which are tested independently
|
||||
actual = redact(actual)
|
||||
expected = redact(expected)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
||||
|
||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewCycloneDxPresenter(catalog, metadata),
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestCycloneDxImgsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.RpmPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
PURL: "the-purl-1",
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.RpmPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"Apache-v2",
|
||||
},
|
||||
PURL: "the-purl-2",
|
||||
})
|
||||
|
||||
s, err := source.NewFromImage(img, "user-image-input")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// This accounts for the non-deterministic digest value that we end up with when
|
||||
// we build a container image dynamically during testing. Ultimately, we should
|
||||
// use a golden image as a test fixture in place of building this image during
|
||||
// testing. At that time, this line will no longer be necessary.
|
||||
//
|
||||
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
|
||||
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
pres := NewCycloneDxPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *update {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which are tested independently
|
||||
actual = redact(actual)
|
||||
expected = redact(expected)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewCycloneDxPresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateCycloneDx,
|
||||
cycloneDxRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func redact(s []byte) []byte {
|
||||
func cycloneDxRedactor(s []byte) []byte {
|
||||
serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`)
|
||||
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
|
||||
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
||||
@@ -24,160 +17,21 @@ func must(c pkg.CPE, e error) pkg.CPE {
|
||||
return c
|
||||
}
|
||||
|
||||
func TestJSONDirsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
var d *distro.Distro
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateJSONGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
func TestJSONDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, dist := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
|
||||
*updateJSONGoldenFiles,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
func TestJSONImgsPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
func TestJSONImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
|
||||
if *updateJSONGoldenFiles {
|
||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||
}
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-1",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
|
||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
s, err := source.NewFromImage(img, "user-image-input")
|
||||
var d *distro.Distro
|
||||
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateJSONGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
catalog, metadata, dist := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
|
||||
testImage,
|
||||
*updateJSONGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package spdx22
|
||||
|
||||
import "time"
|
||||
|
||||
type AnnotationType string
|
||||
|
||||
const (
|
||||
ReviewerAnnotationType AnnotationType = "REVIEWER"
|
||||
OtherAnnotationType AnnotationType = "OTHER"
|
||||
)
|
||||
|
||||
type Annotation struct {
|
||||
// Identify when the comment was made. This is to be specified according to the combined date and time in the
|
||||
// UTC format, as specified in the ISO 8601 standard.
|
||||
AnnotationDate time.Time `json:"annotationDate"`
|
||||
// Type of the annotation
|
||||
AnnotationType AnnotationType `json:"annotationType"`
|
||||
// This field identifies the person, organization or tool that has commented on a file, package, or the entire document.
|
||||
Annotator string `json:"annotator"`
|
||||
Comment string `json:"comment"`
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package spdx22
|
||||
|
||||
type Checksum struct {
|
||||
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
|
||||
Algorithm string `json:"algorithm"`
|
||||
ChecksumValue string `json:"checksumValue"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package spdx22
|
||||
|
||||
import "time"
|
||||
|
||||
type CreationInfo struct {
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Identify when the SPDX file was originally created. The date is to be specified according to combined date and
|
||||
// time in UTC format as specified in ISO 8601 standard. This field is distinct from the fields in section 8,
|
||||
// which involves the addition of information during a subsequent review.
|
||||
Created time.Time `json:"created"`
|
||||
// Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an
|
||||
// individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization,
|
||||
//indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
|
||||
// for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person
|
||||
// name or organization name may be designated as “anonymous” if appropriate.
|
||||
Creators []string `json:"creators"`
|
||||
// An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.
|
||||
LicenseListVersion string `json:"licenseListVersion"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package spdx22
|
||||
|
||||
// derived from:
|
||||
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
|
||||
// - https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json
|
||||
// - https://github.com/spdx/spdx-spec/tree/v2.2/ontology
|
||||
|
||||
type Document struct {
|
||||
Element
|
||||
SPDXVersion string `json:"spdxVersion"`
|
||||
// One instance is required for each SPDX file produced. It provides the necessary information for forward
|
||||
// and backward compatibility for processing tools.
|
||||
CreationInfo CreationInfo `json:"creationInfo"`
|
||||
// 2.2: Data License; should be "CC0-1.0"
|
||||
// Cardinality: mandatory, one
|
||||
// License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX
|
||||
// fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous
|
||||
// fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without
|
||||
// opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text
|
||||
// is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any
|
||||
// portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any
|
||||
// SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative
|
||||
// Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree
|
||||
// and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or
|
||||
// warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including
|
||||
// without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement,
|
||||
// or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not
|
||||
// discoverable, all to the greatest extent permissible under applicable law.
|
||||
DataLicense string `json:"dataLicense"`
|
||||
// Information about an external SPDX document reference including the checksum. This allows for verification of the external references.
|
||||
ExternalDocumentRefs []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
|
||||
// Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.
|
||||
HasExtractedLicensingInfos []HasExtractedLicensingInfo `json:"hasExtractedLicensingInfos,omitempty"`
|
||||
// note: found in example documents from SPDX, but not in the JSON schema. See https://spdx.github.io/spdx-spec/2-document-creation-information/#25-spdx-document-namespace
|
||||
DocumentNamespace string `json:"documentNamespace"`
|
||||
// note: found in example documents from SPDX, but not in the JSON schema
|
||||
// DocumentDescribes []string `json:"documentDescribes"`
|
||||
Packages []Package `json:"packages"`
|
||||
// Files referenced in the SPDX document
|
||||
Files []File `json:"files,omitempty"`
|
||||
// Snippets referenced in the SPDX document
|
||||
Snippets []Snippet `json:"snippets,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package spdx22
|
||||
|
||||
type Element struct {
|
||||
SPDXID string `json:"SPDXID"`
|
||||
// Identify name of this SpdxElement.
|
||||
Name string `json:"name"`
|
||||
// Relationships referenced in the SPDX document
|
||||
Relationships []Relationship `json:"relationships,omitempty"`
|
||||
// Provide additional information about an SpdxElement.
|
||||
Annotations []Annotation `json:"annotations,omitempty"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package spdx22
|
||||
|
||||
// ElementID represents the identifier string portion of an SPDX element
|
||||
// identifier. DocElementID should be used for any attributes which can
|
||||
// contain identifiers defined in a different SPDX document.
|
||||
// ElementIDs should NOT contain the mandatory 'SPDXRef-' portion.
|
||||
type ElementID string
|
||||
|
||||
func (e ElementID) String() string {
|
||||
return "SPDXRef-" + string(e)
|
||||
}
|
||||
|
||||
// DocElementID represents an SPDX element identifier that could be defined
|
||||
// in a different SPDX document, and therefore could have a "DocumentRef-"
|
||||
// portion, such as Relationship and Annotations.
|
||||
// ElementID is used for attributes in which a "DocumentRef-" portion cannot
|
||||
// appear, such as a Package or File definition (since it is necessarily
|
||||
// being defined in the present document).
|
||||
// DocumentRefID will be the empty string for elements defined in the
|
||||
// present document.
|
||||
// DocElementIDs should NOT contain the mandatory 'DocumentRef-' or
|
||||
// 'SPDXRef-' portions.
|
||||
type DocElementID struct {
|
||||
DocumentRefID string
|
||||
ElementRefID ElementID
|
||||
}
|
||||
|
||||
// RenderDocElementID takes a DocElementID and returns the string equivalent,
|
||||
// with the SPDXRef- prefix (and, if applicable, the DocumentRef- prefix)
|
||||
// reinserted.
|
||||
func (d DocElementID) String() string {
|
||||
prefix := ""
|
||||
if d.DocumentRefID != "" {
|
||||
prefix = "DocumentRef-" + d.DocumentRefID + ":"
|
||||
}
|
||||
return prefix + d.ElementRefID.String()
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package spdx22
|
||||
|
||||
type ExternalDocumentRef struct {
|
||||
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
|
||||
ExternalDocumentID string `json:"externalDocumentId"`
|
||||
Checksum Checksum `json:"checksum"`
|
||||
// SPDX ID for SpdxDocument. A propoerty containing an SPDX document.
|
||||
SpdxDocument string `json:"spdxDocument"`
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package spdx22
|
||||
|
||||
type ReferenceCategory string
|
||||
|
||||
const (
|
||||
SecurityReferenceCategory ReferenceCategory = "SECURITY"
|
||||
PackageManagerReferenceCategory ReferenceCategory = "PACKAGE_MANAGER"
|
||||
OtherReferenceCategory ReferenceCategory = "OTHER"
|
||||
)
|
||||
|
||||
// source: https://spdx.github.io/spdx-spec/appendix-VI-external-repository-identifiers/
|
||||
|
||||
type ExternalRefType string
|
||||
|
||||
const (
|
||||
// see https://nvd.nist.gov/cpe
|
||||
Cpe22ExternalRefType ExternalRefType = "cpe22Type"
|
||||
// see https://nvd.nist.gov/cpe
|
||||
Cpe23ExternalRefType ExternalRefType = "cpe23Type"
|
||||
// see http://repo1.maven.org/maven2/
|
||||
MavenCentralExternalRefType ExternalRefType = "maven-central"
|
||||
// see https://www.npmjs.com/
|
||||
NpmExternalRefType ExternalRefType = "npm"
|
||||
// see https://www.nuget.org/
|
||||
NugetExternalRefType ExternalRefType = "nuget"
|
||||
// see http://bower.io/
|
||||
BowerExternalRefType ExternalRefType = "bower"
|
||||
// see https://github.com/package-url/purl-spec
|
||||
PurlExternalRefType ExternalRefType = "purl"
|
||||
// These point to objects present in the Software Heritage archive by the means of SoftWare Heritage persistent Identifiers (SWHID)
|
||||
SwhExternalRefType ExternalRefType = "swh"
|
||||
)
|
||||
|
||||
type ExternalRef struct {
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Category for the external reference.
|
||||
ReferenceCategory ReferenceCategory `json:"referenceCategory"`
|
||||
// The unique string with no spaces necessary to access the package-specific information, metadata, or content
|
||||
// within the target location. The format of the locator is subject to constraints defined by the <type>.
|
||||
ReferenceLocator string `json:"referenceLocator"`
|
||||
// Type of the external reference. These are defined in an appendix in the SPDX specification.
|
||||
ReferenceType ExternalRefType `json:"referenceType"`
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package spdx22
|
||||
|
||||
type FileType string
|
||||
|
||||
const (
|
||||
DocumentationFileType FileType = "DOCUMENTATION"
|
||||
ImageFileType FileType = "IMAGE"
|
||||
VideoFileType FileType = "VIDEO"
|
||||
ArchiveFileType FileType = "ARCHIVE"
|
||||
SpdxFileType FileType = "SPDX"
|
||||
ApplicationFileType FileType = "APPLICATION"
|
||||
SourceFileType FileType = "SOURCE"
|
||||
BinaryFileType FileType = "BINARY"
|
||||
TextFileType FileType = "TEXT"
|
||||
AudioFileType FileType = "AUDIO"
|
||||
OtherFileType FileType = "OTHER"
|
||||
)
|
||||
|
||||
type File struct {
|
||||
Item
|
||||
// (At least one is required.) The checksum property provides a mechanism that can be used to verify that the
|
||||
// contents of a File or Package have not changed.
|
||||
Checksums []Checksum `json:"checksums"`
|
||||
// This field provides a place for the SPDX file creator to record file contributors. Contributors could include
|
||||
// names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.
|
||||
FileContributors []string `json:"fileContributors"`
|
||||
// Each element is a SPDX ID for a File.
|
||||
FileDependencies []string `json:"fileDependencies"`
|
||||
// The name of the file relative to the root of the package.
|
||||
FileName string `json:"fileName"`
|
||||
// The type of the file
|
||||
FileTypes []string `json:"fileTypes"`
|
||||
// This field provides a place for the SPDX file creator to record potential legal notices found in the file.
|
||||
// This may or may not include copyright statements.
|
||||
NoticeText string `json:"noticeText,omitempty"`
|
||||
// Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name
|
||||
// properties and the URI (if one is known) of doap:Project resources that are values of this property. All other
|
||||
// properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or
|
||||
// from some SPDX formats.
|
||||
ArtifactOf []string `json:"artifactOf"`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package spdx22
|
||||
|
||||
type HasExtractedLicensingInfo struct {
|
||||
// Verbatim license or licensing notice text that was discovered.
|
||||
ExtractedText string `json:"extractedText"`
|
||||
// A human readable short form license identifier for a license. The license ID is iether on the standard license
|
||||
// oist or the form \"LicenseRef-\"[idString] where [idString] is a unique string containing letters,
|
||||
// numbers, \".\", \"-\" or \"+\".
|
||||
LicenseID string `json:"licenseId"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
// Identify name of this SpdxElement.
|
||||
Name string `json:"name,omitempty"`
|
||||
SeeAlsos []string `json:"seeAlsos,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package spdx22
|
||||
|
||||
type Item struct {
|
||||
Element
|
||||
// The licenseComments property allows the preparer of the SPDX document to describe why the licensing in
|
||||
// spdx:licenseConcluded was chosen.
|
||||
LicenseComments string `json:"licenseComments,omitempty"`
|
||||
LicenseConcluded string `json:"licenseConcluded"`
|
||||
// The licensing information that was discovered directly within the package. There will be an instance of this
|
||||
// property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.
|
||||
LicenseInfoFromFiles []string `json:"licenseInfoFromFiles,omitempty"`
|
||||
// Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.
|
||||
LicenseInfoInFiles []string `json:"licenseInfoInFiles,omitempty"`
|
||||
// The text of copyright declarations recited in the Package or File.
|
||||
CopyrightText string `json:"copyrightText,omitempty"`
|
||||
// This field provides a place for the SPDX data creator to record acknowledgements that may be required to be
|
||||
// communicated in some contexts. This is not meant to include the actual complete license text (see
|
||||
// licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText).
|
||||
// The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from
|
||||
// license texts, which may be necessary or desirable to reproduce.
|
||||
AttributionTexts []string `json:"attributionTexts,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package spdx22
|
||||
|
||||
type Package struct {
|
||||
Item
|
||||
// The checksum property provides a mechanism that can be used to verify that the contents of a File or
|
||||
// Package have not changed.
|
||||
Checksums []Checksum `json:"checksums,omitempty"`
|
||||
// Provides a detailed description of the package.
|
||||
Description string `json:"description,omitempty"`
|
||||
// The URI at which this package is available for download. Private (i.e., not publicly reachable) URIs are
|
||||
// acceptable as values of this property. The values http://spdx.org/rdf/terms#none and http://spdx.org/rdf/terms#noassertion
|
||||
// may be used to specify that the package is not downloadable or that no attempt was made to determine its
|
||||
// download location, respectively.
|
||||
DownloadLocation string `json:"downloadLocation,omitempty"`
|
||||
// An External Reference allows a Package to reference an external source of additional information, metadata,
|
||||
// enumerations, asset identifiers, or downloadable content believed to be relevant to the Package.
|
||||
ExternalRefs []ExternalRef `json:"externalRefs,omitempty"`
|
||||
// Indicates whether the file content of this package has been available for or subjected to analysis when
|
||||
// creating the SPDX document. If false indicates packages that represent metadata or URI references to a
|
||||
// project, product, artifact, distribution or a component. If set to false, the package must not contain any files
|
||||
FilesAnalyzed bool `json:"filesAnalyzed"`
|
||||
// Indicates that a particular file belongs to a package (elements are SPDX ID for a File).
|
||||
HasFiles []string `json:"hasFiles,omitempty"`
|
||||
// Provide a place for the SPDX file creator to record a web site that serves as the package's home page.
|
||||
// This link can also be used to reference further information about the package referenced by the SPDX file creator.
|
||||
Homepage string `json:"homepage,omitempty"`
|
||||
// List the licenses that have been declared by the authors of the package. Any license information that does not
|
||||
// originate from the package authors, e.g. license information from a third party repository, should not be included in this field.
|
||||
LicenseDeclared string `json:"licenseDeclared"`
|
||||
// The name and, optionally, contact information of the person or organization that originally created the package.
|
||||
// Values of this property must conform to the agent and tool syntax.
|
||||
Originator string `json:"originator,omitempty"`
|
||||
// The base name of the package file name. For example, zlib-1.2.5.tar.gz.
|
||||
PackageFileName string `json:"packageFileName,omitempty"`
|
||||
// A manifest based verification code (the algorithm is defined in section 4.7 of the full specification) of the
|
||||
// SPDX Item. This allows consumers of this data and/or database to determine if an SPDX item they have in hand
|
||||
// is identical to the SPDX item from which the data was produced. This algorithm works even if the SPDX document
|
||||
// is included in the SPDX item.
|
||||
PackageVerificationCode *PackageVerificationCode `json:"packageVerificationCode,omitempty"`
|
||||
// Allows the producer(s) of the SPDX document to describe how the package was acquired and/or changed from the original source.
|
||||
SourceInfo string `json:"sourceInfo,omitempty"`
|
||||
// Provides a short description of the package.
|
||||
Summary string `json:"summary,omitempty"`
|
||||
// The name and, optionally, contact information of the person or organization who was the immediate supplier
|
||||
// of this package to the recipient. The supplier may be different than originator when the software has been
|
||||
// repackaged. Values of this property must conform to the agent and tool syntax.
|
||||
Supplier string `json:"supplier,omitempty"`
|
||||
// Provides an indication of the version of the package that is described by this SpdxDocument.
|
||||
VersionInfo string `json:"versionInfo,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package spdx22
|
||||
|
||||
// Why are there two package identifier fields Package Checksum and Package Verification?
|
||||
// Although the values of the two fields Package Checksum and Package Verification are similar, they each serve a
|
||||
// different purpose. The Package Checksum provides a unique identifier of a software package which is computed by
|
||||
// taking the SHA1 of the entire software package file. This enables one to quickly determine if two different copies
|
||||
// of a package are the same. One disadvantage of this approach is that one cannot add an SPDX data file into the
|
||||
// original package without changing the Package Checksum value. Alternatively, the Package Verification field enables
|
||||
// the inclusion of an SPDX file. It enables one to quickly verify if one or more of the original package files has
|
||||
// changed. The Package Verification field is a unique identifier that is based on SHAing only the original package
|
||||
// files (e.g., excluding the SPDX file). This allows one to add an SPDX file to the original package without changing
|
||||
// this unique identifier.
|
||||
// source: https://wiki.spdx.org/view/SPDX_FAQ
|
||||
type PackageVerificationCode struct {
|
||||
// "A file that was excluded when calculating the package verification code. This is usually a file containing
|
||||
// SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded
|
||||
// from the package verification code. If this is not done it would be impossible to correctly calculate the
|
||||
// verification codes in both files.
|
||||
PackageVerificationCodeExcludedFiles []string `json:"packageVerificationCodeExcludedFiles"`
|
||||
|
||||
// The actual package verification code as a hex encoded value.
|
||||
PackageVerificationCodeValue string `json:"packageVerificationCodeValue"`
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package spdx22
|
||||
|
||||
type Relationship struct {
|
||||
// SPDX ID for SpdxElement. A related SpdxElement.
|
||||
RelatedSpdxElement string `json:"relatedSpdxElement"`
|
||||
// Describes the type of relationship between two SPDX elements.
|
||||
RelationshipType RelationshipType `json:"relationshipType"`
|
||||
Comment string `json:"comment,omitempty"`
|
||||
}
|
||||
|
||||
// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/
|
||||
type RelationshipType string
|
||||
|
||||
const (
|
||||
// DescribedByRelationship is to be used when SPDXRef-A is described by SPDXREF-Document.
|
||||
// Example: The package 'WildFly' is described by SPDX document WildFly.spdx.
|
||||
DescribedByRelationship RelationshipType = "DESCRIBED_BY"
|
||||
|
||||
// ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B.
|
||||
// Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c.
|
||||
ContainsRelationship RelationshipType = "CONTAINS"
|
||||
|
||||
// ContainedByRelationship is to be used when SPDXRef-A is contained by SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c is contained by ARCHIVE file bar.tgz
|
||||
ContainedByRelationship RelationshipType = "CONTAINED_BY"
|
||||
|
||||
// DependsOnRelationship is to be used when SPDXRef-A depends on SPDXRef-B.
|
||||
// Example: Package A depends on the presence of package B in order to build and run
|
||||
DependsOnRelationship RelationshipType = "DEPENDS_ON"
|
||||
|
||||
// DependencyOfRelationship is to be used when SPDXRef-A is dependency of SPDXRef-B.
|
||||
// Example: A is explicitly stated as a dependency of B in a machine-readable file. Use when a package manager does not define scopes.
|
||||
DependencyOfRelationship RelationshipType = "DEPENDENCY_OF"
|
||||
|
||||
// DependencyManifestOfRelationship is to be used when SPDXRef-A is a manifest file that lists a set of dependencies for SPDXRef-B.
|
||||
// Example: A file package.json is the dependency manifest of a package foo. Note that only one manifest should be used to define the same dependency graph.
|
||||
DependencyManifestOfRelationship RelationshipType = "DEPENDENCY_MANIFEST_OF"
|
||||
|
||||
// BuildDependencyOfRelationship is to be used when SPDXRef-A is a build dependency of SPDXRef-B.
|
||||
// Example: A is in the compile scope of B in a Maven project.
|
||||
BuildDependencyOfRelationship RelationshipType = "BUILD_DEPENDENCY_OF"
|
||||
|
||||
// DevDependencyOfRelationship is to be used when SPDXRef-A is a development dependency of SPDXRef-B.
|
||||
// Example: A is in the devDependencies scope of B in a Maven project.
|
||||
DevDependencyOfRelationship RelationshipType = "DEV_DEPENDENCY_OF"
|
||||
|
||||
// OptionalDependencyOfRelationship is to be used when SPDXRef-A is an optional dependency of SPDXRef-B.
|
||||
// Example: Use when building the code will proceed even if a dependency cannot be found, fails to install, or is only installed on a specific platform. For example, A is in the optionalDependencies scope of npm project B.
|
||||
OptionalDependencyOfRelationship RelationshipType = "OPTIONAL_DEPENDENCY_OF"
|
||||
|
||||
// ProvidedDependencyOfRelationship is to be used when SPDXRef-A is a to be provided dependency of SPDXRef-B.
|
||||
// Example: A is in the provided scope of B in a Maven project, indicating that the project expects it to be provided, for instance, by the container or JDK.
|
||||
ProvidedDependencyOfRelationship RelationshipType = "PROVIDED_DEPENDENCY_OF"
|
||||
|
||||
// TestDependencyOfRelationship is to be used when SPDXRef-A is a test dependency of SPDXRef-B.
|
||||
// Example: A is in the test scope of B in a Maven project.
|
||||
TestDependencyOfRelationship RelationshipType = "TEST_DEPENDENCY_OF"
|
||||
|
||||
// RuntimeDependencyOfRelationship is to be used when SPDXRef-A is a dependency required for the execution of SPDXRef-B.
|
||||
// Example: A is in the runtime scope of B in a Maven project.
|
||||
RuntimeDependencyOfRelationship RelationshipType = "RUNTIME_DEPENDENCY_OF"
|
||||
|
||||
// ExampleOfRelationship is to be used when SPDXRef-A is an example of SPDXRef-B.
|
||||
// Example: The file or snippet that illustrates how to use an application or library.
|
||||
ExampleOfRelationship RelationshipType = "EXAMPLE_OF"
|
||||
|
||||
// GeneratesRelationship is to be used when SPDXRef-A generates SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk generates a BINARY file a.out
|
||||
GeneratesRelationship RelationshipType = "GENERATES"
|
||||
|
||||
// GeneratedFromRelationship is to be used when SPDXRef-A was generated from SPDXRef-B.
|
||||
// Example: A BINARY file a.out has been generated from a SOURCE file makefile.mk. A BINARY file foolib.a is generated from a SOURCE file bar.c.
|
||||
GeneratedFromRelationship RelationshipType = "GENERATED_FROM"
|
||||
|
||||
// AncestorOfRelationship is to be used when SPDXRef-A is an ancestor (same lineage but pre-dates) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk is a version of the original ancestor SOURCE file 'makefile2.mk'
|
||||
AncestorOfRelationship RelationshipType = "ANCESTOR_OF"
|
||||
|
||||
// DescendantOfRelationship is to be used when SPDXRef-A is a descendant of (same lineage but postdates) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile2.mk is a descendant of the original SOURCE file 'makefile.mk'
|
||||
DescendantOfRelationship RelationshipType = "DESCENDANT_OF"
|
||||
|
||||
// VariantOfRelationship is to be used when SPDXRef-A is a variant of (same lineage but not clear which came first) SPDXRef-B.
|
||||
// Example: A SOURCE file makefile2.mk is a variant of SOURCE file makefile.mk if they differ by some edit, but there is no way to tell which came first (no reliable date information).
|
||||
VariantOfRelationship RelationshipType = "VARIANT_OF"
|
||||
|
||||
// DistributionArtifactRelationship is to be used when distributing SPDXRef-A requires that SPDXRef-B also be distributed.
|
||||
// Example: A BINARY file foo.o requires that the ARCHIVE file bar-sources.tgz be made available on distribution.
|
||||
DistributionArtifactRelationship RelationshipType = "DISTRIBUTION_ARTIFACT"
|
||||
|
||||
// PatchForRelationship is to be used when SPDXRef-A is a patch file for (to be applied to) SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff is a patch file for SOURCE file foo.c.
|
||||
PatchForRelationship RelationshipType = "PATCH_FOR"
|
||||
|
||||
// PatchAppliedRelationship is to be used when SPDXRef-A is a patch file that has been applied to SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff is a patch file that has been applied to SOURCE file 'foo-patched.c'.
|
||||
PatchAppliedRelationship RelationshipType = "PATCH_APPLIED"
|
||||
|
||||
// CopyOfRelationship is to be used when SPDXRef-A is an exact copy of SPDXRef-B.
|
||||
// Example: A BINARY file alib.a is an exact copy of BINARY file a2lib.a.
|
||||
CopyOfRelationship RelationshipType = "COPY_OF"
|
||||
|
||||
// FileAddedRelationship is to be used when SPDXRef-A is a file that was added to SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c has been added to package ARCHIVE bar.tgz.
|
||||
FileAddedRelationship RelationshipType = "FILE_ADDED"
|
||||
|
||||
// FileDeletedRelationship is to be used when SPDXRef-A is a file that was deleted from SPDXRef-B.
|
||||
// Example: A SOURCE file foo.diff has been deleted from package ARCHIVE bar.tgz.
|
||||
FileDeletedRelationship RelationshipType = "FILE_DELETED"
|
||||
|
||||
// FileModifiedRelationship is to be used when SPDXRef-A is a file that was modified from SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c has been modified from SOURCE file foo.orig.c.
|
||||
FileModifiedRelationship RelationshipType = "FILE_MODIFIED"
|
||||
|
||||
// ExpandedFromArchiveRelationship is to be used when SPDXRef-A is expanded from the archive SPDXRef-B.
|
||||
// Example: A SOURCE file foo.c, has been expanded from the archive ARCHIVE file xyz.tgz.
|
||||
ExpandedFromArchiveRelationship RelationshipType = "EXPANDED_FROM_ARCHIVE"
|
||||
|
||||
// DynamicLinkRelationship is to be used when SPDXRef-A dynamically links to SPDXRef-B.
|
||||
// Example: An APPLICATION file 'myapp' dynamically links to BINARY file zlib.so.
|
||||
DynamicLinkRelationship RelationshipType = "DYNAMIC_LINK"
|
||||
|
||||
// StaticLinkRelationship is to be used when SPDXRef-A statically links to SPDXRef-B.
|
||||
// Example: An APPLICATION file 'myapp' statically links to BINARY zlib.a.
|
||||
StaticLinkRelationship RelationshipType = "STATIC_LINK"
|
||||
|
||||
// DataFileOfRelationship is to be used when SPDXRef-A is a data file used in SPDXRef-B.
|
||||
// Example: An IMAGE file 'kitty.jpg' is a data file of an APPLICATION 'hellokitty'.
|
||||
DataFileOfRelationship RelationshipType = "DATA_FILE_OF"
|
||||
|
||||
// TestCaseOfRelationship is to be used when SPDXRef-A is a test case used in testing SPDXRef-B.
|
||||
// Example: A SOURCE file testMyCode.java is a unit test file used to test an APPLICATION MyPackage.
|
||||
TestCaseOfRelationship RelationshipType = "TEST_CASE_OF"
|
||||
|
||||
// BuildToolOfRelationship is to be used when SPDXRef-A is used to build SPDXRef-B.
|
||||
// Example: A SOURCE file makefile.mk is used to build an APPLICATION 'zlib'.
|
||||
BuildToolOfRelationship RelationshipType = "BUILD_TOOL_OF"
|
||||
|
||||
// DevToolOfRelationship is to be used when SPDXRef-A is used as a development tool for SPDXRef-B.
|
||||
// Example: Any tool used for development such as a code debugger.
|
||||
DevToolOfRelationship RelationshipType = "DEV_TOOL_OF"
|
||||
|
||||
// TestOfRelationship is to be used when SPDXRef-A is used for testing SPDXRef-B.
|
||||
// Example: Generic relationship for cases where it's clear that something is used for testing but unclear whether it's TEST_CASE_OF or TEST_TOOL_OF.
|
||||
TestOfRelationship RelationshipType = "TEST_OF"
|
||||
|
||||
// TestToolOfRelationship is to be used when SPDXRef-A is used as a test tool for SPDXRef-B.
|
||||
// Example: Any tool used to test the code such as ESlint.
|
||||
TestToolOfRelationship RelationshipType = "TEST_TOOL_OF"
|
||||
|
||||
// DocumentationOfRelationship is to be used when SPDXRef-A provides documentation of SPDXRef-B.
|
||||
// Example: A DOCUMENTATION file readme.txt documents the APPLICATION 'zlib'.
|
||||
DocumentationOfRelationship RelationshipType = "DOCUMENTATION_OF"
|
||||
|
||||
// OptionalComponentOfRelationship is to be used when SPDXRef-A is an optional component of SPDXRef-B.
|
||||
// Example: A SOURCE file fool.c (which is in the contributors directory) may or may not be included in the build of APPLICATION 'atthebar'.
|
||||
OptionalComponentOfRelationship RelationshipType = "OPTIONAL_COMPONENT_OF"
|
||||
|
||||
// MetafileOfRelationship is to be used when SPDXRef-A is a metafile of SPDXRef-B.
|
||||
// Example: A SOURCE file pom.xml is a metafile of the APPLICATION 'Apache Xerces'.
|
||||
MetafileOfRelationship RelationshipType = "METAFILE_OF"
|
||||
|
||||
// PackageOfRelationship is to be used when SPDXRef-A is used as a package as part of SPDXRef-B.
|
||||
// Example: A Linux distribution contains an APPLICATION package gawk as part of the distribution MyLinuxDistro.
|
||||
PackageOfRelationship RelationshipType = "PACKAGE_OF"
|
||||
|
||||
// AmendsRelationship is to be used when (current) SPDXRef-DOCUMENT amends the SPDX information in SPDXRef-B.
|
||||
// Example: (Current) SPDX document A version 2 contains a correction to a previous version of the SPDX document A version 1. Note the reserved identifier SPDXRef-DOCUMENT for the current document is required.
|
||||
AmendsRelationship RelationshipType = "AMENDS"
|
||||
|
||||
// PrerequisiteForRelationship is to be used when SPDXRef-A is a prerequisite for SPDXRef-B.
|
||||
// Example: A library bar.dll is a prerequisite or dependency for APPLICATION foo.exe
|
||||
PrerequisiteForRelationship RelationshipType = "PREREQUISITE_FOR"
|
||||
|
||||
// HasPrerequisiteRelationship is to be used when SPDXRef-A has as a prerequisite SPDXRef-B.
|
||||
// Example: An APPLICATION foo.exe has prerequisite or dependency on bar.dll
|
||||
HasPrerequisiteRelationship RelationshipType = "HAS_PREREQUISITE"
|
||||
|
||||
// OtherRelationship is to be used for a relationship which has not been defined in the formal SPDX specification. A description of the relationship should be included in the Relationship comments field.
|
||||
OtherRelationship RelationshipType = "OTHER"
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
package spdx22
|
||||
|
||||
type StartPointer struct {
|
||||
Offset int `json:"offset,omitempty"`
|
||||
LineNumber int `json:"lineNumber,omitempty"`
|
||||
// SPDX ID for File
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
type EndPointer struct {
|
||||
Offset int `json:"offset,omitempty"`
|
||||
LineNumber int `json:"lineNumber,omitempty"`
|
||||
// SPDX ID for File
|
||||
Reference string `json:"reference"`
|
||||
}
|
||||
|
||||
type Range struct {
|
||||
StartPointer StartPointer `json:"startPointer"`
|
||||
EndPointer EndPointer `json:"endPointer"`
|
||||
}
|
||||
|
||||
type Snippet struct {
|
||||
Item
|
||||
// Licensing information that was discovered directly in the subject snippet. This is also considered a declared
|
||||
// license for the snippet. (elements are license expressions)
|
||||
LicenseInfoInSnippets []string `json:"licenseInfoInSnippets"`
|
||||
// SPDX ID for File. File containing the SPDX element (e.g. the file contaning a snippet).
|
||||
SnippetFromFile string `json:"snippetFromFile"`
|
||||
// (At least 1 range is required). This field defines the byte range in the original host file (in X.2) that the
|
||||
// snippet information applies to.
|
||||
Ranges []Range `json:"ranges"`
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
package spdx22
|
||||
|
||||
const Version = "SPDX-2.2"
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -1,35 +0,0 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,168 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func getSPDXExternalRefs(p *pkg.Package) (externalRefs []spdx22.ExternalRef) {
|
||||
externalRefs = make([]spdx22.ExternalRef, 0)
|
||||
for _, c := range p.CPEs {
|
||||
externalRefs = append(externalRefs, spdx22.ExternalRef{
|
||||
ReferenceCategory: spdx22.SecurityReferenceCategory,
|
||||
ReferenceLocator: c.BindToFmtString(),
|
||||
ReferenceType: spdx22.Cpe23ExternalRefType,
|
||||
})
|
||||
}
|
||||
|
||||
if p.PURL != "" {
|
||||
externalRefs = append(externalRefs, spdx22.ExternalRef{
|
||||
ReferenceCategory: spdx22.PackageManagerReferenceCategory,
|
||||
ReferenceLocator: p.PURL,
|
||||
ReferenceType: spdx22.PurlExternalRefType,
|
||||
})
|
||||
}
|
||||
return externalRefs
|
||||
}
|
||||
|
||||
func getSPDXLicense(p *pkg.Package) string {
|
||||
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
|
||||
// The options to populate this field are limited to:
|
||||
// A valid SPDX License Expression as defined in Appendix IV;
|
||||
// NONE, if the SPDX file creator concludes there is no license available for this package; or
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
|
||||
if len(p.Licenses) == 0 {
|
||||
return "NONE"
|
||||
}
|
||||
|
||||
// take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
|
||||
var parsedLicenses []string
|
||||
for _, l := range p.Licenses {
|
||||
if value, exists := spdxlicense.ID(l); exists {
|
||||
parsedLicenses = append(parsedLicenses, value)
|
||||
}
|
||||
}
|
||||
|
||||
if len(parsedLicenses) == 0 {
|
||||
return "NOASSERTION"
|
||||
}
|
||||
|
||||
return strings.Join(parsedLicenses, " AND ")
|
||||
}
|
||||
|
||||
func noneIfEmpty(value string) string {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return "NONE"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func getSPDXDownloadLocation(p *pkg.Package) string {
|
||||
// 3.7: Package Download Location
|
||||
// Cardinality: mandatory, one
|
||||
// NONE if there is no download location whatsoever.
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return noneIfEmpty(metadata.URL)
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return noneIfEmpty(metadata.URL)
|
||||
default:
|
||||
return "NOASSERTION"
|
||||
}
|
||||
}
|
||||
|
||||
func getSPDXHomepage(p *pkg.Package) string {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.GemMetadata:
|
||||
return metadata.Homepage
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Homepage
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getSPDXSourceInfo(p *pkg.Package) string {
|
||||
answer := ""
|
||||
switch p.Type {
|
||||
case pkg.RpmPkg:
|
||||
answer = "acquired package info from RPM DB"
|
||||
case pkg.ApkPkg:
|
||||
answer = "acquired package info from APK DB"
|
||||
case pkg.DebPkg:
|
||||
answer = "acquired package info from DPKG DB"
|
||||
case pkg.NpmPkg:
|
||||
answer = "acquired package info from installed node module manifest file"
|
||||
case pkg.PythonPkg:
|
||||
answer = "acquired package info from installed python package manifest file"
|
||||
case pkg.JavaPkg, pkg.JenkinsPluginPkg:
|
||||
answer = "acquired package info from installed java archive"
|
||||
case pkg.GemPkg:
|
||||
answer = "acquired package info from installed gem metadata file"
|
||||
case pkg.GoModulePkg:
|
||||
answer = "acquired package info from go module information"
|
||||
case pkg.RustPkg:
|
||||
answer = "acquired package info from rust cargo manifest"
|
||||
default:
|
||||
answer = "acquired package info from the following paths"
|
||||
}
|
||||
var paths []string
|
||||
for _, l := range p.Locations {
|
||||
paths = append(paths, l.RealPath)
|
||||
}
|
||||
|
||||
return answer + ": " + strings.Join(paths, ", ")
|
||||
}
|
||||
|
||||
func getSPDXOriginator(p *pkg.Package) string {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return metadata.Maintainer
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Author
|
||||
case pkg.PythonPackageMetadata:
|
||||
author := metadata.Author
|
||||
if author == "" {
|
||||
return metadata.AuthorEmail
|
||||
}
|
||||
if metadata.AuthorEmail != "" {
|
||||
author += fmt.Sprintf(" <%s>", metadata.AuthorEmail)
|
||||
}
|
||||
return author
|
||||
case pkg.GemMetadata:
|
||||
if len(metadata.Authors) > 0 {
|
||||
return metadata.Authors[0]
|
||||
}
|
||||
return ""
|
||||
case pkg.RpmdbMetadata:
|
||||
return metadata.Vendor
|
||||
case pkg.DpkgMetadata:
|
||||
return metadata.Maintainer
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func getSPDXDescription(p *pkg.Package) string {
|
||||
switch metadata := p.Metadata.(type) {
|
||||
case pkg.ApkMetadata:
|
||||
return metadata.Description
|
||||
case pkg.NpmPackageJSONMetadata:
|
||||
return metadata.Description
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/source"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
|
||||
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
func Test_getSPDXExternalRefs(t *testing.T) {
|
||||
testCPE := must(pkg.NewCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*"))
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected []spdx22.ExternalRef
|
||||
}{
|
||||
{
|
||||
name: "cpe + purl",
|
||||
input: pkg.Package{
|
||||
CPEs: []pkg.CPE{
|
||||
testCPE,
|
||||
},
|
||||
PURL: "a-purl",
|
||||
},
|
||||
expected: []spdx22.ExternalRef{
|
||||
{
|
||||
ReferenceCategory: spdx22.SecurityReferenceCategory,
|
||||
ReferenceLocator: testCPE.BindToFmtString(),
|
||||
ReferenceType: spdx22.Cpe23ExternalRefType,
|
||||
},
|
||||
{
|
||||
ReferenceCategory: spdx22.PackageManagerReferenceCategory,
|
||||
ReferenceLocator: "a-purl",
|
||||
ReferenceType: spdx22.PurlExternalRefType,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.ElementsMatch(t, test.expected, getSPDXExternalRefs(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXLicense(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no licenses",
|
||||
input: pkg.Package{},
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "no SPDX licenses",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"made-up",
|
||||
},
|
||||
},
|
||||
expected: "NOASSERTION",
|
||||
},
|
||||
{
|
||||
name: "with SPDX license",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
},
|
||||
},
|
||||
expected: "MIT",
|
||||
},
|
||||
{
|
||||
name: "with SPDX license expression",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"MIT",
|
||||
"GPL-3.0",
|
||||
},
|
||||
},
|
||||
expected: "MIT AND GPL-3.0",
|
||||
},
|
||||
{
|
||||
name: "cap insensitive",
|
||||
input: pkg.Package{
|
||||
Licenses: []string{
|
||||
"gpl-3.0",
|
||||
},
|
||||
},
|
||||
expected: "GPL-3.0",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXLicense(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_noneIfEmpty(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "non-zero value",
|
||||
value: "something",
|
||||
expected: "something",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
value: "",
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "space",
|
||||
value: " ",
|
||||
expected: "NONE",
|
||||
},
|
||||
{
|
||||
name: "tab",
|
||||
value: "\t",
|
||||
expected: "NONE",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, noneIfEmpty(test.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXDownloadLocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "NOASSERTION",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
URL: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
URL: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
URL: "",
|
||||
},
|
||||
},
|
||||
expected: "NONE",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXDownloadLocation(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXHomepage(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from gem",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.GemMetadata{
|
||||
Homepage: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "http://a-place.gov",
|
||||
},
|
||||
},
|
||||
expected: "http://a-place.gov",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXHomepage(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXSourceInfo(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected []string
|
||||
}{
|
||||
{
|
||||
name: "locations are captured",
|
||||
input: pkg.Package{
|
||||
// note: no type given
|
||||
Locations: []source.Location{
|
||||
{
|
||||
RealPath: "/a-place",
|
||||
VirtualPath: "/b-place",
|
||||
},
|
||||
{
|
||||
RealPath: "/c-place",
|
||||
VirtualPath: "/d-place",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: []string{
|
||||
"from the following paths",
|
||||
"/a-place",
|
||||
"/c-place",
|
||||
},
|
||||
},
|
||||
{
|
||||
// note: no specific support for this
|
||||
input: pkg.Package{
|
||||
Type: pkg.KbPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from the following paths",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.RpmPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from RPM DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.ApkPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from APK DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.DebPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from DPKG DB",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.NpmPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed node module manifest file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.PythonPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed python package manifest file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.JavaPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed java archive",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.JenkinsPluginPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed java archive",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.GemPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from installed gem metadata file",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.GoModulePkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from go module information",
|
||||
},
|
||||
},
|
||||
{
|
||||
input: pkg.Package{
|
||||
Type: pkg.RustPkg,
|
||||
},
|
||||
expected: []string{
|
||||
"from rust cargo manifest",
|
||||
},
|
||||
},
|
||||
}
|
||||
var pkgTypes []pkg.Type
|
||||
for _, test := range tests {
|
||||
t.Run(test.name+" "+string(test.input.Type), func(t *testing.T) {
|
||||
if test.input.Type != "" {
|
||||
pkgTypes = append(pkgTypes, test.input.Type)
|
||||
}
|
||||
actual := getSPDXSourceInfo(&test.input)
|
||||
for _, expected := range test.expected {
|
||||
assert.Contains(t, actual, expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
assert.ElementsMatch(t, pkg.AllPkgs, pkgTypes, "missing one or more package types to test against (maybe a package type was added?)")
|
||||
}
|
||||
|
||||
func Test_getSPDXOriginator(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from gem",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.GemMetadata{
|
||||
Authors: []string{
|
||||
"auth1",
|
||||
"auth2",
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: "auth1",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Maintainer: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from python - just name",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Author: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from python - just email",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
AuthorEmail: "auth@auth.gov",
|
||||
},
|
||||
},
|
||||
expected: "auth@auth.gov",
|
||||
},
|
||||
{
|
||||
name: "from python - both name and email",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Author: "auth",
|
||||
AuthorEmail: "auth@auth.gov",
|
||||
},
|
||||
},
|
||||
expected: "auth <auth@auth.gov>",
|
||||
},
|
||||
{
|
||||
name: "from rpm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.RpmdbMetadata{
|
||||
Vendor: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
name: "from dpkg",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Maintainer: "auth",
|
||||
},
|
||||
},
|
||||
expected: "auth",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Author: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXOriginator(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_getSPDXDescription(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input pkg.Package
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "no metadata",
|
||||
input: pkg.Package{},
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "from apk",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.ApkMetadata{
|
||||
Description: "a description!",
|
||||
},
|
||||
},
|
||||
expected: "a description!",
|
||||
},
|
||||
{
|
||||
name: "from npm",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Description: "a description!",
|
||||
},
|
||||
},
|
||||
expected: "a description!",
|
||||
},
|
||||
{
|
||||
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
|
||||
name: "empty",
|
||||
input: pkg.Package{
|
||||
Metadata: pkg.NpmPackageJSONMetadata{
|
||||
Homepage: "",
|
||||
},
|
||||
},
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, getSPDXDescription(&test.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
"github.com/anchore/syft/internal/version"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
)
|
||||
|
||||
// SPDXJsonPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
|
||||
type SPDXJsonPresenter struct {
|
||||
catalog *pkg.Catalog
|
||||
srcMetadata source.Metadata
|
||||
}
|
||||
|
||||
// NewSPDXJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
||||
func NewSPDXJSONPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXJsonPresenter {
|
||||
return &SPDXJsonPresenter{
|
||||
catalog: catalog,
|
||||
srcMetadata: srcMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Present the catalog results to the given writer.
|
||||
func (pres *SPDXJsonPresenter) Present(output io.Writer) error {
|
||||
doc := newSPDXJsonDocument(pres.catalog, pres.srcMetadata)
|
||||
|
||||
enc := json.NewEncoder(output)
|
||||
// prevent > and < from being escaped in the payload
|
||||
enc.SetEscapeHTML(false)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(&doc)
|
||||
}
|
||||
|
||||
func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document {
|
||||
var name string
|
||||
switch srcMetadata.Scheme {
|
||||
case source.ImageScheme:
|
||||
name = srcMetadata.ImageMetadata.UserInput
|
||||
case source.DirectoryScheme:
|
||||
name = srcMetadata.Path
|
||||
}
|
||||
|
||||
return spdx22.Document{
|
||||
Element: spdx22.Element{
|
||||
SPDXID: spdx22.ElementID("DOCUMENT").String(),
|
||||
Name: name,
|
||||
},
|
||||
SPDXVersion: spdx22.Version,
|
||||
CreationInfo: spdx22.CreationInfo{
|
||||
Created: time.Now().UTC(),
|
||||
Creators: []string{
|
||||
// note: key-value format derived from the JSON example document examples: https://github.com/spdx/spdx-spec/blob/v2.2/examples/SPDXJSONExample-v2.2.spdx.json
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: " + internal.ApplicationName + "-" + version.FromBuild().Version,
|
||||
},
|
||||
LicenseListVersion: spdxlicense.Version,
|
||||
},
|
||||
DataLicense: "CC0-1.0",
|
||||
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput),
|
||||
Packages: newSPDXJsonPackages(catalog),
|
||||
}
|
||||
}
|
||||
|
||||
func newSPDXJsonPackages(catalog *pkg.Catalog) []spdx22.Package {
|
||||
results := make([]spdx22.Package, 0)
|
||||
for _, p := range catalog.Sorted() {
|
||||
license := getSPDXLicense(p)
|
||||
|
||||
// note: the license concluded and declared should be the same since we are collecting license information
|
||||
// from the project data itself (the installed package files).
|
||||
results = append(results, spdx22.Package{
|
||||
Description: getSPDXDescription(p),
|
||||
DownloadLocation: getSPDXDownloadLocation(p),
|
||||
ExternalRefs: getSPDXExternalRefs(p),
|
||||
FilesAnalyzed: false,
|
||||
Homepage: getSPDXHomepage(p),
|
||||
LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package
|
||||
Originator: getSPDXOriginator(p),
|
||||
SourceInfo: getSPDXSourceInfo(p),
|
||||
VersionInfo: p.Version,
|
||||
Item: spdx22.Item{
|
||||
LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package
|
||||
Element: spdx22.Element{
|
||||
SPDXID: spdx22.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(),
|
||||
Name: p.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
|
||||
|
||||
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewSPDXJSONPresenter(catalog, metadata),
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewSPDXJSONPresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateSpdxJson,
|
||||
spdxJsonRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func spdxJsonRedactor(s []byte) []byte {
|
||||
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
||||
s = regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
|
||||
return regexp.MustCompile(`"licenseListVersion": .*`).ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"time"
|
||||
|
||||
"github.com/anchore/syft/internal/spdxlicense"
|
||||
|
||||
"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/spdx/tools-golang/spdx"
|
||||
"github.com/spdx/tools-golang/tvsaver"
|
||||
)
|
||||
|
||||
// SPDXTagValuePresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
|
||||
type SPDXTagValuePresenter struct {
|
||||
catalog *pkg.Catalog
|
||||
srcMetadata source.Metadata
|
||||
}
|
||||
|
||||
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
||||
func NewSPDXTagValuePresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXTagValuePresenter {
|
||||
return &SPDXTagValuePresenter{
|
||||
catalog: catalog,
|
||||
srcMetadata: srcMetadata,
|
||||
}
|
||||
}
|
||||
|
||||
// Present the catalog results to the given writer.
|
||||
// nolint: funlen
|
||||
func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
|
||||
doc := spdx.Document2_2{
|
||||
CreationInfo: &spdx.CreationInfo2_2{
|
||||
// 2.1: SPDX Version; should be in the format "SPDX-2.2"
|
||||
// Cardinality: mandatory, one
|
||||
SPDXVersion: "SPDX-2.2",
|
||||
|
||||
// 2.2: Data License; should be "CC0-1.0"
|
||||
// Cardinality: mandatory, one
|
||||
DataLicense: "CC0-1.0",
|
||||
|
||||
// 2.3: SPDX Identifier; should be "DOCUMENT" to represent mandatory identifier of SPDXRef-DOCUMENT
|
||||
// Cardinality: mandatory, one
|
||||
SPDXIdentifier: spdx.ElementID("DOCUMENT"),
|
||||
|
||||
// 2.4: Document Name
|
||||
// Cardinality: mandatory, one
|
||||
DocumentName: pres.srcMetadata.ImageMetadata.UserInput,
|
||||
|
||||
// 2.5: Document Namespace
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: Provide an SPDX document specific namespace as a unique absolute Uniform Resource
|
||||
// Identifier (URI) as specified in RFC-3986, with the exception of the ‘#’ delimiter. The SPDX
|
||||
// Document URI cannot contain a URI "part" (e.g. the "#" character), since the ‘#’ is used in SPDX
|
||||
// element URIs (packages, files, snippets, etc) to separate the document namespace from the
|
||||
// element’s SPDX identifier. Additionally, a scheme (e.g. “https:”) is required.
|
||||
|
||||
// The URI must be unique for the SPDX document including the specific version of the SPDX document.
|
||||
// If the SPDX document is updated, thereby creating a new version, a new URI for the updated
|
||||
// document must be used. There can only be one URI for an SPDX document and only one SPDX document
|
||||
// for a given URI.
|
||||
|
||||
// Note that the URI does not have to be accessible. It is only intended to provide a unique ID.
|
||||
// In many cases, the URI will point to a web accessible document, but this should not be assumed
|
||||
// to be the case.
|
||||
|
||||
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", pres.srcMetadata.ImageMetadata.UserInput),
|
||||
|
||||
// 2.6: External Document References
|
||||
// Cardinality: optional, one or many
|
||||
ExternalDocumentReferences: nil,
|
||||
|
||||
// 2.7: License List Version
|
||||
// Cardinality: optional, one
|
||||
LicenseListVersion: spdxlicense.Version,
|
||||
|
||||
// 2.8: Creators: may have multiple keys for Person, Organization
|
||||
// and/or Tool
|
||||
// Cardinality: mandatory, one or many
|
||||
CreatorPersons: nil,
|
||||
CreatorOrganizations: []string{"Anchore, Inc"},
|
||||
CreatorTools: []string{internal.ApplicationName + "-" + version.FromBuild().Version},
|
||||
|
||||
// 2.9: Created: data format YYYY-MM-DDThh:mm:ssZ
|
||||
// Cardinality: mandatory, one
|
||||
Created: time.Now().UTC().Format(time.RFC3339),
|
||||
|
||||
// 2.10: Creator Comment
|
||||
// Cardinality: optional, one
|
||||
CreatorComment: "",
|
||||
|
||||
// 2.11: Document Comment
|
||||
// Cardinality: optional, one
|
||||
DocumentComment: "",
|
||||
},
|
||||
Packages: pres.packages(),
|
||||
}
|
||||
|
||||
return tvsaver.Save2_2(&doc, output)
|
||||
}
|
||||
|
||||
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
|
||||
// nolint: funlen
|
||||
func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_2 {
|
||||
results := make(map[spdx.ElementID]*spdx.Package2_2)
|
||||
|
||||
for p := range pres.catalog.Enumerate() {
|
||||
// name should be guaranteed to be unique, but semantically useful and stable
|
||||
id := fmt.Sprintf("Package-%+v-%s", p.Type, p.Name)
|
||||
|
||||
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
|
||||
// in the Comments on License field (section 3.16). With respect to NOASSERTION, a written explanation in
|
||||
// the Comments on License field (section 3.16) is preferred.
|
||||
license := getSPDXLicense(p)
|
||||
|
||||
results[spdx.ElementID(id)] = &spdx.Package2_2{
|
||||
|
||||
// NOT PART OF SPEC
|
||||
// flag: does this "package" contain files that were in fact "unpackaged",
|
||||
// e.g. included directly in the Document without being in a Package?
|
||||
IsUnpackaged: false,
|
||||
|
||||
// 3.1: Package Name
|
||||
// Cardinality: mandatory, one
|
||||
PackageName: p.Name,
|
||||
|
||||
// 3.2: Package SPDX Identifier: "SPDXRef-[idstring]"
|
||||
// Cardinality: mandatory, one
|
||||
PackageSPDXIdentifier: spdx.ElementID(id),
|
||||
|
||||
// 3.3: Package Version
|
||||
// Cardinality: optional, one
|
||||
PackageVersion: p.Version,
|
||||
|
||||
// 3.4: Package File Name
|
||||
// Cardinality: optional, one
|
||||
PackageFileName: "",
|
||||
|
||||
// 3.5: Package Supplier: may have single result for either Person or Organization,
|
||||
// or NOASSERTION
|
||||
// Cardinality: optional, one
|
||||
PackageSupplierPerson: "",
|
||||
PackageSupplierOrganization: "",
|
||||
PackageSupplierNOASSERTION: false,
|
||||
|
||||
// 3.6: Package Originator: may have single result for either Person or Organization,
|
||||
// or NOASSERTION
|
||||
// Cardinality: optional, one
|
||||
PackageOriginatorPerson: "",
|
||||
PackageOriginatorOrganization: "",
|
||||
PackageOriginatorNOASSERTION: false,
|
||||
|
||||
// 3.7: Package Download Location
|
||||
// Cardinality: mandatory, one
|
||||
// NONE if there is no download location whatsoever.
|
||||
// NOASSERTION if:
|
||||
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
|
||||
// (ii) the SPDX file creator has made no attempt to determine this field; or
|
||||
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
PackageDownloadLocation: "NOASSERTION",
|
||||
|
||||
// 3.8: FilesAnalyzed
|
||||
// Cardinality: optional, one; default value is "true" if omitted
|
||||
|
||||
// Purpose: Indicates whether the file content of this package has been available for or subjected to
|
||||
// analysis when creating the SPDX document. If false, indicates packages that represent metadata or
|
||||
// URI references to a project, product, artifact, distribution or a component. If false, the package
|
||||
// must not contain any files.
|
||||
|
||||
// Intent: A package can refer to a project, product, artifact, distribution or a component that is
|
||||
// external to the SPDX document.
|
||||
FilesAnalyzed: false,
|
||||
// NOT PART OF SPEC: did FilesAnalyzed tag appear?
|
||||
IsFilesAnalyzedTagPresent: true,
|
||||
|
||||
// 3.9: Package Verification Code
|
||||
// Cardinality: mandatory, one if filesAnalyzed is true / omitted;
|
||||
// zero (must be omitted) if filesAnalyzed is false
|
||||
PackageVerificationCode: "",
|
||||
// Spec also allows specifying a single file to exclude from the
|
||||
// verification code algorithm; intended to enable exclusion of
|
||||
// the SPDX document file itself.
|
||||
PackageVerificationCodeExcludedFile: "",
|
||||
|
||||
// 3.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
|
||||
// Cardinality: optional, one or many
|
||||
|
||||
// 3.10.1 Purpose: Provide an independently reproducible mechanism that permits unique identification of
|
||||
// a specific package that correlates to the data in this SPDX file. This identifier enables a recipient
|
||||
// to determine if any file in the original package has been changed. If the SPDX file is to be included
|
||||
// in a package, this value should not be calculated. The SHA-1 algorithm will be used to provide the
|
||||
// checksum by default.
|
||||
|
||||
// note: based on the purpose above no discovered checksums should be provided, but instead, only
|
||||
// tool-derived checksums.
|
||||
PackageChecksumSHA1: "",
|
||||
PackageChecksumSHA256: "",
|
||||
PackageChecksumMD5: "",
|
||||
|
||||
// 3.11: Package Home Page
|
||||
// Cardinality: optional, one
|
||||
PackageHomePage: "",
|
||||
|
||||
// 3.12: Source Information
|
||||
// Cardinality: optional, one
|
||||
PackageSourceInfo: "",
|
||||
|
||||
// 3.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: Contain the license the SPDX file creator has concluded as governing the
|
||||
// package or alternative values, if the governing license cannot be determined.
|
||||
PackageLicenseConcluded: license,
|
||||
|
||||
// 3.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
|
||||
// zero (must be omitted) if filesAnalyzed is false
|
||||
PackageLicenseInfoFromFiles: nil,
|
||||
|
||||
// 3.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: List the licenses that have been declared by the authors of the package.
|
||||
// Any license information that does not originate from the package authors, e.g. license
|
||||
// information from a third party repository, should not be included in this field.
|
||||
PackageLicenseDeclared: license,
|
||||
|
||||
// 3.16: Comments on License
|
||||
// Cardinality: optional, one
|
||||
PackageLicenseComments: "",
|
||||
|
||||
// 3.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
|
||||
// Cardinality: mandatory, one
|
||||
// Purpose: Identify the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to:
|
||||
//
|
||||
// Any text related to a copyright notice, even if not complete;
|
||||
// NONE if the package contains no copyright information whatsoever; or
|
||||
// NOASSERTION, if
|
||||
// (i) the SPDX document creator has made no attempt to determine this field; or
|
||||
// (ii) the SPDX document creator has intentionally provided no information (no meaning should be implied by doing so).
|
||||
//
|
||||
PackageCopyrightText: "NOASSERTION",
|
||||
|
||||
// 3.18: Package Summary Description
|
||||
// Cardinality: optional, one
|
||||
PackageSummary: "",
|
||||
|
||||
// 3.19: Package Detailed Description
|
||||
// Cardinality: optional, one
|
||||
PackageDescription: "",
|
||||
|
||||
// 3.20: Package Comment
|
||||
// Cardinality: optional, one
|
||||
PackageComment: "",
|
||||
|
||||
// 3.21: Package External Reference
|
||||
// Cardinality: optional, one or many
|
||||
PackageExternalReferences: formatSPDXExternalRefs(p),
|
||||
|
||||
// 3.22: Package External Reference Comment
|
||||
// Cardinality: conditional (optional, one) for each External Reference
|
||||
// contained within PackageExternalReference2_1 struct, if present
|
||||
|
||||
// 3.23: Package Attribution Text
|
||||
// Cardinality: optional, one or many
|
||||
PackageAttributionTexts: nil,
|
||||
|
||||
// Files contained in this Package
|
||||
Files: nil,
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
|
||||
for _, ref := range getSPDXExternalRefs(p) {
|
||||
refs = append(refs, &spdx.PackageExternalReference2_2{
|
||||
Category: string(ref.ReferenceCategory),
|
||||
RefType: string(ref.ReferenceType),
|
||||
Locator: ref.ReferenceLocator,
|
||||
ExternalRefComment: ref.Comment,
|
||||
})
|
||||
}
|
||||
return refs
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
|
||||
|
||||
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewSPDXTagValuePresenter(catalog, metadata),
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewSPDXTagValuePresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateSpdxTagValue,
|
||||
spdxTagValueRedactor,
|
||||
)
|
||||
}
|
||||
|
||||
func spdxTagValueRedactor(s []byte) []byte {
|
||||
// each SBOM reports the time it was generated, which is not useful during snapshot testing
|
||||
s = regexp.MustCompile(`Created: .*`).ReplaceAll(s, []byte("redacted"))
|
||||
// the license list will be updated periodically, the value here should not be directly tested in snapshot tests
|
||||
return regexp.MustCompile(`LicenseListVersion: .*`).ReplaceAll(s, []byte("redacted"))
|
||||
}
|
||||
@@ -1,72 +1,22 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/go-test/deep"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table presenters")
|
||||
|
||||
func TestTablePresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
testImage := "image-simple"
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
pres := NewTablePresenter(catalog)
|
||||
|
||||
// run presenter
|
||||
err := pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateTablePresenterGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
catalog, _, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewTablePresenter(catalog),
|
||||
testImage,
|
||||
*updateTablePresenterGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
func TestRemoveDuplicateRows(t *testing.T) {
|
||||
|
||||
+9
-10
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:544b9f08-b39d-4d0c-8723-32609567e8ed">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:5404937f-72d6-44a2-8e9b-954305ecb4f6">
|
||||
<metadata>
|
||||
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -16,20 +16,19 @@
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package1</name>
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
+9
-12
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e1515688-2fd6-422a-acb2-97405a44cdfe">
|
||||
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e34bad2e-cd27-483c-86dc-f4e26d6103b0">
|
||||
<metadata>
|
||||
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
|
||||
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
|
||||
<tools>
|
||||
<tool>
|
||||
<vendor>anchore</vendor>
|
||||
@@ -16,22 +16,19 @@
|
||||
</metadata>
|
||||
<components>
|
||||
<component type="library">
|
||||
<name>package1</name>
|
||||
<name>package-1</name>
|
||||
<version>1.0.1</version>
|
||||
<purl>the-purl-1</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package2</name>
|
||||
<version>2.0.1</version>
|
||||
<licenses>
|
||||
<license>
|
||||
<name>MIT</name>
|
||||
</license>
|
||||
<license>
|
||||
<name>Apache-v2</name>
|
||||
</license>
|
||||
</licenses>
|
||||
<purl>the-purl-2</purl>
|
||||
<purl>a-purl-1</purl>
|
||||
</component>
|
||||
<component type="library">
|
||||
<name>package-2</name>
|
||||
<version>2.0.1</version>
|
||||
<purl>a-purl-2</purl>
|
||||
</component>
|
||||
</components>
|
||||
</bom>
|
||||
+3
-3
@@ -66,9 +66,9 @@
|
||||
"target": "/some/path"
|
||||
},
|
||||
"distro": {
|
||||
"name": "",
|
||||
"version": "",
|
||||
"idLike": ""
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"idLike": "like!"
|
||||
},
|
||||
"descriptor": {
|
||||
"name": "syft",
|
||||
+3
-3
@@ -93,9 +93,9 @@
|
||||
}
|
||||
},
|
||||
"distro": {
|
||||
"name": "",
|
||||
"version": "",
|
||||
"idLike": ""
|
||||
"name": "debian",
|
||||
"version": "1.2.3",
|
||||
"idLike": "like!"
|
||||
},
|
||||
"descriptor": {
|
||||
"name": "syft",
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "/some/path",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-06-23T17:48:32.734847Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
],
|
||||
"licenseListVersion": "3.13"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-2",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"licenseDeclared": "MIT",
|
||||
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-2",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"licenseDeclared": "NONE",
|
||||
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
|
||||
"versionInfo": "2.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"SPDXID": "SPDXRef-DOCUMENT",
|
||||
"name": "user-image-input",
|
||||
"spdxVersion": "SPDX-2.2",
|
||||
"creationInfo": {
|
||||
"created": "2021-06-23T17:48:32.7379Z",
|
||||
"creators": [
|
||||
"Organization: Anchore, Inc",
|
||||
"Tool: syft-[not provided]"
|
||||
],
|
||||
"licenseListVersion": "3.13"
|
||||
},
|
||||
"dataLicense": "CC0-1.0",
|
||||
"documentNamespace": "https://anchore.com/syft/image/user-image-input",
|
||||
"packages": [
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
|
||||
"name": "package-1",
|
||||
"licenseConcluded": "MIT",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-1",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"licenseDeclared": "MIT",
|
||||
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
|
||||
"versionInfo": "1.0.1"
|
||||
},
|
||||
{
|
||||
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
|
||||
"name": "package-2",
|
||||
"licenseConcluded": "NONE",
|
||||
"downloadLocation": "NOASSERTION",
|
||||
"externalRefs": [
|
||||
{
|
||||
"referenceCategory": "SECURITY",
|
||||
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
|
||||
"referenceType": "cpe23Type"
|
||||
},
|
||||
{
|
||||
"referenceCategory": "PACKAGE_MANAGER",
|
||||
"referenceLocator": "a-purl-2",
|
||||
"referenceType": "purl"
|
||||
}
|
||||
],
|
||||
"filesAnalyzed": false,
|
||||
"licenseDeclared": "NONE",
|
||||
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
|
||||
"versionInfo": "2.0.1"
|
||||
}
|
||||
]
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
SPDXVersion: SPDX-2.2
|
||||
DataLicense: CC0-1.0
|
||||
SPDXID: SPDXRef-DOCUMENT
|
||||
DocumentNamespace: https://anchore.com/syft/image/
|
||||
LicenseListVersion: 3.13
|
||||
Creator: Organization: Anchore, Inc
|
||||
Creator: Tool: syft-[not provided]
|
||||
Created: 2021-06-23T17:49:25Z
|
||||
|
||||
##### Package: package-2
|
||||
|
||||
PackageName: package-2
|
||||
SPDXID: SPDXRef-Package-deb-package-2
|
||||
PackageVersion: 2.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: NONE
|
||||
PackageLicenseDeclared: NONE
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-2
|
||||
|
||||
##### Package: package-1
|
||||
|
||||
PackageName: package-1
|
||||
SPDXID: SPDXRef-Package-python-package-1
|
||||
PackageVersion: 1.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: MIT
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-2
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
SPDXVersion: SPDX-2.2
|
||||
DataLicense: CC0-1.0
|
||||
SPDXID: SPDXRef-DOCUMENT
|
||||
DocumentName: user-image-input
|
||||
DocumentNamespace: https://anchore.com/syft/image/user-image-input
|
||||
LicenseListVersion: 3.13
|
||||
Creator: Organization: Anchore, Inc
|
||||
Creator: Tool: syft-[not provided]
|
||||
Created: 2021-06-23T17:49:25Z
|
||||
|
||||
##### Package: package-2
|
||||
|
||||
PackageName: package-2
|
||||
SPDXID: SPDXRef-Package-deb-package-2
|
||||
PackageVersion: 2.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: NONE
|
||||
PackageLicenseDeclared: NONE
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-2
|
||||
|
||||
##### Package: package-1
|
||||
|
||||
PackageName: package-1
|
||||
SPDXID: SPDXRef-Package-python-package-1
|
||||
PackageVersion: 1.0.1
|
||||
PackageDownloadLocation: NOASSERTION
|
||||
FilesAnalyzed: false
|
||||
PackageLicenseConcluded: MIT
|
||||
PackageLicenseDeclared: MIT
|
||||
PackageCopyrightText: NOASSERTION
|
||||
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*
|
||||
ExternalRef: PACKAGE_MANAGER purl a-purl-1
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
NAME VERSION TYPE
|
||||
package-1 1.0.1 deb
|
||||
package-2 2.0.1 deb
|
||||
NAME VERSION TYPE
|
||||
package-1 1.0.1 python
|
||||
package-2 2.0.1 deb
|
||||
|
||||
+3
-3
@@ -1,11 +1,11 @@
|
||||
[Path: /some/path]
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
Type: python
|
||||
Found by: the-cataloger-1
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
Found by: the-cataloger-2
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
[Image]
|
||||
Layer: 0
|
||||
Digest: sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59
|
||||
Size: 22
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
Layer: 1
|
||||
Digest: sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec
|
||||
Size: 16
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: python
|
||||
Found by: the-cataloger-1
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by: the-cataloger-2
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
[Image]
|
||||
Layer: 0
|
||||
Digest: sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53
|
||||
Size: 22
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
Layer: 1
|
||||
Digest: sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53
|
||||
Size: 16
|
||||
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
|
||||
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: deb
|
||||
Found by: dpkg
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by: dpkg
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
[Path: /some/path]
|
||||
[package-1]
|
||||
Version: 1.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
|
||||
[package-2]
|
||||
Version: 2.0.1
|
||||
Type: deb
|
||||
Found by:
|
||||
|
||||
@@ -1,127 +1,26 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
)
|
||||
|
||||
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
|
||||
|
||||
func TestTextDirPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
s, err := source.NewFromDirectory("/some/path")
|
||||
if err != nil {
|
||||
t.Fatalf("unable to create source: %+v", err)
|
||||
}
|
||||
pres := NewTextPresenter(catalog, s.Metadata)
|
||||
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
|
||||
if *updateTextPresenterGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
func TestTextDirectoryPresenter(t *testing.T) {
|
||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
||||
assertPresenterAgainstGoldenSnapshot(t,
|
||||
NewTextPresenter(catalog, metadata),
|
||||
*updateTextPresenterGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
type PackageInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
}
|
||||
|
||||
func TestTextImgPresenter(t *testing.T) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
FoundBy: "dpkg",
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
FoundBy: "dpkg",
|
||||
Metadata: PackageInfo{Name: "package-2", Version: "1.0.2"},
|
||||
Type: pkg.DebPkg,
|
||||
})
|
||||
|
||||
// stub out all the digests so that they don't affect tests comparisons
|
||||
// TODO: update with stereoscope test utils feature when this issue is resolved: https://github.com/anchore/stereoscope/issues/43
|
||||
for _, l := range img.Layers {
|
||||
l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53"
|
||||
}
|
||||
|
||||
s, err := source.NewFromImage(img, "user-image-input")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
pres := NewTextPresenter(catalog, s.Metadata)
|
||||
// run presenter
|
||||
err = pres.Present(&buffer)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
actual := buffer.Bytes()
|
||||
if *updateTextPresenterGoldenFiles {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(actual), string(expected), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
|
||||
func TestTextImagePresenter(t *testing.T) {
|
||||
testImage := "image-simple"
|
||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
||||
NewTextPresenter(catalog, metadata),
|
||||
testImage,
|
||||
*updateTextPresenterGoldenFiles,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
package packages
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/go-testutils"
|
||||
"github.com/anchore/stereoscope/pkg/filetree"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/syft/distro"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/presenter"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type redactor func(s []byte) []byte
|
||||
|
||||
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
// grab the latest image contents and persist
|
||||
if updateSnapshot {
|
||||
imagetest.UpdateGoldenFixtureImage(t, testImage)
|
||||
}
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
assert.NoError(t, err)
|
||||
actual := buffer.Bytes()
|
||||
|
||||
// replace the expected snapshot contents with the current presenter contents
|
||||
if updateSnapshot {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
expected = r(expected)
|
||||
}
|
||||
|
||||
// assert that the golden file snapshot matches the actual contents
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
|
||||
var buffer bytes.Buffer
|
||||
|
||||
err := pres.Present(&buffer)
|
||||
assert.NoError(t, err)
|
||||
actual := buffer.Bytes()
|
||||
|
||||
// replace the expected snapshot contents with the current presenter contents
|
||||
if updateSnapshot {
|
||||
testutils.UpdateGoldenFileContents(t, actual)
|
||||
}
|
||||
|
||||
var expected = testutils.GetGoldenFileContents(t)
|
||||
|
||||
// remove dynamic values, which should be tested independently
|
||||
for _, r := range redactors {
|
||||
actual = r(actual)
|
||||
expected = r(expected)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expected, actual) {
|
||||
dmp := diffmatchpatch.New()
|
||||
diffs := dmp.DiffMain(string(expected), string(actual), true)
|
||||
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
|
||||
}
|
||||
}
|
||||
|
||||
func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
||||
t.Helper()
|
||||
catalog := pkg.NewCatalog()
|
||||
img := imagetest.GetGoldenFixtureImage(t, testImage)
|
||||
|
||||
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
|
||||
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
|
||||
},
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-1",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Locations: []source.Location{
|
||||
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
|
||||
},
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
|
||||
// this is a hard coded value that is not given by the fixture helper and must be provided manually
|
||||
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
|
||||
|
||||
src, err := source.NewFromImage(img, "user-image-input")
|
||||
assert.NoError(t, err)
|
||||
|
||||
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return catalog, src.Metadata, &dist
|
||||
}
|
||||
|
||||
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
||||
catalog := pkg.NewCatalog()
|
||||
|
||||
// populate catalog with test data
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-1-id",
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
Type: pkg.PythonPkg,
|
||||
FoundBy: "the-cataloger-1",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
Language: pkg.Python,
|
||||
MetadataType: pkg.PythonPackageMetadataType,
|
||||
Licenses: []string{"MIT"},
|
||||
Metadata: pkg.PythonPackageMetadata{
|
||||
Name: "package-1",
|
||||
Version: "1.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
catalog.Add(pkg.Package{
|
||||
ID: "package-2-id",
|
||||
Name: "package-2",
|
||||
Version: "2.0.1",
|
||||
Type: pkg.DebPkg,
|
||||
FoundBy: "the-cataloger-2",
|
||||
Locations: []source.Location{
|
||||
{RealPath: "/some/path/pkg1"},
|
||||
},
|
||||
MetadataType: pkg.DpkgMetadataType,
|
||||
Metadata: pkg.DpkgMetadata{
|
||||
Package: "package-2",
|
||||
Version: "2.0.1",
|
||||
},
|
||||
PURL: "a-purl-2",
|
||||
CPEs: []pkg.CPE{
|
||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||
},
|
||||
})
|
||||
|
||||
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
|
||||
assert.NoError(t, err)
|
||||
|
||||
src, err := source.NewFromDirectory("/some/path")
|
||||
assert.NoError(t, err)
|
||||
|
||||
return catalog, src.Metadata, &dist
|
||||
}
|
||||
@@ -174,7 +174,8 @@
|
||||
"host": "",
|
||||
"path": "",
|
||||
"dockerfile": "",
|
||||
"overwrite-existing-image": false
|
||||
"overwrite-existing-image": false,
|
||||
"import-timeout": 0
|
||||
},
|
||||
"dev": {
|
||||
"profile-cpu": false,
|
||||
@@ -226,6 +227,7 @@
|
||||
},
|
||||
"registry": {
|
||||
"insecure-skip-tls-verify": false,
|
||||
"insecure-use-http": false,
|
||||
"auth": null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
|
||||
// This program generates license_list.go.
|
||||
const (
|
||||
source = "license_list.go"
|
||||
url = "https://spdx.org/licenses/licenses.json"
|
||||
)
|
||||
|
||||
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at {{ .Timestamp }}
|
||||
// using data from {{ .URL }}
|
||||
package spdxlicense
|
||||
|
||||
const Version = {{ printf "%q" .Version }}
|
||||
|
||||
var licenseIDs = map[string]string{
|
||||
{{- range $k, $v := .LicenseIDs }}
|
||||
{{ printf "%q" $k }}: {{ printf "%q" $v }},
|
||||
{{- end }}
|
||||
}
|
||||
`))
|
||||
|
||||
type LicenseList struct {
|
||||
Version string `json:"licenseListVersion"`
|
||||
Licenses []struct {
|
||||
ID string `json:"licenseId"`
|
||||
Name string `json:"name"`
|
||||
Text string `json:"licenseText"`
|
||||
Deprecated bool `json:"isDeprecatedLicenseId"`
|
||||
OSIApproved bool `json:"isOsiApproved"`
|
||||
SeeAlso []string `json:"seeAlso"`
|
||||
} `json:"licenses"`
|
||||
}
|
||||
|
||||
func main() {
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to get licenses list: %+v", err)
|
||||
}
|
||||
|
||||
var result LicenseList
|
||||
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
log.Fatalf("unable to decode license list: %+v", err)
|
||||
}
|
||||
|
||||
f, err := os.Create(source)
|
||||
if err != nil {
|
||||
log.Fatalf("unable to create %q: %+v", source, err)
|
||||
}
|
||||
defer func() {
|
||||
if err := f.Close(); err != nil {
|
||||
log.Fatalf("unable to close %q: %+v", source, err)
|
||||
}
|
||||
}()
|
||||
|
||||
var licenseIDs = make(map[string]string)
|
||||
for _, l := range result.Licenses {
|
||||
cleanID := strings.ToLower(l.ID)
|
||||
if _, exists := licenseIDs[cleanID]; exists {
|
||||
log.Fatalf("duplicate license ID found: %q", cleanID)
|
||||
}
|
||||
licenseIDs[cleanID] = l.ID
|
||||
}
|
||||
|
||||
err = tmp.Execute(f, struct {
|
||||
Timestamp time.Time
|
||||
URL string
|
||||
Version string
|
||||
LicenseIDs map[string]string
|
||||
}{
|
||||
Timestamp: time.Now(),
|
||||
URL: url,
|
||||
Version: result.Version,
|
||||
LicenseIDs: licenseIDs,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.Fatalf("unable to generate template: %+v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package spdxlicense
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:generate go run generate_license_list.go
|
||||
|
||||
func ID(id string) (string, bool) {
|
||||
value, exists := licenseIDs[strings.ToLower(id)]
|
||||
return value, exists
|
||||
}
|
||||
@@ -0,0 +1,483 @@
|
||||
// Code generated by go generate; DO NOT EDIT.
|
||||
// This file was generated by robots at 2021-08-09 15:08:22.639351 -0400 EDT m=+0.318960693
|
||||
// using data from https://spdx.org/licenses/licenses.json
|
||||
package spdxlicense
|
||||
|
||||
const Version = "3.14"
|
||||
|
||||
var licenseIDs = map[string]string{
|
||||
"0bsd": "0BSD",
|
||||
"aal": "AAL",
|
||||
"abstyles": "Abstyles",
|
||||
"adobe-2006": "Adobe-2006",
|
||||
"adobe-glyph": "Adobe-Glyph",
|
||||
"adsl": "ADSL",
|
||||
"afl-1.1": "AFL-1.1",
|
||||
"afl-1.2": "AFL-1.2",
|
||||
"afl-2.0": "AFL-2.0",
|
||||
"afl-2.1": "AFL-2.1",
|
||||
"afl-3.0": "AFL-3.0",
|
||||
"afmparse": "Afmparse",
|
||||
"agpl-1.0": "AGPL-1.0",
|
||||
"agpl-1.0-only": "AGPL-1.0-only",
|
||||
"agpl-1.0-or-later": "AGPL-1.0-or-later",
|
||||
"agpl-3.0": "AGPL-3.0",
|
||||
"agpl-3.0-only": "AGPL-3.0-only",
|
||||
"agpl-3.0-or-later": "AGPL-3.0-or-later",
|
||||
"aladdin": "Aladdin",
|
||||
"amdplpa": "AMDPLPA",
|
||||
"aml": "AML",
|
||||
"ampas": "AMPAS",
|
||||
"antlr-pd": "ANTLR-PD",
|
||||
"antlr-pd-fallback": "ANTLR-PD-fallback",
|
||||
"apache-1.0": "Apache-1.0",
|
||||
"apache-1.1": "Apache-1.1",
|
||||
"apache-2.0": "Apache-2.0",
|
||||
"apafml": "APAFML",
|
||||
"apl-1.0": "APL-1.0",
|
||||
"apsl-1.0": "APSL-1.0",
|
||||
"apsl-1.1": "APSL-1.1",
|
||||
"apsl-1.2": "APSL-1.2",
|
||||
"apsl-2.0": "APSL-2.0",
|
||||
"artistic-1.0": "Artistic-1.0",
|
||||
"artistic-1.0-cl8": "Artistic-1.0-cl8",
|
||||
"artistic-1.0-perl": "Artistic-1.0-Perl",
|
||||
"artistic-2.0": "Artistic-2.0",
|
||||
"bahyph": "Bahyph",
|
||||
"barr": "Barr",
|
||||
"beerware": "Beerware",
|
||||
"bittorrent-1.0": "BitTorrent-1.0",
|
||||
"bittorrent-1.1": "BitTorrent-1.1",
|
||||
"blessing": "blessing",
|
||||
"blueoak-1.0.0": "BlueOak-1.0.0",
|
||||
"borceux": "Borceux",
|
||||
"bsd-1-clause": "BSD-1-Clause",
|
||||
"bsd-2-clause": "BSD-2-Clause",
|
||||
"bsd-2-clause-freebsd": "BSD-2-Clause-FreeBSD",
|
||||
"bsd-2-clause-netbsd": "BSD-2-Clause-NetBSD",
|
||||
"bsd-2-clause-patent": "BSD-2-Clause-Patent",
|
||||
"bsd-2-clause-views": "BSD-2-Clause-Views",
|
||||
"bsd-3-clause": "BSD-3-Clause",
|
||||
"bsd-3-clause-attribution": "BSD-3-Clause-Attribution",
|
||||
"bsd-3-clause-clear": "BSD-3-Clause-Clear",
|
||||
"bsd-3-clause-lbnl": "BSD-3-Clause-LBNL",
|
||||
"bsd-3-clause-modification": "BSD-3-Clause-Modification",
|
||||
"bsd-3-clause-no-military-license": "BSD-3-Clause-No-Military-License",
|
||||
"bsd-3-clause-no-nuclear-license": "BSD-3-Clause-No-Nuclear-License",
|
||||
"bsd-3-clause-no-nuclear-license-2014": "BSD-3-Clause-No-Nuclear-License-2014",
|
||||
"bsd-3-clause-no-nuclear-warranty": "BSD-3-Clause-No-Nuclear-Warranty",
|
||||
"bsd-3-clause-open-mpi": "BSD-3-Clause-Open-MPI",
|
||||
"bsd-4-clause": "BSD-4-Clause",
|
||||
"bsd-4-clause-shortened": "BSD-4-Clause-Shortened",
|
||||
"bsd-4-clause-uc": "BSD-4-Clause-UC",
|
||||
"bsd-protection": "BSD-Protection",
|
||||
"bsd-source-code": "BSD-Source-Code",
|
||||
"bsl-1.0": "BSL-1.0",
|
||||
"busl-1.1": "BUSL-1.1",
|
||||
"bzip2-1.0.5": "bzip2-1.0.5",
|
||||
"bzip2-1.0.6": "bzip2-1.0.6",
|
||||
"c-uda-1.0": "C-UDA-1.0",
|
||||
"cal-1.0": "CAL-1.0",
|
||||
"cal-1.0-combined-work-exception": "CAL-1.0-Combined-Work-Exception",
|
||||
"caldera": "Caldera",
|
||||
"catosl-1.1": "CATOSL-1.1",
|
||||
"cc-by-1.0": "CC-BY-1.0",
|
||||
"cc-by-2.0": "CC-BY-2.0",
|
||||
"cc-by-2.5": "CC-BY-2.5",
|
||||
"cc-by-2.5-au": "CC-BY-2.5-AU",
|
||||
"cc-by-3.0": "CC-BY-3.0",
|
||||
"cc-by-3.0-at": "CC-BY-3.0-AT",
|
||||
"cc-by-3.0-de": "CC-BY-3.0-DE",
|
||||
"cc-by-3.0-nl": "CC-BY-3.0-NL",
|
||||
"cc-by-3.0-us": "CC-BY-3.0-US",
|
||||
"cc-by-4.0": "CC-BY-4.0",
|
||||
"cc-by-nc-1.0": "CC-BY-NC-1.0",
|
||||
"cc-by-nc-2.0": "CC-BY-NC-2.0",
|
||||
"cc-by-nc-2.5": "CC-BY-NC-2.5",
|
||||
"cc-by-nc-3.0": "CC-BY-NC-3.0",
|
||||
"cc-by-nc-3.0-de": "CC-BY-NC-3.0-DE",
|
||||
"cc-by-nc-4.0": "CC-BY-NC-4.0",
|
||||
"cc-by-nc-nd-1.0": "CC-BY-NC-ND-1.0",
|
||||
"cc-by-nc-nd-2.0": "CC-BY-NC-ND-2.0",
|
||||
"cc-by-nc-nd-2.5": "CC-BY-NC-ND-2.5",
|
||||
"cc-by-nc-nd-3.0": "CC-BY-NC-ND-3.0",
|
||||
"cc-by-nc-nd-3.0-de": "CC-BY-NC-ND-3.0-DE",
|
||||
"cc-by-nc-nd-3.0-igo": "CC-BY-NC-ND-3.0-IGO",
|
||||
"cc-by-nc-nd-4.0": "CC-BY-NC-ND-4.0",
|
||||
"cc-by-nc-sa-1.0": "CC-BY-NC-SA-1.0",
|
||||
"cc-by-nc-sa-2.0": "CC-BY-NC-SA-2.0",
|
||||
"cc-by-nc-sa-2.0-fr": "CC-BY-NC-SA-2.0-FR",
|
||||
"cc-by-nc-sa-2.0-uk": "CC-BY-NC-SA-2.0-UK",
|
||||
"cc-by-nc-sa-2.5": "CC-BY-NC-SA-2.5",
|
||||
"cc-by-nc-sa-3.0": "CC-BY-NC-SA-3.0",
|
||||
"cc-by-nc-sa-3.0-de": "CC-BY-NC-SA-3.0-DE",
|
||||
"cc-by-nc-sa-3.0-igo": "CC-BY-NC-SA-3.0-IGO",
|
||||
"cc-by-nc-sa-4.0": "CC-BY-NC-SA-4.0",
|
||||
"cc-by-nd-1.0": "CC-BY-ND-1.0",
|
||||
"cc-by-nd-2.0": "CC-BY-ND-2.0",
|
||||
"cc-by-nd-2.5": "CC-BY-ND-2.5",
|
||||
"cc-by-nd-3.0": "CC-BY-ND-3.0",
|
||||
"cc-by-nd-3.0-de": "CC-BY-ND-3.0-DE",
|
||||
"cc-by-nd-4.0": "CC-BY-ND-4.0",
|
||||
"cc-by-sa-1.0": "CC-BY-SA-1.0",
|
||||
"cc-by-sa-2.0": "CC-BY-SA-2.0",
|
||||
"cc-by-sa-2.0-uk": "CC-BY-SA-2.0-UK",
|
||||
"cc-by-sa-2.1-jp": "CC-BY-SA-2.1-JP",
|
||||
"cc-by-sa-2.5": "CC-BY-SA-2.5",
|
||||
"cc-by-sa-3.0": "CC-BY-SA-3.0",
|
||||
"cc-by-sa-3.0-at": "CC-BY-SA-3.0-AT",
|
||||
"cc-by-sa-3.0-de": "CC-BY-SA-3.0-DE",
|
||||
"cc-by-sa-4.0": "CC-BY-SA-4.0",
|
||||
"cc-pddc": "CC-PDDC",
|
||||
"cc0-1.0": "CC0-1.0",
|
||||
"cddl-1.0": "CDDL-1.0",
|
||||
"cddl-1.1": "CDDL-1.1",
|
||||
"cdl-1.0": "CDL-1.0",
|
||||
"cdla-permissive-1.0": "CDLA-Permissive-1.0",
|
||||
"cdla-permissive-2.0": "CDLA-Permissive-2.0",
|
||||
"cdla-sharing-1.0": "CDLA-Sharing-1.0",
|
||||
"cecill-1.0": "CECILL-1.0",
|
||||
"cecill-1.1": "CECILL-1.1",
|
||||
"cecill-2.0": "CECILL-2.0",
|
||||
"cecill-2.1": "CECILL-2.1",
|
||||
"cecill-b": "CECILL-B",
|
||||
"cecill-c": "CECILL-C",
|
||||
"cern-ohl-1.1": "CERN-OHL-1.1",
|
||||
"cern-ohl-1.2": "CERN-OHL-1.2",
|
||||
"cern-ohl-p-2.0": "CERN-OHL-P-2.0",
|
||||
"cern-ohl-s-2.0": "CERN-OHL-S-2.0",
|
||||
"cern-ohl-w-2.0": "CERN-OHL-W-2.0",
|
||||
"clartistic": "ClArtistic",
|
||||
"cnri-jython": "CNRI-Jython",
|
||||
"cnri-python": "CNRI-Python",
|
||||
"cnri-python-gpl-compatible": "CNRI-Python-GPL-Compatible",
|
||||
"condor-1.1": "Condor-1.1",
|
||||
"copyleft-next-0.3.0": "copyleft-next-0.3.0",
|
||||
"copyleft-next-0.3.1": "copyleft-next-0.3.1",
|
||||
"cpal-1.0": "CPAL-1.0",
|
||||
"cpl-1.0": "CPL-1.0",
|
||||
"cpol-1.02": "CPOL-1.02",
|
||||
"crossword": "Crossword",
|
||||
"crystalstacker": "CrystalStacker",
|
||||
"cua-opl-1.0": "CUA-OPL-1.0",
|
||||
"cube": "Cube",
|
||||
"curl": "curl",
|
||||
"d-fsl-1.0": "D-FSL-1.0",
|
||||
"diffmark": "diffmark",
|
||||
"doc": "DOC",
|
||||
"dotseqn": "Dotseqn",
|
||||
"drl-1.0": "DRL-1.0",
|
||||
"dsdp": "DSDP",
|
||||
"dvipdfm": "dvipdfm",
|
||||
"ecl-1.0": "ECL-1.0",
|
||||
"ecl-2.0": "ECL-2.0",
|
||||
"ecos-2.0": "eCos-2.0",
|
||||
"efl-1.0": "EFL-1.0",
|
||||
"efl-2.0": "EFL-2.0",
|
||||
"egenix": "eGenix",
|
||||
"entessa": "Entessa",
|
||||
"epics": "EPICS",
|
||||
"epl-1.0": "EPL-1.0",
|
||||
"epl-2.0": "EPL-2.0",
|
||||
"erlpl-1.1": "ErlPL-1.1",
|
||||
"etalab-2.0": "etalab-2.0",
|
||||
"eudatagrid": "EUDatagrid",
|
||||
"eupl-1.0": "EUPL-1.0",
|
||||
"eupl-1.1": "EUPL-1.1",
|
||||
"eupl-1.2": "EUPL-1.2",
|
||||
"eurosym": "Eurosym",
|
||||
"fair": "Fair",
|
||||
"frameworx-1.0": "Frameworx-1.0",
|
||||
"freebsd-doc": "FreeBSD-DOC",
|
||||
"freeimage": "FreeImage",
|
||||
"fsfap": "FSFAP",
|
||||
"fsful": "FSFUL",
|
||||
"fsfullr": "FSFULLR",
|
||||
"ftl": "FTL",
|
||||
"gd": "GD",
|
||||
"gfdl-1.1": "GFDL-1.1",
|
||||
"gfdl-1.1-invariants-only": "GFDL-1.1-invariants-only",
|
||||
"gfdl-1.1-invariants-or-later": "GFDL-1.1-invariants-or-later",
|
||||
"gfdl-1.1-no-invariants-only": "GFDL-1.1-no-invariants-only",
|
||||
"gfdl-1.1-no-invariants-or-later": "GFDL-1.1-no-invariants-or-later",
|
||||
"gfdl-1.1-only": "GFDL-1.1-only",
|
||||
"gfdl-1.1-or-later": "GFDL-1.1-or-later",
|
||||
"gfdl-1.2": "GFDL-1.2",
|
||||
"gfdl-1.2-invariants-only": "GFDL-1.2-invariants-only",
|
||||
"gfdl-1.2-invariants-or-later": "GFDL-1.2-invariants-or-later",
|
||||
"gfdl-1.2-no-invariants-only": "GFDL-1.2-no-invariants-only",
|
||||
"gfdl-1.2-no-invariants-or-later": "GFDL-1.2-no-invariants-or-later",
|
||||
"gfdl-1.2-only": "GFDL-1.2-only",
|
||||
"gfdl-1.2-or-later": "GFDL-1.2-or-later",
|
||||
"gfdl-1.3": "GFDL-1.3",
|
||||
"gfdl-1.3-invariants-only": "GFDL-1.3-invariants-only",
|
||||
"gfdl-1.3-invariants-or-later": "GFDL-1.3-invariants-or-later",
|
||||
"gfdl-1.3-no-invariants-only": "GFDL-1.3-no-invariants-only",
|
||||
"gfdl-1.3-no-invariants-or-later": "GFDL-1.3-no-invariants-or-later",
|
||||
"gfdl-1.3-only": "GFDL-1.3-only",
|
||||
"gfdl-1.3-or-later": "GFDL-1.3-or-later",
|
||||
"giftware": "Giftware",
|
||||
"gl2ps": "GL2PS",
|
||||
"glide": "Glide",
|
||||
"glulxe": "Glulxe",
|
||||
"glwtpl": "GLWTPL",
|
||||
"gnuplot": "gnuplot",
|
||||
"gpl-1.0": "GPL-1.0",
|
||||
"gpl-1.0+": "GPL-1.0+",
|
||||
"gpl-1.0-only": "GPL-1.0-only",
|
||||
"gpl-1.0-or-later": "GPL-1.0-or-later",
|
||||
"gpl-2.0": "GPL-2.0",
|
||||
"gpl-2.0+": "GPL-2.0+",
|
||||
"gpl-2.0-only": "GPL-2.0-only",
|
||||
"gpl-2.0-or-later": "GPL-2.0-or-later",
|
||||
"gpl-2.0-with-autoconf-exception": "GPL-2.0-with-autoconf-exception",
|
||||
"gpl-2.0-with-bison-exception": "GPL-2.0-with-bison-exception",
|
||||
"gpl-2.0-with-classpath-exception": "GPL-2.0-with-classpath-exception",
|
||||
"gpl-2.0-with-font-exception": "GPL-2.0-with-font-exception",
|
||||
"gpl-2.0-with-gcc-exception": "GPL-2.0-with-GCC-exception",
|
||||
"gpl-3.0": "GPL-3.0",
|
||||
"gpl-3.0+": "GPL-3.0+",
|
||||
"gpl-3.0-only": "GPL-3.0-only",
|
||||
"gpl-3.0-or-later": "GPL-3.0-or-later",
|
||||
"gpl-3.0-with-autoconf-exception": "GPL-3.0-with-autoconf-exception",
|
||||
"gpl-3.0-with-gcc-exception": "GPL-3.0-with-GCC-exception",
|
||||
"gsoap-1.3b": "gSOAP-1.3b",
|
||||
"haskellreport": "HaskellReport",
|
||||
"hippocratic-2.1": "Hippocratic-2.1",
|
||||
"hpnd": "HPND",
|
||||
"hpnd-sell-variant": "HPND-sell-variant",
|
||||
"htmltidy": "HTMLTIDY",
|
||||
"ibm-pibs": "IBM-pibs",
|
||||
"icu": "ICU",
|
||||
"ijg": "IJG",
|
||||
"imagemagick": "ImageMagick",
|
||||
"imatix": "iMatix",
|
||||
"imlib2": "Imlib2",
|
||||
"info-zip": "Info-ZIP",
|
||||
"intel": "Intel",
|
||||
"intel-acpi": "Intel-ACPI",
|
||||
"interbase-1.0": "Interbase-1.0",
|
||||
"ipa": "IPA",
|
||||
"ipl-1.0": "IPL-1.0",
|
||||
"isc": "ISC",
|
||||
"jasper-2.0": "JasPer-2.0",
|
||||
"jpnic": "JPNIC",
|
||||
"json": "JSON",
|
||||
"lal-1.2": "LAL-1.2",
|
||||
"lal-1.3": "LAL-1.3",
|
||||
"latex2e": "Latex2e",
|
||||
"leptonica": "Leptonica",
|
||||
"lgpl-2.0": "LGPL-2.0",
|
||||
"lgpl-2.0+": "LGPL-2.0+",
|
||||
"lgpl-2.0-only": "LGPL-2.0-only",
|
||||
"lgpl-2.0-or-later": "LGPL-2.0-or-later",
|
||||
"lgpl-2.1": "LGPL-2.1",
|
||||
"lgpl-2.1+": "LGPL-2.1+",
|
||||
"lgpl-2.1-only": "LGPL-2.1-only",
|
||||
"lgpl-2.1-or-later": "LGPL-2.1-or-later",
|
||||
"lgpl-3.0": "LGPL-3.0",
|
||||
"lgpl-3.0+": "LGPL-3.0+",
|
||||
"lgpl-3.0-only": "LGPL-3.0-only",
|
||||
"lgpl-3.0-or-later": "LGPL-3.0-or-later",
|
||||
"lgpllr": "LGPLLR",
|
||||
"libpng": "Libpng",
|
||||
"libpng-2.0": "libpng-2.0",
|
||||
"libselinux-1.0": "libselinux-1.0",
|
||||
"libtiff": "libtiff",
|
||||
"liliq-p-1.1": "LiLiQ-P-1.1",
|
||||
"liliq-r-1.1": "LiLiQ-R-1.1",
|
||||
"liliq-rplus-1.1": "LiLiQ-Rplus-1.1",
|
||||
"linux-openib": "Linux-OpenIB",
|
||||
"lpl-1.0": "LPL-1.0",
|
||||
"lpl-1.02": "LPL-1.02",
|
||||
"lppl-1.0": "LPPL-1.0",
|
||||
"lppl-1.1": "LPPL-1.1",
|
||||
"lppl-1.2": "LPPL-1.2",
|
||||
"lppl-1.3a": "LPPL-1.3a",
|
||||
"lppl-1.3c": "LPPL-1.3c",
|
||||
"makeindex": "MakeIndex",
|
||||
"miros": "MirOS",
|
||||
"mit": "MIT",
|
||||
"mit-0": "MIT-0",
|
||||
"mit-advertising": "MIT-advertising",
|
||||
"mit-cmu": "MIT-CMU",
|
||||
"mit-enna": "MIT-enna",
|
||||
"mit-feh": "MIT-feh",
|
||||
"mit-modern-variant": "MIT-Modern-Variant",
|
||||
"mit-open-group": "MIT-open-group",
|
||||
"mitnfa": "MITNFA",
|
||||
"motosoto": "Motosoto",
|
||||
"mpich2": "mpich2",
|
||||
"mpl-1.0": "MPL-1.0",
|
||||
"mpl-1.1": "MPL-1.1",
|
||||
"mpl-2.0": "MPL-2.0",
|
||||
"mpl-2.0-no-copyleft-exception": "MPL-2.0-no-copyleft-exception",
|
||||
"ms-pl": "MS-PL",
|
||||
"ms-rl": "MS-RL",
|
||||
"mtll": "MTLL",
|
||||
"mulanpsl-1.0": "MulanPSL-1.0",
|
||||
"mulanpsl-2.0": "MulanPSL-2.0",
|
||||
"multics": "Multics",
|
||||
"mup": "Mup",
|
||||
"naist-2003": "NAIST-2003",
|
||||
"nasa-1.3": "NASA-1.3",
|
||||
"naumen": "Naumen",
|
||||
"nbpl-1.0": "NBPL-1.0",
|
||||
"ncgl-uk-2.0": "NCGL-UK-2.0",
|
||||
"ncsa": "NCSA",
|
||||
"net-snmp": "Net-SNMP",
|
||||
"netcdf": "NetCDF",
|
||||
"newsletr": "Newsletr",
|
||||
"ngpl": "NGPL",
|
||||
"nist-pd": "NIST-PD",
|
||||
"nist-pd-fallback": "NIST-PD-fallback",
|
||||
"nlod-1.0": "NLOD-1.0",
|
||||
"nlod-2.0": "NLOD-2.0",
|
||||
"nlpl": "NLPL",
|
||||
"nokia": "Nokia",
|
||||
"nosl": "NOSL",
|
||||
"noweb": "Noweb",
|
||||
"npl-1.0": "NPL-1.0",
|
||||
"npl-1.1": "NPL-1.1",
|
||||
"nposl-3.0": "NPOSL-3.0",
|
||||
"nrl": "NRL",
|
||||
"ntp": "NTP",
|
||||
"ntp-0": "NTP-0",
|
||||
"nunit": "Nunit",
|
||||
"o-uda-1.0": "O-UDA-1.0",
|
||||
"occt-pl": "OCCT-PL",
|
||||
"oclc-2.0": "OCLC-2.0",
|
||||
"odbl-1.0": "ODbL-1.0",
|
||||
"odc-by-1.0": "ODC-By-1.0",
|
||||
"ofl-1.0": "OFL-1.0",
|
||||
"ofl-1.0-no-rfn": "OFL-1.0-no-RFN",
|
||||
"ofl-1.0-rfn": "OFL-1.0-RFN",
|
||||
"ofl-1.1": "OFL-1.1",
|
||||
"ofl-1.1-no-rfn": "OFL-1.1-no-RFN",
|
||||
"ofl-1.1-rfn": "OFL-1.1-RFN",
|
||||
"ogc-1.0": "OGC-1.0",
|
||||
"ogdl-taiwan-1.0": "OGDL-Taiwan-1.0",
|
||||
"ogl-canada-2.0": "OGL-Canada-2.0",
|
||||
"ogl-uk-1.0": "OGL-UK-1.0",
|
||||
"ogl-uk-2.0": "OGL-UK-2.0",
|
||||
"ogl-uk-3.0": "OGL-UK-3.0",
|
||||
"ogtsl": "OGTSL",
|
||||
"oldap-1.1": "OLDAP-1.1",
|
||||
"oldap-1.2": "OLDAP-1.2",
|
||||
"oldap-1.3": "OLDAP-1.3",
|
||||
"oldap-1.4": "OLDAP-1.4",
|
||||
"oldap-2.0": "OLDAP-2.0",
|
||||
"oldap-2.0.1": "OLDAP-2.0.1",
|
||||
"oldap-2.1": "OLDAP-2.1",
|
||||
"oldap-2.2": "OLDAP-2.2",
|
||||
"oldap-2.2.1": "OLDAP-2.2.1",
|
||||
"oldap-2.2.2": "OLDAP-2.2.2",
|
||||
"oldap-2.3": "OLDAP-2.3",
|
||||
"oldap-2.4": "OLDAP-2.4",
|
||||
"oldap-2.5": "OLDAP-2.5",
|
||||
"oldap-2.6": "OLDAP-2.6",
|
||||
"oldap-2.7": "OLDAP-2.7",
|
||||
"oldap-2.8": "OLDAP-2.8",
|
||||
"oml": "OML",
|
||||
"openssl": "OpenSSL",
|
||||
"opl-1.0": "OPL-1.0",
|
||||
"opubl-1.0": "OPUBL-1.0",
|
||||
"oset-pl-2.1": "OSET-PL-2.1",
|
||||
"osl-1.0": "OSL-1.0",
|
||||
"osl-1.1": "OSL-1.1",
|
||||
"osl-2.0": "OSL-2.0",
|
||||
"osl-2.1": "OSL-2.1",
|
||||
"osl-3.0": "OSL-3.0",
|
||||
"parity-6.0.0": "Parity-6.0.0",
|
||||
"parity-7.0.0": "Parity-7.0.0",
|
||||
"pddl-1.0": "PDDL-1.0",
|
||||
"php-3.0": "PHP-3.0",
|
||||
"php-3.01": "PHP-3.01",
|
||||
"plexus": "Plexus",
|
||||
"polyform-noncommercial-1.0.0": "PolyForm-Noncommercial-1.0.0",
|
||||
"polyform-small-business-1.0.0": "PolyForm-Small-Business-1.0.0",
|
||||
"postgresql": "PostgreSQL",
|
||||
"psf-2.0": "PSF-2.0",
|
||||
"psfrag": "psfrag",
|
||||
"psutils": "psutils",
|
||||
"python-2.0": "Python-2.0",
|
||||
"qhull": "Qhull",
|
||||
"qpl-1.0": "QPL-1.0",
|
||||
"rdisc": "Rdisc",
|
||||
"rhecos-1.1": "RHeCos-1.1",
|
||||
"rpl-1.1": "RPL-1.1",
|
||||
"rpl-1.5": "RPL-1.5",
|
||||
"rpsl-1.0": "RPSL-1.0",
|
||||
"rsa-md": "RSA-MD",
|
||||
"rscpl": "RSCPL",
|
||||
"ruby": "Ruby",
|
||||
"sax-pd": "SAX-PD",
|
||||
"saxpath": "Saxpath",
|
||||
"scea": "SCEA",
|
||||
"sendmail": "Sendmail",
|
||||
"sendmail-8.23": "Sendmail-8.23",
|
||||
"sgi-b-1.0": "SGI-B-1.0",
|
||||
"sgi-b-1.1": "SGI-B-1.1",
|
||||
"sgi-b-2.0": "SGI-B-2.0",
|
||||
"shl-0.5": "SHL-0.5",
|
||||
"shl-0.51": "SHL-0.51",
|
||||
"simpl-2.0": "SimPL-2.0",
|
||||
"sissl": "SISSL",
|
||||
"sissl-1.2": "SISSL-1.2",
|
||||
"sleepycat": "Sleepycat",
|
||||
"smlnj": "SMLNJ",
|
||||
"smppl": "SMPPL",
|
||||
"snia": "SNIA",
|
||||
"spencer-86": "Spencer-86",
|
||||
"spencer-94": "Spencer-94",
|
||||
"spencer-99": "Spencer-99",
|
||||
"spl-1.0": "SPL-1.0",
|
||||
"ssh-openssh": "SSH-OpenSSH",
|
||||
"ssh-short": "SSH-short",
|
||||
"sspl-1.0": "SSPL-1.0",
|
||||
"standardml-nj": "StandardML-NJ",
|
||||
"sugarcrm-1.1.3": "SugarCRM-1.1.3",
|
||||
"swl": "SWL",
|
||||
"tapr-ohl-1.0": "TAPR-OHL-1.0",
|
||||
"tcl": "TCL",
|
||||
"tcp-wrappers": "TCP-wrappers",
|
||||
"tmate": "TMate",
|
||||
"torque-1.1": "TORQUE-1.1",
|
||||
"tosl": "TOSL",
|
||||
"tu-berlin-1.0": "TU-Berlin-1.0",
|
||||
"tu-berlin-2.0": "TU-Berlin-2.0",
|
||||
"ucl-1.0": "UCL-1.0",
|
||||
"unicode-dfs-2015": "Unicode-DFS-2015",
|
||||
"unicode-dfs-2016": "Unicode-DFS-2016",
|
||||
"unicode-tou": "Unicode-TOU",
|
||||
"unlicense": "Unlicense",
|
||||
"upl-1.0": "UPL-1.0",
|
||||
"vim": "Vim",
|
||||
"vostrom": "VOSTROM",
|
||||
"vsl-1.0": "VSL-1.0",
|
||||
"w3c": "W3C",
|
||||
"w3c-19980720": "W3C-19980720",
|
||||
"w3c-20150513": "W3C-20150513",
|
||||
"watcom-1.0": "Watcom-1.0",
|
||||
"wsuipa": "Wsuipa",
|
||||
"wtfpl": "WTFPL",
|
||||
"wxwindows": "wxWindows",
|
||||
"x11": "X11",
|
||||
"xerox": "Xerox",
|
||||
"xfree86-1.1": "XFree86-1.1",
|
||||
"xinetd": "xinetd",
|
||||
"xnet": "Xnet",
|
||||
"xpp": "xpp",
|
||||
"xskat": "XSkat",
|
||||
"ypl-1.0": "YPL-1.0",
|
||||
"ypl-1.1": "YPL-1.1",
|
||||
"zed": "Zed",
|
||||
"zend-2.0": "Zend-2.0",
|
||||
"zimbra-1.3": "Zimbra-1.3",
|
||||
"zimbra-1.4": "Zimbra-1.4",
|
||||
"zlib": "Zlib",
|
||||
"zlib-acknowledgement": "zlib-acknowledgement",
|
||||
"zpl-1.1": "ZPL-1.1",
|
||||
"zpl-2.0": "ZPL-2.0",
|
||||
"zpl-2.1": "ZPL-2.1",
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package spdxlicense
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestLicenceListIDs(t *testing.T) {
|
||||
// do a sanity check on the generated data
|
||||
assert.Equal(t, "0BSD", licenseIDs["0bsd"])
|
||||
assert.Equal(t, "ZPL-2.1", licenseIDs["zpl-2.1"])
|
||||
assert.NotEmpty(t, Version)
|
||||
}
|
||||
@@ -12,3 +12,10 @@ func HasAnyOfPrefixes(input string, prefixes ...string) bool {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func TruncateMiddleEllipsis(input string, maxLen int) string {
|
||||
if len(input) <= maxLen {
|
||||
return input
|
||||
}
|
||||
return input[:maxLen/2] + "..." + input[len(input)-(maxLen/2):]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package internal
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -63,3 +64,43 @@ func TestHasAnyOfPrefixes(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTruncateMiddleEllipsis(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
len int
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
input: "nobody expects the spanish inquisition",
|
||||
len: 39,
|
||||
expected: "nobody expects the spanish inquisition",
|
||||
},
|
||||
{
|
||||
input: "nobody expects the spanish inquisition",
|
||||
len: 30,
|
||||
expected: "nobody expects ...ish inquisition",
|
||||
},
|
||||
{
|
||||
input: "nobody expects the spanish inquisition",
|
||||
len: 38,
|
||||
expected: "nobody expects the spanish inquisition",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
len: 30,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
input: "",
|
||||
len: 0,
|
||||
expected: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.input+":"+strconv.Itoa(test.len), func(t *testing.T) {
|
||||
assert.Equal(t, test.expected, TruncateMiddleEllipsis(test.input, test.len))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// CatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
||||
// via the given presenter to stdout.
|
||||
func CatalogerPresenterReady(event partybus.Event) error {
|
||||
// show the report to stdout
|
||||
pres, err := syftEventParsers.ParsePresenterReady(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||
}
|
||||
|
||||
if err := pres.Present(os.Stdout); err != nil {
|
||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TODO: move me to a common module (used in multiple repos)
|
||||
|
||||
const (
|
||||
SpinnerCircleOutlineSet = "◜◠◯◎◉●◉◎◯◡◞"
|
||||
SpinnerCircleSet = "◌◯◎◉●◉◎◯"
|
||||
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
SpinnerHorizontalBarSet = "▉▊▋▌▍▎▏▎▍▌▋▊▉"
|
||||
SpinnerVerticalBarSet = "▁▃▄▅▆▇█▇▆▅▄▃▁"
|
||||
SpinnerDoubleBarSet = "▁▂▃▄▅▆▇█▉▊▋▌▍▎▏▏▎▍▌▋▊▉█▇▆▅▄▃▂▁"
|
||||
SpinnerArrowSet = "←↖↑↗→↘↓↙"
|
||||
)
|
||||
|
||||
var SpinnerCircleDotSet = []string{
|
||||
"⠈⠁",
|
||||
"⠈⠑",
|
||||
"⠈⠱",
|
||||
"⠈⡱",
|
||||
"⢀⡱",
|
||||
"⢄⡱",
|
||||
"⢄⡱",
|
||||
"⢆⡱",
|
||||
"⢎⡱",
|
||||
"⢎⡰",
|
||||
"⢎⡠",
|
||||
"⢎⡀",
|
||||
"⢎⠁",
|
||||
"⠎⠁",
|
||||
"⠊⠁",
|
||||
}
|
||||
|
||||
type Spinner struct {
|
||||
index int
|
||||
charset []string
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewSpinner(charset string) Spinner {
|
||||
return Spinner{
|
||||
charset: strings.Split(charset, ""),
|
||||
}
|
||||
}
|
||||
|
||||
func NewSpinnerFromSlice(charset []string) Spinner {
|
||||
return Spinner{
|
||||
charset: charset,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Spinner) Current() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.charset[s.index]
|
||||
}
|
||||
|
||||
func (s *Spinner) Next() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
c := s.charset[s.index]
|
||||
s.index++
|
||||
if s.index >= len(s.charset) {
|
||||
s.index = 0
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// TODO: move me to a common module (used in multiple repos)
|
||||
|
||||
const (
|
||||
SpinnerDotSet = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏"
|
||||
)
|
||||
|
||||
type Spinner struct {
|
||||
index int
|
||||
charset []string
|
||||
lock sync.Mutex
|
||||
}
|
||||
|
||||
func NewSpinner(charset string) Spinner {
|
||||
return Spinner{
|
||||
charset: strings.Split(charset, ""),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Spinner) Current() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
|
||||
return s.charset[s.index]
|
||||
}
|
||||
|
||||
func (s *Spinner) Next() string {
|
||||
s.lock.Lock()
|
||||
defer s.lock.Unlock()
|
||||
c := s.charset[s.index]
|
||||
s.index++
|
||||
if s.index >= len(s.charset) {
|
||||
s.index = 0
|
||||
}
|
||||
return c
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/anchore/syft/ui"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/jotframe/pkg/frame"
|
||||
)
|
||||
|
||||
// ephemeralTerminalUI provides an "ephemeral" terminal user interface to display the application state dynamically.
|
||||
// The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line
|
||||
// UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen
|
||||
// must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make
|
||||
// a shared state, bytes coming from elsewhere to the screen will disrupt this state.
|
||||
//
|
||||
// This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a
|
||||
// published element on the event bus, typically polling the element for the latest state. This allows for the UI to
|
||||
// control update frequency to the screen, provide "liveness" indications that are interpolated between bus events,
|
||||
// and overall loosely couple the bus events from screen interactions.
|
||||
//
|
||||
// By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should
|
||||
// attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by
|
||||
// convention, each new event that the UI should respond to should be added either in this package as a handler function,
|
||||
// or in the shared ui package as a function on the main handler object. All handler functions should be completed
|
||||
// processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
|
||||
type ephemeralTerminalUI struct {
|
||||
unsubscribe func() error
|
||||
handler *ui.Handler
|
||||
waitGroup *sync.WaitGroup
|
||||
frame *frame.Frame
|
||||
logBuffer *bytes.Buffer
|
||||
output *os.File
|
||||
}
|
||||
|
||||
func NewEphemeralTerminalUI() UI {
|
||||
return &ephemeralTerminalUI{
|
||||
handler: ui.NewHandler(),
|
||||
waitGroup: &sync.WaitGroup{},
|
||||
output: os.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) Setup(unsubscribe func() error) error {
|
||||
h.unsubscribe = unsubscribe
|
||||
hideCursor(h.output)
|
||||
|
||||
// prep the logger to not clobber the screen from now on (logrus only)
|
||||
h.logBuffer = bytes.NewBufferString("")
|
||||
logWrapper, ok := log.Log.(*logger.LogrusLogger)
|
||||
if ok {
|
||||
logWrapper.Logger.SetOutput(h.logBuffer)
|
||||
}
|
||||
|
||||
return h.openScreen()
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) Handle(event partybus.Event) error {
|
||||
ctx := context.Background()
|
||||
switch {
|
||||
case h.handler.RespondsTo(event):
|
||||
if err := h.handler.Handle(ctx, h.frame, event, h.waitGroup); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
case event.Type == syftEvent.AppUpdateAvailable:
|
||||
if err := handleAppUpdateAvailable(ctx, h.frame, event, h.waitGroup); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
case event.Type == syftEvent.PresenterReady:
|
||||
// we need to close the screen now since signaling the the presenter is ready means that we
|
||||
// are about to write bytes to stdout, so we should reset the terminal state first
|
||||
h.closeScreen(false)
|
||||
|
||||
if err := handleCatalogerPresenterReady(event); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", event.Type, err)
|
||||
}
|
||||
|
||||
// this is the last expected event, stop listening to events
|
||||
return h.unsubscribe()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) openScreen() error {
|
||||
config := frame.Config{
|
||||
PositionPolicy: frame.PolicyFloatForward,
|
||||
// only report output to stderr, reserve report output for stdout
|
||||
Output: h.output,
|
||||
}
|
||||
|
||||
fr, err := frame.New(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create the screen object: %w", err)
|
||||
}
|
||||
h.frame = fr
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) closeScreen(force bool) {
|
||||
// we may have other background processes still displaying progress, wait for them to
|
||||
// finish before discontinuing dynamic content and showing the final report
|
||||
if !h.frame.IsClosed() {
|
||||
if !force {
|
||||
h.waitGroup.Wait()
|
||||
}
|
||||
h.frame.Close()
|
||||
// TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output
|
||||
frame.Close()
|
||||
|
||||
// only flush the log on close
|
||||
h.flushLog()
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) flushLog() {
|
||||
// flush any errors to the screen before the report
|
||||
logWrapper, ok := log.Log.(*logger.LogrusLogger)
|
||||
if ok {
|
||||
fmt.Fprint(logWrapper.Output, h.logBuffer.String())
|
||||
logWrapper.Logger.SetOutput(h.output)
|
||||
} else {
|
||||
fmt.Fprint(h.output, h.logBuffer.String())
|
||||
}
|
||||
}
|
||||
|
||||
func (h *ephemeralTerminalUI) Teardown(force bool) error {
|
||||
h.closeScreen(force)
|
||||
showCursor(h.output)
|
||||
return nil
|
||||
}
|
||||
|
||||
func hideCursor(output io.Writer) {
|
||||
fmt.Fprint(output, "\x1b[?25l")
|
||||
}
|
||||
|
||||
func showCursor(output io.Writer) {
|
||||
fmt.Fprint(output, "\x1b[?25h")
|
||||
}
|
||||
@@ -1,180 +0,0 @@
|
||||
/*
|
||||
Package etui provides an "ephemeral" terminal user interface to display the application state dynamically.
|
||||
The terminal is placed into raw mode and the cursor is manipulated to allow for a dynamic, multi-line
|
||||
UI (provided by the jotframe lib), for this reason all other application mechanisms that write to the screen
|
||||
must be suppressed before starting (such as logs); since bytes in the device and in application memory combine to make
|
||||
a shared state, bytes coming from elsewhere to the screen will disrupt this state.
|
||||
|
||||
This UI is primarily driven off of events from the event bus, creating single-line terminal widgets to represent a
|
||||
published element on the event bus, typically polling the element for the latest state. This allows for the UI to
|
||||
control update frequency to the screen, provide "liveness" indications that are interpolated between bus events,
|
||||
and overall loosely couple the bus events from screen interactions.
|
||||
|
||||
By convention, all elements published on the bus should be treated as read-only, and publishers on the bus should
|
||||
attempt to enforce this when possible by wrapping complex objects with interfaces to prescribe interactions. Also by
|
||||
convention, each new event that the UI should respond to should be added either in this package as a handler function,
|
||||
or in the shared ui package as a function on the main handler object. All handler functions should be completed
|
||||
processing an event before the ETUI exits (coordinated with a sync.WaitGroup)
|
||||
*/
|
||||
package etui
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
|
||||
"github.com/anchore/syft/internal/logger"
|
||||
|
||||
"github.com/anchore/syft/internal/ui/common"
|
||||
"github.com/anchore/syft/ui"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/jotframe/pkg/frame"
|
||||
)
|
||||
|
||||
// TODO: specify per-platform implementations with build tags (needed when windows is supported by syft)
|
||||
|
||||
// setupScreen creates a new jotframe object to manage specific screen lines dynamically, preparing the screen device
|
||||
// as needed (i.e. setting the terminal to raw mode).
|
||||
func setupScreen(output *os.File) *frame.Frame {
|
||||
config := frame.Config{
|
||||
PositionPolicy: frame.PolicyFloatForward,
|
||||
// only report output to stderr, reserve report output for stdout
|
||||
Output: output,
|
||||
}
|
||||
|
||||
fr, err := frame.New(config)
|
||||
if err != nil {
|
||||
log.Errorf("failed to create screen object: %+v", err)
|
||||
return nil
|
||||
}
|
||||
return fr
|
||||
}
|
||||
|
||||
// nolint:funlen,gocognit
|
||||
// OutputToEphemeralTUI is a UI function that provides a terminal UI experience without a separate, in-application
|
||||
// screen buffer. All logging is suppressed, buffered to a string, and output after the ETUI has been torn down.
|
||||
func OutputToEphemeralTUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
||||
output := os.Stderr
|
||||
|
||||
// prep the logger to not clobber the screen from now on (logrus only)
|
||||
logBuffer := bytes.NewBufferString("")
|
||||
logWrapper, ok := log.Log.(*logger.LogrusLogger)
|
||||
if ok {
|
||||
logWrapper.Logger.SetOutput(logBuffer)
|
||||
}
|
||||
|
||||
// hide cursor
|
||||
_, _ = fmt.Fprint(output, "\x1b[?25l")
|
||||
// show cursor
|
||||
defer fmt.Fprint(output, "\x1b[?25h")
|
||||
|
||||
fr := setupScreen(output)
|
||||
if fr == nil {
|
||||
return fmt.Errorf("unable to setup screen")
|
||||
}
|
||||
var isClosed bool
|
||||
defer func() {
|
||||
if !isClosed {
|
||||
fr.Close()
|
||||
frame.Close()
|
||||
// flush any errors to the screen before the report
|
||||
logWrapper, ok := log.Log.(*logger.LogrusLogger)
|
||||
if ok {
|
||||
fmt.Fprint(logWrapper.Output, logBuffer.String())
|
||||
} else {
|
||||
fmt.Fprint(output, logBuffer.String())
|
||||
}
|
||||
}
|
||||
logWrapper, ok := log.Log.(*logger.LogrusLogger)
|
||||
if ok {
|
||||
logWrapper.Logger.SetOutput(output)
|
||||
}
|
||||
}()
|
||||
|
||||
var err error
|
||||
var wg = &sync.WaitGroup{}
|
||||
events := subscription.Events()
|
||||
ctx := context.Background()
|
||||
syftUIHandler := ui.NewHandler()
|
||||
|
||||
signals := interruptingSignals()
|
||||
|
||||
eventLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-workerErrs:
|
||||
// TODO: we should show errors more explicitly in the ETUI
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case e, ok := <-events:
|
||||
if !ok {
|
||||
break eventLoop
|
||||
}
|
||||
switch {
|
||||
case syftUIHandler.RespondsTo(e):
|
||||
if err = syftUIHandler.Handle(ctx, fr, e, wg); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", e.Type, err)
|
||||
}
|
||||
|
||||
case e.Type == syftEvent.AppUpdateAvailable:
|
||||
if err = appUpdateAvailableHandler(ctx, fr, e, wg); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", e.Type, err)
|
||||
}
|
||||
|
||||
case e.Type == syftEvent.PresenterReady:
|
||||
// we may have other background processes still displaying progress, wait for them to
|
||||
// finish before discontinuing dynamic content and showing the final report
|
||||
wg.Wait()
|
||||
fr.Close()
|
||||
// TODO: there is a race condition within frame.Close() that sometimes leads to an extra blank line being output
|
||||
frame.Close()
|
||||
isClosed = true
|
||||
|
||||
// flush any errors to the screen before the report
|
||||
logWrapper, ok := log.Log.(*logger.LogrusLogger)
|
||||
if ok {
|
||||
fmt.Fprint(logWrapper.Output, logBuffer.String())
|
||||
} else {
|
||||
fmt.Fprint(output, logBuffer.String())
|
||||
}
|
||||
|
||||
if err := common.CatalogerPresenterReady(e); err != nil {
|
||||
log.Errorf("unable to show %s event: %+v", e.Type, err)
|
||||
}
|
||||
|
||||
// this is the last expected event
|
||||
break eventLoop
|
||||
}
|
||||
case <-ctx.Done():
|
||||
if ctx.Err() != nil {
|
||||
log.Errorf("cancelled (%+v)", err)
|
||||
}
|
||||
break eventLoop
|
||||
case <-signals:
|
||||
break eventLoop
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func interruptingSignals() chan os.Signal {
|
||||
c := make(chan os.Signal, 1) // Note: A buffered channel is recommended for this; see https://golang.org/pkg/os/signal/#Notify
|
||||
|
||||
interruptions := []os.Signal{
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
}
|
||||
|
||||
signal.Notify(c, interruptions...)
|
||||
|
||||
return c
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package etui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/gookit/color"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
"github.com/wagoodman/jotframe/pkg/frame"
|
||||
)
|
||||
|
||||
// appUpdateAvailableHandler is a UI handler function to display a new application version to the top of the screen.
|
||||
func appUpdateAvailableHandler(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
|
||||
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad AppUpdateAvailable event: %w", err)
|
||||
}
|
||||
|
||||
line, err := fr.Prepend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := color.Magenta.Sprintf("New version of %s is available: %s", internal.ApplicationName, newVersion)
|
||||
_, _ = io.WriteString(line, message)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/gookit/color"
|
||||
"github.com/wagoodman/jotframe/pkg/frame"
|
||||
|
||||
syftEventParsers "github.com/anchore/syft/syft/event/parsers"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// handleCatalogerPresenterReady is a UI function for processing the CatalogerFinished bus event, displaying the catalog
|
||||
// via the given presenter to stdout.
|
||||
func handleCatalogerPresenterReady(event partybus.Event) error {
|
||||
// show the report to stdout
|
||||
pres, err := syftEventParsers.ParsePresenterReady(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad CatalogerFinished event: %w", err)
|
||||
}
|
||||
|
||||
if err := pres.Present(os.Stdout); err != nil {
|
||||
return fmt.Errorf("unable to show package catalog report: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// handleAppUpdateAvailable is a UI handler function to display a new application version to the top of the screen.
|
||||
func handleAppUpdateAvailable(_ context.Context, fr *frame.Frame, event partybus.Event, _ *sync.WaitGroup) error {
|
||||
newVersion, err := syftEventParsers.ParseAppUpdateAvailable(event)
|
||||
if err != nil {
|
||||
return fmt.Errorf("bad AppUpdateAvailable event: %w", err)
|
||||
}
|
||||
|
||||
line, err := fr.Prepend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
message := color.Magenta.Sprintf("New version of %s is available: %s", internal.ApplicationName, newVersion)
|
||||
_, _ = io.WriteString(line, message)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/ui/common"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// LoggerUI is a UI function that leverages the displays all application logs to the screen.
|
||||
func LoggerUI(workerErrs <-chan error, subscription *partybus.Subscription) error {
|
||||
events := subscription.Events()
|
||||
eventLoop:
|
||||
for {
|
||||
select {
|
||||
case err := <-workerErrs:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
case e, ok := <-events:
|
||||
if !ok {
|
||||
// event bus closed...
|
||||
break eventLoop
|
||||
}
|
||||
|
||||
// ignore all events except for the final event
|
||||
if e.Type == syftEvent.PresenterReady {
|
||||
err := common.CatalogerPresenterReady(e)
|
||||
if err != nil {
|
||||
log.Errorf("unable to show catalog image finished event: %+v", err)
|
||||
}
|
||||
|
||||
// this is the last expected event
|
||||
break eventLoop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"github.com/anchore/syft/internal/log"
|
||||
syftEvent "github.com/anchore/syft/syft/event"
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
type loggerUI struct {
|
||||
unsubscribe func() error
|
||||
}
|
||||
|
||||
func NewLoggerUI() UI {
|
||||
return &loggerUI{}
|
||||
}
|
||||
|
||||
func (l *loggerUI) Setup(unsubscribe func() error) error {
|
||||
l.unsubscribe = unsubscribe
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l loggerUI) Handle(event partybus.Event) error {
|
||||
// ignore all events except for the final event
|
||||
if event.Type != syftEvent.PresenterReady {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := handleCatalogerPresenterReady(event); err != nil {
|
||||
log.Warnf("unable to show catalog image finished event: %+v", err)
|
||||
}
|
||||
|
||||
// this is the last expected event, stop listening to events
|
||||
return l.unsubscribe()
|
||||
}
|
||||
|
||||
func (l loggerUI) Teardown(_ bool) error {
|
||||
return nil
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"github.com/anchore/syft/internal/ui/etui"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
@@ -21,9 +20,9 @@ func Select(verbose, quiet bool) UI {
|
||||
|
||||
switch {
|
||||
case runtime.GOOS == "windows" || verbose || quiet || notATerminal || !isStderrATty:
|
||||
ui = LoggerUI
|
||||
ui = NewLoggerUI()
|
||||
default:
|
||||
ui = etui.OutputToEphemeralTUI
|
||||
ui = NewEphemeralTerminalUI()
|
||||
}
|
||||
|
||||
return ui
|
||||
|
||||
+5
-3
@@ -4,6 +4,8 @@ import (
|
||||
"github.com/wagoodman/go-partybus"
|
||||
)
|
||||
|
||||
// UI is a function that takes a channel of errors from the main() worker and a event bus subscription and is
|
||||
// responsible for displaying pertinent events to the user, on the screen or otherwise.
|
||||
type UI func(<-chan error, *partybus.Subscription) error
|
||||
type UI interface {
|
||||
Setup(unsubscribe func() error) error
|
||||
partybus.Handler
|
||||
Teardown(force bool) error
|
||||
}
|
||||
|
||||
@@ -0,0 +1,610 @@
|
||||
{
|
||||
"$schema" : "http://json-schema.org/draft-07/schema#",
|
||||
"$id" : "http://spdx.org/rdf/terms",
|
||||
"title" : "SPDX 2.2",
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"Document" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"revieweds" : {
|
||||
"description" : "Reviewed",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"reviewer" : {
|
||||
"description" : "The name and, optionally, contact information of the person who performed the review. Values of this property must conform to the agent and tool syntax.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"reviewDate" : {
|
||||
"description" : "The date and time at which the SpdxDocument was reviewed. This value must be in UTC and have 'Z' as its timezone indicator.",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"hasExtractedLicensingInfos" : {
|
||||
"description" : "Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"seeAlsos" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"name" : {
|
||||
"description" : "Identify name of this SpdxElement.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"licenseId" : {
|
||||
"description" : "A human readable short form license identifier for a license. The license ID is iether on the standard license oist or the form \"LicenseRef-\"[idString] where [idString] is a unique string containing letters, numbers, \".\", \"-\" or \"+\".",
|
||||
"type" : "string"
|
||||
},
|
||||
"extractedText" : {
|
||||
"description" : "Verbatim license or licensing notice text that was discovered.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"description" : "An ExtractedLicensingInfo represents a license or licensing notice that was found in the package. Any license text that is recognized as a license may be represented as a License rather than an ExtractedLicensingInfo."
|
||||
}
|
||||
},
|
||||
"name" : {
|
||||
"description" : "Identify name of this SpdxElement.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"spdxVersion" : {
|
||||
"description" : "Provide a reference number that can be used to understand how to parse and interpret the rest of the file. It will enable both future changes to the specification and to support backward compatibility. The version number consists of a major and minor version indicator. The major field will be incremented when incompatible changes between versions are made (one or more sections are created, modified or deleted). The minor field will be incremented when backwards compatible changes are made.",
|
||||
"type" : "string"
|
||||
},
|
||||
"annotations" : {
|
||||
"description" : "Provide additional information about an SpdxElement.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"annotationDate" : {
|
||||
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"annotator" : {
|
||||
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
|
||||
"type" : "string"
|
||||
},
|
||||
"annotationType" : {
|
||||
"description" : "Type of the annotation.",
|
||||
"type" : "string",
|
||||
"enum" : [ "OTHER", "REVIEW" ]
|
||||
}
|
||||
},
|
||||
"description" : "An Annotation is a comment on an SpdxItem by an agent."
|
||||
}
|
||||
},
|
||||
"describesPackages" : {
|
||||
"description" : "The describesPackage property relates an SpdxDocument to the package which it describes.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "SPDX ID for Package. The describesPackage property relates an SpdxDocument to the package which it describes.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"dataLicense" : {
|
||||
"description" : "License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement, or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.",
|
||||
"type" : "string"
|
||||
},
|
||||
"externalDocumentRefs" : {
|
||||
"description" : "Identify any external SPDX documents referenced within this SPDX document.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"externalDocumentId" : {
|
||||
"description" : "externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.",
|
||||
"type" : "string"
|
||||
},
|
||||
"checksum" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"algorithm" : {
|
||||
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
|
||||
"type" : "string",
|
||||
"enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ]
|
||||
},
|
||||
"checksumValue" : {
|
||||
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
|
||||
},
|
||||
"spdxDocument" : {
|
||||
"description" : "SPDX ID for SpdxDocument. A propoerty containing an SPDX document.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"description" : "Information about an external SPDX document reference including the checksum. This allows for verification of the external references."
|
||||
}
|
||||
},
|
||||
"creationInfo" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"created" : {
|
||||
"description" : "Identify when the SPDX file was originally created. The date is to be specified according to combined date and time in UTC format as specified in ISO 8601 standard. This field is distinct from the fields in section 8, which involves the addition of information during a subsequent review.",
|
||||
"type" : "string"
|
||||
},
|
||||
"creators" : {
|
||||
"description" : "Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization, indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization, indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.",
|
||||
"type" : "string"
|
||||
},
|
||||
"minItems" : 1
|
||||
},
|
||||
"licenseListVersion" : {
|
||||
"description" : "An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"description" : "One instance is required for each SPDX file produced. It provides the necessary information for forward and backward compatibility for processing tools."
|
||||
},
|
||||
"packages" : {
|
||||
"description" : "Packages referenced in the SPDX document",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"attributionTexts" : {
|
||||
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"annotations" : {
|
||||
"description" : "Provide additional information about an SpdxElement.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"annotationDate" : {
|
||||
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"annotator" : {
|
||||
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
|
||||
"type" : "string"
|
||||
},
|
||||
"annotationType" : {
|
||||
"description" : "Type of the annotation.",
|
||||
"type" : "string",
|
||||
"enum" : [ "OTHER", "REVIEW" ]
|
||||
}
|
||||
},
|
||||
"description" : "An Annotation is a comment on an SpdxItem by an agent."
|
||||
}
|
||||
},
|
||||
"supplier" : {
|
||||
"description" : "The name and, optionally, contact information of the person or organization who was the immediate supplier of this package to the recipient. The supplier may be different than originator when the software has been repackaged. Values of this property must conform to the agent and tool syntax.",
|
||||
"type" : "string"
|
||||
},
|
||||
"homepage" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"packageVerificationCode" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"packageVerificationCodeValue" : {
|
||||
"description" : "The actual package verification code as a hex encoded value.",
|
||||
"type" : "string"
|
||||
},
|
||||
"packageVerificationCodeExcludedFiles" : {
|
||||
"description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"description" : "A manifest based verification code (the algorithm is defined in section 4.7 of the full specification) of the SPDX Item. This allows consumers of this data and/or database to determine if an SPDX item they have in hand is identical to the SPDX item from which the data was produced. This algorithm works even if the SPDX document is included in the SPDX item."
|
||||
},
|
||||
"checksums" : {
|
||||
"description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"algorithm" : {
|
||||
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
|
||||
"type" : "string",
|
||||
"enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ]
|
||||
},
|
||||
"checksumValue" : {
|
||||
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
|
||||
}
|
||||
},
|
||||
"downloadLocation" : {
|
||||
"description" : "The URI at which this package is available for download. Private (i.e., not publicly reachable) URIs are acceptable as values of this property. The values http://spdx.org/rdf/terms#none and http://spdx.org/rdf/terms#noassertion may be used to specify that the package is not downloadable or that no attempt was made to determine its download location, respectively.",
|
||||
"type" : "string"
|
||||
},
|
||||
"filesAnalyzed" : {
|
||||
"description" : "Indicates whether the file content of this package has been available for or subjected to analysis when creating the SPDX document. If false indicates packages that represent metadata or URI references to a project, product, artifact, distribution or a component. If set to false, the package must not contain any files.",
|
||||
"type" : "boolean"
|
||||
},
|
||||
"externalRefs" : {
|
||||
"description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"referenceCategory" : {
|
||||
"description" : "Category for the external reference",
|
||||
"type" : "string",
|
||||
"enum" : [ "OTHER", "SECURITY", "PACKAGE_MANAGER" ]
|
||||
},
|
||||
"referenceLocator" : {
|
||||
"description" : "The unique string with no spaces necessary to access the package-specific information, metadata, or content within the target location. The format of the locator is subject to constraints defined by the <type>.",
|
||||
"type" : "string"
|
||||
},
|
||||
"referenceType" : {
|
||||
"description" : "Type of the external reference. These are definined in an appendix in the SPDX specification.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package."
|
||||
}
|
||||
},
|
||||
"licenseComments" : {
|
||||
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
|
||||
"type" : "string"
|
||||
},
|
||||
"name" : {
|
||||
"description" : "Identify name of this SpdxElement.",
|
||||
"type" : "string"
|
||||
},
|
||||
"hasFiles" : {
|
||||
"description" : "Indicates that a particular file belongs to a package.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "SPDX ID for File. Indicates that a particular file belongs to a package.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"summary" : {
|
||||
"description" : "Provides a short description of the package.",
|
||||
"type" : "string"
|
||||
},
|
||||
"copyrightText" : {
|
||||
"description" : "The text of copyright declarations recited in the Package or File.",
|
||||
"type" : "string"
|
||||
},
|
||||
"originator" : {
|
||||
"description" : "The name and, optionally, contact information of the person or organization that originally created the package. Values of this property must conform to the agent and tool syntax.",
|
||||
"type" : "string"
|
||||
},
|
||||
"packageFileName" : {
|
||||
"description" : "The base name of the package file name. For example, zlib-1.2.5.tar.gz.",
|
||||
"type" : "string"
|
||||
},
|
||||
"licenseInfoFromFiles" : {
|
||||
"description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"versionInfo" : {
|
||||
"description" : "Provides an indication of the version of the package that is described by this SpdxDocument.",
|
||||
"type" : "string"
|
||||
},
|
||||
"sourceInfo" : {
|
||||
"description" : "Allows the producer(s) of the SPDX document to describe how the package was acquired and/or changed from the original source.",
|
||||
"type" : "string"
|
||||
},
|
||||
"description" : {
|
||||
"description" : "Provides a detailed description of the package.",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"files" : {
|
||||
"description" : "Files referenced in the SPDX document",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"fileTypes" : {
|
||||
"description" : "The type of the file.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "The type of the file.",
|
||||
"type" : "string",
|
||||
"enum" : [ "OTHER", "DOCUMENTATION", "IMAGE", "VIDEO", "ARCHIVE", "SPDX", "APPLICATION", "SOURCE", "BINARY", "TEXT", "AUDIO" ]
|
||||
}
|
||||
},
|
||||
"attributionTexts" : {
|
||||
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"annotations" : {
|
||||
"description" : "Provide additional information about an SpdxElement.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"annotationDate" : {
|
||||
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"annotator" : {
|
||||
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
|
||||
"type" : "string"
|
||||
},
|
||||
"annotationType" : {
|
||||
"description" : "Type of the annotation.",
|
||||
"type" : "string",
|
||||
"enum" : [ "OTHER", "REVIEW" ]
|
||||
}
|
||||
},
|
||||
"description" : "An Annotation is a comment on an SpdxItem by an agent."
|
||||
}
|
||||
},
|
||||
"checksums" : {
|
||||
"description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"algorithm" : {
|
||||
"description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.",
|
||||
"type" : "string",
|
||||
"enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ]
|
||||
},
|
||||
"checksumValue" : {
|
||||
"description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented."
|
||||
},
|
||||
"minItems" : 1
|
||||
},
|
||||
"noticeText" : {
|
||||
"description" : "This field provides a place for the SPDX file creator to record potential legal notices found in the file. This may or may not include copyright statements.",
|
||||
"type" : "string"
|
||||
},
|
||||
"artifactOfs" : {
|
||||
"description" : "Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name properties and the URI (if one is known) of doap:Project resources that are values of this property. All other properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or from some SPDX formats.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : { }
|
||||
}
|
||||
},
|
||||
"licenseComments" : {
|
||||
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
|
||||
"type" : "string"
|
||||
},
|
||||
"fileName" : {
|
||||
"description" : "The name of the file relative to the root of the package.",
|
||||
"type" : "string"
|
||||
},
|
||||
"name" : {
|
||||
"description" : "Identify name of this SpdxElement.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"copyrightText" : {
|
||||
"description" : "The text of copyright declarations recited in the Package or File.",
|
||||
"type" : "string"
|
||||
},
|
||||
"fileContributors" : {
|
||||
"description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"licenseInfoInFiles" : {
|
||||
"description" : "Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "License expression for licenseInfoInFile. Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.",
|
||||
"type" : "string"
|
||||
},
|
||||
"minItems" : 1
|
||||
},
|
||||
"licenseInfoFromFiles" : {
|
||||
"description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"fileDependencies" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "SPDX ID for File",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"snippets" : {
|
||||
"description" : "Snippets referenced in the SPDX document",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"ranges" : {
|
||||
"description" : "This field defines the byte range in the original host file (in X.2) that the snippet information applies to",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"startPointer" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"reference" : {
|
||||
"description" : "SPDX ID for File",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"endPointer" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"reference" : {
|
||||
"description" : "SPDX ID for File",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"minItems" : 1
|
||||
},
|
||||
"licenseComments" : {
|
||||
"description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.",
|
||||
"type" : "string"
|
||||
},
|
||||
"attributionTexts" : {
|
||||
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"name" : {
|
||||
"description" : "Identify name of this SpdxElement.",
|
||||
"type" : "string"
|
||||
},
|
||||
"snippetFromFile" : {
|
||||
"description" : "SPDX ID for File. File containing the SPDX element (e.g. the file contaning a snippet).",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"copyrightText" : {
|
||||
"description" : "The text of copyright declarations recited in the Package or File.",
|
||||
"type" : "string"
|
||||
},
|
||||
"licenseInfoInSnippets" : {
|
||||
"description" : "Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "License expression for licenseInfoInSnippet. Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.",
|
||||
"type" : "string"
|
||||
}
|
||||
},
|
||||
"annotations" : {
|
||||
"description" : "Provide additional information about an SpdxElement.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"annotationDate" : {
|
||||
"description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.",
|
||||
"type" : "string"
|
||||
},
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"annotator" : {
|
||||
"description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.",
|
||||
"type" : "string"
|
||||
},
|
||||
"annotationType" : {
|
||||
"description" : "Type of the annotation.",
|
||||
"type" : "string",
|
||||
"enum" : [ "OTHER", "REVIEW" ]
|
||||
}
|
||||
},
|
||||
"description" : "An Annotation is a comment on an SpdxItem by an agent."
|
||||
}
|
||||
},
|
||||
"licenseInfoFromFiles" : {
|
||||
"description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"relationships" : {
|
||||
"description" : "Relationships referenced in the SPDX document",
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"comment" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"relationshipType" : {
|
||||
"description" : "Describes the type of relationship between two SPDX elements.",
|
||||
"type" : "string",
|
||||
"enum" : [ "VARIANT_OF", "COPY_OF", "PATCH_FOR", "TEST_DEPENDENCY_OF", "CONTAINED_BY", "DATA_FILE_OF", "OPTIONAL_COMPONENT_OF", "ANCESTOR_OF", "GENERATES", "CONTAINS", "OPTIONAL_DEPENDENCY_OF", "FILE_ADDED", "DEV_DEPENDENCY_OF", "DEPENDENCY_OF", "BUILD_DEPENDENCY_OF", "DESCRIBES", "PREREQUISITE_FOR", "HAS_PREREQUISITE", "PROVIDED_DEPENDENCY_OF", "DYNAMIC_LINK", "DESCRIBED_BY", "METAFILE_OF", "DEPENDENCY_MANIFEST_OF", "PATCH_APPLIED", "RUNTIME_DEPENDENCY_OF", "TEST_OF", "TEST_TOOL_OF", "DEPENDS_ON", "FILE_MODIFIED", "DISTRIBUTION_ARTIFACT", "DOCUMENTATION_OF", "GENERATED_FROM", "STATIC_LINK", "OTHER", "BUILD_TOOL_OF", "TEST_CASE_OF", "PACKAGE_OF", "DESCENDANT_OF", "FILE_DELETED", "EXPANDED_FROM_ARCHIVE", "DEV_TOOL_OF", "EXAMPLE_OF" ]
|
||||
},
|
||||
"relatedSpdxElement" : {
|
||||
"description" : "SPDX ID for SpdxElement. A related SpdxElement.",
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,10 +19,12 @@ func NewDistro(t Type, ver, like string) (Distro, error) {
|
||||
if ver == "" {
|
||||
return Distro{Type: t}, nil
|
||||
}
|
||||
|
||||
verObj, err := hashiVer.NewVersion(ver)
|
||||
if err != nil {
|
||||
return Distro{}, fmt.Errorf("could not create distro version: %w", err)
|
||||
}
|
||||
|
||||
return Distro{
|
||||
Type: t,
|
||||
Version: verObj,
|
||||
@@ -39,7 +41,7 @@ func (d Distro) Name() string {
|
||||
// MajorVersion returns the major version value from the pseudo-semantically versioned distro version value.
|
||||
func (d Distro) MajorVersion() string {
|
||||
if d.Version == nil {
|
||||
return fmt.Sprint("(version unknown)")
|
||||
return "(version unknown)"
|
||||
}
|
||||
return fmt.Sprintf("%d", d.Version.Segments()[0])
|
||||
}
|
||||
|
||||
+72
-10
@@ -35,6 +35,16 @@ var identityFiles = []parseEntry{
|
||||
path: "/bin/busybox",
|
||||
fn: parseBusyBox,
|
||||
},
|
||||
{
|
||||
// check for centos:6
|
||||
path: "/etc/system-release-cpe",
|
||||
fn: parseSystemReleaseCPE,
|
||||
},
|
||||
{
|
||||
// last ditch effort for determining older centos version distro information
|
||||
path: "/etc/redhat-release",
|
||||
fn: parseRedhatRelease,
|
||||
},
|
||||
}
|
||||
|
||||
// Identify parses distro-specific files to determine distro metadata like version and release.
|
||||
@@ -87,23 +97,30 @@ identifyLoop:
|
||||
return distro
|
||||
}
|
||||
|
||||
func assemble(name, version, like string) *Distro {
|
||||
distroType, ok := IDMapping[name]
|
||||
func assemble(id, version, like string) *Distro {
|
||||
distroType, ok := IDMapping[id]
|
||||
|
||||
// Both distro and version must be present
|
||||
if len(name) == 0 {
|
||||
if len(id) == 0 && len(version) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if ok {
|
||||
distro, err := NewDistro(distroType, version, like)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return &distro
|
||||
// If it's an unknown distro, try mapping the ID_LIKE
|
||||
if !ok && len(like) != 0 {
|
||||
distroType, ok = IDMapping[like]
|
||||
}
|
||||
|
||||
return nil
|
||||
// If we still can't match allow name to be used in constructor
|
||||
if !ok {
|
||||
distroType = Type(id)
|
||||
}
|
||||
|
||||
distro, err := NewDistro(distroType, version, like)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return &distro
|
||||
}
|
||||
|
||||
func parseOsRelease(contents string) *Distro {
|
||||
@@ -140,3 +157,48 @@ func parseBusyBox(contents string) *Distro {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: we should update parseSystemReleaseCPE to use the CPE struct, pkg.CPE, which requires a refactor to avoid a circular import:
|
||||
// TODO: pkg depends on distro to support pURLs. To avoid the circular import, either try to make pkg to not depend on distro (medium lift-ish)
|
||||
// TODO: or migrate the cpe code out of the pkg package (small lift).
|
||||
// example CPE: cpe:/o:centos:linux:6:GA
|
||||
var systemReleaseCpeMatcher = regexp.MustCompile(`cpe:\/o:(.*?):.*?:(.*?):.*?$`)
|
||||
|
||||
// parseSystemReleaseCPE parses the older centos (6) file to determine distro metadata
|
||||
func parseSystemReleaseCPE(contents string) *Distro {
|
||||
matches := systemReleaseCpeMatcher.FindAllStringSubmatch(contents, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
log.Warnf("system release cpe does not match expected format")
|
||||
return nil
|
||||
}
|
||||
// note: in SubMatches (capture groups), the 0th index is the full match string
|
||||
// see https://pkg.go.dev/regexp#pkg-overview for more info
|
||||
distro := assemble(match[1], match[2], "")
|
||||
if distro != nil {
|
||||
return distro
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// example: "CentOS release 6.10 (Final)"
|
||||
var redhatReleaseMatcher = regexp.MustCompile(`(.*?)\srelease\s(\d\.\d+)`)
|
||||
|
||||
// parseRedhatRelease is a fallback parsing method for determining distro information in older redhat versions
|
||||
func parseRedhatRelease(contents string) *Distro {
|
||||
matches := redhatReleaseMatcher.FindAllStringSubmatch(contents, -1)
|
||||
for _, match := range matches {
|
||||
if len(match) < 3 {
|
||||
log.Warnf("failed to parse redhat-release file, unexpected format")
|
||||
return nil
|
||||
}
|
||||
// note: in SubMatches (capture groups), the 0th index is the full match string
|
||||
// see https://pkg.go.dev/regexp#pkg-overview for more info
|
||||
distro := assemble(strings.ToLower(match[1]), match[2], "")
|
||||
if distro != nil {
|
||||
return distro
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
+135
-28
@@ -6,12 +6,15 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/stretchr/testify/assert"
|
||||
hashiVer "github.com/hashicorp/go-version"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/syft/source"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const CustomDistro Type = "scientific"
|
||||
|
||||
func TestIdentifyDistro(t *testing.T) {
|
||||
tests := []struct {
|
||||
fixture string
|
||||
@@ -68,14 +71,20 @@ func TestIdentifyDistro(t *testing.T) {
|
||||
Type: UnknownDistroType,
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/unmatchable",
|
||||
Type: UnknownDistroType,
|
||||
fixture: "test-fixtures/os/custom",
|
||||
Type: CustomDistro,
|
||||
Version: "8.0.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/opensuse-leap",
|
||||
Type: OpenSuseLeap,
|
||||
Version: "15.2.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/sles",
|
||||
Type: SLES,
|
||||
Version: "15.2.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/photon",
|
||||
Type: Photon,
|
||||
@@ -85,6 +94,30 @@ func TestIdentifyDistro(t *testing.T) {
|
||||
fixture: "test-fixtures/os/arch",
|
||||
Type: ArchLinux,
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/partial-fields/missing-id",
|
||||
Type: Debian,
|
||||
Version: "8.0.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/partial-fields/unknown-id",
|
||||
Type: Debian,
|
||||
Version: "8.0.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/partial-fields/missing-version",
|
||||
Type: UnknownDistroType,
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/centos6",
|
||||
Type: CentOS,
|
||||
Version: "6.0.0",
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/os/centos5",
|
||||
Type: CentOS,
|
||||
Version: "5.7.0",
|
||||
},
|
||||
}
|
||||
|
||||
observedDistros := internal.NewStringSet()
|
||||
@@ -187,18 +220,7 @@ func TestParseOsRelease(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
name := fmt.Sprintf("%s:%s", test.name, test.RawVersion)
|
||||
fixture, err := os.Open(test.fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("could not open test fixture=%s: %+v", test.fixture, err)
|
||||
}
|
||||
defer fixture.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read fixture file: %+v", err)
|
||||
}
|
||||
|
||||
contents := string(b)
|
||||
contents := retrieveFixtureContentsAsString(test.fixture, t)
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
distro := parseOsRelease(contents)
|
||||
@@ -227,18 +249,7 @@ func TestParseOsReleaseFailures(t *testing.T) {
|
||||
|
||||
for _, test := range tests {
|
||||
name := fmt.Sprintf("%s:%s", test.name, test.fixture)
|
||||
fixture, err := os.Open(test.fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("could not open test fixture=%s: %+v", test.fixture, err)
|
||||
}
|
||||
defer fixture.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read fixture file: %+v", err)
|
||||
}
|
||||
|
||||
contents := string(b)
|
||||
contents := retrieveFixtureContentsAsString(test.fixture, t)
|
||||
|
||||
t.Run(name, func(t *testing.T) {
|
||||
distro := parseOsRelease(contents)
|
||||
@@ -247,5 +258,101 @@ func TestParseOsReleaseFailures(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseSystemReleaseCPE(t *testing.T) {
|
||||
centos6Version, _ := hashiVer.NewVersion("6")
|
||||
tests := []struct {
|
||||
fixture string
|
||||
name string
|
||||
expected *Distro
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/os/centos6/etc/system-release-cpe",
|
||||
name: "Centos 6",
|
||||
expected: &Distro{
|
||||
Type: CentOS,
|
||||
Version: centos6Version,
|
||||
RawVersion: "6",
|
||||
},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/bad-system-release-cpe",
|
||||
name: "Centos 6 Bad CPE",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
contents := retrieveFixtureContentsAsString(test.fixture, t)
|
||||
actual := parseSystemReleaseCPE(contents)
|
||||
|
||||
if test.expected == nil {
|
||||
assert.Nil(t, actual)
|
||||
return
|
||||
}
|
||||
|
||||
// not comparing the full distro object because the hashiVer is a pointer
|
||||
assert.Equal(t, test.expected.Type, actual.Type)
|
||||
assert.Equal(t, &test.expected.Version, &actual.Version)
|
||||
assert.Equal(t, test.expected.RawVersion, actual.RawVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRedhatRelease(t *testing.T) {
|
||||
centos5Version, _ := hashiVer.NewVersion("5.7")
|
||||
tests := []struct {
|
||||
fixture string
|
||||
name string
|
||||
expected *Distro
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/os/centos5/etc/redhat-release",
|
||||
name: "Centos 5",
|
||||
expected: &Distro{
|
||||
Type: CentOS,
|
||||
Version: centos5Version,
|
||||
RawVersion: "5.7",
|
||||
},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/bad-redhat-release",
|
||||
name: "Centos 5 Bad Redhat Release",
|
||||
expected: nil,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
contents := retrieveFixtureContentsAsString(test.fixture, t)
|
||||
actual := parseRedhatRelease(contents)
|
||||
|
||||
if test.expected == nil {
|
||||
assert.Nil(t, actual)
|
||||
return
|
||||
}
|
||||
|
||||
// not comparing the full distro object because the hashiVer is a pointer
|
||||
assert.Equal(t, test.expected.Type, actual.Type)
|
||||
assert.Equal(t, &test.expected.Version, &actual.Version)
|
||||
assert.Equal(t, test.expected.RawVersion, actual.RawVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveFixtureContentsAsString(fixturePath string, t *testing.T) string {
|
||||
fixture, err := os.Open(fixturePath)
|
||||
if err != nil {
|
||||
t.Fatalf("could not open test fixture=%s: %+v", fixturePath, err)
|
||||
}
|
||||
defer fixture.Close()
|
||||
|
||||
b, err := ioutil.ReadAll(fixture)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to read fixture file: %+v", err)
|
||||
}
|
||||
|
||||
return string(b)
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
CentOS release 5 (Final)
|
||||
@@ -0,0 +1 @@
|
||||
cpe:/o:centos:6:GA
|
||||
@@ -0,0 +1 @@
|
||||
CentOS release 5.7 (Final)
|
||||
@@ -0,0 +1 @@
|
||||
cpe:/o:centos:linux:6:GA
|
||||
@@ -0,0 +1,9 @@
|
||||
NAME="SLES"
|
||||
VERSION="15-SP2"
|
||||
VERSION_ID="15.2"
|
||||
PRETTY_NAME="SUSE Linux Enterprise Server 15 SP2"
|
||||
ID="sles"
|
||||
ID_LIKE="suse"
|
||||
ANSI_COLOR="0;32"
|
||||
CPE_NAME="cpe:/o:suse:sles:15:sp2"
|
||||
DOCUMENTATION_URL="https://documentation.suse.com/"
|
||||
@@ -0,0 +1,3 @@
|
||||
NAME="Debian GNU/Linux"
|
||||
VERSION_ID="8"
|
||||
ID_LIKE=debian
|
||||
@@ -0,0 +1,2 @@
|
||||
NAME="Debian GNU/Linux"
|
||||
ID_LIKE=debian
|
||||
@@ -0,0 +1,4 @@
|
||||
NAME="Debian GNU/Linux"
|
||||
VERSION_ID="8"
|
||||
ID=my-awesome-distro
|
||||
ID_LIKE=debian
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user