Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(dart): use first version of constraint for dependencies using SDK version #6239

Merged
24 changes: 21 additions & 3 deletions docs/docs/coverage/language/dart.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
89 changes: 82 additions & 7 deletions pkg/dependency/parser/dart/pub/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
)

Expand All @@ -16,37 +18,51 @@ 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 {
return nil, nil, xerrors.Errorf("failed to decode pubspec.lock: %w", err)
}
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)
Expand All @@ -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:
Expand All @@ -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() != "^" {
Copy link
Collaborator

@knqyf263 knqyf263 Jun 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we check the length of constratints so it won't panic? I presume css.List() doesn't return empty, but unexpected things always happen. It's better to ensure it's non-empty.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it won't hurt.
Added check in 35a6f0a

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
}
4 changes: 2 additions & 2 deletions pkg/dependency/parser/dart/pub/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
{
Expand Down
2 changes: 1 addition & 1 deletion pkg/dependency/parser/dart/pub/testdata/happy.lock
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,4 @@ packages:
version: "3.0.6"
sdks:
dart: ">=2.18.0 <3.0.0"
flutter: ">=3.3.0"
flutter: "^3.3.0"