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): add graph support #5374

Merged
merged 11 commits into from
Oct 20, 2023
Merged
65 changes: 38 additions & 27 deletions docs/docs/configuration/reporting.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,32 +41,31 @@ In some cases, vulnerable dependencies are not linked directly, and it requires
To make this task simpler Trivy can show a dependency origin tree with the `--dependency-tree` flag.
This flag is only available with the `--format table` flag.

The following packages/languages are currently supported:

- OS packages
- apk
- dpkg
- rpm
- Node.js
- npm: package-lock.json
- pnpm: pnpm-lock.yaml
- yarn: yarn.lock
- .NET
- NuGet: packages.lock.json
- Python
- Poetry: poetry.lock
- Ruby
- Bundler: Gemfile.lock
- Rust
- Binaries built with [cargo-auditable][cargo-auditable]
- Go
- Modules: go.mod
- PHP
- Composer
- Java
- Maven: pom.xml

This tree is the reverse of the npm list command.
The following OS package managers are currently supported:

| OS Package Managers |
|---------------------|
| apk |
| dpkg |
| rpm |

The following languages are currently supported:

| Language | File |
|----------|--------------------------------------------|
| Node.js | [package-lock.json][nodejs-package-lock] |
| | [pnpm-lock.yaml][pnpm-lock] |
| | [yarn.lock][yarn-lock] |
| .NET | [packages.lock.json][dotnet-packages-lock] |
| Python | [poetry.lock][poetry-lock] |
| Ruby | [Gemfile.lock][gemfile-lock] |
| Rust | [cargo-auditable binaries][cargo-binaries] |
| Go | [go.mod][go-mod] |
| PHP | [composer.lock][composer-lock] |
| Java | [pom.xml][pom-xml] |
| Dart | [pubspec.lock][pubspec-lock] |

This tree is the reverse of the dependency graph.
However, if you want to resolve a vulnerability in a particular indirect dependency, the reversed tree is useful to know where that dependency comes from and identify which package you actually need to update.

In table output, it looks like:
Expand Down Expand Up @@ -408,4 +407,16 @@ $ trivy convert --format table --severity CRITICAL result.json
[github-sbom-submit]: https://docs.github.com/en/rest/dependency-graph/dependency-submission?apiVersion=2022-11-28#create-a-snapshot-of-dependencies-for-a-repository

[os_packages]: ../scanner/vulnerability.md#os-packages
[language_packages]: ../scanner/vulnerability.md#language-specific-packages
[language_packages]: ../scanner/vulnerability.md#language-specific-packages

[nodejs-package-lock]: ../coverage/language/nodejs.md#npm
[pnpm-lock]: ../coverage/language/nodejs.md#pnpm
[yarn-lock]: ../coverage/language/nodejs.md#yarn
[dotnet-packages-lock]: ../coverage/language/dotnet.md#packageslockjson
[poetry-lock]: ../coverage/language/python.md#poetry
[gemfile-lock]: ../coverage/language/ruby.md#bundler
[go-mod]: ../coverage/language/golang.md#go-modules
[composer-lock]: ../coverage/language/php.md#composer
[pom-xml]: ../coverage/language/java.md#pomxml
[pubspec-lock]: ../coverage/language/dart.md#dart
[cargo-binaries]: ../coverage/language/rust.md#binaries
7 changes: 6 additions & 1 deletion docs/docs/coverage/language/dart.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,19 @@ The following table provides an outline of the features Trivy offers.

| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
|-------------------------|--------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
| [Dart][dart-repository] | pubspec.lock | ✓ | Included | - | - |
| [Dart][dart-repository] | pubspec.lock | ✓ | Included | | - |

## Dart
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.

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.

[dart]: https://dart.dev/
[dart-repository]: https://pub.dev/
[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies
[cache-directory]: https://dart.dev/tools/pub/glossary#system-cache
154 changes: 145 additions & 9 deletions pkg/fanal/analyzer/language/dart/pub/pubspec.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,35 +2,171 @@ package pub

import (
"context"
"io"
"io/fs"
"os"
"path/filepath"
"runtime"
"sort"

"github.com/samber/lo"
"golang.org/x/exp/maps"
"golang.org/x/xerrors"
"gopkg.in/yaml.v3"

"github.com/aquasecurity/go-dep-parser/pkg/dart/pub"
godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
"github.com/aquasecurity/go-dep-parser/pkg/utils"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)

func init() {
analyzer.RegisterAnalyzer(&pubSpecLockAnalyzer{})
analyzer.RegisterPostAnalyzer(analyzer.TypePubSpecLock, newPubSpecLockAnalyzer)
}

const (
version = 1
version = 2
pubSpecYamlFileName = "pubspec.yaml"
)

// pubSpecLockAnalyzer analyzes pubspec.lock
type pubSpecLockAnalyzer struct{}
// pubSpecLockAnalyzer analyzes `pubspec.lock`
type pubSpecLockAnalyzer struct {
parser godeptypes.Parser
}

func newPubSpecLockAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
return pubSpecLockAnalyzer{
parser: pub.NewParser(),
}, nil
}

