Add downloadLocation URI validation (#3697)

* Add downloadLocation URI validation

Signed-off-by: Stef Graces <stefgraces@hotmail.com>

* Update function names

Signed-off-by: Stef Graces <stefgraces@hotmail.com>

* Fixes for make lint-fix + Changes to when NONE and NOASSERTION in downloadLocation

Signed-off-by: Stef Graces <stefgraces@hotmail.com>

---------

Signed-off-by: Stef Graces <stefgraces@hotmail.com>
Co-authored-by: Keith Zantow <kzantow@gmail.com>
This commit is contained in:
Stef Graces 2025-03-06 15:45:47 +01:00 committed by GitHub
parent 974ce23722
commit 694eec4079
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 574 additions and 65 deletions

View File

@ -61,6 +61,12 @@ linters-settings:
gosec: gosec:
excludes: excludes:
- G115 - G115
staticcheck:
checks:
- all
- -SA4023
run: run:
timeout: 10m timeout: 10m
tests: false tests: false

1
go.mod
View File

@ -93,6 +93,7 @@ require (
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
github.com/hashicorp/hcl/v2 v2.23.0 github.com/hashicorp/hcl/v2 v2.23.0
github.com/magiconair/properties v1.8.9 github.com/magiconair/properties v1.8.9
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56
) )

1
go.sum
View File

@ -736,6 +736,7 @@ github.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQ
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb h1:bLo8hvc8XFm9J47r690TUKBzcjSWdJDxmjXJZ+/f92U=
github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM= github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsSAXRurWqqrdyZen1R7kxl8TkmWk2OyM=
github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk= github.com/spdx/tools-golang v0.5.5 h1:61c0KLfAcNqAjlg6UNMdkwpMernhw3zVRwDZ2x9XOmk=
github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE= github.com/spdx/tools-golang v0.5.5/go.mod h1:MVIsXx8ZZzaRWNQpUDhC4Dud34edUYJYecciXgrw5vE=

View File

@ -1,6 +1,12 @@
package helpers package helpers
import "github.com/anchore/syft/syft/pkg" import (
"strings"
urilib "github.com/spdx/gordf/uri"
"github.com/anchore/syft/syft/pkg"
)
const NONE = "NONE" const NONE = "NONE"
const NOASSERTION = "NOASSERTION" const NOASSERTION = "NOASSERTION"
@ -14,21 +20,37 @@ func DownloadLocation(p pkg.Package) string {
// (ii) the SPDX file creator has made no attempt to determine this field; or // (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). // (iii) the SPDX file creator has intentionally provided no information (no meaning should be implied by doing so).
var location string
if hasMetadata(p) { if hasMetadata(p) {
switch metadata := p.Metadata.(type) { switch metadata := p.Metadata.(type) {
case pkg.ApkDBEntry: case pkg.ApkDBEntry:
return NoneIfEmpty(metadata.URL) location = metadata.URL
case pkg.NpmPackage: case pkg.NpmPackage:
return NoneIfEmpty(metadata.URL) location = metadata.URL
case pkg.NpmPackageLockEntry: case pkg.NpmPackageLockEntry:
return NoneIfEmpty(metadata.Resolved) location = metadata.Resolved
case pkg.PhpComposerLockEntry: case pkg.PhpComposerLockEntry:
return NoneIfEmpty(metadata.Dist.URL) location = metadata.Dist.URL
case pkg.PhpComposerInstalledEntry: case pkg.PhpComposerInstalledEntry:
return NoneIfEmpty(metadata.Dist.URL) location = metadata.Dist.URL
case pkg.OpamPackage: case pkg.OpamPackage:
return NoneIfEmpty(metadata.URL) location = metadata.URL
} }
} }
return URIValue(location)
}
func isURIValid(uri string) bool {
_, err := urilib.NewURIRef(uri)
return err == nil
}
func URIValue(uri string) string {
if strings.ToLower(uri) != "none" {
if isURIValid(uri) {
return uri
}
return NOASSERTION return NOASSERTION
}
return NONE
} }

View File

