mirror of
https://github.com/aquasecurity/trivy.git
synced 2025-12-12 07:40:48 -08:00
feat(php): add support for dev dependencies in Composer (#9910)
This commit is contained in:
@@ -11,10 +11,10 @@ The following scanners are supported.
|
||||
The following table provides an outline of the features Trivy offers.
|
||||
|
||||
|
||||
| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
|
||||
|-----------------|----------------|:-----------------------:|:----------------:|:------------------------------------:|:--------:|
|
||||
| Composer | composer.lock | ✓ | Excluded | ✓ | ✓ |
|
||||
| Composer | installed.json | ✓ | Excluded | - | ✓ |
|
||||
| Package manager | File | Transitive dependencies | Dev dependencies | [Dependency graph][dependency-graph] | Position |
|
||||
|-----------------|----------------|:-----------------------:|:----------------------------------:|:------------------------------------:|:--------:|
|
||||
| Composer | composer.lock | ✓ | [Excluded](#development-dependencies) | ✓ | ✓ |
|
||||
| Composer | installed.json | ✓ | Excluded | - | ✓ |
|
||||
|
||||
## composer.lock
|
||||
In order to detect dependencies, Trivy searches for `composer.lock`.
|
||||
@@ -23,6 +23,12 @@ Trivy also supports dependency trees; however, to display an accurate tree, it n
|
||||
Since this information is not included in `composer.lock`, Trivy parses `composer.json`, which should be located next to `composer.lock`.
|
||||
If you want to see the dependency tree, please ensure that `composer.json` is present.
|
||||
|
||||
### Development dependencies
|
||||
By default, Trivy doesn't report development dependencies (`packages-dev` in `composer.lock`).
|
||||
Use the `--include-dev-deps` flag to include them.
|
||||
|
||||
To correctly identify direct development dependencies, Trivy parses `require-dev` from `composer.json`, which should be located next to `composer.lock`.
|
||||
|
||||
## installed.json
|
||||
Trivy also supports dependency detection for `installed.json` files. By default, you can find this file at `path_to_app/vendor/composer/installed.json`.
|
||||
|
||||
|
||||
@@ -17,7 +17,8 @@ import (
|
||||
)
|
||||
|
||||
type LockFile struct {
|
||||
Packages []packageInfo `json:"packages"`
|
||||
Packages []packageInfo `json:"packages"`
|
||||
PackagesDev []packageInfo `json:"packages-dev"`
|
||||
}
|
||||
type packageInfo struct {
|
||||
Name string `json:"name"`
|
||||
@@ -45,30 +46,11 @@ func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package,
|
||||
|
||||
pkgs := make(map[string]ftypes.Package)
|
||||
foundDeps := make(map[string][]string)
|
||||
for _, lpkg := range lockFile.Packages {
|
||||
pkg := ftypes.Package{
|
||||
ID: dependency.ID(ftypes.Composer, lpkg.Name, lpkg.Version),
|
||||
Name: lpkg.Name,
|
||||
Version: lpkg.Version,
|
||||
Relationship: ftypes.RelationshipUnknown, // composer.lock file doesn't have info about direct/indirect dependencies
|
||||
Licenses: licenses(lpkg.License),
|
||||
Locations: []ftypes.Location{ftypes.Location(lpkg.Location)},
|
||||
}
|
||||
pkgs[pkg.Name] = pkg
|
||||
|
||||
var dependsOn []string
|
||||
for depName := range lpkg.Require {
|
||||
// Require field includes required php version, skip this
|
||||
// Also skip PHP extensions
|
||||
if depName == "php" || strings.HasPrefix(depName, "ext") {
|
||||
continue
|
||||
}
|
||||
dependsOn = append(dependsOn, depName) // field uses range of versions, so later we will fill in the versions from the packages
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
foundDeps[pkg.ID] = dependsOn
|
||||
}
|
||||
}
|
||||
// Production packages are parsed first to ensure they take precedence
|
||||
// when the same package exists in both "packages" and "packages-dev".
|
||||
p.parseProdPackages(lockFile, pkgs, foundDeps)
|
||||
p.parseDevPackages(lockFile, pkgs, foundDeps)
|
||||
|
||||
// fill deps versions
|
||||
var deps ftypes.Dependencies
|
||||
@@ -95,6 +77,50 @@ func (p *Parser) Parse(_ context.Context, r xio.ReadSeekerAt) ([]ftypes.Package,
|
||||
return pkgSlice, deps, nil
|
||||
}
|
||||
|
||||
// parseProdPackages parses packages from the "packages" field in composer.lock.
|
||||
func (p *Parser) parseProdPackages(lockFile LockFile, pkgs map[string]ftypes.Package, foundDeps map[string][]string) {
|
||||
p.parsePackages(lockFile.Packages, false, pkgs, foundDeps)
|
||||
}
|
||||
|
||||
// parseDevPackages parses packages from the "packages-dev" field in composer.lock.
|
||||
// Packages already present in pkgs (i.e., production packages) are skipped.
|
||||
func (p *Parser) parseDevPackages(lockFile LockFile, pkgs map[string]ftypes.Package, foundDeps map[string][]string) {
|
||||
p.parsePackages(lockFile.PackagesDev, true, pkgs, foundDeps)
|
||||
}
|
||||
|
||||
func (p *Parser) parsePackages(lockPkgs []packageInfo, isDev bool, pkgs map[string]ftypes.Package, foundDeps map[string][]string) {
|
||||
for _, lpkg := range lockPkgs {
|
||||
// Skip if the package already exists (production packages take precedence over dev packages)
|
||||
if _, ok := pkgs[lpkg.Name]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
pkg := ftypes.Package{
|
||||
ID: dependency.ID(ftypes.Composer, lpkg.Name, lpkg.Version),
|
||||
Name: lpkg.Name,
|
||||
Version: lpkg.Version,
|
||||
Relationship: ftypes.RelationshipUnknown, // composer.lock file doesn't have info about direct/indirect dependencies
|
||||
Licenses: licenses(lpkg.License),
|
||||
Locations: []ftypes.Location{ftypes.Location(lpkg.Location)},
|
||||
Dev: isDev,
|
||||
}
|
||||
pkgs[pkg.Name] = pkg
|
||||
|
||||
var dependsOn []string
|
||||
for depName := range lpkg.Require {
|
||||
// Require field includes required php version, skip this
|
||||
// Also skip PHP extensions
|
||||
if depName == "php" || strings.HasPrefix(depName, "ext") {
|
||||
continue
|
||||
}
|
||||
dependsOn = append(dependsOn, depName) // field uses range of versions, so later we will fill in the versions from the packages
|
||||
}
|
||||
if len(dependsOn) > 0 {
|
||||
foundDeps[pkg.ID] = dependsOn
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// licenses returns slice of licenses from string, string with separators (`or`, `and`, etc.) or string array
|
||||
// cf. https://getcomposer.org/doc/04-schema.md#license
|
||||
func licenses(val any) []string {
|
||||
|
||||
@@ -54,6 +54,32 @@ var (
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "pear/log@1.13.3",
|
||||
Name: "pear/log",
|
||||
Version: "1.13.3",
|
||||
Dev: true,
|
||||
Licenses: []string{"MIT"},
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 660,
|
||||
EndLine: 719,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "pear/pear_exception@v1.0.2",
|
||||
Name: "pear/pear_exception",
|
||||
Version: "v1.0.2",
|
||||
Dev: true,
|
||||
Licenses: []string{"BSD-2-Clause"},
|
||||
Locations: []ftypes.Location{
|
||||
{
|
||||
StartLine: 720,
|
||||
EndLine: 778,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "psr/http-message@1.0.1",
|
||||
Name: "psr/http-message",
|
||||
@@ -132,6 +158,12 @@ var (
|
||||
"ralouphie/getallheaders@3.0.3",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "pear/log@1.13.3",
|
||||
DependsOn: []string{
|
||||
"pear/pear_exception@v1.0.2",
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "symfony/polyfill-intl-idn@v1.27.0",
|
||||
DependsOn: []string{
|
||||
|
||||
@@ -106,7 +106,7 @@ func (a composerAnalyzer) parseComposerLock(ctx context.Context, path string, r
|
||||
func (a composerAnalyzer) mergeComposerJson(fsys fs.FS, dir string, app *types.Application) error {
|
||||
// Parse composer.json to identify the direct dependencies
|
||||
path := filepath.Join(dir, types.ComposerJson)
|
||||
p, err := a.parseComposerJson(fsys, path)
|
||||
cj, err := a.parseComposerJson(fsys, path)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
// Assume all the packages are direct dependencies as it cannot identify them from composer.lock
|
||||
log.Debug("Unable to determine the direct dependencies, composer.json not found", log.FilePath(path))
|
||||
@@ -117,7 +117,9 @@ func (a composerAnalyzer) mergeComposerJson(fsys fs.FS, dir string, app *types.A
|
||||
|
||||
for i, pkg := range app.Packages {
|
||||
// Identify the direct/transitive dependencies
|
||||
if _, ok := p[pkg.Name]; ok {
|
||||
if _, ok := cj.Require[pkg.Name]; ok {
|
||||
app.Packages[i].Relationship = types.RelationshipDirect
|
||||
} else if _, ok := cj.RequireDev[pkg.Name]; ok {
|
||||
app.Packages[i].Relationship = types.RelationshipDirect
|
||||
} else {
|
||||
app.Packages[i].Indirect = true
|
||||
@@ -129,21 +131,22 @@ func (a composerAnalyzer) mergeComposerJson(fsys fs.FS, dir string, app *types.A
|
||||
}
|
||||
|
||||
type composerJson struct {
|
||||
Require map[string]string `json:"require"`
|
||||
Require map[string]string `json:"require"`
|
||||
RequireDev map[string]string `json:"require-dev"`
|
||||
}
|
||||
|
||||
func (a composerAnalyzer) parseComposerJson(fsys fs.FS, path string) (map[string]string, error) {
|
||||
func (a composerAnalyzer) parseComposerJson(fsys fs.FS, path string) (composerJson, error) {
|
||||
// Parse composer.json
|
||||
f, err := fsys.Open(path)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("file open error: %w", err)
|
||||
return composerJson{}, xerrors.Errorf("file open error: %w", err)
|
||||
}
|
||||
defer func() { _ = f.Close() }()
|
||||
|
||||
jsonFile := composerJson{}
|
||||
var jsonFile composerJson
|
||||
err = json.NewDecoder(f).Decode(&jsonFile)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("json decode error: %w", err)
|
||||
return composerJson{}, xerrors.Errorf("json decode error: %w", err)
|
||||
}
|
||||
return jsonFile.Require, nil
|
||||
return jsonFile, nil
|
||||
}
|
||||
|
||||
@@ -151,6 +151,65 @@ func Test_composerAnalyzer_PostAnalyze(t *testing.T) {
|
||||
dir: "testdata/composer/sad",
|
||||
want: &analyzer.AnalysisResult{},
|
||||
},
|
||||
{
|
||||
name: "with dev dependencies",
|
||||
dir: "testdata/composer/with-dev",
|
||||
want: &analyzer.AnalysisResult{
|
||||
Applications: []types.Application{
|
||||
{
|
||||
Type: types.Composer,
|
||||
FilePath: "composer.lock",
|
||||
Packages: types.Packages{
|
||||
{
|
||||
ID: "pear/log@1.14.6",
|
||||
Name: "pear/log",
|
||||
Version: "1.14.6",
|
||||
Dev: true,
|
||||
Indirect: false,
|
||||
Relationship: types.RelationshipDirect,
|
||||
Licenses: []string{"MIT"},
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 61,
|
||||
EndLine: 121,
|
||||
},
|
||||
},
|
||||
DependsOn: []string{"pear/pear_exception@v1.0.2"},
|
||||
},
|
||||
{
|
||||
ID: "psr/log@1.1.4",
|
||||
Name: "psr/log",
|
||||
Version: "1.1.4",
|
||||
Indirect: false,
|
||||
Relationship: types.RelationshipDirect,
|
||||
Licenses: []string{"MIT"},
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 9,
|
||||
EndLine: 58,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ID: "pear/pear_exception@v1.0.2",
|
||||
Name: "pear/pear_exception",
|
||||
Version: "v1.0.2",
|
||||
Dev: true,
|
||||
Indirect: true,
|
||||
Relationship: types.RelationshipIndirect,
|
||||
Licenses: []string{"BSD-2-Clause"},
|
||||
Locations: []types.Location{
|
||||
{
|
||||
StartLine: 122,
|
||||
EndLine: 180,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
|
||||
8
pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.json
vendored
Normal file
8
pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.json
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"require": {
|
||||
"psr/log": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"pear/log": "^1.13"
|
||||
}
|
||||
}
|
||||
190
pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.lock
generated
vendored
Normal file
190
pkg/fanal/analyzer/language/php/composer/testdata/composer/with-dev/composer.lock
generated
vendored
Normal file
@@ -0,0 +1,190 @@
|
||||
{
|
||||
"_readme": [
|
||||
"This file locks the dependencies of your project to a known state",
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "2c9e13a2460669ca09226814c0aefb51",
|
||||
"packages": [
|
||||
{
|
||||
"name": "psr/log",
|
||||
"version": "1.1.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/php-fig/log.git",
|
||||
"reference": "d49695b909c3b7628b6289db5479a1c204601f11"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11",
|
||||
"reference": "d49695b909c3b7628b6289db5479a1c204601f11",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.1.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Psr\\Log\\": "Psr/Log/"
|
||||
}
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "PHP-FIG",
|
||||
"homepage": "https://www.php-fig.org/"
|
||||
}
|
||||
],
|
||||
"description": "Common interface for logging libraries",
|
||||
"homepage": "https://github.com/php-fig/log",
|
||||
"keywords": [
|
||||
"log",
|
||||
"psr",
|
||||
"psr-3"
|
||||
],
|
||||
"support": {
|
||||
"source": "https://github.com/php-fig/log/tree/1.1.4"
|
||||
},
|
||||
"time": "2021-05-03T11:20:27+00:00"
|
||||
}
|
||||
],
|
||||
"packages-dev": [
|
||||
{
|
||||
"name": "pear/log",
|
||||
"version": "1.14.6",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pear/Log.git",
|
||||
"reference": "e136d31ff6d5991e9707862f5fbfb97d40cd37a3"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pear/Log/zipball/e136d31ff6d5991e9707862f5fbfb97d40cd37a3",
|
||||
"reference": "e136d31ff6d5991e9707862f5fbfb97d40cd37a3",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"pear/pear_exception": "1.0.1 || 1.0.2",
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "*",
|
||||
"rector/rector": "*"
|
||||
},
|
||||
"suggest": {
|
||||
"pear/db": "Install optionally via your project's composer.json"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"psr-0": {
|
||||
"Log": "./"
|
||||
},
|
||||
"exclude-from-classmap": [
|
||||
"/examples/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"include-path": [
|
||||
""
|
||||
],
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Jon Parise",
|
||||
"email": "jon@php.net",
|
||||
"homepage": "https://www.indelible.org/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "PEAR Logging Framework",
|
||||
"homepage": "https://pear.github.io/Log/",
|
||||
"keywords": [
|
||||
"log",
|
||||
"logging"
|
||||
],
|
||||
"support": {
|
||||
"issues": "https://github.com/pear/Log/issues",
|
||||
"source": "https://github.com/pear/Log"
|
||||
},
|
||||
"time": "2025-07-27T00:25:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "pear/pear_exception",
|
||||
"version": "v1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/pear/PEAR_Exception.git",
|
||||
"reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/pear/PEAR_Exception/zipball/b14fbe2ddb0b9f94f5b24cf08783d599f776fff0",
|
||||
"reference": "b14fbe2ddb0b9f94f5b24cf08783d599f776fff0",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.2.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "<9"
|
||||
},
|
||||
"type": "class",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.0.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"PEAR/"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"include-path": [
|
||||
"."
|
||||
],
|
||||
"license": [
|
||||
"BSD-2-Clause"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Helgi Thormar",
|
||||
"email": "dufuz@php.net"
|
||||
},
|
||||
{
|
||||
"name": "Greg Beaver",
|
||||
"email": "cellog@php.net"
|
||||
}
|
||||
],
|
||||
"description": "The PEAR Exception base class.",
|
||||
"homepage": "https://github.com/pear/PEAR_Exception",
|
||||
"keywords": [
|
||||
"exception"
|
||||
],
|
||||
"support": {
|
||||
"issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=PEAR_Exception",
|
||||
"source": "https://github.com/pear/PEAR_Exception"
|
||||
},
|
||||
"time": "2021-03-21T15:43:46+00:00"
|
||||
}
|
||||
],
|
||||
"aliases": [],
|
||||
"minimum-stability": "stable",
|
||||
"stability-flags": {},
|
||||
"prefer-stable": false,
|
||||
"prefer-lowest": false,
|
||||
"platform": {},
|
||||
"platform-dev": {},
|
||||
"plugin-api-version": "2.9.0"
|
||||
}
|
||||
Reference in New Issue
Block a user