mirror of
https://github.com/anchore/syft.git
synced 2026-10-12 22:27:22 +02:00
Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91c5c850ea | ||
|
|
42c6259d4b | ||
|
|
abed9f2d5f | ||
|
|
e4f4aa2956 | ||
|
|
0afe26152f | ||
|
|
dd0e7dc20f | ||
|
|
e5fd03d2f6 |
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/spf13/cobra"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/clio"
|
||||
"github.com/anchore/fangs"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
_ "modernc.org/sqlite" // required for rpmdb and other features
|
||||
|
||||
"github.com/anchore/syft/syft"
|
||||
|
||||
@@ -24,7 +24,7 @@ require (
|
||||
github.com/anchore/go-testutils v0.0.0-20200925183923-d5f45b0d3c04
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115
|
||||
github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385
|
||||
github.com/anchore/stereoscope v0.1.8
|
||||
github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be
|
||||
github.com/aquasecurity/go-pep440-version v0.0.1
|
||||
github.com/bitnami/go-version v0.0.0-20250131085805-b1f57a8634ef
|
||||
@@ -88,10 +88,10 @@ require (
|
||||
github.com/xeipuuv/gojsonschema v1.2.0
|
||||
github.com/zyedidia/generic v1.2.2-0.20230320175451-4410d2372cb1
|
||||
go.uber.org/goleak v1.3.0
|
||||
go.yaml.in/yaml/v3 v3.0.4
|
||||
golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
|
||||
golang.org/x/mod v0.27.0
|
||||
golang.org/x/net v0.43.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.38.2
|
||||
)
|
||||
|
||||
@@ -132,7 +132,7 @@ require (
|
||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/containerd/cgroups v1.1.0 // indirect
|
||||
github.com/containerd/containerd v1.7.28 // indirect
|
||||
github.com/containerd/containerd v1.7.27 // indirect
|
||||
github.com/containerd/containerd/api v1.8.0 // indirect
|
||||
github.com/containerd/continuity v0.4.4 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
@@ -149,7 +149,7 @@ require (
|
||||
github.com/docker/distribution v2.8.3+incompatible // indirect
|
||||
github.com/docker/docker v28.3.3+incompatible // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.3 // indirect
|
||||
github.com/docker/go-connections v0.6.0 // indirect
|
||||
github.com/docker/go-connections v0.5.0 // indirect
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
@@ -273,8 +273,8 @@ require (
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.34.0 // indirect
|
||||
golang.org/x/text v0.28.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/tools v0.35.0
|
||||
golang.org/x/time v0.7.0 // indirect
|
||||
golang.org/x/tools v0.35.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/api v0.203.0 // indirect
|
||||
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697 // indirect
|
||||
@@ -283,7 +283,6 @@ require (
|
||||
google.golang.org/grpc v1.67.3 // indirect
|
||||
google.golang.org/protobuf v1.36.4 // indirect
|
||||
gopkg.in/warnings.v0 v0.1.2 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
modernc.org/libc v1.66.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
|
||||
@@ -694,8 +694,8 @@ github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b h1:e1bmaoJfZV
|
||||
github.com/anchore/go-version v1.2.2-0.20200701162849-18adb9c92b9b/go.mod h1:Bkc+JYWjMCF8OyZ340IMSIi2Ebf3uwByOk6ho4wne1E=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115 h1:ZyRCmiEjnoGJZ1+Ah0ZZ/mKKqNhGcUZBl0s7PTTDzvY=
|
||||
github.com/anchore/packageurl-go v0.1.1-0.20250220190351-d62adb6e1115/go.mod h1:KoYIv7tdP5+CC9VGkeZV4/vGCKsY55VvoG+5dadg4YI=
|
||||
github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385 h1:icCqbvAKGZXf29lEi8JmwvHVCBCYkiyZMuSnk+5ajYo=
|
||||
github.com/anchore/stereoscope v0.1.9-0.20250826202322-ef061ea78385/go.mod h1:0UCjLz5MdPNiH9F0h2tSNf3yGF6/MnK8ZCPo0YfDQVc=
|
||||
github.com/anchore/stereoscope v0.1.8 h1:K1NRFUqGy6z/jp4ADHNXZWbZ+xd6fPr0Kn4gYp1zm1c=
|
||||
github.com/anchore/stereoscope v0.1.8/go.mod h1:VA9zyFcUzN7GIFsXfe8lj3Z6Ocs4CP5QZqbmFc1I7ag=
|
||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.2-0.20250424173009-453214e765f3 h1:8PmGpDEZl9yDpcdEr6Odf23feCxK3LNUNMxjXg41pZQ=
|
||||
@@ -812,8 +812,8 @@ github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWH
|
||||
github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs=
|
||||
github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM=
|
||||
github.com/containerd/cgroups v1.1.0/go.mod h1:6ppBcbh/NOOUU+dMKrykgaBnK9lCIBxHqJDGwsa1mIw=
|
||||
github.com/containerd/containerd v1.7.28 h1:Nsgm1AtcmEh4AHAJ4gGlNSaKgXiNccU270Dnf81FQ3c=
|
||||
github.com/containerd/containerd v1.7.28/go.mod h1:azUkWcOvHrWvaiUjSQH0fjzuHIwSPg1WL5PshGP4Szs=
|
||||
github.com/containerd/containerd v1.7.27 h1:yFyEyojddO3MIGVER2xJLWoCIn+Up4GaHFquP7hsFII=
|
||||
github.com/containerd/containerd v1.7.27/go.mod h1:xZmPnl75Vc+BLGt4MIfu6bp+fy03gdHAn9bz+FreFR0=
|
||||
github.com/containerd/containerd/api v1.8.0 h1:hVTNJKR8fMc/2Tiw60ZRijntNMd1U+JVMyTRdsD2bS0=
|
||||
github.com/containerd/containerd/api v1.8.0/go.mod h1:dFv4lt6S20wTu/hMcP4350RL87qPWLVa/OHOwmmdnYc=
|
||||
github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII=
|
||||
@@ -865,8 +865,8 @@ github.com/docker/docker v28.3.3+incompatible h1:Dypm25kh4rmk49v1eiVbsAtpAsYURjY
|
||||
github.com/docker/docker v28.3.3+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8=
|
||||
github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo=
|
||||
github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94=
|
||||
github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE=
|
||||
github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c=
|
||||
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ+oDZB4KHQFypsfjYlq/C4rfL7D3g8=
|
||||
github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA=
|
||||
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
|
||||
@@ -1621,8 +1621,6 @@ go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9i
|
||||
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
|
||||
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
|
||||
go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
|
||||
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
@@ -1982,8 +1980,8 @@ golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxb
|
||||
golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.7.0 h1:ntUhktv3OPE6TgYxXWv9vKvUSJyIFJlyohwbkEwPrKQ=
|
||||
golang.org/x/time v0.7.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
|
||||
@@ -3,5 +3,5 @@ package internal
|
||||
const (
|
||||
// JSONSchemaVersion is the current schema version output by the JSON encoder
|
||||
// This is roughly following the "SchemaVer" guidelines for versioning the JSON schema. Please see schema/json/README.md for details on how to increment.
|
||||
JSONSchemaVersion = "16.0.39"
|
||||
JSONSchemaVersion = "16.0.38"
|
||||
)
|
||||
|
||||
@@ -126,6 +126,7 @@ func DefaultPackageTaskFactories() Factories {
|
||||
pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#",
|
||||
),
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetPackagesLockCataloger, pkgcataloging.DeclaredTag, pkgcataloging.ImageTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
|
||||
newSimplePackageTaskFactory(dotnet.NewDotnetCsprojCataloger, pkgcataloging.DeclaredTag, pkgcataloging.DirectoryTag, pkgcataloging.LanguageTag, "dotnet", "c#"),
|
||||
newSimplePackageTaskFactory(python.NewInstalledPackageCataloger, pkgcataloging.DirectoryTag, pkgcataloging.InstalledTag, pkgcataloging.ImageTag, pkgcataloging.LanguageTag, "python"),
|
||||
newPackageTaskFactory(
|
||||
func(cfg CatalogingFactoryConfig) pkg.Cataloger {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.39/document",
|
||||
"$id": "anchore.io/schema/syft/json/16.0.38/document",
|
||||
"$ref": "#/$defs/Document",
|
||||
"$defs": {
|
||||
"AlpmDbEntry": {
|
||||
@@ -1236,29 +1236,6 @@
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"GoSourceEntry": {
|
||||
"properties": {
|
||||
"h1Digest": {
|
||||
"type": "string"
|
||||
},
|
||||
"os": {
|
||||
"type": "string"
|
||||
},
|
||||
"architecture": {
|
||||
"type": "string"
|
||||
},
|
||||
"buildTags": {
|
||||
"type": "string"
|
||||
},
|
||||
"cgoEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
"required": [
|
||||
"cgoEnabled"
|
||||
]
|
||||
},
|
||||
"HaskellHackageStackEntry": {
|
||||
"properties": {
|
||||
"pkgHash": {
|
||||
@@ -2098,9 +2075,6 @@
|
||||
{
|
||||
"$ref": "#/$defs/GoModuleEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/GoSourceEntry"
|
||||
},
|
||||
{
|
||||
"$ref": "#/$defs/HaskellHackageStackEntry"
|
||||
},
|
||||
|
||||
@@ -28,7 +28,6 @@ func Test_OriginatorSupplier(t *testing.T) {
|
||||
pkg.ErlangRebarLockEntry{},
|
||||
pkg.GolangBinaryBuildinfoEntry{},
|
||||
pkg.GolangModuleEntry{},
|
||||
pkg.GolangSourceEntry{},
|
||||
pkg.HomebrewFormula{},
|
||||
pkg.HackageStackYamlLockEntry{},
|
||||
pkg.HackageStackYamlEntry{},
|
||||
|
||||
@@ -30,7 +30,6 @@ func AllTypes() []any {
|
||||
pkg.GitHubActionsUseStatement{},
|
||||
pkg.GolangBinaryBuildinfoEntry{},
|
||||
pkg.GolangModuleEntry{},
|
||||
pkg.GolangSourceEntry{},
|
||||
pkg.HackageStackYamlEntry{},
|
||||
pkg.HackageStackYamlLockEntry{},
|
||||
pkg.HomebrewFormula{},
|
||||
|
||||
@@ -82,7 +82,6 @@ var jsonTypes = makeJSONTypes(
|
||||
jsonNames(pkg.GitHubActionsUseStatement{}, "github-actions-use-statement"),
|
||||
jsonNames(pkg.GolangBinaryBuildinfoEntry{}, "go-module-buildinfo-entry", "GolangBinMetadata", "GolangMetadata"),
|
||||
jsonNames(pkg.GolangModuleEntry{}, "go-module-entry", "GolangModMetadata"),
|
||||
jsonNames(pkg.GolangSourceEntry{}, "go-source-entry"),
|
||||
jsonNames(pkg.HackageStackYamlLockEntry{}, "haskell-hackage-stack-lock-entry", "HackageMetadataType"),
|
||||
jsonNamesWithoutLookup(pkg.HackageStackYamlEntry{}, "haskell-hackage-stack-entry", "HackageMetadataType"), // the legacy value is split into two types, where the other is preferred
|
||||
jsonNames(pkg.JavaArchive{}, "java-archive", "JavaMetadata"),
|
||||
|
||||
@@ -347,12 +347,6 @@ func Test_JSONName_JSONLegacyName(t *testing.T) {
|
||||
expectedJSONName: "go-module-entry",
|
||||
expectedLegacyName: "GolangModMetadata",
|
||||
},
|
||||
{
|
||||
name: "GolangSourceMetadata",
|
||||
metadata: pkg.GolangSourceEntry{},
|
||||
expectedJSONName: "go-source-entry",
|
||||
expectedLegacyName: "go-source-entry",
|
||||
},
|
||||
{
|
||||
name: "HackageStackYamlLockMetadata",
|
||||
metadata: pkg.HackageStackYamlLockEntry{},
|
||||
|
||||
@@ -1,18 +1,12 @@
|
||||
package binary
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func Test_JavaBinaryImage(t *testing.T) {
|
||||
// no match for platform in manifest on Apple Silicon
|
||||
if runtime.GOOS == "darwin" && runtime.GOARCH == "arm64" {
|
||||
t.Skip("Java binary images not supported on Apple Silicon (darwin/arm64)")
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
image string
|
||||
expected []string
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/binary/test-fixtures/manager/internal/config"
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/scylladb/go-set/strset"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const Path = "config.yaml"
|
||||
|
||||
+1
-1
@@ -6,7 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/OneOfOne/xxhash"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type BinaryFromImage struct {
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type SnippetMetadata struct {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"regexp"
|
||||
"sort"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
|
||||
@@ -29,3 +29,9 @@ func NewDotnetPackagesLockCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("dotnet-packages-lock-cataloger").
|
||||
WithParserByGlobs(parseDotnetPackagesLock, "**/packages.lock.json")
|
||||
}
|
||||
|
||||
// NewDotnetCsprojCataloger returns a cataloger based on .csproj files.
|
||||
func NewDotnetCsprojCataloger() pkg.Cataloger {
|
||||
return generic.NewCataloger("dotnet-csproj-cataloger").
|
||||
WithParserByGlobs(parseDotnetCsproj, "**/*.csproj")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
package dotnet
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/internal/pkgtest"
|
||||
)
|
||||
|
||||
func TestDotnetCsprojCataloger(t *testing.T) {
|
||||
fixture := "test-fixtures/weatherforecast-steeltoe"
|
||||
|
||||
expectedLocation := file.NewLocation("Sample.csproj")
|
||||
fixtureLocationSet := file.NewLocationSet(expectedLocation)
|
||||
|
||||
expected := []pkg.Package{
|
||||
{
|
||||
Name: "Steeltoe.Common.Hosting",
|
||||
Version: "3.2.*",
|
||||
FoundBy: "dotnet-csproj-cataloger",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Steeltoe.Common.Hosting@3.2.%2A",
|
||||
Locations: fixtureLocationSet,
|
||||
Metadata: pkg.DotnetDepsEntry{
|
||||
Name: "Steeltoe.Common.Hosting",
|
||||
Version: "3.2.*",
|
||||
Path: ".",
|
||||
Sha512: "",
|
||||
HashPath: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Steeltoe.Management.EndpointCore",
|
||||
Version: "3.2.*",
|
||||
FoundBy: "dotnet-csproj-cataloger",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Steeltoe.Management.EndpointCore@3.2.%2A",
|
||||
Locations: fixtureLocationSet,
|
||||
Metadata: pkg.DotnetDepsEntry{
|
||||
Name: "Steeltoe.Management.EndpointCore",
|
||||
Version: "3.2.*",
|
||||
Path: ".",
|
||||
Sha512: "",
|
||||
HashPath: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Swashbuckle.AspNetCore",
|
||||
Version: "6.2.*",
|
||||
FoundBy: "dotnet-csproj-cataloger",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Swashbuckle.AspNetCore@6.2.%2A",
|
||||
Locations: fixtureLocationSet,
|
||||
Metadata: pkg.DotnetDepsEntry{
|
||||
Name: "Swashbuckle.AspNetCore",
|
||||
Version: "6.2.*",
|
||||
Path: ".",
|
||||
Sha512: "",
|
||||
HashPath: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.TestCataloger(t, fixture, NewDotnetCsprojCataloger(), expected, nil)
|
||||
}
|
||||
|
||||
func TestDotnetCsprojCataloger_GlopsMatch(t *testing.T) {
|
||||
expectedPackages := []pkg.Package{
|
||||
{
|
||||
Name: "Steeltoe.Common.Hosting",
|
||||
Version: "3.2.*",
|
||||
FoundBy: "dotnet-csproj-cataloger",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Steeltoe.Common.Hosting@3.2.%2A",
|
||||
Locations: file.NewLocationSet(file.NewLocation("Sample.csproj")),
|
||||
Metadata: pkg.DotnetDepsEntry{
|
||||
Name: "Steeltoe.Common.Hosting",
|
||||
Version: "3.2.*",
|
||||
Path: ".",
|
||||
Sha512: "",
|
||||
HashPath: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Steeltoe.Management.EndpointCore",
|
||||
Version: "3.2.*",
|
||||
FoundBy: "dotnet-csproj-cataloger",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Steeltoe.Management.EndpointCore@3.2.%2A",
|
||||
Locations: file.NewLocationSet(file.NewLocation("Sample.csproj")),
|
||||
Metadata: pkg.DotnetDepsEntry{
|
||||
Name: "Steeltoe.Management.EndpointCore",
|
||||
Version: "3.2.*",
|
||||
Path: ".",
|
||||
Sha512: "",
|
||||
HashPath: "",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "Swashbuckle.AspNetCore",
|
||||
Version: "6.2.*",
|
||||
FoundBy: "dotnet-csproj-cataloger",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Swashbuckle.AspNetCore@6.2.%2A",
|
||||
Locations: file.NewLocationSet(file.NewLocation("Sample.csproj")),
|
||||
Metadata: pkg.DotnetDepsEntry{
|
||||
Name: "Swashbuckle.AspNetCore",
|
||||
Version: "6.2.*",
|
||||
Path: ".",
|
||||
Sha512: "",
|
||||
HashPath: "",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, "test-fixtures/weatherforecast-steeltoe").
|
||||
Expects(expectedPackages, nil).
|
||||
TestCataloger(t, NewDotnetCsprojCataloger())
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
package dotnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/anchore/packageurl-go"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
// csprojProject represents the root element of a .csproj file
|
||||
type csprojProject struct {
|
||||
XMLName xml.Name `xml:"Project"`
|
||||
Sdk string `xml:"Sdk,attr"`
|
||||
PropertyGroups []csprojPropertyGroup `xml:"PropertyGroup"`
|
||||
ItemGroups []csprojItemGroup `xml:"ItemGroup"`
|
||||
}
|
||||
|
||||
// csprojPropertyGroup represents a PropertyGroup element containing MSBuild properties
|
||||
type csprojPropertyGroup struct {
|
||||
Properties []csprojProperty `xml:",any"`
|
||||
}
|
||||
|
||||
// csprojProperty represents any property within a PropertyGroup
|
||||
type csprojProperty struct {
|
||||
XMLName xml.Name
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// csprojItemGroup represents an ItemGroup element containing references
|
||||
type csprojItemGroup struct {
|
||||
PackageReferences []csprojPackageReference `xml:"PackageReference"`
|
||||
ProjectReferences []csprojProjectReference `xml:"ProjectReference"`
|
||||
}
|
||||
|
||||
// csprojPackageReference represents a PackageReference element
|
||||
type csprojPackageReference struct {
|
||||
Include string `xml:"Include,attr"`
|
||||
Version string `xml:"Version,attr"`
|
||||
PrivateAssets string `xml:"PrivateAssets,attr"`
|
||||
IncludeAssets string `xml:"IncludeAssets,attr"`
|
||||
Condition string `xml:"Condition,attr"`
|
||||
}
|
||||
|
||||
// csprojProjectReference represents a ProjectReference element
|
||||
type csprojProjectReference struct {
|
||||
Include string `xml:"Include,attr"`
|
||||
Condition string `xml:"Condition,attr"`
|
||||
}
|
||||
|
||||
func parseDotnetCsproj(_ context.Context, _ file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
contents, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to read .csproj file: %w", err)
|
||||
}
|
||||
|
||||
var project csprojProject
|
||||
if err := xml.Unmarshal(contents, &project); err != nil {
|
||||
return nil, nil, fmt.Errorf("unable to parse .csproj XML: %w", err)
|
||||
}
|
||||
|
||||
// Build property map from PropertyGroups
|
||||
properties := buildPropertyMap(project.PropertyGroups)
|
||||
|
||||
var pkgs []pkg.Package
|
||||
var relationships []artifact.Relationship
|
||||
|
||||
// Process PackageReference elements
|
||||
for _, itemGroup := range project.ItemGroups {
|
||||
for _, pkgRef := range itemGroup.PackageReferences {
|
||||
// Skip packages that are build-time only or analyzers
|
||||
if shouldSkipPackageReference(pkgRef) {
|
||||
continue
|
||||
}
|
||||
|
||||
// Resolve any MSBuild property variables in the version
|
||||
resolvedVersion := resolveProperties(pkgRef.Version, properties)
|
||||
pkgRef.Version = resolvedVersion
|
||||
|
||||
p := buildPackageFromReference(pkgRef, reader.Location)
|
||||
if p != nil {
|
||||
pkgs = append(pkgs, *p)
|
||||
}
|
||||
}
|
||||
|
||||
// Process ProjectReference elements (if we want to include them as relationships)
|
||||
for _, projRef := range itemGroup.ProjectReferences {
|
||||
// ProjectReferences represent internal project dependencies
|
||||
// We could create relationships here, but for now we skip them
|
||||
// since they represent source-to-source dependencies within the same solution
|
||||
_ = projRef
|
||||
}
|
||||
}
|
||||
|
||||
return pkgs, relationships, nil
|
||||
}
|
||||
|
||||
// shouldSkipPackageReference determines if a package reference should be skipped
|
||||
func shouldSkipPackageReference(ref csprojPackageReference) bool {
|
||||
// Skip packages that are private assets only (build-time dependencies)
|
||||
if ref.PrivateAssets == "all" || ref.PrivateAssets == "All" {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip conditional references that are likely build/development only
|
||||
condition := strings.ToLower(ref.Condition)
|
||||
if strings.Contains(condition, "debug") && !strings.Contains(condition, "release") {
|
||||
return true
|
||||
}
|
||||
|
||||
// Skip packages that are commonly build-time only
|
||||
lowerName := strings.ToLower(ref.Include)
|
||||
buildTimePackages := map[string]bool{
|
||||
"microsoft.net.test.sdk": true,
|
||||
"stylecop.analyzers": true,
|
||||
"microsoft.codeanalysis": true,
|
||||
"coverlet.collector": true,
|
||||
"xunit.runner.visualstudio": true,
|
||||
"nunit": true,
|
||||
"nunit3testadapter": true,
|
||||
"mstest.testadapter": true,
|
||||
"mstest.testframework": true,
|
||||
}
|
||||
|
||||
for buildPkg := range buildTimePackages {
|
||||
if strings.Contains(lowerName, buildPkg) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// buildPropertyMap creates a map of MSBuild properties from PropertyGroups
|
||||
func buildPropertyMap(propertyGroups []csprojPropertyGroup) map[string]string {
|
||||
properties := make(map[string]string)
|
||||
|
||||
for _, group := range propertyGroups {
|
||||
for _, prop := range group.Properties {
|
||||
propertyName := prop.XMLName.Local
|
||||
propertyValue := strings.TrimSpace(prop.Value)
|
||||
if propertyName != "" && propertyValue != "" {
|
||||
properties[propertyName] = propertyValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return properties
|
||||
}
|
||||
|
||||
// resolveProperties resolves MSBuild property variables like $(PropertyName) in a string
|
||||
func resolveProperties(input string, properties map[string]string) string {
|
||||
if input == "" {
|
||||
return input
|
||||
}
|
||||
|
||||
// Pattern matches $(PropertyName)
|
||||
propertyPattern := `\$\(([^)]+)\)`
|
||||
re := regexp.MustCompile(propertyPattern)
|
||||
|
||||
result := re.ReplaceAllStringFunc(input, func(match string) string {
|
||||
// Extract property name from $(PropertyName)
|
||||
propertyName := match[2 : len(match)-1] // Remove $( and )
|
||||
|
||||
if value, exists := properties[propertyName]; exists {
|
||||
return value
|
||||
}
|
||||
|
||||
// Return original if property not found (preserve for debugging)
|
||||
return match
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// buildPackageFromReference creates a Package from a PackageReference element
|
||||
func buildPackageFromReference(ref csprojPackageReference, location file.Location) *pkg.Package {
|
||||
name := strings.TrimSpace(ref.Include)
|
||||
if name == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
version := strings.TrimSpace(ref.Version)
|
||||
// If version is empty, this might be a framework reference or implicit version
|
||||
// For now, we'll skip packages without explicit versions since we can't determine them
|
||||
// from the .csproj alone (would need props/targets files or lock files)
|
||||
if version == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Skip packages with unresolved MSBuild properties (contains $(...))
|
||||
if strings.Contains(version, "$(") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Generate PURL following the established pattern for .NET packages
|
||||
purl := packageurl.NewPackageURL(
|
||||
packageurl.TypeNuget,
|
||||
"",
|
||||
name,
|
||||
version,
|
||||
nil,
|
||||
"",
|
||||
)
|
||||
|
||||
p := &pkg.Package{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: purl.ToString(),
|
||||
Locations: file.NewLocationSet(location),
|
||||
Metadata: pkg.DotnetDepsEntry{
|
||||
Name: name,
|
||||
Version: version,
|
||||
Path: filepath.Dir(location.RealPath),
|
||||
Sha512: "",
|
||||
HashPath: "",
|
||||
},
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
||||
@@ -0,0 +1,658 @@
|
||||
package dotnet
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
func TestParseDotnetCsproj(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected []pkg.Package
|
||||
expectedError bool
|
||||
}{
|
||||
{
|
||||
name: "basic PackageReference parsing",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
{
|
||||
Name: "Serilog",
|
||||
Version: "2.10.0",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Serilog@2.10.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skip private assets",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" PrivateAssets="All" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skip conditional debug-only references",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" Condition="'$(Configuration)' == 'Debug'" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "complex IncludeAssets and PrivateAssets handling",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.ChakraCore" Version="1.11.24" IncludeAssets="runtime; build; native" />
|
||||
<PackageReference Include="NUnit" Version="3.13.3" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
{
|
||||
Name: "Microsoft.ChakraCore",
|
||||
Version: "1.11.24",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Microsoft.ChakraCore@1.11.24",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skip build-time packages",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.4" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "skip packages without version",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.App" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple ItemGroup elements",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Serilog" Version="2.10.0" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
{
|
||||
Name: "Serilog",
|
||||
Version: "2.10.0",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Serilog@2.10.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty project",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{},
|
||||
},
|
||||
{
|
||||
name: "ProjectReference ignored",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
|
||||
<ProjectReference Include="../SharedLibrary/SharedLibrary.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "property variable resolution",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<SteeltoeVersion>3.2.0</SteeltoeVersion>
|
||||
<TestVersion>1.0.0</TestVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Steeltoe.Common" Version="$(SteeltoeVersion)" />
|
||||
<PackageReference Include="Test.Package" Version="$(TestVersion)" />
|
||||
<PackageReference Include="Regular.Package" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Steeltoe.Common",
|
||||
Version: "3.2.0",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Steeltoe.Common@3.2.0",
|
||||
},
|
||||
{
|
||||
Name: "Test.Package",
|
||||
Version: "1.0.0",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Test.Package@1.0.0",
|
||||
},
|
||||
{
|
||||
Name: "Regular.Package",
|
||||
Version: "2.0.0",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Regular.Package@2.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "wildcard versions preserved",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<SteeltoeVersion>3.2.*</SteeltoeVersion>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Steeltoe.Common" Version="$(SteeltoeVersion)" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.*" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Steeltoe.Common",
|
||||
Version: "3.2.*",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Steeltoe.Common@3.2.%2A",
|
||||
},
|
||||
{
|
||||
Name: "Swashbuckle.AspNetCore",
|
||||
Version: "6.2.*",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Swashbuckle.AspNetCore@6.2.%2A",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "unresolved property preserved",
|
||||
input: `<Project Sdk="Microsoft.NET.Sdk">
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Some.Package" Version="$(UndefinedProperty)" />
|
||||
<PackageReference Include="Regular.Package" Version="2.0.0" />
|
||||
</ItemGroup>
|
||||
</Project>`,
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "Regular.Package",
|
||||
Version: "2.0.0",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Regular.Package@2.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "malformed XML",
|
||||
input: `<Project><ItemGroup><PackageReference Include="Test"`,
|
||||
expected: nil,
|
||||
expectedError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
reader := file.NewLocationReadCloser(
|
||||
file.NewLocation("/test/project.csproj"),
|
||||
io.NopCloser(strings.NewReader(test.input)),
|
||||
)
|
||||
|
||||
packages, relationships, err := parseDotnetCsproj(context.Background(), nil, &generic.Environment{}, reader)
|
||||
|
||||
if test.expectedError {
|
||||
require.Error(t, err)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Empty(t, relationships)
|
||||
require.Len(t, packages, len(test.expected))
|
||||
|
||||
for i, expectedPkg := range test.expected {
|
||||
actualPkg := packages[i]
|
||||
assert.Equal(t, expectedPkg.Name, actualPkg.Name)
|
||||
assert.Equal(t, expectedPkg.Version, actualPkg.Version)
|
||||
assert.Equal(t, expectedPkg.Language, actualPkg.Language)
|
||||
assert.Equal(t, expectedPkg.Type, actualPkg.Type)
|
||||
assert.Equal(t, expectedPkg.PURL, actualPkg.PURL)
|
||||
|
||||
// Verify metadata structure
|
||||
metadata, ok := actualPkg.Metadata.(pkg.DotnetDepsEntry)
|
||||
require.True(t, ok, "expected DotnetDepsEntry metadata")
|
||||
assert.Equal(t, expectedPkg.Name, metadata.Name)
|
||||
assert.Equal(t, expectedPkg.Version, metadata.Version)
|
||||
assert.NotEmpty(t, metadata.Path)
|
||||
|
||||
// Verify locations are set
|
||||
assert.NotEmpty(t, actualPkg.Locations)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldSkipPackageReference(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref csprojPackageReference
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "regular package",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "private assets all",
|
||||
ref: csprojPackageReference{
|
||||
Include: "StyleCop.Analyzers",
|
||||
Version: "1.2.0",
|
||||
PrivateAssets: "all",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "private assets All (capitalized)",
|
||||
ref: csprojPackageReference{
|
||||
Include: "StyleCop.Analyzers",
|
||||
Version: "1.2.0",
|
||||
PrivateAssets: "All",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "debug condition",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Microsoft.NET.Test.Sdk",
|
||||
Version: "17.8.0",
|
||||
Condition: "'$(Configuration)' == 'Debug'",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "test SDK package",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Microsoft.NET.Test.Sdk",
|
||||
Version: "17.8.0",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "stylecop analyzer",
|
||||
ref: csprojPackageReference{
|
||||
Include: "StyleCop.Analyzers",
|
||||
Version: "1.2.0",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "code analysis package",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Microsoft.CodeAnalysis.Analyzers",
|
||||
Version: "3.3.4",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "includeAssets runtime only",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Some.Package",
|
||||
Version: "1.0.0",
|
||||
IncludeAssets: "runtime",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "mixed condition with release",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Some.Package",
|
||||
Version: "1.0.0",
|
||||
Condition: "'$(Configuration)' == 'Debug' OR '$(Configuration)' == 'Release'",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "nunit test package",
|
||||
ref: csprojPackageReference{
|
||||
Include: "NUnit",
|
||||
Version: "3.13.3",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "mstest framework",
|
||||
ref: csprojPackageReference{
|
||||
Include: "MSTest.TestFramework",
|
||||
Version: "3.1.1",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := shouldSkipPackageReference(test.ref)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPackageFromReference(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
ref csprojPackageReference
|
||||
location file.Location
|
||||
expected *pkg.Package
|
||||
}{
|
||||
{
|
||||
name: "valid package reference",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
},
|
||||
location: file.NewLocation("/test/project.csproj"),
|
||||
expected: &pkg.Package{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "empty name",
|
||||
ref: csprojPackageReference{
|
||||
Include: "",
|
||||
Version: "13.0.3",
|
||||
},
|
||||
location: file.NewLocation("/test/project.csproj"),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "empty version",
|
||||
ref: csprojPackageReference{
|
||||
Include: "Newtonsoft.Json",
|
||||
Version: "",
|
||||
},
|
||||
location: file.NewLocation("/test/project.csproj"),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
name: "whitespace in name and version",
|
||||
ref: csprojPackageReference{
|
||||
Include: " Newtonsoft.Json ",
|
||||
Version: " 13.0.3 ",
|
||||
},
|
||||
location: file.NewLocation("/test/project.csproj"),
|
||||
expected: &pkg.Package{
|
||||
Name: "Newtonsoft.Json",
|
||||
Version: "13.0.3",
|
||||
Language: pkg.Dotnet,
|
||||
Type: pkg.DotnetPkg,
|
||||
PURL: "pkg:nuget/Newtonsoft.Json@13.0.3",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := buildPackageFromReference(test.ref, test.location)
|
||||
|
||||
if test.expected == nil {
|
||||
assert.Nil(t, result)
|
||||
return
|
||||
}
|
||||
|
||||
require.NotNil(t, result)
|
||||
assert.Equal(t, test.expected.Name, result.Name)
|
||||
assert.Equal(t, test.expected.Version, result.Version)
|
||||
assert.Equal(t, test.expected.Language, result.Language)
|
||||
assert.Equal(t, test.expected.Type, result.Type)
|
||||
assert.Equal(t, test.expected.PURL, result.PURL)
|
||||
|
||||
// Verify metadata
|
||||
metadata, ok := result.Metadata.(pkg.DotnetDepsEntry)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, test.expected.Name, metadata.Name)
|
||||
assert.Equal(t, test.expected.Version, metadata.Version)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPropertyMap(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
propertyGroups []csprojPropertyGroup
|
||||
expected map[string]string
|
||||
}{
|
||||
{
|
||||
name: "empty property groups",
|
||||
propertyGroups: []csprojPropertyGroup{},
|
||||
expected: map[string]string{},
|
||||
},
|
||||
{
|
||||
name: "single property group",
|
||||
propertyGroups: []csprojPropertyGroup{
|
||||
{
|
||||
Properties: []csprojProperty{
|
||||
{XMLName: xml.Name{Local: "SteeltoeVersion"}, Value: "3.2.0"},
|
||||
{XMLName: xml.Name{Local: "TestVersion"}, Value: "1.0.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"SteeltoeVersion": "3.2.0",
|
||||
"TestVersion": "1.0.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "multiple property groups",
|
||||
propertyGroups: []csprojPropertyGroup{
|
||||
{
|
||||
Properties: []csprojProperty{
|
||||
{XMLName: xml.Name{Local: "SteeltoeVersion"}, Value: "3.2.0"},
|
||||
},
|
||||
},
|
||||
{
|
||||
Properties: []csprojProperty{
|
||||
{XMLName: xml.Name{Local: "TestVersion"}, Value: "1.0.0"},
|
||||
{XMLName: xml.Name{Local: "TargetFramework"}, Value: "net6.0"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"SteeltoeVersion": "3.2.0",
|
||||
"TestVersion": "1.0.0",
|
||||
"TargetFramework": "net6.0",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "properties with whitespace",
|
||||
propertyGroups: []csprojPropertyGroup{
|
||||
{
|
||||
Properties: []csprojProperty{
|
||||
{XMLName: xml.Name{Local: "Version"}, Value: " 3.2.0 "},
|
||||
{XMLName: xml.Name{Local: "EmptyValue"}, Value: " "},
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"Version": "3.2.0",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := buildPropertyMap(test.propertyGroups)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveProperties(t *testing.T) {
|
||||
properties := map[string]string{
|
||||
"SteeltoeVersion": "3.2.*",
|
||||
"TestVersion": "1.0.0",
|
||||
"MajorVersion": "2",
|
||||
"MinorVersion": "1",
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no properties to resolve",
|
||||
input: "1.2.3",
|
||||
expected: "1.2.3",
|
||||
},
|
||||
{
|
||||
name: "single property resolution",
|
||||
input: "$(SteeltoeVersion)",
|
||||
expected: "3.2.*",
|
||||
},
|
||||
{
|
||||
name: "multiple property resolution",
|
||||
input: "$(MajorVersion).$(MinorVersion).0",
|
||||
expected: "2.1.0",
|
||||
},
|
||||
{
|
||||
name: "mixed resolved and unresolved",
|
||||
input: "$(SteeltoeVersion)-$(UnknownProperty)",
|
||||
expected: "3.2.*-$(UnknownProperty)",
|
||||
},
|
||||
{
|
||||
name: "empty input",
|
||||
input: "",
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
name: "unresolved property preserved",
|
||||
input: "$(UnknownProperty)",
|
||||
expected: "$(UnknownProperty)",
|
||||
},
|
||||
{
|
||||
name: "complex pattern",
|
||||
input: "prefix-$(TestVersion)-suffix",
|
||||
expected: "prefix-1.0.0-suffix",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := resolveProperties(test.input, properties)
|
||||
assert.Equal(t, test.expected, result)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AspNetCoreHostingModel>InProcess</AspNetCoreHostingModel>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<SteeltoeVersion>3.2.*</SteeltoeVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Steeltoe.Common.Hosting" Version="$(SteeltoeVersion)" />
|
||||
<PackageReference Include="Steeltoe.Management.EndpointCore" Version="$(SteeltoeVersion)" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.*" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"io"
|
||||
"regexp"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
|
||||
@@ -1,149 +0,0 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
)
|
||||
|
||||
// resolveModuleLicenses finds and parses license files for Go modules
|
||||
func resolveModuleLicenses(ctx context.Context, pkgInfos []pkgInfo, fs afero.Fs) pkg.LicenseSet {
|
||||
licenses := pkg.NewLicenseSet()
|
||||
|
||||
for _, info := range pkgInfos {
|
||||
modDir, pkgDir, err := getAbsolutePkgPaths(info)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
licenseFiles, err := findAllLicenseCandidatesUpwards(pkgDir, licenseRegexp, modDir, fs)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, f := range licenseFiles {
|
||||
contents, err := fs.Open(f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
licenses.Add(pkg.NewLicensesFromReadCloserWithContext(ctx, file.NewLocationReadCloser(file.Location{}, contents))...)
|
||||
_ = contents.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return licenses
|
||||
}
|
||||
|
||||
/*
|
||||
findAllLicenseCandidatesUpwards performs a bubble-up search per package:
|
||||
1. pkgInfos represents a sparse vertical distribution of packages within modules
|
||||
2. we get more pkgInfos for free when the build configuration is updated
|
||||
|
||||
The recursion terminates via two conditions:
|
||||
- When dir is outside stopAt boundary (happy case)
|
||||
- When reaching filesystem root where parent == dir (edge case)
|
||||
|
||||
Note: The code does NOT follow symlinks. It returns a slice of absolute paths that
|
||||
represent license file matches that are resolved independently of the bubble-up.
|
||||
|
||||
When we should consider redesign tip to stem:
|
||||
- Reduced filesystem calls: Single traversal vs multiple per-package
|
||||
- Path deduplication: Avoids re-scanning common parent directories
|
||||
- Better for wide module structures: Efficient when many packages share parent paths
|
||||
- We need to consider the case here where nested modules are visited by accident and licenses
|
||||
are erroneously associated to a 'parent module'; bubble up currently prevents this
|
||||
*/
|
||||
func findAllLicenseCandidatesUpwards(dir string, r *regexp.Regexp, stopAt string, fs afero.Fs) ([]string, error) {
|
||||
// Validate that both paths are absolute
|
||||
if !filepath.IsAbs(dir) {
|
||||
return nil, fmt.Errorf("dir must be an absolute path, got: %s", dir)
|
||||
}
|
||||
if !filepath.IsAbs(stopAt) {
|
||||
return nil, fmt.Errorf("stopAt must be an absolute path, got: %s", stopAt)
|
||||
}
|
||||
|
||||
licenses, err := findLicenseCandidates(dir, r, stopAt, fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure we return an empty slice rather than nil for consistency
|
||||
if licenses == nil {
|
||||
return []string{}, nil
|
||||
}
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
func findLicenseCandidates(dir string, r *regexp.Regexp, stopAt string, fs afero.Fs) ([]string, error) {
|
||||
// stop if we've gone outside the stopAt directory
|
||||
if !strings.HasPrefix(dir, stopAt) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
licenses, err := findLicensesInDir(dir, r, fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parent := filepath.Dir(dir)
|
||||
// can't go any higher up the directory tree: "/" case
|
||||
if parent == dir {
|
||||
return licenses, nil
|
||||
}
|
||||
|
||||
// search parent directory and combine results
|
||||
parentLicenses, err := findLicenseCandidates(parent, r, stopAt, fs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Combine current directory licenses with parent directory licenses
|
||||
return append(licenses, parentLicenses...), nil
|
||||
}
|
||||
|
||||
func getAbsolutePkgPaths(info pkgInfo) (modDir string, pkgDir string, err error) {
|
||||
pkgDir, err = filepath.Abs(info.pkgDir)
|
||||
if err != nil {
|
||||
return modDir, pkgDir, err
|
||||
}
|
||||
|
||||
modDir, err = filepath.Abs(info.moduleDir)
|
||||
if err != nil {
|
||||
return modDir, pkgDir, err
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(pkgDir, modDir) {
|
||||
return modDir, pkgDir, fmt.Errorf("modDir %s should contain pkgDir %s", modDir, pkgDir)
|
||||
}
|
||||
|
||||
return modDir, pkgDir, nil
|
||||
}
|
||||
|
||||
func findLicensesInDir(dir string, r *regexp.Regexp, fs afero.Fs) ([]string, error) {
|
||||
var licenses []string
|
||||
|
||||
dirContents, err := afero.ReadDir(fs, dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, f := range dirContents {
|
||||
if f.IsDir() {
|
||||
continue
|
||||
}
|
||||
|
||||
if r.MatchString(f.Name()) {
|
||||
path := filepath.Join(dir, f.Name())
|
||||
licenses = append(licenses, path)
|
||||
}
|
||||
}
|
||||
|
||||
return licenses, nil
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestFindAllLicenseCandidatesUpwards(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
setupFS func(afero.Fs)
|
||||
startDir string
|
||||
stopAt string
|
||||
expectedFiles []string
|
||||
expectedError bool
|
||||
description string
|
||||
}{
|
||||
{
|
||||
name: "normal traversal up to root",
|
||||
startDir: "/project/subdir/deeper",
|
||||
stopAt: "/project",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project/subdir/deeper", 0755)
|
||||
afero.WriteFile(fs, "/project/LICENSE", []byte("MIT"), 0644)
|
||||
afero.WriteFile(fs, "/project/foobar", []byte("MIT"), 0644)
|
||||
afero.WriteFile(fs, "/project/subdir/LICENSE.txt", []byte("Apache"), 0644)
|
||||
afero.WriteFile(fs, "/project/subdir/deeper/COPYING", []byte("GPL"), 0644)
|
||||
},
|
||||
expectedFiles: []string{
|
||||
"/project/subdir/deeper/COPYING",
|
||||
"/project/subdir/LICENSE.txt",
|
||||
"/project/LICENSE",
|
||||
},
|
||||
description: "Should find all license files traversing upward",
|
||||
},
|
||||
{
|
||||
name: "stops at boundary directory",
|
||||
startDir: "/project/subdir/deeper",
|
||||
stopAt: "/project/subdir",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project/subdir/deeper", 0755)
|
||||
afero.WriteFile(fs, "/project/LICENSE", []byte("MIT"), 0644)
|
||||
afero.WriteFile(fs, "/project/subdir/LICENSE.txt", []byte("Apache"), 0644)
|
||||
afero.WriteFile(fs, "/project/subdir/deeper/COPYING", []byte("GPL"), 0644)
|
||||
},
|
||||
expectedFiles: []string{
|
||||
"/project/subdir/deeper/COPYING",
|
||||
"/project/subdir/LICENSE.txt",
|
||||
},
|
||||
description: "Should stop at stopAt boundary and not find LICENSE in /project",
|
||||
},
|
||||
{
|
||||
name: "handles non-existent directory",
|
||||
startDir: "/nonexistent",
|
||||
stopAt: "/",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
// Don't create the directory
|
||||
},
|
||||
expectedError: true,
|
||||
description: "Should return error for non-existent directory",
|
||||
},
|
||||
{
|
||||
name: "handles empty directory tree",
|
||||
startDir: "/empty/dir/tree",
|
||||
stopAt: "/empty",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/empty/dir/tree", 0755)
|
||||
// No license files
|
||||
},
|
||||
expectedFiles: []string{},
|
||||
description: "Should return empty slice when no license files found",
|
||||
},
|
||||
{
|
||||
name: "handles directory at filesystem root",
|
||||
startDir: "/",
|
||||
stopAt: "/",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
afero.WriteFile(fs, "/LICENSE", []byte("MIT"), 0644)
|
||||
},
|
||||
expectedFiles: []string{"/LICENSE"},
|
||||
description: "Should handle traversal starting at root",
|
||||
},
|
||||
{
|
||||
name: "ignores directories with license-like names",
|
||||
startDir: "/project/subdir",
|
||||
stopAt: "/project",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project/subdir", 0755)
|
||||
fs.MkdirAll("/project/LICENSE_DIR", 0755) // Directory, should be ignored
|
||||
afero.WriteFile(fs, "/project/LICENSE", []byte("MIT"), 0644)
|
||||
},
|
||||
expectedFiles: []string{"/project/LICENSE"},
|
||||
description: "Should ignore directories even if they match license pattern",
|
||||
},
|
||||
{
|
||||
name: "startDir equals stopAt",
|
||||
startDir: "/project",
|
||||
stopAt: "/project",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project", 0755)
|
||||
afero.WriteFile(fs, "/project/LICENSE", []byte("MIT"), 0644)
|
||||
},
|
||||
expectedFiles: []string{"/project/LICENSE"},
|
||||
description: "Should handle case where start equals stop directory",
|
||||
},
|
||||
{
|
||||
name: "startDir is parent of stopAt (returns empty)",
|
||||
startDir: "/",
|
||||
stopAt: "/project",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project", 0755)
|
||||
afero.WriteFile(fs, "/LICENSE", []byte("MIT"), 0644)
|
||||
},
|
||||
expectedFiles: []string{},
|
||||
description: "Should return empty when startDir is above stopAt",
|
||||
},
|
||||
{
|
||||
name: "very deep nesting",
|
||||
startDir: "/a/b/c/d/e/f/g/h/i/j",
|
||||
stopAt: "/a",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/a/b/c/d/e/f/g/h/i/j", 0755)
|
||||
afero.WriteFile(fs, "/a/LICENSE", []byte("MIT"), 0644)
|
||||
afero.WriteFile(fs, "/a/b/c/d/e/NOTICE", []byte("Notice"), 0644)
|
||||
},
|
||||
expectedFiles: []string{
|
||||
"/a/b/c/d/e/NOTICE",
|
||||
"/a/LICENSE",
|
||||
},
|
||||
description: "Should handle deep directory nesting without stack overflow",
|
||||
},
|
||||
{
|
||||
name: "relative dir path rejected",
|
||||
startDir: "project/subdir",
|
||||
stopAt: "/project",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project/subdir", 0755)
|
||||
},
|
||||
expectedError: true,
|
||||
description: "Should reject relative dir path",
|
||||
},
|
||||
{
|
||||
name: "relative stopAt path rejected",
|
||||
startDir: "/project/subdir",
|
||||
stopAt: "project",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project/subdir", 0755)
|
||||
},
|
||||
expectedError: true,
|
||||
description: "Should reject relative stopAt path",
|
||||
},
|
||||
{
|
||||
name: "stopAt is descendant of startDir",
|
||||
startDir: "/project",
|
||||
stopAt: "/project/subdir/deeper",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project/subdir/deeper", 0755)
|
||||
afero.WriteFile(fs, "/project/LICENSE", []byte("MIT"), 0644)
|
||||
},
|
||||
expectedFiles: []string{},
|
||||
description: "Should return empty when stopAt is below startDir",
|
||||
},
|
||||
{
|
||||
name: "disjoint paths",
|
||||
startDir: "/foo/bar",
|
||||
stopAt: "/baz/qux",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/foo/bar", 0755)
|
||||
fs.MkdirAll("/baz/qux", 0755)
|
||||
afero.WriteFile(fs, "/foo/bar/LICENSE", []byte("MIT"), 0644)
|
||||
afero.WriteFile(fs, "/LICENSE", []byte("Root"), 0644)
|
||||
},
|
||||
expectedFiles: []string{},
|
||||
description: "Should return empty for completely disjoint paths",
|
||||
},
|
||||
{
|
||||
name: "empty stopAt rejected",
|
||||
startDir: "/project/deep/path",
|
||||
stopAt: "",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project/deep/path", 0755)
|
||||
},
|
||||
expectedError: true,
|
||||
description: "Should reject empty stopAt string",
|
||||
},
|
||||
{
|
||||
name: "empty startDir rejected",
|
||||
startDir: "",
|
||||
stopAt: "/project",
|
||||
setupFS: func(fs afero.Fs) {
|
||||
fs.MkdirAll("/project", 0755)
|
||||
},
|
||||
expectedError: true,
|
||||
description: "Should reject empty startDir string",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Create in-memory filesystem
|
||||
fs := afero.NewMemMapFs()
|
||||
tt.setupFS(fs)
|
||||
|
||||
// Run the function
|
||||
result, err := findAllLicenseCandidatesUpwards(tt.startDir, licenseRegexp, tt.stopAt, fs)
|
||||
|
||||
// Check error expectation
|
||||
if tt.expectedError {
|
||||
assert.Error(t, err, tt.description)
|
||||
return
|
||||
}
|
||||
|
||||
require.NoError(t, err, tt.description)
|
||||
assert.Equal(t, tt.expectedFiles, result, tt.description)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -4,30 +4,20 @@ import (
|
||||
"bufio"
|
||||
"context"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/afero"
|
||||
"golang.org/x/mod/modfile"
|
||||
"golang.org/x/tools/go/packages"
|
||||
|
||||
"github.com/anchore/syft/internal"
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
"github.com/anchore/syft/syft/pkg/cataloger/generic"
|
||||
)
|
||||
|
||||
var (
|
||||
licenseRegexp = regexp.MustCompile(`^(?i)((UN)?LICEN(S|C)E|COPYING|NOTICE).*$`)
|
||||
)
|
||||
|
||||
type goModCataloger struct {
|
||||
licenseResolver goLicenseResolver
|
||||
}
|
||||
@@ -38,322 +28,59 @@ func newGoModCataloger(opts CatalogerConfig) *goModCataloger {
|
||||
}
|
||||
}
|
||||
|
||||
// parseGoModFile takes a go.mod and tries to resolve and lists all packages discovered.
|
||||
// parseGoModFile takes a go.mod and lists all packages discovered.
|
||||
//
|
||||
//nolint:funlen
|
||||
func (c *goModCataloger) parseGoModFile(ctx context.Context, resolver file.Resolver, _ *generic.Environment, reader file.LocationReadCloser) ([]pkg.Package, []artifact.Relationship, error) {
|
||||
modDir := filepath.Dir(string(reader.Location.Reference().RealPath))
|
||||
packages := make(map[string]pkg.Package)
|
||||
|
||||
contents, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read go module: %w", err)
|
||||
}
|
||||
|
||||
f, err := modfile.Parse(reader.RealPath, contents, nil)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse go module: %w", err)
|
||||
}
|
||||
|
||||
digests, err := parseGoSumFile(resolver, reader)
|
||||
if err != nil {
|
||||
log.Debugf("unable to get go.sum: %v", err)
|
||||
}
|
||||
|
||||
// source analysis using go toolchain if available
|
||||
syftSourcePackages, sourceModules, sourceDependencies, unknownErr := c.loadPackages(modDir, reader.Location)
|
||||
catalogedModules, sourceModuleToPkg := c.catalogModules(ctx, syftSourcePackages, sourceModules, reader, digests)
|
||||
relationships := buildModuleRelationships(catalogedModules, sourceDependencies, sourceModuleToPkg)
|
||||
|
||||
// base case go.mod file parsing
|
||||
modFile, err := c.parseModFileContents(reader)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// only use mod packages NOT found in source analysis
|
||||
goModPackages := c.createGoModPackages(ctx, resolver, modFile, sourceModules, reader, digests)
|
||||
c.applyReplaceDirectives(ctx, resolver, modFile, goModPackages, reader, digests)
|
||||
c.applyExcludeDirectives(modFile, goModPackages)
|
||||
|
||||
finalPkgs := c.assembleResults(catalogedModules, goModPackages)
|
||||
return finalPkgs, relationships, unknownErr
|
||||
}
|
||||
|
||||
// loadPackages uses golang.org/x/tools/go/packages to get dependency information.
|
||||
func (c *goModCataloger) loadPackages(modDir string, loc file.Location) (pkgs map[string][]pkgInfo, modules map[string]*packages.Module, dependencies map[string][]string, unknownErr error) {
|
||||
cfg := &packages.Config{
|
||||
// Mode flags control what information is loaded for each package.
|
||||
// Performance impact increases significantly with each additional flag:
|
||||
//
|
||||
// packages.NeedModule - Required for module metadata (path, version, replace directives).
|
||||
// Essential for SBOM generation. Minimal performance impact.
|
||||
//
|
||||
// packages.NeedName - Required for package names & package Path. Minimal performance impact.
|
||||
// Needed to identify packages and filter out standard library packages.
|
||||
//
|
||||
// packages.NeedFiles - Loads source file paths for each package.
|
||||
// Moderate performance impact as it requires filesystem traversal.
|
||||
// Required for license discovery.
|
||||
//
|
||||
// packages.NeedDeps - Loads the dependency graph between packages.
|
||||
// High performance impact as it builds the complete import graph.
|
||||
// Critical for generating accurate dependency relationships in SBOM.
|
||||
//
|
||||
// packages.NeedImports - Loads import information for each package.
|
||||
// High performance impact, especially with large codebases.
|
||||
// Required for building module-to-module dependency mappings.
|
||||
//
|
||||
// Adding flags like NeedTypes, NeedSyntax, or NeedTypesInfo would dramatically
|
||||
// increase memory usage and processing time (10x+ slower) but are not needed
|
||||
// for SBOM generation as we only require dependency and module metadata.
|
||||
Mode: packages.NeedModule | packages.NeedName | packages.NeedFiles | packages.NeedDeps | packages.NeedImports,
|
||||
Dir: modDir,
|
||||
Tests: true,
|
||||
}
|
||||
|
||||
// From Go documentation: "all" expands to all packages in the main module
|
||||
// and their dependencies, including dependencies needed by tests.
|
||||
//
|
||||
// The special pattern "all" specifies all the active modules,
|
||||
// first the main module and then dependencies sorted by module path.
|
||||
// A pattern containing "..." specifies the active modules whose module paths match the pattern.
|
||||
// On implementation we could not find a test case that differentiated between all and ...
|
||||
// There may be a case where ... is non inclusive so we default to all for the inclusive guarantee
|
||||
rootPkgs, err := packages.Load(cfg, "all")
|
||||
if err != nil {
|
||||
log.Debugf("error loading packages: %v", err)
|
||||
}
|
||||
|
||||
// Check for any errors in loading
|
||||
for _, p := range rootPkgs {
|
||||
if len(p.Errors) > 0 {
|
||||
// Log errors but continue processing
|
||||
for _, e := range p.Errors {
|
||||
log.Debugf("package load error for %s: %v", p.PkgPath, e)
|
||||
unknownErr = unknown.Append(unknownErr, loc, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// note: dependencies have already pruned local imports and only focuses on module => module dependencies
|
||||
return c.visitPackages(rootPkgs, loc, unknownErr)
|
||||
}
|
||||
|
||||
type pkgInfo struct {
|
||||
// pkgPath is the import path of the package.
|
||||
pkgPath string
|
||||
// modulePath is the module path of the package.
|
||||
modulePath string
|
||||
// pkgDir is the directory containing the package's source code.
|
||||
pkgDir string
|
||||
// moduleDir is the directory containing the module's source code.
|
||||
moduleDir string
|
||||
}
|
||||
|
||||
// visitPackages processes Go module import graphs to get all modules
|
||||
func (c *goModCataloger) visitPackages(
|
||||
rootPkgs []*packages.Package,
|
||||
loc file.Location,
|
||||
uke error,
|
||||
) (pkgs map[string][]pkgInfo, modules map[string]*packages.Module, dependencies map[string][]string, unknownErr error) {
|
||||
modules = make(map[string]*packages.Module)
|
||||
// note: packages are specific to inside the module - they do not include transitive pkgInfo
|
||||
// packages is used for identifying licensing documents for modules that could contain multiple licenses
|
||||
// dependencies cover transitive module imports; see p.Imports array in packages.Visit
|
||||
pkgs = make(map[string][]pkgInfo)
|
||||
// dependencies are module => module dependencies
|
||||
dependencies = make(map[string][]string)
|
||||
// persist unknown errs from previous parts of the catalog
|
||||
unknownErr = uke
|
||||
// closure (p *Package) bool
|
||||
// return bool determines whether the imports of package p are visited.
|
||||
packages.Visit(rootPkgs, func(p *packages.Package) bool {
|
||||
if len(p.Errors) > 0 {
|
||||
for _, err := range p.Errors {
|
||||
unknownErr = unknown.Append(unknownErr, loc, err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// skip for common causes
|
||||
if shouldSkipVisit(p) {
|
||||
return false
|
||||
}
|
||||
|
||||
// different from above; we still might want to visit imports
|
||||
// ignoring a package shouldn't end walking the tree
|
||||
// since we need to get the full picture for license discovery
|
||||
// for _, prefix := range c.config.IgnorePaths {
|
||||
// if strings.HasPrefix(p.PkgPath, prefix) {
|
||||
// return c.config.IncludeIgnoredDeps
|
||||
// }
|
||||
//}
|
||||
pkgDir := resolvePkgDir(p)
|
||||
if pkgDir == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
module := newModule(p.Module)
|
||||
if module.Dir == "" {
|
||||
// We continue processing even when module.Dir is empty because we still want to:
|
||||
// 1. Extract module dependencies from p.Imports for dependency graph construction
|
||||
// 2. Create syft packages with available metadata (name, version, etc.)
|
||||
// 3. Build relationships between modules even without complete filesystem info
|
||||
// Not having the DIR here just means that we're not going to process the licenses
|
||||
|
||||
// Common causes for module.Dir being empty:
|
||||
// - Vendored dependencies where Go toolchain loses some module metadata
|
||||
// - Replace directives pointing to non-existent or inaccessible paths
|
||||
// A known cause is that the module is vendored, so some information is lost.
|
||||
isVendored := strings.Contains(pkgDir, "/vendor/")
|
||||
if !isVendored {
|
||||
log.Debugf("module %s does not have dir and it's not vendored", module.Path)
|
||||
}
|
||||
}
|
||||
|
||||
// extract module dependencies
|
||||
for _, imp := range p.Imports {
|
||||
if imp.Module != nil && imp.Module.Path != module.Path {
|
||||
if dependencies[module.Path] == nil {
|
||||
dependencies[module.Path] = []string{imp.Module.Path}
|
||||
} else {
|
||||
dependencies[module.Path] = append(dependencies[module.Path], imp.Module.Path)
|
||||
}
|
||||
}
|
||||
}
|
||||
pkgs[module.Path] = append(pkgs[module.Path], pkgInfo{
|
||||
pkgPath: p.PkgPath,
|
||||
modulePath: module.Path,
|
||||
pkgDir: pkgDir,
|
||||
moduleDir: module.Dir,
|
||||
})
|
||||
modules[p.Module.Path] = module
|
||||
|
||||
return true
|
||||
}, nil)
|
||||
return pkgs, modules, dependencies, unknownErr
|
||||
}
|
||||
|
||||
// create syft packages from Go modules found by the go toolchain
|
||||
func (c *goModCataloger) catalogModules(
|
||||
ctx context.Context,
|
||||
pkgs map[string][]pkgInfo,
|
||||
modules map[string]*packages.Module,
|
||||
reader file.LocationReadCloser,
|
||||
digests map[string]string,
|
||||
) ([]pkg.Package, map[string]artifact.Identifiable) {
|
||||
syftPackages := make([]pkg.Package, 0)
|
||||
moduleToPackage := make(map[string]artifact.Identifiable)
|
||||
|
||||
for _, m := range modules {
|
||||
if isRelativeImportOrMain(m.Path) {
|
||||
// relativeImport modules are already accounted for by their full module paths at other portions of syft's cataloging
|
||||
// example: something like ../../ found as a module for go.mod b, which is sub to go.mod a is accounted for
|
||||
// in another call to the goModCataloger when go.mod a is parsed
|
||||
// local modules that use a "main" heuristic, no module naming (sometimes common pre go module support)
|
||||
// are also not built as syft packages
|
||||
continue
|
||||
}
|
||||
|
||||
pkgInfos := pkgs[m.Path]
|
||||
moduleLicenses := resolveModuleLicenses(ctx, pkgInfos, afero.NewOsFs())
|
||||
// we do out of source lookups for module parsing
|
||||
// locations are NOT included in the SBOM because of this
|
||||
goModulePkg := pkg.Package{
|
||||
Name: m.Path,
|
||||
Version: m.Version,
|
||||
for _, m := range f.Require {
|
||||
lics := c.licenseResolver.getLicenses(ctx, resolver, m.Mod.Path, m.Mod.Version)
|
||||
packages[m.Mod.Path] = pkg.Package{
|
||||
Name: m.Mod.Path,
|
||||
Version: m.Mod.Version,
|
||||
Licenses: pkg.NewLicenseSet(lics...),
|
||||
Locations: file.NewLocationSet(reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
Licenses: moduleLicenses,
|
||||
PURL: packageURL(m.Mod.Path, m.Mod.Version),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
PURL: packageURL(m.Path, m.Version),
|
||||
Metadata: createSourceMetadata(digests[fmt.Sprintf("%s %s", m.Path, m.Version)]),
|
||||
}
|
||||
goModulePkg.SetID()
|
||||
|
||||
moduleToPackage[m.Path] = goModulePkg
|
||||
syftPackages = append(syftPackages, goModulePkg)
|
||||
}
|
||||
|
||||
return syftPackages, moduleToPackage
|
||||
}
|
||||
|
||||
// buildModuleRelationships creates artifact relationships between Go modules.
|
||||
func buildModuleRelationships(
|
||||
syftPkgs []pkg.Package,
|
||||
dependencies map[string][]string,
|
||||
moduleToPkg map[string]artifact.Identifiable,
|
||||
) []artifact.Relationship {
|
||||
var rels []artifact.Relationship
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
for _, fromPkg := range syftPkgs {
|
||||
for _, dep := range dependencies[fromPkg.Name] {
|
||||
if dep == fromPkg.Name {
|
||||
continue
|
||||
}
|
||||
toPkg, ok := moduleToPkg[dep]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
key := string(fromPkg.ID()) + string(toPkg.ID())
|
||||
if _, exists := seen[key]; exists {
|
||||
continue
|
||||
}
|
||||
|
||||
rels = append(rels, artifact.Relationship{
|
||||
From: toPkg, // dep
|
||||
To: fromPkg, // parent
|
||||
Type: artifact.DependencyOfRelationship,
|
||||
})
|
||||
seen[key] = struct{}{}
|
||||
Metadata: pkg.GolangModuleEntry{
|
||||
H1Digest: digests[fmt.Sprintf("%s %s", m.Mod.Path, m.Mod.Version)],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return rels
|
||||
}
|
||||
|
||||
func (c *goModCataloger) parseModFileContents(reader file.LocationReadCloser) (*modfile.File, error) {
|
||||
contents, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read go module: %w", err)
|
||||
}
|
||||
|
||||
f, err := modfile.Parse(reader.RealPath, contents, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse go module: %w", err)
|
||||
}
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// note this handles the deduplication from source by checking if the mod path exists in the sourceModules map
|
||||
func (c *goModCataloger) createGoModPackages(ctx context.Context, resolver file.Resolver, modFile *modfile.File, sourceModules map[string]*packages.Module, reader file.LocationReadCloser, digests map[string]string) map[string]pkg.Package {
|
||||
goModPackages := make(map[string]pkg.Package)
|
||||
|
||||
for _, m := range modFile.Require {
|
||||
if _, exists := sourceModules[m.Mod.Path]; !exists {
|
||||
lics := c.licenseResolver.getLicenses(ctx, resolver, m.Mod.Path, m.Mod.Version)
|
||||
goModPkg := pkg.Package{
|
||||
Name: m.Mod.Path,
|
||||
Version: m.Mod.Version,
|
||||
Licenses: pkg.NewLicenseSet(lics...),
|
||||
Locations: file.NewLocationSet(reader.WithAnnotation(pkg.EvidenceAnnotationKey, pkg.PrimaryEvidenceAnnotation)),
|
||||
PURL: packageURL(m.Mod.Path, m.Mod.Version),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{
|
||||
H1Digest: digests[fmt.Sprintf("%s %s", m.Mod.Path, m.Mod.Version)],
|
||||
},
|
||||
}
|
||||
goModPkg.SetID()
|
||||
goModPackages[m.Mod.Path] = goModPkg
|
||||
}
|
||||
}
|
||||
|
||||
return goModPackages
|
||||
}
|
||||
|
||||
// applyReplaceDirectives processes replace directives from go.mod
|
||||
func (c *goModCataloger) applyReplaceDirectives(ctx context.Context, resolver file.Resolver, modFile *modfile.File, goModPackages map[string]pkg.Package, reader file.LocationReadCloser, digests map[string]string) {
|
||||
for _, m := range modFile.Replace {
|
||||
// remove any old packages and replace with new ones...
|
||||
for _, m := range f.Replace {
|
||||
lics := c.licenseResolver.getLicenses(ctx, resolver, m.New.Path, m.New.Version)
|
||||
|
||||
// the old path and new path may be the same, in which case this is a noop,
|
||||
// but if they're different we need to remove the old package.
|
||||
// note that we may change the path but we should always reference the new version (since the old version
|
||||
// cannot be trusted as a correct value).
|
||||
var finalPath string
|
||||
if !strings.HasPrefix(m.New.Path, ".") && !strings.HasPrefix(m.New.Path, "/") {
|
||||
finalPath = m.New.Path
|
||||
delete(goModPackages, m.Old.Path)
|
||||
delete(packages, m.Old.Path)
|
||||
} else {
|
||||
finalPath = m.Old.Path
|
||||
}
|
||||
goModPkg := pkg.Package{
|
||||
packages[finalPath] = pkg.Package{
|
||||
Name: finalPath,
|
||||
Version: m.New.Version,
|
||||
Licenses: pkg.NewLicenseSet(lics...),
|
||||
@@ -365,31 +92,26 @@ func (c *goModCataloger) applyReplaceDirectives(ctx context.Context, resolver fi
|
||||
H1Digest: digests[fmt.Sprintf("%s %s", finalPath, m.New.Version)],
|
||||
},
|
||||
}
|
||||
goModPkg.SetID()
|
||||
goModPackages[finalPath] = goModPkg
|
||||
}
|
||||
}
|
||||
|
||||
func (c *goModCataloger) applyExcludeDirectives(modFile *modfile.File, goModPackages map[string]pkg.Package) {
|
||||
for _, m := range modFile.Exclude {
|
||||
delete(goModPackages, m.Mod.Path)
|
||||
// remove any packages from the exclude fields
|
||||
for _, m := range f.Exclude {
|
||||
delete(packages, m.Mod.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *goModCataloger) assembleResults(catalogedPkgs []pkg.Package, goModPackages map[string]pkg.Package) []pkg.Package {
|
||||
pkgsSlice := make([]pkg.Package, 0)
|
||||
|
||||
pkgsSlice = append(pkgsSlice, catalogedPkgs...)
|
||||
|
||||
for _, p := range goModPackages {
|
||||
pkgsSlice = append(pkgsSlice, p)
|
||||
pkgsSlice := make([]pkg.Package, len(packages))
|
||||
idx := 0
|
||||
for _, p := range packages {
|
||||
p.SetID()
|
||||
pkgsSlice[idx] = p
|
||||
idx++
|
||||
}
|
||||
|
||||
sort.SliceStable(pkgsSlice, func(i, j int) bool {
|
||||
return pkgsSlice[i].Name < pkgsSlice[j].Name
|
||||
})
|
||||
|
||||
return pkgsSlice
|
||||
return pkgsSlice, nil, nil
|
||||
}
|
||||
|
||||
func parseGoSumFile(resolver file.Resolver, reader file.LocationReadCloser) (map[string]string, error) {
|
||||
@@ -429,94 +151,3 @@ func parseGoSumFile(resolver file.Resolver, reader file.LocationReadCloser) (map
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// createSourceMetadata creates metadata for packages found through source analysis using build.Default
|
||||
func createSourceMetadata(h1Digest string) pkg.GolangSourceEntry {
|
||||
return pkg.GolangSourceEntry{
|
||||
H1Digest: h1Digest,
|
||||
OperatingSystem: build.Default.GOOS,
|
||||
Architecture: build.Default.GOARCH,
|
||||
BuildTags: strings.Join(build.Default.BuildTags, ","),
|
||||
CgoEnabled: build.Default.CgoEnabled,
|
||||
}
|
||||
}
|
||||
|
||||
func resolvePkgDir(p *packages.Package) string {
|
||||
switch {
|
||||
case len(p.GoFiles) > 0:
|
||||
return filepath.Dir(p.GoFiles[0])
|
||||
case len(p.CompiledGoFiles) > 0:
|
||||
return filepath.Dir(p.CompiledGoFiles[0])
|
||||
case len(p.OtherFiles) > 0:
|
||||
return filepath.Dir(p.OtherFiles[0])
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func shouldSkipVisit(p *packages.Package) bool {
|
||||
// skip packages that don't have module info
|
||||
if p.Module == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
// skip stdlib
|
||||
if isStdLib(p) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// isStdLib returns true if this package is part of the Go standard library.
|
||||
func isStdLib(pkg *packages.Package) bool {
|
||||
if pkg.Name == "unsafe" {
|
||||
// Special case unsafe stdlib, because it does not contain go files.
|
||||
return true
|
||||
}
|
||||
if len(pkg.GoFiles) == 0 {
|
||||
return false
|
||||
}
|
||||
prefix := build.Default.GOROOT
|
||||
sep := string(filepath.Separator)
|
||||
if !strings.HasSuffix(prefix, sep) {
|
||||
prefix += sep
|
||||
}
|
||||
return strings.HasPrefix(pkg.GoFiles[0], prefix)
|
||||
}
|
||||
|
||||
// handle replace directives
|
||||
func newModule(mod *packages.Module) *packages.Module {
|
||||
// Example of a module with replace directive: k8s.io/kubernetes => k8s.io/kubernetes v1.11.1
|
||||
// {
|
||||
// "Path": "k8s.io/kubernetes",
|
||||
// "Version": "v0.17.9",
|
||||
// "Replace": {
|
||||
// "Path": "k8s.io/kubernetes",
|
||||
// "Version": "v1.11.1",
|
||||
// "Time": "2018-07-17T04:20:29Z",
|
||||
// "Dir": "/home/gongyuan_kubeflow_org/go/pkg/mod/k8s.io/kubernetes@v1.11.1",
|
||||
// "GoMod": "/home/gongyuan_kubeflow_org/go/pkg/mod/cache/download/k8s.io/kubernetes/@v/v1.11.1.mod"
|
||||
// },
|
||||
// "Dir": "/home/gongyuan_kubeflow_org/go/pkg/mod/k8s.io/kubernetes@v1.11.1",
|
||||
// "GoMod": "/home/gongyuan_kubeflow_org/go/pkg/mod/cache/download/k8s.io/kubernetes/@v/v1.11.1.mod"
|
||||
// }
|
||||
// handle replace directives
|
||||
// Note, we specifically want to replace version field.
|
||||
// Haven't confirmed, but we may also need to override the
|
||||
// entire struct when using replace directive with local folders.
|
||||
tmp := *mod
|
||||
if tmp.Replace != nil {
|
||||
tmp = *tmp.Replace
|
||||
}
|
||||
|
||||
return &tmp
|
||||
}
|
||||
|
||||
func isRelativeImportOrMain(p string) bool {
|
||||
if p == "main" {
|
||||
return true
|
||||
}
|
||||
// true for ".", "..", "./...", "../..."
|
||||
return build.IsLocalImport(p)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
package golang
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
"github.com/anchore/syft/syft/file"
|
||||
"github.com/anchore/syft/syft/internal/fileresolver"
|
||||
"github.com/anchore/syft/syft/pkg"
|
||||
@@ -20,13 +15,13 @@ func TestParseGoMod(t *testing.T) {
|
||||
expected []pkg.Package
|
||||
}{
|
||||
{
|
||||
fixture: "test-fixtures/go-mod-fixtures/one-package/go.mod",
|
||||
fixture: "test-fixtures/one-package",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "github.com/bmatcuk/doublestar",
|
||||
Version: "v1.3.1",
|
||||
PURL: "pkg:golang/github.com/bmatcuk/doublestar@v1.3.1",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/one-package/go.mod")),
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/one-package")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
@@ -34,28 +29,28 @@ func TestParseGoMod(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
fixture: "test-fixtures/go-mod-fixtures/relative-replace/go.mod",
|
||||
fixture: "test-fixtures/relative-replace",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "github.com/aws/aws-sdk-go-v2",
|
||||
Version: "",
|
||||
PURL: "pkg:golang/github.com/aws/aws-sdk-go-v2",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/relative-replace")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/relative-replace/go.mod")),
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
fixture: "test-fixtures/go-mod-fixtures/many-packages/go.mod",
|
||||
fixture: "test-fixtures/many-packages",
|
||||
expected: []pkg.Package{
|
||||
{
|
||||
Name: "github.com/anchore/archiver/v3",
|
||||
Version: "v3.5.2",
|
||||
PURL: "pkg:golang/github.com/anchore/archiver@v3.5.2#v3",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/many-packages/go.mod")),
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/many-packages")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
@@ -64,7 +59,7 @@ func TestParseGoMod(t *testing.T) {
|
||||
Name: "github.com/anchore/go-testutils",
|
||||
Version: "v0.0.0-20200624184116-66aa578126db",
|
||||
PURL: "pkg:golang/github.com/anchore/go-testutils@v0.0.0-20200624184116-66aa578126db",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/many-packages/go.mod")),
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/many-packages")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
@@ -73,7 +68,7 @@ func TestParseGoMod(t *testing.T) {
|
||||
Name: "github.com/anchore/go-version",
|
||||
Version: "v1.2.2-0.20200701162849-18adb9c92b9b",
|
||||
PURL: "pkg:golang/github.com/anchore/go-version@v1.2.2-0.20200701162849-18adb9c92b9b",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/many-packages/go.mod")),
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/many-packages")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
@@ -82,7 +77,7 @@ func TestParseGoMod(t *testing.T) {
|
||||
Name: "github.com/anchore/stereoscope",
|
||||
Version: "v0.0.0-20200706164556-7cf39d7f4639",
|
||||
PURL: "pkg:golang/github.com/anchore/stereoscope@v0.0.0-20200706164556-7cf39d7f4639",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/many-packages/go.mod")),
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/many-packages")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
@@ -91,7 +86,7 @@ func TestParseGoMod(t *testing.T) {
|
||||
Name: "github.com/bmatcuk/doublestar",
|
||||
Version: "v8.8.8",
|
||||
PURL: "pkg:golang/github.com/bmatcuk/doublestar@v8.8.8",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/many-packages/go.mod")),
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/many-packages")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
@@ -100,7 +95,7 @@ func TestParseGoMod(t *testing.T) {
|
||||
Name: "github.com/go-test/deep",
|
||||
Version: "v1.0.6",
|
||||
PURL: "pkg:golang/github.com/go-test/deep@v1.0.6",
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/go-mod-fixtures/many-packages/go.mod")),
|
||||
Locations: file.NewLocationSet(file.NewLocation("test-fixtures/many-packages")),
|
||||
Language: pkg.Go,
|
||||
Type: pkg.GoModulePkg,
|
||||
Metadata: pkg.GolangModuleEntry{},
|
||||
@@ -184,156 +179,3 @@ func Test_corruptGoMod(t *testing.T) {
|
||||
WithError().
|
||||
TestCataloger(t, c)
|
||||
}
|
||||
|
||||
func Test_parseGoSource_packageResolution(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
fixturePath string
|
||||
config CatalogerConfig
|
||||
expectedPkgs []string
|
||||
expectedRels []string
|
||||
expectedLicenses map[string][]string
|
||||
}{
|
||||
{
|
||||
name: "go-source with direct, transitive, and deps of transitive",
|
||||
fixturePath: filepath.Join("test-fixtures", "go-source"),
|
||||
expectedPkgs: []string{
|
||||
"anchore.io/not/real @ (go.mod)",
|
||||
"github.com/davecgh/go-spew @ v1.1.1 (go.mod)",
|
||||
"github.com/go-viper/mapstructure/v2 @ v2.2.1 (go.mod)",
|
||||
"github.com/google/uuid @ v1.6.0 (go.mod)",
|
||||
"github.com/pmezard/go-difflib @ v1.0.0 (go.mod)",
|
||||
"github.com/sagikazarmark/locafero @ v0.7.0 (go.mod)",
|
||||
"github.com/sirupsen/logrus @ v1.9.3 (go.mod)",
|
||||
"github.com/sourcegraph/conc @ v0.3.0 (go.mod)",
|
||||
"github.com/spf13/afero @ v1.12.0 (go.mod)",
|
||||
"github.com/spf13/cast @ v1.7.1 (go.mod)",
|
||||
"github.com/spf13/pflag @ v1.0.6 (go.mod)",
|
||||
"github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod)",
|
||||
"github.com/subosito/gotenv @ v1.6.0 (go.mod)",
|
||||
"go.uber.org/multierr @ v1.10.0 (go.mod)",
|
||||
"go.uber.org/zap @ v1.27.0 (go.mod)",
|
||||
"golang.org/x/sys @ v0.33.0 (go.mod)",
|
||||
"golang.org/x/text @ v0.21.0 (go.mod)",
|
||||
"gopkg.in/yaml.v3 @ v3.0.1 (go.mod)",
|
||||
"github.com/fsnotify/fsnotify @ v1.8.0 (go.mod)",
|
||||
"github.com/pelletier/go-toml/v2 @ v2.2.3 (go.mod)",
|
||||
"github.com/frankban/quicktest @ v1.14.6 (go.mod)",
|
||||
"github.com/google/go-cmp @ v0.6.0 (go.mod)",
|
||||
"github.com/kr/pretty @ v0.3.1 (go.mod)",
|
||||
"github.com/kr/text @ v0.2.0 (go.mod)",
|
||||
"github.com/rogpeppe/go-internal @ v1.9.0 (go.mod)",
|
||||
"go.uber.org/goleak @ v1.3.0 (go.mod)",
|
||||
"gopkg.in/check.v1 @ v1.0.0-20190902080502-41f04d3bba15 (go.mod)",
|
||||
},
|
||||
expectedRels: []string{
|
||||
"github.com/davecgh/go-spew @ v1.1.1 (go.mod) [dependency-of] github.com/stretchr/testify @ v1.10.0 (go.mod)",
|
||||
"github.com/frankban/quicktest @ v1.14.6 (go.mod) [dependency-of] github.com/spf13/cast @ v1.7.1 (go.mod)",
|
||||
"github.com/fsnotify/fsnotify @ v1.8.0 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/go-viper/mapstructure/v2 @ v2.2.1 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/google/go-cmp @ v0.6.0 (go.mod) [dependency-of] github.com/frankban/quicktest @ v1.14.6 (go.mod)",
|
||||
"github.com/google/uuid @ v1.6.0 (go.mod) [dependency-of] anchore.io/not/real @ (go.mod)",
|
||||
"github.com/kr/pretty @ v0.3.1 (go.mod) [dependency-of] github.com/frankban/quicktest @ v1.14.6 (go.mod)",
|
||||
"github.com/kr/pretty @ v0.3.1 (go.mod) [dependency-of] gopkg.in/check.v1 @ v1.0.0-20190902080502-41f04d3bba15 (go.mod)",
|
||||
"github.com/kr/text @ v0.2.0 (go.mod) [dependency-of] github.com/kr/pretty @ v0.3.1 (go.mod)",
|
||||
"github.com/pelletier/go-toml/v2 @ v2.2.3 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/pmezard/go-difflib @ v1.0.0 (go.mod) [dependency-of] github.com/stretchr/testify @ v1.10.0 (go.mod)",
|
||||
"github.com/rogpeppe/go-internal @ v1.9.0 (go.mod) [dependency-of] github.com/kr/pretty @ v0.3.1 (go.mod)",
|
||||
"github.com/sagikazarmark/locafero @ v0.7.0 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/sirupsen/logrus @ v1.9.3 (go.mod) [dependency-of] anchore.io/not/real @ (go.mod)",
|
||||
"github.com/sourcegraph/conc @ v0.3.0 (go.mod) [dependency-of] github.com/sagikazarmark/locafero @ v0.7.0 (go.mod)",
|
||||
"github.com/spf13/afero @ v1.12.0 (go.mod) [dependency-of] github.com/sagikazarmark/locafero @ v0.7.0 (go.mod)",
|
||||
"github.com/spf13/afero @ v1.12.0 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/spf13/cast @ v1.7.1 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/spf13/pflag @ v1.0.6 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/spf13/viper @ v1.20.1 (go.mod) [dependency-of] anchore.io/not/real @ (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] anchore.io/not/real @ (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] github.com/pelletier/go-toml/v2 @ v2.2.3 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] github.com/sagikazarmark/locafero @ v0.7.0 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] github.com/sirupsen/logrus @ v1.9.3 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] github.com/sourcegraph/conc @ v0.3.0 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] github.com/subosito/gotenv @ v1.6.0 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] go.uber.org/multierr @ v1.10.0 (go.mod)",
|
||||
"github.com/stretchr/testify @ v1.10.0 (go.mod) [dependency-of] go.uber.org/zap @ v1.27.0 (go.mod)",
|
||||
"github.com/subosito/gotenv @ v1.6.0 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"go.uber.org/goleak @ v1.3.0 (go.mod) [dependency-of] go.uber.org/zap @ v1.27.0 (go.mod)",
|
||||
"go.uber.org/multierr @ v1.10.0 (go.mod) [dependency-of] go.uber.org/zap @ v1.27.0 (go.mod)",
|
||||
"go.uber.org/zap @ v1.27.0 (go.mod) [dependency-of] anchore.io/not/real @ (go.mod)",
|
||||
"golang.org/x/sys @ v0.33.0 (go.mod) [dependency-of] github.com/fsnotify/fsnotify @ v1.8.0 (go.mod)",
|
||||
"golang.org/x/sys @ v0.33.0 (go.mod) [dependency-of] github.com/sirupsen/logrus @ v1.9.3 (go.mod)",
|
||||
"golang.org/x/text @ v0.21.0 (go.mod) [dependency-of] github.com/spf13/afero @ v1.12.0 (go.mod)",
|
||||
"golang.org/x/text @ v0.21.0 (go.mod) [dependency-of] github.com/subosito/gotenv @ v1.6.0 (go.mod)",
|
||||
"gopkg.in/check.v1 @ v1.0.0-20190902080502-41f04d3bba15 (go.mod) [dependency-of] gopkg.in/yaml.v3 @ v3.0.1 (go.mod)",
|
||||
"gopkg.in/yaml.v3 @ v3.0.1 (go.mod) [dependency-of] github.com/spf13/viper @ v1.20.1 (go.mod)",
|
||||
"gopkg.in/yaml.v3 @ v3.0.1 (go.mod) [dependency-of] github.com/stretchr/testify @ v1.10.0 (go.mod)",
|
||||
"gopkg.in/yaml.v3 @ v3.0.1 (go.mod) [dependency-of] go.uber.org/zap @ v1.27.0 (go.mod)",
|
||||
},
|
||||
expectedLicenses: map[string][]string{
|
||||
"github.com/fsnotify/fsnotify": {"BSD-3-Clause"},
|
||||
"github.com/go-viper/mapstructure/v2": {"MIT"},
|
||||
"github.com/google/uuid": {"BSD-3-Clause"},
|
||||
"github.com/pelletier/go-toml/v2": {"MIT"},
|
||||
"github.com/sagikazarmark/locafero": {"MIT"},
|
||||
"github.com/sirupsen/logrus": {"MIT"},
|
||||
"github.com/sourcegraph/conc": {"MIT"},
|
||||
"github.com/spf13/afero": {"Apache-2.0"},
|
||||
"github.com/spf13/cast": {"MIT"},
|
||||
"github.com/spf13/pflag": {"BSD-3-Clause"},
|
||||
"github.com/spf13/viper": {"MIT"},
|
||||
"github.com/subosito/gotenv": {"MIT"},
|
||||
"go.uber.org/multierr": {"MIT"},
|
||||
"go.uber.org/zap": {"MIT"},
|
||||
"golang.org/x/sys": {"BSD-3-Clause"},
|
||||
"golang.org/x/text": {"BSD-3-Clause"},
|
||||
"gopkg.in/yaml.v3": {"Apache-2.0", "MIT"},
|
||||
"github.com/davecgh/go-spew": {"ISC"},
|
||||
"github.com/pmezard/go-difflib": {"BSD-3-Clause"},
|
||||
"github.com/stretchr/testify": {"MIT"},
|
||||
"github.com/frankban/quicktest": {"MIT"},
|
||||
"github.com/google/go-cmp": {"BSD-3-Clause"},
|
||||
"github.com/kr/text": {"MIT"},
|
||||
"github.com/kr/pretty": {"MIT"},
|
||||
"github.com/rogpeppe/go-internal": {"BSD-3-Clause"},
|
||||
"go.uber.org/goleak": {"MIT"},
|
||||
"gopkg.in/check.v1": {"BSD-2-Clause"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
pkgtest.NewCatalogTester().
|
||||
FromDirectory(t, tt.fixturePath).
|
||||
ExpectsPackageStrings(tt.expectedPkgs).
|
||||
ExpectsRelationshipStrings(tt.expectedRels).
|
||||
ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
|
||||
for _, p := range pkgs {
|
||||
if metadata, ok := p.Metadata.(pkg.GolangSourceEntry); ok {
|
||||
// Validate that GolangSourceEntry metadata is present but don't assert on specific field values
|
||||
// since these might vary across development machines
|
||||
require.IsType(t, pkg.GolangSourceEntry{}, metadata, "expected GolangSourceEntry metadata for package %s", p.Name)
|
||||
// Verify that the metadata struct is populated (non-zero values indicate go source method was used)
|
||||
require.NotEmpty(t, metadata, "GolangSourceEntry metadata should not be empty for package %s", p.Name)
|
||||
}
|
||||
}
|
||||
}).
|
||||
ExpectsAssertion(func(t *testing.T, pkgs []pkg.Package, relationships []artifact.Relationship) {
|
||||
actualLicenses := make(map[string][]string)
|
||||
for _, p := range pkgs {
|
||||
for _, l := range p.Licenses.ToSlice() {
|
||||
if actualLicenses[p.Name] == nil {
|
||||
actualLicenses[p.Name] = make([]string, 0)
|
||||
}
|
||||
actualLicenses[p.Name] = append(actualLicenses[p.Name], l.Value)
|
||||
}
|
||||
}
|
||||
if diff := cmp.Diff(tt.expectedLicenses, actualLicenses); diff != "" {
|
||||
t.Errorf("mismatch in licenses (-want +got):\n%s", diff)
|
||||
}
|
||||
}).
|
||||
TestCataloger(t, NewGoModuleFileCataloger(CatalogerConfig{}))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
github.com/bmatcuk/doublestar v1.3.1/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"anchore.io/not/real/pk1"
|
||||
"anchore.io/not/real/pk2"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pk1.Test()
|
||||
pk2.Test()
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"anchore.io/not/real/pk1"
|
||||
"anchore.io/not/real/pk3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
pk1.Test()
|
||||
pk3.Zap()
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
module anchore.io/not/real
|
||||
|
||||
go 1.24.3
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/viper v1.20.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
go.uber.org/zap v1.27.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.7.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.12.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/text v0.21.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
@@ -1,60 +0,0 @@
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo=
|
||||
github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||
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/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs=
|
||||
github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4=
|
||||
github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y=
|
||||
github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
|
||||
go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -1,17 +0,0 @@
|
||||
package pk1
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func Test() {
|
||||
return
|
||||
}
|
||||
|
||||
func NewID() string {
|
||||
log.WithFields(log.Fields{
|
||||
"animal": "walrus",
|
||||
}).Info("A walrus appears")
|
||||
return uuid.New().String()
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
package pk1
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewID(t *testing.T) {
|
||||
id := NewID()
|
||||
assert.NotEmpty(t, id)
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
package pk2
|
||||
|
||||
func Test() {
|
||||
return
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package pk3
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/spf13/viper"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func Zap() {
|
||||
sugar := zap.NewExample().Sugar()
|
||||
defer sugar.Sync()
|
||||
sugar.Infow("failed to fetch URL",
|
||||
"url", "http://example.com",
|
||||
"attempt", 3,
|
||||
"backoff", time.Second,
|
||||
)
|
||||
sugar.Infof("failed to fetch URL: %s", "http://example.com")
|
||||
viper.SetDefault("ContentDir", "content")
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -17,7 +16,6 @@ import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
stereofile "github.com/anchore/stereoscope/pkg/file"
|
||||
"github.com/anchore/stereoscope/pkg/imagetest"
|
||||
"github.com/anchore/syft/internal/cmptest"
|
||||
"github.com/anchore/syft/internal/licenses"
|
||||
@@ -132,14 +130,11 @@ func (p *CatalogTester) FromFile(t *testing.T, path string) *CatalogTester {
|
||||
return p
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
fixture, err := os.Open(path)
|
||||
require.NoError(t, err)
|
||||
|
||||
p.reader = file.LocationReadCloser{
|
||||
Location: file.NewVirtualLocationFromDirectory(fixture.Name(), fixture.Name(), *stereofile.NewFileReference(stereofile.Path(absPath))),
|
||||
Location: file.NewLocation(fixture.Name()),
|
||||
ReadCloser: fixture,
|
||||
}
|
||||
return p
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/internal/log"
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
|
||||
"github.com/anchore/syft/internal/unknown"
|
||||
"github.com/anchore/syft/syft/artifact"
|
||||
|
||||
@@ -15,12 +15,3 @@ type GolangBinaryBuildinfoEntry struct {
|
||||
type GolangModuleEntry struct {
|
||||
H1Digest string `json:"h1Digest,omitempty" cyclonedx:"h1Digest"`
|
||||
}
|
||||
|
||||
// GolangSourceEntry represents all captured data for a Golang package found through source analysis
|
||||
type GolangSourceEntry struct {
|
||||
H1Digest string `json:"h1Digest,omitempty" cyclonedx:"h1Digest"`
|
||||
OperatingSystem string `json:"os,omitempty" cyclonedx:"os"`
|
||||
Architecture string `json:"architecture,omitempty" cyclonedx:"architecture"`
|
||||
BuildTags string `json:"buildTags,omitempty" cyclonedx:"buildTags"`
|
||||
CgoEnabled bool `json:"cgoEnabled" cyclonedx:"cgoEnabled"`
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.yaml.in/yaml/v3"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func Test_configLoading(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user