feat: Add Julia language analyzer support (#5635)

This commit is contained in:
Octogonapus
2024-05-14 22:56:48 -04:00
committed by GitHub
parent 7c22ee3df5
commit fecafb1fc5
31 changed files with 849 additions and 2 deletions

View File

@@ -77,6 +77,7 @@ jobs:
swift
bitnami
conda
julia
os
lang

View File

@@ -143,6 +143,7 @@ language:
- go
- elixir
- dart
- julia
vuln:

View File

@@ -47,6 +47,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.

View File

@@ -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.

View File

@@ -397,6 +397,15 @@ func TestRepository(t *testing.T) {
want.ArtifactType = artifact.TypeFilesystem
},
},
{
name: "julia generating SPDX SBOM",
args: args{
command: "rootfs",
format: "spdx-json",
input: "testdata/fixtures/repo/julia",
},
golden: "testdata/julia-spdx.json.golden",
},
}
// Set up testing DB

View File

@@ -0,0 +1,16 @@
# This file is machine-generated - editing it directly is not advised
julia_version = "1.9.0"
manifest_format = "2.0"
project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc"
[[deps.A]]
uuid = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60"
[deps.A.deps]
B = "f41f7b98-334e-11e9-1257-49272045fb24"
[[deps.B]]
uuid = "f41f7b98-334e-11e9-1257-49272045fb24"
[[deps.B]]
uuid = "edca9bc6-334e-11e9-3554-9595dbb4349c"

View File

@@ -0,0 +1,7 @@
name = "packageName"
uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773"
version = "0.1.0"
[deps]
A = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60"
B = "edca9bc6-334e-11e9-3554-9595dbb4349c"

View File

@@ -0,0 +1,138 @@
{
"spdxVersion": "SPDX-2.3",
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "testdata/fixtures/repo/julia",
"documentNamespace": "http://aquasecurity.github.io/trivy/filesystem/testdata/fixtures/repo/julia-3ff14136-e09f-4df9-80ea-000000000006",
"creationInfo": {
"creators": [
"Organization: aquasecurity",
"Tool: trivy-dev"
],
"created": "2021-08-25T12:20:30Z"
},
"packages": [
{
"name": "Manifest.toml",
"SPDXID": "SPDXRef-Application-18fc3597717a3e56",
"downloadLocation": "NONE",
"filesAnalyzed": false,
"attributionTexts": [
"Class: lang-pkgs",
"Type: julia"
],
"primaryPackagePurpose": "APPLICATION"
},
{
"name": "A",
"SPDXID": "SPDXRef-Package-2a46714189f3b9de",
"versionInfo": "1.9.0",
"supplier": "NOASSERTION",
"downloadLocation": "NONE",
"filesAnalyzed": false,
"sourceInfo": "package found in: Manifest.toml",
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:julia/A@1.9.0?uuid=ead4f63c-334e-11e9-00e6-e7f0a5f21b60"
}
],
"attributionTexts": [
"PkgID: ead4f63c-334e-11e9-00e6-e7f0a5f21b60",
"PkgType: julia"
],
"primaryPackagePurpose": "LIBRARY"
},
{
"name": "B",
"SPDXID": "SPDXRef-Package-4a8e351c4c9b7318",
"versionInfo": "1.9.0",
"supplier": "NOASSERTION",
"downloadLocation": "NONE",
"filesAnalyzed": false,
"sourceInfo": "package found in: Manifest.toml",
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:julia/B@1.9.0?uuid=edca9bc6-334e-11e9-3554-9595dbb4349c"
}
],
"attributionTexts": [
"PkgID: edca9bc6-334e-11e9-3554-9595dbb4349c",
"PkgType: julia"
],
"primaryPackagePurpose": "LIBRARY"
},
{
"name": "B",
"SPDXID": "SPDXRef-Package-d10d5e4a30a43fff",
"versionInfo": "1.9.0",
"supplier": "NOASSERTION",
"downloadLocation": "NONE",
"filesAnalyzed": false,
"sourceInfo": "package found in: Manifest.toml",
"licenseConcluded": "NONE",
"licenseDeclared": "NONE",
"externalRefs": [
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:julia/B@1.9.0?uuid=f41f7b98-334e-11e9-1257-49272045fb24"
}
],
"attributionTexts": [
"PkgID: f41f7b98-334e-11e9-1257-49272045fb24",
"PkgType: julia"
],
"primaryPackagePurpose": "LIBRARY"
},
{
"name": "testdata/fixtures/repo/julia",
"SPDXID": "SPDXRef-Filesystem-1be792dd0077c431",
"downloadLocation": "NONE",
"filesAnalyzed": false,
"attributionTexts": [
"SchemaVersion: 2"
],
"primaryPackagePurpose": "SOURCE"
}
],
"relationships": [
{
"spdxElementId": "SPDXRef-Application-18fc3597717a3e56",
"relatedSpdxElement": "SPDXRef-Package-2a46714189f3b9de",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Application-18fc3597717a3e56",
"relatedSpdxElement": "SPDXRef-Package-4a8e351c4c9b7318",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Application-18fc3597717a3e56",
"relatedSpdxElement": "SPDXRef-Package-d10d5e4a30a43fff",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-DOCUMENT",
"relatedSpdxElement": "SPDXRef-Filesystem-1be792dd0077c431",
"relationshipType": "DESCRIBES"
},
{
"spdxElementId": "SPDXRef-Filesystem-1be792dd0077c431",
"relatedSpdxElement": "SPDXRef-Application-18fc3597717a3e56",
"relationshipType": "CONTAINS"
},
{
"spdxElementId": "SPDXRef-Package-2a46714189f3b9de",
"relatedSpdxElement": "SPDXRef-Package-d10d5e4a30a43fff",
"relationshipType": "DEPENDS_ON"
}
]
}

