Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(conda): add licenses support for environment.yml files #6953

Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 13 additions & 10 deletions docs/docs/coverage/os/conda.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Trivy supports the following scanners for Conda packages.
|:-------------:|:---------:|
| SBOM | ✓ |
| Vulnerability | - |
| License | ✓[^1] |
| License | |


## SBOM
Expand All @@ -16,21 +16,24 @@ Trivy detects packages that have been installed with `Conda`.
### `<package>.json`
Trivy parses `<conda-root>/envs/<env>/conda-meta/<package>.json` files to find the version and license for the dependencies installed in your env.

### `environment.yml`[^2]
Trivy supports parsing [environment.yml][environment.yml][^2] files to find dependency list.
### `environment.yml`[^1]
Trivy supports parsing [environment.yml][environment.yml][^1] files to find dependency list.

!!! note
License detection is currently not supported.

`environment.yml`[^2] files supports [version range][env-version-range]. We can't be sure about versions for these dependencies.
Therefore, you need to use `conda env export` command to get dependency list in `Conda` default format before scanning `environment.yml`[^2] file.
`environment.yml`[^1] files supports [version range][env-version-range]. We can't be sure about versions for these dependencies.
Therefore, you need to use `conda env export` command to get dependency list in `Conda` default format before scanning `environment.yml`[^1] file.

!!! note
For dependencies in a non-Conda format, Trivy doesn't include a version of them.

#### licenses
knqyf263 marked this conversation as resolved.
Show resolved Hide resolved
Trivy parses `conda-meta/<package>.json` files at the [prefix] path.
To correctly define licenses, make sure your `environment.yml`[^1] contains `prefix` field and `prefix` directory contains `package.json` files.

!!! note
To get correct `environment.yml`[^1] file and fill `prefix` directory - use `conda env export` command.

[^1]: License detection is only supported for `<package>.json` files
[^2]: Trivy supports both `yaml` and `yml` extensions.
[^1]: Trivy supports both `yaml` and `yml` extensions.

