mirror of
https://github.com/anchore/syft.git
synced 2026-10-20 18:12:32 +02:00
Compare commits
17
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdc1effd6e | ||
|
|
055542694f | ||
|
|
c6447cb62b | ||
|
|
bd89e22fe5 | ||
|
|
3ca797dc53 | ||
|
|
50408b52d1 | ||
|
|
c00afc7fa9 | ||
|
|
4a82e4843a | ||
|
|
d474281731 | ||
|
|
0c0890ef44 | ||
|
|
8d522efcf6 | ||
|
|
124cfd86c7 | ||
|
|
12b44af471 | ||
|
|
bcdebcadaf | ||
|
|
f6cc0c8628 | ||
|
|
c1346ad62c | ||
|
|
9ded1c4c22 |
+8
-6
@@ -6,6 +6,8 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
|
||||||
"github.com/anchore/stereoscope"
|
"github.com/anchore/stereoscope"
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
"github.com/anchore/syft/internal/anchore"
|
"github.com/anchore/syft/internal/anchore"
|
||||||
@@ -48,7 +50,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
packagesPresenterOpt packages.PresenterOption
|
packagesPresenterOpt format.Option
|
||||||
packagesArgs = cobra.MaximumNArgs(1)
|
packagesArgs = cobra.MaximumNArgs(1)
|
||||||
packagesCmd = &cobra.Command{
|
packagesCmd = &cobra.Command{
|
||||||
Use: "packages [SOURCE]",
|
Use: "packages [SOURCE]",
|
||||||
@@ -71,8 +73,8 @@ var (
|
|||||||
}
|
}
|
||||||
|
|
||||||
// set the presenter
|
// set the presenter
|
||||||
presenterOption := packages.ParsePresenterOption(appConfig.Output)
|
presenterOption := format.ParseOption(appConfig.Output)
|
||||||
if presenterOption == packages.UnknownPresenterOption {
|
if presenterOption == format.UnknownOption {
|
||||||
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
|
return fmt.Errorf("bad --output value '%s'", appConfig.Output)
|
||||||
}
|
}
|
||||||
packagesPresenterOpt = presenterOption
|
packagesPresenterOpt = presenterOption
|
||||||
@@ -109,8 +111,8 @@ func setPackageFlags(flags *pflag.FlagSet) {
|
|||||||
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
fmt.Sprintf("selection of layers to catalog, options=%v", source.AllScopes))
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
"output", "o", string(packages.TablePresenterOption),
|
"output", "o", string(format.TableOption),
|
||||||
fmt.Sprintf("report output formatter, options=%v", packages.AllPresenters),
|
fmt.Sprintf("report output formatter, options=%v", format.AllOptions),
|
||||||
)
|
)
|
||||||
|
|
||||||
flags.StringP(
|
flags.StringP(
|
||||||
@@ -247,7 +249,7 @@ func packagesExecWorker(userInput string) <-chan error {
|
|||||||
|
|
||||||
bus.Publish(partybus.Event{
|
bus.Publish(partybus.Event{
|
||||||
Type: event.PresenterReady,
|
Type: event.PresenterReady,
|
||||||
Value: packages.Presenter(packagesPresenterOpt, packages.PresenterConfig{
|
Value: packages.Presenter(packagesPresenterOpt, packages.Config{
|
||||||
SourceMetadata: src.Metadata,
|
SourceMetadata: src.Metadata,
|
||||||
Catalog: catalog,
|
Catalog: catalog,
|
||||||
Distro: d,
|
Distro: d,
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/presenter/packages"
|
"github.com/anchore/syft/internal/formats"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
|
||||||
"github.com/wagoodman/go-progress"
|
"github.com/wagoodman/go-progress"
|
||||||
|
|
||||||
@@ -24,9 +25,9 @@ type packageSBOMImportAPI interface {
|
|||||||
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
|
ImportImagePackages(context.Context, string, external.ImagePackageManifest) (external.ImageImportContentResponse, *http.Response, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, scope source.Scope) (*external.ImagePackageManifest, error) {
|
func packageSbomModel(s source.Metadata, catalog *pkg.Catalog, d *distro.Distro, _ source.Scope) (*external.ImagePackageManifest, error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
pres := packages.NewJSONPresenter(catalog, s, d, scope)
|
pres := formats.ByOption(format.JSONOption).Presenter(catalog, &s, d)
|
||||||
err := pres.Present(&buf)
|
err := pres.Present(&buf)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
return nil, fmt.Errorf("unable to serialize results: %w", err)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package anchore
|
package anchore
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -9,18 +8,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/presenter/packages"
|
|
||||||
|
|
||||||
"github.com/wagoodman/go-progress"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/distro"
|
|
||||||
|
|
||||||
"github.com/docker/docker/pkg/ioutils"
|
|
||||||
|
|
||||||
"github.com/anchore/client-go/pkg/external"
|
"github.com/anchore/client-go/pkg/external"
|
||||||
|
syftjsonModel "github.com/anchore/syft/internal/formats/syftjson/model"
|
||||||
|
"github.com/anchore/syft/syft"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/docker/docker/pkg/ioutils"
|
||||||
"github.com/go-test/deep"
|
"github.com/go-test/deep"
|
||||||
|
"github.com/wagoodman/go-progress"
|
||||||
)
|
)
|
||||||
|
|
||||||
func must(c pkg.CPE, e error) pkg.CPE {
|
func must(c pkg.CPE, e error) pkg.CPE {
|
||||||
@@ -87,20 +84,19 @@ func TestPackageSbomToModel(t *testing.T) {
|
|||||||
t.Fatalf("unable to marshal model: %+v", err)
|
t.Fatalf("unable to marshal model: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
by, err := syft.Encode(c, &m, &d, format.JSONOption)
|
||||||
pres := packages.NewJSONPresenter(c, m, &d, source.AllLayersScope)
|
if err != nil {
|
||||||
if err := pres.Present(&buf); err != nil {
|
|
||||||
t.Fatalf("unable to get expected json: %+v", err)
|
t.Fatalf("unable to get expected json: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal expected result
|
// unmarshal expected result
|
||||||
var expectedDoc packages.JSONDocument
|
var expectedDoc syftjsonModel.Document
|
||||||
if err := json.Unmarshal(buf.Bytes(), &expectedDoc); err != nil {
|
if err := json.Unmarshal(by, &expectedDoc); err != nil {
|
||||||
t.Fatalf("unable to parse json doc: %+v", err)
|
t.Fatalf("unable to parse json doc: %+v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// unmarshal actual result
|
// unmarshal actual result
|
||||||
var actualDoc packages.JSONDocument
|
var actualDoc syftjsonModel.Document
|
||||||
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
|
if err := json.Unmarshal(modelJSON, &actualDoc); err != nil {
|
||||||
t.Fatalf("unable to parse json doc: %+v", err)
|
t.Fatalf("unable to parse json doc: %+v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/pkg"
|
||||||
|
|
||||||
|
func Description(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,56 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Description(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, Description(&test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/pkg"
|
||||||
|
|
||||||
|
func DownloadLocation(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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_DownloadLocation(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, DownloadLocation(&test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ExternalRefs(p *pkg.Package) (externalRefs []model.ExternalRef) {
|
||||||
|
externalRefs = make([]model.ExternalRef, 0)
|
||||||
|
for _, c := range p.CPEs {
|
||||||
|
externalRefs = append(externalRefs, model.ExternalRef{
|
||||||
|
ReferenceCategory: model.SecurityReferenceCategory,
|
||||||
|
ReferenceLocator: c.BindToFmtString(),
|
||||||
|
ReferenceType: model.Cpe23ExternalRefType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if p.PURL != "" {
|
||||||
|
externalRefs = append(externalRefs, model.ExternalRef{
|
||||||
|
ReferenceCategory: model.PackageManagerReferenceCategory,
|
||||||
|
ReferenceLocator: p.PURL,
|
||||||
|
ReferenceType: model.PurlExternalRefType,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return externalRefs
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractPURL(refs []model.ExternalRef) string {
|
||||||
|
for _, r := range refs {
|
||||||
|
if r.ReferenceType == model.PurlExternalRefType {
|
||||||
|
return r.ReferenceLocator
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExtractCPEs(refs []model.ExternalRef) (cpes []pkg.CPE) {
|
||||||
|
for _, r := range refs {
|
||||||
|
if r.ReferenceType == model.Cpe23ExternalRefType {
|
||||||
|
cpe, err := pkg.NewCPE(r.ReferenceLocator)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to extract SPDX CPE=%q: %+v", r.ReferenceLocator, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cpes = append(cpes, cpe)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cpes
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_ExternalRefs(t *testing.T) {
|
||||||
|
testCPE := testutils.MustCPE(pkg.NewCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*"))
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input pkg.Package
|
||||||
|
expected []model.ExternalRef
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "cpe + purl",
|
||||||
|
input: pkg.Package{
|
||||||
|
CPEs: []pkg.CPE{
|
||||||
|
testCPE,
|
||||||
|
},
|
||||||
|
PURL: "a-purl",
|
||||||
|
},
|
||||||
|
expected: []model.ExternalRef{
|
||||||
|
{
|
||||||
|
ReferenceCategory: model.SecurityReferenceCategory,
|
||||||
|
ReferenceLocator: testCPE.BindToFmtString(),
|
||||||
|
ReferenceType: model.Cpe23ExternalRefType,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ReferenceCategory: model.PackageManagerReferenceCategory,
|
||||||
|
ReferenceLocator: "a-purl",
|
||||||
|
ReferenceType: model.PurlExternalRefType,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
assert.ElementsMatch(t, test.expected, ExternalRefs(&test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Files(packageSpdxID string, p *pkg.Package) (files []model.File, fileIDs []string, relationships []model.Relationship) {
|
||||||
|
files = make([]model.File, 0)
|
||||||
|
fileIDs = make([]string, 0)
|
||||||
|
relationships = make([]model.Relationship, 0)
|
||||||
|
|
||||||
|
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
|
||||||
|
if !ok {
|
||||||
|
return files, fileIDs, relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
|
||||||
|
baseFileName := filepath.Base(ownedFilePath)
|
||||||
|
pathHash := sha256.Sum256([]byte(ownedFilePath))
|
||||||
|
fileSpdxID := model.ElementID(fmt.Sprintf("File-%s-%x", p.Name, pathHash)).String()
|
||||||
|
|
||||||
|
fileIDs = append(fileIDs, fileSpdxID)
|
||||||
|
|
||||||
|
files = append(files, model.File{
|
||||||
|
FileName: ownedFilePath,
|
||||||
|
Item: model.Item{
|
||||||
|
Element: model.Element{
|
||||||
|
SPDXID: fileSpdxID,
|
||||||
|
Name: baseFileName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
relationships = append(relationships, model.Relationship{
|
||||||
|
SpdxElementID: packageSpdxID,
|
||||||
|
RelationshipType: model.ContainsRelationship,
|
||||||
|
RelatedSpdxElement: fileSpdxID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return files, fileIDs, relationships
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/pkg"
|
||||||
|
|
||||||
|
func Homepage(p *pkg.Package) string {
|
||||||
|
switch metadata := p.Metadata.(type) {
|
||||||
|
case pkg.GemMetadata:
|
||||||
|
return metadata.Homepage
|
||||||
|
case pkg.NpmPackageJSONMetadata:
|
||||||
|
return metadata.Homepage
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Homepage(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, Homepage(&test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/spdxlicense"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func License(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 ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_License(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",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "debian to spdx conversion",
|
||||||
|
input: pkg.Package{
|
||||||
|
Licenses: []string{
|
||||||
|
"GPL-2",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expected: "GPL-2.0",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
assert.Equal(t, test.expected, License(&test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NoneIfEmpty(value string) string {
|
||||||
|
if strings.TrimSpace(value) == "" {
|
||||||
|
return "NONE"
|
||||||
|
}
|
||||||
|
return value
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
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))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_Originator(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, Originator(&test.input))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Originator(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 ""
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SourceInfo(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, ", ")
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package spdxhelpers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Test_SourceInfo(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 := SourceInfo(&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?)")
|
||||||
|
}
|
||||||
+19
-11
@@ -1,15 +1,16 @@
|
|||||||
package packages
|
package testutils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/presenter"
|
||||||
|
|
||||||
"github.com/anchore/go-testutils"
|
"github.com/anchore/go-testutils"
|
||||||
"github.com/anchore/stereoscope/pkg/filetree"
|
"github.com/anchore/stereoscope/pkg/filetree"
|
||||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||||
"github.com/anchore/syft/syft/distro"
|
"github.com/anchore/syft/syft/distro"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/presenter"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
"github.com/anchore/syft/syft/source"
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -17,7 +18,14 @@ import (
|
|||||||
|
|
||||||
type redactor func(s []byte) []byte
|
type redactor func(s []byte) []byte
|
||||||
|
|
||||||
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
|
func MustCPE(c pkg.CPE, e error) pkg.CPE {
|
||||||
|
if e != nil {
|
||||||
|
panic(e)
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
// grab the latest image contents and persist
|
// grab the latest image contents and persist
|
||||||
@@ -50,7 +58,7 @@ func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Pres
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
|
func AssertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
|
||||||
var buffer bytes.Buffer
|
var buffer bytes.Buffer
|
||||||
|
|
||||||
err := pres.Present(&buffer)
|
err := pres.Present(&buffer)
|
||||||
@@ -77,7 +85,7 @@ func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
func ImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
img := imagetest.GetGoldenFixtureImage(t, testImage)
|
img := imagetest.GetGoldenFixtureImage(t, testImage)
|
||||||
@@ -104,7 +112,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
|
|||||||
},
|
},
|
||||||
PURL: "a-purl-1",
|
PURL: "a-purl-1",
|
||||||
CPEs: []pkg.CPE{
|
CPEs: []pkg.CPE{
|
||||||
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
MustCPE(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
catalog.Add(pkg.Package{
|
catalog.Add(pkg.Package{
|
||||||
@@ -123,7 +131,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
|
|||||||
},
|
},
|
||||||
PURL: "a-purl-2",
|
PURL: "a-purl-2",
|
||||||
CPEs: []pkg.CPE{
|
CPEs: []pkg.CPE{
|
||||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
MustCPE(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -139,7 +147,7 @@ func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.M
|
|||||||
return catalog, src.Metadata, &dist
|
return catalog, src.Metadata, &dist
|
||||||
}
|
}
|
||||||
|
|
||||||
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
func DirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
|
||||||
catalog := pkg.NewCatalog()
|
catalog := pkg.NewCatalog()
|
||||||
|
|
||||||
// populate catalog with test data
|
// populate catalog with test data
|
||||||
@@ -160,13 +168,13 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
|
|||||||
Version: "1.0.1",
|
Version: "1.0.1",
|
||||||
Files: []pkg.PythonFileRecord{
|
Files: []pkg.PythonFileRecord{
|
||||||
{
|
{
|
||||||
Path: "/some/path/pkg1/depedencies/foo",
|
Path: "/some/path/pkg1/dependencies/foo",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
PURL: "a-purl-2",
|
PURL: "a-purl-2",
|
||||||
CPEs: []pkg.CPE{
|
CPEs: []pkg.CPE{
|
||||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
MustCPE(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
catalog.Add(pkg.Package{
|
catalog.Add(pkg.Package{
|
||||||
@@ -185,7 +193,7 @@ func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *dist
|
|||||||
},
|
},
|
||||||
PURL: "a-purl-2",
|
PURL: "a-purl-2",
|
||||||
CPEs: []pkg.CPE{
|
CPEs: []pkg.CPE{
|
||||||
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
MustCPE(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json"
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: eventually this is the source of truth for all formatters
|
||||||
|
func All() []format.Format {
|
||||||
|
return []format.Format{
|
||||||
|
spdx22json.Format(),
|
||||||
|
syftjson.Format(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Identify(by []byte) (*format.Format, error) {
|
||||||
|
for _, f := range All() {
|
||||||
|
if f.Detect(by) {
|
||||||
|
return &f, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ByOption(option format.Option) *format.Format {
|
||||||
|
for _, f := range All() {
|
||||||
|
if f.Option == option {
|
||||||
|
return &f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package formats
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIdentify(t *testing.T) {
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
fixture string
|
||||||
|
expected format.Option
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
fixture: "test-fixtures/alpine-syft.json",
|
||||||
|
expected: format.JSONOption,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
fixture: "test-fixtures/alpine-syft-spdx.json",
|
||||||
|
expected: format.SPDXJSONOption,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range tests {
|
||||||
|
t.Run(test.fixture, func(t *testing.T) {
|
||||||
|
f, err := os.Open(test.fixture)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
by, err := io.ReadAll(f)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
frmt, err := Identify(by)
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.NotNil(t, frmt)
|
||||||
|
assert.Equal(t, test.expected, frmt.Option)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package spdx22json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decoder(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, error) {
|
||||||
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
|
var doc model.Document
|
||||||
|
err := dec.Decode(&doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("unable to decode spdx-json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return toSyftModel(doc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package spdx22json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeDecodeCycle(t *testing.T) {
|
||||||
|
testImage := "image-simple"
|
||||||
|
originalCatalog, originalMetadata, _ := testutils.ImageInput(t, testImage)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
assert.NoError(t, encoder(&buf, originalCatalog, &originalMetadata, nil))
|
||||||
|
|
||||||
|
actualCatalog, actualMetadata, _, err := decoder(bytes.NewReader(buf.Bytes()))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for _, d := range deep.Equal(originalMetadata, *actualMetadata) {
|
||||||
|
t.Errorf("metadata difference: %+v", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualPackages := actualCatalog.Sorted()
|
||||||
|
for idx, p := range originalCatalog.Sorted() {
|
||||||
|
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
|
||||||
|
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ids will never be equal
|
||||||
|
p.ID = ""
|
||||||
|
actualPackages[idx].ID = ""
|
||||||
|
|
||||||
|
for _, d := range deep.Equal(*p, *actualPackages[idx]) {
|
||||||
|
if strings.Contains(d, ".VirtualPath: ") {
|
||||||
|
// location.Virtual path is not exposed in the json output
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(d, "<nil slice> != []") {
|
||||||
|
// semantically the same
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Errorf("package difference (%s): %+v", p.Name, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package spdx22json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
const anchoreNamespace = "https://anchore.com/syft"
|
||||||
|
|
||||||
|
func encoder(output io.Writer, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro) error {
|
||||||
|
doc := toFormatModel(catalog, srcMetadata, d)
|
||||||
|
|
||||||
|
enc := json.NewEncoder(output)
|
||||||
|
// prevent > and < from being escaped in the payload
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
return enc.Encode(&doc)
|
||||||
|
}
|
||||||
+10
-7
@@ -1,17 +1,20 @@
|
|||||||
package packages
|
package spdx22json
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
|
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
|
||||||
|
|
||||||
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
||||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
catalog, metadata, distro := testutils.DirectoryInput(t)
|
||||||
assertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||||
NewSPDXJSONPresenter(catalog, metadata),
|
format.NewPresenter(encoder, catalog, &metadata, distro),
|
||||||
*updateSpdxJson,
|
*updateSpdxJson,
|
||||||
spdxJsonRedactor,
|
spdxJsonRedactor,
|
||||||
)
|
)
|
||||||
@@ -19,9 +22,9 @@ func TestSPDXJSONDirectoryPresenter(t *testing.T) {
|
|||||||
|
|
||||||
func TestSPDXJSONImagePresenter(t *testing.T) {
|
func TestSPDXJSONImagePresenter(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
catalog, metadata, distro := testutils.ImageInput(t, testImage)
|
||||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||||
NewSPDXJSONPresenter(catalog, metadata),
|
format.NewPresenter(encoder, catalog, &metadata, distro),
|
||||||
testImage,
|
testImage,
|
||||||
*updateSpdxJson,
|
*updateSpdxJson,
|
||||||
spdxJsonRedactor,
|
spdxJsonRedactor,
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package spdx22json
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/format"
|
||||||
|
|
||||||
|
func Format() format.Format {
|
||||||
|
return format.NewFormat(
|
||||||
|
format.SPDXJSONOption,
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type Checksum struct {
|
type Checksum struct {
|
||||||
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
|
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
+7
-1
@@ -1,4 +1,6 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/source"
|
||||||
|
|
||||||
// derived from:
|
// derived from:
|
||||||
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
|
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
|
||||||
@@ -11,6 +13,10 @@ type Document struct {
|
|||||||
// One instance is required for each SPDX file produced. It provides the necessary information for forward
|
// One instance is required for each SPDX file produced. It provides the necessary information for forward
|
||||||
// and backward compatibility for processing tools.
|
// and backward compatibility for processing tools.
|
||||||
CreationInfo CreationInfo `json:"creationInfo"`
|
CreationInfo CreationInfo `json:"creationInfo"`
|
||||||
|
// SyftSourceData contains information about what is being described in this SPDX document (e.g. a container image, a directory, etc)
|
||||||
|
SyftSourceData *source.Metadata `json:"syftSourceData,omitempty"`
|
||||||
|
// SyftDistroData contains information about the linux distribution discovered
|
||||||
|
SyftDistroData *SyftDistroData `json:"syftDistroData,omitempty"`
|
||||||
// 2.2: Data License; should be "CC0-1.0"
|
// 2.2: Data License; should be "CC0-1.0"
|
||||||
// Cardinality: mandatory, one
|
// Cardinality: mandatory, one
|
||||||
// License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX
|
// License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type Element struct {
|
type Element struct {
|
||||||
SPDXID string `json:"SPDXID"`
|
SPDXID string `json:"SPDXID"`
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
// ElementID represents the identifier string portion of an SPDX element
|
// ElementID represents the identifier string portion of an SPDX element
|
||||||
// identifier. DocElementID should be used for any attributes which can
|
// identifier. DocElementID should be used for any attributes which can
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type ExternalDocumentRef struct {
|
type ExternalDocumentRef struct {
|
||||||
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
|
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type ReferenceCategory string
|
type ReferenceCategory string
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type FileType string
|
type FileType string
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type HasExtractedLicensingInfo struct {
|
type HasExtractedLicensingInfo struct {
|
||||||
// Verbatim license or licensing notice text that was discovered.
|
// Verbatim license or licensing notice text that was discovered.
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type Item struct {
|
type Item struct {
|
||||||
Element
|
Element
|
||||||
+4
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type Package struct {
|
type Package struct {
|
||||||
Item
|
Item
|
||||||
@@ -47,4 +47,7 @@ type Package struct {
|
|||||||
Supplier string `json:"supplier,omitempty"`
|
Supplier string `json:"supplier,omitempty"`
|
||||||
// Provides an indication of the version of the package that is described by this SpdxDocument.
|
// Provides an indication of the version of the package that is described by this SpdxDocument.
|
||||||
VersionInfo string `json:"versionInfo,omitempty"`
|
VersionInfo string `json:"versionInfo,omitempty"`
|
||||||
|
// SyftPackageData provides a spot to add syft-specific data that is not available in-spec or is not easily decodable from
|
||||||
|
// other SPDX fields.
|
||||||
|
SyftPackageData *SyftPackageData `json:"syftPackageData,omitempty"`
|
||||||
}
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
// Why are there two package identifier fields Package Checksum and Package Verification?
|
// 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
|
// Although the values of the two fields Package Checksum and Package Verification are similar, they each serve a
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type Relationship struct {
|
type Relationship struct {
|
||||||
// Id to which the SPDX element is related
|
// Id to which the SPDX element is related
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
type StartPointer struct {
|
type StartPointer struct {
|
||||||
Offset int `json:"offset,omitempty"`
|
Offset int `json:"offset,omitempty"`
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type SyftDistroData struct {
|
||||||
|
Name string `json:"name"` // Name of the Linux distribution
|
||||||
|
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
|
||||||
|
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
|
||||||
|
}
|
||||||
@@ -0,0 +1,115 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SyftPackageData struct {
|
||||||
|
SyftPackageBasicData
|
||||||
|
SyftPackageCustomData
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyftPackageCustomData struct {
|
||||||
|
MetadataType pkg.MetadataType `json:"metadataType,omitempty"`
|
||||||
|
Metadata interface{} `json:"metadata,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SyftPackageBasicData struct {
|
||||||
|
PackageType pkg.Type `json:"type,omitempty"`
|
||||||
|
FoundBy string `json:"foundBy,omitempty"`
|
||||||
|
Locations []source.Location `json:"locations,omitempty"`
|
||||||
|
Licenses []string `json:"licenses,omitempty"`
|
||||||
|
Language pkg.Language `json:"language,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// syftPackageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
|
||||||
|
type syftPackageMetadataUnpacker struct {
|
||||||
|
MetadataType pkg.MetadataType `json:"metadataType"`
|
||||||
|
Metadata json.RawMessage `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *syftPackageMetadataUnpacker) String() string {
|
||||||
|
return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
|
||||||
|
// nolint:funlen
|
||||||
|
func (p *SyftPackageData) UnmarshalJSON(b []byte) error {
|
||||||
|
var basic SyftPackageBasicData
|
||||||
|
if err := json.Unmarshal(b, &basic); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.SyftPackageBasicData = basic
|
||||||
|
|
||||||
|
var unpacker syftPackageMetadataUnpacker
|
||||||
|
if err := json.Unmarshal(b, &unpacker); err != nil {
|
||||||
|
log.Warnf("failed to unmarshall into syftPackageMetadataUnpacker: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.MetadataType = unpacker.MetadataType
|
||||||
|
|
||||||
|
switch p.MetadataType {
|
||||||
|
case pkg.ApkMetadataType:
|
||||||
|
var payload pkg.ApkMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.RpmdbMetadataType:
|
||||||
|
var payload pkg.RpmdbMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.DpkgMetadataType:
|
||||||
|
var payload pkg.DpkgMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.JavaMetadataType:
|
||||||
|
var payload pkg.JavaMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.RustCargoPackageMetadataType:
|
||||||
|
var payload pkg.CargoPackageMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.GemMetadataType:
|
||||||
|
var payload pkg.GemMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.KbPackageMetadataType:
|
||||||
|
var payload pkg.KbPackageMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.PythonPackageMetadataType:
|
||||||
|
var payload pkg.PythonPackageMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.NpmPackageJSONMetadataType:
|
||||||
|
var payload pkg.NpmPackageJSONMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
package spdx22
|
package model
|
||||||
|
|
||||||
const Version = "SPDX-2.2"
|
const Version = "SPDX-2.2"
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
|
||||||
|
FROM scratch
|
||||||
|
ADD file-1.txt /somefile-1.txt
|
||||||
|
ADD file-2.txt /somefile-2.txt
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
this file has contents
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
file-2 contents!
|
||||||
+73
-4
@@ -3,15 +3,36 @@
|
|||||||
"name": "/some/path",
|
"name": "/some/path",
|
||||||
"spdxVersion": "SPDX-2.2",
|
"spdxVersion": "SPDX-2.2",
|
||||||
"creationInfo": {
|
"creationInfo": {
|
||||||
"created": "2021-09-16T20:44:35.198887Z",
|
"created": "2021-10-11T01:54:40.609143Z",
|
||||||
"creators": [
|
"creators": [
|
||||||
"Organization: Anchore, Inc",
|
"Organization: Anchore, Inc",
|
||||||
"Tool: syft-[not provided]"
|
"Tool: syft-[not provided]"
|
||||||
],
|
],
|
||||||
"licenseListVersion": "3.14"
|
"licenseListVersion": "3.14"
|
||||||
},
|
},
|
||||||
|
"syftSourceData": {
|
||||||
|
"Scheme": "DirectoryScheme",
|
||||||
|
"ImageMetadata": {
|
||||||
|
"userInput": "",
|
||||||
|
"imageID": "",
|
||||||
|
"manifestDigest": "",
|
||||||
|
"mediaType": "",
|
||||||
|
"tags": null,
|
||||||
|
"imageSize": 0,
|
||||||
|
"layers": null,
|
||||||
|
"manifest": null,
|
||||||
|
"config": null,
|
||||||
|
"repoDigests": null
|
||||||
|
},
|
||||||
|
"Path": "/some/path"
|
||||||
|
},
|
||||||
|
"syftDistroData": {
|
||||||
|
"name": "debian",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"idLike": "like!"
|
||||||
|
},
|
||||||
"dataLicense": "CC0-1.0",
|
"dataLicense": "CC0-1.0",
|
||||||
"documentNamespace": "https://anchore.com/syft/image/",
|
"documentNamespace": "https:/anchore.com/syft/dir/some/path-e94d6e43-5de5-4849-8e21-983ada9103c5",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
|
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
|
||||||
@@ -36,7 +57,35 @@
|
|||||||
],
|
],
|
||||||
"licenseDeclared": "MIT",
|
"licenseDeclared": "MIT",
|
||||||
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
|
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
|
||||||
"versionInfo": "1.0.1"
|
"versionInfo": "1.0.1",
|
||||||
|
"syftPackageData": {
|
||||||
|
"type": "python",
|
||||||
|
"foundBy": "the-cataloger-1",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/some/path/pkg1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"licenses": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"language": "python",
|
||||||
|
"metadataType": "PythonPackageMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"name": "package-1",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"license": "",
|
||||||
|
"author": "",
|
||||||
|
"authorEmail": "",
|
||||||
|
"platform": "",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "/some/path/pkg1/depedencies/foo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sitePackagesRootPath": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
|
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
|
||||||
@@ -58,7 +107,27 @@
|
|||||||
"filesAnalyzed": false,
|
"filesAnalyzed": false,
|
||||||
"licenseDeclared": "NONE",
|
"licenseDeclared": "NONE",
|
||||||
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
|
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
|
||||||
"versionInfo": "2.0.1"
|
"versionInfo": "2.0.1",
|
||||||
|
"syftPackageData": {
|
||||||
|
"type": "deb",
|
||||||
|
"foundBy": "the-cataloger-2",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/some/path/pkg1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadataType": "DpkgMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"package": "package-2",
|
||||||
|
"source": "",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"sourceVersion": "",
|
||||||
|
"architecture": "",
|
||||||
|
"maintainer": "",
|
||||||
|
"installedSize": 0,
|
||||||
|
"files": null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
{
|
||||||
|
"SPDXID": "SPDXRef-DOCUMENT",
|
||||||
|
"name": "user-image-input",
|
||||||
|
"spdxVersion": "SPDX-2.2",
|
||||||
|
"creationInfo": {
|
||||||
|
"created": "2021-10-11T01:54:40.61361Z",
|
||||||
|
"creators": [
|
||||||
|
"Organization: Anchore, Inc",
|
||||||
|
"Tool: syft-[not provided]"
|
||||||
|
],
|
||||||
|
"licenseListVersion": "3.14"
|
||||||
|
},
|
||||||
|
"syftSourceData": {
|
||||||
|
"Scheme": "ImageScheme",
|
||||||
|
"ImageMetadata": {
|
||||||
|
"userInput": "user-image-input",
|
||||||
|
"imageID": "sha256:2480160b55bec40c44d3b145c7b2c1c47160db8575c3dcae086d76b9370ae7ca",
|
||||||
|
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||||
|
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
|
"tags": [
|
||||||
|
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b"
|
||||||
|
],
|
||||||
|
"imageSize": 38,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
|
"digest": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59",
|
||||||
|
"size": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
|
"digest": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec",
|
||||||
|
"size": 16
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NjcsImRpZ2VzdCI6InNoYTI1NjoyNDgwMTYwYjU1YmVjNDBjNDRkM2IxNDVjN2IyYzFjNDcxNjBkYjg1NzVjM2RjYWUwODZkNzZiOTM3MGFlN2NhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmYjZiZWVjYjc1YjM5ZjRiYjgxM2RiZjE3N2U1MDFlZGQ1ZGRiM2U2OWJiNDVjZWRlYjc4YzY3NmVlMWI3YTU5In0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2OjMxOWI1ODhjZTY0MjUzYTg3YjUzM2M4ZWQwMWNmMDAyNWUwZWFjOThlN2I1MTZlMTI1MzI5NTdlMTI0NGZkZWMifV19",
|
||||||
|
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMTAtMDRUMTE6NDA6MDAuNjM4Mzk0NVoiLCJoaXN0b3J5IjpbeyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC41OTA3MzE2WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0xLnR4dCAvc29tZWZpbGUtMS50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn0seyJjcmVhdGVkIjoiMjAyMS0xMC0wNFQxMTo0MDowMC42MzgzOTQ1WiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmI2YmVlY2I3NWIzOWY0YmI4MTNkYmYxNzdlNTAxZWRkNWRkYjNlNjliYjQ1Y2VkZWI3OGM2NzZlZTFiN2E1OSIsInNoYTI1NjozMTliNTg4Y2U2NDI1M2E4N2I1MzNjOGVkMDFjZjAwMjVlMGVhYzk4ZTdiNTE2ZTEyNTMyOTU3ZTEyNDRmZGVjIl19fQ==",
|
||||||
|
"repoDigests": []
|
||||||
|
},
|
||||||
|
"Path": ""
|
||||||
|
},
|
||||||
|
"syftDistroData": {
|
||||||
|
"name": "debian",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"idLike": "like!"
|
||||||
|
},
|
||||||
|
"dataLicense": "CC0-1.0",
|
||||||
|
"documentNamespace": "https:/anchore.com/syft/image/user-image-input-9badd1fb-a067-4207-addf-243c5b7705fe",
|
||||||
|
"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",
|
||||||
|
"syftPackageData": {
|
||||||
|
"type": "python",
|
||||||
|
"foundBy": "the-cataloger-1",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/somefile-1.txt",
|
||||||
|
"layerID": "sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"licenses": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"language": "python",
|
||||||
|
"metadataType": "PythonPackageMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"name": "package-1",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"license": "",
|
||||||
|
"author": "",
|
||||||
|
"authorEmail": "",
|
||||||
|
"platform": "",
|
||||||
|
"sitePackagesRootPath": ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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",
|
||||||
|
"syftPackageData": {
|
||||||
|
"type": "deb",
|
||||||
|
"foundBy": "the-cataloger-2",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/somefile-2.txt",
|
||||||
|
"layerID": "sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"metadataType": "DpkgMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"package": "package-2",
|
||||||
|
"source": "",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"sourceVersion": "",
|
||||||
|
"architecture": "",
|
||||||
|
"maintainer": "",
|
||||||
|
"installedSize": 0,
|
||||||
|
"files": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,164 @@
|
|||||||
|
package spdx22json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||||
|
"github.com/anchore/syft/internal/spdxlicense"
|
||||||
|
"github.com/anchore/syft/internal/version"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// toFormatModel creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
|
||||||
|
func toFormatModel(catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro) model.Document {
|
||||||
|
name := documentName(srcMetadata)
|
||||||
|
packages, files, relationships := extractFromCatalog(catalog)
|
||||||
|
|
||||||
|
return model.Document{
|
||||||
|
Element: model.Element{
|
||||||
|
SPDXID: model.ElementID("DOCUMENT").String(),
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
SPDXVersion: model.Version,
|
||||||
|
CreationInfo: model.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: documentNamespace(name, srcMetadata),
|
||||||
|
Packages: packages,
|
||||||
|
Files: files,
|
||||||
|
Relationships: relationships,
|
||||||
|
SyftSourceData: srcMetadata,
|
||||||
|
SyftDistroData: toSyftDistroData(d),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftDistroData(d *distro.Distro) *model.SyftDistroData {
|
||||||
|
if d == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &model.SyftDistroData{
|
||||||
|
Name: d.Name(),
|
||||||
|
Version: d.FullVersion(),
|
||||||
|
IDLike: d.IDLike,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentName(srcMetadata *source.Metadata) string {
|
||||||
|
if srcMetadata != nil {
|
||||||
|
switch srcMetadata.Scheme {
|
||||||
|
case source.ImageScheme:
|
||||||
|
return cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
|
||||||
|
case source.DirectoryScheme:
|
||||||
|
return cleanSPDXName(srcMetadata.Path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: is this alright?
|
||||||
|
return uuid.Must(uuid.NewRandom()).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func documentNamespace(name string, srcMetadata *source.Metadata) string {
|
||||||
|
input := "unknown-source-type"
|
||||||
|
if srcMetadata != nil {
|
||||||
|
switch srcMetadata.Scheme {
|
||||||
|
case source.ImageScheme:
|
||||||
|
input = "image"
|
||||||
|
case source.DirectoryScheme:
|
||||||
|
input = "dir"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
uniqueID := uuid.Must(uuid.NewRandom())
|
||||||
|
identifier := path.Join(input, uniqueID.String())
|
||||||
|
if name != "." {
|
||||||
|
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.Join(anchoreNamespace, identifier)
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractFromCatalog(catalog *pkg.Catalog) ([]model.Package, []model.File, []model.Relationship) {
|
||||||
|
packages := make([]model.Package, 0)
|
||||||
|
relationships := make([]model.Relationship, 0)
|
||||||
|
files := make([]model.File, 0)
|
||||||
|
|
||||||
|
for _, p := range catalog.Sorted() {
|
||||||
|
license := spdxhelpers.License(p)
|
||||||
|
packageSpdxID := model.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String()
|
||||||
|
|
||||||
|
packageFiles, fileIDs, packageFileRelationships := spdxhelpers.Files(packageSpdxID, p)
|
||||||
|
files = append(files, packageFiles...)
|
||||||
|
|
||||||
|
relationships = append(relationships, packageFileRelationships...)
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
packages = append(packages, model.Package{
|
||||||
|
Description: spdxhelpers.Description(p),
|
||||||
|
DownloadLocation: spdxhelpers.DownloadLocation(p),
|
||||||
|
ExternalRefs: spdxhelpers.ExternalRefs(p),
|
||||||
|
FilesAnalyzed: false,
|
||||||
|
HasFiles: fileIDs,
|
||||||
|
Homepage: spdxhelpers.Homepage(p),
|
||||||
|
LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package
|
||||||
|
Originator: spdxhelpers.Originator(p),
|
||||||
|
SourceInfo: spdxhelpers.SourceInfo(p),
|
||||||
|
VersionInfo: p.Version,
|
||||||
|
Item: model.Item{
|
||||||
|
LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package
|
||||||
|
Element: model.Element{
|
||||||
|
SPDXID: packageSpdxID,
|
||||||
|
Name: p.Name,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
SyftPackageData: toSyftPackageData(p),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return packages, files, relationships
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftPackageData(p *pkg.Package) *model.SyftPackageData {
|
||||||
|
if p == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &model.SyftPackageData{
|
||||||
|
SyftPackageBasicData: model.SyftPackageBasicData{
|
||||||
|
PackageType: p.Type,
|
||||||
|
FoundBy: p.FoundBy,
|
||||||
|
Locations: p.Locations,
|
||||||
|
Language: p.Language,
|
||||||
|
Licenses: p.Licenses,
|
||||||
|
},
|
||||||
|
SyftPackageCustomData: model.SyftPackageCustomData{
|
||||||
|
MetadataType: p.MetadataType,
|
||||||
|
Metadata: p.Metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanSPDXName(name string) string {
|
||||||
|
// remove # according to specification
|
||||||
|
name = strings.ReplaceAll(name, "#", "-")
|
||||||
|
|
||||||
|
// remove : for url construction
|
||||||
|
name = strings.ReplaceAll(name, ":", "-")
|
||||||
|
|
||||||
|
// clean relative pathing
|
||||||
|
return path.Clean(name)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package spdx22json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
|
||||||
|
"github.com/anchore/syft/internal/formats/spdx22json/model"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toSyftModel(doc model.Document) (*pkg.Catalog, *source.Metadata, *distro.Distro, error) {
|
||||||
|
d, err := toSyftDistro(doc.SyftDistroData)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to parse distro info=%+v: %+v", d, err)
|
||||||
|
d = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return toSyftCatalog(doc.Packages), doc.SyftSourceData, d, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftCatalog(pkgs []model.Package) *pkg.Catalog {
|
||||||
|
catalog := pkg.NewCatalog()
|
||||||
|
for _, p := range pkgs {
|
||||||
|
catalog.Add(toSyftPackage(p))
|
||||||
|
}
|
||||||
|
return catalog
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftPackage(p model.Package) pkg.Package {
|
||||||
|
syftPkg := pkg.Package{
|
||||||
|
Name: p.Name,
|
||||||
|
Version: p.VersionInfo,
|
||||||
|
CPEs: spdxhelpers.ExtractCPEs(p.ExternalRefs),
|
||||||
|
PURL: spdxhelpers.ExtractPURL(p.ExternalRefs),
|
||||||
|
}
|
||||||
|
|
||||||
|
if extra := p.SyftPackageData; extra != nil {
|
||||||
|
syftPkg.Type = extra.PackageType
|
||||||
|
syftPkg.FoundBy = extra.FoundBy
|
||||||
|
syftPkg.Locations = extra.Locations
|
||||||
|
syftPkg.Language = extra.Language
|
||||||
|
syftPkg.Licenses = extra.Licenses
|
||||||
|
syftPkg.MetadataType = extra.MetadataType
|
||||||
|
syftPkg.Metadata = extra.Metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
//if syftPkg.Type == "" && syftPkg.PURL != "" {
|
||||||
|
// // TODO: extract package type from purl --this is useful for ingesting from tools other than syft and is important for grype
|
||||||
|
//}
|
||||||
|
|
||||||
|
return syftPkg
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftDistro(d *model.SyftDistroData) (*distro.Distro, error) {
|
||||||
|
if d == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
newDistro, err := distro.NewDistro(distro.Type(d.Name), d.Version, d.IDLike)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &newDistro, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package spdx22json
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validator(reader io.Reader) error {
|
||||||
|
type Document struct {
|
||||||
|
SPDXID string `json:"SPDXID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
|
var doc Document
|
||||||
|
err := dec.Decode(&doc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to decode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if doc.SPDXID != "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("could not extract document SPDXID")
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson/model"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func decoder(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, error) {
|
||||||
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
|
var doc model.Document
|
||||||
|
err := dec.Decode(&doc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, fmt.Errorf("unable to decode syft-json: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return toSyftModel(doc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
|
"github.com/go-test/deep"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncodeDecodeCycle(t *testing.T) {
|
||||||
|
testImage := "image-simple"
|
||||||
|
originalCatalog, originalMetadata, _ := testutils.ImageInput(t, testImage)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
assert.NoError(t, encoder(&buf, originalCatalog, &originalMetadata, nil))
|
||||||
|
|
||||||
|
actualCatalog, actualMetadata, _, err := decoder(bytes.NewReader(buf.Bytes()))
|
||||||
|
assert.NoError(t, err)
|
||||||
|
|
||||||
|
for _, d := range deep.Equal(originalMetadata, *actualMetadata) {
|
||||||
|
t.Errorf("metadata difference: %+v", d)
|
||||||
|
}
|
||||||
|
|
||||||
|
actualPackages := actualCatalog.Sorted()
|
||||||
|
for idx, p := range originalCatalog.Sorted() {
|
||||||
|
if !assert.Equal(t, p.Name, actualPackages[idx].Name) {
|
||||||
|
t.Errorf("different package at idx=%d: %s vs %s", idx, p.Name, actualPackages[idx].Name)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// ids will never be equal
|
||||||
|
p.ID = ""
|
||||||
|
actualPackages[idx].ID = ""
|
||||||
|
|
||||||
|
for _, d := range deep.Equal(*p, *actualPackages[idx]) {
|
||||||
|
if strings.Contains(d, ".VirtualPath: ") {
|
||||||
|
// location.Virtual path is not exposed in the json output
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(d, "<nil slice> != []") {
|
||||||
|
// semantically the same
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Errorf("package difference (%s): %+v", p.Name, d)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func encoder(output io.Writer, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro) error {
|
||||||
|
// TODO: application config not available yet
|
||||||
|
doc := ToFormatModel(catalog, srcMetadata, d, nil)
|
||||||
|
|
||||||
|
enc := json.NewEncoder(output)
|
||||||
|
// prevent > and < from being escaped in the payload
|
||||||
|
enc.SetEscapeHTML(false)
|
||||||
|
enc.SetIndent("", " ")
|
||||||
|
|
||||||
|
return enc.Encode(&doc)
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
|
)
|
||||||
|
|
||||||
|
var updateJson = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
||||||
|
|
||||||
|
func TestJSONDirectoryPresenter(t *testing.T) {
|
||||||
|
catalog, metadata, distro := testutils.DirectoryInput(t)
|
||||||
|
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||||
|
format.NewPresenter(encoder, catalog, &metadata, distro),
|
||||||
|
*updateJson,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestJSONImagePresenter(t *testing.T) {
|
||||||
|
testImage := "image-simple"
|
||||||
|
catalog, metadata, distro := testutils.ImageInput(t, testImage)
|
||||||
|
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||||
|
format.NewPresenter(encoder, catalog, &metadata, distro),
|
||||||
|
testImage,
|
||||||
|
*updateJson,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import "github.com/anchore/syft/syft/format"
|
||||||
|
|
||||||
|
func Format() format.Format {
|
||||||
|
return format.NewFormat(
|
||||||
|
format.JSONOption,
|
||||||
|
encoder,
|
||||||
|
decoder,
|
||||||
|
validator,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Distro provides information about a detected Linux Distro.
|
||||||
|
type Distro struct {
|
||||||
|
Name string `json:"name"` // Name of the Linux distribution
|
||||||
|
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
|
||||||
|
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
// Document represents the syft cataloging findings as a JSON document
|
||||||
|
type Document struct {
|
||||||
|
Artifacts []Package `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
||||||
|
ArtifactRelationships []Relationship `json:"artifactRelationships"`
|
||||||
|
Source Source `json:"source"` // Source represents the original object that was cataloged
|
||||||
|
Distro Distro `json:"distro"` // Distro represents the Linux distribution that was detected from the source
|
||||||
|
Descriptor Descriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
|
||||||
|
Schema Schema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
|
||||||
|
}
|
||||||
|
|
||||||
|
// Descriptor describes what created the document as well as surrounding metadata
|
||||||
|
type Descriptor struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Configuration interface{} `json:"configuration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Schema struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
URL string `json:"url"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Package represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
|
||||||
|
type Package struct {
|
||||||
|
PackageBasicData
|
||||||
|
PackageCustomData
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageBasicData contains non-ambiguous values (type-wise) from pkg.Package.
|
||||||
|
type PackageBasicData struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Type pkg.Type `json:"type"`
|
||||||
|
FoundBy string `json:"foundBy"`
|
||||||
|
Locations []source.Location `json:"locations"`
|
||||||
|
Licenses []string `json:"licenses"`
|
||||||
|
Language pkg.Language `json:"language"`
|
||||||
|
CPEs []string `json:"cpes"`
|
||||||
|
PURL string `json:"purl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PackageCustomData contains ambiguous values (type-wise) from pkg.Package.
|
||||||
|
type PackageCustomData struct {
|
||||||
|
MetadataType pkg.MetadataType `json:"metadataType"`
|
||||||
|
Metadata interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// packageMetadataUnpacker is all values needed from Package to disambiguate ambiguous fields during json unmarshaling.
|
||||||
|
type packageMetadataUnpacker struct {
|
||||||
|
MetadataType pkg.MetadataType `json:"metadataType"`
|
||||||
|
Metadata json.RawMessage `json:"metadata"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *packageMetadataUnpacker) String() string {
|
||||||
|
return fmt.Sprintf("metadataType: %s, metadata: %s", p.MetadataType, string(p.Metadata))
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON is a custom unmarshaller for handling basic values and values with ambiguous types.
|
||||||
|
// nolint:funlen
|
||||||
|
func (p *Package) UnmarshalJSON(b []byte) error {
|
||||||
|
var basic PackageBasicData
|
||||||
|
if err := json.Unmarshal(b, &basic); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.PackageBasicData = basic
|
||||||
|
|
||||||
|
var unpacker packageMetadataUnpacker
|
||||||
|
if err := json.Unmarshal(b, &unpacker); err != nil {
|
||||||
|
log.Warnf("failed to unmarshall into packageMetadataUnpacker: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
p.MetadataType = unpacker.MetadataType
|
||||||
|
|
||||||
|
switch p.MetadataType {
|
||||||
|
case pkg.ApkMetadataType:
|
||||||
|
var payload pkg.ApkMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.RpmdbMetadataType:
|
||||||
|
var payload pkg.RpmdbMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.DpkgMetadataType:
|
||||||
|
var payload pkg.DpkgMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.JavaMetadataType:
|
||||||
|
var payload pkg.JavaMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.RustCargoPackageMetadataType:
|
||||||
|
var payload pkg.CargoPackageMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.GemMetadataType:
|
||||||
|
var payload pkg.GemMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.KbPackageMetadataType:
|
||||||
|
var payload pkg.KbPackageMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.PythonPackageMetadataType:
|
||||||
|
var payload pkg.PythonPackageMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
case pkg.NpmPackageJSONMetadataType:
|
||||||
|
var payload pkg.NpmPackageJSONMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Metadata, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Metadata = payload
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type Relationship struct {
|
||||||
|
Parent string `json:"parent"`
|
||||||
|
Child string `json:"child"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Metadata interface{} `json:"metadata"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Source object represents the thing that was cataloged
|
||||||
|
type Source struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Target interface{} `json:"target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// sourceUnpacker is used to unmarshal Source objects
|
||||||
|
type sourceUnpacker struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Target json.RawMessage `json:"target"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON populates a source object from JSON bytes.
|
||||||
|
func (s *Source) UnmarshalJSON(b []byte) error {
|
||||||
|
var unpacker sourceUnpacker
|
||||||
|
if err := json.Unmarshal(b, &unpacker); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Type = unpacker.Type
|
||||||
|
|
||||||
|
switch s.Type {
|
||||||
|
case "directory":
|
||||||
|
s.Target = string(unpacker.Target[:])
|
||||||
|
case "image":
|
||||||
|
var payload source.ImageMetadata
|
||||||
|
if err := json.Unmarshal(unpacker.Target, &payload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.Target = payload
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported package metadata type: %+v", s.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Note: changes to this file will result in updating several test values. Consider making a new image fixture instead of editing this one.
|
||||||
|
FROM scratch
|
||||||
|
ADD file-1.txt /somefile-1.txt
|
||||||
|
ADD file-2.txt /somefile-2.txt
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
this file has contents
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
file-2 contents!
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
{
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"id": "package-1-id",
|
||||||
|
"name": "package-1",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"type": "python",
|
||||||
|
"foundBy": "the-cataloger-1",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/some/path/pkg1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"licenses": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"language": "python",
|
||||||
|
"cpes": [
|
||||||
|
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||||
|
],
|
||||||
|
"purl": "a-purl-2",
|
||||||
|
"metadataType": "PythonPackageMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"name": "package-1",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"license": "",
|
||||||
|
"author": "",
|
||||||
|
"authorEmail": "",
|
||||||
|
"platform": "",
|
||||||
|
"files": [
|
||||||
|
{
|
||||||
|
"path": "/some/path/pkg1/depedencies/foo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"sitePackagesRootPath": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "package-2-id",
|
||||||
|
"name": "package-2",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"type": "deb",
|
||||||
|
"foundBy": "the-cataloger-2",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/some/path/pkg1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"licenses": [],
|
||||||
|
"language": "",
|
||||||
|
"cpes": [
|
||||||
|
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||||
|
],
|
||||||
|
"purl": "a-purl-2",
|
||||||
|
"metadataType": "DpkgMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"package": "package-2",
|
||||||
|
"source": "",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"sourceVersion": "",
|
||||||
|
"architecture": "",
|
||||||
|
"maintainer": "",
|
||||||
|
"installedSize": 0,
|
||||||
|
"files": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactRelationships": [],
|
||||||
|
"source": {
|
||||||
|
"type": "directory",
|
||||||
|
"target": "/some/path"
|
||||||
|
},
|
||||||
|
"distro": {
|
||||||
|
"name": "debian",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"idLike": "like!"
|
||||||
|
},
|
||||||
|
"descriptor": {
|
||||||
|
"name": "syft",
|
||||||
|
"version": "[not provided]"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
{
|
||||||
|
"artifacts": [
|
||||||
|
{
|
||||||
|
"id": "package-1-id",
|
||||||
|
"name": "package-1",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"type": "python",
|
||||||
|
"foundBy": "the-cataloger-1",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/somefile-1.txt",
|
||||||
|
"layerID": "sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"licenses": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"language": "python",
|
||||||
|
"cpes": [
|
||||||
|
"cpe:2.3:*:some:package:1:*:*:*:*:*:*:*"
|
||||||
|
],
|
||||||
|
"purl": "a-purl-1",
|
||||||
|
"metadataType": "PythonPackageMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"name": "package-1",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"license": "",
|
||||||
|
"author": "",
|
||||||
|
"authorEmail": "",
|
||||||
|
"platform": "",
|
||||||
|
"sitePackagesRootPath": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "package-2-id",
|
||||||
|
"name": "package-2",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"type": "deb",
|
||||||
|
"foundBy": "the-cataloger-2",
|
||||||
|
"locations": [
|
||||||
|
{
|
||||||
|
"path": "/somefile-2.txt",
|
||||||
|
"layerID": "sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"licenses": [],
|
||||||
|
"language": "",
|
||||||
|
"cpes": [
|
||||||
|
"cpe:2.3:*:some:package:2:*:*:*:*:*:*:*"
|
||||||
|
],
|
||||||
|
"purl": "a-purl-2",
|
||||||
|
"metadataType": "DpkgMetadata",
|
||||||
|
"metadata": {
|
||||||
|
"package": "package-2",
|
||||||
|
"source": "",
|
||||||
|
"version": "2.0.1",
|
||||||
|
"sourceVersion": "",
|
||||||
|
"architecture": "",
|
||||||
|
"maintainer": "",
|
||||||
|
"installedSize": 0,
|
||||||
|
"files": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"artifactRelationships": [],
|
||||||
|
"source": {
|
||||||
|
"type": "image",
|
||||||
|
"target": {
|
||||||
|
"userInput": "user-image-input",
|
||||||
|
"imageID": "sha256:112851310e48e604f7379e2a3acddab50e91ce926edacb598a532e60ff6b776a",
|
||||||
|
"manifestDigest": "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368",
|
||||||
|
"mediaType": "application/vnd.docker.distribution.manifest.v2+json",
|
||||||
|
"tags": [
|
||||||
|
"stereoscope-fixture-image-simple:85066c51088bdd274f7a89e99e00490f666c49e72ffc955707cd6e18f0e22c5b"
|
||||||
|
],
|
||||||
|
"imageSize": 38,
|
||||||
|
"layers": [
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
|
"digest": "sha256:ffb5e9eaa453a002110719d12c294960117ca2903953d1faa40f01dc3f77045c",
|
||||||
|
"size": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
|
||||||
|
"digest": "sha256:8463854829fc53d47b9dcdf7ee79fe7eb4ca7933c910f67f8521412f7a2f5c21",
|
||||||
|
"size": 16
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"manifest": "eyJzY2hlbWFWZXJzaW9uIjoyLCJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmRpc3RyaWJ1dGlvbi5tYW5pZmVzdC52Mitqc29uIiwiY29uZmlnIjp7Im1lZGlhVHlwZSI6ImFwcGxpY2F0aW9uL3ZuZC5kb2NrZXIuY29udGFpbmVyLmltYWdlLnYxK2pzb24iLCJzaXplIjo2NzMsImRpZ2VzdCI6InNoYTI1NjoxMTI4NTEzMTBlNDhlNjA0ZjczNzllMmEzYWNkZGFiNTBlOTFjZTkyNmVkYWNiNTk4YTUzMmU2MGZmNmI3NzZhIn0sImxheWVycyI6W3sibWVkaWFUeXBlIjoiYXBwbGljYXRpb24vdm5kLmRvY2tlci5pbWFnZS5yb290ZnMuZGlmZi50YXIuZ3ppcCIsInNpemUiOjIwNDgsImRpZ2VzdCI6InNoYTI1NjpmZmI1ZTllYWE0NTNhMDAyMTEwNzE5ZDEyYzI5NDk2MDExN2NhMjkwMzk1M2QxZmFhNDBmMDFkYzNmNzcwNDVjIn0seyJtZWRpYVR5cGUiOiJhcHBsaWNhdGlvbi92bmQuZG9ja2VyLmltYWdlLnJvb3Rmcy5kaWZmLnRhci5nemlwIiwic2l6ZSI6MjA0OCwiZGlnZXN0Ijoic2hhMjU2Ojg0NjM4NTQ4MjlmYzUzZDQ3YjlkY2RmN2VlNzlmZTdlYjRjYTc5MzNjOTEwZjY3Zjg1MjE0MTJmN2EyZjVjMjEifV19",
|
||||||
|
"config": "eyJhcmNoaXRlY3R1cmUiOiJhbWQ2NCIsImNvbmZpZyI6eyJFbnYiOlsiUEFUSD0vdXNyL2xvY2FsL3NiaW46L3Vzci9sb2NhbC9iaW46L3Vzci9zYmluOi91c3IvYmluOi9zYmluOi9iaW4iXSwiV29ya2luZ0RpciI6Ii8iLCJPbkJ1aWxkIjpudWxsfSwiY3JlYXRlZCI6IjIwMjEtMDktMDhUMTc6MjE6NTguODk2NTI5MTkyWiIsImhpc3RvcnkiOlt7ImNyZWF0ZWQiOiIyMDIxLTA5LTA4VDE3OjIxOjU4Ljg3OTY5MDgyNFoiLCJjcmVhdGVkX2J5IjoiQUREIGZpbGUtMS50eHQgL3NvbWVmaWxlLTEudHh0ICMgYnVpbGRraXQiLCJjb21tZW50IjoiYnVpbGRraXQuZG9ja2VyZmlsZS52MCJ9LHsiY3JlYXRlZCI6IjIwMjEtMDktMDhUMTc6MjE6NTguODk2NTI5MTkyWiIsImNyZWF0ZWRfYnkiOiJBREQgZmlsZS0yLnR4dCAvc29tZWZpbGUtMi50eHQgIyBidWlsZGtpdCIsImNvbW1lbnQiOiJidWlsZGtpdC5kb2NrZXJmaWxlLnYwIn1dLCJvcyI6ImxpbnV4Iiwicm9vdGZzIjp7InR5cGUiOiJsYXllcnMiLCJkaWZmX2lkcyI6WyJzaGEyNTY6ZmZiNWU5ZWFhNDUzYTAwMjExMDcxOWQxMmMyOTQ5NjAxMTdjYTI5MDM5NTNkMWZhYTQwZjAxZGMzZjc3MDQ1YyIsInNoYTI1Njo4NDYzODU0ODI5ZmM1M2Q0N2I5ZGNkZjdlZTc5ZmU3ZWI0Y2E3OTMzYzkxMGY2N2Y4NTIxNDEyZjdhMmY1YzIxIl19fQ==",
|
||||||
|
"repoDigests": [],
|
||||||
|
"scope": "Squashed"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"distro": {
|
||||||
|
"name": "debian",
|
||||||
|
"version": "1.2.3",
|
||||||
|
"idLike": "like!"
|
||||||
|
},
|
||||||
|
"descriptor": {
|
||||||
|
"name": "syft",
|
||||||
|
"version": "[not provided]"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"url": "https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-1.1.0.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
Binary file not shown.
@@ -0,0 +1,135 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal"
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson/model"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/internal/version"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
//// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
|
||||||
|
//func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (Document, error) {
|
||||||
|
//
|
||||||
|
//}
|
||||||
|
|
||||||
|
// TODO: this is export4ed for the use of the power-user command (temp)
|
||||||
|
func ToFormatModel(catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro, applicationConfig interface{}) model.Document {
|
||||||
|
src, err := toSourceModel(srcMetadata)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("unable to create syft-json source object: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.Document{
|
||||||
|
Artifacts: toPackageModels(catalog),
|
||||||
|
ArtifactRelationships: toRelationshipModel(pkg.NewRelationships(catalog)),
|
||||||
|
Source: src,
|
||||||
|
Distro: toDistroModel(d),
|
||||||
|
Descriptor: model.Descriptor{
|
||||||
|
Name: internal.ApplicationName,
|
||||||
|
Version: version.FromBuild().Version,
|
||||||
|
Configuration: applicationConfig,
|
||||||
|
},
|
||||||
|
Schema: model.Schema{
|
||||||
|
Version: internal.JSONSchemaVersion,
|
||||||
|
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toPackageModels(catalog *pkg.Catalog) []model.Package {
|
||||||
|
artifacts := make([]model.Package, 0)
|
||||||
|
if catalog == nil {
|
||||||
|
return artifacts
|
||||||
|
}
|
||||||
|
for _, p := range catalog.Sorted() {
|
||||||
|
artifacts = append(artifacts, toPackageModel(p))
|
||||||
|
}
|
||||||
|
return artifacts
|
||||||
|
}
|
||||||
|
|
||||||
|
// toPackageModel crates a new Package from the given pkg.Package.
|
||||||
|
func toPackageModel(p *pkg.Package) model.Package {
|
||||||
|
var cpes = make([]string, len(p.CPEs))
|
||||||
|
for i, c := range p.CPEs {
|
||||||
|
cpes[i] = c.BindToFmtString()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensure collections are never nil for presentation reasons
|
||||||
|
var locations = make([]source.Location, 0)
|
||||||
|
if p.Locations != nil {
|
||||||
|
locations = p.Locations
|
||||||
|
}
|
||||||
|
|
||||||
|
var licenses = make([]string, 0)
|
||||||
|
if p.Licenses != nil {
|
||||||
|
licenses = p.Licenses
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.Package{
|
||||||
|
PackageBasicData: model.PackageBasicData{
|
||||||
|
ID: string(p.ID),
|
||||||
|
Name: p.Name,
|
||||||
|
Version: p.Version,
|
||||||
|
Type: p.Type,
|
||||||
|
FoundBy: p.FoundBy,
|
||||||
|
Locations: locations,
|
||||||
|
Licenses: licenses,
|
||||||
|
Language: p.Language,
|
||||||
|
CPEs: cpes,
|
||||||
|
PURL: p.PURL,
|
||||||
|
},
|
||||||
|
PackageCustomData: model.PackageCustomData{
|
||||||
|
MetadataType: p.MetadataType,
|
||||||
|
Metadata: p.Metadata,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toRelationshipModel(relationships []pkg.Relationship) []model.Relationship {
|
||||||
|
result := make([]model.Relationship, len(relationships))
|
||||||
|
for i, r := range relationships {
|
||||||
|
result[i] = model.Relationship{
|
||||||
|
Parent: string(r.Parent),
|
||||||
|
Child: string(r.Child),
|
||||||
|
Type: string(r.Type),
|
||||||
|
Metadata: r.Metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// toSourceModel creates a new source object to be represented into JSON.
|
||||||
|
func toSourceModel(src *source.Metadata) (model.Source, error) {
|
||||||
|
switch src.Scheme {
|
||||||
|
case source.ImageScheme:
|
||||||
|
return model.Source{
|
||||||
|
Type: "image",
|
||||||
|
Target: src.ImageMetadata,
|
||||||
|
}, nil
|
||||||
|
case source.DirectoryScheme:
|
||||||
|
return model.Source{
|
||||||
|
Type: "directory",
|
||||||
|
Target: src.Path,
|
||||||
|
}, nil
|
||||||
|
default:
|
||||||
|
return model.Source{}, fmt.Errorf("unsupported source: %q", src.Scheme)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// toDistroModel creates a struct with the Linux distribution to be represented in JSON.
|
||||||
|
func toDistroModel(d *distro.Distro) model.Distro {
|
||||||
|
if d == nil {
|
||||||
|
return model.Distro{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return model.Distro{
|
||||||
|
Name: d.Name(),
|
||||||
|
Version: d.FullVersion(),
|
||||||
|
IDLike: d.IDLike,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson/model"
|
||||||
|
"github.com/anchore/syft/internal/log"
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
func toSyftModel(doc model.Document) (*pkg.Catalog, *source.Metadata, *distro.Distro, error) {
|
||||||
|
dist, err := distro.NewDistro(distro.Type(doc.Distro.Name), doc.Distro.Version, doc.Distro.IDLike)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return toSyftCatalog(doc.Artifacts), toSyftSourceMetadata(doc.Source), &dist, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftSourceMetadata(s model.Source) *source.Metadata {
|
||||||
|
switch s.Type {
|
||||||
|
case "directory":
|
||||||
|
return &source.Metadata{
|
||||||
|
Scheme: source.DirectoryScheme,
|
||||||
|
Path: s.Target.(string),
|
||||||
|
}
|
||||||
|
case "image":
|
||||||
|
return &source.Metadata{
|
||||||
|
Scheme: source.ImageScheme,
|
||||||
|
ImageMetadata: s.Target.(source.ImageMetadata),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftCatalog(pkgs []model.Package) *pkg.Catalog {
|
||||||
|
catalog := pkg.NewCatalog()
|
||||||
|
for _, p := range pkgs {
|
||||||
|
catalog.Add(toSyftPackage(p))
|
||||||
|
}
|
||||||
|
return catalog
|
||||||
|
}
|
||||||
|
|
||||||
|
func toSyftPackage(p model.Package) pkg.Package {
|
||||||
|
var cpes []pkg.CPE
|
||||||
|
for _, c := range p.CPEs {
|
||||||
|
value, err := pkg.NewCPE(c)
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("excluding invalid CPE %q: %v", c, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cpes = append(cpes, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pkg.Package{
|
||||||
|
ID: pkg.ID(p.ID),
|
||||||
|
Name: p.Name,
|
||||||
|
Version: p.Version,
|
||||||
|
FoundBy: p.FoundBy,
|
||||||
|
Locations: p.Locations,
|
||||||
|
Licenses: p.Licenses,
|
||||||
|
Language: p.Language,
|
||||||
|
Type: p.Type,
|
||||||
|
CPEs: cpes,
|
||||||
|
PURL: p.PURL,
|
||||||
|
MetadataType: p.MetadataType,
|
||||||
|
Metadata: p.Metadata,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package syftjson
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func validator(reader io.Reader) error {
|
||||||
|
type Document struct {
|
||||||
|
Schema model.Schema `json:"schema"`
|
||||||
|
}
|
||||||
|
|
||||||
|
dec := json.NewDecoder(reader)
|
||||||
|
|
||||||
|
var doc Document
|
||||||
|
err := dec.Decode(&doc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("unable to decode: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// note: we accept al schema versions
|
||||||
|
// TODO: add per-schema version parsing
|
||||||
|
if strings.Contains(doc.Schema.URL, "anchore/syft") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("could not extract syft schema")
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,15 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
|
||||||
|
|
||||||
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
||||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
catalog, metadata, _ := testutils.DirectoryInput(t)
|
||||||
assertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||||
NewCycloneDxPresenter(catalog, metadata),
|
NewCycloneDxPresenter(catalog, metadata),
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
cycloneDxRedactor,
|
cycloneDxRedactor,
|
||||||
@@ -19,8 +21,8 @@ func TestCycloneDxDirectoryPresenter(t *testing.T) {
|
|||||||
|
|
||||||
func TestCycloneDxImagePresenter(t *testing.T) {
|
func TestCycloneDxImagePresenter(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
catalog, metadata, _ := testutils.ImageInput(t, testImage)
|
||||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||||
NewCycloneDxPresenter(catalog, metadata),
|
NewCycloneDxPresenter(catalog, metadata),
|
||||||
testImage,
|
testImage,
|
||||||
*updateCycloneDx,
|
*updateCycloneDx,
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import "github.com/anchore/syft/syft/distro"
|
|
||||||
|
|
||||||
// JSONDistribution provides information about a detected Linux JSONDistribution.
|
|
||||||
type JSONDistribution struct {
|
|
||||||
Name string `json:"name"` // Name of the Linux distribution
|
|
||||||
Version string `json:"version"` // Version of the Linux distribution (major or major.minor version)
|
|
||||||
IDLike string `json:"idLike"` // the ID_LIKE field found within the /etc/os-release file
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewJSONDistribution creates a struct with the Linux distribution to be represented in JSON.
|
|
||||||
func NewJSONDistribution(d *distro.Distro) JSONDistribution {
|
|
||||||
if d == nil {
|
|
||||||
return JSONDistribution{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSONDistribution{
|
|
||||||
Name: d.Name(),
|
|
||||||
Version: d.FullVersion(),
|
|
||||||
IDLike: d.IDLike,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
|
||||||
"github.com/anchore/syft/internal/version"
|
|
||||||
"github.com/anchore/syft/syft/distro"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSONDocument represents the syft cataloging findings as a JSON document
|
|
||||||
type JSONDocument struct {
|
|
||||||
Artifacts []JSONPackage `json:"artifacts"` // Artifacts is the list of packages discovered and placed into the catalog
|
|
||||||
ArtifactRelationships []JSONRelationship `json:"artifactRelationships"`
|
|
||||||
Source JSONSource `json:"source"` // Source represents the original object that was cataloged
|
|
||||||
Distro JSONDistribution `json:"distro"` // Distro represents the Linux distribution that was detected from the source
|
|
||||||
Descriptor JSONDescriptor `json:"descriptor"` // Descriptor is a block containing self-describing information about syft
|
|
||||||
Schema JSONSchema `json:"schema"` // Schema is a block reserved for defining the version for the shape of this JSON document and where to find the schema document to validate the shape
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
|
|
||||||
func NewJSONDocument(catalog *pkg.Catalog, srcMetadata source.Metadata, d *distro.Distro, scope source.Scope, configuration interface{}) (JSONDocument, error) {
|
|
||||||
src, err := NewJSONSource(srcMetadata, scope)
|
|
||||||
if err != nil {
|
|
||||||
return JSONDocument{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
artifacts, err := NewJSONPackages(catalog)
|
|
||||||
if err != nil {
|
|
||||||
return JSONDocument{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSONDocument{
|
|
||||||
Artifacts: artifacts,
|
|
||||||
ArtifactRelationships: newJSONRelationships(pkg.NewRelationships(catalog)),
|
|
||||||
Source: src,
|
|
||||||
Distro: NewJSONDistribution(d),
|
|
||||||
Descriptor: JSONDescriptor{
|
|
||||||
Name: internal.ApplicationName,
|
|
||||||
Version: version.FromBuild().Version,
|
|
||||||
Configuration: configuration,
|
|
||||||
},
|
|
||||||
Schema: JSONSchema{
|
|
||||||
Version: internal.JSONSchemaVersion,
|
|
||||||
URL: fmt.Sprintf("https://raw.githubusercontent.com/anchore/syft/main/schema/json/schema-%s.json", internal.JSONSchemaVersion),
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// JSONDescriptor describes what created the document as well as surrounding metadata
|
|
||||||
type JSONDescriptor struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Configuration interface{} `json:"configuration,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONSchema struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
URL string `json:"url"`
|
|
||||||
}
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSONPackage represents a pkg.Package object specialized for JSON marshaling and unmarshaling.
|
|
||||||
type JSONPackage struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
FoundBy string `json:"foundBy"`
|
|
||||||
Locations []source.Location `json:"locations"`
|
|
||||||
Licenses []string `json:"licenses"`
|
|
||||||
Language string `json:"language"`
|
|
||||||
CPEs []string `json:"cpes"`
|
|
||||||
PURL string `json:"purl"`
|
|
||||||
MetadataType string `json:"metadataType"`
|
|
||||||
Metadata interface{} `json:"metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewJSONPackages(catalog *pkg.Catalog) ([]JSONPackage, error) {
|
|
||||||
artifacts := make([]JSONPackage, 0)
|
|
||||||
if catalog == nil {
|
|
||||||
return artifacts, nil
|
|
||||||
}
|
|
||||||
for _, p := range catalog.Sorted() {
|
|
||||||
art, err := NewJSONPackage(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
artifacts = append(artifacts, art)
|
|
||||||
}
|
|
||||||
return artifacts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewJSONPackage crates a new JSONPackage from the given pkg.Package.
|
|
||||||
func NewJSONPackage(p *pkg.Package) (JSONPackage, error) {
|
|
||||||
var cpes = make([]string, len(p.CPEs))
|
|
||||||
for i, c := range p.CPEs {
|
|
||||||
cpes[i] = c.BindToFmtString()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ensure collections are never nil for presentation reasons
|
|
||||||
var locations = make([]source.Location, 0)
|
|
||||||
if p.Locations != nil {
|
|
||||||
locations = p.Locations
|
|
||||||
}
|
|
||||||
|
|
||||||
var licenses = make([]string, 0)
|
|
||||||
if p.Licenses != nil {
|
|
||||||
licenses = p.Licenses
|
|
||||||
}
|
|
||||||
|
|
||||||
return JSONPackage{
|
|
||||||
ID: string(p.ID),
|
|
||||||
Name: p.Name,
|
|
||||||
Version: p.Version,
|
|
||||||
Type: string(p.Type),
|
|
||||||
FoundBy: p.FoundBy,
|
|
||||||
Locations: locations,
|
|
||||||
Licenses: licenses,
|
|
||||||
Language: string(p.Language),
|
|
||||||
CPEs: cpes,
|
|
||||||
PURL: p.PURL,
|
|
||||||
MetadataType: string(p.MetadataType),
|
|
||||||
Metadata: p.Metadata,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/distro"
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSONPresenter is a JSON presentation object for the syft results
|
|
||||||
type JSONPresenter struct {
|
|
||||||
catalog *pkg.Catalog
|
|
||||||
srcMetadata source.Metadata
|
|
||||||
distro *distro.Distro
|
|
||||||
scope source.Scope
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
|
|
||||||
func NewJSONPresenter(catalog *pkg.Catalog, s source.Metadata, d *distro.Distro, scope source.Scope) *JSONPresenter {
|
|
||||||
return &JSONPresenter{
|
|
||||||
catalog: catalog,
|
|
||||||
srcMetadata: s,
|
|
||||||
distro: d,
|
|
||||||
scope: scope,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Present the catalog results to the given writer.
|
|
||||||
func (pres *JSONPresenter) Present(output io.Writer) error {
|
|
||||||
// we do not pass in configuration for backwards compatibility
|
|
||||||
doc, err := NewJSONDocument(pres.catalog, pres.srcMetadata, pres.distro, pres.scope, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
enc := json.NewEncoder(output)
|
|
||||||
// prevent > and < from being escaped in the payload
|
|
||||||
enc.SetEscapeHTML(false)
|
|
||||||
enc.SetIndent("", " ")
|
|
||||||
return enc.Encode(&doc)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"flag"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/pkg"
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
|
|
||||||
|
|
||||||
func must(c pkg.CPE, e error) pkg.CPE {
|
|
||||||
if e != nil {
|
|
||||||
panic(e)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJSONDirectoryPresenter(t *testing.T) {
|
|
||||||
catalog, metadata, dist := presenterDirectoryInput(t)
|
|
||||||
assertPresenterAgainstGoldenSnapshot(t,
|
|
||||||
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
|
|
||||||
*updateJSONGoldenFiles,
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestJSONImagePresenter(t *testing.T) {
|
|
||||||
testImage := "image-simple"
|
|
||||||
catalog, metadata, dist := presenterImageInput(t, testImage)
|
|
||||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
|
||||||
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
|
|
||||||
testImage,
|
|
||||||
*updateJSONGoldenFiles,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import "github.com/anchore/syft/syft/pkg"
|
|
||||||
|
|
||||||
type JSONRelationship struct {
|
|
||||||
Parent string `json:"parent"`
|
|
||||||
Child string `json:"child"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Metadata interface{} `json:"metadata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func newJSONRelationships(relationships []pkg.Relationship) []JSONRelationship {
|
|
||||||
result := make([]JSONRelationship, len(relationships))
|
|
||||||
for i, r := range relationships {
|
|
||||||
result[i] = JSONRelationship{
|
|
||||||
Parent: string(r.Parent),
|
|
||||||
Child: string(r.Child),
|
|
||||||
Type: string(r.Type),
|
|
||||||
Metadata: r.Metadata,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/anchore/syft/syft/source"
|
|
||||||
)
|
|
||||||
|
|
||||||
// JSONSource object represents the thing that was cataloged
|
|
||||||
type JSONSource struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Target interface{} `json:"target"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type JSONImageSource struct {
|
|
||||||
source.ImageMetadata
|
|
||||||
Scope source.Scope `json:"scope"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewJSONSource creates a new source object to be represented into JSON.
|
|
||||||
func NewJSONSource(src source.Metadata, scope source.Scope) (JSONSource, error) {
|
|
||||||
switch src.Scheme {
|
|
||||||
case source.ImageScheme:
|
|
||||||
return JSONSource{
|
|
||||||
Type: "image",
|
|
||||||
Target: JSONImageSource{
|
|
||||||
Scope: scope,
|
|
||||||
ImageMetadata: src.ImageMetadata,
|
|
||||||
},
|
|
||||||
}, nil
|
|
||||||
case source.DirectoryScheme:
|
|
||||||
return JSONSource{
|
|
||||||
Type: "directory",
|
|
||||||
Target: src.Path,
|
|
||||||
}, nil
|
|
||||||
default:
|
|
||||||
return JSONSource{}, fmt.Errorf("unsupported source: %q", src.Scheme)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,207 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"crypto/sha256"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"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 getSPDXFiles(packageSpdxID string, p *pkg.Package) (files []spdx22.File, fileIDs []string, relationships []spdx22.Relationship) {
|
|
||||||
files = make([]spdx22.File, 0)
|
|
||||||
fileIDs = make([]string, 0)
|
|
||||||
relationships = make([]spdx22.Relationship, 0)
|
|
||||||
|
|
||||||
pkgFileOwner, ok := p.Metadata.(pkg.FileOwner)
|
|
||||||
if !ok {
|
|
||||||
return files, fileIDs, relationships
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ownedFilePath := range pkgFileOwner.OwnedFiles() {
|
|
||||||
baseFileName := filepath.Base(ownedFilePath)
|
|
||||||
pathHash := sha256.Sum256([]byte(ownedFilePath))
|
|
||||||
fileSpdxID := spdx22.ElementID(fmt.Sprintf("File-%s-%x", p.Name, pathHash)).String()
|
|
||||||
|
|
||||||
fileIDs = append(fileIDs, fileSpdxID)
|
|
||||||
|
|
||||||
files = append(files, spdx22.File{
|
|
||||||
FileName: ownedFilePath,
|
|
||||||
Item: spdx22.Item{
|
|
||||||
Element: spdx22.Element{
|
|
||||||
SPDXID: fileSpdxID,
|
|
||||||
Name: baseFileName,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
relationships = append(relationships, spdx22.Relationship{
|
|
||||||
SpdxElementID: packageSpdxID,
|
|
||||||
RelationshipType: spdx22.ContainsRelationship,
|
|
||||||
RelatedSpdxElement: fileSpdxID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return files, fileIDs, relationships
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,527 +0,0 @@
|
|||||||
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",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "debian to spdx conversion",
|
|
||||||
input: pkg.Package{
|
|
||||||
Licenses: []string{
|
|
||||||
"GPL-2",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
expected: "GPL-2.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))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
package packages
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"path"
|
|
||||||
"strings"
|
|
||||||
"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"
|
|
||||||
"github.com/google/uuid"
|
|
||||||
)
|
|
||||||
|
|
||||||
const anchoreNamespace = "https://anchore.com/syft"
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// newSPDXJsonDocument creates and populates a new JSON document struct that follows the SPDX 2.2 spec from the given cataloging results.
|
|
||||||
func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document {
|
|
||||||
uniqueID := uuid.Must(uuid.NewRandom())
|
|
||||||
|
|
||||||
var name, input, identifier string
|
|
||||||
switch srcMetadata.Scheme {
|
|
||||||
case source.ImageScheme:
|
|
||||||
name = cleanSPDXName(srcMetadata.ImageMetadata.UserInput)
|
|
||||||
input = "image"
|
|
||||||
case source.DirectoryScheme:
|
|
||||||
name = cleanSPDXName(srcMetadata.Path)
|
|
||||||
input = "dir"
|
|
||||||
}
|
|
||||||
|
|
||||||
if name != "." {
|
|
||||||
identifier = path.Join(input, fmt.Sprintf("%s-%s", name, uniqueID.String()))
|
|
||||||
} else {
|
|
||||||
identifier = path.Join(input, uniqueID.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace := path.Join(anchoreNamespace, identifier)
|
|
||||||
packages, files, relationships := newSPDXJsonElements(catalog)
|
|
||||||
|
|
||||||
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: namespace,
|
|
||||||
Packages: packages,
|
|
||||||
Files: files,
|
|
||||||
Relationships: relationships,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newSPDXJsonElements(catalog *pkg.Catalog) ([]spdx22.Package, []spdx22.File, []spdx22.Relationship) {
|
|
||||||
packages := make([]spdx22.Package, 0)
|
|
||||||
relationships := make([]spdx22.Relationship, 0)
|
|
||||||
files := make([]spdx22.File, 0)
|
|
||||||
|
|
||||||
for _, p := range catalog.Sorted() {
|
|
||||||
license := getSPDXLicense(p)
|
|
||||||
packageSpdxID := spdx22.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String()
|
|
||||||
|
|
||||||
packageFiles, fileIDs, packageFileRelationships := getSPDXFiles(packageSpdxID, p)
|
|
||||||
files = append(files, packageFiles...)
|
|
||||||
|
|
||||||
relationships = append(relationships, packageFileRelationships...)
|
|
||||||
|
|
||||||
// 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).
|
|
||||||
packages = append(packages, spdx22.Package{
|
|
||||||
Description: getSPDXDescription(p),
|
|
||||||
DownloadLocation: getSPDXDownloadLocation(p),
|
|
||||||
ExternalRefs: getSPDXExternalRefs(p),
|
|
||||||
FilesAnalyzed: false,
|
|
||||||
HasFiles: fileIDs,
|
|
||||||
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: packageSpdxID,
|
|
||||||
Name: p.Name,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return packages, files, relationships
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanSPDXName(name string) string {
|
|
||||||
// remove # according to specification
|
|
||||||
name = strings.Replace(name, "#", "-", -1)
|
|
||||||
|
|
||||||
// remove : for url construction
|
|
||||||
name = strings.Replace(name, ":", "-", -1)
|
|
||||||
|
|
||||||
// clean relative pathing
|
|
||||||
return path.Clean(name)
|
|
||||||
}
|
|
||||||
@@ -5,6 +5,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/spdxhelpers"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/spdxlicense"
|
"github.com/anchore/syft/internal/spdxlicense"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal"
|
"github.com/anchore/syft/internal"
|
||||||
@@ -114,7 +116,7 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_
|
|||||||
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
|
// 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
|
// 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.
|
// the Comments on License field (section 3.16) is preferred.
|
||||||
license := getSPDXLicense(p)
|
license := spdxhelpers.License(p)
|
||||||
|
|
||||||
results[spdx.ElementID(id)] = &spdx.Package2_2{
|
results[spdx.ElementID(id)] = &spdx.Package2_2{
|
||||||
|
|
||||||
@@ -274,7 +276,7 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_
|
|||||||
}
|
}
|
||||||
|
|
||||||
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
|
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
|
||||||
for _, ref := range getSPDXExternalRefs(p) {
|
for _, ref := range spdxhelpers.ExternalRefs(p) {
|
||||||
refs = append(refs, &spdx.PackageExternalReference2_2{
|
refs = append(refs, &spdx.PackageExternalReference2_2{
|
||||||
Category: string(ref.ReferenceCategory),
|
Category: string(ref.ReferenceCategory),
|
||||||
RefType: string(ref.ReferenceType),
|
RefType: string(ref.ReferenceType),
|
||||||
|
|||||||
@@ -4,13 +4,15 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"regexp"
|
"regexp"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
|
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
|
||||||
|
|
||||||
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
||||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
catalog, metadata, _ := testutils.DirectoryInput(t)
|
||||||
assertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||||
NewSPDXTagValuePresenter(catalog, metadata),
|
NewSPDXTagValuePresenter(catalog, metadata),
|
||||||
*updateSpdxTagValue,
|
*updateSpdxTagValue,
|
||||||
spdxTagValueRedactor,
|
spdxTagValueRedactor,
|
||||||
@@ -19,8 +21,8 @@ func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
|
|||||||
|
|
||||||
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
func TestSPDXTagValueImagePresenter(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
catalog, metadata, _ := testutils.ImageInput(t, testImage)
|
||||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||||
NewSPDXTagValuePresenter(catalog, metadata),
|
NewSPDXTagValuePresenter(catalog, metadata),
|
||||||
testImage,
|
testImage,
|
||||||
*updateSpdxTagValue,
|
*updateSpdxTagValue,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
|
|
||||||
"github.com/go-test/deep"
|
"github.com/go-test/deep"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -11,8 +13,8 @@ var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update t
|
|||||||
|
|
||||||
func TestTablePresenter(t *testing.T) {
|
func TestTablePresenter(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
catalog, _, _ := presenterImageInput(t, testImage)
|
catalog, _, _ := testutils.ImageInput(t, testImage)
|
||||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||||
NewTablePresenter(catalog),
|
NewTablePresenter(catalog),
|
||||||
testImage,
|
testImage,
|
||||||
*updateTablePresenterGoldenFiles,
|
*updateTablePresenterGoldenFiles,
|
||||||
|
|||||||
@@ -1,61 +0,0 @@
|
|||||||
{
|
|
||||||
"SPDXID": "SPDXRef-DOCUMENT",
|
|
||||||
"name": "user-image-input",
|
|
||||||
"spdxVersion": "SPDX-2.2",
|
|
||||||
"creationInfo": {
|
|
||||||
"created": "2021-09-16T20:44:35.203911Z",
|
|
||||||
"creators": [
|
|
||||||
"Organization: Anchore, Inc",
|
|
||||||
"Tool: syft-[not provided]"
|
|
||||||
],
|
|
||||||
"licenseListVersion": "3.14"
|
|
||||||
},
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,15 @@ package packages
|
|||||||
import (
|
import (
|
||||||
"flag"
|
"flag"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/internal/formats/common/testutils"
|
||||||
)
|
)
|
||||||
|
|
||||||
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
|
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
|
||||||
|
|
||||||
func TestTextDirectoryPresenter(t *testing.T) {
|
func TestTextDirectoryPresenter(t *testing.T) {
|
||||||
catalog, metadata, _ := presenterDirectoryInput(t)
|
catalog, metadata, _ := testutils.DirectoryInput(t)
|
||||||
assertPresenterAgainstGoldenSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenSnapshot(t,
|
||||||
NewTextPresenter(catalog, metadata),
|
NewTextPresenter(catalog, metadata),
|
||||||
*updateTextPresenterGoldenFiles,
|
*updateTextPresenterGoldenFiles,
|
||||||
)
|
)
|
||||||
@@ -17,8 +19,8 @@ func TestTextDirectoryPresenter(t *testing.T) {
|
|||||||
|
|
||||||
func TestTextImagePresenter(t *testing.T) {
|
func TestTextImagePresenter(t *testing.T) {
|
||||||
testImage := "image-simple"
|
testImage := "image-simple"
|
||||||
catalog, metadata, _ := presenterImageInput(t, testImage)
|
catalog, metadata, _ := testutils.ImageInput(t, testImage)
|
||||||
assertPresenterAgainstGoldenImageSnapshot(t,
|
testutils.AssertPresenterAgainstGoldenImageSnapshot(t,
|
||||||
NewTextPresenter(catalog, metadata),
|
NewTextPresenter(catalog, metadata),
|
||||||
testImage,
|
testImage,
|
||||||
*updateTextPresenterGoldenFiles,
|
*updateTextPresenterGoldenFiles,
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package poweruser
|
package poweruser
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/anchore/syft/internal/presenter/packages"
|
"github.com/anchore/syft/internal/formats/syftjson"
|
||||||
|
"github.com/anchore/syft/internal/formats/syftjson/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
type JSONDocument struct {
|
type JSONDocument struct {
|
||||||
// note: poweruser.JSONDocument is meant to always be a superset of packages.JSONDocument, any additional fields
|
// note: poweruser.JSONDocument is meant to always be a superset of packages.Document, any additional fields
|
||||||
// here should be optional by supplying "omitempty" on these fields hint to the jsonschema generator to not
|
// here should be optional by supplying "omitempty" on these fields hint to the jsonschema generator to not
|
||||||
// require these fields. As an accepted rule in this repo all collections should still be initialized in the
|
// require these fields. As an accepted rule in this repo all collections should still be initialized in the
|
||||||
// context of being used in a JSON document.
|
// context of being used in a JSON document.
|
||||||
@@ -13,15 +14,12 @@ type JSONDocument struct {
|
|||||||
FileContents []JSONFileContents `json:"fileContents,omitempty"` // note: must have omitempty
|
FileContents []JSONFileContents `json:"fileContents,omitempty"` // note: must have omitempty
|
||||||
FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"` // note: must have omitempty
|
FileMetadata []JSONFileMetadata `json:"fileMetadata,omitempty"` // note: must have omitempty
|
||||||
Secrets []JSONSecrets `json:"secrets,omitempty"` // note: must have omitempty
|
Secrets []JSONSecrets `json:"secrets,omitempty"` // note: must have omitempty
|
||||||
packages.JSONDocument
|
model.Document
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
|
// NewJSONDocument creates and populates a new JSON document struct from the given cataloging results.
|
||||||
func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
|
func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
|
||||||
pkgsDoc, err := packages.NewJSONDocument(config.PackageCatalog, config.SourceMetadata, config.Distro, config.ApplicationConfig.Package.Cataloger.ScopeOpt, config.ApplicationConfig)
|
pkgsDoc := syftjson.ToFormatModel(config.PackageCatalog, &config.SourceMetadata, config.Distro, config.ApplicationConfig)
|
||||||
if err != nil {
|
|
||||||
return JSONDocument{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests)
|
fileMetadata, err := NewJSONFileMetadata(config.FileMetadata, config.FileDigests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,6 +31,6 @@ func NewJSONDocument(config JSONDocumentConfig) (JSONDocument, error) {
|
|||||||
FileContents: NewJSONFileContents(config.FileContents),
|
FileContents: NewJSONFileContents(config.FileContents),
|
||||||
FileMetadata: fileMetadata,
|
FileMetadata: fileMetadata,
|
||||||
Secrets: NewJSONSecrets(config.Secrets),
|
Secrets: NewJSONSecrets(config.Secrets),
|
||||||
JSONDocument: pkgsDoc,
|
Document: pkgsDoc,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Decoder func(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, error)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Encoder func(io.Writer, *pkg.Catalog, *source.Metadata, *distro.Distro) error
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrEncodingNotSupported = errors.New("encoding not supported")
|
||||||
|
ErrDecodingNotSupported = errors.New("decoding not supported")
|
||||||
|
)
|
||||||
|
|
||||||
|
type Format struct {
|
||||||
|
Option Option
|
||||||
|
encoder Encoder
|
||||||
|
decoder Decoder
|
||||||
|
validator Validator
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFormat(option Option, encoder Encoder, decoder Decoder, validator Validator) Format {
|
||||||
|
return Format{
|
||||||
|
Option: option,
|
||||||
|
encoder: encoder,
|
||||||
|
decoder: decoder,
|
||||||
|
validator: validator,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Format) Encode(output io.Writer, catalog *pkg.Catalog, d *distro.Distro, metadata *source.Metadata) error {
|
||||||
|
if f.encoder == nil {
|
||||||
|
return ErrEncodingNotSupported
|
||||||
|
}
|
||||||
|
return f.encoder(output, catalog, metadata, d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Format) Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, error) {
|
||||||
|
if f.decoder == nil {
|
||||||
|
return nil, nil, nil, ErrDecodingNotSupported
|
||||||
|
}
|
||||||
|
return f.decoder(reader)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Format) Detect(b []byte) bool {
|
||||||
|
if f.validator == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.validator(bytes.NewReader(b)); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f Format) Presenter(catalog *pkg.Catalog, metadata *source.Metadata, d *distro.Distro) *Presenter {
|
||||||
|
if f.encoder == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return NewPresenter(f.encoder, catalog, metadata, d)
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import "strings"
|
||||||
|
|
||||||
|
const (
|
||||||
|
UnknownOption Option = "UnknownFormatOption"
|
||||||
|
JSONOption Option = "json"
|
||||||
|
TextOption Option = "text"
|
||||||
|
TableOption Option = "table"
|
||||||
|
CycloneDxOption Option = "cyclonedx"
|
||||||
|
SPDXTagValueOption Option = "spdx-tag-value"
|
||||||
|
SPDXJSONOption Option = "spdx-json"
|
||||||
|
)
|
||||||
|
|
||||||
|
var AllOptions = []Option{
|
||||||
|
JSONOption,
|
||||||
|
TextOption,
|
||||||
|
TableOption,
|
||||||
|
CycloneDxOption,
|
||||||
|
SPDXTagValueOption,
|
||||||
|
SPDXJSONOption,
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option string
|
||||||
|
|
||||||
|
func ParseOption(userStr string) Option {
|
||||||
|
switch strings.ToLower(userStr) {
|
||||||
|
case string(JSONOption), "syft-json":
|
||||||
|
return JSONOption
|
||||||
|
case string(TextOption):
|
||||||
|
return TextOption
|
||||||
|
case string(TableOption):
|
||||||
|
return TableOption
|
||||||
|
case string(CycloneDxOption), "cyclone", "cyclone-dx":
|
||||||
|
return CycloneDxOption
|
||||||
|
case string(SPDXTagValueOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv", "spdxtv":
|
||||||
|
return SPDXTagValueOption
|
||||||
|
case string(SPDXJSONOption), "spdxjson":
|
||||||
|
return SPDXJSONOption
|
||||||
|
default:
|
||||||
|
return UnknownOption
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
|
||||||
|
"github.com/anchore/syft/syft/pkg"
|
||||||
|
"github.com/anchore/syft/syft/source"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Presenter struct {
|
||||||
|
catalog *pkg.Catalog
|
||||||
|
srcMetadata *source.Metadata
|
||||||
|
distro *distro.Distro
|
||||||
|
encoder Encoder
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPresenter(encoder Encoder, catalog *pkg.Catalog, srcMetadata *source.Metadata, d *distro.Distro) *Presenter {
|
||||||
|
return &Presenter{
|
||||||
|
catalog: catalog,
|
||||||
|
srcMetadata: srcMetadata,
|
||||||
|
distro: d,
|
||||||
|
encoder: encoder,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pres *Presenter) Present(output io.Writer) error {
|
||||||
|
return pres.encoder(output, pres.catalog, pres.srcMetadata, pres.distro)
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package format
|
||||||
|
|
||||||
|
import "io"
|
||||||
|
|
||||||
|
type Validator func(reader io.Reader) error
|
||||||
+33
@@ -17,11 +17,15 @@ Similar to the cataloging process, Linux distribution identification is also per
|
|||||||
package syft
|
package syft
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
"github.com/anchore/syft/internal/bus"
|
"github.com/anchore/syft/internal/bus"
|
||||||
|
"github.com/anchore/syft/internal/formats"
|
||||||
"github.com/anchore/syft/internal/log"
|
"github.com/anchore/syft/internal/log"
|
||||||
"github.com/anchore/syft/syft/distro"
|
"github.com/anchore/syft/syft/distro"
|
||||||
|
"github.com/anchore/syft/syft/format"
|
||||||
"github.com/anchore/syft/syft/logger"
|
"github.com/anchore/syft/syft/logger"
|
||||||
"github.com/anchore/syft/syft/pkg"
|
"github.com/anchore/syft/syft/pkg"
|
||||||
"github.com/anchore/syft/syft/pkg/cataloger"
|
"github.com/anchore/syft/syft/pkg/cataloger"
|
||||||
@@ -67,6 +71,35 @@ func CatalogPackages(src *source.Source, scope source.Scope) (*pkg.Catalog, *dis
|
|||||||
return catalog, theDistro, nil
|
return catalog, theDistro, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: encapsulate input data into common sbom document object
|
||||||
|
func Encode(catalog *pkg.Catalog, metadata *source.Metadata, dist *distro.Distro, option format.Option) ([]byte, error) {
|
||||||
|
f := formats.ByOption(option)
|
||||||
|
if f == nil {
|
||||||
|
return nil, fmt.Errorf("unsupported format: %+v", option)
|
||||||
|
}
|
||||||
|
buff := bytes.Buffer{}
|
||||||
|
|
||||||
|
return buff.Bytes(), f.Encode(&buff, catalog, dist, metadata)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: encapsulate return data into common sbom document object
|
||||||
|
func Decode(reader io.Reader) (*pkg.Catalog, *source.Metadata, *distro.Distro, format.Option, error) {
|
||||||
|
by, err := io.ReadAll(reader)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, format.UnknownOption, fmt.Errorf("unable to read sbom: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := formats.Identify(by)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, format.UnknownOption, fmt.Errorf("unable to detect format: %w", err)
|
||||||
|
}
|
||||||
|
if f == nil {
|
||||||
|
return nil, nil, nil, format.UnknownOption, fmt.Errorf("unable to identify format")
|
||||||
|
}
|
||||||
|
c, m, d, err := f.Decode(bytes.NewReader(by))
|
||||||
|
return c, m, d, f.Option, err
|
||||||
|
}
|
||||||
|
|
||||||
// SetLogger sets the logger object used for all syft logging calls.
|
// SetLogger sets the logger object used for all syft logging calls.
|
||||||
func SetLogger(logger logger.Logger) {
|
func SetLogger(logger logger.Logger) {
|
||||||
log.Log = logger
|
log.Log = logger
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user