Skip to content

Commit

Permalink
feat(dart): add graph support (#5374)
Browse files Browse the repository at this point in the history
Signed-off-by: knqyf263 <knqyf263@gmail.com>
Co-authored-by: knqyf263 <knqyf263@gmail.com>
  • Loading branch information
DmitriyLewen and knqyf263 authored Oct 20, 2023
1 parent f2a12f5 commit 1a15a3a
Show file tree
Hide file tree
Showing 12 changed files with 428 additions and 97 deletions.
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

0 comments on commit 1a15a3a

Please sign in to comment.