[environment.yml]: https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#sharing-an-environment
[env-version-range]: https://docs.conda.io/projects/conda-build/en/latest/resources/package-spec.html#examples-of-package-specs
[prefix]: https://conda.io/projects/conda/en/latest/user-guide/tasks/manage-environments.html#specifying-a-location-for-an-environment
6 changes: 4 additions & 2 deletions pkg/dependency/parser/conda/environment/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (

type environment struct {
Entries []Entry `yaml:"dependencies"`
Prefix string `yaml:"prefix"`
}

type Entry struct {
Expand Down Expand Up @@ -48,7 +49,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
var pkgs ftypes.Packages
for _, entry := range env.Entries {
for _, dep := range entry.Dependencies {
pkg := p.toPackage(dep)
pkg := p.toPackage(dep, env.Prefix)
// Skip empty pkgs
if pkg.Name == "" {
continue
Expand All @@ -61,7 +62,7 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
return pkgs, nil, nil
}

func (p *Parser) toPackage(dep Dependency) ftypes.Package {
func (p *Parser) toPackage(dep Dependency, prefix string) ftypes.Package {
name, ver := p.parseDependency(dep.Value)
if ver == "" {
p.once.Do(func() {
Expand All @@ -77,6 +78,7 @@ func (p *Parser) toPackage(dep Dependency) ftypes.Package {
EndLine: dep.Line,
},
},
FilePath: prefix,
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use prefix as FilePath in dependency parser.
This is a workaround.
Perhaps we need to add a prefix or a new field to Package.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's inefficient, but what if parsing environment.yaml again in pkg/fanal/analyzer/language/conda/environment/environment.go to just extract a prefix? Using FilePath is a bit hacky.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah. I doubted this decision.

I didn't think about it right away, but what if we will use logic as for package.json file.
I mean don't implement Parser interface:

type Package struct {
ftypes.Package
Dependencies map[string]string
OptionalDependencies map[string]string
DevDependencies map[string]string
Workspaces []string
}
type Parser struct{}
func NewParser() *Parser {
return &Parser{}
}
func (p *Parser) Parse(r io.Reader) (Package, error) {

wdyt?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's also okay for me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@DmitriyLewen Do you think you'll update it shortly?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I want to do this today.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@knqyf263 i updated PR (67daea7 + 9200c44)
take a look, please.

}
}

Expand Down
17 changes: 17 additions & 0 deletions pkg/dependency/parser/conda/environment/parse_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ func TestParse(t *testing.T) {
EndLine: 6,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "asgiref",
Expand All @@ -40,6 +41,7 @@ func TestParse(t *testing.T) {
EndLine: 21,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "blas",
Expand All @@ -50,6 +52,7 @@ func TestParse(t *testing.T) {
EndLine: 5,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "bzip2",
Expand All @@ -60,6 +63,7 @@ func TestParse(t *testing.T) {
EndLine: 19,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "ca-certificates",
Expand All @@ -70,6 +74,7 @@ func TestParse(t *testing.T) {
EndLine: 7,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "django",
Expand All @@ -80,6 +85,7 @@ func TestParse(t *testing.T) {
EndLine: 22,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "ld_impl_linux-aarch64",
Expand All @@ -89,6 +95,7 @@ func TestParse(t *testing.T) {
EndLine: 8,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libblas",
Expand All @@ -98,6 +105,7 @@ func TestParse(t *testing.T) {
EndLine: 9,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libcblas",
Expand All @@ -107,6 +115,7 @@ func TestParse(t *testing.T) {
EndLine: 10,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libexpat",
Expand All @@ -117,6 +126,7 @@ func TestParse(t *testing.T) {
EndLine: 11,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libffi",
Expand All @@ -127,6 +137,7 @@ func TestParse(t *testing.T) {
EndLine: 12,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libgcc-ng",
Expand All @@ -136,6 +147,7 @@ func TestParse(t *testing.T) {
EndLine: 13,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libgfortran-ng",
Expand All @@ -145,6 +157,7 @@ func TestParse(t *testing.T) {
EndLine: 14,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libgfortran5",
Expand All @@ -154,6 +167,7 @@ func TestParse(t *testing.T) {
EndLine: 15,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libgomp",
Expand All @@ -164,6 +178,7 @@ func TestParse(t *testing.T) {
EndLine: 16,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "liblapack",
Expand All @@ -173,6 +188,7 @@ func TestParse(t *testing.T) {
EndLine: 17,
},
},
FilePath: "/opt/conda/envs/test-env",
},
{
Name: "libnsl",
Expand All @@ -183,6 +199,7 @@ func TestParse(t *testing.T) {
EndLine: 18,
},
},
FilePath: "/opt/conda/envs/test-env",
},
},
},
Expand Down
67 changes: 66 additions & 1 deletion pkg/fanal/analyzer/language/conda/environment/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,27 @@ package environment

import (
"context"
"fmt"
"os"
"path/filepath"
"sync"

"github.com/bmatcuk/doublestar/v4"
"golang.org/x/xerrors"

"github.com/aquasecurity/trivy/pkg/dependency/parser/conda/environment"
"github.com/aquasecurity/trivy/pkg/dependency/parser/conda/meta"
"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"
)

func init() {
analyzer.RegisterAnalyzer(&environmentAnalyzer{})
}

const version = 1
const version = 2

type environmentAnalyzer struct{}

Expand All @@ -26,8 +31,68 @@ func (a environmentAnalyzer) Analyze(_ context.Context, input analyzer.AnalysisI
if err != nil {
return nil, xerrors.Errorf("unable to parse environment.yaml: %w", err)
}

if res != nil && len(res.Applications) > 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I prefer an early return.

Suggested change
if res != nil && len(res.Applications) > 0 {
if res == nil || len(res.Applications) == 0 {
return nil, nil
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

updated in d75bce2

once := sync.Once{}
// For `environment.yaml` Applications always contains only 1 Application
for i, pkg := range res.Applications[0].Packages {
// Skip packages without a version, because in this case we will not be able to get the correct file name.
if pkg.Version != "" {
licenses, err := findLicenseFromEnvDir(pkg)
if err != nil {
// Show log once per file
once.Do(func() {
log.WithPrefix("conda").Debug("License not found. For more information, see https://aquasecurity.github.io/trivy/latest/docs/coverage/os/conda/#licenses",
log.String("file", input.FilePath), log.String("pkg", pkg.Name), log.Err(err))
})
}
pkg.Licenses = licenses
}
pkg.FilePath = "" // remove `prefix` from FilePath
res.Applications[0].Packages[i] = pkg
}

}

return res, nil
}

func findLicenseFromEnvDir(pkg types.Package) ([]string, error) {
if pkg.FilePath == "" {
return nil, xerrors.Errorf("`prefix` field doesn't exist")
}
condaMetaDir := filepath.Join(pkg.FilePath, "conda-meta")
entries, err := os.ReadDir(condaMetaDir)
if err != nil {
return nil, xerrors.Errorf("unable to read conda-meta dir: %w", err)
}
for _, entry := range entries {
if entry.IsDir() {
continue
}

pattern := fmt.Sprintf("%s-%s-*.json", pkg.Name, pkg.Version)
matched, err := doublestar.Match(pattern, entry.Name())
if err != nil {
return nil, xerrors.Errorf("incorrect packageJSON file pattern: %w", err)
}
if matched {
file, err := os.Open(filepath.Join(condaMetaDir, entry.Name()))
if err != nil {
return nil, xerrors.Errorf("unable to open packageJSON file: %w", err)
}
packageJson, _, err := meta.NewParser().Parse(file)
if err != nil {
return nil, xerrors.Errorf("unable to parse packageJSON file: %w", err)
}
// packageJson always contain only 1 element
// cf. https://github.com/aquasecurity/trivy/blob/c3192f061d7e84eaf38df8df7c879dc00b4ca137/pkg/dependency/parser/conda/meta/parse.go#L39-L45
return packageJson[0].Licenses, nil
}
}
return nil, xerrors.Errorf("meta file didn't find")
}

func (a environmentAnalyzer) Required(filePath string, _ os.FileInfo) bool {
return filepath.Base(filePath) == types.CondaEnvYml || filepath.Base(filePath) == types.CondaEnvYaml
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,65 @@ func Test_environmentAnalyzer_Analyze(t *testing.T) {
},
},
},
{
name: "happy path with licenses",
inputFile: "testdata/environment-with-licenses.yaml",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.CondaEnv,
FilePath: "testdata/environment-with-licenses.yaml",
Packages: types.Packages{
{
Name: "_libgcc_mutex",
Locations: []types.Location{
{
StartLine: 5,
EndLine: 5,
},
},
},
{
Name: "_openmp_mutex",
Version: "5.1",
Locations: []types.Location{
{
StartLine: 6,
EndLine: 6,
},
},
Licenses: []string{
"BSD-3-Clause",
},
},
{
Name: "blas",
Version: "1.0",
Locations: []types.Location{
{
StartLine: 7,
EndLine: 7,
},
},
},
{
Name: "bzip2",
Version: "1.0.8",
Locations: []types.Location{
{
StartLine: 8,
EndLine: 8,
},
},
Licenses: []string{
"bzip2-1.0.8",
},
},
},
},
},
},
},
{
name: "invalid",
inputFile: "testdata/invalid.yaml",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"license": "MIT",
"name": "_libgcc_mutex",
"version": "0.1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"license": "BSD-3-Clause",
"name": "_openmp_mutex",
"version": "5.1"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"license": "",
"name": "blas",
"version": "1.0"
}
Loading