From b8b71f52d5ed0b937aaa92c1a2041df73c41662a Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sat, 27 Mar 2021 11:11:05 -0400 Subject: [PATCH 01/13] add initial spdx support Signed-off-by: Alex Goodman --- go.mod | 2 + go.sum | 7 + internal/presenter/packages/spdx_presenter.go | 288 ++++++++++++++++++ syft/presenter/packages/presenter.go | 2 + syft/presenter/packages/presenter_option.go | 4 + 5 files changed, 303 insertions(+) create mode 100644 internal/presenter/packages/spdx_presenter.go diff --git a/go.mod b/go.mod index 4887a45d98e..535800de92d 100644 --- a/go.mod +++ b/go.mod @@ -22,6 +22,7 @@ require ( github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-version v1.2.0 github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/go-spdx v0.1.0 github.com/mitchellh/mapstructure v1.3.1 github.com/olekukonko/tablewriter v0.0.4 github.com/package-url/packageurl-go v0.1.0 @@ -30,6 +31,7 @@ require ( github.com/scylladb/go-set v1.0.2 github.com/sergi/go-diff v1.1.0 github.com/sirupsen/logrus v1.6.0 + github.com/spdx/tools-golang v0.1.0 github.com/spf13/afero v1.2.2 github.com/spf13/cobra v1.0.1-0.20200909172742-8a63648dd905 github.com/spf13/pflag v1.0.5 diff --git a/go.sum b/go.sum index ff16a6e65eb..2beb90e0631 100644 --- a/go.sum +++ b/go.sum @@ -409,6 +409,8 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -536,6 +538,8 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= +github.com/mitchellh/go-spdx v0.1.0 h1:50JnVzkL3kWreQ5Qb4Pi3Qx9e+bbYrt8QglJDpfeBEs= +github.com/mitchellh/go-spdx v0.1.0/go.mod h1:FFi4Cg1fBuN/JCtPtP8PEDmcBjvO3gijQVl28YjIBVQ= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -672,6 +676,9 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k github.com/sourcegraph/go-diff v0.5.1/go.mod h1:j2dHj3m8aZgQO8lMTcTnBcXkRRRqi34cd2MNlA9u1mE= github.com/sourcegraph/go-diff v0.5.3/go.mod h1:v9JDtjCE4HHHCZGId75rg8gkKKa98RVjBcBGsVmMmak= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= +github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= +github.com/spdx/tools-golang v0.1.0 h1:iDMNEPqQk6CdiDj6eWDIDw85j0wQ3IR3pH9p0X05TSQ= +github.com/spdx/tools-golang v0.1.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= diff --git a/internal/presenter/packages/spdx_presenter.go b/internal/presenter/packages/spdx_presenter.go new file mode 100644 index 00000000000..997b755a5de --- /dev/null +++ b/internal/presenter/packages/spdx_presenter.go @@ -0,0 +1,288 @@ +package packages + +import ( + "fmt" + "io" + "time" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/version" + + "github.com/anchore/syft/syft/source" + + "github.com/anchore/syft/internal/log" + + "github.com/anchore/syft/syft/pkg" + spdxLicense "github.com/mitchellh/go-spdx" + "github.com/spdx/tools-golang/spdx" + "github.com/spdx/tools-golang/tvsaver" +) + +// SPDXPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec) +type SPDXPresenter struct { + catalog *pkg.Catalog + srcMetadata source.Metadata +} + +// NewJSONPresenter creates a new JSON presenter object for the given cataloging results. +func NewSPDXPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXPresenter { + return &SPDXPresenter{ + catalog: catalog, + srcMetadata: srcMetadata, + } +} + +// Present the catalog results to the given writer. +func (pres *SPDXPresenter) 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. + + // TODO: rethink this + 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: "", + + // 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().Format(time.RFC3339), + + // 2.10: Creator Comment + // Cardinality: optional, one + CreatorComment: "", + + // 2.11: Document Comment + // Cardinality: optional, one + DocumentComment: "", + }, + Packages: pres.packages(), + // TODO: consider adding the following fields + //UnpackagedFiles: nil, + //OtherLicenses: nil, + //Relationships: nil, + //Annotations: nil, + } + + 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/) +func (pres *SPDXPresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { + results := make(map[spdx.ElementID]*spdx.Package2_2) + + for p := range pres.catalog.Enumerate() { + // TODO: name should be guaranteed to be unique, but semantically useful (and stable) + id := fmt.Sprintf("Package-%+v-%s", p.Type, p.Name) + + // 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). + license := "NONE" + if len(p.Licenses) > 0 { + // note: we are not supporting complex expressions at this time, only individual licenses + licenseInfo, err := spdxLicense.License(p.Licenses[0]) + if err != nil { + log.Warnf("unable to parse SPDX license for package=%+v : %+v", p, err) + license = "NOASSERTION" + } else { + license = licenseInfo.ID + } + } + + 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: false, + + // 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 + 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: "NOASSERTION", + + // 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: nil, + + // 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 +} diff --git a/syft/presenter/packages/presenter.go b/syft/presenter/packages/presenter.go index ce9bb2dbe0d..7c5f08358a5 100644 --- a/syft/presenter/packages/presenter.go +++ b/syft/presenter/packages/presenter.go @@ -20,6 +20,8 @@ func Presenter(option PresenterOption, config PresenterConfig) presenter.Present return packages.NewTablePresenter(config.Catalog) case CycloneDxPresenterOption: return packages.NewCycloneDxPresenter(config.Catalog, config.SourceMetadata) + case SPDXPresenterOption: + return packages.NewSPDXPresenter(config.Catalog, config.SourceMetadata) default: return nil } diff --git a/syft/presenter/packages/presenter_option.go b/syft/presenter/packages/presenter_option.go index 9bfe1f6f2f9..4e703dde761 100644 --- a/syft/presenter/packages/presenter_option.go +++ b/syft/presenter/packages/presenter_option.go @@ -8,6 +8,7 @@ const ( TextPresenterOption PresenterOption = "text" TablePresenterOption PresenterOption = "table" CycloneDxPresenterOption PresenterOption = "cyclonedx" + SPDXPresenterOption PresenterOption = "spdx" ) var AllPresenters = []PresenterOption{ @@ -15,6 +16,7 @@ var AllPresenters = []PresenterOption{ TextPresenterOption, TablePresenterOption, CycloneDxPresenterOption, + SPDXPresenterOption, } type PresenterOption string @@ -29,6 +31,8 @@ func ParsePresenterOption(userStr string) PresenterOption { return TablePresenterOption case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx": return CycloneDxPresenterOption + case string(SPDXPresenterOption): + return SPDXPresenterOption default: return UnknownPresenterOption } From 40e93c8e888ba77cef9261965818dbcbe2f7d2e2 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Sun, 4 Apr 2021 09:16:54 -0400 Subject: [PATCH 02/13] expose FileOwner and use in SPDX presenter Signed-off-by: Alex Goodman --- internal/presenter/packages/spdx_presenter.go | 101 +++++++++++++++++- syft/pkg/apk_metadata.go | 4 +- syft/pkg/apk_metadata_test.go | 2 +- syft/pkg/dpkg_metadata.go | 4 +- syft/pkg/dpkg_metadata_test.go | 2 +- syft/pkg/file_owner.go | 4 +- syft/pkg/ownership_by_files_relationship.go | 2 +- syft/pkg/python_package_metadata.go | 4 +- syft/pkg/python_package_metadata_test.go | 2 +- syft/pkg/rpmdb_metadata.go | 4 +- syft/pkg/rpmdb_metadata_test.go | 2 +- 11 files changed, 113 insertions(+), 18 deletions(-) diff --git a/internal/presenter/packages/spdx_presenter.go b/internal/presenter/packages/spdx_presenter.go index 997b755a5de..477eaff13dc 100644 --- a/internal/presenter/packages/spdx_presenter.go +++ b/internal/presenter/packages/spdx_presenter.go @@ -139,6 +139,8 @@ func (pres *SPDXPresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { } } + filesAnalyzed, files := pres.packageFiles(p) + results[spdx.ElementID(id)] = &spdx.Package2_2{ // NOT PART OF SPEC @@ -195,9 +197,9 @@ func (pres *SPDXPresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { // Intent: A package can refer to a project, product, artifact, distribution or a component that is // external to the SPDX document. - FilesAnalyzed: false, + FilesAnalyzed: filesAnalyzed, // NOT PART OF SPEC: did FilesAnalyzed tag appear? - IsFilesAnalyzedTagPresent: false, + IsFilesAnalyzedTagPresent: true, // 3.9: Package Verification Code // Cardinality: mandatory, one if filesAnalyzed is true / omitted; @@ -210,6 +212,15 @@ func (pres *SPDXPresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { // 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: "", @@ -281,8 +292,92 @@ func (pres *SPDXPresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { PackageAttributionTexts: nil, // Files contained in this Package - Files: nil, + Files: files, } } return results } + +func (pres *SPDXPresenter) packageFiles(p *pkg.Package) (bool, map[spdx.ElementID]*spdx.File2_2) { + filesAnalyzed := false + files := make(map[spdx.ElementID]*spdx.File2_2) + if owner, ok := p.Metadata.(pkg.FileOwner); ok { + filesAnalyzed = true + for _, f := range owner.OwnedFiles() { + // TODO: should we include layer information in the element id? + id := spdx.ElementID(f) + files[id] = &spdx.File2_2{ + + // 4.1: File Name + // Cardinality: mandatory, one + FileName: f, + + // 4.2: File SPDX Identifier: "SPDXRef-[idstring]" + // Cardinality: mandatory, one + FileSPDXIdentifier: id, + + // 4.3: File Type + // Cardinality: optional, multiple + FileType: nil, + + // 4.4: File Checksum: may have keys for SHA1, SHA256 and/or MD5 + // Cardinality: mandatory, one SHA1, others may be optionally provided + // TODO: we don't have the resolvers at this point, but we could make that available? + FileChecksumSHA1: "", + FileChecksumSHA256: "", + FileChecksumMD5: "", + + // 4.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION" + // Cardinality: mandatory, one + LicenseConcluded: "NOASSERTION", + + // 4.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION" + // Cardinality: mandatory, one or many + // TODO: could use a license classifier here + LicenseInfoInFile: []string{"NOASSERTION"}, + + // 4.7: Comments on License + // Cardinality: optional, one + LicenseComments: "", + + // 4.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION" + // Cardinality: mandatory, one + FileCopyrightText: "NOASSERTION", + + // DEPRECATED in version 2.1 of spec + // 4.9-4.11: Artifact of Project variables (defined below) + // Cardinality: optional, one or many + ArtifactOfProjects: nil, + + // 4.12: File Comment + // Cardinality: optional, one + FileComment: "", + + // 4.13: File Notice + // Cardinality: optional, one + FileNotice: "", + + // 4.14: File Contributor + // Cardinality: optional, one or many + FileContributor: nil, + + // 4.15: File Attribution Text + // Cardinality: optional, one or many + FileAttributionTexts: nil, + + // DEPRECATED in version 2.0 of spec + // 4.16: File Dependencies + // Cardinality: optional, one or many + FileDependencies: nil, + + // Snippets contained in this File + // Note that Snippets could be defined in a different Document! However, + // the only ones that _THIS_ document can contain are this ones that are + // defined here -- so this should just be an ElementID. + Snippets: nil, + } + } + } + + return filesAnalyzed, files +} diff --git a/syft/pkg/apk_metadata.go b/syft/pkg/apk_metadata.go index 84b14864599..0578f30610c 100644 --- a/syft/pkg/apk_metadata.go +++ b/syft/pkg/apk_metadata.go @@ -11,7 +11,7 @@ import ( const ApkDbGlob = "**/lib/apk/db/installed" -var _ fileOwner = (*ApkMetadata)(nil) +var _ FileOwner = (*ApkMetadata)(nil) // ApkMetadata represents all captured data for a Alpine DB package entry. // See the following sources for more information: @@ -63,7 +63,7 @@ func (m ApkMetadata) PackageURL() string { return pURL.ToString() } -func (m ApkMetadata) ownedFiles() (result []string) { +func (m ApkMetadata) OwnedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/apk_metadata_test.go b/syft/pkg/apk_metadata_test.go index 1ff43a12b41..acce1e9b993 100644 --- a/syft/pkg/apk_metadata_test.go +++ b/syft/pkg/apk_metadata_test.go @@ -69,7 +69,7 @@ func TestApkMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(fileOwner).ownedFiles() + actual := i.(FileOwner).OwnedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index 8f2245c5513..b17bc385a86 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -12,7 +12,7 @@ import ( const DpkgDbGlob = "**/var/lib/dpkg/{status,status.d/**}" -var _ fileOwner = (*DpkgMetadata)(nil) +var _ FileOwner = (*DpkgMetadata)(nil) // DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described // at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section. @@ -55,7 +55,7 @@ func (m DpkgMetadata) PackageURL(d *distro.Distro) string { return pURL.ToString() } -func (m DpkgMetadata) ownedFiles() (result []string) { +func (m DpkgMetadata) OwnedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/dpkg_metadata_test.go b/syft/pkg/dpkg_metadata_test.go index 81a0cb66589..e0e42d949a2 100644 --- a/syft/pkg/dpkg_metadata_test.go +++ b/syft/pkg/dpkg_metadata_test.go @@ -88,7 +88,7 @@ func TestDpkgMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(fileOwner).ownedFiles() + actual := i.(FileOwner).OwnedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } diff --git a/syft/pkg/file_owner.go b/syft/pkg/file_owner.go index 24520c304e7..fba023d8b8a 100644 --- a/syft/pkg/file_owner.go +++ b/syft/pkg/file_owner.go @@ -1,5 +1,5 @@ package pkg -type fileOwner interface { - ownedFiles() []string +type FileOwner interface { + OwnedFiles() []string } diff --git a/syft/pkg/ownership_by_files_relationship.go b/syft/pkg/ownership_by_files_relationship.go index f235557a4ba..b1408b63e3b 100644 --- a/syft/pkg/ownership_by_files_relationship.go +++ b/syft/pkg/ownership_by_files_relationship.go @@ -55,7 +55,7 @@ func findOwnershipByFilesRelationships(catalog *Catalog) map[ID]map[ID]*strset.S } // check to see if this is a file owner - pkgFileOwner, ok := candidateOwnerPkg.Metadata.(fileOwner) + pkgFileOwner, ok := candidateOwnerPkg.Metadata.(FileOwner) if !ok { continue } diff --git a/syft/pkg/python_package_metadata.go b/syft/pkg/python_package_metadata.go index c1e752d217e..f059dfac35a 100644 --- a/syft/pkg/python_package_metadata.go +++ b/syft/pkg/python_package_metadata.go @@ -6,7 +6,7 @@ import ( "github.com/scylladb/go-set/strset" ) -var _ fileOwner = (*PythonPackageMetadata)(nil) +var _ FileOwner = (*PythonPackageMetadata)(nil) // PythonFileDigest represents the file metadata for a single file attributed to a python package. type PythonFileDigest struct { @@ -34,7 +34,7 @@ type PythonPackageMetadata struct { TopLevelPackages []string `json:"topLevelPackages,omitempty"` } -func (m PythonPackageMetadata) ownedFiles() (result []string) { +func (m PythonPackageMetadata) OwnedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/python_package_metadata_test.go b/syft/pkg/python_package_metadata_test.go index c2cd378176b..fab80bc2fcb 100644 --- a/syft/pkg/python_package_metadata_test.go +++ b/syft/pkg/python_package_metadata_test.go @@ -41,7 +41,7 @@ func TestPythonMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(fileOwner).ownedFiles() + actual := i.(FileOwner).OwnedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } diff --git a/syft/pkg/rpmdb_metadata.go b/syft/pkg/rpmdb_metadata.go index b6460c824f5..e48eb723cd6 100644 --- a/syft/pkg/rpmdb_metadata.go +++ b/syft/pkg/rpmdb_metadata.go @@ -15,7 +15,7 @@ import ( const RpmDbGlob = "**/var/lib/rpm/Packages" -var _ fileOwner = (*RpmdbMetadata)(nil) +var _ FileOwner = (*RpmdbMetadata)(nil) // RpmdbMetadata represents all captured data for a RPM DB package entry. type RpmdbMetadata struct { @@ -79,7 +79,7 @@ func (m RpmdbMetadata) PackageURL(d *distro.Distro) string { return pURL.ToString() } -func (m RpmdbMetadata) ownedFiles() (result []string) { +func (m RpmdbMetadata) OwnedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/rpmdb_metadata_test.go b/syft/pkg/rpmdb_metadata_test.go index 7eded204660..73a951f0046 100644 --- a/syft/pkg/rpmdb_metadata_test.go +++ b/syft/pkg/rpmdb_metadata_test.go @@ -90,7 +90,7 @@ func TestRpmMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(fileOwner).ownedFiles() + actual := i.(FileOwner).OwnedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } From de0c9bd50c54b832e7c241f8f1257ba961f27826 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Fri, 4 Jun 2021 09:25:42 -0400 Subject: [PATCH 03/13] add initial json support for SPDX Signed-off-by: Alex Goodman --- .../packages/model/spdx_2_2/spdx_2_2.go | 527 +++++++++++++++ .../presenter/packages/spdx_json_presenter.go | 150 +++++ ...esenter.go => spdx_tag_value_presenter.go} | 23 +- schema/spdx-json/spdx-schema-2.2.json | 610 ++++++++++++++++++ syft/pkg/ownership_by_files_relationship.go | 2 +- syft/presenter/packages/presenter.go | 6 +- syft/presenter/packages/presenter_option.go | 22 +- test/cli/json_schema_test.go | 8 +- test/cli/spdx_json_schema_test.go | 90 +++ test/cli/utils_test.go | 24 +- 10 files changed, 1423 insertions(+), 39 deletions(-) create mode 100644 internal/presenter/packages/model/spdx_2_2/spdx_2_2.go create mode 100644 internal/presenter/packages/spdx_json_presenter.go rename internal/presenter/packages/{spdx_presenter.go => spdx_tag_value_presenter.go} (95%) create mode 100644 schema/spdx-json/spdx-schema-2.2.json create mode 100644 test/cli/spdx_json_schema_test.go diff --git a/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go b/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go new file mode 100644 index 00000000000..bda4780df5e --- /dev/null +++ b/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go @@ -0,0 +1,527 @@ +package spdx_2_2 + +import "time" + +// 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 + +// 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 Relationships 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() +} + +type Element struct { + SPDXID string `json:"SPDXID"` + // Provide additional information about an SpdxElement. + Annotations []Annotation `json:"annotations"` + Comment string `json:"comment"` + // Identify name of this SpdxElement. + Name string `json:"name"` + // Relationships referenced in the SPDX document + Relationships []Relationships `json:"relationships"` +} + +type Document struct { + 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"` + DocumentNamespace string `json:"documentNamespace"` + DocumentDescribes []string `json:"documentDescribes"` + Packages []Package `json:"packages"` + // Files referenced in the SPDX document + Files []Files `json:"files"` + // Snippets referenced in the SPDX document + Snippets []Snippets `json:"snippets"` + Element +} + +type Item struct { + // 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"` + Element +} + +type CreationInfo struct { + Comment string `json:"comment"` + // 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"` +} +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"` +} +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"` +} +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"` +} + +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"` +} + +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 . + ReferenceLocator string `json:"referenceLocator"` + // Type of the external reference. These are defined in an appendix in the SPDX specification. + ReferenceType ExternalRefType `json:"referenceType"` +} + +// 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"` +} +type Package struct { + // 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"` + // 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"` + Homepage string `json:"homepage"` + 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"` + Item +} + +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 Files struct { + // (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"` + Item +} + +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 Snippets struct { + // 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"` + Item +} +type Relationships 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" +) diff --git a/internal/presenter/packages/spdx_json_presenter.go b/internal/presenter/packages/spdx_json_presenter.go new file mode 100644 index 00000000000..4db6c5bad7b --- /dev/null +++ b/internal/presenter/packages/spdx_json_presenter.go @@ -0,0 +1,150 @@ +package packages + +import ( + "encoding/json" + "fmt" + "io" + "time" + + spdxLicense "github.com/mitchellh/go-spdx" + + "github.com/anchore/syft/internal" + "github.com/anchore/syft/internal/log" + "github.com/anchore/syft/internal/presenter/packages/model/spdx_2_2" + "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) spdx_2_2.Document { + return spdx_2_2.Document{ + SPDXVersion: "SPDX-2.2", + CreationInfo: spdx_2_2.CreationInfo{ + Comment: "", + Created: time.Now().UTC(), + Creators: []string{ + "Anchore, Inc", + internal.ApplicationName + "-" + version.FromBuild().Version, + }, + LicenseListVersion: "", + }, + DataLicense: "CC0-1.0", + ExternalDocumentRefs: nil, + HasExtractedLicensingInfos: nil, + // TODO: rethink this + DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput), + DocumentDescribes: nil, + Packages: newSpdxJsonPackages(catalog), + Files: nil, + Snippets: nil, + Element: spdx_2_2.Element{ + // should this be unique to the user's input? or otherwise just say document? + SPDXID: spdx_2_2.ElementID("Document").String(), + Annotations: nil, + Comment: "", + Name: srcMetadata.ImageMetadata.UserInput, + Relationships: nil, + }, + } +} + +func newSpdxJsonPackages(catalog *pkg.Catalog) []spdx_2_2.Package { + results := make([]spdx_2_2.Package, 0) + for _, p := range catalog.Sorted() { + license := "NONE" + if len(p.Licenses) > 0 { + // note: we are not supporting complex expressions at this time, only individual licenses + licenseInfo, err := spdxLicense.License(p.Licenses[0]) + if err != nil { + log.Warnf("unable to parse SPDX license for package=%+v : %+v", p, err) + license = "NOASSERTION" + } else { + license = licenseInfo.ID + } + } + + externalRefs := make([]spdx_2_2.ExternalRef, 0) + for _, c := range p.CPEs { + externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ + Comment: "", + ReferenceCategory: spdx_2_2.SecurityReferenceCategory, + ReferenceLocator: c.BindToFmtString(), + ReferenceType: spdx_2_2.Cpe23ExternalRefType, + }) + } + + if p.PURL != "" { + externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ + Comment: "", + ReferenceCategory: spdx_2_2.PackageManagerReferenceCategory, + ReferenceLocator: p.PURL, + ReferenceType: spdx_2_2.PurlExternalRefType, + }) + } + + // 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, spdx_2_2.Package{ + Checksums: nil, + Description: "", + DownloadLocation: "", + ExternalRefs: externalRefs, + FilesAnalyzed: false, + HasFiles: nil, + Homepage: "", + // The Declared License is what the authors of a project believe govern the package + LicenseDeclared: license, + Originator: "", + PackageFileName: "", + PackageVerificationCode: spdx_2_2.PackageVerificationCode{}, + SourceInfo: "", + Summary: "", + Supplier: "", + VersionInfo: p.Version, + Item: spdx_2_2.Item{ + LicenseComments: "", + // The Concluded License field is the license the SPDX file creator believes governs the package + LicenseConcluded: license, + LicenseInfoFromFiles: nil, + LicenseInfoInFiles: nil, + CopyrightText: "", + AttributionTexts: nil, + Element: spdx_2_2.Element{ + SPDXID: spdx_2_2.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(), + Annotations: nil, + Comment: "", + Name: p.Name, + Relationships: nil, + }, + }, + }) + } + return results +} diff --git a/internal/presenter/packages/spdx_presenter.go b/internal/presenter/packages/spdx_tag_value_presenter.go similarity index 95% rename from internal/presenter/packages/spdx_presenter.go rename to internal/presenter/packages/spdx_tag_value_presenter.go index 477eaff13dc..6fd06974b29 100644 --- a/internal/presenter/packages/spdx_presenter.go +++ b/internal/presenter/packages/spdx_tag_value_presenter.go @@ -6,34 +6,31 @@ import ( "time" "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/version" - - "github.com/anchore/syft/syft/source" - "github.com/anchore/syft/internal/log" - + "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft/pkg" + "github.com/anchore/syft/syft/source" spdxLicense "github.com/mitchellh/go-spdx" "github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/tvsaver" ) -// SPDXPresenter is a SPDX presentation object for the syft results (see https://github.com/spdx/spdx-spec) -type SPDXPresenter struct { +// 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 NewSPDXPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXPresenter { - return &SPDXPresenter{ +func NewSPDXTagValuePresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SPDXTagValuePresenter { + return &SPDXTagValuePresenter{ catalog: catalog, srcMetadata: srcMetadata, } } // Present the catalog results to the given writer. -func (pres *SPDXPresenter) Present(output io.Writer) error { +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" @@ -90,7 +87,7 @@ func (pres *SPDXPresenter) Present(output io.Writer) error { // 2.9: Created: data format YYYY-MM-DDThh:mm:ssZ // Cardinality: mandatory, one - Created: time.Now().Format(time.RFC3339), + Created: time.Now().UTC().Format(time.RFC3339), // 2.10: Creator Comment // Cardinality: optional, one @@ -112,7 +109,7 @@ func (pres *SPDXPresenter) Present(output io.Writer) error { } // packages populates all Package Information from the package Catalog (see https://spdx.github.io/spdx-spec/3-package-information/) -func (pres *SPDXPresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { +func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { results := make(map[spdx.ElementID]*spdx.Package2_2) for p := range pres.catalog.Enumerate() { @@ -298,7 +295,7 @@ func (pres *SPDXPresenter) packages() map[spdx.ElementID]*spdx.Package2_2 { return results } -func (pres *SPDXPresenter) packageFiles(p *pkg.Package) (bool, map[spdx.ElementID]*spdx.File2_2) { +func (pres *SPDXTagValuePresenter) packageFiles(p *pkg.Package) (bool, map[spdx.ElementID]*spdx.File2_2) { filesAnalyzed := false files := make(map[spdx.ElementID]*spdx.File2_2) if owner, ok := p.Metadata.(pkg.FileOwner); ok { diff --git a/schema/spdx-json/spdx-schema-2.2.json b/schema/spdx-json/spdx-schema-2.2.json new file mode 100644 index 00000000000..8279648c998 --- /dev/null +++ b/schema/spdx-json/spdx-schema-2.2.json @@ -0,0 +1,610 @@ +{ + "$schema" : "http://json-schema.org/draft-07/schema#", + "$id" : "http://spdx.org/rdf/terms", + "title" : "SPDX 2.2", + "type" : "object", + "properties" : { + "Document" : { + "type" : "object", + "properties" : { + "revieweds" : { + "description" : "Reviewed", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "reviewer" : { + "description" : "The name and, optionally, contact information of the person who performed the review. Values of this property must conform to the agent and tool syntax.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "reviewDate" : { + "description" : "The date and time at which the SpdxDocument was reviewed. This value must be in UTC and have 'Z' as its timezone indicator.", + "type" : "string" + } + } + } + }, + "hasExtractedLicensingInfos" : { + "description" : "Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "seeAlsos" : { + "type" : "array", + "items" : { + "type" : "string" + } + }, + "name" : { + "description" : "Identify name of this SpdxElement.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "licenseId" : { + "description" : "A human readable short form license identifier for a license. The license ID is iether on the standard license oist or the form \"LicenseRef-\"[idString] where [idString] is a unique string containing letters, numbers, \".\", \"-\" or \"+\".", + "type" : "string" + }, + "extractedText" : { + "description" : "Verbatim license or licensing notice text that was discovered.", + "type" : "string" + } + }, + "description" : "An ExtractedLicensingInfo represents a license or licensing notice that was found in the package. Any license text that is recognized as a license may be represented as a License rather than an ExtractedLicensingInfo." + } + }, + "name" : { + "description" : "Identify name of this SpdxElement.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "spdxVersion" : { + "description" : "Provide a reference number that can be used to understand how to parse and interpret the rest of the file. It will enable both future changes to the specification and to support backward compatibility. The version number consists of a major and minor version indicator. The major field will be incremented when incompatible changes between versions are made (one or more sections are created, modified or deleted). The minor field will be incremented when backwards compatible changes are made.", + "type" : "string" + }, + "annotations" : { + "description" : "Provide additional information about an SpdxElement.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "annotationDate" : { + "description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "annotator" : { + "description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.", + "type" : "string" + }, + "annotationType" : { + "description" : "Type of the annotation.", + "type" : "string", + "enum" : [ "OTHER", "REVIEW" ] + } + }, + "description" : "An Annotation is a comment on an SpdxItem by an agent." + } + }, + "describesPackages" : { + "description" : "The describesPackage property relates an SpdxDocument to the package which it describes.", + "type" : "array", + "items" : { + "description" : "SPDX ID for Package. The describesPackage property relates an SpdxDocument to the package which it describes.", + "type" : "string" + } + }, + "dataLicense" : { + "description" : "License expression for dataLicense. Compliance with the SPDX specification includes populating the SPDX fields therein with data related to such fields (\"SPDX-Metadata\"). The SPDX specification contains numerous fields where an SPDX document creator may provide relevant explanatory text in SPDX-Metadata. Without opining on the lawfulness of \"database rights\" (in jurisdictions where applicable), such explanatory text is copyrightable subject matter in most Berne Convention countries. By using the SPDX specification, or any portion hereof, you hereby agree that any copyright rights (as determined by your jurisdiction) in any SPDX-Metadata, including without limitation explanatory text, shall be subject to the terms of the Creative Commons CC0 1.0 Universal license. For SPDX-Metadata not containing any copyright rights, you hereby agree and acknowledge that the SPDX-Metadata is provided to you \"as-is\" and without any representations or warranties of any kind concerning the SPDX-Metadata, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non-infringement, or the absence of latent or other defects, accuracy, or the presence or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law.", + "type" : "string" + }, + "externalDocumentRefs" : { + "description" : "Identify any external SPDX documents referenced within this SPDX document.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "externalDocumentId" : { + "description" : "externalDocumentId is a string containing letters, numbers, ., - and/or + which uniquely identifies an external document within this document.", + "type" : "string" + }, + "checksum" : { + "type" : "object", + "properties" : { + "algorithm" : { + "description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.", + "type" : "string", + "enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ] + }, + "checksumValue" : { + "description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.", + "type" : "string" + } + }, + "description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented." + }, + "spdxDocument" : { + "description" : "SPDX ID for SpdxDocument. A propoerty containing an SPDX document.", + "type" : "string" + } + }, + "description" : "Information about an external SPDX document reference including the checksum. This allows for verification of the external references." + } + }, + "creationInfo" : { + "type" : "object", + "properties" : { + "comment" : { + "type" : "string" + }, + "created" : { + "description" : "Identify when the SPDX file was originally created. The date is to be specified according to combined date and time in UTC format as specified in ISO 8601 standard. This field is distinct from the fields in section 8, which involves the addition of information during a subsequent review.", + "type" : "string" + }, + "creators" : { + "description" : "Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization, indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.", + "type" : "array", + "items" : { + "description" : "Identify who (or what, in the case of a tool) created the SPDX file. If the SPDX file was created by an individual, indicate the person's name. If the SPDX file was created on behalf of a company or organization, indicate the entity name. If the SPDX file was created using a software tool, indicate the name and version for that tool. If multiple participants or tools were involved, use multiple instances of this field. Person name or organization name may be designated as “anonymous” if appropriate.", + "type" : "string" + }, + "minItems" : 1 + }, + "licenseListVersion" : { + "description" : "An optional field for creators of the SPDX file to provide the version of the SPDX License List used when the SPDX file was created.", + "type" : "string" + } + }, + "description" : "One instance is required for each SPDX file produced. It provides the necessary information for forward and backward compatibility for processing tools." + }, + "packages" : { + "description" : "Packages referenced in the SPDX document", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "attributionTexts" : { + "description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.", + "type" : "array", + "items" : { + "description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.", + "type" : "string" + } + }, + "annotations" : { + "description" : "Provide additional information about an SpdxElement.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "annotationDate" : { + "description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "annotator" : { + "description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.", + "type" : "string" + }, + "annotationType" : { + "description" : "Type of the annotation.", + "type" : "string", + "enum" : [ "OTHER", "REVIEW" ] + } + }, + "description" : "An Annotation is a comment on an SpdxItem by an agent." + } + }, + "supplier" : { + "description" : "The name and, optionally, contact information of the person or organization who was the immediate supplier of this package to the recipient. The supplier may be different than originator when the software has been repackaged. Values of this property must conform to the agent and tool syntax.", + "type" : "string" + }, + "homepage" : { + "type" : "string" + }, + "packageVerificationCode" : { + "type" : "object", + "properties" : { + "packageVerificationCodeValue" : { + "description" : "The actual package verification code as a hex encoded value.", + "type" : "string" + }, + "packageVerificationCodeExcludedFiles" : { + "description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.", + "type" : "array", + "items" : { + "description" : "A file that was excluded when calculating the package verification code. This is usually a file containing SPDX data regarding the package. If a package contains more than one SPDX file all SPDX files must be excluded from the package verification code. If this is not done it would be impossible to correctly calculate the verification codes in both files.", + "type" : "string" + } + } + }, + "description" : "A manifest based verification code (the algorithm is defined in section 4.7 of the full specification) of the SPDX Item. This allows consumers of this data and/or database to determine if an SPDX item they have in hand is identical to the SPDX item from which the data was produced. This algorithm works even if the SPDX document is included in the SPDX item." + }, + "checksums" : { + "description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "algorithm" : { + "description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.", + "type" : "string", + "enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ] + }, + "checksumValue" : { + "description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.", + "type" : "string" + } + }, + "description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented." + } + }, + "downloadLocation" : { + "description" : "The URI at which this package is available for download. Private (i.e., not publicly reachable) URIs are acceptable as values of this property. The values http://spdx.org/rdf/terms#none and http://spdx.org/rdf/terms#noassertion may be used to specify that the package is not downloadable or that no attempt was made to determine its download location, respectively.", + "type" : "string" + }, + "filesAnalyzed" : { + "description" : "Indicates whether the file content of this package has been available for or subjected to analysis when creating the SPDX document. If false indicates packages that represent metadata or URI references to a project, product, artifact, distribution or a component. If set to false, the package must not contain any files.", + "type" : "boolean" + }, + "externalRefs" : { + "description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "comment" : { + "type" : "string" + }, + "referenceCategory" : { + "description" : "Category for the external reference", + "type" : "string", + "enum" : [ "OTHER", "SECURITY", "PACKAGE_MANAGER" ] + }, + "referenceLocator" : { + "description" : "The unique string with no spaces necessary to access the package-specific information, metadata, or content within the target location. The format of the locator is subject to constraints defined by the .", + "type" : "string" + }, + "referenceType" : { + "description" : "Type of the external reference. These are definined in an appendix in the SPDX specification.", + "type" : "string" + } + }, + "description" : "An External Reference allows a Package to reference an external source of additional information, metadata, enumerations, asset identifiers, or downloadable content believed to be relevant to the Package." + } + }, + "licenseComments" : { + "description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.", + "type" : "string" + }, + "name" : { + "description" : "Identify name of this SpdxElement.", + "type" : "string" + }, + "hasFiles" : { + "description" : "Indicates that a particular file belongs to a package.", + "type" : "array", + "items" : { + "description" : "SPDX ID for File. Indicates that a particular file belongs to a package.", + "type" : "string" + } + }, + "comment" : { + "type" : "string" + }, + "summary" : { + "description" : "Provides a short description of the package.", + "type" : "string" + }, + "copyrightText" : { + "description" : "The text of copyright declarations recited in the Package or File.", + "type" : "string" + }, + "originator" : { + "description" : "The name and, optionally, contact information of the person or organization that originally created the package. Values of this property must conform to the agent and tool syntax.", + "type" : "string" + }, + "packageFileName" : { + "description" : "The base name of the package file name. For example, zlib-1.2.5.tar.gz.", + "type" : "string" + }, + "licenseInfoFromFiles" : { + "description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.", + "type" : "array", + "items" : { + "description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.", + "type" : "string" + } + }, + "versionInfo" : { + "description" : "Provides an indication of the version of the package that is described by this SpdxDocument.", + "type" : "string" + }, + "sourceInfo" : { + "description" : "Allows the producer(s) of the SPDX document to describe how the package was acquired and/or changed from the original source.", + "type" : "string" + }, + "description" : { + "description" : "Provides a detailed description of the package.", + "type" : "string" + } + } + } + }, + "files" : { + "description" : "Files referenced in the SPDX document", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "fileTypes" : { + "description" : "The type of the file.", + "type" : "array", + "items" : { + "description" : "The type of the file.", + "type" : "string", + "enum" : [ "OTHER", "DOCUMENTATION", "IMAGE", "VIDEO", "ARCHIVE", "SPDX", "APPLICATION", "SOURCE", "BINARY", "TEXT", "AUDIO" ] + } + }, + "attributionTexts" : { + "description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.", + "type" : "array", + "items" : { + "description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.", + "type" : "string" + } + }, + "annotations" : { + "description" : "Provide additional information about an SpdxElement.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "annotationDate" : { + "description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "annotator" : { + "description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.", + "type" : "string" + }, + "annotationType" : { + "description" : "Type of the annotation.", + "type" : "string", + "enum" : [ "OTHER", "REVIEW" ] + } + }, + "description" : "An Annotation is a comment on an SpdxItem by an agent." + } + }, + "checksums" : { + "description" : "The checksum property provides a mechanism that can be used to verify that the contents of a File or Package have not changed.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "algorithm" : { + "description" : "Identifies the algorithm used to produce the subject Checksum. Currently, SHA-1 is the only supported algorithm. It is anticipated that other algorithms will be supported at a later time.", + "type" : "string", + "enum" : [ "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" ] + }, + "checksumValue" : { + "description" : "The checksumValue property provides a lower case hexidecimal encoded digest value produced using a specific algorithm.", + "type" : "string" + } + }, + "description" : "A Checksum is value that allows the contents of a file to be authenticated. Even small changes to the content of the file will change its checksum. This class allows the results of a variety of checksum and cryptographic message digest algorithms to be represented." + }, + "minItems" : 1 + }, + "noticeText" : { + "description" : "This field provides a place for the SPDX file creator to record potential legal notices found in the file. This may or may not include copyright statements.", + "type" : "string" + }, + "artifactOfs" : { + "description" : "Indicates the project in which the SpdxElement originated. Tools must preserve doap:homepage and doap:name properties and the URI (if one is known) of doap:Project resources that are values of this property. All other properties of doap:Projects are not directly supported by SPDX and may be dropped when translating to or from some SPDX formats.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { } + } + }, + "licenseComments" : { + "description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.", + "type" : "string" + }, + "fileName" : { + "description" : "The name of the file relative to the root of the package.", + "type" : "string" + }, + "name" : { + "description" : "Identify name of this SpdxElement.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "copyrightText" : { + "description" : "The text of copyright declarations recited in the Package or File.", + "type" : "string" + }, + "fileContributors" : { + "description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.", + "type" : "array", + "items" : { + "description" : "This field provides a place for the SPDX file creator to record file contributors. Contributors could include names of copyright holders and/or authors who may not be copyright holders yet contributed to the file content.", + "type" : "string" + } + }, + "licenseInfoInFiles" : { + "description" : "Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.", + "type" : "array", + "items" : { + "description" : "License expression for licenseInfoInFile. Licensing information that was discovered directly in the subject file. This is also considered a declared license for the file.", + "type" : "string" + }, + "minItems" : 1 + }, + "licenseInfoFromFiles" : { + "description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.", + "type" : "array", + "items" : { + "description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.", + "type" : "string" + } + }, + "fileDependencies" : { + "type" : "array", + "items" : { + "description" : "SPDX ID for File", + "type" : "string" + } + } + } + } + }, + "snippets" : { + "description" : "Snippets referenced in the SPDX document", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "ranges" : { + "description" : "This field defines the byte range in the original host file (in X.2) that the snippet information applies to", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "startPointer" : { + "type" : "object", + "properties" : { + "reference" : { + "description" : "SPDX ID for File", + "type" : "string" + } + } + }, + "endPointer" : { + "type" : "object", + "properties" : { + "reference" : { + "description" : "SPDX ID for File", + "type" : "string" + } + } + } + } + }, + "minItems" : 1 + }, + "licenseComments" : { + "description" : "The licenseComments property allows the preparer of the SPDX document to describe why the licensing in spdx:licenseConcluded was chosen.", + "type" : "string" + }, + "attributionTexts" : { + "description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.", + "type" : "array", + "items" : { + "description" : "This field provides a place for the SPDX data creator to record acknowledgements that may be required to be communicated in some contexts. This is not meant to include theactual complete license text (see licenseConculded and licenseDeclared), and may or may not include copyright notices (see also copyrightText). The SPDX data creator may use this field to record other acknowledgements, such as particular clauses from license texts, which may be necessary or desirable to reproduce.", + "type" : "string" + } + }, + "name" : { + "description" : "Identify name of this SpdxElement.", + "type" : "string" + }, + "snippetFromFile" : { + "description" : "SPDX ID for File. File containing the SPDX element (e.g. the file contaning a snippet).", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "copyrightText" : { + "description" : "The text of copyright declarations recited in the Package or File.", + "type" : "string" + }, + "licenseInfoInSnippets" : { + "description" : "Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.", + "type" : "array", + "items" : { + "description" : "License expression for licenseInfoInSnippet. Licensing information that was discovered directly in the subject snippet. This is also considered a declared license for the snippet.", + "type" : "string" + } + }, + "annotations" : { + "description" : "Provide additional information about an SpdxElement.", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "annotationDate" : { + "description" : "Identify when the comment was made. This is to be specified according to the combined date and time in the UTC format, as specified in the ISO 8601 standard.", + "type" : "string" + }, + "comment" : { + "type" : "string" + }, + "annotator" : { + "description" : "This field identifies the person, organization or tool that has commented on a file, package, or the entire document.", + "type" : "string" + }, + "annotationType" : { + "description" : "Type of the annotation.", + "type" : "string", + "enum" : [ "OTHER", "REVIEW" ] + } + }, + "description" : "An Annotation is a comment on an SpdxItem by an agent." + } + }, + "licenseInfoFromFiles" : { + "description" : "The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.", + "type" : "array", + "items" : { + "description" : "License expression for licenseInfoFromFiles. The licensing information that was discovered directly within the package. There will be an instance of this property for each distinct value of alllicenseInfoInFile properties of all files contained in the package.", + "type" : "string" + } + } + } + } + }, + "relationships" : { + "description" : "Relationships referenced in the SPDX document", + "type" : "array", + "items" : { + "type" : "object", + "properties" : { + "comment" : { + "type" : "string" + }, + "relationshipType" : { + "description" : "Describes the type of relationship between two SPDX elements.", + "type" : "string", + "enum" : [ "VARIANT_OF", "COPY_OF", "PATCH_FOR", "TEST_DEPENDENCY_OF", "CONTAINED_BY", "DATA_FILE_OF", "OPTIONAL_COMPONENT_OF", "ANCESTOR_OF", "GENERATES", "CONTAINS", "OPTIONAL_DEPENDENCY_OF", "FILE_ADDED", "DEV_DEPENDENCY_OF", "DEPENDENCY_OF", "BUILD_DEPENDENCY_OF", "DESCRIBES", "PREREQUISITE_FOR", "HAS_PREREQUISITE", "PROVIDED_DEPENDENCY_OF", "DYNAMIC_LINK", "DESCRIBED_BY", "METAFILE_OF", "DEPENDENCY_MANIFEST_OF", "PATCH_APPLIED", "RUNTIME_DEPENDENCY_OF", "TEST_OF", "TEST_TOOL_OF", "DEPENDS_ON", "FILE_MODIFIED", "DISTRIBUTION_ARTIFACT", "DOCUMENTATION_OF", "GENERATED_FROM", "STATIC_LINK", "OTHER", "BUILD_TOOL_OF", "TEST_CASE_OF", "PACKAGE_OF", "DESCENDANT_OF", "FILE_DELETED", "EXPANDED_FROM_ARCHIVE", "DEV_TOOL_OF", "EXAMPLE_OF" ] + }, + "relatedSpdxElement" : { + "description" : "SPDX ID for SpdxElement. A related SpdxElement.", + "type" : "string" + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/syft/pkg/ownership_by_files_relationship.go b/syft/pkg/ownership_by_files_relationship.go index b1408b63e3b..4e2b4d3148b 100644 --- a/syft/pkg/ownership_by_files_relationship.go +++ b/syft/pkg/ownership_by_files_relationship.go @@ -59,7 +59,7 @@ func findOwnershipByFilesRelationships(catalog *Catalog) map[ID]map[ID]*strset.S if !ok { continue } - for _, ownedFilePath := range pkgFileOwner.ownedFiles() { + for _, ownedFilePath := range pkgFileOwner.OwnedFiles() { if matchesAny(ownedFilePath, globsForbiddenFromBeingOwned) { // we skip over known exceptions to file ownership, such as the RPM package owning // the RPM DB path, otherwise the RPM package would "own" all RPMs, which is not intended diff --git a/syft/presenter/packages/presenter.go b/syft/presenter/packages/presenter.go index 7c5f08358a5..6d0d594efd4 100644 --- a/syft/presenter/packages/presenter.go +++ b/syft/presenter/packages/presenter.go @@ -20,8 +20,10 @@ func Presenter(option PresenterOption, config PresenterConfig) presenter.Present return packages.NewTablePresenter(config.Catalog) case CycloneDxPresenterOption: return packages.NewCycloneDxPresenter(config.Catalog, config.SourceMetadata) - case SPDXPresenterOption: - return packages.NewSPDXPresenter(config.Catalog, config.SourceMetadata) + case SPDXTagValuePresenterOption: + return packages.NewSPDXTagValuePresenter(config.Catalog, config.SourceMetadata) + case SPDXJSONPresenterOption: + return packages.NewSPDXJSONPresenter(config.Catalog, config.SourceMetadata) default: return nil } diff --git a/syft/presenter/packages/presenter_option.go b/syft/presenter/packages/presenter_option.go index 4e703dde761..ee3603853c3 100644 --- a/syft/presenter/packages/presenter_option.go +++ b/syft/presenter/packages/presenter_option.go @@ -3,12 +3,13 @@ package packages import "strings" const ( - UnknownPresenterOption PresenterOption = "UnknownPresenterOption" - JSONPresenterOption PresenterOption = "json" - TextPresenterOption PresenterOption = "text" - TablePresenterOption PresenterOption = "table" - CycloneDxPresenterOption PresenterOption = "cyclonedx" - SPDXPresenterOption PresenterOption = "spdx" + UnknownPresenterOption PresenterOption = "UnknownPresenterOption" + JSONPresenterOption PresenterOption = "json" + TextPresenterOption PresenterOption = "text" + TablePresenterOption PresenterOption = "table" + CycloneDxPresenterOption PresenterOption = "cyclonedx" + SPDXTagValuePresenterOption PresenterOption = "spdx-tag-value" + SPDXJSONPresenterOption PresenterOption = "spdx-json" ) var AllPresenters = []PresenterOption{ @@ -16,7 +17,8 @@ var AllPresenters = []PresenterOption{ TextPresenterOption, TablePresenterOption, CycloneDxPresenterOption, - SPDXPresenterOption, + SPDXTagValuePresenterOption, + SPDXJSONPresenterOption, } type PresenterOption string @@ -31,8 +33,10 @@ func ParsePresenterOption(userStr string) PresenterOption { return TablePresenterOption case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx": return CycloneDxPresenterOption - case string(SPDXPresenterOption): - return SPDXPresenterOption + case string(SPDXTagValuePresenterOption), "spdx-tagvalue", "spdxtagvalue", "spdx-tv": + return SPDXTagValuePresenterOption + case string(SPDXJSONPresenterOption), "spdxjson": + return SPDXJSONPresenterOption default: return UnknownPresenterOption } diff --git a/test/cli/json_schema_test.go b/test/cli/json_schema_test.go index 6691236393a..19c9fa93e1d 100644 --- a/test/cli/json_schema_test.go +++ b/test/cli/json_schema_test.go @@ -60,18 +60,18 @@ func TestJSONSchema(t *testing.T) { args = append(args, a) } - _, stdout, _ := runSyftCommand(t, nil, args...) + _, stdout, stderr := runSyftCommand(t, nil, args...) if len(strings.Trim(stdout, "\n ")) < 100 { - t.Fatalf("bad syft output: %q", stdout) + t.Fatalf("bad syft run:\noutput: %q\n:error: %q", stdout, stderr) } - validateAgainstV1Schema(t, stdout) + validateJsonAgainstSchema(t, stdout) }) } } -func validateAgainstV1Schema(t testing.TB, json string) { +func validateJsonAgainstSchema(t testing.TB, json string) { fullSchemaPath := path.Join(repoRoot(t), jsonSchemaPath, fmt.Sprintf("schema-%s.json", internal.JSONSchemaVersion)) schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", fullSchemaPath)) documentLoader := gojsonschema.NewStringLoader(json) diff --git a/test/cli/spdx_json_schema_test.go b/test/cli/spdx_json_schema_test.go new file mode 100644 index 00000000000..468508def7b --- /dev/null +++ b/test/cli/spdx_json_schema_test.go @@ -0,0 +1,90 @@ +package cli + +import ( + "fmt" + "path" + "strings" + "testing" + + "github.com/anchore/stereoscope/pkg/imagetest" + "github.com/xeipuuv/gojsonschema" +) + +// this is the path to the json schema directory relative to the root of the repo +const spdxJsonSchemaPath = "schema/spdx-json" + +func TestSPDXJSONSchema(t *testing.T) { + + imageFixture := func(t *testing.T) string { + fixtureImageName := "image-pkg-coverage" + imagetest.GetFixtureImage(t, "docker-archive", fixtureImageName) + tarPath := imagetest.GetFixtureImageTarPath(t, fixtureImageName) + return "docker-archive:" + tarPath + } + + tests := []struct { + name string + subcommand string + args []string + fixture func(*testing.T) string + }{ + { + name: "packages:image:docker-archive:pkg-coverage", + subcommand: "packages", + args: []string{"-o", "spdx-json"}, + fixture: imageFixture, + }, + { + name: "power-user:image:docker-archive:pkg-coverage", + subcommand: "power-user", + fixture: imageFixture, + }, + { + name: "packages:dir:pkg-coverage", + subcommand: "packages", + args: []string{"-o", "spdx-json"}, + fixture: func(t *testing.T) string { + return "dir:test-fixtures/image-pkg-coverage" + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fixtureRef := test.fixture(t) + args := []string{ + test.subcommand, fixtureRef, "-q", + } + for _, a := range test.args { + args = append(args, a) + } + + _, stdout, _ := runSyftCommand(t, nil, args...) + + if len(strings.Trim(stdout, "\n ")) < 100 { + t.Fatalf("bad syft output: %q", stdout) + } + + validateSpdxJsonAgainstSchema(t, stdout) + }) + } +} + +func validateSpdxJsonAgainstSchema(t testing.TB, json string) { + fullSchemaPath := path.Join(repoRoot(t), spdxJsonSchemaPath, fmt.Sprintf("spdx-schema-2.2.json")) + schemaLoader := gojsonschema.NewReferenceLoader(fmt.Sprintf("file://%s", fullSchemaPath)) + documentLoader := gojsonschema.NewStringLoader(json) + + result, err := gojsonschema.Validate(schemaLoader, documentLoader) + if err != nil { + t.Fatal("unable to validate json schema:", err.Error()) + } + + if !result.Valid() { + t.Errorf("failed json schema validation:") + t.Errorf("JSON:\n%s\n", json) + for _, desc := range result.Errors() { + t.Errorf(" - %s\n", desc) + } + } +} diff --git a/test/cli/utils_test.go b/test/cli/utils_test.go index a12525d47e7..58737303b48 100644 --- a/test/cli/utils_test.go +++ b/test/cli/utils_test.go @@ -5,9 +5,7 @@ import ( "fmt" "os" "os/exec" - "path" "path/filepath" - "runtime" "strings" "testing" @@ -49,16 +47,22 @@ func getSyftCommand(t testing.TB, args ...string) *exec.Cmd { binaryLocation = os.Getenv("SYFT_BINARY_LOCATION") } else { // note: there is a subtle - vs _ difference between these versions - switch runtime.GOOS { - case "darwin": - binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft-macos_darwin_%s/syft", runtime.GOARCH)) - case "linux": - binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft_linux_%s/syft", runtime.GOARCH)) - default: - t.Fatalf("unsupported OS: %s", runtime.GOOS) - } + //switch runtime.GOOS { + //case "darwin": + // binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft-macos_darwin_%s/syft", runtime.GOARCH)) + //case "linux": + // binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft_linux_%s/syft", runtime.GOARCH)) + //default: + // t.Fatalf("unsupported OS: %s", runtime.GOOS) + //} + // TMP TMP TMP + binaryLocation = "/Users/wagoodman/.gimme/versions/go1.15.2.darwin.amd64/bin/go" + args = append([]string{"run", "../../main.go"}, args...) } + + //t.Log(binaryLocation, args) + return exec.Command(binaryLocation, args...) } From 421641f22b82259deb0053621b02e57ea3896efc Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 7 Jun 2021 17:20:53 -0400 Subject: [PATCH 04/13] add remaining package fields Signed-off-by: Alex Goodman --- .../packages/model/spdx_2_2/spdx_2_2.go | 36 +-- .../presenter/packages/spdx_json_presenter.go | 211 ++++++++++++------ 2 files changed, 167 insertions(+), 80 deletions(-) diff --git a/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go b/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go index bda4780df5e..74fde53774f 100644 --- a/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go +++ b/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go @@ -19,7 +19,7 @@ func (e ElementID) String() string { // DocElementID represents an SPDX element identifier that could be defined // in a different SPDX document, and therefore could have a "DocumentRef-" -// portion, such as Relationships and Annotations. +// 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). @@ -46,12 +46,12 @@ func (d DocElementID) String() string { type Element struct { SPDXID string `json:"SPDXID"` // Provide additional information about an SpdxElement. - Annotations []Annotation `json:"annotations"` - Comment string `json:"comment"` + Annotations []Annotation `json:"annotations,omitempty"` + Comment string `json:"comment,omitempty"` // Identify name of this SpdxElement. Name string `json:"name"` // Relationships referenced in the SPDX document - Relationships []Relationships `json:"relationships"` + Relationships []Relationship `json:"relationships,omitempty"` } type Document struct { @@ -79,13 +79,15 @@ type Document struct { ExternalDocumentRefs []ExternalDocumentRef `json:"externalDocumentRefs,omitempty"` // Indicates that a particular ExtractedLicensingInfo was defined in the subject SpdxDocument. HasExtractedLicensingInfos []HasExtractedLicensingInfo `json:"hasExtractedLicensingInfos,omitempty"` - DocumentNamespace string `json:"documentNamespace"` - DocumentDescribes []string `json:"documentDescribes"` - Packages []Package `json:"packages"` + // 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 []Files `json:"files"` + Files []File `json:"files,omitempty"` // Snippets referenced in the SPDX document - Snippets []Snippets `json:"snippets"` + Snippets []Snippet `json:"snippets,omitempty"` Element } @@ -111,7 +113,7 @@ type Item struct { } type CreationInfo struct { - Comment string `json:"comment"` + 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. @@ -123,7 +125,7 @@ type CreationInfo struct { // 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"` + LicenseListVersion string `json:"licenseListVersion,omitempty"` } type Checksum struct { // Identifies the algorithm used to produce the subject Checksum. One of: "SHA256", "SHA1", "SHA384", "MD2", "MD4", "SHA512", "MD6", "MD5", "SHA224" @@ -241,7 +243,7 @@ type Package struct { // 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"` + 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"` @@ -251,7 +253,7 @@ type Package struct { 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"` - Homepage string `json:"homepage"` + Homepage string `json:"homepage,omitempty"` 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. @@ -262,7 +264,7 @@ type Package struct { // 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"` + 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. @@ -292,7 +294,7 @@ const ( OtherFileType FileType = "OTHER" ) -type Files struct { +type File struct { // (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"` @@ -335,7 +337,7 @@ type Range struct { EndPointer EndPointer `json:"endPointer"` } -type Snippets struct { +type Snippet struct { // 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"` @@ -346,7 +348,7 @@ type Snippets struct { Ranges []Range `json:"ranges"` Item } -type Relationships struct { +type Relationship struct { // SPDX ID for SpdxElement. A related SpdxElement. RelatedSpdxElement string `json:"relatedSpdxElement"` // Describes the type of relationship between two SPDX elements. diff --git a/internal/presenter/packages/spdx_json_presenter.go b/internal/presenter/packages/spdx_json_presenter.go index 4db6c5bad7b..24245414fe7 100644 --- a/internal/presenter/packages/spdx_json_presenter.go +++ b/internal/presenter/packages/spdx_json_presenter.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "io" + "strings" "time" spdxLicense "github.com/mitchellh/go-spdx" @@ -57,12 +58,10 @@ func newSpdxJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx DataLicense: "CC0-1.0", ExternalDocumentRefs: nil, HasExtractedLicensingInfos: nil, - // TODO: rethink this - DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput), - DocumentDescribes: nil, - Packages: newSpdxJsonPackages(catalog), - Files: nil, - Snippets: nil, + DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput), + Packages: newSpdxJsonPackages(catalog), + Files: nil, + Snippets: nil, Element: spdx_2_2.Element{ // should this be unique to the user's input? or otherwise just say document? SPDXID: spdx_2_2.ElementID("Document").String(), @@ -77,74 +76,160 @@ func newSpdxJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx func newSpdxJsonPackages(catalog *pkg.Catalog) []spdx_2_2.Package { results := make([]spdx_2_2.Package, 0) for _, p := range catalog.Sorted() { - license := "NONE" - if len(p.Licenses) > 0 { - // note: we are not supporting complex expressions at this time, only individual licenses - licenseInfo, err := spdxLicense.License(p.Licenses[0]) - if err != nil { - log.Warnf("unable to parse SPDX license for package=%+v : %+v", p, err) - license = "NOASSERTION" - } else { - license = licenseInfo.ID - } - } - - externalRefs := make([]spdx_2_2.ExternalRef, 0) - for _, c := range p.CPEs { - externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ - Comment: "", - ReferenceCategory: spdx_2_2.SecurityReferenceCategory, - ReferenceLocator: c.BindToFmtString(), - ReferenceType: spdx_2_2.Cpe23ExternalRefType, - }) - } - - if p.PURL != "" { - externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ - Comment: "", - ReferenceCategory: spdx_2_2.PackageManagerReferenceCategory, - ReferenceLocator: p.PURL, - ReferenceType: spdx_2_2.PurlExternalRefType, - }) - } + license := getLicense(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, spdx_2_2.Package{ - Checksums: nil, - Description: "", - DownloadLocation: "", - ExternalRefs: externalRefs, + Description: getDescription(p), + DownloadLocation: getDownloadLocation(p), + ExternalRefs: getExternalRefs(p), FilesAnalyzed: false, - HasFiles: nil, - Homepage: "", - // The Declared License is what the authors of a project believe govern the package - LicenseDeclared: license, - Originator: "", - PackageFileName: "", - PackageVerificationCode: spdx_2_2.PackageVerificationCode{}, - SourceInfo: "", - Summary: "", - Supplier: "", - VersionInfo: p.Version, + Homepage: getHomepage(p), + LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package + Originator: getOriginator(p), + SourceInfo: getSourceInfo(p), + VersionInfo: p.Version, Item: spdx_2_2.Item{ - LicenseComments: "", - // The Concluded License field is the license the SPDX file creator believes governs the package - LicenseConcluded: license, - LicenseInfoFromFiles: nil, - LicenseInfoInFiles: nil, - CopyrightText: "", - AttributionTexts: nil, + LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package Element: spdx_2_2.Element{ - SPDXID: spdx_2_2.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(), - Annotations: nil, - Comment: "", - Name: p.Name, - Relationships: nil, + SPDXID: spdx_2_2.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(), + Name: p.Name, }, }, }) } return results } + +func getExternalRefs(p *pkg.Package) (externalRefs []spdx_2_2.ExternalRef) { + externalRefs = make([]spdx_2_2.ExternalRef, 0) + for _, c := range p.CPEs { + externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ + Comment: "", + ReferenceCategory: spdx_2_2.SecurityReferenceCategory, + ReferenceLocator: c.BindToFmtString(), + ReferenceType: spdx_2_2.Cpe23ExternalRefType, + }) + } + + if p.PURL != "" { + externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ + Comment: "", + ReferenceCategory: spdx_2_2.PackageManagerReferenceCategory, + ReferenceLocator: p.PURL, + ReferenceType: spdx_2_2.PurlExternalRefType, + }) + } + return externalRefs +} + +func getLicense(p *pkg.Package) string { + license := "NONE" + if len(p.Licenses) > 0 { + // note: we are not supporting complex expressions at this time, only individual licenses + licenseInfo, err := spdxLicense.License(p.Licenses[0]) + if err != nil { + log.Warnf("unable to parse SPDX license for package=%+v : %+v", p, err) + license = "NOASSERTION" + } else { + license = licenseInfo.ID + } + } + return license +} + +func getDownloadLocation(p *pkg.Package) string { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + return metadata.URL + case pkg.NpmPackageJSONMetadata: + return metadata.URL + default: + return "" + } +} + +func getHomepage(p *pkg.Package) string { + switch metadata := p.Metadata.(type) { + case pkg.GemMetadata: + return metadata.Homepage + case pkg.NpmPackageJSONMetadata: + return metadata.Homepage + default: + return "" + } +} + +func getSourceInfo(p *pkg.Package) string { + answer := "" + switch p.Type { + case pkg.RpmPkg: + answer = "acquired package info RPM DB" + case pkg.ApkPkg: + answer = "acquired package info APK DB" + case pkg.DebPkg: + answer = "acquired package info 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 metadata file" + case pkg.RustPkg: + answer = "acquired package info from rust cargo manifest" + default: + answer = "determine from the following paths" + } + var paths []string + for _, l := range p.Locations { + paths = append(paths, l.RealPath) + } + + return answer + ": " + strings.Join(paths, ", ") +} + +func getOriginator(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 getDescription(p *pkg.Package) string { + switch metadata := p.Metadata.(type) { + case pkg.ApkMetadata: + return metadata.Description + case pkg.NpmPackageJSONMetadata: + return metadata.Description + default: + return "" + } +} From 5269c47c422468364870f5a6e9e5fe6a30f119eb Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 7 Jun 2021 22:55:09 -0400 Subject: [PATCH 05/13] add spdx license list generation + tests Signed-off-by: Alex Goodman --- go.mod | 1 - go.sum | 4 - .../packages/model/spdx22/annotation.go | 21 + .../packages/model/spdx22/checksum.go | 7 + .../packages/model/spdx22/creation_info.go | 19 + .../packages/model/spdx22/document.go | 43 ++ .../packages/model/spdx22/element.go | 12 + .../packages/model/spdx22/element_id.go | 37 ++ .../model/spdx22/external_document_ref.go | 9 + .../packages/model/spdx22/external_ref.go | 43 ++ .../presenter/packages/model/spdx22/file.go | 41 ++ .../spdx22/has_extracted_licensing_info.go | 14 + .../presenter/packages/model/spdx22/item.go | 22 + .../packages/model/spdx22/package.go | 46 ++ .../model/spdx22/package_verification_code.go | 23 + .../packages/model/spdx22/relationship.go | 181 ++++++ .../packages/model/spdx22/snippet.go | 32 ++ .../packages/model/spdx22/version.go | 3 + .../packages/model/spdx_2_2/spdx_2_2.go | 529 ------------------ internal/presenter/packages/spdx_helpers.go | 170 ++++++ .../presenter/packages/spdx_json_presenter.go | 203 +------ .../packages/spdx_tag_value_presenter.go | 133 +---- internal/spdxlicense/generate_license_list.go | 92 +++ internal/spdxlicense/license.go | 12 + internal/spdxlicense/license_list.go | 469 ++++++++++++++++ internal/spdxlicense/license_list_test.go | 14 + syft/pkg/cataloger/deb/cataloger_test.go | 2 +- syft/pkg/cataloger/deb/parse_copyright.go | 50 +- .../pkg/cataloger/deb/parse_copyright_test.go | 11 +- .../deb/test-fixtures/copyright/libc6 | 509 +++++++++++++++++ test/integration/license_list_test.go | 36 ++ 31 files changed, 1941 insertions(+), 847 deletions(-) create mode 100644 internal/presenter/packages/model/spdx22/annotation.go create mode 100644 internal/presenter/packages/model/spdx22/checksum.go create mode 100644 internal/presenter/packages/model/spdx22/creation_info.go create mode 100644 internal/presenter/packages/model/spdx22/document.go create mode 100644 internal/presenter/packages/model/spdx22/element.go create mode 100644 internal/presenter/packages/model/spdx22/element_id.go create mode 100644 internal/presenter/packages/model/spdx22/external_document_ref.go create mode 100644 internal/presenter/packages/model/spdx22/external_ref.go create mode 100644 internal/presenter/packages/model/spdx22/file.go create mode 100644 internal/presenter/packages/model/spdx22/has_extracted_licensing_info.go create mode 100644 internal/presenter/packages/model/spdx22/item.go create mode 100644 internal/presenter/packages/model/spdx22/package.go create mode 100644 internal/presenter/packages/model/spdx22/package_verification_code.go create mode 100644 internal/presenter/packages/model/spdx22/relationship.go create mode 100644 internal/presenter/packages/model/spdx22/snippet.go create mode 100644 internal/presenter/packages/model/spdx22/version.go delete mode 100644 internal/presenter/packages/model/spdx_2_2/spdx_2_2.go create mode 100644 internal/presenter/packages/spdx_helpers.go create mode 100644 internal/spdxlicense/generate_license_list.go create mode 100644 internal/spdxlicense/license.go create mode 100644 internal/spdxlicense/license_list.go create mode 100644 internal/spdxlicense/license_list_test.go create mode 100644 syft/pkg/cataloger/deb/test-fixtures/copyright/libc6 create mode 100644 test/integration/license_list_test.go diff --git a/go.mod b/go.mod index 535800de92d..0d309c3518d 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,6 @@ require ( github.com/hashicorp/go-multierror v1.1.0 github.com/hashicorp/go-version v1.2.0 github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/go-spdx v0.1.0 github.com/mitchellh/mapstructure v1.3.1 github.com/olekukonko/tablewriter v0.0.4 github.com/package-url/packageurl-go v0.1.0 diff --git a/go.sum b/go.sum index 2beb90e0631..d58de937735 100644 --- a/go.sum +++ b/go.sum @@ -409,8 +409,6 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= -github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= -github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-hclog v0.9.2/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= @@ -538,8 +536,6 @@ github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v0.0.0-20190716172923-621e5597135b/go.mod h1:r1VsdOzOPt1ZSrGZWFoNhsAedKnEd6r9Np1+5blZCWk= -github.com/mitchellh/go-spdx v0.1.0 h1:50JnVzkL3kWreQ5Qb4Pi3Qx9e+bbYrt8QglJDpfeBEs= -github.com/mitchellh/go-spdx v0.1.0/go.mod h1:FFi4Cg1fBuN/JCtPtP8PEDmcBjvO3gijQVl28YjIBVQ= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= diff --git a/internal/presenter/packages/model/spdx22/annotation.go b/internal/presenter/packages/model/spdx22/annotation.go new file mode 100644 index 00000000000..048d4c31e0a --- /dev/null +++ b/internal/presenter/packages/model/spdx22/annotation.go @@ -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"` +} diff --git a/internal/presenter/packages/model/spdx22/checksum.go b/internal/presenter/packages/model/spdx22/checksum.go new file mode 100644 index 00000000000..e137343c988 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/checksum.go @@ -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"` +} diff --git a/internal/presenter/packages/model/spdx22/creation_info.go b/internal/presenter/packages/model/spdx22/creation_info.go new file mode 100644 index 00000000000..9e1f51080e1 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/creation_info.go @@ -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"` +} diff --git a/internal/presenter/packages/model/spdx22/document.go b/internal/presenter/packages/model/spdx22/document.go new file mode 100644 index 00000000000..b34758d3f38 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/document.go @@ -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 { + 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"` + Element +} diff --git a/internal/presenter/packages/model/spdx22/element.go b/internal/presenter/packages/model/spdx22/element.go new file mode 100644 index 00000000000..812390ccb63 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/element.go @@ -0,0 +1,12 @@ +package spdx22 + +type Element struct { + SPDXID string `json:"SPDXID"` + // Provide additional information about an SpdxElement. + Annotations []Annotation `json:"annotations,omitempty"` + Comment string `json:"comment,omitempty"` + // Identify name of this SpdxElement. + Name string `json:"name"` + // Relationships referenced in the SPDX document + Relationships []Relationship `json:"relationships,omitempty"` +} diff --git a/internal/presenter/packages/model/spdx22/element_id.go b/internal/presenter/packages/model/spdx22/element_id.go new file mode 100644 index 00000000000..bf2f871f37a --- /dev/null +++ b/internal/presenter/packages/model/spdx22/element_id.go @@ -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() +} diff --git a/internal/presenter/packages/model/spdx22/external_document_ref.go b/internal/presenter/packages/model/spdx22/external_document_ref.go new file mode 100644 index 00000000000..3aa830f5b01 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/external_document_ref.go @@ -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"` +} diff --git a/internal/presenter/packages/model/spdx22/external_ref.go b/internal/presenter/packages/model/spdx22/external_ref.go new file mode 100644 index 00000000000..e34e4f64bc6 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/external_ref.go @@ -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 . + ReferenceLocator string `json:"referenceLocator"` + // Type of the external reference. These are defined in an appendix in the SPDX specification. + ReferenceType ExternalRefType `json:"referenceType"` +} diff --git a/internal/presenter/packages/model/spdx22/file.go b/internal/presenter/packages/model/spdx22/file.go new file mode 100644 index 00000000000..c6ae1bf5dc1 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/file.go @@ -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 { + // (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"` + Item +} diff --git a/internal/presenter/packages/model/spdx22/has_extracted_licensing_info.go b/internal/presenter/packages/model/spdx22/has_extracted_licensing_info.go new file mode 100644 index 00000000000..acf73ab4d15 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/has_extracted_licensing_info.go @@ -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"` +} diff --git a/internal/presenter/packages/model/spdx22/item.go b/internal/presenter/packages/model/spdx22/item.go new file mode 100644 index 00000000000..9f7fc13a5db --- /dev/null +++ b/internal/presenter/packages/model/spdx22/item.go @@ -0,0 +1,22 @@ +package spdx22 + +type Item struct { + // 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"` + Element +} diff --git a/internal/presenter/packages/model/spdx22/package.go b/internal/presenter/packages/model/spdx22/package.go new file mode 100644 index 00000000000..1e74b026b8a --- /dev/null +++ b/internal/presenter/packages/model/spdx22/package.go @@ -0,0 +1,46 @@ +package spdx22 + +type Package struct { + // 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"` + Homepage string `json:"homepage,omitempty"` + 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"` + Item +} diff --git a/internal/presenter/packages/model/spdx22/package_verification_code.go b/internal/presenter/packages/model/spdx22/package_verification_code.go new file mode 100644 index 00000000000..603fcb15820 --- /dev/null +++ b/internal/presenter/packages/model/spdx22/package_verification_code.go @@ -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"` +} diff --git a/internal/presenter/packages/model/spdx22/relationship.go b/internal/presenter/packages/model/spdx22/relationship.go new file mode 100644 index 00000000000..c1f52204f6c --- /dev/null +++ b/internal/presenter/packages/model/spdx22/relationship.go @@ -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" +) diff --git a/internal/presenter/packages/model/spdx22/snippet.go b/internal/presenter/packages/model/spdx22/snippet.go new file mode 100644 index 00000000000..98e372ae04e --- /dev/null +++ b/internal/presenter/packages/model/spdx22/snippet.go @@ -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 { + // 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"` + Item +} diff --git a/internal/presenter/packages/model/spdx22/version.go b/internal/presenter/packages/model/spdx22/version.go new file mode 100644 index 00000000000..492d3ff1f0f --- /dev/null +++ b/internal/presenter/packages/model/spdx22/version.go @@ -0,0 +1,3 @@ +package spdx22 + +const Version = "SPDX-2.2" diff --git a/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go b/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go deleted file mode 100644 index 74fde53774f..00000000000 --- a/internal/presenter/packages/model/spdx_2_2/spdx_2_2.go +++ /dev/null @@ -1,529 +0,0 @@ -package spdx_2_2 - -import "time" - -// 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 - -// 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() -} - -type Element struct { - SPDXID string `json:"SPDXID"` - // Provide additional information about an SpdxElement. - Annotations []Annotation `json:"annotations,omitempty"` - Comment string `json:"comment,omitempty"` - // Identify name of this SpdxElement. - Name string `json:"name"` - // Relationships referenced in the SPDX document - Relationships []Relationship `json:"relationships,omitempty"` -} - -type Document struct { - 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"` - Element -} - -type Item struct { - // 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"` - Element -} - -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,omitempty"` -} -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"` -} -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"` -} -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"` -} - -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"` -} - -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 . - ReferenceLocator string `json:"referenceLocator"` - // Type of the external reference. These are defined in an appendix in the SPDX specification. - ReferenceType ExternalRefType `json:"referenceType"` -} - -// 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"` -} -type Package struct { - // 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"` - Homepage string `json:"homepage,omitempty"` - 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"` - Item -} - -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 { - // (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"` - Item -} - -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 { - // 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"` - Item -} -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" -) diff --git a/internal/presenter/packages/spdx_helpers.go b/internal/presenter/packages/spdx_helpers.go new file mode 100644 index 00000000000..dda1294d5a6 --- /dev/null +++ b/internal/presenter/packages/spdx_helpers.go @@ -0,0 +1,170 @@ +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{ + Comment: "", + ReferenceCategory: spdx22.SecurityReferenceCategory, + ReferenceLocator: c.BindToFmtString(), + ReferenceType: spdx22.Cpe23ExternalRefType, + }) + } + + if p.PURL != "" { + externalRefs = append(externalRefs, spdx22.ExternalRef{ + Comment: "", + 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 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 metadata file" + 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 "" + } +} diff --git a/internal/presenter/packages/spdx_json_presenter.go b/internal/presenter/packages/spdx_json_presenter.go index 24245414fe7..b4a52a60478 100644 --- a/internal/presenter/packages/spdx_json_presenter.go +++ b/internal/presenter/packages/spdx_json_presenter.go @@ -4,16 +4,12 @@ import ( "encoding/json" "fmt" "io" - "strings" "time" - spdxLicense "github.com/mitchellh/go-spdx" - "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/log" - "github.com/anchore/syft/internal/presenter/packages/model/spdx_2_2" + "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" ) @@ -34,7 +30,7 @@ func NewSPDXJSONPresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) *SP // Present the catalog results to the given writer. func (pres *SPDXJsonPresenter) Present(output io.Writer) error { - doc := newSpdxJsonDocument(pres.catalog, pres.srcMetadata) + doc := newSPDXJsonDocument(pres.catalog, pres.srcMetadata) enc := json.NewEncoder(output) // prevent > and < from being escaped in the payload @@ -43,58 +39,50 @@ func (pres *SPDXJsonPresenter) Present(output io.Writer) error { return enc.Encode(&doc) } -func newSpdxJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx_2_2.Document { - return spdx_2_2.Document{ - SPDXVersion: "SPDX-2.2", - CreationInfo: spdx_2_2.CreationInfo{ - Comment: "", +func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx22.Document { + return spdx22.Document{ + SPDXVersion: spdx22.Version, + CreationInfo: spdx22.CreationInfo{ Created: time.Now().UTC(), Creators: []string{ - "Anchore, Inc", - internal.ApplicationName + "-" + version.FromBuild().Version, + // 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: "", + LicenseListVersion: spdxlicense.Version, }, - DataLicense: "CC0-1.0", - ExternalDocumentRefs: nil, - HasExtractedLicensingInfos: nil, - DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput), - Packages: newSpdxJsonPackages(catalog), - Files: nil, - Snippets: nil, - Element: spdx_2_2.Element{ + DataLicense: "CC0-1.0", + DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput), + Packages: newSPDXJsonPackages(catalog), + Element: spdx22.Element{ // should this be unique to the user's input? or otherwise just say document? - SPDXID: spdx_2_2.ElementID("Document").String(), - Annotations: nil, - Comment: "", - Name: srcMetadata.ImageMetadata.UserInput, - Relationships: nil, + SPDXID: spdx22.ElementID("DOCUMENT").String(), + Name: srcMetadata.ImageMetadata.UserInput, }, } } -func newSpdxJsonPackages(catalog *pkg.Catalog) []spdx_2_2.Package { - results := make([]spdx_2_2.Package, 0) +func newSPDXJsonPackages(catalog *pkg.Catalog) []spdx22.Package { + results := make([]spdx22.Package, 0) for _, p := range catalog.Sorted() { - license := getLicense(p) + 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, spdx_2_2.Package{ - Description: getDescription(p), - DownloadLocation: getDownloadLocation(p), - ExternalRefs: getExternalRefs(p), + results = append(results, spdx22.Package{ + Description: getSPDXDescription(p), + DownloadLocation: getSPDXDownloadLocation(p), + ExternalRefs: getSPDXExternalRefs(p), FilesAnalyzed: false, - Homepage: getHomepage(p), + Homepage: getSPDXHomepage(p), LicenseDeclared: license, // The Declared License is what the authors of a project believe govern the package - Originator: getOriginator(p), - SourceInfo: getSourceInfo(p), + Originator: getSPDXOriginator(p), + SourceInfo: getSPDXSourceInfo(p), VersionInfo: p.Version, - Item: spdx_2_2.Item{ + Item: spdx22.Item{ LicenseConcluded: license, // The Concluded License field is the license the SPDX file creator believes governs the package - Element: spdx_2_2.Element{ - SPDXID: spdx_2_2.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(), + Element: spdx22.Element{ + SPDXID: spdx22.ElementID(fmt.Sprintf("Package-%+v-%s-%s", p.Type, p.Name, p.Version)).String(), Name: p.Name, }, }, @@ -102,134 +90,3 @@ func newSpdxJsonPackages(catalog *pkg.Catalog) []spdx_2_2.Package { } return results } - -func getExternalRefs(p *pkg.Package) (externalRefs []spdx_2_2.ExternalRef) { - externalRefs = make([]spdx_2_2.ExternalRef, 0) - for _, c := range p.CPEs { - externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ - Comment: "", - ReferenceCategory: spdx_2_2.SecurityReferenceCategory, - ReferenceLocator: c.BindToFmtString(), - ReferenceType: spdx_2_2.Cpe23ExternalRefType, - }) - } - - if p.PURL != "" { - externalRefs = append(externalRefs, spdx_2_2.ExternalRef{ - Comment: "", - ReferenceCategory: spdx_2_2.PackageManagerReferenceCategory, - ReferenceLocator: p.PURL, - ReferenceType: spdx_2_2.PurlExternalRefType, - }) - } - return externalRefs -} - -func getLicense(p *pkg.Package) string { - license := "NONE" - if len(p.Licenses) > 0 { - // note: we are not supporting complex expressions at this time, only individual licenses - licenseInfo, err := spdxLicense.License(p.Licenses[0]) - if err != nil { - log.Warnf("unable to parse SPDX license for package=%+v : %+v", p, err) - license = "NOASSERTION" - } else { - license = licenseInfo.ID - } - } - return license -} - -func getDownloadLocation(p *pkg.Package) string { - switch metadata := p.Metadata.(type) { - case pkg.ApkMetadata: - return metadata.URL - case pkg.NpmPackageJSONMetadata: - return metadata.URL - default: - return "" - } -} - -func getHomepage(p *pkg.Package) string { - switch metadata := p.Metadata.(type) { - case pkg.GemMetadata: - return metadata.Homepage - case pkg.NpmPackageJSONMetadata: - return metadata.Homepage - default: - return "" - } -} - -func getSourceInfo(p *pkg.Package) string { - answer := "" - switch p.Type { - case pkg.RpmPkg: - answer = "acquired package info RPM DB" - case pkg.ApkPkg: - answer = "acquired package info APK DB" - case pkg.DebPkg: - answer = "acquired package info 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 metadata file" - case pkg.RustPkg: - answer = "acquired package info from rust cargo manifest" - default: - answer = "determine from the following paths" - } - var paths []string - for _, l := range p.Locations { - paths = append(paths, l.RealPath) - } - - return answer + ": " + strings.Join(paths, ", ") -} - -func getOriginator(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 getDescription(p *pkg.Package) string { - switch metadata := p.Metadata.(type) { - case pkg.ApkMetadata: - return metadata.Description - case pkg.NpmPackageJSONMetadata: - return metadata.Description - default: - return "" - } -} diff --git a/internal/presenter/packages/spdx_tag_value_presenter.go b/internal/presenter/packages/spdx_tag_value_presenter.go index 6fd06974b29..6bd20fc11b8 100644 --- a/internal/presenter/packages/spdx_tag_value_presenter.go +++ b/internal/presenter/packages/spdx_tag_value_presenter.go @@ -5,12 +5,12 @@ import ( "io" "time" + "github.com/anchore/syft/internal/spdxlicense" + "github.com/anchore/syft/internal" - "github.com/anchore/syft/internal/log" "github.com/anchore/syft/internal/version" "github.com/anchore/syft/syft/pkg" "github.com/anchore/syft/syft/source" - spdxLicense "github.com/mitchellh/go-spdx" "github.com/spdx/tools-golang/spdx" "github.com/spdx/tools-golang/tvsaver" ) @@ -30,6 +30,7 @@ func NewSPDXTagValuePresenter(catalog *pkg.Catalog, srcMetadata source.Metadata) } // 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{ @@ -41,8 +42,7 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error { // Cardinality: mandatory, one DataLicense: "CC0-1.0", - // 2.3: SPDX Identifier; should be "DOCUMENT" to represent - // mandatory identifier of SPDXRef-DOCUMENT + // 2.3: SPDX Identifier; should be "DOCUMENT" to represent mandatory identifier of SPDXRef-DOCUMENT // Cardinality: mandatory, one SPDXIdentifier: spdx.ElementID("DOCUMENT"), @@ -76,7 +76,7 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error { // 2.7: License List Version // Cardinality: optional, one - LicenseListVersion: "", + LicenseListVersion: spdxlicense.Version, // 2.8: Creators: may have multiple keys for Person, Organization // and/or Tool @@ -98,45 +98,24 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error { DocumentComment: "", }, Packages: pres.packages(), - // TODO: consider adding the following fields - //UnpackagedFiles: nil, - //OtherLicenses: nil, - //Relationships: nil, - //Annotations: nil, } 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() { - // TODO: name should be guaranteed to be unique, but semantically useful (and stable) + // name should be guaranteed to be unique, but semantically useful and stable id := fmt.Sprintf("Package-%+v-%s", p.Type, p.Name) - // 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). - license := "NONE" - if len(p.Licenses) > 0 { - // note: we are not supporting complex expressions at this time, only individual licenses - licenseInfo, err := spdxLicense.License(p.Licenses[0]) - if err != nil { - log.Warnf("unable to parse SPDX license for package=%+v : %+v", p, err) - license = "NOASSERTION" - } else { - license = licenseInfo.ID - } - } - - filesAnalyzed, files := pres.packageFiles(p) + // 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{ @@ -194,7 +173,7 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_ // Intent: A package can refer to a project, product, artifact, distribution or a component that is // external to the SPDX document. - FilesAnalyzed: filesAnalyzed, + FilesAnalyzed: false, // NOT PART OF SPEC: did FilesAnalyzed tag appear? IsFilesAnalyzedTagPresent: true, @@ -234,7 +213,7 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_ // 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: "NOASSERTION", + 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; @@ -289,92 +268,8 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_ PackageAttributionTexts: nil, // Files contained in this Package - Files: files, + Files: nil, } } return results } - -func (pres *SPDXTagValuePresenter) packageFiles(p *pkg.Package) (bool, map[spdx.ElementID]*spdx.File2_2) { - filesAnalyzed := false - files := make(map[spdx.ElementID]*spdx.File2_2) - if owner, ok := p.Metadata.(pkg.FileOwner); ok { - filesAnalyzed = true - for _, f := range owner.OwnedFiles() { - // TODO: should we include layer information in the element id? - id := spdx.ElementID(f) - files[id] = &spdx.File2_2{ - - // 4.1: File Name - // Cardinality: mandatory, one - FileName: f, - - // 4.2: File SPDX Identifier: "SPDXRef-[idstring]" - // Cardinality: mandatory, one - FileSPDXIdentifier: id, - - // 4.3: File Type - // Cardinality: optional, multiple - FileType: nil, - - // 4.4: File Checksum: may have keys for SHA1, SHA256 and/or MD5 - // Cardinality: mandatory, one SHA1, others may be optionally provided - // TODO: we don't have the resolvers at this point, but we could make that available? - FileChecksumSHA1: "", - FileChecksumSHA256: "", - FileChecksumMD5: "", - - // 4.5: Concluded License: SPDX License Expression, "NONE" or "NOASSERTION" - // Cardinality: mandatory, one - LicenseConcluded: "NOASSERTION", - - // 4.6: License Information in File: SPDX License Expression, "NONE" or "NOASSERTION" - // Cardinality: mandatory, one or many - // TODO: could use a license classifier here - LicenseInfoInFile: []string{"NOASSERTION"}, - - // 4.7: Comments on License - // Cardinality: optional, one - LicenseComments: "", - - // 4.8: Copyright Text: copyright notice(s) text, "NONE" or "NOASSERTION" - // Cardinality: mandatory, one - FileCopyrightText: "NOASSERTION", - - // DEPRECATED in version 2.1 of spec - // 4.9-4.11: Artifact of Project variables (defined below) - // Cardinality: optional, one or many - ArtifactOfProjects: nil, - - // 4.12: File Comment - // Cardinality: optional, one - FileComment: "", - - // 4.13: File Notice - // Cardinality: optional, one - FileNotice: "", - - // 4.14: File Contributor - // Cardinality: optional, one or many - FileContributor: nil, - - // 4.15: File Attribution Text - // Cardinality: optional, one or many - FileAttributionTexts: nil, - - // DEPRECATED in version 2.0 of spec - // 4.16: File Dependencies - // Cardinality: optional, one or many - FileDependencies: nil, - - // Snippets contained in this File - // Note that Snippets could be defined in a different Document! However, - // the only ones that _THIS_ document can contain are this ones that are - // defined here -- so this should just be an ElementID. - Snippets: nil, - } - } - } - - return filesAnalyzed, files -} diff --git a/internal/spdxlicense/generate_license_list.go b/internal/spdxlicense/generate_license_list.go new file mode 100644 index 00000000000..743c71bf55b --- /dev/null +++ b/internal/spdxlicense/generate_license_list.go @@ -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) + } +} diff --git a/internal/spdxlicense/license.go b/internal/spdxlicense/license.go new file mode 100644 index 00000000000..bcbdc7c7383 --- /dev/null +++ b/internal/spdxlicense/license.go @@ -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 +} diff --git a/internal/spdxlicense/license_list.go b/internal/spdxlicense/license_list.go new file mode 100644 index 00000000000..183a6613af5 --- /dev/null +++ b/internal/spdxlicense/license_list.go @@ -0,0 +1,469 @@ +// Code generated by go generate; DO NOT EDIT. +// This file was generated by robots at 2021-06-07 22:00:19.80339 -0400 EDT m=+0.287837998 +// 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", +} diff --git a/internal/spdxlicense/license_list_test.go b/internal/spdxlicense/license_list_test.go new file mode 100644 index 00000000000..6dccd7badcf --- /dev/null +++ b/internal/spdxlicense/license_list_test.go @@ -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) +} diff --git a/syft/pkg/cataloger/deb/cataloger_test.go b/syft/pkg/cataloger/deb/cataloger_test.go index 84bef7c11fb..482ef77555e 100644 --- a/syft/pkg/cataloger/deb/cataloger_test.go +++ b/syft/pkg/cataloger/deb/cataloger_test.go @@ -33,7 +33,7 @@ func TestDpkgCataloger(t *testing.T) { Name: "libpam-runtime", Version: "1.1.8-3.6", FoundBy: "dpkgdb-cataloger", - Licenses: []string{"GPL-2", "LGPL-2.1"}, + Licenses: []string{"GPL-1", "GPL-2", "LGPL-2.1"}, Type: pkg.DebPkg, MetadataType: pkg.DpkgMetadataType, Metadata: pkg.DpkgMetadata{ diff --git a/syft/pkg/cataloger/deb/parse_copyright.go b/syft/pkg/cataloger/deb/parse_copyright.go index 95ee16a8f35..20046391105 100644 --- a/syft/pkg/cataloger/deb/parse_copyright.go +++ b/syft/pkg/cataloger/deb/parse_copyright.go @@ -12,7 +12,10 @@ import ( // For more information see: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/#license-syntax -var licensePattern = regexp.MustCompile(`^License: (?P\S*)`) +var ( + licensePattern = regexp.MustCompile(`^License: (?P\S*)`) + commonLicensePathPattern = regexp.MustCompile(`/usr/share/common-licenses/(?P[0-9A-Za-z_.\-]+)`) +) func parseLicensesFromCopyright(reader io.Reader) []string { findings := internal.NewStringSet() @@ -20,22 +23,11 @@ func parseLicensesFromCopyright(reader io.Reader) []string { for scanner.Scan() { line := scanner.Text() - - matchesByGroup := internal.MatchNamedCaptureGroups(licensePattern, line) - if len(matchesByGroup) > 0 { - candidate, ok := matchesByGroup["license"] - if !ok { - continue - } - - candidate = strings.TrimSpace(candidate) - if strings.Contains(candidate, " or ") || strings.Contains(candidate, " and ") { - // this is a multi-license summary, ignore this as other recurrent license lines should cover this - continue - } - if candidate != "" && strings.ToLower(candidate) != "none" { - findings.Add(candidate) - } + if value := findLicenseClause(licensePattern, "license", line); value != "" { + findings.Add(value) + } + if value := findLicenseClause(commonLicensePathPattern, "license", line); value != "" { + findings.Add(value) } } @@ -45,3 +37,27 @@ func parseLicensesFromCopyright(reader io.Reader) []string { return results } + +func findLicenseClause(pattern *regexp.Regexp, valueGroup, line string) string { + matchesByGroup := internal.MatchNamedCaptureGroups(pattern, line) + + candidate, ok := matchesByGroup[valueGroup] + if !ok { + return "" + } + + return ensureIsSingleLicense(candidate) +} + +func ensureIsSingleLicense(candidate string) (license string) { + candidate = strings.TrimSpace(candidate) + if strings.Contains(candidate, " or ") || strings.Contains(candidate, " and ") { + // this is a multi-license summary, ignore this as other recurrent license lines should cover this + return + } + if candidate != "" && strings.ToLower(candidate) != "none" { + // the license may be at the end of a sentence, clean . characters + license = strings.TrimSuffix(candidate, ".") + } + return license +} diff --git a/syft/pkg/cataloger/deb/parse_copyright_test.go b/syft/pkg/cataloger/deb/parse_copyright_test.go index 8242c902a70..c6cd0c0c9f3 100644 --- a/syft/pkg/cataloger/deb/parse_copyright_test.go +++ b/syft/pkg/cataloger/deb/parse_copyright_test.go @@ -12,22 +12,27 @@ func TestParseLicensesFromCopyright(t *testing.T) { fixture string expected []string }{ + { + fixture: "test-fixtures/copyright/libc6", + // note: there are other licenses in this file that are not matched --we don't do full text license identification yet + expected: []string{"GPL-2", "LGPL-2.1"}, + }, { fixture: "test-fixtures/copyright/trilicense", expected: []string{"GPL-2", "LGPL-2.1", "MPL-1.1"}, }, { fixture: "test-fixtures/copyright/liblzma5", - expected: []string{"Autoconf", "GPL-2", "GPL-2+", "LGPL-2.1+", "PD", "PD-debian", "config-h", "noderivs", "permissive-fsf", "permissive-nowarranty", "probably-PD"}, + expected: []string{"Autoconf", "GPL-2", "GPL-2+", "GPL-3", "LGPL-2", "LGPL-2.1", "LGPL-2.1+", "PD", "PD-debian", "config-h", "noderivs", "permissive-fsf", "permissive-nowarranty", "probably-PD"}, }, { fixture: "test-fixtures/copyright/libaudit-common", - expected: []string{"GPL-2", "LGPL-2.1"}, + expected: []string{"GPL-1", "GPL-2", "LGPL-2.1"}, }, { fixture: "test-fixtures/copyright/python", // note: this should not capture #, Permission, This, see ... however it's not clear how to fix this (this is probably good enough) - expected: []string{"#", "Apache", "Apache-2", "Apache-2.0", "Expat", "ISC", "LGPL-2.1+", "PSF-2", "Permission", "Python", "This", "see"}, + expected: []string{"#", "Apache", "Apache-2", "Apache-2.0", "Expat", "GPL-2", "ISC", "LGPL-2.1+", "PSF-2", "Permission", "Python", "This", "see"}, }, } diff --git a/syft/pkg/cataloger/deb/test-fixtures/copyright/libc6 b/syft/pkg/cataloger/deb/test-fixtures/copyright/libc6 new file mode 100644 index 00000000000..203faf1b327 --- /dev/null +++ b/syft/pkg/cataloger/deb/test-fixtures/copyright/libc6 @@ -0,0 +1,509 @@ +This is the Debian prepackaged version of the GNU C Library version 2.23. + +It was put together by the GNU Libc Maintainers +from + +* Most of the GNU C library is under the following copyright: + + Copyright (C) 1991-2015 Free Software Foundation, Inc. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, write to the Free + Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA + 02110-1301 USA + + On Debian systems, the complete text of the GNU Library + General Public License can be found in `/usr/share/common-licenses/LGPL-2.1'. + +* The utilities associated with GNU C library is under the following + copyright: + + Copyright (C) 1991-2015 Free Software Foundation, Inc. + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published + by the Free Software Foundation; version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software Foundation, + Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + + On Debian systems, the complete text of the GNU Library + General Public License can be found in `/usr/share/common-licenses/GPL-2'. + +* All code incorporated from 4.4 BSD is distributed under the following + license: + + Copyright (C) 1991 Regents of the University of California. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. [This condition was removed.] + 4. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + +* The DNS resolver code, taken from BIND 4.9.5, is copyrighted both by + UC Berkeley and by Digital Equipment Corporation. The DEC portions + are under the following license: + + Portions Copyright (C) 1993 by Digital Equipment Corporation. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies, and + that the name of Digital Equipment Corporation not be used in + advertising or publicity pertaining to distribution of the document or + software without specific, written prior permission. + + THE SOFTWARE IS PROVIDED ``AS IS'' AND DIGITAL EQUIPMENT CORP. + DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, INCLUDING ALL + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL + DIGITAL EQUIPMENT CORPORATION BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING + FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, + NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION + WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +* The ISC portions are under the following license: + + Portions Copyright (c) 1996-1999 by Internet Software Consortium. + + Permission to use, copy, modify, and distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THE SOFTWARE IS PROVIDED "AS IS" AND INTERNET SOFTWARE CONSORTIUM DISCLAIMS + ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES + OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL INTERNET SOFTWARE + CONSORTIUM BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL + DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR + PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS + ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS + SOFTWARE. + +* The Sun RPC support (from rpcsrc-4.0) is covered by the following + license: + + Copyright (c) 2010, Oracle America, Inc. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above + copyright notice, this list of conditions and the following + disclaimer in the documentation and/or other materials + provided with the distribution. + * Neither the name of the "Oracle America, Inc." nor the names of its + contributors may be used to endorse or promote products derived + from this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, + INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, + WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +* The following CMU license covers some of the support code for Mach, + derived from Mach 3.0: + + Mach Operating System + Copyright (C) 1991,1990,1989 Carnegie Mellon University + All Rights Reserved. + + Permission to use, copy, modify and distribute this software and its + documentation is hereby granted, provided that both the copyright + notice and this permission notice appear in all copies of the + software, derivative works or modified versions, and any portions + thereof, and that both notices appear in supporting documentation. + + CARNEGIE MELLON ALLOWS FREE USE OF THIS SOFTWARE IN ITS ``AS IS'' + CONDITION. CARNEGIE MELLON DISCLAIMS ANY LIABILITY OF ANY KIND FOR + ANY DAMAGES WHATSOEVER RESULTING FROM THE USE OF THIS SOFTWARE. + + Carnegie Mellon requests users of this software to return to + + Software Distribution Coordinator + School of Computer Science + Carnegie Mellon University + Pittsburgh PA 15213-3890 + + or Software.Distribution@CS.CMU.EDU any improvements or + extensions that they make and grant Carnegie Mellon the rights to + redistribute these changes. + +* The file if_ppp.h is under the following CMU license: + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the University nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY CARNEGIE MELLON UNIVERSITY AND + CONTRIBUTORS ``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, + INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + IN NO EVENT SHALL THE UNIVERSITY OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE + GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS + INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER + IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR + OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN + IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +* The following license covers the files from Intel's "Highly Optimized + Mathematical Functions for Itanium" collection: + + Intel License Agreement + + Copyright (c) 2000, Intel Corporation + + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are + met: + + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + * The name of Intel Corporation may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL INTEL OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +* The files inet/getnameinfo.c and sysdeps/posix/getaddrinfo.c are copyright + (C) by Craig Metz and are distributed under the following license: + + /* The Inner Net License, Version 2.00 + + The author(s) grant permission for redistribution and use in source and + binary forms, with or without modification, of the software and documentation + provided that the following conditions are met: + + 0. If you receive a version of the software that is specifically labelled + as not being for redistribution (check the version message and/or README), + you are not permitted to redistribute that version of the software in any + way or form. + 1. All terms of the all other applicable copyrights and licenses must be + followed. + 2. Redistributions of source code must retain the authors' copyright + notice(s), this list of conditions, and the following disclaimer. + 3. Redistributions in binary form must reproduce the authors' copyright + notice(s), this list of conditions, and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 4. [The copyright holder has authorized the removal of this clause.] + 5. Neither the name(s) of the author(s) nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY ITS AUTHORS AND CONTRIBUTORS ``AS IS'' AND ANY + EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE AUTHORS OR CONTRIBUTORS BE LIABLE FOR ANY + DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + If these license terms cause you a real problem, contact the author. */ + +* The file sunrpc/des_impl.c is copyright Eric Young: + + Copyright (C) 1992 Eric Young + Collected from libdes and modified for SECURE RPC by Martin Kuck 1994 + This file is distributed under the terms of the GNU Lesser General + Public License, version 2.1 or later - see the file COPYING.LIB for details. + If you did not receive a copy of the license with this program, please + see to obtain a copy. + +* The libidn code is copyright Simon Josefsson, with portions copyright + The Internet Society, Tom Tromey and Red Hat, Inc.: + + Copyright (C) 2002, 2003, 2004, 2011 Simon Josefsson + + This file is part of GNU Libidn. + + GNU Libidn is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + GNU Libidn is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with GNU Libidn; if not, see . + +* The following notice applies to portions of libidn/nfkc.c: + + This file contains functions from GLIB, including gutf8.c and + gunidecomp.c, all licensed under LGPL and copyright hold by: + + Copyright (C) 1999, 2000 Tom Tromey + Copyright 2000 Red Hat, Inc. + +* The following applies to portions of libidn/punycode.c and + libidn/punycode.h: + + This file is derived from RFC 3492bis written by Adam M. Costello. + + Disclaimer and license: Regarding this entire document or any + portion of it (including the pseudocode and C code), the author + makes no guarantees and is not responsible for any damage resulting + from its use. The author grants irrevocable permission to anyone + to use, modify, and distribute it in any way that does not diminish + the rights of anyone else to use, modify, and distribute it, + provided that redistributed derivative works do not contain + misleading author or version information. Derivative works need + not be licensed under similar terms. + + Copyright (C) The Internet Society (2003). All Rights Reserved. + + This document and translations of it may be copied and furnished to + others, and derivative works that comment on or otherwise explain it + or assist in its implementation may be prepared, copied, published + and distributed, in whole or in part, without restriction of any + kind, provided that the above copyright notice and this paragraph are + included on all such copies and derivative works. However, this + document itself may not be modified in any way, such as by removing + the copyright notice or references to the Internet Society or other + Internet organizations, except as needed for the purpose of + developing Internet standards in which case the procedures for + copyrights defined in the Internet Standards process must be + followed, or as required to translate it into languages other than + English. + + The limited permissions granted above are perpetual and will not be + revoked by the Internet Society or its successors or assigns. + + This document and the information contained herein is provided on an + "AS IS" basis and THE INTERNET SOCIETY AND THE INTERNET ENGINEERING + TASK FORCE DISCLAIMS ALL WARRANTIES, EXPRESS OR IMPLIED, INCLUDING + BUT NOT LIMITED TO ANY WARRANTY THAT THE USE OF THE INFORMATION + HEREIN WILL NOT INFRINGE ANY RIGHTS OR ANY IMPLIED WARRANTIES OF + MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. + +* The file inet/rcmd.c is under a UCB copyright and the following: + + Copyright (C) 1998 WIDE Project. + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + 3. Neither the name of the project nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE PROJECT AND CONTRIBUTORS ``AS IS'' AND + ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + ARE DISCLAIMED. IN NO EVENT SHALL THE PROJECT OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + SUCH DAMAGE. + + * The file posix/runtests.c is copyright Tom Lord: + + Copyright 1995 by Tom Lord + + All Rights Reserved + + Permission to use, copy, modify, and distribute this software and its + documentation for any purpose and without fee is hereby granted, + provided that the above copyright notice appear in all copies and that + both that copyright notice and this permission notice appear in + supporting documentation, and that the name of the copyright holder not be + used in advertising or publicity pertaining to distribution of the + software without specific, written prior permission. + + Tom Lord DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE, + INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS, IN NO + EVENT SHALL TOM LORD BE LIABLE FOR ANY SPECIAL, INDIRECT OR + CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF + USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + +* The posix/rxspencer tests are copyright Henry Spencer: + + Copyright 1992, 1993, 1994, 1997 Henry Spencer. All rights reserved. + This software is not subject to any license of the American Telephone + and Telegraph Company or of the Regents of the University of California. + + Permission is granted to anyone to use this software for any purpose on + any computer system, and to alter it and redistribute it, subject + to the following restrictions: + + 1. The author is not responsible for the consequences of use of this + software, no matter how awful, even if they arise from flaws in it. + + 2. The origin of this software must not be misrepresented, either by + explicit claim or by omission. Since few users ever read sources, + credits must appear in the documentation. + + 3. Altered versions must be plainly marked as such, and must not be + misrepresented as being the original software. Since few users + ever read sources, credits must appear in the documentation. + + 4. This notice may not be removed or altered. + +* The file posix/PCRE.tests is copyright University of Cambridge: + + Copyright (c) 1997-2003 University of Cambridge + + Permission is granted to anyone to use this software for any purpose on any + computer system, and to redistribute it freely, subject to the following + restrictions: + + 1. This software is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + + 2. The origin of this software must not be misrepresented, either by + explicit claim or by omission. In practice, this means that if you use + PCRE in software that you distribute to others, commercially or + otherwise, you must put a sentence like this + + Regular expression support is provided by the PCRE library package, + which is open source software, written by Philip Hazel, and copyright + by the University of Cambridge, England. + + somewhere reasonably visible in your documentation and in any relevant + files or online help data or similar. A reference to the ftp site for + the source, that is, to + + ftp://ftp.csx.cam.ac.uk/pub/software/programming/pcre/ + + should also be given in the documentation. However, this condition is not + intended to apply to whole chains of software. If package A includes PCRE, + it must acknowledge it, but if package B is software that includes package + A, the condition is not imposed on package B (unless it uses PCRE + independently). + + 3. Altered versions must be plainly marked as such, and must not be + misrepresented as being the original software. + + 4. If PCRE is embedded in any software that is released under the GNU + General Purpose Licence (GPL), or Lesser General Purpose Licence (LGPL), + then the terms of that licence shall supersede any condition above with + which it is incompatible. + +* Files from Sun fdlibm are copyright Sun Microsystems, Inc.: + + Copyright (C) 1993 by Sun Microsystems, Inc. All rights reserved. + + Developed at SunPro, a Sun Microsystems, Inc. business. + Permission to use, copy, modify, and distribute this + software is freely granted, provided that this notice + is preserved. + +* Part of stdio-common/tst-printf.c is copyright C E Chew: + + (C) Copyright C E Chew + + Feel free to copy, use and distribute this software provided: + + 1. you do not pretend that you wrote it + 2. you leave this copyright notice intact. + +* Various long double libm functions are copyright Stephen L. Moshier: + + Copyright 2001 by Stephen L. Moshier + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with this library; if not, see + . */ \ No newline at end of file diff --git a/test/integration/license_list_test.go b/test/integration/license_list_test.go new file mode 100644 index 00000000000..e0e384123b8 --- /dev/null +++ b/test/integration/license_list_test.go @@ -0,0 +1,36 @@ +package integration + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/anchore/syft/internal/spdxlicense" + "github.com/stretchr/testify/assert" +) + +func TestSPDXLicenseListIsTheLatest(t *testing.T) { + resp, err := http.Get("https://spdx.org/licenses/licenses.json") + if err != nil { + t.Fatalf("unable to get licenses list: %+v", err) + } + + 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"` + } + + var latest licenseList + if err = json.NewDecoder(resp.Body).Decode(&latest); err != nil { + t.Fatalf("unable to decode license list: %+v", err) + } + + assert.Equal(t, latest.Version, spdxlicense.Version) +} From a6ef8a75dadb41b0b24fceaaa1b0eafc688e9587 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 7 Jun 2021 23:03:31 -0400 Subject: [PATCH 06/13] keep fileOwner unexported from pkg Signed-off-by: Alex Goodman --- syft/pkg/apk_metadata.go | 4 ++-- syft/pkg/apk_metadata_test.go | 2 +- syft/pkg/dpkg_metadata.go | 4 ++-- syft/pkg/dpkg_metadata_test.go | 2 +- syft/pkg/file_owner.go | 4 ++-- syft/pkg/ownership_by_files_relationship.go | 4 ++-- syft/pkg/python_package_metadata.go | 4 ++-- syft/pkg/python_package_metadata_test.go | 2 +- syft/pkg/rpmdb_metadata.go | 4 ++-- syft/pkg/rpmdb_metadata_test.go | 2 +- 10 files changed, 16 insertions(+), 16 deletions(-) diff --git a/syft/pkg/apk_metadata.go b/syft/pkg/apk_metadata.go index 0578f30610c..84b14864599 100644 --- a/syft/pkg/apk_metadata.go +++ b/syft/pkg/apk_metadata.go @@ -11,7 +11,7 @@ import ( const ApkDbGlob = "**/lib/apk/db/installed" -var _ FileOwner = (*ApkMetadata)(nil) +var _ fileOwner = (*ApkMetadata)(nil) // ApkMetadata represents all captured data for a Alpine DB package entry. // See the following sources for more information: @@ -63,7 +63,7 @@ func (m ApkMetadata) PackageURL() string { return pURL.ToString() } -func (m ApkMetadata) OwnedFiles() (result []string) { +func (m ApkMetadata) ownedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/apk_metadata_test.go b/syft/pkg/apk_metadata_test.go index acce1e9b993..1ff43a12b41 100644 --- a/syft/pkg/apk_metadata_test.go +++ b/syft/pkg/apk_metadata_test.go @@ -69,7 +69,7 @@ func TestApkMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(FileOwner).OwnedFiles() + actual := i.(fileOwner).ownedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } diff --git a/syft/pkg/dpkg_metadata.go b/syft/pkg/dpkg_metadata.go index b17bc385a86..8f2245c5513 100644 --- a/syft/pkg/dpkg_metadata.go +++ b/syft/pkg/dpkg_metadata.go @@ -12,7 +12,7 @@ import ( const DpkgDbGlob = "**/var/lib/dpkg/{status,status.d/**}" -var _ FileOwner = (*DpkgMetadata)(nil) +var _ fileOwner = (*DpkgMetadata)(nil) // DpkgMetadata represents all captured data for a Debian package DB entry; available fields are described // at http://manpages.ubuntu.com/manpages/xenial/man1/dpkg-query.1.html in the --showformat section. @@ -55,7 +55,7 @@ func (m DpkgMetadata) PackageURL(d *distro.Distro) string { return pURL.ToString() } -func (m DpkgMetadata) OwnedFiles() (result []string) { +func (m DpkgMetadata) ownedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/dpkg_metadata_test.go b/syft/pkg/dpkg_metadata_test.go index e0e42d949a2..81a0cb66589 100644 --- a/syft/pkg/dpkg_metadata_test.go +++ b/syft/pkg/dpkg_metadata_test.go @@ -88,7 +88,7 @@ func TestDpkgMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(FileOwner).OwnedFiles() + actual := i.(fileOwner).ownedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } diff --git a/syft/pkg/file_owner.go b/syft/pkg/file_owner.go index fba023d8b8a..24520c304e7 100644 --- a/syft/pkg/file_owner.go +++ b/syft/pkg/file_owner.go @@ -1,5 +1,5 @@ package pkg -type FileOwner interface { - OwnedFiles() []string +type fileOwner interface { + ownedFiles() []string } diff --git a/syft/pkg/ownership_by_files_relationship.go b/syft/pkg/ownership_by_files_relationship.go index 4e2b4d3148b..f235557a4ba 100644 --- a/syft/pkg/ownership_by_files_relationship.go +++ b/syft/pkg/ownership_by_files_relationship.go @@ -55,11 +55,11 @@ func findOwnershipByFilesRelationships(catalog *Catalog) map[ID]map[ID]*strset.S } // check to see if this is a file owner - pkgFileOwner, ok := candidateOwnerPkg.Metadata.(FileOwner) + pkgFileOwner, ok := candidateOwnerPkg.Metadata.(fileOwner) if !ok { continue } - for _, ownedFilePath := range pkgFileOwner.OwnedFiles() { + for _, ownedFilePath := range pkgFileOwner.ownedFiles() { if matchesAny(ownedFilePath, globsForbiddenFromBeingOwned) { // we skip over known exceptions to file ownership, such as the RPM package owning // the RPM DB path, otherwise the RPM package would "own" all RPMs, which is not intended diff --git a/syft/pkg/python_package_metadata.go b/syft/pkg/python_package_metadata.go index f059dfac35a..c1e752d217e 100644 --- a/syft/pkg/python_package_metadata.go +++ b/syft/pkg/python_package_metadata.go @@ -6,7 +6,7 @@ import ( "github.com/scylladb/go-set/strset" ) -var _ FileOwner = (*PythonPackageMetadata)(nil) +var _ fileOwner = (*PythonPackageMetadata)(nil) // PythonFileDigest represents the file metadata for a single file attributed to a python package. type PythonFileDigest struct { @@ -34,7 +34,7 @@ type PythonPackageMetadata struct { TopLevelPackages []string `json:"topLevelPackages,omitempty"` } -func (m PythonPackageMetadata) OwnedFiles() (result []string) { +func (m PythonPackageMetadata) ownedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/python_package_metadata_test.go b/syft/pkg/python_package_metadata_test.go index fab80bc2fcb..c2cd378176b 100644 --- a/syft/pkg/python_package_metadata_test.go +++ b/syft/pkg/python_package_metadata_test.go @@ -41,7 +41,7 @@ func TestPythonMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(FileOwner).OwnedFiles() + actual := i.(fileOwner).ownedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } diff --git a/syft/pkg/rpmdb_metadata.go b/syft/pkg/rpmdb_metadata.go index e48eb723cd6..b6460c824f5 100644 --- a/syft/pkg/rpmdb_metadata.go +++ b/syft/pkg/rpmdb_metadata.go @@ -15,7 +15,7 @@ import ( const RpmDbGlob = "**/var/lib/rpm/Packages" -var _ FileOwner = (*RpmdbMetadata)(nil) +var _ fileOwner = (*RpmdbMetadata)(nil) // RpmdbMetadata represents all captured data for a RPM DB package entry. type RpmdbMetadata struct { @@ -79,7 +79,7 @@ func (m RpmdbMetadata) PackageURL(d *distro.Distro) string { return pURL.ToString() } -func (m RpmdbMetadata) OwnedFiles() (result []string) { +func (m RpmdbMetadata) ownedFiles() (result []string) { s := strset.New() for _, f := range m.Files { if f.Path != "" { diff --git a/syft/pkg/rpmdb_metadata_test.go b/syft/pkg/rpmdb_metadata_test.go index 73a951f0046..7eded204660 100644 --- a/syft/pkg/rpmdb_metadata_test.go +++ b/syft/pkg/rpmdb_metadata_test.go @@ -90,7 +90,7 @@ func TestRpmMetadata_fileOwner(t *testing.T) { t.Run(strings.Join(test.expected, ","), func(t *testing.T) { var i interface{} i = test.metadata - actual := i.(FileOwner).OwnedFiles() + actual := i.(fileOwner).ownedFiles() for _, d := range deep.Equal(test.expected, actual) { t.Errorf("diff: %+v", d) } From 87618c016fa3b0a163918c1f6aca9623424887fd Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Mon, 7 Jun 2021 23:05:19 -0400 Subject: [PATCH 07/13] restore cli test util Signed-off-by: Alex Goodman --- test/cli/utils_test.go | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/test/cli/utils_test.go b/test/cli/utils_test.go index 58737303b48..4de1f70a4bd 100644 --- a/test/cli/utils_test.go +++ b/test/cli/utils_test.go @@ -5,7 +5,9 @@ import ( "fmt" "os" "os/exec" + "path" "path/filepath" + "runtime" "strings" "testing" @@ -46,22 +48,19 @@ func getSyftCommand(t testing.TB, args ...string) *exec.Cmd { // SYFT_BINARY_LOCATION is the absolute path to the snapshot binary binaryLocation = os.Getenv("SYFT_BINARY_LOCATION") } else { - // note: there is a subtle - vs _ difference between these versions - //switch runtime.GOOS { - //case "darwin": - // binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft-macos_darwin_%s/syft", runtime.GOARCH)) - //case "linux": - // binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft_linux_%s/syft", runtime.GOARCH)) - //default: - // t.Fatalf("unsupported OS: %s", runtime.GOOS) - //} + //note: there is a subtle - vs _ difference between these versions + switch runtime.GOOS { + case "darwin": + binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft-macos_darwin_%s/syft", runtime.GOARCH)) + case "linux": + binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft_linux_%s/syft", runtime.GOARCH)) + default: + t.Fatalf("unsupported OS: %s", runtime.GOOS) + } - // TMP TMP TMP - binaryLocation = "/Users/wagoodman/.gimme/versions/go1.15.2.darwin.amd64/bin/go" - args = append([]string{"run", "../../main.go"}, args...) } - //t.Log(binaryLocation, args) + t.Log(binaryLocation, args) return exec.Command(binaryLocation, args...) } From 7761619467d95777b379ddc4b3afd45bebe8cd56 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Tue, 8 Jun 2021 13:34:36 -0400 Subject: [PATCH 08/13] add external refs to spdx tag-value format Signed-off-by: Alex Goodman --- .../packages/model/spdx22/document.go | 2 +- .../presenter/packages/model/spdx22/element.go | 6 +++--- .../presenter/packages/model/spdx22/file.go | 2 +- .../presenter/packages/model/spdx22/item.go | 2 +- .../presenter/packages/model/spdx22/package.go | 2 +- .../presenter/packages/model/spdx22/snippet.go | 2 +- .../presenter/packages/spdx_json_presenter.go | 18 +++++++++++++----- .../packages/spdx_tag_value_presenter.go | 14 +++++++++++++- 8 files changed, 34 insertions(+), 14 deletions(-) diff --git a/internal/presenter/packages/model/spdx22/document.go b/internal/presenter/packages/model/spdx22/document.go index b34758d3f38..995ae8f7dbb 100644 --- a/internal/presenter/packages/model/spdx22/document.go +++ b/internal/presenter/packages/model/spdx22/document.go @@ -6,6 +6,7 @@ package spdx22 // - 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. @@ -39,5 +40,4 @@ type Document struct { Files []File `json:"files,omitempty"` // Snippets referenced in the SPDX document Snippets []Snippet `json:"snippets,omitempty"` - Element } diff --git a/internal/presenter/packages/model/spdx22/element.go b/internal/presenter/packages/model/spdx22/element.go index 812390ccb63..65f3ae756cc 100644 --- a/internal/presenter/packages/model/spdx22/element.go +++ b/internal/presenter/packages/model/spdx22/element.go @@ -2,11 +2,11 @@ package spdx22 type Element struct { SPDXID string `json:"SPDXID"` - // Provide additional information about an SpdxElement. - Annotations []Annotation `json:"annotations,omitempty"` - Comment string `json:"comment,omitempty"` // 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"` } diff --git a/internal/presenter/packages/model/spdx22/file.go b/internal/presenter/packages/model/spdx22/file.go index c6ae1bf5dc1..913642493ee 100644 --- a/internal/presenter/packages/model/spdx22/file.go +++ b/internal/presenter/packages/model/spdx22/file.go @@ -17,6 +17,7 @@ const ( ) 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"` @@ -37,5 +38,4 @@ type File struct { // 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"` - Item } diff --git a/internal/presenter/packages/model/spdx22/item.go b/internal/presenter/packages/model/spdx22/item.go index 9f7fc13a5db..ce42f2ad245 100644 --- a/internal/presenter/packages/model/spdx22/item.go +++ b/internal/presenter/packages/model/spdx22/item.go @@ -1,6 +1,7 @@ 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"` @@ -18,5 +19,4 @@ type Item struct { // 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"` - Element } diff --git a/internal/presenter/packages/model/spdx22/package.go b/internal/presenter/packages/model/spdx22/package.go index 1e74b026b8a..aa1ac60564f 100644 --- a/internal/presenter/packages/model/spdx22/package.go +++ b/internal/presenter/packages/model/spdx22/package.go @@ -1,6 +1,7 @@ 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"` @@ -42,5 +43,4 @@ type Package struct { 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"` - Item } diff --git a/internal/presenter/packages/model/spdx22/snippet.go b/internal/presenter/packages/model/spdx22/snippet.go index 98e372ae04e..5b9ae639e3a 100644 --- a/internal/presenter/packages/model/spdx22/snippet.go +++ b/internal/presenter/packages/model/spdx22/snippet.go @@ -20,6 +20,7 @@ type Range struct { } 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"` @@ -28,5 +29,4 @@ type Snippet struct { // (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"` - Item } diff --git a/internal/presenter/packages/spdx_json_presenter.go b/internal/presenter/packages/spdx_json_presenter.go index b4a52a60478..0fcfb4cc762 100644 --- a/internal/presenter/packages/spdx_json_presenter.go +++ b/internal/presenter/packages/spdx_json_presenter.go @@ -40,7 +40,20 @@ func (pres *SPDXJsonPresenter) Present(output io.Writer) error { } 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{ + // should this be unique to the user's input? or otherwise just say document? + SPDXID: spdx22.ElementID("DOCUMENT").String(), + Name: name, + }, SPDXVersion: spdx22.Version, CreationInfo: spdx22.CreationInfo{ Created: time.Now().UTC(), @@ -54,11 +67,6 @@ func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx DataLicense: "CC0-1.0", DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", srcMetadata.ImageMetadata.UserInput), Packages: newSPDXJsonPackages(catalog), - Element: spdx22.Element{ - // should this be unique to the user's input? or otherwise just say document? - SPDXID: spdx22.ElementID("DOCUMENT").String(), - Name: srcMetadata.ImageMetadata.UserInput, - }, } } diff --git a/internal/presenter/packages/spdx_tag_value_presenter.go b/internal/presenter/packages/spdx_tag_value_presenter.go index 6bd20fc11b8..6a46c16970d 100644 --- a/internal/presenter/packages/spdx_tag_value_presenter.go +++ b/internal/presenter/packages/spdx_tag_value_presenter.go @@ -257,7 +257,7 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_ // 3.21: Package External Reference // Cardinality: optional, one or many - PackageExternalReferences: nil, + PackageExternalReferences: formatSPDXExternalRefs(p), // 3.22: Package External Reference Comment // Cardinality: conditional (optional, one) for each External Reference @@ -273,3 +273,15 @@ func (pres *SPDXTagValuePresenter) packages() map[spdx.ElementID]*spdx.Package2_ } 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 +} From 71fbadfc0c37d04f804fcfe982ea53743a89c995 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 9 Jun 2021 15:38:53 -0400 Subject: [PATCH 09/13] add golang support to CPE generation Signed-off-by: Alex Goodman --- internal/spdxlicense/license_list.go | 2 +- syft/pkg/cataloger/cpe.go | 78 ++++++++++++++++- syft/pkg/cataloger/cpe_test.go | 121 +++++++++++++++++++++++++++ test/cli/utils_test.go | 4 +- 4 files changed, 200 insertions(+), 5 deletions(-) diff --git a/internal/spdxlicense/license_list.go b/internal/spdxlicense/license_list.go index 183a6613af5..39bd692129a 100644 --- a/internal/spdxlicense/license_list.go +++ b/internal/spdxlicense/license_list.go @@ -1,5 +1,5 @@ // Code generated by go generate; DO NOT EDIT. -// This file was generated by robots at 2021-06-07 22:00:19.80339 -0400 EDT m=+0.287837998 +// 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 diff --git a/syft/pkg/cataloger/cpe.go b/syft/pkg/cataloger/cpe.go index b11d1cec7c1..346c446e68b 100644 --- a/syft/pkg/cataloger/cpe.go +++ b/syft/pkg/cataloger/cpe.go @@ -2,6 +2,7 @@ package cataloger import ( "fmt" + "net/url" "sort" "strings" @@ -106,6 +107,10 @@ func generatePackageCPEs(p pkg.Package) []pkg.CPE { vendors := candidateVendors(p) products := candidateProducts(p) + if len(products) == 0 { + return nil + } + keys := internal.NewStringSet() cpes := make([]pkg.CPE, 0) for _, product := range products { @@ -145,6 +150,8 @@ func candidateTargetSoftwareAttrs(p pkg.Package) []string { targetSw = append(targetSw, "ruby", "rails") case pkg.Python: targetSw = append(targetSw, "python") + case pkg.Go: + targetSw = append(targetSw, "go", "golang") } return targetSw @@ -163,11 +170,20 @@ func candidateVendors(p pkg.Package) []string { // TODO: Confirm whether using products as vendors is helpful to the matching process vendors := candidateProducts(p) - if p.Language == pkg.Java { + switch p.Language { + case pkg.Java: if p.MetadataType == pkg.JavaMetadataType { vendors = append(vendors, candidateVendorsForJava(p)...) } + case pkg.Go: + // replace all candidates with only the golang-specific helper + vendors = nil + vendor := candidateVendorForGo(p.Name) + if vendor != "" { + vendors = []string{vendor} + } } + return vendors } @@ -181,6 +197,13 @@ func candidateProducts(p pkg.Package) []string { } case pkg.Java: products = append(products, candidateProductsForJava(p)...) + case pkg.Go: + // replace all candidates with only the golang-specific helper + products = nil + prod := candidateProductForGo(p.Name) + if prod != "" { + products = []string{prod} + } } for _, prod := range products { @@ -193,6 +216,59 @@ func candidateProducts(p pkg.Package) []string { return append(productCandidatesByPkgType.getCandidates(p.Type, p.Name), products...) } +// candidateProductForGo attempts to find a single product name in a best-effort attempt. This implementation prefers +// to return no vendor over returning potentially nonsensical results. +func candidateProductForGo(name string) string { + // note: url.Parse requires a scheme for correct processing, which a golang module will not have, so one is provided. + u, err := url.Parse("http://" + name) + if err != nil { + return "" + } + + cleanPath := strings.Trim(u.Path, "/") + pathElements := strings.Split(cleanPath, "/") + + switch u.Host { + case "golang.org", "gopkg.in": + return cleanPath + case "google.golang.org": + return pathElements[0] + } + + if len(pathElements) < 2 { + return "" + } + + return pathElements[1] +} + +// candidateVendorForGo attempts to find a single vendor name in a best-effort attempt. This implementation prefers +// to return no vendor over returning potentially nonsensical results. +func candidateVendorForGo(name string) string { + // note: url.Parse requires a scheme for correct processing, which a golang module will not have, so one is provided. + u, err := url.Parse("http://" + name) + if err != nil { + return "" + } + + cleanPath := strings.Trim(u.Path, "/") + + switch u.Host { + case "google.golang.org": + return "google" + case "golang.org": + return "golang" + case "gopkg.in": + return "" + } + + pathElements := strings.Split(cleanPath, "/") + if len(pathElements) < 2 { + return "" + } + return pathElements[0] +} + func candidateProductsForJava(p pkg.Package) []string { // TODO: we could get group-id-like info from the MANIFEST.MF "Automatic-Module-Name" field // for more info see pkg:maven/commons-io/commons-io@2.8.0 within cloudbees/cloudbees-core-mm:2.263.4.2 diff --git a/syft/pkg/cataloger/cpe_test.go b/syft/pkg/cataloger/cpe_test.go index c0e5d173a42..c122084db7a 100644 --- a/syft/pkg/cataloger/cpe_test.go +++ b/syft/pkg/cataloger/cpe_test.go @@ -423,6 +423,35 @@ func TestGeneratePackageCPEs(t *testing.T) { "cpe:2.3:a:jenkins:cloudbees_installation_manager:2.89.0.33:*:*:*:*:maven:*:*", }, }, + { + name: "go product and vendor candidates are wired up", + p: pkg.Package{ + Name: "github.com/someone/something", + Version: "3.2", + FoundBy: "go-cataloger", + Language: pkg.Go, + Type: pkg.GoModulePkg, + }, + expected: []string{ + "cpe:2.3:a:*:something:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:*:something:3.2:*:*:*:*:go:*:*", + "cpe:2.3:a:*:something:3.2:*:*:*:*:golang:*:*", + "cpe:2.3:a:someone:something:3.2:*:*:*:*:*:*:*", + "cpe:2.3:a:someone:something:3.2:*:*:*:*:go:*:*", + "cpe:2.3:a:someone:something:3.2:*:*:*:*:golang:*:*", + }, + }, + { + name: "generate no CPEs for indeterminate golang package name", + p: pkg.Package{ + Name: "github.com/what", + Version: "3.2", + FoundBy: "go-cataloger", + Language: pkg.Go, + Type: pkg.GoModulePkg, + }, + expected: []string{}, + }, } for _, test := range tests { @@ -577,3 +606,95 @@ func TestCandidateTargetSoftwareAttrs(t *testing.T) { }) } } + +func TestCandidateProductForGo(t *testing.T) { + tests := []struct { + pkg string + expected string + }{ + { + pkg: "github.com/someone/something", + expected: "something", + }, + { + pkg: "golang.org/x/xerrors", + expected: "x/xerrors", + }, + { + pkg: "gopkg.in/yaml.v2", + expected: "yaml.v2", + }, + { + pkg: "place", + expected: "", + }, + { + pkg: "place.com/", + expected: "", + }, + { + pkg: "place.com/someone-or-thing", + expected: "", + }, + { + pkg: "google.golang.org/genproto/googleapis/rpc/status", + expected: "genproto", + }, + { + pkg: "github.com/someone/something/long/package/name", + expected: "something", + }, + } + + for _, test := range tests { + t.Run(test.pkg, func(t *testing.T) { + assert.Equal(t, test.expected, candidateProductForGo(test.pkg)) + }) + } +} + +func TestCandidateVendorForGo(t *testing.T) { + tests := []struct { + pkg string + expected string + }{ + { + pkg: "github.com/someone/something", + expected: "someone", + }, + { + pkg: "golang.org/x/xerrors", + expected: "golang", + }, + { + pkg: "gopkg.in/yaml.v2", + expected: "", + }, + { + pkg: "place", + expected: "", + }, + { + pkg: "place.com/", + expected: "", + }, + { + pkg: "place.com/someone-or-thing", + expected: "", + }, + { + pkg: "google.golang.org/genproto/googleapis/rpc/status", + expected: "google", + }, + { + pkg: "github.com/someone/something/long/package/name", + expected: "someone", + }, + } + + for _, test := range tests { + t.Run(test.pkg, func(t *testing.T) { + assert.Equal(t, test.expected, candidateVendorForGo(test.pkg)) + }) + } +} diff --git a/test/cli/utils_test.go b/test/cli/utils_test.go index 4de1f70a4bd..a651dd7382c 100644 --- a/test/cli/utils_test.go +++ b/test/cli/utils_test.go @@ -48,7 +48,7 @@ func getSyftCommand(t testing.TB, args ...string) *exec.Cmd { // SYFT_BINARY_LOCATION is the absolute path to the snapshot binary binaryLocation = os.Getenv("SYFT_BINARY_LOCATION") } else { - //note: there is a subtle - vs _ difference between these versions + // note: there is a subtle - vs _ difference between these versions switch runtime.GOOS { case "darwin": binaryLocation = path.Join(repoRoot(t), fmt.Sprintf("snapshot/syft-macos_darwin_%s/syft", runtime.GOARCH)) @@ -60,8 +60,6 @@ func getSyftCommand(t testing.TB, args ...string) *exec.Cmd { } - t.Log(binaryLocation, args) - return exec.Command(binaryLocation, args...) } From f8755d95be0916e2db4069b05bebf3fbaad47748 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 23 Jun 2021 11:23:14 -0400 Subject: [PATCH 10/13] use tag-value format as default "spdx" format flavor Signed-off-by: Alex Goodman --- syft/presenter/packages/presenter_option.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/syft/presenter/packages/presenter_option.go b/syft/presenter/packages/presenter_option.go index ee3603853c3..7bdcbc07572 100644 --- a/syft/presenter/packages/presenter_option.go +++ b/syft/presenter/packages/presenter_option.go @@ -33,7 +33,7 @@ func ParsePresenterOption(userStr string) PresenterOption { return TablePresenterOption case string(CycloneDxPresenterOption), "cyclone", "cyclone-dx": return CycloneDxPresenterOption - case string(SPDXTagValuePresenterOption), "spdx-tagvalue", "spdxtagvalue", "spdx-tv": + case string(SPDXTagValuePresenterOption), "spdx", "spdx-tagvalue", "spdxtagvalue", "spdx-tv": return SPDXTagValuePresenterOption case string(SPDXJSONPresenterOption), "spdxjson": return SPDXJSONPresenterOption From 421f6027aa9fd6a4514dc6741fa9a07d286f1fb5 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 23 Jun 2021 13:51:03 -0400 Subject: [PATCH 11/13] add tests around spdx presenters + refactor presenter tests Signed-off-by: Alex Goodman --- .../packages/cyclonedx_presenter_test.go | 171 +----- .../presenter/packages/json_presenter_test.go | 172 +----- .../packages/model/spdx22/package.go | 10 +- internal/presenter/packages/spdx_helpers.go | 6 +- .../presenter/packages/spdx_helpers_test.go | 518 ++++++++++++++++++ .../packages/spdx_json_presenter_test.go | 33 ++ .../packages/spdx_tag_value_presenter_test.go | 33 ++ .../packages/table_presenter_test.go | 62 +-- ...=> TestCycloneDxDirectoryPresenter.golden} | 19 +- ...den => TestCycloneDxImagePresenter.golden} | 21 +- ...lden => TestJSONDirectoryPresenter.golden} | 6 +- ...r.golden => TestJSONImagePresenter.golden} | 6 +- .../TestSPDXJSONDirectoryPresenter.golden | 61 +++ .../TestSPDXJSONImagePresenter.golden | 61 +++ .../TestSPDXTagValueDirectoryPresenter.golden | 35 ++ .../TestSPDXTagValueImagePresenter.golden | 36 ++ .../snapshot/TestTablePresenter.golden | 6 +- ...lden => TestTextDirectoryPresenter.golden} | 6 +- .../snapshot/TestTextImagePresenter.golden | 21 + .../snapshot/TestTextImgPresenter.golden | 21 - .../snapshot/TestTextPresenter.golden | 11 - .../presenter/packages/text_presenter_test.go | 129 +---- internal/presenter/packages/utils_test.go | 194 +++++++ 23 files changed, 1082 insertions(+), 556 deletions(-) create mode 100644 internal/presenter/packages/spdx_helpers_test.go create mode 100644 internal/presenter/packages/spdx_json_presenter_test.go create mode 100644 internal/presenter/packages/spdx_tag_value_presenter_test.go rename internal/presenter/packages/test-fixtures/snapshot/{TestCycloneDxDirsPresenter.golden => TestCycloneDxDirectoryPresenter.golden} (74%) rename internal/presenter/packages/test-fixtures/snapshot/{TestCycloneDxImgsPresenter.golden => TestCycloneDxImagePresenter.golden} (71%) rename internal/presenter/packages/test-fixtures/snapshot/{TestJSONDirsPresenter.golden => TestJSONDirectoryPresenter.golden} (95%) rename internal/presenter/packages/test-fixtures/snapshot/{TestJSONImgsPresenter.golden => TestJSONImagePresenter.golden} (98%) create mode 100644 internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden create mode 100644 internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden create mode 100644 internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueDirectoryPresenter.golden create mode 100644 internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueImagePresenter.golden rename internal/presenter/packages/test-fixtures/snapshot/{TestTextDirPresenter.golden => TestTextDirectoryPresenter.golden} (56%) create mode 100644 internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden delete mode 100644 internal/presenter/packages/test-fixtures/snapshot/TestTextImgPresenter.golden delete mode 100644 internal/presenter/packages/test-fixtures/snapshot/TestTextPresenter.golden create mode 100644 internal/presenter/packages/utils_test.go diff --git a/internal/presenter/packages/cyclonedx_presenter_test.go b/internal/presenter/packages/cyclonedx_presenter_test.go index 46d3364f4ee..4914edbb445 100644 --- a/internal/presenter/packages/cyclonedx_presenter_test.go +++ b/internal/presenter/packages/cyclonedx_presenter_test.go @@ -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]))`) diff --git a/internal/presenter/packages/json_presenter_test.go b/internal/presenter/packages/json_presenter_test.go index 582efdbe07a..9620c2da725 100644 --- a/internal/presenter/packages/json_presenter_test.go +++ b/internal/presenter/packages/json_presenter_test.go @@ -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, + ) } diff --git a/internal/presenter/packages/model/spdx22/package.go b/internal/presenter/packages/model/spdx22/package.go index aa1ac60564f..b35b83c2fd2 100644 --- a/internal/presenter/packages/model/spdx22/package.go +++ b/internal/presenter/packages/model/spdx22/package.go @@ -20,9 +20,13 @@ type Package struct { // 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"` - Homepage string `json:"homepage,omitempty"` - LicenseDeclared string `json:"licenseDeclared"` + 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"` diff --git a/internal/presenter/packages/spdx_helpers.go b/internal/presenter/packages/spdx_helpers.go index dda1294d5a6..a847a2430d0 100644 --- a/internal/presenter/packages/spdx_helpers.go +++ b/internal/presenter/packages/spdx_helpers.go @@ -13,7 +13,6 @@ func getSPDXExternalRefs(p *pkg.Package) (externalRefs []spdx22.ExternalRef) { externalRefs = make([]spdx22.ExternalRef, 0) for _, c := range p.CPEs { externalRefs = append(externalRefs, spdx22.ExternalRef{ - Comment: "", ReferenceCategory: spdx22.SecurityReferenceCategory, ReferenceLocator: c.BindToFmtString(), ReferenceType: spdx22.Cpe23ExternalRefType, @@ -22,7 +21,6 @@ func getSPDXExternalRefs(p *pkg.Package) (externalRefs []spdx22.ExternalRef) { if p.PURL != "" { externalRefs = append(externalRefs, spdx22.ExternalRef{ - Comment: "", ReferenceCategory: spdx22.PackageManagerReferenceCategory, ReferenceLocator: p.PURL, ReferenceType: spdx22.PurlExternalRefType, @@ -61,7 +59,7 @@ func getSPDXLicense(p *pkg.Package) string { } func noneIfEmpty(value string) string { - if value == "" { + if strings.TrimSpace(value) == "" { return "NONE" } return value @@ -115,7 +113,7 @@ func getSPDXSourceInfo(p *pkg.Package) string { case pkg.GemPkg: answer = "acquired package info from installed gem metadata file" case pkg.GoModulePkg: - answer = "acquired package info from go module metadata file" + answer = "acquired package info from go module information" case pkg.RustPkg: answer = "acquired package info from rust cargo manifest" default: diff --git a/internal/presenter/packages/spdx_helpers_test.go b/internal/presenter/packages/spdx_helpers_test.go new file mode 100644 index 00000000000..8bca4b2b828 --- /dev/null +++ b/internal/presenter/packages/spdx_helpers_test.go @@ -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 ", + }, + { + 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)) + }) + } +} diff --git a/internal/presenter/packages/spdx_json_presenter_test.go b/internal/presenter/packages/spdx_json_presenter_test.go new file mode 100644 index 00000000000..4e750ed4016 --- /dev/null +++ b/internal/presenter/packages/spdx_json_presenter_test.go @@ -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")) +} diff --git a/internal/presenter/packages/spdx_tag_value_presenter_test.go b/internal/presenter/packages/spdx_tag_value_presenter_test.go new file mode 100644 index 00000000000..7cd81cdf3c0 --- /dev/null +++ b/internal/presenter/packages/spdx_tag_value_presenter_test.go @@ -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")) +} diff --git a/internal/presenter/packages/table_presenter_test.go b/internal/presenter/packages/table_presenter_test.go index 54e1db38e7d..7d59fb1badb 100644 --- a/internal/presenter/packages/table_presenter_test.go +++ b/internal/presenter/packages/table_presenter_test.go @@ -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) { diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxDirsPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxDirectoryPresenter.golden similarity index 74% rename from internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxDirsPresenter.golden rename to internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxDirectoryPresenter.golden index 5c3da3632d5..5c473495a7a 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxDirsPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxDirectoryPresenter.golden @@ -1,7 +1,7 @@ - + - 2020-12-28T13:56:29-05:00 + 2021-06-23T13:40:33-04:00 anchore @@ -16,20 +16,19 @@ - package1 + package-1 1.0.1 - - - package2 - 2.0.1 MIT - - Apache-v2 - + a-purl-2 + + + package-2 + 2.0.1 + a-purl-2 diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxImagePresenter.golden similarity index 71% rename from internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden rename to internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxImagePresenter.golden index fd72a5ce870..d81de3a3205 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxImgsPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestCycloneDxImagePresenter.golden @@ -1,7 +1,7 @@ - + - 2020-12-28T13:56:29-05:00 + 2021-06-23T13:40:33-04:00 anchore @@ -16,22 +16,19 @@ - package1 + package-1 1.0.1 - the-purl-1 - - - package2 - 2.0.1 MIT - - Apache-v2 - - the-purl-2 + a-purl-1 + + + package-2 + 2.0.1 + a-purl-2 diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirsPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirectoryPresenter.golden similarity index 95% rename from internal/presenter/packages/test-fixtures/snapshot/TestJSONDirsPresenter.golden rename to internal/presenter/packages/test-fixtures/snapshot/TestJSONDirectoryPresenter.golden index 6f2217d0f59..5a58549c995 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirsPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestJSONDirectoryPresenter.golden @@ -66,9 +66,9 @@ "target": "/some/path" }, "distro": { - "name": "", - "version": "", - "idLike": "" + "name": "debian", + "version": "1.2.3", + "idLike": "like!" }, "descriptor": { "name": "syft", diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestJSONImgsPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestJSONImagePresenter.golden similarity index 98% rename from internal/presenter/packages/test-fixtures/snapshot/TestJSONImgsPresenter.golden rename to internal/presenter/packages/test-fixtures/snapshot/TestJSONImagePresenter.golden index 2efd1f9c89e..092d50b3eb8 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestJSONImgsPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestJSONImagePresenter.golden @@ -93,9 +93,9 @@ } }, "distro": { - "name": "", - "version": "", - "idLike": "" + "name": "debian", + "version": "1.2.3", + "idLike": "like!" }, "descriptor": { "name": "syft", diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden new file mode 100644 index 00000000000..169f9bd8c39 --- /dev/null +++ b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONDirectoryPresenter.golden @@ -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" + } + ] +} diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden new file mode 100644 index 00000000000..f9dc50d19dd --- /dev/null +++ b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXJSONImagePresenter.golden @@ -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" + } + ] +} diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueDirectoryPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueDirectoryPresenter.golden new file mode 100644 index 00000000000..f0ef0bf52cb --- /dev/null +++ b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueDirectoryPresenter.golden @@ -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 + diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueImagePresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueImagePresenter.golden new file mode 100644 index 00000000000..54bfd6d9492 --- /dev/null +++ b/internal/presenter/packages/test-fixtures/snapshot/TestSPDXTagValueImagePresenter.golden @@ -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 + diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestTablePresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestTablePresenter.golden index 66d2408f957..2094e69e0e5 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestTablePresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestTablePresenter.golden @@ -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 diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestTextDirPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestTextDirectoryPresenter.golden similarity index 56% rename from internal/presenter/packages/test-fixtures/snapshot/TestTextDirPresenter.golden rename to internal/presenter/packages/test-fixtures/snapshot/TestTextDirectoryPresenter.golden index f41bf461de1..25881f2d952 100644 --- a/internal/presenter/packages/test-fixtures/snapshot/TestTextDirPresenter.golden +++ b/internal/presenter/packages/test-fixtures/snapshot/TestTextDirectoryPresenter.golden @@ -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 diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden new file mode 100644 index 00000000000..4ab3a446e0c --- /dev/null +++ b/internal/presenter/packages/test-fixtures/snapshot/TestTextImagePresenter.golden @@ -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 + diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestTextImgPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestTextImgPresenter.golden deleted file mode 100644 index 0d11efbc13d..00000000000 --- a/internal/presenter/packages/test-fixtures/snapshot/TestTextImgPresenter.golden +++ /dev/null @@ -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 - diff --git a/internal/presenter/packages/test-fixtures/snapshot/TestTextPresenter.golden b/internal/presenter/packages/test-fixtures/snapshot/TestTextPresenter.golden deleted file mode 100644 index f41bf461de1..00000000000 --- a/internal/presenter/packages/test-fixtures/snapshot/TestTextPresenter.golden +++ /dev/null @@ -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: - diff --git a/internal/presenter/packages/text_presenter_test.go b/internal/presenter/packages/text_presenter_test.go index 3633149cbfd..2a2164285ea 100644 --- a/internal/presenter/packages/text_presenter_test.go +++ b/internal/presenter/packages/text_presenter_test.go @@ -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, + ) } diff --git a/internal/presenter/packages/utils_test.go b/internal/presenter/packages/utils_test.go new file mode 100644 index 00000000000..c23d4caf827 --- /dev/null +++ b/internal/presenter/packages/utils_test.go @@ -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 +} From 1ae60e72bb16ef4aae5a2b473ace3c1ab5e10777 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 23 Jun 2021 15:04:01 -0400 Subject: [PATCH 12/13] add bouncer exception for spdx tools-golang repo Signed-off-by: Alex Goodman --- .bouncer.yaml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.bouncer.yaml b/.bouncer.yaml index 119d1c7ce91..bc48f739373 100644 --- a/.bouncer.yaml +++ b/.bouncer.yaml @@ -6,4 +6,12 @@ permit: - ISC ignore-packages: # packageurl-go is released under the MIT license located in the root of the repo at /mit.LICENSE - - github.com/package-url/packageurl-go \ No newline at end of file + - github.com/package-url/packageurl-go + + # from: https://github.com/spdx/tools-golang/blob/main/LICENSE.code + # The tools-golang source code is provided and may be used, at your option, + # under either: + # * Apache License, version 2.0 (Apache-2.0), OR + # * GNU General Public License, version 2.0 or later (GPL-2.0-or-later). + # (we choose Apache-2.0) + - github.com/spdx/tools-golang \ No newline at end of file From af8318c2db611780bcd7a97fb791579766d761d2 Mon Sep 17 00:00:00 2001 From: Alex Goodman Date: Wed, 23 Jun 2021 15:15:08 -0400 Subject: [PATCH 13/13] remove spdx model questions Signed-off-by: Alex Goodman --- internal/presenter/packages/spdx_json_presenter.go | 1 - internal/presenter/packages/spdx_tag_value_presenter.go | 1 - 2 files changed, 2 deletions(-) diff --git a/internal/presenter/packages/spdx_json_presenter.go b/internal/presenter/packages/spdx_json_presenter.go index 0fcfb4cc762..da90c362651 100644 --- a/internal/presenter/packages/spdx_json_presenter.go +++ b/internal/presenter/packages/spdx_json_presenter.go @@ -50,7 +50,6 @@ func newSPDXJsonDocument(catalog *pkg.Catalog, srcMetadata source.Metadata) spdx return spdx22.Document{ Element: spdx22.Element{ - // should this be unique to the user's input? or otherwise just say document? SPDXID: spdx22.ElementID("DOCUMENT").String(), Name: name, }, diff --git a/internal/presenter/packages/spdx_tag_value_presenter.go b/internal/presenter/packages/spdx_tag_value_presenter.go index 6a46c16970d..5f66fdf4aa9 100644 --- a/internal/presenter/packages/spdx_tag_value_presenter.go +++ b/internal/presenter/packages/spdx_tag_value_presenter.go @@ -67,7 +67,6 @@ func (pres *SPDXTagValuePresenter) Present(output io.Writer) error { // In many cases, the URI will point to a web accessible document, but this should not be assumed // to be the case. - // TODO: rethink this DocumentNamespace: fmt.Sprintf("https://anchore.com/syft/image/%s", pres.srcMetadata.ImageMetadata.UserInput), // 2.6: External Document References