View File

@@ -102,6 +102,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

View File

@@ -36,8 +36,9 @@ func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependenc
var primMan primitiveManifest
var manMetadata toml.MetaData
decoder := toml.NewDecoder(r)
// Try to read the old Manifest format. If that fails, try the new format.
if _, err := decoder.Decode(&oldDeps); err != nil {
// Try to read the old Manifest format. This can also read the v1.0 Manifest format, which we parse out later.
var err error
if manMetadata, err = decoder.Decode(&oldDeps); err != nil {
if _, err = r.Seek(0, io.SeekStart); err != nil {
return nil, nil, xerrors.Errorf("seek error: %w", err)
}

View File

@@ -54,6 +54,12 @@ func TestParse(t *testing.T) {
want: juliaV1_9ShadowedDepPkgs,
wantDeps: juliaV1_9ShadowedDepDeps,
},
{
name: "julia v1.0 format",
file: "testdata/julia_v1.0_format/Manifest.toml",
want: juliaV10FormatPkgs,
wantDeps: juliaV10FormatDeps,
},
}
for _, tt := range tests {

View File

@@ -74,4 +74,19 @@ var (
juliaV1_9ShadowedDepDeps = []ftypes.Dependency{
{ID: "ead4f63c-334e-11e9-00e6-e7f0a5f21b60", DependsOn: []string{"f41f7b98-334e-11e9-1257-49272045fb24"}},
}
juliaV10FormatPkgs = []ftypes.Package{
{ID: "767738be-2f1f-45a9-b806-0234f3164144", Name: "Foo", Version: "unknown", Locations: []ftypes.Location{{StartLine: 1, EndLine: 5}}},
{ID: "6f418443-bd2e-4783-b551-cdbac608adf2", Name: "Foo", Version: "unknown", Locations: []ftypes.Location{{StartLine: 7, EndLine: 10}}},
{ID: "2a550a13-6bab-4a91-a4ee-dff34d6b99d0", Name: "Bar", Version: "unknown", Locations: []ftypes.Location{{StartLine: 12, EndLine: 14}}},
{ID: "6801f525-dc68-44e8-a4e8-cabd286279e7", Name: "Baz", Version: "unknown", Locations: []ftypes.Location{{StartLine: 19, EndLine: 21}}},
{ID: "b5ec9b9c-e354-47fd-b367-a348bdc8f909", Name: "Qux", Version: "unknown", Locations: []ftypes.Location{{StartLine: 26, EndLine: 28}}},
}
juliaV10FormatDeps = []ftypes.Dependency{
{ID: "767738be-2f1f-45a9-b806-0234f3164144", DependsOn: []string{"2a550a13-6bab-4a91-a4ee-dff34d6b99d0", "6801f525-dc68-44e8-a4e8-cabd286279e7", "b5ec9b9c-e354-47fd-b367-a348bdc8f909"}},
{ID: "6f418443-bd2e-4783-b551-cdbac608adf2", DependsOn: []string{"b5ec9b9c-e354-47fd-b367-a348bdc8f909"}},
{ID: "2a550a13-6bab-4a91-a4ee-dff34d6b99d0", DependsOn: []string{"6801f525-dc68-44e8-a4e8-cabd286279e7", "6f418443-bd2e-4783-b551-cdbac608adf2"}},
{ID: "6801f525-dc68-44e8-a4e8-cabd286279e7", DependsOn: []string{"6f418443-bd2e-4783-b551-cdbac608adf2", "b5ec9b9c-e354-47fd-b367-a348bdc8f909"}},
}
)

View File

@@ -0,0 +1,28 @@
[[Foo]]
deps = ["Bar", "Baz", "Qux"]
uuid = "767738be-2f1f-45a9-b806-0234f3164144"
git-tree-sha1 = "7c626031568a5e432112a74009c3763f9b851e3e"
path = "deps/Foo1"
[[Foo]]
deps = ["Qux"]
uuid = "6f418443-bd2e-4783-b551-cdbac608adf2"
path = "deps/Foo2.jl"
[[Bar]]
uuid = "2a550a13-6bab-4a91-a4ee-dff34d6b99d0"
path = "deps/Bar"
[Bar.deps]
Baz = "6801f525-dc68-44e8-a4e8-cabd286279e7"
Foo = "6f418443-bd2e-4783-b551-cdbac608adf2"
[[Baz]]
uuid = "6801f525-dc68-44e8-a4e8-cabd286279e7"
git-tree-sha1 = "efc7e24c53d6a328011975294a2c75fed2f9800a"
[Baz.deps]
Foo = "6f418443-bd2e-4783-b551-cdbac608adf2"
Qux = "b5ec9b9c-e354-47fd-b367-a348bdc8f909"
[[Qux]]
uuid = "b5ec9b9c-e354-47fd-b367-a348bdc8f909"
path = "deps/Qux.jl"

View File

@@ -0,0 +1,6 @@
name = "TestProject"
uuid = "84c38c17-0c6f-4d12-a694-d20b69c16777"
[deps]
Foo = "767738be-2f1f-45a9-b806-0234f3164144"
Bar = "2a550a13-6bab-4a91-a4ee-dff34d6b99d0"

View File

@@ -81,6 +81,9 @@ func NewDriver(libType ftypes.LangType) (Driver, bool) {
case ftypes.K8sUpstream:
ecosystem = vulnerability.Kubernetes
comparer = compare.GenericComparer{}
case ftypes.Julia:
log.Warn("Julia is supported for SBOM, not for vulnerability scanning")
return Driver{}, false
default:
log.Warn("The library type is not supported for vulnerability scanning",
log.String("type", string(libType)))

View File

@@ -20,6 +20,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"

View File

@@ -94,6 +94,9 @@ const (
// Dart
TypePubSpecLock Type = "pubspec-lock"
// Julia
TypeJulia Type = "julia"
// ============
// Non-packaged
// ============
@@ -191,6 +194,7 @@ var (
TypeSwift,
TypePubSpecLock,
TypeMixLock,
TypeJulia,
}
// TypeLockfiles has all lock file analyzers

View File

@@ -0,0 +1,190 @@
package pkgjl
import (
"context"
"errors"
"io"
"io/fs"
"os"
"path/filepath"
"sort"
"github.com/BurntSushi/toml"
"github.com/samber/lo"
"golang.org/x/exp/maps"
"golang.org/x/exp/slices"
"golang.org/x/xerrors"
julia "github.com/aquasecurity/trivy/pkg/dependency/parser/julia/manifest"
"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 language.Parser
logger *log.Logger
}
type Project struct {
Dependencies map[string]string `toml:"deps"`
Extras map[string]string `toml:"extras"`
}
func newJuliaAnalyzer(_ analyzer.AnalyzerOptions) (analyzer.PostAnalyzer, error) {
return &juliaAnalyzer{
lockParser: julia.NewParser(),
logger: log.WithPrefix("julia"),
}, 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.analyzeDependencies(input.FS, filepath.Dir(path), app); err != nil {
a.logger.Warn("Unable to parse file to analyze dependencies",
log.String("FILEPATH", filepath.Join(filepath.Dir(path), types.JuliaProject)), log.Err(err))
}
sort.Sort(app.Packages)
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)
}
func (a juliaAnalyzer) analyzeDependencies(fsys fs.FS, dir string, app *types.Application) error {
deps, devDeps, err := a.getProjectDeps(fsys, dir)
if err != nil {
return err
}
pkgs := walkDependencies(deps, app.Packages, false)
devPkgs := walkDependencies(devDeps, app.Packages, true)
app.Packages = append(pkgs, devPkgs...)
return nil
}
// getProjectDeps parses project.toml and returns root and dev dependencies.
func (a juliaAnalyzer) getProjectDeps(fsys fs.FS, dir string) (map[string]string, map[string]string, error) {
projectPath := filepath.Join(dir, types.JuliaProject)
project, err := parseJuliaProject(fsys, projectPath)
if errors.Is(err, fs.ErrNotExist) {
a.logger.Debug("Julia project not found", log.String("PROJECT_PATH", projectPath))
return nil, nil, nil
} else if err != nil {
return nil, nil, xerrors.Errorf("unable to parse %s: %w", projectPath, err)
}
return project.Dependencies, project.Extras, 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 the given direct dependencies as direct, then marks those packages' dependencies as indirect.
// Marks all encountered packages' Dev flag according to `dev`.
// Modifies the packages in `allPackages`.
func walkDependencies(directDeps map[string]string, allPackages types.Packages, dev bool) []types.Package {
pkgsByID := lo.SliceToMap(allPackages, func(pkg types.Package) (string, types.Package) {
return pkg.ID, pkg
})
// Identify direct dependencies
// Everything in `directDeps` is assumed to be direct
visited := make(map[string]types.Package)
for _, uuid := range directDeps {
if pkg, ok := pkgsByID[uuid]; ok {
pkg.Indirect = false
pkg.Dev = dev
visited[pkg.ID] = pkg
}
}
// Identify indirect dependencies
for _, pkg := range visited {
walkIndirectDependencies(pkg, pkgsByID, visited)
}
return maps.Values(visited)
}
// Marks all indirect dependencies as indirect. Starts from `rootPkg`. Visited deps are added to `visited`.
func walkIndirectDependencies(rootPkg types.Package, allPkgIDs, 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
dep.Dev = rootPkg.Dev
visited[dep.ID] = dep
walkIndirectDependencies(dep, allPkgIDs, visited)
}
}

View File

@@ -0,0 +1,209 @@
package pkgjl
import (
"context"
"os"
"testing"
"github.com/aquasecurity/trivy/pkg/fanal/analyzer"
"github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_juliaAnalyzer_Analyze(t *testing.T) {
tests := []struct {
name string
dir string
want *analyzer.AnalysisResult
}{
{
name: "happy path",
dir: "testdata/happy",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.Julia,
FilePath: "Manifest.toml",
Packages: types.Packages{
{
ID: "ade2ca70-3891-5945-98fb-dc099432e06a",
Name: "Dates",
Version: "1.9.0",
Indirect: false,
Locations: []types.Location{{StartLine: 7, EndLine: 9}},
DependsOn: []string{"de0858da-6303-5e67-8744-51eddeeeb8d7"},
},
{
ID: "d9a60922-03b4-4a1b-81be-b8d05b827236",
Name: "DevDep",
Version: "1.0.0",
Indirect: false,
Dev: true,
Locations: []types.Location{{StartLine: 65, EndLine: 68}},
DependsOn: []string{"b637660b-5035-4894-8335-b3805a4b50d8"},
},
{
ID: "b637660b-5035-4894-8335-b3805a4b50d8",
Name: "IndirectDevDep",
Version: "2.0.0",
Indirect: true,
Dev: true,
Locations: []types.Location{{StartLine: 70, EndLine: 72}},
},
{
ID: "682c06a0-de6a-54ab-a142-c8b1cf79cde6",
Name: "JSON",
Version: "0.21.4",
Indirect: false,
Locations: []types.Location{{StartLine: 11, EndLine: 15}},
DependsOn: []string{
"4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5",
"69de0a69-1ddd-5017-9359-2bf0b02dc9f0",
"a63ad114-7e13-5084-954f-fe012c677804",
"ade2ca70-3891-5945-98fb-dc099432e06a",
},
},
{
ID: "a63ad114-7e13-5084-954f-fe012c677804",
Name: "Mmap",
Version: "1.9.0",
Indirect: true,
Locations: []types.Location{{StartLine: 17, EndLine: 18}},
},
{
ID: "69de0a69-1ddd-5017-9359-2bf0b02dc9f0",
Name: "Parsers",
Version: "2.5.10",
Indirect: true,
Locations: []types.Location{{StartLine: 20, EndLine: 24}},
DependsOn: []string{
"ade2ca70-3891-5945-98fb-dc099432e06a",
"aea7be01-6a6a-4083-8856-8a6e6704d82a",
"cf7118a7-6976-5b1a-9a39-7adc72f591a4",
},
},
{
ID: "aea7be01-6a6a-4083-8856-8a6e6704d82a",
Name: "PrecompileTools",
Version: "1.1.1",
Indirect: true,
Locations: []types.Location{{StartLine: 26, EndLine: 30}},
DependsOn: []string{"21216c6a-2e73-6563-6e65-726566657250"},
},
{
ID: "21216c6a-2e73-6563-6e65-726566657250",
Name: "Preferences",
Version: "1.4.0",
Indirect: true,
Locations: []types.Location{{StartLine: 32, EndLine: 36}},
DependsOn: []string{"fa267f1f-6049-4f14-aa54-33bafae1ed76"},
},
{
ID: "de0858da-6303-5e67-8744-51eddeeeb8d7",
Name: "Printf",
Version: "1.9.0",
Indirect: true,
Locations: []types.Location{{StartLine: 38, EndLine: 40}},
DependsOn: []string{"4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"},
},
{
ID: "9a3f8284-a2c9-5f02-9a11-845980a1fd5c",
Name: "Random",
Version: "1.9.0",
Indirect: true,
Locations: []types.Location{{StartLine: 42, EndLine: 44}},
DependsOn: []string{"9e88b42a-f829-5b0c-bbe9-9e923198166b", "ea8e919c-243c-51af-8825-aaa63cd721ce"},
},
{
ID: "ea8e919c-243c-51af-8825-aaa63cd721ce",
Name: "SHA",
Version: "0.7.0",
Indirect: true,
Locations: []types.Location{{StartLine: 46, EndLine: 48}},
},
{
ID: "9e88b42a-f829-5b0c-bbe9-9e923198166b",
Name: "Serialization",
Version: "1.9.0",
Indirect: true,
Locations: []types.Location{{StartLine: 50, EndLine: 51}},
},
{
ID: "fa267f1f-6049-4f14-aa54-33bafae1ed76",
Name: "TOML",
Version: "1.0.3",
Indirect: true,
Locations: []types.Location{{StartLine: 53, EndLine: 56}},
DependsOn: []string{"ade2ca70-3891-5945-98fb-dc099432e06a"},
},
{
ID: "cf7118a7-6976-5b1a-9a39-7adc72f591a4",
Name: "UUIDs",
Version: "1.9.0",
Indirect: true,
Locations: []types.Location{{StartLine: 58, EndLine: 60}},
DependsOn: []string{"9a3f8284-a2c9-5f02-9a11-845980a1fd5c", "ea8e919c-243c-51af-8825-aaa63cd721ce"},
},
{
ID: "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5",
Name: "Unicode",
Version: "1.9.0",
Indirect: true,
Locations: []types.Location{{StartLine: 62, EndLine: 63}},
},
},
},
},
},
},
{
name: "no_deps_v1.6",
dir: "testdata/no_deps_v1.6",
want: &analyzer.AnalysisResult{},
},
{
name: "dep_ext_v1.9",
dir: "testdata/dep_ext_v1.9",
want: &analyzer.AnalysisResult{
Applications: []types.Application{
{
Type: types.Julia,
FilePath: "Manifest.toml",
Packages: types.Packages{
{
ID: "621f4979-c628-5d54-868e-fcf4e3e8185c",
Name: "AbstractFFTs",
Version: "1.3.1",
Indirect: false,
Locations: []types.Location{{StartLine: 7, EndLine: 10}},
DependsOn: nil,
},
},
},
},
},
},
{
name: "no_manifest",
dir: "testdata/no_manifest",
want: &analyzer.AnalysisResult{
Applications: nil,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
a, err := newJuliaAnalyzer(analyzer.AnalyzerOptions{})
require.NoError(t, err)
got, err := a.PostAnalyze(context.Background(), analyzer.PostAnalysisInput{
FS: os.DirFS(tt.dir),
})
assert.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

View File

@@ -0,0 +1,16 @@
# This file is machine-generated - editing it directly is not advised
julia_version = "1.9.0"
manifest_format = "2.0"
project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc"
[[deps.AbstractFFTs]]
git-tree-sha1 = "16b6dbc4cf7caee4e1e75c49485ec67b667098a0"
uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c"
version = "1.3.1"
[deps.AbstractFFTs.extensions]
AbstractFFTsChainRulesCoreExt = "ChainRulesCore"
[deps.AbstractFFTs.weakdeps]
ChainRulesCore = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4"

View File

@@ -0,0 +1,9 @@
name = "packageName"
uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773"
version = "0.1.0"
[deps]
AbstractFFTs = "621f4979-c628-5d54-868e-fcf4e3e8185c"
[compat]
AbstractFFTs = "1.3"

View File

@@ -0,0 +1,72 @@
# This file is machine-generated - editing it directly is not advised
julia_version = "1.9.0"
manifest_format = "2.0"
project_hash = "f65b9de676a27ce78ee011db6d477b3d44d1a7c5"
[[deps.Dates]]
deps = ["Printf"]
uuid = "ade2ca70-3891-5945-98fb-dc099432e06a"
[[deps.JSON]]
deps = ["Dates", "Mmap", "Parsers", "Unicode"]
git-tree-sha1 = "31e996f0a15c7b280ba9f76636b3ff9e2ae58c9a"
uuid = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
version = "0.21.4"
[[deps.Mmap]]
uuid = "a63ad114-7e13-5084-954f-fe012c677804"
[[deps.Parsers]]
deps = ["Dates", "PrecompileTools", "UUIDs"]
git-tree-sha1 = "a5aef8d4a6e8d81f171b2bd4be5265b01384c74c"
uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0"
version = "2.5.10"
[[deps.PrecompileTools]]
deps = ["Preferences"]
git-tree-sha1 = "259e206946c293698122f63e2b513a7c99a244e8"
uuid = "aea7be01-6a6a-4083-8856-8a6e6704d82a"
version = "1.1.1"
[[deps.Preferences]]
deps = ["TOML"]
git-tree-sha1 = "7eb1686b4f04b82f96ed7a4ea5890a4f0c7a09f1"
uuid = "21216c6a-2e73-6563-6e65-726566657250"
version = "1.4.0"
[[deps.Printf]]
deps = ["Unicode"]
uuid = "de0858da-6303-5e67-8744-51eddeeeb8d7"
[[deps.Random]]
deps = ["SHA", "Serialization"]
uuid = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
[[deps.SHA]]
uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce"
version = "0.7.0"
[[deps.Serialization]]
uuid = "9e88b42a-f829-5b0c-bbe9-9e923198166b"
[[deps.TOML]]
deps = ["Dates"]
uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76"
version = "1.0.3"
[[deps.UUIDs]]
deps = ["Random", "SHA"]
uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4"
[[deps.Unicode]]
uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5"
[[deps.DevDep]]
deps = ["IndirectDevDep"]
uuid = "d9a60922-03b4-4a1b-81be-b8d05b827236"
version = "1.0.0"
[[deps.IndirectDevDep]]
uuid = "b637660b-5035-4894-8335-b3805a4b50d8"
version = "2.0.0"

View File

@@ -0,0 +1,17 @@
name = "packageName"
uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773"
version = "0.1.0"
[deps]
Dates = "ade2ca70-3891-5945-98fb-dc099432e06a"
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
[compat]
JSON = "0.21"
julia = "1.8"
[extras]
DevDep = "d9a60922-03b4-4a1b-81be-b8d05b827236"
[targets]
test = ["DevDep"]

View File

@@ -0,0 +1,2 @@
# This file is machine-generated - editing it directly is not advised

View File

@@ -0,0 +1,3 @@
name = "packageName"
uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773"
version = "0.1.0"

View File

@@ -0,0 +1,2 @@
[deps]
A = "ead4f63c-334e-11e9-00e6-e7f0a5f21b62"

View File

@@ -0,0 +1,16 @@
# This file is machine-generated - editing it directly is not advised
julia_version = "1.9.0"
manifest_format = "2.0"
project_hash = "f0a796fb78285c02ad123fec6e14c8bac09a2ccc"
[[deps.A]]
uuid = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60"
[deps.A.deps]
B = "f41f7b98-334e-11e9-1257-49272045fb24"
[[deps.B]]
uuid = "f41f7b98-334e-11e9-1257-49272045fb24"
[[deps.B]]
uuid = "edca9bc6-334e-11e9-3554-9595dbb4349c"

View File

@@ -0,0 +1,7 @@
name = "packageName"
uuid = "1c653b0a-0b5a-4cff-b25a-92f0db012773"
version = "0.1.0"
[deps]
A = "ead4f63c-334e-11e9-00e6-e7f0a5f21b60"
B = "edca9bc6-334e-11e9-3554-9595dbb4349c"

View File

@@ -73,6 +73,7 @@ const (
Pub LangType = "pub"
Hex LangType = "hex"
Bitnami LangType = "bitnami"
Julia LangType = "julia"
K8sUpstream LangType = "kubernetes"
EKS LangType = "eks" // Amazon Elastic Kubernetes Service
@@ -143,4 +144,7 @@ const (
CondaEnvYaml = "environment.yaml"
CondaEnvYml = "environment.yml"
JuliaProject = "Project.toml"
JuliaManifest = "Manifest.toml"
)

View File

@@ -109,6 +109,10 @@ func New(t ftypes.TargetType, metadata types.Metadata, pkg ftypes.Package) (*Pac
return nil, nil
}
return (*PackageURL)(purl), nil
case packageurl.TypeJulia:
var qs packageurl.Qualifiers
namespace, name, qs = parseJulia(name, pkg.ID) // for Julia, the ID is set to the package UUID
qualifiers = append(qualifiers, qs...)
}
return (*PackageURL)(packageurl.NewPackageURL(ptype, namespace, name, ver, qualifiers, subpath)), nil
@@ -424,6 +428,18 @@ func parseNpm(pkgName string) (string, string) {
return parsePkgName(name)
}
// ref. https://github.com/package-url/purl-spec/blob/7759d1cf81629267742eeeb0cdfccf5ebd624cc5/PURL-TYPES.rst#julia
func parseJulia(pkgName, pkgUUID string) (string, string, packageurl.Qualifiers) {
namespace, name := parsePkgName(pkgName)
qualifiers := packageurl.Qualifiers{
{
Key: "uuid",
Value: pkgUUID,
},
}
return namespace, name, qualifiers
}
func purlType(t ftypes.TargetType) string {
switch t {
case ftypes.Jar, ftypes.Pom, ftypes.Gradle:
@@ -462,6 +478,8 @@ func purlType(t ftypes.TargetType) string {
return packageurl.TypeRPM
case TypeOCI:
return packageurl.TypeOCI
case ftypes.Julia:
return packageurl.TypeJulia
}
return string(t)
}

View File

@@ -406,6 +406,26 @@ func TestNewPackageURL(t *testing.T) {
},
wantErr: "failed to parse digest",
},
{
name: "julia project",
typ: ftypes.Julia,
pkg: ftypes.Package{
ID: "ade2ca70-3891-5945-98fb-dc099432e06a",
Name: "Dates",
Version: "1.9.0",
},
want: &purl.PackageURL{
Type: packageurl.TypeJulia,
Name: "Dates",
Version: "1.9.0",
Qualifiers: packageurl.Qualifiers{
{
Key: "uuid",
Value: "ade2ca70-3891-5945-98fb-dc099432e06a",
},
},
},
},
}
for _, tc := range testCases {