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
## [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)

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.
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
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_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
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`
- `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
[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
// see `package-lock_v3_with_workspace.json` to better understanding
func (p *Parser) resolveLinks(packages map[string]Package) {
links := lo.PickBy(packages, func(_ string, pkg Package) bool {
return pkg.Link
links := lo.PickBy(packages, func(pkgPath string, pkg Package) bool {
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
if len(links) == 0 {
@@ -208,7 +216,9 @@ func (p *Parser) resolveLinks(packages map[string]Package) {
}
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 {
if !strings.HasPrefix(pkgPath, link.Resolved) {
continue

View File

@@ -53,6 +53,12 @@ func TestParse(t *testing.T) {
want: npmV3WithoutRootDepsField,
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 {

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 {
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
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.
for _, pkg := range resolvedPkgs {
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.
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]
if !ok {
return
@@ -227,10 +231,11 @@ func (p *Parser) markRootPkgs(id string, pkgs map[string]ftypes.Package, deps ma
pkg.Dev = false
pkgs[id] = pkg
visited[id] = struct{}{}
// Update child deps
for _, depID := range deps[id].DependsOn {
p.markRootPkgs(depID, pkgs, deps)
p.markRootPkgs(depID, pkgs, deps, visited)
}
return
}

View File

@@ -59,6 +59,18 @@ func TestParse(t *testing.T) {
want: pnpmV9,
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 {

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) {
name = normalizePkgName(name)
name = NormalizePkgName(name)
vers, ok := pkgVersions[name]
if !ok {
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
}
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.
// 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.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

View File

@@ -8,6 +8,7 @@ import (
"os"
"path/filepath"
"github.com/samber/lo"
"golang.org/x/xerrors"
"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)
}
// Identify the direct/transitive dependencies
for i, pkg := range app.Packages {
// Identify the direct/transitive dependencies
if _, ok := p[pkg.Name]; ok {
app.Packages[i].Relationship = types.RelationshipDirect
} else {
@@ -127,5 +128,11 @@ func (a poetryAnalyzer) parsePyProject(fsys fs.FS, path string) (map[string]any,
if err != nil {
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
}

View File

@@ -6,7 +6,7 @@ authors = ["Trivy"]
[tool.poetry.dependencies]
python = "^3.9"
flask = "^1.0"
Flask = "^1.0"
requests = {version = "2.28.1", optional = true}
[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)
// Source info was added from component or properties
if pkg.SrcName != "" && pkg.SrcVersion != "" {
return
}
// Fill source package information for components in third-party SBOMs .
if pkg.SrcName == "" {
pkg.SrcName = pkg.Name