func (a pubSpecLockAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
p := pub.NewParser()
res, err := language.Analyze(types.Pub, input.FilePath, input.Content, p)
func (a pubSpecLockAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
var apps []types.Application

// get all DependsOn from cache dir
// lib ID -> DependsOn names
allDependsOn, err := findDependsOn()
if err != nil {
return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err)
log.Logger.Warnf("Unable to parse cache dir: %s", err)
}

required := func(path string, d fs.DirEntry) bool {
return filepath.Base(path) == types.PubSpecLock
}

err = fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error {
app, err := language.Parse(types.Pub, path, r, a.parser)
if err != nil {
return xerrors.Errorf("unable to parse %q: %w", path, err)
}

if app == nil {
return nil
}

if allDependsOn != nil {
// Required to search for library versions for DependsOn.
libs := lo.SliceToMap(app.Libraries, func(lib types.Package) (string, string) {
return lib.Name, lib.ID
})

for i, lib := range app.Libraries {
var dependsOn []string
for _, depName := range allDependsOn[lib.ID] {
if depID, ok := libs[depName]; ok {
dependsOn = append(dependsOn, depID)
}
}
app.Libraries[i].DependsOn = dependsOn
}
}

sort.Sort(app.Libraries)
apps = append(apps, *app)
return nil
})
if err != nil {
return nil, xerrors.Errorf("walk error: %w", err)
}

return &analyzer.AnalysisResult{
Applications: apps,
}, nil
}

func findDependsOn() (map[string][]string, error) {
dir := cacheDir()
if !fsutils.DirExists(dir) {
log.Logger.Debugf("Cache dir (%s) not found. Need 'dart pub get' to fill dependency relationships", dir)
return nil, nil
}

required := func(path string, d fs.DirEntry) bool {
return filepath.Base(path) == pubSpecYamlFileName
}
return res, nil

deps := make(map[string][]string)
if err := fsutils.WalkDir(os.DirFS(dir), ".", required, func(path string, d fs.DirEntry, r io.Reader) error {
id, dependsOn, err := parsePubSpecYaml(r)
if err != nil {
log.Logger.Debugf("Unable to parse %q: %s", path, err)
return nil
}
if id != "" {
deps[id] = dependsOn
}
return nil

}); err != nil {
return nil, xerrors.Errorf("walk error: %w", err)
}
return deps, nil
}

// https://dart.dev/tools/pub/glossary#system-cache
func cacheDir() string {
if dir := os.Getenv("PUB_CACHE"); dir != "" {
return dir
}

// `%LOCALAPPDATA%\Pub\Cache` for Windows
if runtime.GOOS == "windows" {
return filepath.Join(os.Getenv("LOCALAPPDATA"), "Pub", "Cache")
}

// `~/.pub-cache` for Linux or Mac
return filepath.Join(os.Getenv("HOME"), ".pub_cache")
}

type pubSpecYaml struct {
Name string `yaml:"name"`
Version string `yaml:"version,omitempty"`
Dependencies map[string]interface{} `yaml:"dependencies,omitempty"`
}

func parsePubSpecYaml(r io.Reader) (string, []string, error) {
var spec pubSpecYaml
if err := yaml.NewDecoder(r).Decode(&spec); err != nil {
return "", nil, xerrors.Errorf("unable to decode: %w", err)
}

// Version is a required field only for packages from pub.dev:
// https://dart.dev/tools/pub/pubspec#version
// We can skip packages without version,
// because we compare packages by ID (name+version)
if spec.Version == "" || len(spec.Dependencies) == 0 {
return "", nil, nil
}

// pubspec.yaml uses version ranges
// save only dependencies names
dependsOn := maps.Keys(spec.Dependencies)

return utils.PackageID(spec.Name, spec.Version), dependsOn, nil
}

func (a pubSpecLockAnalyzer) Required(filePath string, _ os.FileInfo) bool {
Expand Down
Loading