Compare commits

...

8 Commits

Author SHA1 Message Date
Aqua Security automated builds
a3caf0658c release: v0.52.1 [release/v0.52] (#6877) 2024-06-10 08:34:00 +00:00
Aqua Security automated builds
01dbb42ae9 fix(nodejs): fix infinite loop when package link from package-lock.json file is broken [backport: release/v0.52] (#6888)
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
2024-06-10 07:47:58 +00:00
Aqua Security automated builds
f186d22bf2 fix(sbom): don't overwrite srcEpoch when decoding SBOM files [backport: release/v0.52] (#6881)
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
2024-06-07 11:32:05 +00:00
Aqua Security automated builds
093c0ae020 fix(python): compare pkg names from poetry.lock and pyproject.toml in lowercase [backport: release/v0.52] (#6878)
Co-authored-by: DmitriyLewen <91113035+DmitriyLewen@users.noreply.github.com>
2024-06-07 11:30:29 +00:00
Teppei Fukuda
6bfda76022 Merge pull request #6879 from aquasecurity/backport-pr-6864-to-release/v0.52
docs: explain how VEX is applied [backport: release/v0.52]
2024-06-07 10:12:55 +00:00
Teppei Fukuda
53850c8b20 docs: explain how VEX is applied (#6864)
Signed-off-by: knqyf263 <knqyf263@gmail.com>
2024-06-07 10:10:19 +00:00
Teppei Fukuda
221196202c Merge pull request #6875 from aquasecurity/backport-pr-6857-to-release/v0.52
fix(nodejs): fix infinity loops for `pnpm` with cyclic imports [backport: release/v0.52]
2024-06-07 08:56:42 +00:00
DmitriyLewen
a614b693d7 fix(nodejs): fix infinity loops for pnpm with cyclic imports (#6857) 2024-06-07 06:22:26 +00:00
14 changed files with 325 additions and 12 deletions

View File

@@ -1 +1 @@
{".":"0.52.0"} {".":"0.52.1"}

View File

@@ -1,5 +1,15 @@
# Changelog # Changelog
## [0.52.1](https://github.com/aquasecurity/trivy/compare/v0.52.0...v0.52.1) (2024-06-10)
### Bug Fixes
* **nodejs:** fix infinite loop when package link from `package-lock.json` file is broken [backport: release/v0.52] ([#6888](https://github.com/aquasecurity/trivy/issues/6888)) ([01dbb42](https://github.com/aquasecurity/trivy/commit/01dbb42ae9ecff21d1c71f095a27f47a6ac9adaa))
* **nodejs:** fix infinity loops for `pnpm` with cyclic imports ([#6857](https://github.com/aquasecurity/trivy/issues/6857)) ([a614b69](https://github.com/aquasecurity/trivy/commit/a614b693d7b948df7d4ed3516e79573cb8424406))
* **python:** compare pkg names from `poetry.lock` and `pyproject.toml` in lowercase [backport: release/v0.52] ([#6878](https://github.com/aquasecurity/trivy/issues/6878)) ([093c0ae](https://github.com/aquasecurity/trivy/commit/093c0ae020548bf6f3d1896d4d55210eb42c7b0e))
* **sbom:** don't overwrite `srcEpoch` when decoding SBOM files [backport: release/v0.52] ([#6881](https://github.com/aquasecurity/trivy/issues/6881)) ([f186d22](https://github.com/aquasecurity/trivy/commit/f186d22bf275e872bd664f07131604f6a0216f20))
## [0.52.0](https://github.com/aquasecurity/trivy/compare/v0.51.1...v0.52.0) (2024-06-03) ## [0.52.0](https://github.com/aquasecurity/trivy/compare/v0.51.1...v0.52.0) (2024-06-03)

View File

@@ -263,6 +263,8 @@ $ trivy image ghcr.io/aquasecurity/trivy:0.50.0 --vex trivy.openvex.json
VEX documents can indeed be reused across different container images, eliminating the need to issue separate VEX documents for each image. VEX documents can indeed be reused across different container images, eliminating the need to issue separate VEX documents for each image.
This is particularly useful when there is a common component or library that is used across multiple projects or container images. This is particularly useful when there is a common component or library that is used across multiple projects or container images.
You can see [the appendix](#applying-vex-to-dependency-trees) for more details on how VEX is applied in Trivy.
### Scan with VEX ### Scan with VEX
Provide the VEX when scanning your target. Provide the VEX when scanning your target.
@@ -412,6 +414,8 @@ At present, the specified relationship category is not taken into account and al
- installed_on - installed_on
- installed_with - installed_with
You can see [the appendix](#applying-vex-to-dependency-trees) for more details on how VEX is applied in Trivy.
### Scan with CSAF VEX ### Scan with CSAF VEX
Provide the CSAF document when scanning your target. Provide the CSAF document when scanning your target.
@@ -470,6 +474,103 @@ does not match:
- `pkg:maven/com.google.guava/guava@24.1.1?classifier=sources` - `pkg:maven/com.google.guava/guava@24.1.1?classifier=sources`
- `classifier` must have the same value. - `classifier` must have the same value.
### Applying VEX to Dependency Trees
Trivy internally generates a dependency tree and applies VEX statements to this graph.
Let's consider a project with the following dependency tree, where `Module C v2.0.0` is assumed to have a vulnerability CVE-XXXX-YYYY:
```mermaid
graph TD;
modRootA(Module Root A v1.0.0)
modB(Module B v1.0.0)
modC(Module C v2.0.0)
modRootA-->modB
modB-->modC
```
Now, suppose a VEX statement is issued for `Module B` as follows:
```json
"statements": [
{
"vulnerability": {"name": "CVE-XXXX-YYYY"},
"products": [
{
"@id": "pkg:golang/module-b@1.0.0",
"subcomponents": [
{ "@id": "pkg:golang/module-c@2.0.0" }
]
}
],
"status": "not_affected",
"justification": "vulnerable_code_not_in_execute_path"
}
]
```
It declares that `Module B` is not affected by CVE-XXXX-YYYY on `Module C`.
!!! note
The VEX in this example defines the relationship between `Module B` and `Module C`.
However, as Trivy traverses all parents from vulnerable packages, it is also possible to define a VEX for the relationship between a vulnerable package and any parent, such as `Module A` and `Module C`, etc.
Mapping this VEX onto the dependency tree would look like this:
```mermaid
graph TD;
modRootA(Module Root A v1.0.0)
subgraph "VEX (Not Affected)"
modB(Module B v1.0.0)
modC(Module C v2.0.0)
end
modRootA-->modB
modB-->modC
```
In this case, it's clear that `Module Root A` is also not affected by CVE-XXXX-YYYY, so this vulnerability is suppressed.
Now, let's consider another project:
```mermaid
graph TD;
modRootZ(Module Root Z v1.0.0)
modB'(Module B v1.0.0)
modC'(Module C v2.0.0)
modD'(Module D v3.0.0)
modRootZ-->modB'
modRootZ-->modD'
modB'-->modC'
modD'-->modC'
```
Assuming the same VEX as before, applying it to this dependency tree would look like:
```mermaid
graph TD;
modRootZ(Module Root Z v1.0.0)
subgraph "VEX (Not Affected)"
modB'(Module B v1.0.0)
modC'(Module C v2.0.0)
end
modD'(Module D v3.0.0)
modRootZ-->modB'
modRootZ-->modD'
modB'-->modC'
modD'-->modC'
```
`Module Root Z` depends on `Module C` via multiple paths.
While the VEX tells us that `Module B` is not affected by the vulnerability, `Module D` might be.
In the absence of a VEX, the default assumption is that it is affected.
Taking all of this into account, Trivy determines that `Module Root Z` is affected by this vulnerability.
[csaf]: https://oasis-open.github.io/csaf-documentation/specification.html [csaf]: https://oasis-open.github.io/csaf-documentation/specification.html
[openvex]: https://github.com/openvex/spec [openvex]: https://github.com/openvex/spec

View File

@@ -194,8 +194,16 @@ func (p *Parser) parseV2(packages map[string]Package) ([]ftypes.Package, []ftype
// node_modules/func1 -> link to target // node_modules/func1 -> link to target
// see `package-lock_v3_with_workspace.json` to better understanding // see `package-lock_v3_with_workspace.json` to better understanding
func (p *Parser) resolveLinks(packages map[string]Package) { func (p *Parser) resolveLinks(packages map[string]Package) {
links := lo.PickBy(packages, func(_ string, pkg Package) bool { links := lo.PickBy(packages, func(pkgPath string, pkg Package) bool {
return pkg.Link if !pkg.Link {
return false
}
if pkg.Resolved == "" {
p.logger.Warn("`package-lock.json` contains broken link with empty `resolved` field. This package will be skipped to avoid receiving an empty package", log.String("pkg", pkgPath))
delete(packages, pkgPath)
return false
}
return true
}) })
// Early return // Early return
if len(links) == 0 { if len(links) == 0 {
@@ -208,7 +216,9 @@ func (p *Parser) resolveLinks(packages map[string]Package) {
} }
workspaces := rootPkg.Workspaces workspaces := rootPkg.Workspaces
for pkgPath, pkg := range packages { // Changing the map during the map iteration causes unexpected behavior,
// so we need to iterate over the cloned `packages` map, but change the original `packages` map.
for pkgPath, pkg := range maps.Clone(packages) {
for linkPath, link := range links { for linkPath, link := range links {
if !strings.HasPrefix(pkgPath, link.Resolved) { if !strings.HasPrefix(pkgPath, link.Resolved) {
continue continue

View File

@@ -53,6 +53,12 @@ func TestParse(t *testing.T) {
want: npmV3WithoutRootDepsField, want: npmV3WithoutRootDepsField,
wantDeps: npmV3WithoutRootDepsFieldDeps, wantDeps: npmV3WithoutRootDepsFieldDeps,
}, },
{
name: "lock version v3 with broken link",
file: "testdata/package-lock_v3_broken_link.json",
want: nil,
wantDeps: nil,
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -0,0 +1,24 @@
{
"name": "node_v3_without_direct_deps",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "node_v3_without_direct_deps",
"version": "1.0.0",
"license": "ISC"
},
"functions/func1": {
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"debug": "^2.6.9"
}
},
"node_modules/func1": {
"resolved": "",
"link": true
}
}
}

View File

@@ -183,7 +183,7 @@ func (p *Parser) parseV9(lockFile LockFile) ([]ftypes.Package, []ftypes.Dependen
if dep, ok := lockFile.Importers.Root.DevDependencies[name]; ok && dep.Version == ver { if dep, ok := lockFile.Importers.Root.DevDependencies[name]; ok && dep.Version == ver {
relationship = ftypes.RelationshipDirect relationship = ftypes.RelationshipDirect
} }
if dep, ok := lockFile.Importers.Root.Dependencies[name]; ok && dep.Version == ver { if dep, ok := lockFile.Importers.Root.Dependencies[name]; ok && p.trimPeerDeps(dep.Version, lockVer) == ver {
relationship = ftypes.RelationshipDirect relationship = ftypes.RelationshipDirect
dev = false // mark root direct deps to update `dev` field of their child deps. dev = false // mark root direct deps to update `dev` field of their child deps.
} }
@@ -208,10 +208,11 @@ func (p *Parser) parseV9(lockFile LockFile) ([]ftypes.Package, []ftypes.Dependen
} }
} }
visited := make(map[string]struct{})
// Overwrite the `Dev` field for dev deps and their child dependencies. // Overwrite the `Dev` field for dev deps and their child dependencies.
for _, pkg := range resolvedPkgs { for _, pkg := range resolvedPkgs {
if !pkg.Dev { if !pkg.Dev {
p.markRootPkgs(pkg.ID, resolvedPkgs, resolvedDeps) p.markRootPkgs(pkg.ID, resolvedPkgs, resolvedDeps, visited)
} }
} }
@@ -219,7 +220,10 @@ func (p *Parser) parseV9(lockFile LockFile) ([]ftypes.Package, []ftypes.Dependen
} }
// markRootPkgs sets `Dev` to false for non dev dependency. // markRootPkgs sets `Dev` to false for non dev dependency.
func (p *Parser) markRootPkgs(id string, pkgs map[string]ftypes.Package, deps map[string]ftypes.Dependency) { func (p *Parser) markRootPkgs(id string, pkgs map[string]ftypes.Package, deps map[string]ftypes.Dependency, visited map[string]struct{}) {
if _, ok := visited[id]; ok {
return
}
pkg, ok := pkgs[id] pkg, ok := pkgs[id]
if !ok { if !ok {
return return
@@ -227,10 +231,11 @@ func (p *Parser) markRootPkgs(id string, pkgs map[string]ftypes.Package, deps ma
pkg.Dev = false pkg.Dev = false
pkgs[id] = pkg pkgs[id] = pkg
visited[id] = struct{}{}
// Update child deps // Update child deps
for _, depID := range deps[id].DependsOn { for _, depID := range deps[id].DependsOn {
p.markRootPkgs(depID, pkgs, deps) p.markRootPkgs(depID, pkgs, deps, visited)
} }
return return
} }

View File

@@ -59,6 +59,18 @@ func TestParse(t *testing.T) {
want: pnpmV9, want: pnpmV9,
wantDeps: pnpmV9Deps, wantDeps: pnpmV9Deps,
}, },
{
name: "v9",
file: "testdata/pnpm-lock_v9.yaml",
want: pnpmV9,
wantDeps: pnpmV9Deps,
},
{
name: "v9 with cyclic dependencies import",
file: "testdata/pnpm-lock_v9_cyclic_import.yaml",
want: pnpmV9CyclicImport,
wantDeps: pnpmV9CyclicImportDeps,
},
} }
for _, tt := range tests { for _, tt := range tests {

View File

@@ -900,4 +900,68 @@ var (
}, },
}, },
} }
pnpmV9CyclicImport = []ftypes.Package{
{
ID: "update-browserslist-db@1.0.16",
Name: "update-browserslist-db",
Version: "1.0.16",
Relationship: ftypes.RelationshipDirect,
},
{
ID: "browserslist@4.23.0",
Name: "browserslist",
Version: "4.23.0",
Relationship: ftypes.RelationshipIndirect,
},
{
ID: "caniuse-lite@1.0.30001627",
Name: "caniuse-lite",
Version: "1.0.30001627",
Relationship: ftypes.RelationshipIndirect,
},
{
ID: "electron-to-chromium@1.4.789",
Name: "electron-to-chromium",
Version: "1.4.789",
Relationship: ftypes.RelationshipIndirect,
},
{
ID: "escalade@3.1.2",
Name: "escalade",
Version: "3.1.2",
Relationship: ftypes.RelationshipIndirect,
},
{
ID: "node-releases@2.0.14",
Name: "node-releases",
Version: "2.0.14",
Relationship: ftypes.RelationshipIndirect,
},
{
ID: "picocolors@1.0.1",
Name: "picocolors",
Version: "1.0.1",
Relationship: ftypes.RelationshipIndirect,
},
}
pnpmV9CyclicImportDeps = []ftypes.Dependency{
{
ID: "browserslist@4.23.0",
DependsOn: []string{
"caniuse-lite@1.0.30001627",
"electron-to-chromium@1.4.789",
"node-releases@2.0.14",
"update-browserslist-db@1.0.16",
},
},
{
ID: "update-browserslist-db@1.0.16",
DependsOn: []string{
"browserslist@4.23.0",
"escalade@3.1.2",
"picocolors@1.0.1",
},
},
}
) )

View File

@@ -0,0 +1,67 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
dependencies:
update-browserslist-db:
specifier: 1.0.16
version: 1.0.16(browserslist@4.23.0)
packages:
browserslist@4.23.0:
resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==}
engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7}
hasBin: true
caniuse-lite@1.0.30001627:
resolution: {integrity: sha512-4zgNiB8nTyV/tHhwZrFs88ryjls/lHiqFhrxCW4qSTeuRByBVnPYpDInchOIySWknznucaf31Z4KYqjfbrecVw==}
electron-to-chromium@1.4.789:
resolution: {integrity: sha512-0VbyiaXoT++Fi2vHGo2ThOeS6X3vgRCWrjPeO2FeIAWL6ItiSJ9BqlH8LfCXe3X1IdcG+S0iLoNaxQWhfZoGzQ==}
escalade@3.1.2:
resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==}
engines: {node: '>=6'}
node-releases@2.0.14:
resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==}
picocolors@1.0.1:
resolution: {integrity: sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==}
update-browserslist-db@1.0.16:
resolution: {integrity: sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
snapshots:
browserslist@4.23.0:
dependencies:
caniuse-lite: 1.0.30001627
electron-to-chromium: 1.4.789
node-releases: 2.0.14
update-browserslist-db: 1.0.16(browserslist@4.23.0)
caniuse-lite@1.0.30001627: {}
electron-to-chromium@1.4.789: {}
escalade@3.1.2: {}
node-releases@2.0.14: {}
picocolors@1.0.1: {}
update-browserslist-db@1.0.16(browserslist@4.23.0):
dependencies:
browserslist: 4.23.0
escalade: 3.1.2
picocolors: 1.0.1

View File

@@ -105,7 +105,7 @@ func (p *Parser) parseDependencies(deps map[string]any, pkgVersions map[string][
} }
func (p *Parser) parseDependency(name string, versRange any, pkgVersions map[string][]string) (string, error) { func (p *Parser) parseDependency(name string, versRange any, pkgVersions map[string][]string) (string, error) {
name = normalizePkgName(name) name = NormalizePkgName(name)
vers, ok := pkgVersions[name] vers, ok := pkgVersions[name]
if !ok { if !ok {
return "", xerrors.Errorf("no version found for %q", name) return "", xerrors.Errorf("no version found for %q", name)
@@ -149,9 +149,11 @@ func matchVersion(currentVersion, constraint string) (bool, error) {
return c.Check(v), nil return c.Check(v), nil
} }
func normalizePkgName(name string) string { // NormalizePkgName normalizes the package name based on pep-0426
func NormalizePkgName(name string) string {
// The package names don't use `_`, `.` or upper case, but dependency names can contain them. // The package names don't use `_`, `.` or upper case, but dependency names can contain them.
// We need to normalize those names. // We need to normalize those names.
// cf. https://peps.python.org/pep-0426/#name
name = strings.ToLower(name) // e.g. https://github.com/python-poetry/poetry/blob/c8945eb110aeda611cc6721565d7ad0c657d453a/poetry.lock#L819 name = strings.ToLower(name) // e.g. https://github.com/python-poetry/poetry/blob/c8945eb110aeda611cc6721565d7ad0c657d453a/poetry.lock#L819
name = strings.ReplaceAll(name, "_", "-") // e.g. https://github.com/python-poetry/poetry/blob/c8945eb110aeda611cc6721565d7ad0c657d453a/poetry.lock#L50 name = strings.ReplaceAll(name, "_", "-") // e.g. https://github.com/python-poetry/poetry/blob/c8945eb110aeda611cc6721565d7ad0c657d453a/poetry.lock#L50
name = strings.ReplaceAll(name, ".", "-") // e.g. https://github.com/python-poetry/poetry/blob/c8945eb110aeda611cc6721565d7ad0c657d453a/poetry.lock#L816 name = strings.ReplaceAll(name, ".", "-") // e.g. https://github.com/python-poetry/poetry/blob/c8945eb110aeda611cc6721565d7ad0c657d453a/poetry.lock#L816

View File

@@ -8,6 +8,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"github.com/samber/lo"
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/aquasecurity/trivy/pkg/dependency/parser/python/poetry" "github.com/aquasecurity/trivy/pkg/dependency/parser/python/poetry"
@@ -102,8 +103,8 @@ func (a poetryAnalyzer) mergePyProject(fsys fs.FS, dir string, app *types.Applic
return xerrors.Errorf("unable to parse %s: %w", path, err) return xerrors.Errorf("unable to parse %s: %w", path, err)
} }
// Identify the direct/transitive dependencies
for i, pkg := range app.Packages { for i, pkg := range app.Packages {
// Identify the direct/transitive dependencies
if _, ok := p[pkg.Name]; ok { if _, ok := p[pkg.Name]; ok {
app.Packages[i].Relationship = types.RelationshipDirect app.Packages[i].Relationship = types.RelationshipDirect
} else { } else {
@@ -127,5 +128,11 @@ func (a poetryAnalyzer) parsePyProject(fsys fs.FS, path string) (map[string]any,
if err != nil { if err != nil {
return nil, err return nil, err
} }
// Packages from `pyproject.toml` can use uppercase characters, `.` and `_`.
parsed = lo.MapKeys(parsed, func(_ any, pkgName string) string {
return poetry.NormalizePkgName(pkgName)
})
return parsed, nil return parsed, nil
} }

View File

@@ -6,7 +6,7 @@ authors = ["Trivy"]
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.9" python = "^3.9"
flask = "^1.0" Flask = "^1.0"
requests = {version = "2.28.1", optional = true} requests = {version = "2.28.1", optional = true}
[tool.poetry.dev-dependencies] [tool.poetry.dev-dependencies]

View File

@@ -271,6 +271,11 @@ func (m *Decoder) fillSrcPkg(c *core.Component, pkg *ftypes.Package) {
} }
m.parseSrcVersion(pkg, c.SrcVersion) m.parseSrcVersion(pkg, c.SrcVersion)
// Source info was added from component or properties
if pkg.SrcName != "" && pkg.SrcVersion != "" {
return
}
// Fill source package information for components in third-party SBOMs . // Fill source package information for components in third-party SBOMs .
if pkg.SrcName == "" { if pkg.SrcName == "" {
pkg.SrcName = pkg.Name pkg.SrcName = pkg.Name