@ -44,7 +44,7 @@ func Test_DownloadLocation(t *testing.T) {
URL: "", URL: "",
}, },
}, },
expected: NONE, expected: NOASSERTION,
}, },
{ {
name: "from npm package-lock should include resolved", name: "from npm package-lock should include resolved",
@ -62,7 +62,7 @@ func Test_DownloadLocation(t *testing.T) {
Resolved: "", Resolved: "",
}, },
}, },
expected: NONE, expected: NOASSERTION,
}, },
{ {
name: "from php installed.json", name: "from php installed.json",
@ -84,7 +84,7 @@ func Test_DownloadLocation(t *testing.T) {
}, },
}, },
}, },
expected: "NONE", expected: NOASSERTION,
}, },
{ {
name: "from php composer.lock", name: "from php composer.lock",
@ -106,7 +106,539 @@ func Test_DownloadLocation(t *testing.T) {
}, },
}, },
}, },
expected: "NONE", expected: NOASSERTION,
},
{
name: "none",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "none",
},
},
},
expected: NONE,
},
{
name: "none uppercase",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "NONE",
},
},
},
expected: NONE,
},
{
name: "invalid uri",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "example/package",
},
},
},
expected: NOASSERTION,
},
{
name: "Basic Git Protocol URL",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git://git.myproject.org/MyProject",
},
},
},
expected: "git://git.myproject.org/MyProject",
},
{
name: "Git HTTPS URL with .git Extension",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+https://git.myproject.org/MyProject.git",
},
},
},
expected: "git+https://git.myproject.org/MyProject.git",
},
{
name: "Git HTTP URL",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+http://git.myproject.org/MyProject",
},
},
},
expected: "git+http://git.myproject.org/MyProject",
},
{
name: "Git SSH URL with .git Extension",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+ssh://git.myproject.org/MyProject.git",
},
},
},
expected: "git+ssh://git.myproject.org/MyProject.git",
},
{
name: "Git Protocol with Prefix",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+git://git.myproject.org/MyProject",
},
},
},
expected: "git+git://git.myproject.org/MyProject",
},
{
name: "Git URL with C File Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git://git.myproject.org/MyProject#src/somefile.c",
},
},
},
expected: "git://git.myproject.org/MyProject#src/somefile.c",
},
{
name: "Git HTTPS URL with Java File Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+https://git.myproject.org/MyProject#src/Class.java",
},
},
},
expected: "git+https://git.myproject.org/MyProject#src/Class.java",
},
{
name: "Git URL with Master Branch",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git://git.myproject.org/MyProject.git@master",
},
},
},
expected: "git://git.myproject.org/MyProject.git@master",
},
{
name: "Git HTTPS URL with Version Tag",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+https://git.myproject.org/MyProject.git@v1.0",
},
},
},
expected: "git+https://git.myproject.org/MyProject.git@v1.0",
},
{
name: "Git URL with Full Commit Hash",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git://git.myproject.org/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709",
},
},
},
expected: "git://git.myproject.org/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709",
},
{
name: "Git HTTPS URL with Branch and CPP File Path",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+https://git.myproject.org/MyProject.git@master#/src/MyClass.cpp",
},
},
},
expected: "git+https://git.myproject.org/MyProject.git@master#/src/MyClass.cpp",
},
{
name: "Git HTTPS URL with Commit Hash and Ruby File",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "git+https://git.myproject.org/MyProject@da39a3ee5e6b4b0d3255bfef95601890afd80709#lib/variable.rb",
},
},
},
expected: "git+https://git.myproject.org/MyProject@da39a3ee5e6b4b0d3255bfef95601890afd80709#lib/variable.rb",
},
{
name: "Basic Mercurial HTTP URL",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+http://hg.myproject.org/MyProject",
},
},
},
expected: "hg+http://hg.myproject.org/MyProject",
},
{
name: "Basic Mercurial HTTPS URL",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject",
},
{
name: "Basic Mercurial SSH URL",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+ssh://hg.myproject.org/MyProject",
},
},
},
expected: "hg+ssh://hg.myproject.org/MyProject",
},
{
name: "Mercurial URL with File Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject#src/somefile.c",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject#src/somefile.c",
},
{
name: "Mercurial URL with Java Class Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject#src/Class.java",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject#src/Class.java",
},
{
name: "Mercurial URL with Commit Hash",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject@da39a3ee5e6b",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject@da39a3ee5e6b",
},
{
name: "Mercurial URL with Year Reference",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject@2019",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject@2019",
},
{
name: "Mercurial URL with Version Tag",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject@v1.0",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject@v1.0",
},
{
name: "Mercurial URL with Feature Branch",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject@special_feature",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject@special_feature",
},
{
name: "Mercurial URL with Branch and File Path",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject@master#/src/MyClass.cpp",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject@master#/src/MyClass.cpp",
},
{
name: "Mercurial URL with Commit Hash and Ruby File",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "hg+https://hg.myproject.org/MyProject@da39a3ee5e6b#lib/variable.rb",
},
},
},
expected: "hg+https://hg.myproject.org/MyProject@da39a3ee5e6b#lib/variable.rb",
},
// Test cases for Subversion (svn) URLs
{
name: "Basic SVN URL",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn://svn.myproject.org/svn/MyProject",
},
},
},
expected: "svn://svn.myproject.org/svn/MyProject",
},
{
name: "SVN URL with Protocol Prefix",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+svn://svn.myproject.org/svn/MyProject",
},
},
},
expected: "svn+svn://svn.myproject.org/svn/MyProject",
},
{
name: "SVN HTTP URL with Trunk",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+http://svn.myproject.org/svn/MyProject/trunk",
},
},
},
expected: "svn+http://svn.myproject.org/svn/MyProject/trunk",
},
{
name: "SVN HTTPS URL with Trunk",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/svn/MyProject/trunk",
},
},
},
expected: "svn+https://svn.myproject.org/svn/MyProject/trunk",
},
{
name: "SVN URL with C File Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/MyProject#src/somefile.c",
},
},
},
expected: "svn+https://svn.myproject.org/MyProject#src/somefile.c",
},
{
name: "SVN URL with Java Class Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/MyProject#src/Class.java",
},
},
},
expected: "svn+https://svn.myproject.org/MyProject#src/Class.java",
},
{
name: "SVN URL with Trunk and C File Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/MyProject/trunk#src/somefile.c",
},
},
},
expected: "svn+https://svn.myproject.org/MyProject/trunk#src/somefile.c",
},
{
name: "SVN URL with Full File Path",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/MyProject/trunk/src/somefile.c",
},
},
},
expected: "svn+https://svn.myproject.org/MyProject/trunk/src/somefile.c",
},
{
name: "SVN URL with Revision Number",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/svn/MyProject/trunk@2019",
},
},
},
expected: "svn+https://svn.myproject.org/svn/MyProject/trunk@2019",
},
{
name: "SVN URL with Revision and CPP File Path",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/MyProject@123#/src/MyClass.cpp",
},
},
},
expected: "svn+https://svn.myproject.org/MyProject@123#/src/MyClass.cpp",
},
{
name: "SVN URL with Trunk, Revision and Ruby File Path",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "svn+https://svn.myproject.org/MyProject/trunk@1234#lib/variable/variable.rb",
},
},
},
expected: "svn+https://svn.myproject.org/MyProject/trunk@1234#lib/variable/variable.rb",
},
// Test cases for Bazaar (bzr) URLs
{
name: "Bazaar HTTPS URL with Trunk",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+https://bzr.myproject.org/MyProject/trunk",
},
},
},
expected: "bzr+https://bzr.myproject.org/MyProject/trunk",
},
{
name: "Bazaar HTTP URL with Trunk",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+http://bzr.myproject.org/MyProject/trunk",
},
},
},
expected: "bzr+http://bzr.myproject.org/MyProject/trunk",
},
{
name: "Bazaar SFTP URL with Trunk",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+sftp://myproject.org/MyProject/trunk",
},
},
},
expected: "bzr+sftp://myproject.org/MyProject/trunk",
},
{
name: "Bazaar SSH URL with Trunk",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+ssh://myproject.org/MyProject/trunk",
},
},
},
expected: "bzr+ssh://myproject.org/MyProject/trunk",
},
{
name: "Bazaar FTP URL with Trunk",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+ftp://myproject.org/MyProject/trunk",
},
},
},
expected: "bzr+ftp://myproject.org/MyProject/trunk",
},
{
name: "Bazaar Launchpad URL",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+lp:MyProject",
},
},
},
expected: "bzr+lp:MyProject",
},
{
name: "Bazaar URL with C File Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+https://bzr.myproject.org/MyProject/trunk#src/somefile.c",
},
},
},
expected: "bzr+https://bzr.myproject.org/MyProject/trunk#src/somefile.c",
},
{
name: "Bazaar URL with Java Class Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+https://bzr.myproject.org/MyProject/trunk#src/Class.java",
},
},
},
expected: "bzr+https://bzr.myproject.org/MyProject/trunk#src/Class.java",
},
{
name: "Bazaar URL with Revision",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+https://bzr.myproject.org/MyProject/trunk@2019",
},
},
},
expected: "bzr+https://bzr.myproject.org/MyProject/trunk@2019",
},
{
name: "Bazaar URL with Version Tag",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+http://bzr.myproject.org/MyProject/trunk@v1.0",
},
},
},
expected: "bzr+http://bzr.myproject.org/MyProject/trunk@v1.0",
},
{
name: "Bazaar URL with Revision and C File Fragment",
input: pkg.Package{
Metadata: pkg.PhpComposerLockEntry{
Dist: pkg.PhpComposerExternalReference{
URL: "bzr+https://bzr.myproject.org/MyProject/trunk@2019#src/somefile.c",
},
},
},
expected: "bzr+https://bzr.myproject.org/MyProject/trunk@2019#src/somefile.c",
}, },
} }
for _, test := range tests { for _, test := range tests {

View File

@ -1,12 +0,0 @@
package helpers
import (
"strings"
)
func NoneIfEmpty(value string) string {
if strings.TrimSpace(value) == "" {
return NONE
}
return value
}

View File

@ -1,41 +0,0 @@
package helpers
import (
"testing"
"github.com/stretchr/testify/assert"
)
func Test_noneIfEmpty(t *testing.T) {
tests := []struct {
name string
value string
expected string
}{
{
name: "non-zero value",
value: "something",
expected: "something",
},
{
name: "empty",
value: "",
expected: NONE,
},
{
name: "space",
value: " ",
expected: NONE,
},
{
name: "tab",
value: "\t",
expected: NONE,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.expected, NoneIfEmpty(test.value))
})
}
}