Skip to content

Commit

Permalink
feat(julia): Add Julia language analyzer support
Browse files Browse the repository at this point in the history
  • Loading branch information
Octogonapus committed Nov 27, 2023
1 parent ad977a4 commit a65056c
Show file tree
Hide file tree
Showing 26 changed files with 703 additions and 3 deletions.
1 change: 1 addition & 0 deletions .github/workflows/semantic-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ jobs:
dart
swift
bitnami
julia
os
lang
Expand Down
1 change: 1 addition & 0 deletions docs/community/contribute/pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ language:
- go
- elixir
- dart
- julia

vuln:

Expand Down
1 change: 1 addition & 0 deletions docs/docs/coverage/language/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ On the other hand, when the target is a post-build artifact, like a container im
| [Dart](dart.md) | pubspec.lock | - | - |||
| [Swift](swift.md) | Podfile.lock | - | - |||
| | Package.resolved | - | - |||
| [Julia](julia.md) | Manifest.toml |||||

The path of these files does not matter.

Expand Down
24 changes: 24 additions & 0 deletions docs/docs/coverage/language/julia.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Julia

## Features

Trivy supports [Pkg.jl](https://pkgdocs.julialang.org/v1/), which is the Julia package manager.
The following table provides an outline of the features Trivy offers.

| Package manager | File | Transitive dependencies | Dev dependencies | License | Dependency graph | Position |
| --------------- | ------------- | :---------------------: | :--------------- | :-----: | :--------------: | :------: |
| Pkg.jl | Manifest.toml || Excluded[^1] | - |||

### Pkg.jl

Trivy searches for `Manifest.toml` to detect dependencies.

Trivy also supports dependency trees; however, to display an accurate tree, it needs to know whether each package is a direct dependency of the project.
Since this information is not included in `Manifest.toml`, Trivy parses `Project.toml`, which should be located next to `Project.toml`.
If you want to see the dependency tree, please ensure that `Project.toml` is present.

Scanning `Manifest.toml` and `Project.toml` together also removes developer dependencies.

Dependency extensions are currently ignored.

[^1]: When you scan `Manifest.toml` and `Project.toml` together.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ require (
github.com/alicebob/miniredis/v2 v2.30.4
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986
github.com/aquasecurity/defsec v0.93.2-0.20231024055158-015ab97ce898
github.com/aquasecurity/go-dep-parser v0.0.0-20231120074854-8322cc2242bf
github.com/aquasecurity/go-dep-parser v0.0.0-20231122010617-fc7f2b470d89
github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce
github.com/aquasecurity/go-npm-version v0.0.0-20201110091526-0b796d180798
github.com/aquasecurity/go-pep440-version v0.0.0-20210121094942-22b2f8951d46
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -322,8 +322,8 @@ github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986 h1:2a30
github.com/aquasecurity/bolt-fixtures v0.0.0-20200903104109-d34e7f983986/go.mod h1:NT+jyeCzXk6vXR5MTkdn4z64TgGfE5HMLC8qfj5unl8=
github.com/aquasecurity/defsec v0.93.2-0.20231024055158-015ab97ce898 h1:gu7XQvv2CswgzOdOFHg/AmtR4vBonG35XvGxHHvcIr4=
github.com/aquasecurity/defsec v0.93.2-0.20231024055158-015ab97ce898/go.mod h1:J30VViSgmoW2Ic/6aqVJO2qvuADsmZ3MYuNxPcU6Vt0=
github.com/aquasecurity/go-dep-parser v0.0.0-20231120074854-8322cc2242bf h1:kweQrNMfarPfjZGI1537GtuujhpzhsuT/MvmW2FwaBE=
github.com/aquasecurity/go-dep-parser v0.0.0-20231120074854-8322cc2242bf/go.mod h1:7+xrs6AWD5+onpmX8f7qIkAhUgkPP0mhUdBjxJBcfas=
github.com/aquasecurity/go-dep-parser v0.0.0-20231122010617-fc7f2b470d89 h1:IssyGUDzAYvTcv28EJMmqYWhQh5sW9HiJwDphEFNSWM=
github.com/aquasecurity/go-dep-parser v0.0.0-20231122010617-fc7f2b470d89/go.mod h1:7+xrs6AWD5+onpmX8f7qIkAhUgkPP0mhUdBjxJBcfas=
github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce h1:QgBRgJvtEOBtUXilDb1MLi1p1MWoyFDXAu5DEUl5nwM=
github.com/aquasecurity/go-gem-version v0.0.0-20201115065557-8eed6fe000ce/go.mod h1:HXgVzOPvXhVGLJs4ZKO817idqr/xhwsTcj17CLYY74s=
github.com/aquasecurity/go-mock-aws v0.0.0-20230810212901-d6feebd39060 h1:V7nC90NpRDEubNpNEgRDtTfLH3RKQlZeY9/HSqxEze8=
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ nav:
- Ruby: docs/coverage/language/ruby.md
- Rust: docs/coverage/language/rust.md
- Swift: docs/coverage/language/swift.md
- Julia: docs/coverage/language/julia.md
- IaC:
- Overview: docs/coverage/iac/index.md
- Azure ARM Template: docs/coverage/iac/azure-arm.md
Expand Down
3 changes: 3 additions & 0 deletions pkg/detector/library/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,9 @@ func NewDriver(libType ftypes.LangType) (Driver, bool) {
case ftypes.K8sUpstream:
ecosystem = vulnerability.Kubernetes
comparer = compare.GenericComparer{}
case ftypes.Julia:
log.Logger.Warn("Julia is supported for SBOM, not for vulnerability scanning")
return Driver{}, false
default:
log.Logger.Warnf("The %q library type is not supported for vulnerability scanning", libType)
return Driver{}, false
Expand Down
1 change: 1 addition & 0 deletions pkg/fanal/analyzer/all/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/gradle"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/jar"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/java/pom"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/julia/pkg"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/npm"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/pkg"
_ "github.com/aquasecurity/trivy/pkg/fanal/analyzer/language/nodejs/pnpm"
Expand Down
4 changes: 4 additions & 0 deletions pkg/fanal/analyzer/const.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ const (
// Dart
TypePubSpecLock Type = "pubspec-lock"

// Julia
TypeJulia Type = "julia"

// ============
// Non-packaged
// ============
Expand Down Expand Up @@ -184,6 +187,7 @@ var (
TypeSwift,
TypePubSpecLock,
TypeMixLock,
TypeJulia,
}

// TypeLockfiles has all lock file analyzers
Expand Down
178 changes: 178 additions & 0 deletions pkg/fanal/analyzer/language/julia/pkg/pkg.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
package pkgjl

import (
"context"
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"sort"

"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"

"github.com/BurntSushi/toml"
"github.com/samber/lo"

julia "github.com/aquasecurity/go-dep-parser/pkg/julia/manifest"
godeptypes "github.com/aquasecurity/go-dep-parser/pkg/types"
"github.com/aquasecurity/trivy/pkg/detector/library/compare"
"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"
"github.com/aquasecurity/trivy/pkg/utils/fsutils"
)

func init() {
analyzer.RegisterPostAnalyzer(analyzer.TypeJulia, newJuliaAnalyzer)
}

const version = 1

var requiredFiles = []string{
types.JuliaManifest,
types.JuliaProject,
}

type juliaAnalyzer struct {
lockParser godeptypes.Parser
comparer compare.GenericComparer
}

type Project struct {
Dependencies map[string]string `toml:"deps"`
}

func newJuliaAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
return &juliaAnalyzer{
lockParser: julia.NewParser(),
comparer: compare.GenericComparer{},
}, nil
}

func (a juliaAnalyzer) PostAnalyze(_ context.Context, input analyzer.PostAnalysisInput) (*analyzer.AnalysisResult, error) {
var apps []types.Application

required := func(path string, d fs.DirEntry) bool {
return filepath.Base(path) == types.JuliaManifest
}

err := fsutils.WalkDir(input.FS, ".", required, func(path string, d fs.DirEntry, r io.Reader) error {
// Parse Manifest.toml
app, err := a.parseJuliaManifest(path, r)
if err != nil {
return xerrors.Errorf("parse error: %w", err)
} else if app == nil {
return nil
}

// Parse Project.toml alongside Manifest.toml to identify the direct dependencies. This mutates `app`.
if err = a.removeExtraDependencies(input.FS, filepath.Dir(path), app); err != nil {
log.Logger.Warnf("Unable to parse %q to identify direct dependencies: %s", filepath.Join(filepath.Dir(path), types.JuliaProject), err)
}

sort.Sort(app.Libraries)
apps = append(apps, *app)
return nil
})
if err != nil {
return nil, xerrors.Errorf("julia walk error: %w", err)
}

return &analyzer.AnalysisResult{
Applications: apps,
}, nil
}

func (a juliaAnalyzer) Required(filePath string, _ os.FileInfo) bool {
fileName := filepath.Base(filePath)
return slices.Contains(requiredFiles, fileName)
}

func (a juliaAnalyzer) Type() analyzer.Type {
return analyzer.TypeJulia
}

func (a juliaAnalyzer) Version() int {
return version
}

func (a juliaAnalyzer) parseJuliaManifest(path string, r io.Reader) (*types.Application, error) {
return language.Parse(types.Julia, path, r, a.lockParser)
}

// Removes dependencies not specified as direct dependencies in the Project.toml file.
// This is not strictly necessary, but given that test dependencies are in flux right now, this is future-proofing.
// https://pkgdocs.julialang.org/v1/creating-packages/#target-based-test-specific-dependencies
func (a juliaAnalyzer) removeExtraDependencies(fsys fs.FS, dir string, app *types.Application) error {
projectPath := filepath.Join(dir, types.JuliaProject)
project, err := parseJuliaProject(fsys, projectPath)
if errors.Is(err, fs.ErrNotExist) {
log.Logger.Debugf("Julia: %s not found", projectPath)
return nil
} else if err != nil {
return xerrors.Errorf("unable to parse %s: %w", projectPath, err)
}

// Project.toml file can contain same libraries with different versions.
// Save versions separately for version comparison by comparator
pkgIDs := lo.SliceToMap(app.Libraries, func(pkg types.Package) (string, types.Package) {
return pkg.ID, pkg
})

// Identify direct dependencies
visited := map[string]types.Package{}
for _, uuid := range project.Dependencies {
// uuid is a direct dep since it's in the Project's dependencies section. Search through Libraries to mark the matching one as a direct dep.
if pkg, ok := pkgIDs[uuid]; ok {
// Mark as a direct dependency
pkg.Indirect = false
visited[pkg.ID] = pkg
}
}

// Identify indirect dependencies
for _, pkg := range visited {
walkIndirectDependencies(pkg, pkgIDs, visited)
}

// Include only the packages we visited so that we don't include any deps from the [extras] section
app.Libraries = maps.Values(visited)
return nil
}

// Parses Project.toml
func parseJuliaProject(fsys fs.FS, path string) (Project, error) {
proj := Project{}
f, err := fsys.Open(path)
if err != nil {
return proj, xerrors.Errorf("file open error: %w", err)
}
defer func() { _ = f.Close() }()

if _, err = toml.NewDecoder(f).Decode(&proj); err != nil {
return proj, xerrors.Errorf("decode error: %w", err)
}
return proj, nil
}

// Marks all indirect dependencies as indirect. Starts from `rootPkg`. Visited deps are added to `visited`.
func walkIndirectDependencies(rootPkg types.Package, allPkgIDs map[string]types.Package, visited map[string]types.Package) {
for _, pkgID := range rootPkg.DependsOn {
if _, ok := visited[pkgID]; ok {
continue
}

dep, ok := allPkgIDs[pkgID]
if !ok {
continue
}

dep.Indirect = true
visited[dep.ID] = dep
walkIndirectDependencies(dep, allPkgIDs, visited)
}
}

0 comments on commit a65056c

Please sign in to comment.