Add SPDX support (#445)

* add initial spdx support

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* expose FileOwner and use in SPDX presenter

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add initial json support for SPDX

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add remaining package fields

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add spdx license list generation + tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* keep fileOwner unexported from pkg

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* restore cli test util

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add external refs to spdx tag-value format

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add golang support to CPE generation

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* use tag-value format as default "spdx" format flavor

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add tests around spdx presenters + refactor presenter tests

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* add bouncer exception for spdx tools-golang repo

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>

* remove spdx model questions

Signed-off-by: Alex Goodman <alex.goodman@anchore.com>
This commit is contained in:
Alex Goodman
2021-06-25 16:30:41 -04:00
committed by GitHub
parent 66e71c39e2
commit 706322f826
60 changed files with 4290 additions and 581 deletions
@@ -1,169 +1,34 @@
package packages
import (
"bytes"
"flag"
"regexp"
"testing"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/go-testutils"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
)
var update = flag.Bool("update", false, "update the *.golden files for json presenters")
func TestCycloneDxDirsPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package1",
Version: "1.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-1",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
},
Metadata: pkg.DpkgMetadata{
Package: "package1",
Version: "1.0.1",
Architecture: "amd64",
},
})
catalog.Add(pkg.Package{
Name: "package2",
Version: "2.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
},
Licenses: []string{
"MIT",
"Apache-v2",
},
Metadata: pkg.DpkgMetadata{
Package: "package2",
Version: "1.0.2",
Architecture: "amd64",
},
})
s, err := source.NewFromDirectory("/some/path")
if err != nil {
t.Fatal(err)
}
pres := NewCycloneDxPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
// remove dynamic values, which are tested independently
actual = redact(actual)
expected = redact(expected)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
var updateCycloneDx = flag.Bool("update-cyclonedx", false, "update the *.golden files for cyclone-dx presenters")
func TestCycloneDxDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewCycloneDxPresenter(catalog, metadata),
*updateCycloneDx,
cycloneDxRedactor,
)
}
func TestCycloneDxImgsPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package1",
Version: "1.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
},
Type: pkg.RpmPkg,
FoundBy: "the-cataloger-1",
PURL: "the-purl-1",
})
catalog.Add(pkg.Package{
Name: "package2",
Version: "2.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
},
Type: pkg.RpmPkg,
FoundBy: "the-cataloger-2",
Licenses: []string{
"MIT",
"Apache-v2",
},
PURL: "the-purl-2",
})
s, err := source.NewFromImage(img, "user-image-input")
if err != nil {
t.Fatal(err)
}
// This accounts for the non-deterministic digest value that we end up with when
// we build a container image dynamically during testing. Ultimately, we should
// use a golden image as a test fixture in place of building this image during
// testing. At that time, this line will no longer be necessary.
//
// This value is sourced from the "version" node in "./test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden"
s.Metadata.ImageMetadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
pres := NewCycloneDxPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *update {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
// remove dynamic values, which are tested independently
actual = redact(actual)
expected = redact(expected)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
func TestCycloneDxImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewCycloneDxPresenter(catalog, metadata),
testImage,
*updateCycloneDx,
cycloneDxRedactor,
)
}
func redact(s []byte) []byte {
func cycloneDxRedactor(s []byte) []byte {
serialPattern := regexp.MustCompile(`serialNumber="[a-zA-Z0-9\-:]+"`)
rfc3339Pattern := regexp.MustCompile(`([0-9]+)-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])[Tt]([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(\.[0-9]+)?(([Zz])|([\+|\-]([01][0-9]|2[0-3]):[0-5][0-9]))`)
@@ -1,18 +1,11 @@
package packages
import (
"bytes"
"flag"
"testing"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
)
var updateJSONGoldenFiles = flag.Bool("update-json", false, "update the *.golden files for json presenters")
@@ -24,160 +17,21 @@ func must(c pkg.CPE, e error) pkg.CPE {
return c
}
func TestJSONDirsPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
},
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"},
Metadata: pkg.PythonPackageMetadata{
Name: "package-1",
Version: "1.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
},
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "package-2",
Version: "2.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
var d *distro.Distro
s, err := source.NewFromDirectory("/some/path")
if err != nil {
t.Fatal(err)
}
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
// run presenter
err = pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *updateJSONGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
func TestJSONDirectoryPresenter(t *testing.T) {
catalog, metadata, dist := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
*updateJSONGoldenFiles,
)
}
func TestJSONImgsPresenter(t *testing.T) {
var buffer bytes.Buffer
func TestJSONImagePresenter(t *testing.T) {
testImage := "image-simple"
if *updateJSONGoldenFiles {
imagetest.UpdateGoldenFixtureImage(t, testImage)
}
catalog := pkg.NewCatalog()
img := imagetest.GetGoldenFixtureImage(t, testImage)
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
},
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"},
Metadata: pkg.PythonPackageMetadata{
Name: "package-1",
Version: "1.0.1",
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
},
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "package-2",
Version: "2.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
// this is a hard coded value that is not given by the fixture helper and must be provided manually
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
s, err := source.NewFromImage(img, "user-image-input")
var d *distro.Distro
pres := NewJSONPresenter(catalog, s.Metadata, d, source.SquashedScope)
// run presenter
err = pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *updateJSONGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
catalog, metadata, dist := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewJSONPresenter(catalog, metadata, dist, source.SquashedScope),
testImage,
*updateJSONGoldenFiles,
)
}
@@ -0,0 +1,21 @@
package spdx22
import "time"
type AnnotationType string
const (
ReviewerAnnotationType AnnotationType = "REVIEWER"
OtherAnnotationType AnnotationType = "OTHER"
)
type Annotation struct {
// Identify when the comment was made. This is to be specified according to the combined date and time in the
// UTC format, as specified in the ISO 8601 standard.
AnnotationDate time.Time `json:"annotationDate"`
// Type of the annotation
AnnotationType AnnotationType `json:"annotationType"`
// This field identifies the person, organization or tool that has commented on a file, package, or the entire document.
Annotator string `json:"annotator"`
Comment string `json:"comment"`
}
@@ -0,0 +1,7 @@
package spdx22
type Checksum struct {
// Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224"
Algorithm string `json:"algorithm"`
ChecksumValue string `json:"checksumValue"`
}
@@ -0,0 +1,19 @@
package spdx22
import "time"
type CreationInfo struct {
Comment string `json:"comment,omitempty"`
// Identify when the SPDX file was originally created. The date is to be specified according to combined date and
// time in UTC format as specified in ISO 8601 standard. This field is distinct from the fields in section 8,
// which involves the addition of information during a subsequent review.
Created time.Time `json:"created"`
// Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an
// individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization,
//indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version
// for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person
// name or organization name may be designated as “anonymous” if appropriate.
Creators []string `json:"creators"`
// An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.
LicenseListVersion string `json:"licenseListVersion"`
}
@@ -0,0 +1,43 @@
package spdx22
// derived from:
// - https://spdx.github.io/spdx-spec/appendix-III-RDF-data-model-implementation-and-identifier-syntax/
// - https://github.com/spdx/spdx-spec/blob/v2.2/schemas/spdx-schema.json
// - https://github.com/spdx/spdx-spec/tree/v2.2/ontology
type Document struct {
Element
SPDXVersion string `json:"spdxVersion"`
// One instance is required for each SPDX file produced. It provides the necessary information for forward
// and backward compatibility for processing tools.
CreationInfo CreationInfo `json:"creationInfo"`
// 2.2: Data License; should be "CC0-1.0"
// Cardinality: mandatory, one
// License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX
// fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous
// fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without
// opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text
// is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any
// portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any
// SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative
// Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree
// and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or
// warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including
// without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement,
// or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not
// discoverable, all to the greatest extent permissible under applicable law.
DataLicense string `json:"dataLicense"`
// Information about an external SPDX document reference including the checksum. This allows for verification of the external references.
ExternalDocumentRefs []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"`
// Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.
HasExtractedLicensingInfos []HasExtractedLicensingInfo `json:"hasExtractedLicensingInfos,omitempty"`
// note: found in example documents from SPDX, but not in the JSON schema. See https://spdx.github.io/spdx-spec/2-document-creation-information/#25-spdx-document-namespace
DocumentNamespace string `json:"documentNamespace"`
// note: found in example documents from SPDX, but not in the JSON schema
// DocumentDescribes []string `json:"documentDescribes"`
Packages []Package `json:"packages"`
// Files referenced in the SPDX document
Files []File `json:"files,omitempty"`
// Snippets referenced in the SPDX document
Snippets []Snippet `json:"snippets,omitempty"`
}
@@ -0,0 +1,12 @@
package spdx22
type Element struct {
SPDXID string `json:"SPDXID"`
// Identify name of this SpdxElement.
Name string `json:"name"`
// Relationships referenced in the SPDX document
Relationships []Relationship `json:"relationships,omitempty"`
// Provide additional information about an SpdxElement.
Annotations []Annotation `json:"annotations,omitempty"`
Comment string `json:"comment,omitempty"`
}
@@ -0,0 +1,37 @@
package spdx22
// ElementID represents the identifier string portion of an SPDX element
// identifier. DocElementID should be used for any attributes which can
// contain identifiers defined in a different SPDX document.
// ElementIDs should NOT contain the mandatory 'SPDXRef-' portion.
type ElementID string
func (e ElementID) String() string {
return "SPDXRef-" + string(e)
}
// DocElementID represents an SPDX element identifier that could be defined
// in a different SPDX document, and therefore could have a "DocumentRef-"
// portion, such as Relationship and Annotations.
// ElementID is used for attributes in which a "DocumentRef-" portion cannot
// appear, such as a Package or File definition (since it is necessarily
// being defined in the present document).
// DocumentRefID will be the empty string for elements defined in the
// present document.
// DocElementIDs should NOT contain the mandatory 'DocumentRef-' or
// 'SPDXRef-' portions.
type DocElementID struct {
DocumentRefID string
ElementRefID ElementID
}
// RenderDocElementID takes a DocElementID and returns the string equivalent,
// with the SPDXRef- prefix (and, if applicable, the DocumentRef- prefix)
// reinserted.
func (d DocElementID) String() string {
prefix := ""
if d.DocumentRefID != "" {
prefix = "DocumentRef-" + d.DocumentRefID + ":"
}
return prefix + d.ElementRefID.String()
}
@@ -0,0 +1,9 @@
package spdx22
type ExternalDocumentRef struct {
// externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.
ExternalDocumentID string `json:"externalDocumentId"`
Checksum Checksum `json:"checksum"`
// SPDX ID for SpdxDocument. A propoerty containing an SPDX document.
SpdxDocument string `json:"spdxDocument"`
}
@@ -0,0 +1,43 @@
package spdx22
type ReferenceCategory string
const (
SecurityReferenceCategory ReferenceCategory = "SECURITY"
PackageManagerReferenceCategory ReferenceCategory = "PACKAGE_MANAGER"
OtherReferenceCategory ReferenceCategory = "OTHER"
)
// source: https://spdx.github.io/spdx-spec/appendix-VI-external-repository-identifiers/
type ExternalRefType string
const (
// see https://nvd.nist.gov/cpe
Cpe22ExternalRefType ExternalRefType = "cpe22Type"
// see https://nvd.nist.gov/cpe
Cpe23ExternalRefType ExternalRefType = "cpe23Type"
// see http://repo1.maven.org/maven2/
MavenCentralExternalRefType ExternalRefType = "maven-central"
// see https://www.npmjs.com/
NpmExternalRefType ExternalRefType = "npm"
// see https://www.nuget.org/
NugetExternalRefType ExternalRefType = "nuget"
// see http://bower.io/
BowerExternalRefType ExternalRefType = "bower"
// see https://github.com/package-url/purl-spec
PurlExternalRefType ExternalRefType = "purl"
// These point to objects present in the Software Heritage archive by the means of SoftWare Heritage persistent Identifiers (SWHID)
SwhExternalRefType ExternalRefType = "swh"
)
type ExternalRef struct {
Comment string `json:"comment,omitempty"`
// Category for the external reference.
ReferenceCategory ReferenceCategory `json:"referenceCategory"`
// The unique string with no spaces necessary to access the package-specific information, metadata, or content
// within the target location. The format of the locator is subject to constraints defined by the <type>.
ReferenceLocator string `json:"referenceLocator"`
// Type of the external reference. These are defined in an appendix in the SPDX specification.
ReferenceType ExternalRefType `json:"referenceType"`
}
@@ -0,0 +1,41 @@
package spdx22
type FileType string
const (
DocumentationFileType FileType = "DOCUMENTATION"
ImageFileType FileType = "IMAGE"
VideoFileType FileType = "VIDEO"
ArchiveFileType FileType = "ARCHIVE"
SpdxFileType FileType = "SPDX"
ApplicationFileType FileType = "APPLICATION"
SourceFileType FileType = "SOURCE"
BinaryFileType FileType = "BINARY"
TextFileType FileType = "TEXT"
AudioFileType FileType = "AUDIO"
OtherFileType FileType = "OTHER"
)
type File struct {
Item
// (At least one is required.) The checksum property provides a mechanism that can be used to verify that the
// contents of a File or Package have not changed.
Checksums []Checksum `json:"checksums"`
// This field provides a place for the SPDX file creator to record file contributors. Contributors could include
// names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.
FileContributors []string `json:"fileContributors"`
// Each element is a SPDX ID for a File.
FileDependencies []string `json:"fileDependencies"`
// The name of the file relative to the root of the package.
FileName string `json:"fileName"`
// The type of the file
FileTypes []string `json:"fileTypes"`
// This field provides a place for the SPDX file creator to record potential legal notices found in the file.
// This may or may not include copyright statements.
NoticeText string `json:"noticeText,omitempty"`
// Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name
// properties and the URI (if one is known) of doap:Project resources that are values of this property. All other
// properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or
// from some SPDX formats.
ArtifactOf []string `json:"artifactOf"`
}
@@ -0,0 +1,14 @@
package spdx22
type HasExtractedLicensingInfo struct {
// Verbatim license or licensing notice text that was discovered.
ExtractedText string `json:"extractedText"`
// A human readable short form license identifier for a license. The license ID is iether on the standard license
// oist or the form \"LicenseRef-\"[idString] where [idString] is a unique string containing letters,
// numbers, \".\", \"-\" or \"+\".
LicenseID string `json:"licenseId"`
Comment string `json:"comment,omitempty"`
// Identify name of this SpdxElement.
Name string `json:"name,omitempty"`
SeeAlsos []string `json:"seeAlsos,omitempty"`
}
@@ -0,0 +1,22 @@
package spdx22
type Item struct {
Element
// The licenseComments property allows the preparer of the SPDX document to describe why the licensing in
// spdx:licenseConcluded was chosen.
LicenseComments string `json:"licenseComments,omitempty"`
LicenseConcluded string `json:"licenseConcluded"`
// The licensing information that was discovered directly within the package. There will be an instance of this
// property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.
LicenseInfoFromFiles []string `json:"licenseInfoFromFiles,omitempty"`
// Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.
LicenseInfoInFiles []string `json:"licenseInfoInFiles,omitempty"`
// The text of copyright declarations recited in the Package or File.
CopyrightText string `json:"copyrightText,omitempty"`
// This field provides a place for the SPDX data creator to record acknowledgements that may be required to be
// communicated in some contexts. This is not meant to include the actual complete license text (see
// licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText).
// The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from
// license texts, which may be necessary or desirable to reproduce.
AttributionTexts []string `json:"attributionTexts,omitempty"`
}
@@ -0,0 +1,50 @@
package spdx22
type Package struct {
Item
// The checksum property provides a mechanism that can be used to verify that the contents of a File or
// Package have not changed.
Checksums []Checksum `json:"checksums,omitempty"`
// Provides a detailed description of the package.
Description string `json:"description,omitempty"`
// The URI at which this package is available for download. Private (i.e., not publicly reachable) URIs are
// acceptable as values of this property. The values http://spdx.org/rdf/terms#none and http://spdx.org/rdf/terms#noassertion
// may be used to specify that the package is not downloadable or that no attempt was made to determine its
// download location, respectively.
DownloadLocation string `json:"downloadLocation,omitempty"`
// An External Reference allows a Package to reference an external source of additional information, metadata,
// enumerations, asset identifiers, or downloadable content believed to be relevant to the Package.
ExternalRefs []ExternalRef `json:"externalRefs,omitempty"`
// Indicates whether the file content of this package has been available for or subjected to analysis when
// creating the SPDX document. If false indicates packages that represent metadata or URI references to a
// project, product, artifact, distribution or a component. If set to false, the package must not contain any files
FilesAnalyzed bool `json:"filesAnalyzed"`
// Indicates that a particular file belongs to a package (elements are SPDX ID for a File).
HasFiles []string `json:"hasFiles,omitempty"`
// Provide a place for the SPDX file creator to record a web site that serves as the package's home page.
// This link can also be used to reference further information about the package referenced by the SPDX file creator.
Homepage string `json:"homepage,omitempty"`
// List the licenses that have been declared by the authors of the package. Any license information that does not
// originate from the package authors, e.g. license information from a third party repository, should not be included in this field.
LicenseDeclared string `json:"licenseDeclared"`
// The name and, optionally, contact information of the person or organization that originally created the package.
// Values of this property must conform to the agent and tool syntax.
Originator string `json:"originator,omitempty"`
// The base name of the package file name. For example, zlib-1.2.5.tar.gz.
PackageFileName string `json:"packageFileName,omitempty"`
// A manifest based verification code (the algorithm is defined in section 4.7 of the full specification) of the
// SPDX Item. This allows consumers of this data and/or database to determine if an SPDX item they have in hand
// is identical to the SPDX item from which the data was produced. This algorithm works even if the SPDX document
// is included in the SPDX item.
PackageVerificationCode *PackageVerificationCode `json:"packageVerificationCode,omitempty"`
// Allows the producer(s) of the SPDX document to describe how the package was acquired and/or changed from the original source.
SourceInfo string `json:"sourceInfo,omitempty"`
// Provides a short description of the package.
Summary string `json:"summary,omitempty"`
// The name and, optionally, contact information of the person or organization who was the immediate supplier
// of this package to the recipient. The supplier may be different than originator when the software has been
// repackaged. Values of this property must conform to the agent and tool syntax.
Supplier string `json:"supplier,omitempty"`
// Provides an indication of the version of the package that is described by this SpdxDocument.
VersionInfo string `json:"versionInfo,omitempty"`
}
@@ -0,0 +1,23 @@
package spdx22
// Why are there two package identifier fields Package Checksum and Package Verification?
// Although the values of the two fields Package Checksum and Package Verification are similar, they each serve a
// different purpose. The Package Checksum provides a unique identifier of a software package which is computed by
// taking the SHA1 of the entire software package file. This enables one to quickly determine if two different copies
// of a package are the same. One disadvantage of this approach is that one cannot add an SPDX data file into the
// original package without changing the Package Checksum value. Alternatively, the Package Verification field enables
// the inclusion of an SPDX file. It enables one to quickly verify if one or more of the original package files has
// changed. The Package Verification field is a unique identifier that is based on SHAing only the original package
// files (e.g., excluding the SPDX file). This allows one to add an SPDX file to the original package without changing
// this unique identifier.
// source: https://wiki.spdx.org/view/SPDX_FAQ
type PackageVerificationCode struct {
// "A file that was excluded when calculating the package verification code. This is usually a file containing
// SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded
// from the package verification code. If this is not done it would be impossible to correctly calculate the
// verification codes in both files.
PackageVerificationCodeExcludedFiles []string `json:"packageVerificationCodeExcludedFiles"`
// The actual package verification code as a hex encoded value.
PackageVerificationCodeValue string `json:"packageVerificationCodeValue"`
}
@@ -0,0 +1,181 @@
package spdx22
type Relationship struct {
// SPDX ID for SpdxElement. A related SpdxElement.
RelatedSpdxElement string `json:"relatedSpdxElement"`
// Describes the type of relationship between two SPDX elements.
RelationshipType RelationshipType `json:"relationshipType"`
Comment string `json:"comment,omitempty"`
}
// source: https://spdx.github.io/spdx-spec/7-relationships-between-SPDX-elements/
type RelationshipType string
const (
// DescribedByRelationship is to be used when SPDXRef-A is described by SPDXREF-Document.
// Example: The package 'WildFly' is described by SPDX document WildFly.spdx.
DescribedByRelationship RelationshipType = "DESCRIBED_BY"
// ContainsRelationship is to be used when SPDXRef-A contains SPDXRef-B.
// Example: An ARCHIVE file bar.tgz contains a SOURCE file foo.c.
ContainsRelationship RelationshipType = "CONTAINS"
// ContainedByRelationship is to be used when SPDXRef-A is contained by SPDXRef-B.
// Example: A SOURCE file foo.c is contained by ARCHIVE file bar.tgz
ContainedByRelationship RelationshipType = "CONTAINED_BY"
// DependsOnRelationship is to be used when SPDXRef-A depends on SPDXRef-B.
// Example: Package A depends on the presence of package B in order to build and run
DependsOnRelationship RelationshipType = "DEPENDS_ON"
// DependencyOfRelationship is to be used when SPDXRef-A is dependency of SPDXRef-B.
// Example: A is explicitly stated as a dependency of B in a machine-readable file. Use when a package manager does not define scopes.
DependencyOfRelationship RelationshipType = "DEPENDENCY_OF"
// DependencyManifestOfRelationship is to be used when SPDXRef-A is a manifest file that lists a set of dependencies for SPDXRef-B.
// Example: A file package.json is the dependency manifest of a package foo. Note that only one manifest should be used to define the same dependency graph.
DependencyManifestOfRelationship RelationshipType = "DEPENDENCY_MANIFEST_OF"
// BuildDependencyOfRelationship is to be used when SPDXRef-A is a build dependency of SPDXRef-B.
// Example: A is in the compile scope of B in a Maven project.
BuildDependencyOfRelationship RelationshipType = "BUILD_DEPENDENCY_OF"
// DevDependencyOfRelationship is to be used when SPDXRef-A is a development dependency of SPDXRef-B.
// Example: A is in the devDependencies scope of B in a Maven project.
DevDependencyOfRelationship RelationshipType = "DEV_DEPENDENCY_OF"
// OptionalDependencyOfRelationship is to be used when SPDXRef-A is an optional dependency of SPDXRef-B.
// Example: Use when building the code will proceed even if a dependency cannot be found, fails to install, or is only installed on a specific platform. For example, A is in the optionalDependencies scope of npm project B.
OptionalDependencyOfRelationship RelationshipType = "OPTIONAL_DEPENDENCY_OF"
// ProvidedDependencyOfRelationship is to be used when SPDXRef-A is a to be provided dependency of SPDXRef-B.
// Example: A is in the provided scope of B in a Maven project, indicating that the project expects it to be provided, for instance, by the container or JDK.
ProvidedDependencyOfRelationship RelationshipType = "PROVIDED_DEPENDENCY_OF"
// TestDependencyOfRelationship is to be used when SPDXRef-A is a test dependency of SPDXRef-B.
// Example: A is in the test scope of B in a Maven project.
TestDependencyOfRelationship RelationshipType = "TEST_DEPENDENCY_OF"
// RuntimeDependencyOfRelationship is to be used when SPDXRef-A is a dependency required for the execution of SPDXRef-B.
// Example: A is in the runtime scope of B in a Maven project.
RuntimeDependencyOfRelationship RelationshipType = "RUNTIME_DEPENDENCY_OF"
// ExampleOfRelationship is to be used when SPDXRef-A is an example of SPDXRef-B.
// Example: The file or snippet that illustrates how to use an application or library.
ExampleOfRelationship RelationshipType = "EXAMPLE_OF"
// GeneratesRelationship is to be used when SPDXRef-A generates SPDXRef-B.
// Example: A SOURCE file makefile.mk generates a BINARY file a.out
GeneratesRelationship RelationshipType = "GENERATES"
// GeneratedFromRelationship is to be used when SPDXRef-A was generated from SPDXRef-B.
// Example: A BINARY file a.out has been generated from a SOURCE file makefile.mk. A BINARY file foolib.a is generated from a SOURCE file bar.c.
GeneratedFromRelationship RelationshipType = "GENERATED_FROM"
// AncestorOfRelationship is to be used when SPDXRef-A is an ancestor (same lineage but pre-dates) SPDXRef-B.
// Example: A SOURCE file makefile.mk is a version of the original ancestor SOURCE file 'makefile2.mk'
AncestorOfRelationship RelationshipType = "ANCESTOR_OF"
// DescendantOfRelationship is to be used when SPDXRef-A is a descendant of (same lineage but postdates) SPDXRef-B.
// Example: A SOURCE file makefile2.mk is a descendant of the original SOURCE file 'makefile.mk'
DescendantOfRelationship RelationshipType = "DESCENDANT_OF"
// VariantOfRelationship is to be used when SPDXRef-A is a variant of (same lineage but not clear which came first) SPDXRef-B.
// Example: A SOURCE file makefile2.mk is a variant of SOURCE file makefile.mk if they differ by some edit, but there is no way to tell which came first (no reliable date information).
VariantOfRelationship RelationshipType = "VARIANT_OF"
// DistributionArtifactRelationship is to be used when distributing SPDXRef-A requires that SPDXRef-B also be distributed.
// Example: A BINARY file foo.o requires that the ARCHIVE file bar-sources.tgz be made available on distribution.
DistributionArtifactRelationship RelationshipType = "DISTRIBUTION_ARTIFACT"
// PatchForRelationship is to be used when SPDXRef-A is a patch file for (to be applied to) SPDXRef-B.
// Example: A SOURCE file foo.diff is a patch file for SOURCE file foo.c.
PatchForRelationship RelationshipType = "PATCH_FOR"
// PatchAppliedRelationship is to be used when SPDXRef-A is a patch file that has been applied to SPDXRef-B.
// Example: A SOURCE file foo.diff is a patch file that has been applied to SOURCE file 'foo-patched.c'.
PatchAppliedRelationship RelationshipType = "PATCH_APPLIED"
// CopyOfRelationship is to be used when SPDXRef-A is an exact copy of SPDXRef-B.
// Example: A BINARY file alib.a is an exact copy of BINARY file a2lib.a.
CopyOfRelationship RelationshipType = "COPY_OF"
// FileAddedRelationship is to be used when SPDXRef-A is a file that was added to SPDXRef-B.
// Example: A SOURCE file foo.c has been added to package ARCHIVE bar.tgz.
FileAddedRelationship RelationshipType = "FILE_ADDED"
// FileDeletedRelationship is to be used when SPDXRef-A is a file that was deleted from SPDXRef-B.
// Example: A SOURCE file foo.diff has been deleted from package ARCHIVE bar.tgz.
FileDeletedRelationship RelationshipType = "FILE_DELETED"
// FileModifiedRelationship is to be used when SPDXRef-A is a file that was modified from SPDXRef-B.
// Example: A SOURCE file foo.c has been modified from SOURCE file foo.orig.c.
FileModifiedRelationship RelationshipType = "FILE_MODIFIED"
// ExpandedFromArchiveRelationship is to be used when SPDXRef-A is expanded from the archive SPDXRef-B.
// Example: A SOURCE file foo.c, has been expanded from the archive ARCHIVE file xyz.tgz.
ExpandedFromArchiveRelationship RelationshipType = "EXPANDED_FROM_ARCHIVE"
// DynamicLinkRelationship is to be used when SPDXRef-A dynamically links to SPDXRef-B.
// Example: An APPLICATION file 'myapp' dynamically links to BINARY file zlib.so.
DynamicLinkRelationship RelationshipType = "DYNAMIC_LINK"
// StaticLinkRelationship is to be used when SPDXRef-A statically links to SPDXRef-B.
// Example: An APPLICATION file 'myapp' statically links to BINARY zlib.a.
StaticLinkRelationship RelationshipType = "STATIC_LINK"
// DataFileOfRelationship is to be used when SPDXRef-A is a data file used in SPDXRef-B.
// Example: An IMAGE file 'kitty.jpg' is a data file of an APPLICATION 'hellokitty'.
DataFileOfRelationship RelationshipType = "DATA_FILE_OF"
// TestCaseOfRelationship is to be used when SPDXRef-A is a test case used in testing SPDXRef-B.
// Example: A SOURCE file testMyCode.java is a unit test file used to test an APPLICATION MyPackage.
TestCaseOfRelationship RelationshipType = "TEST_CASE_OF"
// BuildToolOfRelationship is to be used when SPDXRef-A is used to build SPDXRef-B.
// Example: A SOURCE file makefile.mk is used to build an APPLICATION 'zlib'.
BuildToolOfRelationship RelationshipType = "BUILD_TOOL_OF"
// DevToolOfRelationship is to be used when SPDXRef-A is used as a development tool for SPDXRef-B.
// Example: Any tool used for development such as a code debugger.
DevToolOfRelationship RelationshipType = "DEV_TOOL_OF"
// TestOfRelationship is to be used when SPDXRef-A is used for testing SPDXRef-B.
// Example: Generic relationship for cases where it's clear that something is used for testing but unclear whether it's TEST_CASE_OF or TEST_TOOL_OF.
TestOfRelationship RelationshipType = "TEST_OF"
// TestToolOfRelationship is to be used when SPDXRef-A is used as a test tool for SPDXRef-B.
// Example: Any tool used to test the code such as ESlint.
TestToolOfRelationship RelationshipType = "TEST_TOOL_OF"
// DocumentationOfRelationship is to be used when SPDXRef-A provides documentation of SPDXRef-B.
// Example: A DOCUMENTATION file readme.txt documents the APPLICATION 'zlib'.
DocumentationOfRelationship RelationshipType = "DOCUMENTATION_OF"
// OptionalComponentOfRelationship is to be used when SPDXRef-A is an optional component of SPDXRef-B.
// Example: A SOURCE file fool.c (which is in the contributors directory) may or may not be included in the build of APPLICATION 'atthebar'.
OptionalComponentOfRelationship RelationshipType = "OPTIONAL_COMPONENT_OF"
// MetafileOfRelationship is to be used when SPDXRef-A is a metafile of SPDXRef-B.
// Example: A SOURCE file pom.xml is a metafile of the APPLICATION 'Apache Xerces'.
MetafileOfRelationship RelationshipType = "METAFILE_OF"
// PackageOfRelationship is to be used when SPDXRef-A is used as a package as part of SPDXRef-B.
// Example: A Linux distribution contains an APPLICATION package gawk as part of the distribution MyLinuxDistro.
PackageOfRelationship RelationshipType = "PACKAGE_OF"
// AmendsRelationship is to be used when (current) SPDXRef-DOCUMENT amends the SPDX information in SPDXRef-B.
// Example: (Current) SPDX document A version 2 contains a correction to a previous version of the SPDX document A version 1. Note the reserved identifier SPDXRef-DOCUMENT for the current document is required.
AmendsRelationship RelationshipType = "AMENDS"
// PrerequisiteForRelationship is to be used when SPDXRef-A is a prerequisite for SPDXRef-B.
// Example: A library bar.dll is a prerequisite or dependency for APPLICATION foo.exe
PrerequisiteForRelationship RelationshipType = "PREREQUISITE_FOR"
// HasPrerequisiteRelationship is to be used when SPDXRef-A has as a prerequisite SPDXRef-B.
// Example: An APPLICATION foo.exe has prerequisite or dependency on bar.dll
HasPrerequisiteRelationship RelationshipType = "HAS_PREREQUISITE"
// OtherRelationship is to be used for a relationship which has not been defined in the formal SPDX specification. A description of the relationship should be included in the Relationship comments field.
OtherRelationship RelationshipType = "OTHER"
)
@@ -0,0 +1,32 @@
package spdx22
type StartPointer struct {
Offset int `json:"offset,omitempty"`
LineNumber int `json:"lineNumber,omitempty"`
// SPDX ID for File
Reference string `json:"reference"`
}
type EndPointer struct {
Offset int `json:"offset,omitempty"`
LineNumber int `json:"lineNumber,omitempty"`
// SPDX ID for File
Reference string `json:"reference"`
}
type Range struct {
StartPointer StartPointer `json:"startPointer"`
EndPointer EndPointer `json:"endPointer"`
}
type Snippet struct {
Item
// Licensing information that was discovered directly in the subject snippet. This is also considered a declared
// license for the snippet. (elements are license expressions)
LicenseInfoInSnippets []string `json:"licenseInfoInSnippets"`
// SPDX ID for File. File containing the SPDX element (e.g. the file contaning a snippet).
SnippetFromFile string `json:"snippetFromFile"`
// (At least 1 range is required). This field defines the byte range in the original host file (in X.2) that the
// snippet information applies to.
Ranges []Range `json:"ranges"`
}
@@ -0,0 +1,3 @@
package spdx22
const Version = "SPDX-2.2"
+168
View File
@@ -0,0 +1,168 @@
package packages
import (
"fmt"
"strings"
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/syft/pkg"
)
func getSPDXExternalRefs(p *pkg.Package) (externalRefs []spdx22.ExternalRef) {
externalRefs = make([]spdx22.ExternalRef, 0)
for _, c := range p.CPEs {
externalRefs = append(externalRefs, spdx22.ExternalRef{
ReferenceCategory: spdx22.SecurityReferenceCategory,
ReferenceLocator: c.BindToFmtString(),
ReferenceType: spdx22.Cpe23ExternalRefType,
})
}
if p.PURL != "" {
externalRefs = append(externalRefs, spdx22.ExternalRef{
ReferenceCategory: spdx22.PackageManagerReferenceCategory,
ReferenceLocator: p.PURL,
ReferenceType: spdx22.PurlExternalRefType,
})
}
return externalRefs
}
func getSPDXLicense(p *pkg.Package) string {
// source: https://spdx.github.io/spdx-spec/3-package-information/#313-concluded-license
// The options to populate this field are limited to:
// A valid SPDX License Expression as defined in Appendix IV;
// NONE, if the SPDX file creator concludes there is no license available for this package; or
// NOASSERTION if:
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
if len(p.Licenses) == 0 {
return "NONE"
}
// take all licenses and assume an AND expression; for information about license expressions see https://spdx.github.io/spdx-spec/appendix-IV-SPDX-license-expressions/
var parsedLicenses []string
for _, l := range p.Licenses {
if value, exists := spdxlicense.ID(l); exists {
parsedLicenses = append(parsedLicenses, value)
}
}
if len(parsedLicenses) == 0 {
return "NOASSERTION"
}
return strings.Join(parsedLicenses, " AND ")
}
func noneIfEmpty(value string) string {
if strings.TrimSpace(value) == "" {
return "NONE"
}
return value
}
func getSPDXDownloadLocation(p *pkg.Package) string {
// 3.7: Package Download Location
// Cardinality: mandatory, one
// NONE if there is no download location whatsoever.
// NOASSERTION if:
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return noneIfEmpty(metadata.URL)
case pkg.NpmPackageJSONMetadata:
return noneIfEmpty(metadata.URL)
default:
return "NOASSERTION"
}
}
func getSPDXHomepage(p *pkg.Package) string {
switch metadata := p.Metadata.(type) {
case pkg.GemMetadata:
return metadata.Homepage
case pkg.NpmPackageJSONMetadata:
return metadata.Homepage
default:
return ""
}
}
func getSPDXSourceInfo(p *pkg.Package) string {
answer := ""
switch p.Type {
case pkg.RpmPkg:
answer = "acquired package info from RPM DB"
case pkg.ApkPkg:
answer = "acquired package info from APK DB"
case pkg.DebPkg:
answer = "acquired package info from DPKG DB"
case pkg.NpmPkg:
answer = "acquired package info from installed node module manifest file"
case pkg.PythonPkg:
answer = "acquired package info from installed python package manifest file"
case pkg.JavaPkg, pkg.JenkinsPluginPkg:
answer = "acquired package info from installed java archive"
case pkg.GemPkg:
answer = "acquired package info from installed gem metadata file"
case pkg.GoModulePkg:
answer = "acquired package info from go module information"
case pkg.RustPkg:
answer = "acquired package info from rust cargo manifest"
default:
answer = "acquired package info from the following paths"
}
var paths []string
for _, l := range p.Locations {
paths = append(paths, l.RealPath)
}
return answer + ": " + strings.Join(paths, ", ")
}
func getSPDXOriginator(p *pkg.Package) string {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Maintainer
case pkg.NpmPackageJSONMetadata:
return metadata.Author
case pkg.PythonPackageMetadata:
author := metadata.Author
if author == "" {
return metadata.AuthorEmail
}
if metadata.AuthorEmail != "" {
author += fmt.Sprintf(" <%s>", metadata.AuthorEmail)
}
return author
case pkg.GemMetadata:
if len(metadata.Authors) > 0 {
return metadata.Authors[0]
}
return ""
case pkg.RpmdbMetadata:
return metadata.Vendor
case pkg.DpkgMetadata:
return metadata.Maintainer
default:
return ""
}
}
func getSPDXDescription(p *pkg.Package) string {
switch metadata := p.Metadata.(type) {
case pkg.ApkMetadata:
return metadata.Description
case pkg.NpmPackageJSONMetadata:
return metadata.Description
default:
return ""
}
}
@@ -0,0 +1,518 @@
package packages
import (
"testing"
"github.com/anchore/syft/syft/source"
"github.com/stretchr/testify/assert"
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
"github.com/anchore/syft/syft/pkg"
)
func Test_getSPDXExternalRefs(t *testing.T) {
testCPE := must(pkg.NewCPE("cpe:2.3:a:name:name:3.2:*:*:*:*:*:*:*"))
tests := []struct {
name string
input pkg.Package
expected []spdx22.ExternalRef
}{
{
name: "cpe + purl",
input: pkg.Package{
CPEs: []pkg.CPE{
testCPE,
},
PURL: "a-purl",
},
expected: []spdx22.ExternalRef{
{
ReferenceCategory: spdx22.SecurityReferenceCategory,
ReferenceLocator: testCPE.BindToFmtString(),
ReferenceType: spdx22.Cpe23ExternalRefType,
},
{
ReferenceCategory: spdx22.PackageManagerReferenceCategory,
ReferenceLocator: "a-purl",
ReferenceType: spdx22.PurlExternalRefType,
},
},
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.ElementsMatch(t, test.expected, getSPDXExternalRefs(&test.input))
})
}
}
func Test_getSPDXLicense(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
name: "no licenses",
input: pkg.Package{},
expected: "NONE",
},
{
name: "no SPDX licenses",
input: pkg.Package{
Licenses: []string{
"made-up",
},
},
expected: "NOASSERTION",
},
{
name: "with SPDX license",
input: pkg.Package{
Licenses: []string{
"MIT",
},
},
expected: "MIT",
},
{
name: "with SPDX license expression",
input: pkg.Package{
Licenses: []string{
"MIT",
"GPL-3.0",
},
},
expected: "MIT AND GPL-3.0",
},
{
name: "cap insensitive",
input: pkg.Package{
Licenses: []string{
"gpl-3.0",
},
},
expected: "GPL-3.0",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, getSPDXLicense(&test.input))
})
}
}
func Test_noneIfEmpty(t *testing.T) {
tests := []struct {
name string
value string
expected string
}{
{
name: "non-zero value",
value: "something",
expected: "something",
},
{
name: "empty",
value: "",
expected: "NONE",
},
{
name: "space",
value: " ",
expected: "NONE",
},
{
name: "tab",
value: "\t",
expected: "NONE",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, noneIfEmpty(test.value))
})
}
}
func Test_getSPDXDownloadLocation(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
name: "no metadata",
input: pkg.Package{},
expected: "NOASSERTION",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
URL: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
URL: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
URL: "",
},
},
expected: "NONE",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, getSPDXDownloadLocation(&test.input))
})
}
}
func Test_getSPDXHomepage(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from gem",
input: pkg.Package{
Metadata: pkg.GemMetadata{
Homepage: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Homepage: "http://a-place.gov",
},
},
expected: "http://a-place.gov",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Homepage: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, getSPDXHomepage(&test.input))
})
}
}
func Test_getSPDXSourceInfo(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected []string
}{
{
name: "locations are captured",
input: pkg.Package{
// note: no type given
Locations: []source.Location{
{
RealPath: "/a-place",
VirtualPath: "/b-place",
},
{
RealPath: "/c-place",
VirtualPath: "/d-place",
},
},
},
expected: []string{
"from the following paths",
"/a-place",
"/c-place",
},
},
{
// note: no specific support for this
input: pkg.Package{
Type: pkg.KbPkg,
},
expected: []string{
"from the following paths",
},
},
{
input: pkg.Package{
Type: pkg.RpmPkg,
},
expected: []string{
"from RPM DB",
},
},
{
input: pkg.Package{
Type: pkg.ApkPkg,
},
expected: []string{
"from APK DB",
},
},
{
input: pkg.Package{
Type: pkg.DebPkg,
},
expected: []string{
"from DPKG DB",
},
},
{
input: pkg.Package{
Type: pkg.NpmPkg,
},
expected: []string{
"from installed node module manifest file",
},
},
{
input: pkg.Package{
Type: pkg.PythonPkg,
},
expected: []string{
"from installed python package manifest file",
},
},
{
input: pkg.Package{
Type: pkg.JavaPkg,
},
expected: []string{
"from installed java archive",
},
},
{
input: pkg.Package{
Type: pkg.JenkinsPluginPkg,
},
expected: []string{
"from installed java archive",
},
},
{
input: pkg.Package{
Type: pkg.GemPkg,
},
expected: []string{
"from installed gem metadata file",
},
},
{
input: pkg.Package{
Type: pkg.GoModulePkg,
},
expected: []string{
"from go module information",
},
},
{
input: pkg.Package{
Type: pkg.RustPkg,
},
expected: []string{
"from rust cargo manifest",
},
},
}
var pkgTypes []pkg.Type
for _, test := range tests {
t.Run(test.name+" "+string(test.input.Type), func(t *testing.T) {
if test.input.Type != "" {
pkgTypes = append(pkgTypes, test.input.Type)
}
actual := getSPDXSourceInfo(&test.input)
for _, expected := range test.expected {
assert.Contains(t, actual, expected)
}
})
}
assert.ElementsMatch(t, pkg.AllPkgs, pkgTypes, "missing one or more package types to test against (maybe a package type was added?)")
}
func Test_getSPDXOriginator(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from gem",
input: pkg.Package{
Metadata: pkg.GemMetadata{
Authors: []string{
"auth1",
"auth2",
},
},
},
expected: "auth1",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Author: "auth",
},
},
expected: "auth",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
Maintainer: "auth",
},
},
expected: "auth",
},
{
name: "from python - just name",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
Author: "auth",
},
},
expected: "auth",
},
{
name: "from python - just email",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth@auth.gov",
},
{
name: "from python - both name and email",
input: pkg.Package{
Metadata: pkg.PythonPackageMetadata{
Author: "auth",
AuthorEmail: "auth@auth.gov",
},
},
expected: "auth <auth@auth.gov>",
},
{
name: "from rpm",
input: pkg.Package{
Metadata: pkg.RpmdbMetadata{
Vendor: "auth",
},
},
expected: "auth",
},
{
name: "from dpkg",
input: pkg.Package{
Metadata: pkg.DpkgMetadata{
Maintainer: "auth",
},
},
expected: "auth",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Author: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, getSPDXOriginator(&test.input))
})
}
}
func Test_getSPDXDescription(t *testing.T) {
tests := []struct {
name string
input pkg.Package
expected string
}{
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "no metadata",
input: pkg.Package{},
expected: "",
},
{
name: "from apk",
input: pkg.Package{
Metadata: pkg.ApkMetadata{
Description: "a description!",
},
},
expected: "a description!",
},
{
name: "from npm",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Description: "a description!",
},
},
expected: "a description!",
},
{
// note: since this is an optional field, no value is preferred over NONE or NOASSERTION
name: "empty",
input: pkg.Package{
Metadata: pkg.NpmPackageJSONMetadata{
Homepage: "",
},
},
expected: "",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, getSPDXDescription(&test.input))
})
}
}
@@ -0,0 +1,99 @@
package packages
import (
"encoding/json"
"fmt"
"io"
"time"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/presenter/packages/model/spdx22"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
)
// SPDXJsonPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
type SPDXJsonPresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
}
// NewSPDXJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewSPDXJSONPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXJsonPresenter {
return &SPDXJsonPresenter{
catalog: catalog,
srcMetadata: srcMetadata,
}
}
// Present the catalog results to the given writer.
func (pres *SPDXJsonPresenter) Present(output io.Writer) error {
doc := newSPDXJsonDocument(pres.catalog, pres.srcMetadata)
enc := json.NewEncoder(output)
// prevent > and < from being escaped in the payload
enc.SetEscapeHTML(false)
enc.SetIndent("", " ")
return enc.Encode(&doc)
}
func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document {
var name string
switch srcMetadata.Scheme {
case source.ImageScheme:
name = srcMetadata.ImageMetadata.UserInput
case source.DirectoryScheme:
name = srcMetadata.Path
}
return spdx22.Document{
Element: spdx22.Element{
SPDXID: spdx22.ElementID("DOCUMENT").String(),
Name: name,
},
SPDXVersion: spdx22.Version,
CreationInfo: spdx22.CreationInfo{
Created: time.Now().UTC(),
Creators: []string{
// note: key-value format derived from the JSON example document examples: https://github.com/spdx/spdx-spec/blob/v2.2/examples/SPDXJSONExample-v2.2.spdx.json
"Organization: Anchore, Inc",
"Tool: " + internal.ApplicationName + "-" + version.FromBuild().Version,
},
LicenseListVersion: spdxlicense.Version,
},
DataLicense: "CC0-1.0",
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput),
Packages: newSPDXJsonPackages(catalog),
}
}
func newSPDXJsonPackages(catalog *pkg.Catalog) []spdx22.Package {
results := make([]spdx22.Package, 0)
for _, p := range catalog.Sorted() {
license := getSPDXLicense(p)
// note: the license concluded and declared should be the same since we are collecting license information
// from the project data itself (the installed package files).
results = append(results, spdx22.Package{
Description: getSPDXDescription(p),
DownloadLocation: getSPDXDownloadLocation(p),
ExternalRefs: getSPDXExternalRefs(p),
FilesAnalyzed: false,
Homepage: getSPDXHomepage(p),
LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package
Originator: getSPDXOriginator(p),
SourceInfo: getSPDXSourceInfo(p),
VersionInfo: p.Version,
Item: spdx22.Item{
LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package
Element: spdx22.Element{
SPDXID: spdx22.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(),
Name: p.Name,
},
},
})
}
return results
}
@@ -0,0 +1,33 @@
package packages
import (
"flag"
"regexp"
"testing"
)
var updateSpdxJson = flag.Bool("update-spdx-json", false, "update the *.golden files for spdx-json presenters")
func TestSPDXJSONDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata),
*updateSpdxJson,
spdxJsonRedactor,
)
}
func TestSPDXJSONImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXJSONPresenter(catalog, metadata),
testImage,
*updateSpdxJson,
spdxJsonRedactor,
)
}
func spdxJsonRedactor(s []byte) []byte {
return regexp.MustCompile(`"created": .*`).ReplaceAll(s, []byte("redacted"))
}
@@ -0,0 +1,286 @@
package packages
import (
"fmt"
"io"
"time"
"github.com/anchore/syft/internal/spdxlicense"
"github.com/anchore/syft/internal"
"github.com/anchore/syft/internal/version"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/spdx/tools-golang/spdx"
"github.com/spdx/tools-golang/tvsaver"
)
// SPDXTagValuePresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec)
type SPDXTagValuePresenter struct {
catalog *pkg.Catalog
srcMetadata source.Metadata
}
// NewJSONPresenter creates a new JSON presenter object for the given cataloging results.
func NewSPDXTagValuePresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXTagValuePresenter {
return &SPDXTagValuePresenter{
catalog: catalog,
srcMetadata: srcMetadata,
}
}
// Present the catalog results to the given writer.
// nolint: funlen
func (pres *SPDXTagValuePresenter) Present(output io.Writer) error {
doc := spdx.Document2_2{
CreationInfo: &spdx.CreationInfo2_2{
// 2.1: SPDX Version; should be in the format "SPDX-2.2"
// Cardinality: mandatory, one
SPDXVersion: "SPDX-2.2",
// 2.2: Data License; should be "CC0-1.0"
// Cardinality: mandatory, one
DataLicense: "CC0-1.0",
// 2.3: SPDX Identifier; should be "DOCUMENT" to represent mandatory identifier of SPDXRef-DOCUMENT
// Cardinality: mandatory, one
SPDXIdentifier: spdx.ElementID("DOCUMENT"),
// 2.4: Document Name
// Cardinality: mandatory, one
DocumentName: pres.srcMetadata.ImageMetadata.UserInput,
// 2.5: Document Namespace
// Cardinality: mandatory, one
// Purpose: Provide an SPDX document specific namespace as a unique absolute Uniform Resource
// Identifier (URI) as specified in RFC-3986, with the exception of the ‘#’ delimiter. The SPDX
// Document URI cannot contain a URI "part" (e.g. the "#" character), since the ‘#’ is used in SPDX
// element URIs (packages, files, snippets, etc) to separate the document namespace from the
// element’s SPDX identifier. Additionally, a scheme (e.g. “https:”) is required.
// The URI must be unique for the SPDX document including the specific version of the SPDX document.
// If the SPDX document is updated, thereby creating a new version, a new URI for the updated
// document must be used. There can only be one URI for an SPDX document and only one SPDX document
// for a given URI.
// Note that the URI does not have to be accessible. It is only intended to provide a unique ID.
// In many cases, the URI will point to a web accessible document, but this should not be assumed
// to be the case.
DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", pres.srcMetadata.ImageMetadata.UserInput),
// 2.6: External Document References
// Cardinality: optional, one or many
ExternalDocumentReferences: nil,
// 2.7: License List Version
// Cardinality: optional, one
LicenseListVersion: spdxlicense.Version,
// 2.8: Creators: may have multiple keys for Person, Organization
// and/or Tool
// Cardinality: mandatory, one or many
CreatorPersons: nil,
CreatorOrganizations: []string{"Anchore, Inc"},
CreatorTools: []string{internal.ApplicationName + "-" + version.FromBuild().Version},
// 2.9: Created: data format YYYY-MM-DDThh:mm:ssZ
// Cardinality: mandatory, one
Created: time.Now().UTC().Format(time.RFC3339),
// 2.10: Creator Comment
// Cardinality: optional, one
CreatorComment: "",
// 2.11: Document Comment
// Cardinality: optional, one
DocumentComment: "",
},
Packages: pres.packages(),
}
return tvsaver.Save2_2(&doc, output)
}
// packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/)
// nolint: funlen
func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_2 {
results := make(map[spdx.ElementID]*spdx.Package2_2)
for p := range pres.catalog.Enumerate() {
// name should be guaranteed to be unique, but semantically useful and stable
id := fmt.Sprintf("Package-%+v-%s", p.Type, p.Name)
// If the Concluded License is not the same as the Declared License, a written explanation should be provided
// in the Comments on License field (section 3.16). With respect to NOASSERTION, a written explanation in
// the Comments on License field (section 3.16) is preferred.
license := getSPDXLicense(p)
results[spdx.ElementID(id)] = &spdx.Package2_2{
// NOT PART OF SPEC
// flag: does this "package" contain files that were in fact "unpackaged",
// e.g. included directly in the Document without being in a Package?
IsUnpackaged: false,
// 3.1: Package Name
// Cardinality: mandatory, one
PackageName: p.Name,
// 3.2: Package SPDX Identifier: "SPDXRef-[idstring]"
// Cardinality: mandatory, one
PackageSPDXIdentifier: spdx.ElementID(id),
// 3.3: Package Version
// Cardinality: optional, one
PackageVersion: p.Version,
// 3.4: Package File Name
// Cardinality: optional, one
PackageFileName: "",
// 3.5: Package Supplier: may have single result for either Person or Organization,
// or NOASSERTION
// Cardinality: optional, one
PackageSupplierPerson: "",
PackageSupplierOrganization: "",
PackageSupplierNOASSERTION: false,
// 3.6: Package Originator: may have single result for either Person or Organization,
// or NOASSERTION
// Cardinality: optional, one
PackageOriginatorPerson: "",
PackageOriginatorOrganization: "",
PackageOriginatorNOASSERTION: false,
// 3.7: Package Download Location
// Cardinality: mandatory, one
// NONE if there is no download location whatsoever.
// NOASSERTION if:
// (i) the SPDX file creator has attempted to but cannot reach a reasonable objective determination;
// (ii) the SPDX file creator has made no attempt to determine this field; or
// (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
PackageDownloadLocation: "NOASSERTION",
// 3.8: FilesAnalyzed
// Cardinality: optional, one; default value is "true" if omitted
// Purpose: Indicates whether the file content of this package has been available for or subjected to
// analysis when creating the SPDX document. If false, indicates packages that represent metadata or
// URI references to a project, product, artifact, distribution or a component. If false, the package
// must not contain any files.
// Intent: A package can refer to a project, product, artifact, distribution or a component that is
// external to the SPDX document.
FilesAnalyzed: false,
// NOT PART OF SPEC: did FilesAnalyzed tag appear?
IsFilesAnalyzedTagPresent: true,
// 3.9: Package Verification Code
// Cardinality: mandatory, one if filesAnalyzed is true / omitted;
// zero (must be omitted) if filesAnalyzed is false
PackageVerificationCode: "",
// Spec also allows specifying a single file to exclude from the
// verification code algorithm; intended to enable exclusion of
// the SPDX document file itself.
PackageVerificationCodeExcludedFile: "",
// 3.10: Package Checksum: may have keys for SHA1, SHA256 and/or MD5
// Cardinality: optional, one or many
// 3.10.1 Purpose: Provide an independently reproducible mechanism that permits unique identification of
// a specific package that correlates to the data in this SPDX file. This identifier enables a recipient
// to determine if any file in the original package has been changed. If the SPDX file is to be included
// in a package, this value should not be calculated. The SHA-1 algorithm will be used to provide the
// checksum by default.
// note: based on the purpose above no discovered checksums should be provided, but instead, only
// tool-derived checksums.
PackageChecksumSHA1: "",
PackageChecksumSHA256: "",
PackageChecksumMD5: "",
// 3.11: Package Home Page
// Cardinality: optional, one
PackageHomePage: "",
// 3.12: Source Information
// Cardinality: optional, one
PackageSourceInfo: "",
// 3.13: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
// Purpose: Contain the license the SPDX file creator has concluded as governing the
// package or alternative values, if the governing license cannot be determined.
PackageLicenseConcluded: license,
// 3.14: All Licenses Info from Files: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one or many if filesAnalyzed is true / omitted;
// zero (must be omitted) if filesAnalyzed is false
PackageLicenseInfoFromFiles: nil,
// 3.15: Declared License: SPDX License Expression, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
// Purpose: List the licenses that have been declared by the authors of the package.
// Any license information that does not originate from the package authors, e.g. license
// information from a third party repository, should not be included in this field.
PackageLicenseDeclared: license,
// 3.16: Comments on License
// Cardinality: optional, one
PackageLicenseComments: "",
// 3.17: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION"
// Cardinality: mandatory, one
// Purpose: Identify the copyright holders of the package, as well as any dates present. This will be a free form text field extracted from package information files. The options to populate this field are limited to:
//
// Any text related to a copyright notice, even if not complete;
// NONE if the package contains no copyright information whatsoever; or
// NOASSERTION, if
// (i) the SPDX document creator has made no attempt to determine this field; or
// (ii) the SPDX document creator has intentionally provided no information (no meaning should be implied by doing so).
//
PackageCopyrightText: "NOASSERTION",
// 3.18: Package Summary Description
// Cardinality: optional, one
PackageSummary: "",
// 3.19: Package Detailed Description
// Cardinality: optional, one
PackageDescription: "",
// 3.20: Package Comment
// Cardinality: optional, one
PackageComment: "",
// 3.21: Package External Reference
// Cardinality: optional, one or many
PackageExternalReferences: formatSPDXExternalRefs(p),
// 3.22: Package External Reference Comment
// Cardinality: conditional (optional, one) for each External Reference
// contained within PackageExternalReference2_1 struct, if present
// 3.23: Package Attribution Text
// Cardinality: optional, one or many
PackageAttributionTexts: nil,
// Files contained in this Package
Files: nil,
}
}
return results
}
func formatSPDXExternalRefs(p *pkg.Package) (refs []*spdx.PackageExternalReference2_2) {
for _, ref := range getSPDXExternalRefs(p) {
refs = append(refs, &spdx.PackageExternalReference2_2{
Category: string(ref.ReferenceCategory),
RefType: string(ref.ReferenceType),
Locator: ref.ReferenceLocator,
ExternalRefComment: ref.Comment,
})
}
return refs
}
@@ -0,0 +1,33 @@
package packages
import (
"flag"
"regexp"
"testing"
)
var updateSpdxTagValue = flag.Bool("update-spdx-tv", false, "update the *.golden files for spdx-tv presenters")
func TestSPDXTagValueDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata),
*updateSpdxTagValue,
spdxTagValueRedactor,
)
}
func TestSPDXTagValueImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewSPDXTagValuePresenter(catalog, metadata),
testImage,
*updateSpdxTagValue,
spdxTagValueRedactor,
)
}
func spdxTagValueRedactor(s []byte) []byte {
return regexp.MustCompile(`Created: .*`).ReplaceAll(s, []byte("redacted"))
}
@@ -1,72 +1,22 @@
package packages
import (
"bytes"
"flag"
"testing"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/go-test/deep"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
)
var updateTablePresenterGoldenFiles = flag.Bool("update-table", false, "update the *.golden files for table presenters")
func TestTablePresenter(t *testing.T) {
var buffer bytes.Buffer
testImage := "image-simple"
catalog := pkg.NewCatalog()
img := imagetest.GetFixtureImage(t, "docker-archive", testImage)
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
},
Type: pkg.DebPkg,
})
catalog.Add(pkg.Package{
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
},
Type: pkg.DebPkg,
})
pres := NewTablePresenter(catalog)
// run presenter
err := pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *updateTablePresenterGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
catalog, _, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewTablePresenter(catalog),
testImage,
*updateTablePresenterGoldenFiles,
)
}
func TestRemoveDuplicateRows(t *testing.T) {
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:544b9f08-b39d-4d0c-8723-32609567e8ed">
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:5404937f-72d6-44a2-8e9b-954305ecb4f6">
<metadata>
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@@ -16,20 +16,19 @@
</metadata>
<components>
<component type="library">
<name>package1</name>
<name>package-1</name>
<version>1.0.1</version>
</component>
<component type="library">
<name>package2</name>
<version>2.0.1</version>
<licenses>
<license>
<name>MIT</name>
</license>
<license>
<name>Apache-v2</name>
</license>
</licenses>
<purl>a-purl-2</purl>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<purl>a-purl-2</purl>
</component>
</components>
</bom>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e1515688-2fd6-422a-acb2-97405a44cdfe">
<bom xmlns="http://cyclonedx.org/schema/bom/1.2" version="1" serialNumber="urn:uuid:e34bad2e-cd27-483c-86dc-f4e26d6103b0">
<metadata>
<timestamp>2020-12-28T13:56:29-05:00</timestamp>
<timestamp>2021-06-23T13:40:33-04:00</timestamp>
<tools>
<tool>
<vendor>anchore</vendor>
@@ -16,22 +16,19 @@
</metadata>
<components>
<component type="library">
<name>package1</name>
<name>package-1</name>
<version>1.0.1</version>
<purl>the-purl-1</purl>
</component>
<component type="library">
<name>package2</name>
<version>2.0.1</version>
<licenses>
<license>
<name>MIT</name>
</license>
<license>
<name>Apache-v2</name>
</license>
</licenses>
<purl>the-purl-2</purl>
<purl>a-purl-1</purl>
</component>
<component type="library">
<name>package-2</name>
<version>2.0.1</version>
<purl>a-purl-2</purl>
</component>
</components>
</bom>
@@ -66,9 +66,9 @@
"target": "/some/path"
},
"distro": {
"name": "",
"version": "",
"idLike": ""
"name": "debian",
"version": "1.2.3",
"idLike": "like!"
},
"descriptor": {
"name": "syft",
@@ -93,9 +93,9 @@
}
},
"distro": {
"name": "",
"version": "",
"idLike": ""
"name": "debian",
"version": "1.2.3",
"idLike": "like!"
},
"descriptor": {
"name": "syft",
@@ -0,0 +1,61 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"name": "/some/path",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-06-23T17:48:32.734847Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
],
"licenseListVersion": "3.13"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/",
"packages": [
{
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
"name": "package-1",
"licenseConcluded": "MIT",
"downloadLocation": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
"referenceType": "cpe23Type"
},
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "a-purl-2",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseDeclared": "MIT",
"sourceInfo": "acquired package info from installed python package manifest file: /some/path/pkg1",
"versionInfo": "1.0.1"
},
{
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
"name": "package-2",
"licenseConcluded": "NONE",
"downloadLocation": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
"referenceType": "cpe23Type"
},
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "a-purl-2",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseDeclared": "NONE",
"sourceInfo": "acquired package info from DPKG DB: /some/path/pkg1",
"versionInfo": "2.0.1"
}
]
}
@@ -0,0 +1,61 @@
{
"SPDXID": "SPDXRef-DOCUMENT",
"name": "user-image-input",
"spdxVersion": "SPDX-2.2",
"creationInfo": {
"created": "2021-06-23T17:48:32.7379Z",
"creators": [
"Organization: Anchore, Inc",
"Tool: syft-[not provided]"
],
"licenseListVersion": "3.13"
},
"dataLicense": "CC0-1.0",
"documentNamespace": "https://anchore.com/syft/image/user-image-input",
"packages": [
{
"SPDXID": "SPDXRef-Package-python-package-1-1.0.1",
"name": "package-1",
"licenseConcluded": "MIT",
"downloadLocation": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:*:some:package:1:*:*:*:*:*:*:*",
"referenceType": "cpe23Type"
},
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "a-purl-1",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseDeclared": "MIT",
"sourceInfo": "acquired package info from installed python package manifest file: /somefile-1.txt",
"versionInfo": "1.0.1"
},
{
"SPDXID": "SPDXRef-Package-deb-package-2-2.0.1",
"name": "package-2",
"licenseConcluded": "NONE",
"downloadLocation": "NOASSERTION",
"externalRefs": [
{
"referenceCategory": "SECURITY",
"referenceLocator": "cpe:2.3:*:some:package:2:*:*:*:*:*:*:*",
"referenceType": "cpe23Type"
},
{
"referenceCategory": "PACKAGE_MANAGER",
"referenceLocator": "a-purl-2",
"referenceType": "purl"
}
],
"filesAnalyzed": false,
"licenseDeclared": "NONE",
"sourceInfo": "acquired package info from DPKG DB: /somefile-2.txt",
"versionInfo": "2.0.1"
}
]
}
@@ -0,0 +1,35 @@
SPDXVersion: SPDX-2.2
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentNamespace: https://anchore.com/syft/image/
LicenseListVersion: 3.13
Creator: Organization: Anchore, Inc
Creator: Tool: syft-[not provided]
Created: 2021-06-23T17:49:25Z
##### Package: package-2
PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2
PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
ExternalRef: PACKAGE_MANAGER purl a-purl-2
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1
PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageLicenseConcluded: MIT
PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
ExternalRef: PACKAGE_MANAGER purl a-purl-2
@@ -0,0 +1,36 @@
SPDXVersion: SPDX-2.2
DataLicense: CC0-1.0
SPDXID: SPDXRef-DOCUMENT
DocumentName: user-image-input
DocumentNamespace: https://anchore.com/syft/image/user-image-input
LicenseListVersion: 3.13
Creator: Organization: Anchore, Inc
Creator: Tool: syft-[not provided]
Created: 2021-06-23T17:49:25Z
##### Package: package-2
PackageName: package-2
SPDXID: SPDXRef-Package-deb-package-2
PackageVersion: 2.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageLicenseConcluded: NONE
PackageLicenseDeclared: NONE
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:2:*:*:*:*:*:*:*
ExternalRef: PACKAGE_MANAGER purl a-purl-2
##### Package: package-1
PackageName: package-1
SPDXID: SPDXRef-Package-python-package-1
PackageVersion: 1.0.1
PackageDownloadLocation: NOASSERTION
FilesAnalyzed: false
PackageLicenseConcluded: MIT
PackageLicenseDeclared: MIT
PackageCopyrightText: NOASSERTION
ExternalRef: SECURITY cpe23Type cpe:2.3:*:some:package:1:*:*:*:*:*:*:*
ExternalRef: PACKAGE_MANAGER purl a-purl-1
@@ -1,3 +1,3 @@
NAME VERSION TYPE
package-1 1.0.1 deb
package-2 2.0.1 deb
NAME VERSION TYPE
package-1 1.0.1 python
package-2 2.0.1 deb
@@ -1,11 +1,11 @@
[Path: /some/path]
[package-1]
Version: 1.0.1
Type: deb
Found by:
Type: python
Found by: the-cataloger-1
[package-2]
Version: 2.0.1
Type: deb
Found by:
Found by: the-cataloger-2
@@ -0,0 +1,21 @@
[Image]
Layer: 0
Digest: sha256:fb6beecb75b39f4bb813dbf177e501edd5ddb3e69bb45cedeb78c676ee1b7a59
Size: 22
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
Layer: 1
Digest: sha256:319b588ce64253a87b533c8ed01cf0025e0eac98e7b516e12532957e1244fdec
Size: 16
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
[package-1]
Version: 1.0.1
Type: python
Found by: the-cataloger-1
[package-2]
Version: 2.0.1
Type: deb
Found by: the-cataloger-2
@@ -1,21 +0,0 @@
[Image]
Layer: 0
Digest: sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53
Size: 22
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
Layer: 1
Digest: sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53
Size: 16
MediaType: application/vnd.docker.image.rootfs.diff.tar.gzip
[package-1]
Version: 1.0.1
Type: deb
Found by: dpkg
[package-2]
Version: 2.0.1
Type: deb
Found by: dpkg
@@ -1,11 +0,0 @@
[Path: /some/path]
[package-1]
Version: 1.0.1
Type: deb
Found by:
[package-2]
Version: 2.0.1
Type: deb
Found by:
@@ -1,127 +1,26 @@
package packages
import (
"bytes"
"flag"
"testing"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
)
var updateTextPresenterGoldenFiles = flag.Bool("update-text", false, "update the *.golden files for text presenters")
func TestTextDirPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Type: pkg.DebPkg,
})
catalog.Add(pkg.Package{
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
})
s, err := source.NewFromDirectory("/some/path")
if err != nil {
t.Fatalf("unable to create source: %+v", err)
}
pres := NewTextPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *updateTextPresenterGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
func TestTextDirectoryPresenter(t *testing.T) {
catalog, metadata, _ := presenterDirectoryInput(t)
assertPresenterAgainstGoldenSnapshot(t,
NewTextPresenter(catalog, metadata),
*updateTextPresenterGoldenFiles,
)
}
type PackageInfo struct {
Name string
Version string
}
func TestTextImgPresenter(t *testing.T) {
var buffer bytes.Buffer
catalog := pkg.NewCatalog()
img := imagetest.GetFixtureImage(t, "docker-archive", "image-simple")
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
// populate catalog with test data
catalog.Add(pkg.Package{
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
},
FoundBy: "dpkg",
Type: pkg.DebPkg,
})
catalog.Add(pkg.Package{
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
},
FoundBy: "dpkg",
Metadata: PackageInfo{Name: "package-2", Version: "1.0.2"},
Type: pkg.DebPkg,
})
// stub out all the digests so that they don't affect tests comparisons
// TODO: update with stereoscope test utils feature when this issue is resolved: https://github.com/anchore/stereoscope/issues/43
for _, l := range img.Layers {
l.Metadata.Digest = "sha256:ad8ecdc058976c07e7e347cb89fa9ad86a294b5ceaae6d09713fb035f84115abf3c4a2388a4af3aa60f13b94f4c6846930bdf53"
}
s, err := source.NewFromImage(img, "user-image-input")
if err != nil {
t.Fatal(err)
}
pres := NewTextPresenter(catalog, s.Metadata)
// run presenter
err = pres.Present(&buffer)
if err != nil {
t.Fatal(err)
}
actual := buffer.Bytes()
if *updateTextPresenterGoldenFiles {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(actual), string(expected), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
func TestTextImagePresenter(t *testing.T) {
testImage := "image-simple"
catalog, metadata, _ := presenterImageInput(t, testImage)
assertPresenterAgainstGoldenImageSnapshot(t,
NewTextPresenter(catalog, metadata),
testImage,
*updateTextPresenterGoldenFiles,
)
}
+194
View File
@@ -0,0 +1,194 @@
package packages
import (
"bytes"
"testing"
"github.com/anchore/go-testutils"
"github.com/anchore/stereoscope/pkg/filetree"
"github.com/anchore/stereoscope/pkg/imagetest"
"github.com/anchore/syft/syft/distro"
"github.com/anchore/syft/syft/pkg"
"github.com/anchore/syft/syft/presenter"
"github.com/anchore/syft/syft/source"
"github.com/sergi/go-diff/diffmatchpatch"
"github.com/stretchr/testify/assert"
)
type redactor func(s []byte) []byte
func assertPresenterAgainstGoldenImageSnapshot(t *testing.T, pres presenter.Presenter, testImage string, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
// grab the latest image contents and persist
if updateSnapshot {
imagetest.UpdateGoldenFixtureImage(t, testImage)
}
err := pres.Present(&buffer)
assert.NoError(t, err)
actual := buffer.Bytes()
// replace the expected snapshot contents with the current presenter contents
if updateSnapshot {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
// remove dynamic values, which should be tested independently
for _, r := range redactors {
actual = r(actual)
expected = r(expected)
}
// assert that the golden file snapshot matches the actual contents
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
func assertPresenterAgainstGoldenSnapshot(t *testing.T, pres presenter.Presenter, updateSnapshot bool, redactors ...redactor) {
var buffer bytes.Buffer
err := pres.Present(&buffer)
assert.NoError(t, err)
actual := buffer.Bytes()
// replace the expected snapshot contents with the current presenter contents
if updateSnapshot {
testutils.UpdateGoldenFileContents(t, actual)
}
var expected = testutils.GetGoldenFileContents(t)
// remove dynamic values, which should be tested independently
for _, r := range redactors {
actual = r(actual)
expected = r(expected)
}
if !bytes.Equal(expected, actual) {
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(string(expected), string(actual), true)
t.Errorf("mismatched output:\n%s", dmp.DiffPrettyText(diffs))
}
}
func presenterImageInput(t testing.TB, testImage string) (*pkg.Catalog, source.Metadata, *distro.Distro) {
t.Helper()
catalog := pkg.NewCatalog()
img := imagetest.GetGoldenFixtureImage(t, testImage)
_, ref1, _ := img.SquashedTree().File("/somefile-1.txt", filetree.FollowBasenameLinks)
_, ref2, _ := img.SquashedTree().File("/somefile-2.txt", filetree.FollowBasenameLinks)
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref1.RealPath), *ref1, img),
},
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"},
Metadata: pkg.PythonPackageMetadata{
Name: "package-1",
Version: "1.0.1",
},
PURL: "a-purl-1",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:1:*:*:*:*:*:*:*")),
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Locations: []source.Location{
source.NewLocationFromImage(string(ref2.RealPath), *ref2, img),
},
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "package-2",
Version: "2.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
// this is a hard coded value that is not given by the fixture helper and must be provided manually
img.Metadata.ManifestDigest = "sha256:2731251dc34951c0e50fcc643b4c5f74922dad1a5d98f302b504cf46cd5d9368"
src, err := source.NewFromImage(img, "user-image-input")
assert.NoError(t, err)
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
assert.NoError(t, err)
return catalog, src.Metadata, &dist
}
func presenterDirectoryInput(t testing.TB) (*pkg.Catalog, source.Metadata, *distro.Distro) {
catalog := pkg.NewCatalog()
// populate catalog with test data
catalog.Add(pkg.Package{
ID: "package-1-id",
Name: "package-1",
Version: "1.0.1",
Type: pkg.PythonPkg,
FoundBy: "the-cataloger-1",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
},
Language: pkg.Python,
MetadataType: pkg.PythonPackageMetadataType,
Licenses: []string{"MIT"},
Metadata: pkg.PythonPackageMetadata{
Name: "package-1",
Version: "1.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
catalog.Add(pkg.Package{
ID: "package-2-id",
Name: "package-2",
Version: "2.0.1",
Type: pkg.DebPkg,
FoundBy: "the-cataloger-2",
Locations: []source.Location{
{RealPath: "/some/path/pkg1"},
},
MetadataType: pkg.DpkgMetadataType,
Metadata: pkg.DpkgMetadata{
Package: "package-2",
Version: "2.0.1",
},
PURL: "a-purl-2",
CPEs: []pkg.CPE{
must(pkg.NewCPE("cpe:2.3:*:some:package:2:*:*:*:*:*:*:*")),
},
})
dist, err := distro.NewDistro(distro.Debian, "1.2.3", "like!")
assert.NoError(t, err)
src, err := source.NewFromDirectory("/some/path")
assert.NoError(t, err)
return catalog, src.Metadata, &dist
}
@@ -0,0 +1,92 @@
// +build ignore
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"strings"
"text/template"
"time"
)
// This program generates license_list.go.
const (
source = "license_list.go"
url = "https://spdx.org/licenses/licenses.json"
)
var tmp = template.Must(template.New("").Parse(`// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at {{ .Timestamp }}
// using data from {{ .URL }}
package spdxlicense
const Version = {{ printf "%q" .Version }}
var licenseIDs = map[string]string{
{{- range $k, $v := .LicenseIDs }}
{{ printf "%q" $k }}: {{ printf "%q" $v }},
{{- end }}
}
`))
type LicenseList struct {
Version string `json:"licenseListVersion"`
Licenses []struct {
ID string `json:"licenseId"`
Name string `json:"name"`
Text string `json:"licenseText"`
Deprecated bool `json:"isDeprecatedLicenseId"`
OSIApproved bool `json:"isOsiApproved"`
SeeAlso []string `json:"seeAlso"`
} `json:"licenses"`
}
func main() {
resp, err := http.Get(url)
if err != nil {
log.Fatalf("unable to get licenses list: %+v", err)
}
var result LicenseList
if err = json.NewDecoder(resp.Body).Decode(&result); err != nil {
log.Fatalf("unable to decode license list: %+v", err)
}
f, err := os.Create(source)
if err != nil {
log.Fatalf("unable to create %q: %+v", source, err)
}
defer func() {
if err := f.Close(); err != nil {
log.Fatalf("unable to close %q: %+v", source, err)
}
}()
var licenseIDs = make(map[string]string)
for _, l := range result.Licenses {
cleanID := strings.ToLower(l.ID)
if _, exists := licenseIDs[cleanID]; exists {
log.Fatalf("duplicate license ID found: %q", cleanID)
}
licenseIDs[cleanID] = l.ID
}
err = tmp.Execute(f, struct {
Timestamp time.Time
URL string
Version string
LicenseIDs map[string]string
}{
Timestamp: time.Now(),
URL: url,
Version: result.Version,
LicenseIDs: licenseIDs,
})
if err != nil {
log.Fatalf("unable to generate template: %+v", err)
}
}
+12
View File
@@ -0,0 +1,12 @@
package spdxlicense
import (
"strings"
)
//go:generate go run generate_license_list.go
func ID(id string) (string, bool) {
value, exists := licenseIDs[strings.ToLower(id)]
return value, exists
}
+469
View File
@@ -0,0 +1,469 @@
// Code generated by go generate; DO NOT EDIT.
// This file was generated by robots at 2021-06-23 11:13:41.969613 -0400 EDT m=+0.325660925
// using data from https://spdx.org/licenses/licenses.json
package spdxlicense
const Version = "3.13"
var licenseIDs = map[string]string{
"0bsd": "0BSD",
"aal": "AAL",
"abstyles": "Abstyles",
"adobe-2006": "Adobe-2006",
"adobe-glyph": "Adobe-Glyph",
"adsl": "ADSL",
"afl-1.1": "AFL-1.1",
"afl-1.2": "AFL-1.2",
"afl-2.0": "AFL-2.0",
"afl-2.1": "AFL-2.1",
"afl-3.0": "AFL-3.0",
"afmparse": "Afmparse",
"agpl-1.0": "AGPL-1.0",
"agpl-1.0-only": "AGPL-1.0-only",
"agpl-1.0-or-later": "AGPL-1.0-or-later",
"agpl-3.0": "AGPL-3.0",
"agpl-3.0-only": "AGPL-3.0-only",
"agpl-3.0-or-later": "AGPL-3.0-or-later",
"aladdin": "Aladdin",
"amdplpa": "AMDPLPA",
"aml": "AML",
"ampas": "AMPAS",
"antlr-pd": "ANTLR-PD",
"antlr-pd-fallback": "ANTLR-PD-fallback",
"apache-1.0": "Apache-1.0",
"apache-1.1": "Apache-1.1",
"apache-2.0": "Apache-2.0",
"apafml": "APAFML",
"apl-1.0": "APL-1.0",
"apsl-1.0": "APSL-1.0",
"apsl-1.1": "APSL-1.1",
"apsl-1.2": "APSL-1.2",
"apsl-2.0": "APSL-2.0",
"artistic-1.0": "Artistic-1.0",
"artistic-1.0-cl8": "Artistic-1.0-cl8",
"artistic-1.0-perl": "Artistic-1.0-Perl",
"artistic-2.0": "Artistic-2.0",
"bahyph": "Bahyph",
"barr": "Barr",
"beerware": "Beerware",
"bittorrent-1.0": "BitTorrent-1.0",
"bittorrent-1.1": "BitTorrent-1.1",
"blessing": "blessing",
"blueoak-1.0.0": "BlueOak-1.0.0",
"borceux": "Borceux",
"bsd-1-clause": "BSD-1-Clause",
"bsd-2-clause": "BSD-2-Clause",
"bsd-2-clause-freebsd": "BSD-2-Clause-FreeBSD",
"bsd-2-clause-netbsd": "BSD-2-Clause-NetBSD",
"bsd-2-clause-patent": "BSD-2-Clause-Patent",
"bsd-2-clause-views": "BSD-2-Clause-Views",
"bsd-3-clause": "BSD-3-Clause",
"bsd-3-clause-attribution": "BSD-3-Clause-Attribution",
"bsd-3-clause-clear": "BSD-3-Clause-Clear",
"bsd-3-clause-lbnl": "BSD-3-Clause-LBNL",
"bsd-3-clause-modification": "BSD-3-Clause-Modification",
"bsd-3-clause-no-military-license": "BSD-3-Clause-No-Military-License",
"bsd-3-clause-no-nuclear-license": "BSD-3-Clause-No-Nuclear-License",
"bsd-3-clause-no-nuclear-license-2014": "BSD-3-Clause-No-Nuclear-License-2014",
"bsd-3-clause-no-nuclear-warranty": "BSD-3-Clause-No-Nuclear-Warranty",
"bsd-3-clause-open-mpi": "BSD-3-Clause-Open-MPI",
"bsd-4-clause": "BSD-4-Clause",
"bsd-4-clause-shortened": "BSD-4-Clause-Shortened",
"bsd-4-clause-uc": "BSD-4-Clause-UC",
"bsd-protection": "BSD-Protection",
"bsd-source-code": "BSD-Source-Code",
"bsl-1.0": "BSL-1.0",
"busl-1.1": "BUSL-1.1",
"bzip2-1.0.5": "bzip2-1.0.5",
"bzip2-1.0.6": "bzip2-1.0.6",
"c-uda-1.0": "C-UDA-1.0",
"cal-1.0": "CAL-1.0",
"cal-1.0-combined-work-exception": "CAL-1.0-Combined-Work-Exception",
"caldera": "Caldera",
"catosl-1.1": "CATOSL-1.1",
"cc-by-1.0": "CC-BY-1.0",
"cc-by-2.0": "CC-BY-2.0",
"cc-by-2.5": "CC-BY-2.5",
"cc-by-3.0": "CC-BY-3.0",
"cc-by-3.0-at": "CC-BY-3.0-AT",
"cc-by-3.0-us": "CC-BY-3.0-US",
"cc-by-4.0": "CC-BY-4.0",
"cc-by-nc-1.0": "CC-BY-NC-1.0",
"cc-by-nc-2.0": "CC-BY-NC-2.0",
"cc-by-nc-2.5": "CC-BY-NC-2.5",
"cc-by-nc-3.0": "CC-BY-NC-3.0",
"cc-by-nc-4.0": "CC-BY-NC-4.0",
"cc-by-nc-nd-1.0": "CC-BY-NC-ND-1.0",
"cc-by-nc-nd-2.0": "CC-BY-NC-ND-2.0",
"cc-by-nc-nd-2.5": "CC-BY-NC-ND-2.5",
"cc-by-nc-nd-3.0": "CC-BY-NC-ND-3.0",
"cc-by-nc-nd-3.0-igo": "CC-BY-NC-ND-3.0-IGO",
"cc-by-nc-nd-4.0": "CC-BY-NC-ND-4.0",
"cc-by-nc-sa-1.0": "CC-BY-NC-SA-1.0",
"cc-by-nc-sa-2.0": "CC-BY-NC-SA-2.0",
"cc-by-nc-sa-2.5": "CC-BY-NC-SA-2.5",
"cc-by-nc-sa-3.0": "CC-BY-NC-SA-3.0",
"cc-by-nc-sa-4.0": "CC-BY-NC-SA-4.0",
"cc-by-nd-1.0": "CC-BY-ND-1.0",
"cc-by-nd-2.0": "CC-BY-ND-2.0",
"cc-by-nd-2.5": "CC-BY-ND-2.5",
"cc-by-nd-3.0": "CC-BY-ND-3.0",
"cc-by-nd-4.0": "CC-BY-ND-4.0",
"cc-by-sa-1.0": "CC-BY-SA-1.0",
"cc-by-sa-2.0": "CC-BY-SA-2.0",
"cc-by-sa-2.0-uk": "CC-BY-SA-2.0-UK",
"cc-by-sa-2.1-jp": "CC-BY-SA-2.1-JP",
"cc-by-sa-2.5": "CC-BY-SA-2.5",
"cc-by-sa-3.0": "CC-BY-SA-3.0",
"cc-by-sa-3.0-at": "CC-BY-SA-3.0-AT",
"cc-by-sa-4.0": "CC-BY-SA-4.0",
"cc-pddc": "CC-PDDC",
"cc0-1.0": "CC0-1.0",
"cddl-1.0": "CDDL-1.0",
"cddl-1.1": "CDDL-1.1",
"cdl-1.0": "CDL-1.0",
"cdla-permissive-1.0": "CDLA-Permissive-1.0",
"cdla-sharing-1.0": "CDLA-Sharing-1.0",
"cecill-1.0": "CECILL-1.0",
"cecill-1.1": "CECILL-1.1",
"cecill-2.0": "CECILL-2.0",
"cecill-2.1": "CECILL-2.1",
"cecill-b": "CECILL-B",
"cecill-c": "CECILL-C",
"cern-ohl-1.1": "CERN-OHL-1.1",
"cern-ohl-1.2": "CERN-OHL-1.2",
"cern-ohl-p-2.0": "CERN-OHL-P-2.0",
"cern-ohl-s-2.0": "CERN-OHL-S-2.0",
"cern-ohl-w-2.0": "CERN-OHL-W-2.0",
"clartistic": "ClArtistic",
"cnri-jython": "CNRI-Jython",
"cnri-python": "CNRI-Python",
"cnri-python-gpl-compatible": "CNRI-Python-GPL-Compatible",
"condor-1.1": "Condor-1.1",
"copyleft-next-0.3.0": "copyleft-next-0.3.0",
"copyleft-next-0.3.1": "copyleft-next-0.3.1",
"cpal-1.0": "CPAL-1.0",
"cpl-1.0": "CPL-1.0",
"cpol-1.02": "CPOL-1.02",
"crossword": "Crossword",
"crystalstacker": "CrystalStacker",
"cua-opl-1.0": "CUA-OPL-1.0",
"cube": "Cube",
"curl": "curl",
"d-fsl-1.0": "D-FSL-1.0",
"diffmark": "diffmark",
"doc": "DOC",
"dotseqn": "Dotseqn",
"drl-1.0": "DRL-1.0",
"dsdp": "DSDP",
"dvipdfm": "dvipdfm",
"ecl-1.0": "ECL-1.0",
"ecl-2.0": "ECL-2.0",
"ecos-2.0": "eCos-2.0",
"efl-1.0": "EFL-1.0",
"efl-2.0": "EFL-2.0",
"egenix": "eGenix",
"entessa": "Entessa",
"epics": "EPICS",
"epl-1.0": "EPL-1.0",
"epl-2.0": "EPL-2.0",
"erlpl-1.1": "ErlPL-1.1",
"etalab-2.0": "etalab-2.0",
"eudatagrid": "EUDatagrid",
"eupl-1.0": "EUPL-1.0",
"eupl-1.1": "EUPL-1.1",
"eupl-1.2": "EUPL-1.2",
"eurosym": "Eurosym",
"fair": "Fair",
"frameworx-1.0": "Frameworx-1.0",
"freebsd-doc": "FreeBSD-DOC",
"freeimage": "FreeImage",
"fsfap": "FSFAP",
"fsful": "FSFUL",
"fsfullr": "FSFULLR",
"ftl": "FTL",
"gd": "GD",
"gfdl-1.1": "GFDL-1.1",
"gfdl-1.1-invariants-only": "GFDL-1.1-invariants-only",
"gfdl-1.1-invariants-or-later": "GFDL-1.1-invariants-or-later",
"gfdl-1.1-no-invariants-only": "GFDL-1.1-no-invariants-only",
"gfdl-1.1-no-invariants-or-later": "GFDL-1.1-no-invariants-or-later",
"gfdl-1.1-only": "GFDL-1.1-only",
"gfdl-1.1-or-later": "GFDL-1.1-or-later",
"gfdl-1.2": "GFDL-1.2",
"gfdl-1.2-invariants-only": "GFDL-1.2-invariants-only",
"gfdl-1.2-invariants-or-later": "GFDL-1.2-invariants-or-later",
"gfdl-1.2-no-invariants-only": "GFDL-1.2-no-invariants-only",
"gfdl-1.2-no-invariants-or-later": "GFDL-1.2-no-invariants-or-later",
"gfdl-1.2-only": "GFDL-1.2-only",
"gfdl-1.2-or-later": "GFDL-1.2-or-later",
"gfdl-1.3": "GFDL-1.3",
"gfdl-1.3-invariants-only": "GFDL-1.3-invariants-only",
"gfdl-1.3-invariants-or-later": "GFDL-1.3-invariants-or-later",
"gfdl-1.3-no-invariants-only": "GFDL-1.3-no-invariants-only",
"gfdl-1.3-no-invariants-or-later": "GFDL-1.3-no-invariants-or-later",
"gfdl-1.3-only": "GFDL-1.3-only",
"gfdl-1.3-or-later": "GFDL-1.3-or-later",
"giftware": "Giftware",
"gl2ps": "GL2PS",
"glide": "Glide",
"glulxe": "Glulxe",
"glwtpl": "GLWTPL",
"gnuplot": "gnuplot",
"gpl-1.0": "GPL-1.0",
"gpl-1.0+": "GPL-1.0+",
"gpl-1.0-only": "GPL-1.0-only",
"gpl-1.0-or-later": "GPL-1.0-or-later",
"gpl-2.0": "GPL-2.0",
"gpl-2.0+": "GPL-2.0+",
"gpl-2.0-only": "GPL-2.0-only",
"gpl-2.0-or-later": "GPL-2.0-or-later",
"gpl-2.0-with-autoconf-exception": "GPL-2.0-with-autoconf-exception",
"gpl-2.0-with-bison-exception": "GPL-2.0-with-bison-exception",
"gpl-2.0-with-classpath-exception": "GPL-2.0-with-classpath-exception",
"gpl-2.0-with-font-exception": "GPL-2.0-with-font-exception",
"gpl-2.0-with-gcc-exception": "GPL-2.0-with-GCC-exception",
"gpl-3.0": "GPL-3.0",
"gpl-3.0+": "GPL-3.0+",
"gpl-3.0-only": "GPL-3.0-only",
"gpl-3.0-or-later": "GPL-3.0-or-later",
"gpl-3.0-with-autoconf-exception": "GPL-3.0-with-autoconf-exception",
"gpl-3.0-with-gcc-exception": "GPL-3.0-with-GCC-exception",
"gsoap-1.3b": "gSOAP-1.3b",
"haskellreport": "HaskellReport",
"hippocratic-2.1": "Hippocratic-2.1",
"hpnd": "HPND",
"hpnd-sell-variant": "HPND-sell-variant",
"htmltidy": "HTMLTIDY",
"ibm-pibs": "IBM-pibs",
"icu": "ICU",
"ijg": "IJG",
"imagemagick": "ImageMagick",
"imatix": "iMatix",
"imlib2": "Imlib2",
"info-zip": "Info-ZIP",
"intel": "Intel",
"intel-acpi": "Intel-ACPI",
"interbase-1.0": "Interbase-1.0",
"ipa": "IPA",
"ipl-1.0": "IPL-1.0",
"isc": "ISC",
"jasper-2.0": "JasPer-2.0",
"jpnic": "JPNIC",
"json": "JSON",
"lal-1.2": "LAL-1.2",
"lal-1.3": "LAL-1.3",
"latex2e": "Latex2e",
"leptonica": "Leptonica",
"lgpl-2.0": "LGPL-2.0",
"lgpl-2.0+": "LGPL-2.0+",
"lgpl-2.0-only": "LGPL-2.0-only",
"lgpl-2.0-or-later": "LGPL-2.0-or-later",
"lgpl-2.1": "LGPL-2.1",
"lgpl-2.1+": "LGPL-2.1+",
"lgpl-2.1-only": "LGPL-2.1-only",
"lgpl-2.1-or-later": "LGPL-2.1-or-later",
"lgpl-3.0": "LGPL-3.0",
"lgpl-3.0+": "LGPL-3.0+",
"lgpl-3.0-only": "LGPL-3.0-only",
"lgpl-3.0-or-later": "LGPL-3.0-or-later",
"lgpllr": "LGPLLR",
"libpng": "Libpng",
"libpng-2.0": "libpng-2.0",
"libselinux-1.0": "libselinux-1.0",
"libtiff": "libtiff",
"liliq-p-1.1": "LiLiQ-P-1.1",
"liliq-r-1.1": "LiLiQ-R-1.1",
"liliq-rplus-1.1": "LiLiQ-Rplus-1.1",
"linux-openib": "Linux-OpenIB",
"lpl-1.0": "LPL-1.0",
"lpl-1.02": "LPL-1.02",
"lppl-1.0": "LPPL-1.0",
"lppl-1.1": "LPPL-1.1",
"lppl-1.2": "LPPL-1.2",
"lppl-1.3a": "LPPL-1.3a",
"lppl-1.3c": "LPPL-1.3c",
"makeindex": "MakeIndex",
"miros": "MirOS",
"mit": "MIT",
"mit-0": "MIT-0",
"mit-advertising": "MIT-advertising",
"mit-cmu": "MIT-CMU",
"mit-enna": "MIT-enna",
"mit-feh": "MIT-feh",
"mit-modern-variant": "MIT-Modern-Variant",
"mit-open-group": "MIT-open-group",
"mitnfa": "MITNFA",
"motosoto": "Motosoto",
"mpich2": "mpich2",
"mpl-1.0": "MPL-1.0",
"mpl-1.1": "MPL-1.1",
"mpl-2.0": "MPL-2.0",
"mpl-2.0-no-copyleft-exception": "MPL-2.0-no-copyleft-exception",
"ms-pl": "MS-PL",
"ms-rl": "MS-RL",
"mtll": "MTLL",
"mulanpsl-1.0": "MulanPSL-1.0",
"mulanpsl-2.0": "MulanPSL-2.0",
"multics": "Multics",
"mup": "Mup",
"naist-2003": "NAIST-2003",
"nasa-1.3": "NASA-1.3",
"naumen": "Naumen",
"nbpl-1.0": "NBPL-1.0",
"ncgl-uk-2.0": "NCGL-UK-2.0",
"ncsa": "NCSA",
"net-snmp": "Net-SNMP",
"netcdf": "NetCDF",
"newsletr": "Newsletr",
"ngpl": "NGPL",
"nist-pd": "NIST-PD",
"nist-pd-fallback": "NIST-PD-fallback",
"nlod-1.0": "NLOD-1.0",
"nlpl": "NLPL",
"nokia": "Nokia",
"nosl": "NOSL",
"noweb": "Noweb",
"npl-1.0": "NPL-1.0",
"npl-1.1": "NPL-1.1",
"nposl-3.0": "NPOSL-3.0",
"nrl": "NRL",
"ntp": "NTP",
"ntp-0": "NTP-0",
"nunit": "Nunit",
"o-uda-1.0": "O-UDA-1.0",
"occt-pl": "OCCT-PL",
"oclc-2.0": "OCLC-2.0",
"odbl-1.0": "ODbL-1.0",
"odc-by-1.0": "ODC-By-1.0",
"ofl-1.0": "OFL-1.0",
"ofl-1.0-no-rfn": "OFL-1.0-no-RFN",
"ofl-1.0-rfn": "OFL-1.0-RFN",
"ofl-1.1": "OFL-1.1",
"ofl-1.1-no-rfn": "OFL-1.1-no-RFN",
"ofl-1.1-rfn": "OFL-1.1-RFN",
"ogc-1.0": "OGC-1.0",
"ogdl-taiwan-1.0": "OGDL-Taiwan-1.0",
"ogl-canada-2.0": "OGL-Canada-2.0",
"ogl-uk-1.0": "OGL-UK-1.0",
"ogl-uk-2.0": "OGL-UK-2.0",
"ogl-uk-3.0": "OGL-UK-3.0",
"ogtsl": "OGTSL",
"oldap-1.1": "OLDAP-1.1",
"oldap-1.2": "OLDAP-1.2",
"oldap-1.3": "OLDAP-1.3",
"oldap-1.4": "OLDAP-1.4",
"oldap-2.0": "OLDAP-2.0",
"oldap-2.0.1": "OLDAP-2.0.1",
"oldap-2.1": "OLDAP-2.1",
"oldap-2.2": "OLDAP-2.2",
"oldap-2.2.1": "OLDAP-2.2.1",
"oldap-2.2.2": "OLDAP-2.2.2",
"oldap-2.3": "OLDAP-2.3",
"oldap-2.4": "OLDAP-2.4",
"oldap-2.5": "OLDAP-2.5",
"oldap-2.6": "OLDAP-2.6",
"oldap-2.7": "OLDAP-2.7",
"oldap-2.8": "OLDAP-2.8",
"oml": "OML",
"openssl": "OpenSSL",
"opl-1.0": "OPL-1.0",
"oset-pl-2.1": "OSET-PL-2.1",
"osl-1.0": "OSL-1.0",
"osl-1.1": "OSL-1.1",
"osl-2.0": "OSL-2.0",
"osl-2.1": "OSL-2.1",
"osl-3.0": "OSL-3.0",
"parity-6.0.0": "Parity-6.0.0",
"parity-7.0.0": "Parity-7.0.0",
"pddl-1.0": "PDDL-1.0",
"php-3.0": "PHP-3.0",
"php-3.01": "PHP-3.01",
"plexus": "Plexus",
"polyform-noncommercial-1.0.0": "PolyForm-Noncommercial-1.0.0",
"polyform-small-business-1.0.0": "PolyForm-Small-Business-1.0.0",
"postgresql": "PostgreSQL",
"psf-2.0": "PSF-2.0",
"psfrag": "psfrag",
"psutils": "psutils",
"python-2.0": "Python-2.0",
"qhull": "Qhull",
"qpl-1.0": "QPL-1.0",
"rdisc": "Rdisc",
"rhecos-1.1": "RHeCos-1.1",
"rpl-1.1": "RPL-1.1",
"rpl-1.5": "RPL-1.5",
"rpsl-1.0": "RPSL-1.0",
"rsa-md": "RSA-MD",
"rscpl": "RSCPL",
"ruby": "Ruby",
"sax-pd": "SAX-PD",
"saxpath": "Saxpath",
"scea": "SCEA",
"sendmail": "Sendmail",
"sendmail-8.23": "Sendmail-8.23",
"sgi-b-1.0": "SGI-B-1.0",
"sgi-b-1.1": "SGI-B-1.1",
"sgi-b-2.0": "SGI-B-2.0",
"shl-0.5": "SHL-0.5",
"shl-0.51": "SHL-0.51",
"simpl-2.0": "SimPL-2.0",
"sissl": "SISSL",
"sissl-1.2": "SISSL-1.2",
"sleepycat": "Sleepycat",
"smlnj": "SMLNJ",
"smppl": "SMPPL",
"snia": "SNIA",
"spencer-86": "Spencer-86",
"spencer-94": "Spencer-94",
"spencer-99": "Spencer-99",
"spl-1.0": "SPL-1.0",
"ssh-openssh": "SSH-OpenSSH",
"ssh-short": "SSH-short",
"sspl-1.0": "SSPL-1.0",
"standardml-nj": "StandardML-NJ",
"sugarcrm-1.1.3": "SugarCRM-1.1.3",
"swl": "SWL",
"tapr-ohl-1.0": "TAPR-OHL-1.0",
"tcl": "TCL",
"tcp-wrappers": "TCP-wrappers",
"tmate": "TMate",
"torque-1.1": "TORQUE-1.1",
"tosl": "TOSL",
"tu-berlin-1.0": "TU-Berlin-1.0",
"tu-berlin-2.0": "TU-Berlin-2.0",
"ucl-1.0": "UCL-1.0",
"unicode-dfs-2015": "Unicode-DFS-2015",
"unicode-dfs-2016": "Unicode-DFS-2016",
"unicode-tou": "Unicode-TOU",
"unlicense": "Unlicense",
"upl-1.0": "UPL-1.0",
"vim": "Vim",
"vostrom": "VOSTROM",
"vsl-1.0": "VSL-1.0",
"w3c": "W3C",
"w3c-19980720": "W3C-19980720",
"w3c-20150513": "W3C-20150513",
"watcom-1.0": "Watcom-1.0",
"wsuipa": "Wsuipa",
"wtfpl": "WTFPL",
"wxwindows": "wxWindows",
"x11": "X11",
"xerox": "Xerox",
"xfree86-1.1": "XFree86-1.1",
"xinetd": "xinetd",
"xnet": "Xnet",
"xpp": "xpp",
"xskat": "XSkat",
"ypl-1.0": "YPL-1.0",
"ypl-1.1": "YPL-1.1",
"zed": "Zed",
"zend-2.0": "Zend-2.0",
"zimbra-1.3": "Zimbra-1.3",
"zimbra-1.4": "Zimbra-1.4",
"zlib": "Zlib",
"zlib-acknowledgement": "zlib-acknowledgement",
"zpl-1.1": "ZPL-1.1",
"zpl-2.0": "ZPL-2.0",
"zpl-2.1": "ZPL-2.1",
}
+14
View File
@@ -0,0 +1,14 @@
package spdxlicense
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestLicenceListIDs(t *testing.T) {
// do a sanity check on the generated data
assert.Equal(t, "0BSD", licenseIDs["0bsd"])
assert.Equal(t, "ZPL-2.1", licenseIDs["zpl-2.1"])
assert.NotEmpty(t, Version)
}