Skip to content

Commit 1a15a3a

Browse files
feat(dart): add graph support (#5374)
Signed-off-by: knqyf263 <[email protected]> Co-authored-by: knqyf263 <[email protected]>
1 parent f2a12f5 commit 1a15a3a

File tree

12 files changed

+428
-97
lines changed

12 files changed

+428
-97
lines changed

docs/docs/configuration/reporting.md

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -41,32 +41,31 @@ In some cases, vulnerable dependencies are not linked directly, and it requires
4141
To make this task simpler Trivy can show a dependency origin tree with the `--dependency-tree` flag.
4242
This flag is only available with the `--format table` flag.
4343

44-
The following packages/languages are currently supported:
45-
46-
- OS packages
47-
- apk
48-
- dpkg
49-
- rpm
50-
- Node.js
51-
- npm: package-lock.json
52-
- pnpm: pnpm-lock.yaml
53-
- yarn: yarn.lock
54-
- .NET
55-
- NuGet: packages.lock.json
56-
- Python
57-
- Poetry: poetry.lock
58-
- Ruby
59-
- Bundler: Gemfile.lock
60-
- Rust
61-
- Binaries built with [cargo-auditable][cargo-auditable]
62-
- Go
63-
- Modules: go.mod
64-
- PHP
65-
- Composer
66-
- Java
67-
- Maven: pom.xml
68-
69-
This tree is the reverse of the npm list command.
44+
The following OS package managers are currently supported:
45+
46+
| OS Package Managers |
47+
|---------------------|
48+
| apk |
49+
| dpkg |
50+
| rpm |
51+
52+
The following languages are currently supported:
53+
54+
| Language | File |
55+
|----------|--------------------------------------------|
56+
| Node.js | [package-lock.json][nodejs-package-lock] |
57+
| | [pnpm-lock.yaml][pnpm-lock] |
58+
| | [yarn.lock][yarn-lock] |
59+
| .NET | [packages.lock.json][dotnet-packages-lock] |
60+
| Python | [poetry.lock][poetry-lock] |
61+
| Ruby | [Gemfile.lock][gemfile-lock] |
62+
| Rust | [cargo-auditable binaries][cargo-binaries] |
63+
| Go | [go.mod][go-mod] |
64+
| PHP | [composer.lock][composer-lock] |
65+
| Java | [pom.xml][pom-xml] |
66+
| Dart | [pubspec.lock][pubspec-lock] |
67+
68+
This tree is the reverse of the dependency graph.
7069
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.
7170

7271
In table output, it looks like:
@@ -408,4 +407,16 @@ $ trivy convert --format table --severity CRITICAL result.json
408407
[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
409408

410409
[os_packages]: ../scanner/vulnerability.md#os-packages
411-
[language_packages]: ../scanner/vulnerability.md#language-specific-packages
410+
[language_packages]: ../scanner/vulnerability.md#language-specific-packages
411+
412+
[nodejs-package-lock]: ../coverage/language/nodejs.md#npm
413+
[pnpm-lock]: ../coverage/language/nodejs.md#pnpm
414+
[yarn-lock]: ../coverage/language/nodejs.md#yarn
415+
[dotnet-packages-lock]: ../coverage/language/dotnet.md#packageslockjson
416+
[poetry-lock]: ../coverage/language/python.md#poetry
417+
[gemfile-lock]: ../coverage/language/ruby.md#bundler
418+
[go-mod]: ../coverage/language/golang.md#go-modules
419+
[composer-lock]: ../coverage/language/php.md#composer
420+
[pom-xml]: ../coverage/language/java.md#pomxml
421+
[pubspec-lock]: ../coverage/language/dart.md#dart
422+
[cargo-binaries]: ../coverage/language/rust.md#binaries

docs/docs/coverage/language/dart.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ The following table provides an outline of the features Trivy offers.
1313

1414
| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
1515
|-------------------------|--------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
16-
| [Dart][dart-repository] | pubspec.lock || Included | - | - |
16+
| [Dart][dart-repository] | pubspec.lock || Included | | - |
1717

1818
## Dart
1919
In order to detect dependencies, Trivy searches for `pubspec.lock`.
2020

2121
Trivy marks indirect dependencies, but `pubspec.lock` file doesn't have options to separate root and dev transitive dependencies.
2222
So Trivy includes all dependencies in report.
2323

24+
To build `dependency tree` Trivy parses [cache directory][cache-directory]. Currently supported default directories and `PUB_CACHE` environment (absolute path only).
25+
!!! note
26+
Make sure the cache directory contains all the dependencies installed in your application. To download missing dependencies, use `dart pub get` command.
27+
2428
[dart]: https://dart.dev/
2529
[dart-repository]: https://pub.dev/
2630
[dependency-graph]: ../../configuration/reporting.md#show-origins-of-vulnerable-dependencies
31+
[cache-directory]: https://dart.dev/tools/pub/glossary#system-cache

pkg/fanal/analyzer/language/dart/pub/pubspec.go

Lines changed: 145 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,171 @@ package pub
22

33
import (
44
"context"
5+
"io"
6+
"io/fs"
57
"os"
68
"path/filepath"
9+
"runtime"
10+
"sort"
711

12+
"github.com/samber/lo"
13+
"golang.org/x/exp/maps"
814
"golang.org/x/xerrors"
15+
"gopkg.in/yaml.v3"
916

1017
"github.com/aquasecurity/go-dep-parser/pkg/dart/pub"
18+
godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
19+
"github.com/aquasecurity/go-dep-parser/pkg/utils"
1120
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
1221
"github.com/aquasecurity/trivy/pkg/fanal/analyzer/language"
1322
"github.com/aquasecurity/trivy/pkg/fanal/types"
23+
"github.com/aquasecurity/trivy/pkg/log"
24+
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
1425
)
1526

1627
func init() {
17-
analyzer.RegisterAnalyzer(&pubSpecLockAnalyzer{})
28+
analyzer.RegisterPostAnalyzer(analyzer.TypePubSpecLock, newPubSpecLockAnalyzer)
1829
}
1930

2031
const (
21-
version = 1
32+
version = 2
33+
pubSpecYamlFileName = "pubspec.yaml"
2234
)
2335

24-
// pubSpecLockAnalyzer analyzes pubspec.lock
25-
type pubSpecLockAnalyzer struct{}
36+
// pubSpecLockAnalyzer analyzes `pubspec.lock`
37+
type pubSpecLockAnalyzer struct {
38+
parser godeptypes.Parser
39+
}
40+
41+
func newPubSpecLockAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
42+
return pubSpecLockAnalyzer{
43+
parser: pub.NewParser(),
44+
}, nil
45+
}
2646

27-
func (a pubSpecLockAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisInput) (*analyzer.AnalysisResult, error) {
28-
p := pub.NewParser()
29-
res, err := language.Analyze(types.Pub, input.FilePath, input.Content, p)
47+
func (a pubSpecLockAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
48+
var apps []types.Application
49+
50+
// get all DependsOn from cache dir
51+
// lib ID -> DependsOn names
52+
allDependsOn, err := findDependsOn()
3053
if err != nil {
31-
return nil, xerrors.Errorf("%s parse error: %w", input.FilePath, err)
54+
log.Logger.Warnf("Unable to parse cache dir: %s", err)
55+
}
56+
57+
required := func(path string, d fs.DirEntry) bool {
58+
return filepath.Base(path) == types.PubSpecLock
59+
}
60+
61+
err = fsutils.WalkDir(input.FS, ".", required, func(path string, _ fs.DirEntry, r io.Reader) error {
62+
app, err := language.Parse(types.Pub, path, r, a.parser)
63+
if err != nil {
64+
return xerrors.Errorf("unable to parse %q: %w", path, err)
65+
}
66+
67+
if app == nil {
68+
return nil
69+
}
70+
71+
if allDependsOn != nil {
72+
// Required to search for library versions for DependsOn.
73+
libs := lo.SliceToMap(app.Libraries, func(lib types.Package) (string, string) {
74+
return lib.Name, lib.ID
75+
})
76+
77+
for i, lib := range app.Libraries {
78+
var dependsOn []string
79+
for _, depName := range allDependsOn[lib.ID] {
80+
if depID, ok := libs[depName]; ok {
81+
dependsOn = append(dependsOn, depID)
82+
}
83+
}
84+
app.Libraries[i].DependsOn = dependsOn
85+
}
86+
}
87+
88+
sort.Sort(app.Libraries)
89+
apps = append(apps, *app)
90+
return nil
91+
})
92+
if err != nil {
93+
return nil, xerrors.Errorf("walk error: %w", err)
94+
}
95+
96+
return &analyzer.AnalysisResult{
97+
Applications: apps,
98+
}, nil
99+
}
100+
101+
func findDependsOn() (map[string][]string, error) {
102+
dir := cacheDir()
103+
if !fsutils.DirExists(dir) {
104+
log.Logger.Debugf("Cache dir (%s) not found. Need 'dart pub get' to fill dependency relationships", dir)
105+
return nil, nil
106+
}
107+
108+
required := func(path string, d fs.DirEntry) bool {
109+
return filepath.Base(path) == pubSpecYamlFileName
32110
}
33-
return res, nil
111+
112+
deps := make(map[string][]string)
113+
if err := fsutils.WalkDir(os.DirFS(dir), ".", required, func(path string, d fs.DirEntry, r io.Reader) error {
114+
id, dependsOn, err := parsePubSpecYaml(r)
115+
if err != nil {
116+
log.Logger.Debugf("Unable to parse %q: %s", path, err)
117+
return nil
118+
}
119+
if id != "" {
120+
deps[id] = dependsOn
121+
}
122+
return nil
123+
124+
}); err != nil {
125+
return nil, xerrors.Errorf("walk error: %w", err)
126+
}
127+
return deps, nil
128+
}
129+
130+
// https://dart.dev/tools/pub/glossary#system-cache
131+
func cacheDir() string {
132+
if dir := os.Getenv("PUB_CACHE"); dir != "" {
133+
return dir
134+
}
135+
136+
// `%LOCALAPPDATA%\Pub\Cache` for Windows
137+
if runtime.GOOS == "windows" {
138+
return filepath.Join(os.Getenv("LOCALAPPDATA"), "Pub", "Cache")
139+
}
140+
141+
// `~/.pub-cache` for Linux or Mac
142+
return filepath.Join(os.Getenv("HOME"), ".pub_cache")
143+
}
144+
145+
type pubSpecYaml struct {
146+
Name string `yaml:"name"`
147+
Version string `yaml:"version,omitempty"`
148+
Dependencies map[string]interface{} `yaml:"dependencies,omitempty"`
149+
}
150+
151+
func parsePubSpecYaml(r io.Reader) (string, []string, error) {
152+
var spec pubSpecYaml
153+
if err := yaml.NewDecoder(r).Decode(&spec); err != nil {
154+
return "", nil, xerrors.Errorf("unable to decode: %w", err)
155+
}
156+
157+
// Version is a required field only for packages from pub.dev:
158+
// https://dart.dev/tools/pub/pubspec#version
159+
// We can skip packages without version,
160+
// because we compare packages by ID (name+version)
161+
if spec.Version == "" || len(spec.Dependencies) == 0 {
162+
return "", nil, nil
163+
}
164+
165+
// pubspec.yaml uses version ranges
166+
// save only dependencies names
167+
dependsOn := maps.Keys(spec.Dependencies)
168+
169+
return utils.PackageID(spec.Name, spec.Version), dependsOn, nil
34170
}
35171

36172
func (a pubSpecLockAnalyzer) Required(filePath string, _ os.FileInfo) bool {

0 commit comments

Comments
 (0)