diff --git a/docs/docs/coverage/language/dart.md b/docs/docs/coverage/language/dart.md index 0ce6d1cc52b0..a64a19eb83c2 100644 --- a/docs/docs/coverage/language/dart.md +++ b/docs/docs/coverage/language/dart.md @@ -4,9 +4,9 @@ Trivy supports [Dart][dart]. The following scanners are supported. -| Package manager | SBOM | Vulnerability | License | -|-------------------------| :---: | :-----------: |:-------:| -| [Dart][dart-repository] | ✓ | ✓ | - | +| Package manager | SBOM | Vulnerability | License | +|-------------------------|:----:|:-------------:|:-------:| +| [Dart][dart-repository] | ✓ | ✓ | - | The following table provides an outline of the features Trivy offers. @@ -21,6 +21,24 @@ In order to detect dependencies, Trivy searches for `pubspec.lock`. Trivy marks indirect dependencies, but `pubspec.lock` file doesn't have options to separate root and dev transitive dependencies. So Trivy includes all dependencies in report. +### SDK dependencies +Dart uses version `0.0.0` for SDK dependencies (e.g. Flutter). It is not possible to accurately determine the versions of these dependencies. + +Therefore, we use the first version of the constraint for the SDK. + +For example in this case the version of `flutter` should be `3.3.0`: +```yaml +flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: "^3.3.0" +``` + +### Dependency tree To build `dependency tree` Trivy parses [cache directory][cache-directory]. Currently supported default directories and `PUB_CACHE` environment (absolute path only). !!! note Make sure the cache directory contains all the dependencies installed in your application. To download missing dependencies, use `dart pub get` command. diff --git a/go.mod b/go.mod index a0275a82c98e..ec12d16a8f5a 100644 --- a/go.mod +++ b/go.mod @@ -21,7 +21,7 @@ require ( github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798 github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 - github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 + github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d github.com/aquasecurity/loading v0.0.5 github.com/aquasecurity/table v1.8.0 github.com/aquasecurity/testdocker v0.0.0-20240419073403-90bd43849334 diff --git a/go.sum b/go.sum index f5e2ad6aadfa..943bee0a926c 100644 --- a/go.sum +++ b/go.sum @@ -761,8 +761,9 @@ github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798/go.mod github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46 h1:vmXNl+HDfqqXgr0uY1UgK1GAhps8nbAAtqHNBcgyf+4= github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46/go.mod h1:olhPNdiiAAMiSujemd1O/sc6GcyePr23f/6uGKtthNg= github.com/aquasecurity/go-version v0.0.0-20201107203531-5e48ac5d022a/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= -github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492 h1:rcEG5HI490FF0a7zuvxOxen52ddygCfNVjP0XOCMl+M= github.com/aquasecurity/go-version v0.0.0-20210121072130-637058cfe492/go.mod h1:9Beu8XsUNNfzml7WBf3QmyPToP1wm1Gj/Vc5UJKqTzU= +github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d h1:4zour5Sh9chOg+IqIinIcJ3qtr3cIf8FdFY6aArlXBw= +github.com/aquasecurity/go-version v0.0.0-20240603093900-cf8a8d29271d/go.mod h1:1cPOp4BaQZ1G2F5fnw4dFz6pkOyXJI9KTuak8ghIl3U= github.com/aquasecurity/loading v0.0.5 h1:2iq02sPSSMU+ULFPmk0v0lXnK/eZ2e0dRAj/Dl5TvuM= github.com/aquasecurity/loading v0.0.5/go.mod h1:NSHeeq1JTDTFuXAe87q4yQ2DX57pXiaQMqq8Zm9HCJA= github.com/aquasecurity/table v1.8.0 h1:9ntpSwrUfjrM6/YviArlx/ZBGd6ix8W+MtojQcM7tv0= diff --git a/pkg/dependency/parser/dart/pub/parse.go b/pkg/dependency/parser/dart/pub/parse.go index 10b1e7ab729a..1bd85a56f1d6 100644 --- a/pkg/dependency/parser/dart/pub/parse.go +++ b/pkg/dependency/parser/dart/pub/parse.go @@ -4,8 +4,10 @@ import ( "golang.org/x/xerrors" "gopkg.in/yaml.v3" + goversion "github.com/aquasecurity/go-version/pkg/version" "github.com/aquasecurity/trivy/pkg/dependency" ftypes "github.com/aquasecurity/trivy/pkg/fanal/types" + "github.com/aquasecurity/trivy/pkg/log" xio "github.com/aquasecurity/trivy/pkg/x/io" ) @@ -16,21 +18,30 @@ const ( ) // Parser is a parser for pubspec.lock -type Parser struct{} +type Parser struct { + logger *log.Logger +} func NewParser() *Parser { - return &Parser{} + return &Parser{ + logger: log.WithPrefix("pub"), + } } type lock struct { - Packages map[string]Dep `yaml:"packages"` + Packages map[string]Dep `yaml:"packages"` + Sdks map[string]string `yaml:"sdks"` } type Dep struct { - Dependency string `yaml:"dependency"` - Version string `yaml:"version"` + Dependency string `yaml:"dependency"` + Version string `yaml:"version"` + Source string `yaml:"source"` + Description Description `yaml:"description"` } +type Description string + func (p Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) { l := &lock{} if err := yaml.NewDecoder(r).Decode(&l); err != nil { @@ -38,15 +49,20 @@ func (p Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency } var pkgs []ftypes.Package for name, dep := range l.Packages { + version := dep.Version + if version == "0.0.0" && dep.Source == "sdk" { + version = p.findSDKVersion(l, name, dep) + } + // We would like to exclude dev dependencies, but we cannot identify // which indirect dependencies were introduced by dev dependencies // as there are 3 dependency types, "direct main", "direct dev" and "transitive". // It will be confusing if we exclude direct dev dependencies and include transitive dev dependencies. // We decided to keep all dev dependencies until Pub will add support for "transitive main" and "transitive dev". pkg := ftypes.Package{ - ID: dependency.ID(ftypes.Pub, name, dep.Version), + ID: dependency.ID(ftypes.Pub, name, version), Name: name, - Version: dep.Version, + Version: version, Relationship: p.relationship(dep.Dependency), } pkgs = append(pkgs, pkg) @@ -55,6 +71,31 @@ func (p Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency return pkgs, nil, nil } +// findSDKVersion detects the first version of the SDK constraint specified in the Description. +// If the constraint is not found, it returns the original version. +func (p Parser) findSDKVersion(l *lock, name string, dep Dep) string { + // Some dependencies use one of the SDK versions. + // In this case dep.Version == `0.0.0`. + // We can't get versions for these dependencies. + // Therefore, we use the first version of the SDK constraint specified in the Description. + // See https://github.com/aquasecurity/trivy/issues/6017 + constraint, ok := l.Sdks[string(dep.Description)] + if !ok { + return dep.Version + } + + v, err := firstVersionOfConstrain(constraint) + if err != nil { + p.logger.Warn("Unable to get sdk version from constraint", log.Err(err)) + return dep.Version + } else if v == "" { + return dep.Version + } + p.logger.Info("Using the first version of the constraint from the sdk source", log.String("dep", name), + log.String("constraint", constraint)) + return v +} + func (p Parser) relationship(dep string) ftypes.Relationship { switch dep { case directMain, directDev: @@ -64,3 +105,37 @@ func (p Parser) relationship(dep string) ftypes.Relationship { } return ftypes.RelationshipUnknown } + +// firstVersionOfConstrain returns the first acceptable version for constraint +func firstVersionOfConstrain(constraint string) (string, error) { + css, err := goversion.NewConstraints(constraint) + if err != nil { + return "", xerrors.Errorf("unable to parse constraints: %w", err) + } + + // Dart uses only `>=` and `^` operators: + // cf. https://dart.dev/tools/pub/dependencies#traditional-syntax + constraints := css.List() + if len(constraints) == 0 || len(constraints[0]) == 0 { + return "", nil + } + // We only need to get the first version from the range + if constraints[0][0].Operator() != ">=" && constraints[0][0].Operator() != "^" { + return "", nil + } + + return constraints[0][0].Version(), nil +} + +func (d *Description) UnmarshalYAML(value *yaml.Node) error { + var tmp any + if err := value.Decode(&tmp); err != nil { + return err + } + // Description can be a string or a struct + // We only need a string value for SDK mapping + if desc, ok := tmp.(string); ok { + *d = Description(desc) + } + return nil +} diff --git a/pkg/dependency/parser/dart/pub/parse_test.go b/pkg/dependency/parser/dart/pub/parse_test.go index be698a7933c5..5f4591201b7c 100644 --- a/pkg/dependency/parser/dart/pub/parse_test.go +++ b/pkg/dependency/parser/dart/pub/parse_test.go @@ -31,9 +31,9 @@ func TestParser_Parse(t *testing.T) { Relationship: ftypes.RelationshipDirect, }, { - ID: "flutter_test@0.0.0", + ID: "flutter_test@3.3.0", Name: "flutter_test", - Version: "0.0.0", + Version: "3.3.0", Relationship: ftypes.RelationshipDirect, }, { diff --git a/pkg/dependency/parser/dart/pub/testdata/happy.lock b/pkg/dependency/parser/dart/pub/testdata/happy.lock index 3a37840aa3bb..b589a2ba3f0a 100644 --- a/pkg/dependency/parser/dart/pub/testdata/happy.lock +++ b/pkg/dependency/parser/dart/pub/testdata/happy.lock @@ -22,4 +22,4 @@ packages: version: "3.0.6" sdks: dart: ">=2.18.0 <3.0.0" - flutter: ">=3.3.0" \ No newline at end of file + flutter: "^3.3.0" \ No newline at end of file